diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..faf5ab1 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,9 @@ +[run] +branch = true +source = bread +omit = */tests* + +[report] +fail_under = 85 +show_missing = true +skip_covered = true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..175b320 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,28 @@ +# This workflows will upload a Python Package using Twine when a release is created +# For more information see: +# https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..af86c36 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,77 @@ +name: lint-test + +on: + pull_request: + schedule: + # run once a week on early monday mornings + - cron: '22 2 * * 1' + +jobs: + pre-commit: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: pre-commit/action@v2.0.0 + + test: + runs-on: ubuntu-20.04 + strategy: + matrix: + # tox-gh-actions will only run the tox environments which match the currently + # running python-version. See [gh-actions] in tox.ini for the mapping + python-version: [3.6, 3.7, 3.8, 3.9] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r dev-requirements.txt + - name: Test with tox + run: tox + + coverage: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('dev-requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r dev-requirements.txt + - name: Run coverage report + run: | + coverage run runtests.py + coverage report + + build-docs: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('dev-requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r dev-requirements.txt + - name: Build docs + run: sphinx-build docs docs/_build/html diff --git a/.gitignore b/.gitignore index adb799c..5e08435 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist .tox _build *.egg-info/ +.coverage diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7625200 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.3.0 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-json + - id: check-merge-conflict + - id: check-symlinks + - id: check-yaml + - id: debug-statements + - id: detect-private-key + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + + - repo: https://github.com/psf/black + rev: 20.8b1 + hooks: + - id: black + + - repo: https://github.com/pycqa/isort + rev: 5.6.4 + hooks: + - id: isort + + - repo: https://gitlab.com/pycqa/flake8 + rev: master + hooks: + - id: flake8 diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..f811d15 --- /dev/null +++ b/.python-version @@ -0,0 +1,4 @@ +3.9.0 +3.8.6 +3.7.9 +3.6.12 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d806bc4..0000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -sudo: false - -dist: xenial - -language: python - -python: - - "3.7" - - "3.8" - - "3.9" - -install: - - pip install tox-travis - -script: tox diff --git a/README.rst b/README.rst index dfe527b..4c279d0 100644 --- a/README.rst +++ b/README.rst @@ -34,10 +34,34 @@ Python: 3.7, 3.8, 3.9 For Python 2.7 and/or Django 1.11 support, the 0.5 release series is identical (features-wise) to 0.6 and is available on PyPI: https://pypi.org/project/django-bread/#history -Testing -------- -To run the tests, install "tox" ("pip install tox") and just run it: +Maintainer Information +---------------------- - $ tox - ... +We use Github Actions to lint (using pre-commit, black, isort, and flake8), +test (using tox and tox-gh-actions), calculate coverage (using coverage), and build +documentation (using sphinx). + +We have a local script to do these actions locally, named ``maintain.sh``:: + + $ ./maintain.sh + +A Github Action workflow also builds and pushes a new package to PyPI whenever a new +Release is created in Github. This uses a project-specific PyPI token, as described in +the `PyPI documentation here `_. That token has been +saved in the ``PYPI_PASSWORD`` settings for this repo, but has not been saved anywhere +else so if it is needed for any reason, the current one should be deleted and a new one +generated. + +As always, be sure to bump the version in ``bread/__init__.py`` before creating a +Release, so that the proper version gets pushed to PyPI. + + +Questions or Issues? +-------------------- + +If you have questions, issues or requests for improvements please let us know on +`Github `_. + +Development sponsored by `Caktus Consulting Group, LLC +`_. diff --git a/bread/__init__.py b/bread/__init__.py index 1dea037..5c4105c 100644 --- a/bread/__init__.py +++ b/bread/__init__.py @@ -1 +1 @@ -VERSION = '1.0.1' +__version__ = "1.0.1" diff --git a/bread/bread.py b/bread/bread.py index b934f89..dbe7603 100644 --- a/bread/bread.py +++ b/bread/bread.py @@ -1,5 +1,5 @@ -from functools import reduce import json +from functools import reduce from operator import or_ from urllib.parse import urlencode @@ -10,15 +10,18 @@ from django.contrib.auth.views import redirect_to_login from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ( - ImproperlyConfigured, PermissionDenied, FieldError, EmptyResultSet + EmptyResultSet, + FieldError, + ImproperlyConfigured, + PermissionDenied, ) from django.db.models import Model, Q from django.forms.models import modelform_factory from django.http.response import HttpResponseBadRequest -from django.urls import reverse_lazy, path -from vanilla import ListView, DetailView, CreateView, UpdateView, DeleteView +from django.urls import path, reverse_lazy +from vanilla import CreateView, DeleteView, DetailView, ListView, UpdateView -from .utils import validate_fieldspec, get_verbose_name +from .utils import get_verbose_name, validate_fieldspec class Http400(Exception): @@ -38,12 +41,13 @@ def __init__(self, msg): # Helper to get settings from BREAD dictionary, or default def setting(name, default=None): - BREAD = getattr(settings, 'BREAD', {}) + BREAD = getattr(settings, "BREAD", {}) return BREAD.get(name, default) class BreadViewMixin(object): """We mix this into all the views for some common features""" + bread = None # The Bread object using this view exclude = None @@ -66,10 +70,13 @@ def get_full_perm_name(self, short_name): def __init__(self, *args, **kwargs): # Make sure the permission needed to use this view exists. super(BreadViewMixin, self).__init__(*args, **kwargs) - perm_name = '%s_%s' % (self.perm_name, self.bread.model._meta.object_name.lower()) + perm_name = "%s_%s" % ( + self.perm_name, + self.bread.model._meta.object_name.lower(), + ) perm = Permission.objects.filter( content_type=ContentType.objects.get_for_model(self.bread.model), - codename=perm_name + codename=perm_name, ).first() if not perm: raise ImproperlyConfigured( @@ -86,16 +93,17 @@ def __init__(self, *args, **kwargs): def dispatch(self, request, *args, **kwargs): # Make sure that the permission_required attribute is set on the # view, or raise a configuration error. - if self.permission_required is None: # pragma: no cover + if self.permission_required is None: # pragma: no cover raise ImproperlyConfigured( "'BreadViewMixin' requires " - "'permission_required' attribute to be set.") + "'permission_required' attribute to be set." + ) # Check if the user is logged in if not request.user.is_authenticated: - return redirect_to_login(request.get_full_path(), - settings.LOGIN_URL, - REDIRECT_FIELD_NAME) + return redirect_to_login( + request.get_full_path(), settings.LOGIN_URL, REDIRECT_FIELD_NAME + ) # Check to see if the request's user has the required permission. has_permission = request.user.has_perm(self.permission_required) @@ -106,25 +114,25 @@ def dispatch(self, request, *args, **kwargs): try: return super(BreadViewMixin, self).dispatch(request, *args, **kwargs) except Http400 as e: - return HttpResponseBadRequest(content=e.msg.encode('utf-8')) + return HttpResponseBadRequest(content=e.msg.encode("utf-8")) def get_template_names(self): """Return Django Vanilla templates (app-specific), then - Customized template via Bread object, then - Django Bread template + Customized template via Bread object, then + Django Bread template """ vanilla_templates = super(BreadViewMixin, self).get_template_names() # template_name_suffix may have a leading underscore (to make it work well with Django # Vanilla Views). If it does, then we strip the underscore to get our 'view' name. # e.g. template_name_suffix '_browse' -> view 'browse' - view = self.template_name_suffix.lstrip('_') - default_template = 'bread/%s.html' % view + view = self.template_name_suffix.lstrip("_") + default_template = "bread/%s.html" % view if self.bread.template_name_pattern: custom_template = self.bread.template_name_pattern.format( app_label=self.bread.model._meta.app_label, model=self.bread.model._meta.object_name.lower(), - view=view + view=view, ) return vanilla_templates + [custom_template] + [default_template] return vanilla_templates + [default_template] @@ -144,16 +152,21 @@ def get_context_data(self, **kwargs): # Add 'may_' to the context for each view, so the templates can # tell if the current user may use the named view. - data['may_browse'] = 'B' in self.bread.views \ - and self.request.user.has_perm(self.get_full_perm_name('browse')) - data['may_read'] = 'R' in self.bread.views \ - and self.request.user.has_perm(self.get_full_perm_name('read')) - data['may_edit'] = 'E' in self.bread.views \ - and self.request.user.has_perm(self.get_full_perm_name('change')) - data['may_add'] = 'A' in self.bread.views \ - and self.request.user.has_perm(self.get_full_perm_name('add')) - data['may_delete'] = 'D' in self.bread.views \ - and self.request.user.has_perm(self.get_full_perm_name('delete')) + data["may_browse"] = "B" in self.bread.views and self.request.user.has_perm( + self.get_full_perm_name("browse") + ) + data["may_read"] = "R" in self.bread.views and self.request.user.has_perm( + self.get_full_perm_name("read") + ) + data["may_edit"] = "E" in self.bread.views and self.request.user.has_perm( + self.get_full_perm_name("change") + ) + data["may_add"] = "A" in self.bread.views and self.request.user.has_perm( + self.get_full_perm_name("add") + ) + data["may_delete"] = "D" in self.bread.views and self.request.user.has_perm( + self.get_full_perm_name("delete") + ) return data def get_form(self, data=None, files=None, **kwargs): @@ -161,8 +174,8 @@ def get_form(self, data=None, files=None, **kwargs): if not form_class: form_class = modelform_factory( self.bread.model, - fields='__all__', - exclude=self.exclude or self.bread.exclude + fields="__all__", + exclude=self.exclude or self.bread.exclude, ) return form_class(data=data, files=files, **kwargs) @@ -178,10 +191,10 @@ class BrowseView(BreadViewMixin, ListView): columns = [] filterset = None # Class paginate_by = None - perm_name = 'browse' # Not a default Django permission + perm_name = "browse" # Not a default Django permission search_fields = [] search_terms = None - template_name_suffix = '_browse' + template_name_suffix = "_browse" _valid_sorting_columns = [] # indices of columns that are valid in ordering parms @@ -224,39 +237,45 @@ def get_queryset(self): # Now search # QueryDict.pop() always returns a list - q = query_parms.pop('q', [False])[0] + q = query_parms.pop("q", [False])[0] if self.search_fields and q: qset, use_distinct = self.get_search_results(self.request, qset, q) if use_distinct: qset = qset.distinct() # Sort? - o = query_parms.pop('o', [False])[0] + o = query_parms.pop("o", [False])[0] if o: order_by = [] - for o_field in o.split(','): - prefix = '' - if o_field.startswith('-'): - prefix = '-' + for o_field in o.split(","): + prefix = "" + if o_field.startswith("-"): + prefix = "-" o_field = o_field[1:] try: column_number = int(o_field) except ValueError: - raise Http400("%s is not a valid integer in sorting param o=%r" % - (o_field, o)) + raise Http400( + "%s is not a valid integer in sorting param o=%r" % (o_field, o) + ) if column_number not in self._valid_sorting_columns: raise Http400( "%d is not a valid column number to sort on. " "The valid column numbers are %r" - % (column_number, self._valid_sorting_columns)) - order_by.append('%s%s' % - (prefix, self.get_sort_field_name_for_column(column_number))) + % (column_number, self._valid_sorting_columns) + ) + order_by.append( + "%s%s" + % (prefix, self.get_sort_field_name_for_column(column_number)) + ) # Add any ordering from the model's Meta data that isn't already included. # That will make the rest of the sort stable, if the model has some default sort order. - default_ordering = getattr(self, 'default_ordering', qset.model._meta.ordering) - order_by_without_leading_dashes = [x.lstrip('-') for x in order_by] + default_ordering = getattr( + self, "default_ordering", qset.model._meta.ordering + ) + order_by_without_leading_dashes = [x.lstrip("-") for x in order_by] for order_spec in default_ordering: - if order_spec.lstrip('-') not in order_by_without_leading_dashes: + if order_spec.lstrip("-") not in order_by_without_leading_dashes: order_by.append(order_spec) qset = qset.order_by(*order_by) # Validate those parms @@ -264,7 +283,8 @@ def get_queryset(self): str(qset.query) # unused, just evaluate it to make it compile the query except FieldError as e: raise Http400( - "There is an invalid column for sorting in the ordering parameter: %s" % str(e) + "There is an invalid column for sorting in the ordering parameter: %s" + % str(e) ) except EmptyResultSet: # It can throw this but we don't care @@ -279,28 +299,30 @@ def get_queryset(self): def get_context_data(self, **kwargs): data = super(BrowseView, self).get_context_data(**kwargs) - data['o'] = self.request.GET.get('o', '') - data['q'] = self.request.GET.get('q', '') - data['columns'] = self.columns - data['valid_sorting_columns_json'] = json.dumps(self._valid_sorting_columns) - data['has_filter'] = self.filterset is not None - data['has_search'] = bool(self.search_fields) + data["o"] = self.request.GET.get("o", "") + data["q"] = self.request.GET.get("q", "") + data["columns"] = self.columns + data["valid_sorting_columns_json"] = json.dumps(self._valid_sorting_columns) + data["has_filter"] = self.filterset is not None + data["has_search"] = bool(self.search_fields) if self.search_fields and self.search_terms: - data['search_terms'] = self.search_terms + data["search_terms"] = self.search_terms else: - data['search_terms'] = '' - data['filter'] = self.filter - if data.get('is_paginated', False): - page = data['page_obj'] - num_pages = data['paginator'].num_pages + data["search_terms"] = "" + data["filter"] = self.filter + if data.get("is_paginated", False): + page = data["page_obj"] + num_pages = data["paginator"].num_pages if page.has_next(): if page.next_page_number() != num_pages: - data['next_url'] = self._get_new_url(page=page.next_page_number()) - data['last_url'] = self._get_new_url(page=num_pages) + data["next_url"] = self._get_new_url(page=page.next_page_number()) + data["last_url"] = self._get_new_url(page=num_pages) if page.has_previous(): - data['first_url'] = self._get_new_url(page=1) + data["first_url"] = self._get_new_url(page=1) if page.previous_page_number() != 1: - data['previous_url'] = self._get_new_url(page=page.previous_page_number()) + data["previous_url"] = self._get_new_url( + page=page.previous_page_number() + ) return data # The following is copied from the Django admin and tweaked @@ -311,11 +333,11 @@ def get_search_results(self, request, queryset, search_term): """ # Apply keyword searches. def construct_search(field_name): - if field_name.startswith('^'): + if field_name.startswith("^"): return "%s__istartswith" % field_name[1:] - elif field_name.startswith('='): + elif field_name.startswith("="): return "%s__iexact" % field_name[1:] - elif field_name.startswith('@'): + elif field_name.startswith("@"): return "%s__search" % field_name[1:] else: return "%s__icontains" % field_name @@ -323,11 +345,11 @@ def construct_search(field_name): use_distinct = False search_fields = self.search_fields if search_fields and search_term: - orm_lookups = [construct_search(str(search_field)) - for search_field in search_fields] + orm_lookups = [ + construct_search(str(search_field)) for search_field in search_fields + ] for bit in search_term.split(): - or_queries = [Q(**{orm_lookup: bit}) - for orm_lookup in orm_lookups] + or_queries = [Q(**{orm_lookup: bit}) for orm_lookup in orm_lookups] queryset = queryset.filter(reduce(or_, or_queries)) if not use_distinct: opts = self.bread.model._meta @@ -346,12 +368,13 @@ class ReadView(BreadViewMixin, DetailView): we can iterate over in the template to display it if we don't want to make a custom template for this model. """ - perm_name = 'view' # Default Django permission - template_name_suffix = '_read' + + perm_name = "view" # Default Django permission + template_name_suffix = "_read" def get_context_data(self, **kwargs): data = super(ReadView, self).get_context_data(**kwargs) - data['form'] = self.get_form(instance=self.object) + data["form"] = self.get_form(instance=self.object) return data @@ -388,14 +411,17 @@ class LabelValueReadView(ReadView): (_('Answer'), 42), # Mode 5: '42' ) """ - template_name_suffix = '_label_value_read' + + template_name_suffix = "_label_value_read" fields = [] def get_context_data(self, **kwargs): context_data = super(LabelValueReadView, self).get_context_data(**kwargs) - context_data['read_fields'] = [self.get_field_label_value(label, value, context_data) for - label, value in self.fields] + context_data["read_fields"] = [ + self.get_field_label_value(label, value, context_data) + for label, value in self.fields + ] return context_data @@ -404,7 +430,7 @@ def get_field_label_value(self, label, evaluator, context_data): Implements the modes described in the class docstring. (q.v.) """ - value = '' + value = "" if isinstance(evaluator, str): if hasattr(self.object, evaluator): # This is an instance attr or method @@ -428,8 +454,8 @@ def get_field_label_value(self, label, evaluator, context_data): class EditView(BreadViewMixin, UpdateView): - perm_name = 'change' # Default Django permission - template_name_suffix = '_edit' + perm_name = "change" # Default Django permission + template_name_suffix = "_edit" def form_invalid(self, form): # Return a 400 if the form isn't valid @@ -439,8 +465,8 @@ def form_invalid(self, form): class AddView(BreadViewMixin, CreateView): - perm_name = 'add' # Default Django permission - template_name_suffix = '_edit' # Yes 'edit' not 'add' + perm_name = "add" # Default Django permission + template_name_suffix = "_edit" # Yes 'edit' not 'add' def form_invalid(self, form): # Return a 400 if the form isn't valid @@ -450,8 +476,8 @@ def form_invalid(self, form): class DeleteView(BreadViewMixin, DeleteView): - perm_name = 'delete' # Default Django permission - template_name_suffix = '_delete' + perm_name = "delete" # Default Django permission + template_name_suffix = "_delete" class Bread(object): @@ -498,6 +524,7 @@ class Bread(object): `{view}` (`browse`, `read`, etc.). """ + browse_view = BrowseView read_view = ReadView edit_view = EditView @@ -506,8 +533,8 @@ class Bread(object): exclude = [] # Names of fields not to show views = "BREAD" - base_template = setting('DEFAULT_BASE_TEMPLATE', 'base.html') - namespace = '' + base_template = setting("DEFAULT_BASE_TEMPLATE", "base.html") + namespace = "" template_name_pattern = None plural_name = None form_class = None @@ -517,26 +544,34 @@ def __init__(self): self.views = self.views.upper() if not self.plural_name: - self.plural_name = self.name + 's' + self.plural_name = self.name + "s" if not issubclass(self.model, Model): - raise TypeError("'model' argument for Bread must be a " - "subclass of Model; it is %r" % self.model) + raise TypeError( + "'model' argument for Bread must be a " + "subclass of Model; it is %r" % self.model + ) if self.browse_view.columns: for colspec in self.browse_view.columns: column = colspec[1] validate_fieldspec(self.model, column) - if hasattr(self, 'paginate_by') or hasattr(self, 'columns'): - raise ValueError("The 'paginate_by' and 'columns' settings have been moved " - "from the Bread class to the BrowseView class.") - if hasattr(self, 'filter'): - raise ValueError("The 'filter' setting has been renamed to 'filterset' and moved " - "to the BrowseView.") - if hasattr(self, 'filterset'): - raise ValueError("The 'filterset' setting should be on the BrowseView, not " - "the Bread view.") + if hasattr(self, "paginate_by") or hasattr(self, "columns"): + raise ValueError( + "The 'paginate_by' and 'columns' settings have been moved " + "from the Bread class to the BrowseView class." + ) + if hasattr(self, "filter"): + raise ValueError( + "The 'filter' setting has been renamed to 'filterset' and moved " + "to the BrowseView." + ) + if hasattr(self, "filterset"): + raise ValueError( + "The 'filterset' setting should be on the BrowseView, not " + "the Bread view." + ) def get_additional_context_data(self): """ @@ -545,13 +580,13 @@ def get_additional_context_data(self): and include the results. """ data = {} - data['bread'] = self + data["bread"] = self # Provide references to useful Model Meta attributes - data['verbose_name'] = self.model._meta.verbose_name - data['verbose_name_plural'] = self.model._meta.verbose_name_plural + data["verbose_name"] = self.model._meta.verbose_name + data["verbose_name_plural"] = self.model._meta.verbose_name_plural # Template that the default bread templates should extend - data['base_template'] = self.base_template + data["base_template"] = self.base_template return data ##### @@ -559,7 +594,7 @@ def get_additional_context_data(self): ##### def browse_url_name(self, include_namespace=True): """Return the URL name for browsing this model""" - return self.get_url_name('browse', include_namespace) + return self.get_url_name("browse", include_namespace) def get_browse_view(self): """Return a view method for browsing.""" @@ -573,7 +608,7 @@ def get_browse_view(self): # R # ##### def read_url_name(self, include_namespace=True): - return self.get_url_name('read', include_namespace) + return self.get_url_name("read", include_namespace) def get_read_view(self): return self.read_view.as_view( @@ -585,7 +620,7 @@ def get_read_view(self): # E # ##### def edit_url_name(self, include_namespace=True): - return self.get_url_name('edit', include_namespace) + return self.get_url_name("edit", include_namespace) def get_edit_view(self): return self.edit_view.as_view( @@ -597,7 +632,7 @@ def get_edit_view(self): # A # ##### def add_url_name(self, include_namespace=True): - return self.get_url_name('add', include_namespace) + return self.get_url_name("add", include_namespace) def get_add_view(self): return self.add_view.as_view( @@ -609,7 +644,7 @@ def get_add_view(self): # D # ##### def delete_url_name(self, include_namespace=True): - return self.get_url_name('delete', include_namespace) + return self.get_url_name("delete", include_namespace) def get_delete_view(self): return self.delete_view.as_view( @@ -622,13 +657,13 @@ def get_delete_view(self): ########## def get_url_name(self, view_name, include_namespace=True): if include_namespace: - url_namespace = self.namespace + ':' if self.namespace else '' + url_namespace = self.namespace + ":" if self.namespace else "" else: - url_namespace = '' - if view_name == 'browse': - return '%s%s_%s' % (url_namespace, view_name, self.plural_name) + url_namespace = "" + if view_name == "browse": + return "%s%s_%s" % (url_namespace, view_name, self.plural_name) else: - return '%s%s_%s' % (url_namespace, view_name, self.name) + return "%s%s_%s" % (url_namespace, view_name, self.name) def get_urls(self, prefix=True): """ @@ -656,36 +691,51 @@ def get_urls(self, prefix=True): """ - prefix = '%s/' % self.plural_name if prefix else '' + prefix = "%s/" % self.plural_name if prefix else "" urlpatterns = [] - if 'B' in self.views: + if "B" in self.views: urlpatterns.append( - path('%s' % prefix, - self.get_browse_view(), - name=self.browse_url_name(include_namespace=False))) + path( + "%s" % prefix, + self.get_browse_view(), + name=self.browse_url_name(include_namespace=False), + ) + ) - if 'R' in self.views: + if "R" in self.views: urlpatterns.append( - path('%s/' % prefix, - self.get_read_view(), - name=self.read_url_name(include_namespace=False))) + path( + "%s/" % prefix, + self.get_read_view(), + name=self.read_url_name(include_namespace=False), + ) + ) - if 'E' in self.views: + if "E" in self.views: urlpatterns.append( - path('%s/edit/' % prefix, - self.get_edit_view(), - name=self.edit_url_name(include_namespace=False))) + path( + "%s/edit/" % prefix, + self.get_edit_view(), + name=self.edit_url_name(include_namespace=False), + ) + ) - if 'A' in self.views: + if "A" in self.views: urlpatterns.append( - path('%sadd/' % prefix, - self.get_add_view(), - name=self.add_url_name(include_namespace=False))) + path( + "%sadd/" % prefix, + self.get_add_view(), + name=self.add_url_name(include_namespace=False), + ) + ) - if 'D' in self.views: + if "D" in self.views: urlpatterns.append( - path('%s/delete/' % prefix, - self.get_delete_view(), - name=self.delete_url_name(include_namespace=False))) + path( + "%s/delete/" % prefix, + self.get_delete_view(), + name=self.delete_url_name(include_namespace=False), + ) + ) return urlpatterns diff --git a/bread/migrations/0001_initial.py b/bread/migrations/0001_initial.py index 42cb422..9f5d50b 100644 --- a/bread/migrations/0001_initial.py +++ b/bread/migrations/0001_initial.py @@ -1,22 +1,29 @@ # -*- coding: utf-8 -*- -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='BreadTestModel', + name="BreadTestModel", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=10)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=10)), ], options={ - 'ordering': ['name'], - 'permissions': [('browse_breadtestmodel', 'can browse BreadTestModel')], + "ordering": ["name"], + "permissions": [("browse_breadtestmodel", "can browse BreadTestModel")], }, bases=(models.Model,), ), diff --git a/bread/migrations/0002_delete_breadtestmodel.py b/bread/migrations/0002_delete_breadtestmodel.py index 81138e5..32c091c 100644 --- a/bread/migrations/0002_delete_breadtestmodel.py +++ b/bread/migrations/0002_delete_breadtestmodel.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('bread', '0001_initial'), + ("bread", "0001_initial"), ] operations = [ migrations.DeleteModel( - name='BreadTestModel', + name="BreadTestModel", ), ] diff --git a/bread/templates/bread/includes/delete.html b/bread/templates/bread/includes/delete.html index 1558e1a..404218f 100644 --- a/bread/templates/bread/includes/delete.html +++ b/bread/templates/bread/includes/delete.html @@ -8,4 +8,3 @@
{% trans "Back to list" %} - diff --git a/bread/templatetags/bread_tags.py b/bread/templatetags/bread_tags.py index bd7320c..f4f0f86 100644 --- a/bread/templatetags/bread_tags.py +++ b/bread/templatetags/bread_tags.py @@ -5,12 +5,11 @@ from bread.utils import get_model_field - logger = logging.getLogger(__name__) register = template.Library() -@register.filter(name='getter') +@register.filter(name="getter") def getter(value, arg): """ Given an object `value`, return the value of the attribute named `arg`. diff --git a/bread/utils.py b/bread/utils.py index 41a4a5f..907eab2 100644 --- a/bread/utils.py +++ b/bread/utils.py @@ -30,7 +30,7 @@ """ import inspect -from django.core.exceptions import ValidationError, FieldDoesNotExist +from django.core.exceptions import FieldDoesNotExist, ValidationError from django.db.models import Model from django.db.models.fields.related import RelatedField @@ -46,14 +46,16 @@ def get_model_field(model_instance, spec): if model_instance is None: raise ValueError("None passed into get_model_field") if not isinstance(model_instance, Model): - raise ValueError("%r should be an instance of a model but it is a %s" - % (model_instance, type(model_instance))) + raise ValueError( + "%r should be an instance of a model but it is a %s" + % (model_instance, type(model_instance)) + ) - if (spec.startswith('__') and spec.endswith('__')): + if spec.startswith("__") and spec.endswith("__"): # It's a dunder method; don't split it. name_parts = [spec] else: - name_parts = spec.split('__', 1) + name_parts = spec.split("__", 1) value = getattr(model_instance, name_parts[0]) if callable(value): @@ -96,7 +98,7 @@ def has_required_args(func): spec = inspect.getfullargspec(func) num_args = len(spec.args) # If first arg is 'self', we can ignore one arg - if num_args and spec.args[0] == 'self': + if num_args and spec.args[0] == "self": num_args -= 1 # If there are defaults, we can ignore the same number of args if spec.defaults: @@ -116,9 +118,11 @@ def validate_fieldspec(model, spec): Otherwise just returns. """ if not issubclass(model, Model): - raise TypeError("First argument to validate_fieldspec must be a " - "subclass of Model; it is %r" % model) - parts = spec.split('__', 1) + raise TypeError( + "First argument to validate_fieldspec must be a " + "subclass of Model; it is %r" % model + ) + parts = spec.split("__", 1) rest_of_spec = parts[1] if len(parts) > 1 else None # What are the possibilities for what parts[0] is on our model? @@ -136,7 +140,8 @@ def validate_fieldspec(model, spec): # Not a field - is there an attribute of some sort? if not hasattr(model, parts[0]): raise ValidationError( - "There is no field or attribute named '%s' on model '%s'" % (parts[0], model) + "There is no field or attribute named '%s' on model '%s'" + % (parts[0], model) ) if rest_of_spec: raise ValidationError( diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..a239543 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,9 @@ +. # <- install ourselves +coverage +django>=2.2,<3.0 +factory_boy==2.3.1 +flake8 +pre-commit +sphinx +tox +tox-gh-actions diff --git a/docs/conf.py b/docs/conf.py index 0997ce5..a5507c7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,56 +12,53 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys -import os -import shlex -from bread import VERSION +from bread import __version__ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'Django Bread' -copyright = u'2015, Caktus Consulting, LLC' -author = u'Caktus Consulting, LLC' +project = u"Django Bread" +copyright = u"2015, Caktus Consulting, LLC" +author = u"Caktus Consulting, LLC" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = VERSION +version = ".".join(__version__.split(".")[0:2]) # The full version, including alpha/beta/rc tags. -release = VERSION +release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -72,37 +69,37 @@ # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -112,156 +109,155 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'alabaster' +html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -#html_search_language = 'en' +# html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} +# html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'DjangoBreaddoc' +htmlhelp_basename = "DjangoBreaddoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', + # Latex figure (float) alignment + # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'DjangoBread.tex', u'Django Bread Documentation', - u'Caktus Consulting, LLC', 'manual'), + ( + master_doc, + "DjangoBread.tex", + u"Django Bread Documentation", + u"Caktus Consulting, LLC", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'djangobread', u'Django Bread Documentation', - [author], 1) -] +man_pages = [(master_doc, "djangobread", u"Django Bread Documentation", [author], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -270,23 +266,29 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'DjangoBread', u'Django Bread Documentation', - author, 'DjangoBread', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "DjangoBread", + u"Django Bread Documentation", + author, + "DjangoBread", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/': None} +intersphinx_mapping = {"https://docs.python.org/": None} diff --git a/docs/index.rst b/docs/index.rst index bc1c598..0afbd0c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -31,4 +31,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/maintain.sh b/maintain.sh new file mode 100755 index 0000000..6dea121 --- /dev/null +++ b/maintain.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -ex + +pip install -Ur dev-requirements.txt +pre-commit install +pre-commit run -a +tox +coverage run runtests.py && coverage report +sphinx-build docs docs/_build/html diff --git a/runtests.py b/runtests.py index b520190..8a7c084 100644 --- a/runtests.py +++ b/runtests.py @@ -2,33 +2,32 @@ import logging import sys -from django.conf import settings from django import setup +from django.conf import settings from django.test.utils import get_runner - if not settings.configured: settings.configure( DATABASES={ - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", } }, MIDDLEWARE_CLASSES=(), INSTALLED_APPS=( - 'bread', - 'tests', + "bread", + "tests", "django.contrib.auth", "django.contrib.contenttypes", - 'django.contrib.sessions', + "django.contrib.sessions", ), SITE_ID=1, - SECRET_KEY='super-secret', + SECRET_KEY="super-secret", TEMPLATES=[ { - "BACKEND": 'django.template.backends.django.DjangoTemplates', - "DIRS": ['bread/templates'], + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": ["bread/templates"], } ], ) @@ -47,5 +46,5 @@ def runtests(): sys.exit(failures) -if __name__ == '__main__': +if __name__ == "__main__": runtests() diff --git a/setup.cfg b/setup.cfg index 444018b..1e5897f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,10 @@ [flake8] -exclude = migrations,docs/conf.py,.tox -max-line-length = 100 +exclude = .tox +max-line-length = 120 + +[bdist_wheel] +universal = 1 + +[isort] +profile = black +multi_line_output = 3 diff --git a/setup.py b/setup.py index 05167a3..86f7fcf 100644 --- a/setup.py +++ b/setup.py @@ -1,34 +1,37 @@ -from setuptools import setup, find_packages -from bread import VERSION +from setuptools import find_packages, setup + +from bread import __version__ setup( - name='django_bread', - version=VERSION, + name="django_bread", + version=__version__, packages=find_packages(), - url='https://github.com/caktus/django_bread', - license='APL2', - author='Dan Poirier', - author_email='dpoirier@caktusgroup.com', - description='Helper for building BREAD interfaces', + url="https://github.com/caktus/django_bread", + license="APL2", + author="Dan Poirier", + author_email="dpoirier@caktusgroup.com", + description="Helper for building BREAD interfaces", include_package_data=True, install_requires=[ - 'django-filter', - 'django-vanilla-views', + "django-filter", + "django-vanilla-views", ], - long_description=open('README.rst').read(), + long_description=open("README.rst").read(), classifiers=[ - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.0', - 'Framework :: Django :: 3.1', + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Framework :: Django", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Topic :: Software Development :: Libraries :: Python Modules", + "Framework :: Django :: 2.2", + "Framework :: Django :: 3.0", + "Framework :: Django :: 3.1", ], zip_safe=False, # because we're including media that Django needs ) diff --git a/tests/base.py b/tests/base.py index cc3ee3e..0bb2765 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,26 +1,30 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType -from django.test import TestCase, RequestFactory, override_settings +from django.test import RequestFactory, TestCase, override_settings -from bread.bread import Bread, ReadView, BrowseView +from bread.bread import Bread, BrowseView, ReadView -from .models import BreadTestModel from .factories import BreadTestModelFactory +from .models import BreadTestModel # Set urlpatterns for a test by calling .set_urls() urlpatterns = None -@override_settings(ROOT_URLCONF='tests.base', - BREAD={'DEFAULT_BASE_TEMPLATE': 'bread/empty.html', }) +@override_settings( + ROOT_URLCONF="tests.base", + BREAD={ + "DEFAULT_BASE_TEMPLATE": "bread/empty.html", + }, +) class BreadTestCase(TestCase): - url_namespace = '' + url_namespace = "" extra_bread_attributes = {} def setUp(self): - self.username = 'joe' - self.password = 'random' + self.username = "joe" + self.password = "random" User = get_user_model() self.user = User.objects.create_user(username=self.username) self.user.set_password(self.password) @@ -33,28 +37,34 @@ def setUp(self): class ReadClass(ReadView): columns = [ - ('Name', 'name'), - ('Text', 'other__text'), - ('Model1', 'model1',) - ] + ("Name", "name"), + ("Text", "other__text"), + ( + "Model1", + "model1", + ), + ] class BrowseClass(BrowseView): columns = [ - ('Name', 'name'), - ('Text', 'other__text'), - ('Model1', 'model1',) + ("Name", "name"), + ("Text", "other__text"), + ( + "Model1", + "model1", + ), ] class BreadTestClass(Bread): model = self.model - base_template = 'bread/empty.html' + base_template = "bread/empty.html" browse_view = BrowseClass namespace = self.url_namespace - plural_name = 'testmodels' + plural_name = "testmodels" def get_additional_context_data(self): context = super(BreadTestClass, self).get_additional_context_data() - context['bread_test_class'] = True + context["bread_test_class"] = True return context for k, v in self.extra_bread_attributes.items(): @@ -78,7 +88,7 @@ def get_permission(self, short_name): """ return Permission.objects.get_or_create( content_type=ContentType.objects.get_for_model(self.model), - codename='%s_%s' % (short_name, self.model_name) + codename="%s_%s" % (short_name, self.model_name), )[0] def give_permission(self, short_name): diff --git a/tests/factories.py b/tests/factories.py index 924163b..7139562 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -1,7 +1,7 @@ import factory import factory.fuzzy -from .models import BreadTestModel, BreadTestModel2, BreadLabelValueTestModel +from .models import BreadLabelValueTestModel, BreadTestModel, BreadTestModel2 class BreadTestModel2Factory(factory.DjangoModelFactory): diff --git a/tests/models.py b/tests/models.py index f63350f..4b9951e 100644 --- a/tests/models.py +++ b/tests/models.py @@ -14,8 +14,9 @@ class BreadLabelValueTestModel(models.Model): """Model for testing LabelValueReadView, also for GetVerboseNameTest""" + name = models.CharField(max_length=10) - banana = models.IntegerField(verbose_name='a yellow fruit', default=0) + banana = models.IntegerField(verbose_name="a yellow fruit", default=0) def name_reversed(self): return self.name[::-1] @@ -23,14 +24,18 @@ def name_reversed(self): class BreadTestModel2(models.Model): text = models.CharField(max_length=20) - label_model = models.OneToOneField(BreadLabelValueTestModel, null=True, - related_name='model2', - on_delete=models.CASCADE, - ) - model1 = models.OneToOneField('BreadTestModel', null=True, - related_name='model1', - on_delete=models.CASCADE, - ) + label_model = models.OneToOneField( + BreadLabelValueTestModel, + null=True, + related_name="model2", + on_delete=models.CASCADE, + ) + model1 = models.OneToOneField( + "BreadTestModel", + null=True, + related_name="model1", + on_delete=models.CASCADE, + ) def get_text(self): return self.text @@ -40,17 +45,19 @@ class BreadTestModel(models.Model): name = models.CharField(max_length=10) age = models.IntegerField() other = models.ForeignKey( - BreadTestModel2, blank=True, null=True, + BreadTestModel2, + blank=True, + null=True, on_delete=models.CASCADE, ) class Meta: ordering = [ - 'name', - '-age', # If same name, sort oldest first + "name", + "-age", # If same name, sort oldest first ] permissions = [ - ('browse_breadtestmodel', 'can browse BreadTestModel'), + ("browse_breadtestmodel", "can browse BreadTestModel"), ] def __str__(self): diff --git a/tests/test_add.py b/tests/test_add.py index 7b6c9a4..815cd02 100644 --- a/tests/test_add.py +++ b/tests/test_add.py @@ -2,6 +2,7 @@ from django.urls import reverse from bread.bread import AddView, Bread + from .base import BreadTestCase from .models import BreadTestModel @@ -13,46 +14,49 @@ def setUp(self): def test_new_item(self): self.model.objects.all().delete() - url = reverse(self.bread.get_url_name('add')) - request = self.request_factory.post(url, data={'name': 'Fred Jones', 'age': '19'}) + url = reverse(self.bread.get_url_name("add")) + request = self.request_factory.post( + url, data={"name": "Fred Jones", "age": "19"} + ) request.user = self.user - self.give_permission('add') + self.give_permission("add") view = self.bread.get_add_view() rsp = view(request) self.assertEqual(302, rsp.status_code) - self.assertEqual(reverse(self.bread.get_url_name('browse')), rsp['Location']) + self.assertEqual(reverse(self.bread.get_url_name("browse")), rsp["Location"]) item = self.model.objects.get() - self.assertEqual('Fred Jones', item.name) + self.assertEqual("Fred Jones", item.name) def test_fail_validation(self): self.model.objects.all().delete() - url = reverse(self.bread.get_url_name('add')) + url = reverse(self.bread.get_url_name("add")) request = self.request_factory.post( - url, data={'name': 'this name is too much long yeah', 'age': '19'}) + url, data={"name": "this name is too much long yeah", "age": "19"} + ) request.user = self.user - self.give_permission('add') + self.give_permission("add") view = self.bread.get_add_view() rsp = view(request) self.assertEqual(400, rsp.status_code) context = rsp.context_data - self.assertTrue(context['bread_test_class']) - form = context['form'] + self.assertTrue(context["bread_test_class"]) + form = context["form"] errors = form.errors - self.assertIn('name', errors) + self.assertIn("name", errors) def test_get(self): # Get should give you a blank form - url = reverse(self.bread.get_url_name('add')) + url = reverse(self.bread.get_url_name("add")) request = self.request_factory.get(url) request.user = self.user - self.give_permission('add') + self.give_permission("add") view = self.bread.get_add_view() rsp = view(request) self.assertEqual(200, rsp.status_code) - form = rsp.context_data['form'] + form = rsp.context_data["form"] self.assertFalse(form.is_bound) rsp.render() - body = rsp.content.decode('utf-8') + body = rsp.content.decode("utf-8") self.assertIn('method="POST"', body) def test_setting_form_class(self): @@ -68,7 +72,7 @@ class TestAddView(AddView): # bread, use a fake dispatch method that saves 'self' into a # dictionary we can access in the test. def dispatch(self, *args, **kwargs): - glob['view_object'] = self + glob["view_object"] = self class BreadTest(Bread): model = BreadTestModel @@ -78,4 +82,4 @@ class BreadTest(Bread): view_function = bread.get_add_view() # Call the view function to invoke dispatch so we can get to the view itself view_function(None, None, None) - self.assertEqual(DummyForm, glob['view_object'].form_class) + self.assertEqual(DummyForm, glob["view_object"].form_class) diff --git a/tests/test_browse.py b/tests/test_browse.py index ba2de25..cf59dd5 100644 --- a/tests/test_browse.py +++ b/tests/test_browse.py @@ -4,24 +4,25 @@ from django.urls import reverse from bread.bread import BrowseView + from .base import BreadTestCase from .factories import BreadTestModelFactory class BreadBrowseTest(BreadTestCase): - @patch('bread.templatetags.bread_tags.logger') + @patch("bread.templatetags.bread_tags.logger") def test_get(self, mock_logger): self.set_urls(self.bread) items = [BreadTestModelFactory() for __ in range(5)] - self.give_permission('browse') - url = reverse(self.bread.get_url_name('browse')) + self.give_permission("browse") + url = reverse(self.bread.get_url_name("browse")) request = self.request_factory.get(url) request.user = self.user rsp = self.bread.get_browse_view()(request) self.assertEqual(200, rsp.status_code) rsp.render() - self.assertTrue(rsp.context_data['bread_test_class']) - body = rsp.content.decode('utf-8') + self.assertTrue(rsp.context_data["bread_test_class"]) + body = rsp.content.decode("utf-8") for item in items: self.assertIn(item.name, body) # No exceptions logged @@ -30,8 +31,8 @@ def test_get(self, mock_logger): def test_get_empty_list(self): self.set_urls(self.bread) self.model.objects.all().delete() - self.give_permission('browse') - url = reverse(self.bread.get_url_name('browse')) + self.give_permission("browse") + url = reverse(self.bread.get_url_name("browse")) request = self.request_factory.get(url) request.user = self.user rsp = self.bread.get_browse_view()(request) @@ -39,134 +40,134 @@ def test_get_empty_list(self): def test_post(self): self.set_urls(self.bread) - self.give_permission('browse') - url = reverse(self.bread.get_url_name('browse')) + self.give_permission("browse") + url = reverse(self.bread.get_url_name("browse")) request = self.request_factory.post(url) request.user = self.user rsp = self.bread.get_browse_view()(request) self.assertEqual(405, rsp.status_code) - @patch('bread.templatetags.bread_tags.logger') + @patch("bread.templatetags.bread_tags.logger") def test_sort_all_ascending(self, mock_logger): self.set_urls(self.bread) - BreadTestModelFactory(name='999', other__text='012', age=50) - BreadTestModelFactory(name='555', other__text='333', age=60) - BreadTestModelFactory(name='111', other__text='555', age=10) - BreadTestModelFactory(name='111', other__text='555', age=20) - BreadTestModelFactory(name='111', other__text='555', age=5) - self.give_permission('browse') - url = reverse(self.bread.get_url_name('browse')) + '?o=0,1' + BreadTestModelFactory(name="999", other__text="012", age=50) + BreadTestModelFactory(name="555", other__text="333", age=60) + BreadTestModelFactory(name="111", other__text="555", age=10) + BreadTestModelFactory(name="111", other__text="555", age=20) + BreadTestModelFactory(name="111", other__text="555", age=5) + self.give_permission("browse") + url = reverse(self.bread.get_url_name("browse")) + "?o=0,1" request = self.request_factory.get(url) request.user = self.user rsp = self.bread.get_browse_view()(request) self.assertEqual(200, rsp.status_code) rsp.render() - results = rsp.context_data['object_list'] + results = rsp.context_data["object_list"] i = 0 while i < len(results) - 1: sortA = (results[i].name, results[i].other.text) - sortB = (results[i+1].name, results[i+1].other.text) + sortB = (results[i + 1].name, results[i + 1].other.text) self.assertLessEqual(sortA, sortB) if sortA == sortB: # default sort is '-age' - self.assertGreaterEqual(results[i].age, results[i+1].age) + self.assertGreaterEqual(results[i].age, results[i + 1].age) i += 1 # No exceptions logged self.assertFalse(mock_logger.exception.called) - @patch('bread.templatetags.bread_tags.logger') + @patch("bread.templatetags.bread_tags.logger") def test_sort_most_ascending_with_override_default_order(self, mock_logger): self.set_urls(self.bread) - self.bread.browse_view.default_ordering = ['-other__text', 'age'] - BreadTestModelFactory(name='999', other__text='012', age=50) - BreadTestModelFactory(name='555', other__text='333', age=60) - BreadTestModelFactory(name='111', other__text='555', age=10) - BreadTestModelFactory(name='111', other__text='555', age=20) - BreadTestModelFactory(name='111', other__text='555', age=5) - self.give_permission('browse') - url = reverse(self.bread.get_url_name('browse')) + '?o=0,1' + self.bread.browse_view.default_ordering = ["-other__text", "age"] + BreadTestModelFactory(name="999", other__text="012", age=50) + BreadTestModelFactory(name="555", other__text="333", age=60) + BreadTestModelFactory(name="111", other__text="555", age=10) + BreadTestModelFactory(name="111", other__text="555", age=20) + BreadTestModelFactory(name="111", other__text="555", age=5) + self.give_permission("browse") + url = reverse(self.bread.get_url_name("browse")) + "?o=0,1" request = self.request_factory.get(url) request.user = self.user rsp = self.bread.get_browse_view()(request) self.assertEqual(200, rsp.status_code) rsp.render() - results = rsp.context_data['object_list'] + results = rsp.context_data["object_list"] i = 0 while i < len(results) - 1: sortA = (results[i].name, results[i].other.text) - sortB = (results[i+1].name, results[i+1].other.text) + sortB = (results[i + 1].name, results[i + 1].other.text) self.assertLessEqual(sortA, sortB) if sortA == sortB: # default sort is 'age' - self.assertLessEqual(results[i].age, results[i+1].age) + self.assertLessEqual(results[i].age, results[i + 1].age) i += 1 # No exceptions logged self.assertFalse(mock_logger.exception.called) - @patch('bread.templatetags.bread_tags.logger') + @patch("bread.templatetags.bread_tags.logger") def test_sort_all_descending(self, mock_logger): self.set_urls(self.bread) - BreadTestModelFactory(name='999', other__text='012', age=50) - BreadTestModelFactory(name='555', other__text='333', age=60) - BreadTestModelFactory(name='111', other__text='555', age=10) - BreadTestModelFactory(name='111', other__text='555', age=20) - BreadTestModelFactory(name='111', other__text='555', age=5) - self.give_permission('browse') - url = reverse(self.bread.get_url_name('browse')) + '?o=-0,-1' + BreadTestModelFactory(name="999", other__text="012", age=50) + BreadTestModelFactory(name="555", other__text="333", age=60) + BreadTestModelFactory(name="111", other__text="555", age=10) + BreadTestModelFactory(name="111", other__text="555", age=20) + BreadTestModelFactory(name="111", other__text="555", age=5) + self.give_permission("browse") + url = reverse(self.bread.get_url_name("browse")) + "?o=-0,-1" request = self.request_factory.get(url) request.user = self.user rsp = self.bread.get_browse_view()(request) self.assertEqual(200, rsp.status_code) rsp.render() - results = rsp.context_data['object_list'] + results = rsp.context_data["object_list"] i = 0 while i < len(results) - 1: sortA = (results[i].name, results[i].other.text) - sortB = (results[i+1].name, results[i+1].other.text) + sortB = (results[i + 1].name, results[i + 1].other.text) self.assertGreaterEqual(sortA, sortB) if sortA == sortB: # default sort is '-age' - self.assertGreaterEqual(results[i].age, results[i+1].age) + self.assertGreaterEqual(results[i].age, results[i + 1].age) i += 1 # No exceptions logged self.assertFalse(mock_logger.exception.called) def test_sort_first_ascending(self): self.set_urls(self.bread) - BreadTestModelFactory(name='999', other__text='012') - BreadTestModelFactory(name='555', other__text='333') - BreadTestModelFactory(name='111', other__text='555') - self.give_permission('browse') - url = reverse(self.bread.get_url_name('browse')) + '?o=0' + BreadTestModelFactory(name="999", other__text="012") + BreadTestModelFactory(name="555", other__text="333") + BreadTestModelFactory(name="111", other__text="555") + self.give_permission("browse") + url = reverse(self.bread.get_url_name("browse")) + "?o=0" request = self.request_factory.get(url) request.user = self.user rsp = self.bread.get_browse_view()(request) self.assertEqual(200, rsp.status_code) rsp.render() - results = rsp.context_data['object_list'] + results = rsp.context_data["object_list"] i = 0 while i < len(results) - 1: sortA = (results[i].name, results[i].other.text) - sortB = (results[i+1].name, results[i+1].other.text) + sortB = (results[i + 1].name, results[i + 1].other.text) self.assertLessEqual(sortA, sortB) i += 1 def test_sort_first_ascending_second_descending(self): self.set_urls(self.bread) - e = BreadTestModelFactory(name='999', other__text='012') - d = BreadTestModelFactory(name='999', other__text='212') - c = BreadTestModelFactory(name='999', other__text='312') - a = BreadTestModelFactory(name='111', other__text='555') - b = BreadTestModelFactory(name='555', other__text='333') - self.give_permission('browse') - url = reverse(self.bread.get_url_name('browse')) + '?o=0,-1' + e = BreadTestModelFactory(name="999", other__text="012") + d = BreadTestModelFactory(name="999", other__text="212") + c = BreadTestModelFactory(name="999", other__text="312") + a = BreadTestModelFactory(name="111", other__text="555") + b = BreadTestModelFactory(name="555", other__text="333") + self.give_permission("browse") + url = reverse(self.bread.get_url_name("browse")) + "?o=0,-1" request = self.request_factory.get(url) request.user = self.user rsp = self.bread.get_browse_view()(request) self.assertEqual(200, rsp.status_code) rsp.render() - results = rsp.context_data['object_list'] + results = rsp.context_data["object_list"] self.assertEqual(a, results[0]) self.assertEqual(b, results[1]) self.assertEqual(c, results[2]) @@ -175,19 +176,19 @@ def test_sort_first_ascending_second_descending(self): def test_sort_first_descending_second_ascending(self): self.set_urls(self.bread) - a = BreadTestModelFactory(name='999', other__text='012') - b = BreadTestModelFactory(name='999', other__text='212') - c = BreadTestModelFactory(name='999', other__text='312') - e = BreadTestModelFactory(name='111', other__text='555') - d = BreadTestModelFactory(name='555', other__text='333') - self.give_permission('browse') - url = reverse(self.bread.get_url_name('browse')) + '?o=-0,1' + a = BreadTestModelFactory(name="999", other__text="012") + b = BreadTestModelFactory(name="999", other__text="212") + c = BreadTestModelFactory(name="999", other__text="312") + e = BreadTestModelFactory(name="111", other__text="555") + d = BreadTestModelFactory(name="555", other__text="333") + self.give_permission("browse") + url = reverse(self.bread.get_url_name("browse")) + "?o=-0,1" request = self.request_factory.get(url) request.user = self.user rsp = self.bread.get_browse_view()(request) self.assertEqual(200, rsp.status_code) rsp.render() - results = rsp.context_data['object_list'] + results = rsp.context_data["object_list"] self.assertEqual(a, results[0]) self.assertEqual(b, results[1]) self.assertEqual(c, results[2]) @@ -196,41 +197,41 @@ def test_sort_first_descending_second_ascending(self): def test_sort_second_field_ascending(self): self.set_urls(self.bread) - d = BreadTestModelFactory(name='555', other__text='333') - a = BreadTestModelFactory(name='999', other__text='012') - c = BreadTestModelFactory(name='999', other__text='312') - b = BreadTestModelFactory(name='999', other__text='212') - e = BreadTestModelFactory(name='111', other__text='555') - self.give_permission('browse') - url = reverse(self.bread.get_url_name('browse')) + '?o=1' + d = BreadTestModelFactory(name="555", other__text="333") + a = BreadTestModelFactory(name="999", other__text="012") + c = BreadTestModelFactory(name="999", other__text="312") + b = BreadTestModelFactory(name="999", other__text="212") + e = BreadTestModelFactory(name="111", other__text="555") + self.give_permission("browse") + url = reverse(self.bread.get_url_name("browse")) + "?o=1" request = self.request_factory.get(url) request.user = self.user rsp = self.bread.get_browse_view()(request) self.assertEqual(200, rsp.status_code) rsp.render() - results = rsp.context_data['object_list'] + results = rsp.context_data["object_list"] self.assertEqual(a, results[0]) self.assertEqual(b, results[1]) self.assertEqual(c, results[2]) self.assertEqual(d, results[3]) self.assertEqual(e, results[4]) - @patch('bread.templatetags.bread_tags.logger') + @patch("bread.templatetags.bread_tags.logger") def test_sort_second_field_ascending_first_descending(self, mock_logger): self.set_urls(self.bread) - d = BreadTestModelFactory(name='1', other__text='111') - a = BreadTestModelFactory(name='999', other__text='000') - e = BreadTestModelFactory(name='111', other__text='555') - b = BreadTestModelFactory(name='3', other__text='111') - c = BreadTestModelFactory(name='2', other__text='111') - self.give_permission('browse') - url = reverse(self.bread.get_url_name('browse')) + '?o=1,-0' + d = BreadTestModelFactory(name="1", other__text="111") + a = BreadTestModelFactory(name="999", other__text="000") + e = BreadTestModelFactory(name="111", other__text="555") + b = BreadTestModelFactory(name="3", other__text="111") + c = BreadTestModelFactory(name="2", other__text="111") + self.give_permission("browse") + url = reverse(self.bread.get_url_name("browse")) + "?o=1,-0" request = self.request_factory.get(url) request.user = self.user rsp = self.bread.get_browse_view()(request) self.assertEqual(200, rsp.status_code) rsp.render() - results = rsp.context_data['object_list'] + results = rsp.context_data["object_list"] self.assertEqual(a, results[0]) self.assertEqual(b, results[1]) self.assertEqual(c, results[2]) @@ -242,19 +243,17 @@ def test_sort_second_field_ascending_first_descending(self, mock_logger): class BadSortTest(BreadTestCase): class BrowseClass(BrowseView): - columns = [ - ('Name', 'name'), - ('Text', 'other__get_text') - ] + columns = [("Name", "name"), ("Text", "other__get_text")] + extra_bread_attributes = { - 'browse_view': BrowseClass, + "browse_view": BrowseClass, } def test_unorderable_column(self): self.set_urls(self.bread) - BreadTestModelFactory(name='1', other__text='111') - self.give_permission('browse') - url = reverse(self.bread.get_url_name('browse')) + '?o=1,-0' + BreadTestModelFactory(name="1", other__text="111") + self.give_permission("browse") + url = reverse(self.bread.get_url_name("browse")) + "?o=1,-0" request = self.request_factory.get(url) request.user = self.user rsp = self.bread.get_browse_view()(request) @@ -264,42 +263,46 @@ def test_unorderable_column(self): class NotDisablingSortTest(BreadTestCase): class BrowseClass(BrowseView): columns = [ - ('Name', 'name'), + ("Name", "name"), ] + extra_bread_attributes = { - 'browse_view': BrowseClass, + "browse_view": BrowseClass, } def test_sorting_on_column(self): # 'name' is a valid column to sort on # (we test this because otherwise the DisableSortTest test isn't valid) self.set_urls(self.bread) - self.give_permission('browse') - url = reverse(self.bread.get_url_name('browse')) + self.give_permission("browse") + url = reverse(self.bread.get_url_name("browse")) request = self.request_factory.get(url) request.user = self.user rsp = self.bread.get_browse_view()(request) self.assertEqual(200, rsp.status_code) rsp.render() - self.assertEqual([0], json.loads(rsp.context_data['valid_sorting_columns_json'])) + self.assertEqual( + [0], json.loads(rsp.context_data["valid_sorting_columns_json"]) + ) class DisableSortTest(BreadTestCase): class BrowseClass(BrowseView): columns = [ - ('Name', 'name', False), + ("Name", "name", False), ] + extra_bread_attributes = { - 'browse_view': BrowseClass, + "browse_view": BrowseClass, } def test_not_sorting_on_column(self): self.set_urls(self.bread) - self.give_permission('browse') - url = reverse(self.bread.get_url_name('browse')) + self.give_permission("browse") + url = reverse(self.bread.get_url_name("browse")) request = self.request_factory.get(url) request.user = self.user rsp = self.bread.get_browse_view()(request) self.assertEqual(200, rsp.status_code) rsp.render() - self.assertEqual([], json.loads(rsp.context_data['valid_sorting_columns_json'])) + self.assertEqual([], json.loads(rsp.context_data["valid_sorting_columns_json"])) diff --git a/tests/test_delete.py b/tests/test_delete.py index 688275a..ddcdea4 100644 --- a/tests/test_delete.py +++ b/tests/test_delete.py @@ -1,5 +1,5 @@ -from django.urls import reverse from django.http import Http404 +from django.urls import reverse from .base import BreadTestCase @@ -11,15 +11,15 @@ def setUp(self): def test_delete_item(self): self.item = self.model_factory() - url = reverse(self.bread.get_url_name('delete'), kwargs={'pk': self.item.pk}) - self.give_permission('delete') + url = reverse(self.bread.get_url_name("delete"), kwargs={"pk": self.item.pk}) + self.give_permission("delete") # Get should work and give us a confirmation page request = self.request_factory.get(url) request.user = self.user view = self.bread.get_delete_view() rsp = view(request, pk=self.item.pk) - self.assertTrue(rsp.context_data['bread_test_class']) + self.assertTrue(rsp.context_data["bread_test_class"]) self.assertEqual(200, rsp.status_code) self.assertTrue(self.model.objects.filter(pk=self.item.pk).exists()) @@ -29,12 +29,12 @@ def test_delete_item(self): view = self.bread.get_delete_view() rsp = view(request, pk=self.item.pk) self.assertEqual(302, rsp.status_code) - self.assertEqual(reverse(self.bread.get_url_name('browse')), rsp['Location']) + self.assertEqual(reverse(self.bread.get_url_name("browse")), rsp["Location"]) self.assertFalse(self.model.objects.filter(pk=self.item.pk).exists()) def test_delete_nonexistent_item(self): - url = reverse(self.bread.get_url_name('delete'), kwargs={'pk': 999}) - self.give_permission('delete') + url = reverse(self.bread.get_url_name("delete"), kwargs={"pk": 999}) + self.give_permission("delete") # Get should not work - 404 request = self.request_factory.get(url) diff --git a/tests/test_edit.py b/tests/test_edit.py index 4c2ca3f..56db987 100644 --- a/tests/test_edit.py +++ b/tests/test_edit.py @@ -1,7 +1,8 @@ -from django.urls import reverse from django import forms +from django.urls import reverse + +from bread.bread import Bread, EditView -from bread.bread import EditView, Bread from .base import BreadTestCase from .models import BreadTestModel @@ -13,49 +14,52 @@ def setUp(self): def test_edit_item(self): item = self.model_factory() - url = reverse(self.bread.get_url_name('edit'), kwargs={'pk': item.pk}) - request = self.request_factory.post(url, data={'name': 'Fred Jones', 'age': '19'}) + url = reverse(self.bread.get_url_name("edit"), kwargs={"pk": item.pk}) + request = self.request_factory.post( + url, data={"name": "Fred Jones", "age": "19"} + ) request.user = self.user - self.give_permission('change') + self.give_permission("change") view = self.bread.get_edit_view() rsp = view(request, pk=item.pk) self.assertEqual(302, rsp.status_code) - self.assertEqual(reverse(self.bread.get_url_name('browse')), rsp['Location']) + self.assertEqual(reverse(self.bread.get_url_name("browse")), rsp["Location"]) item = self.model.objects.get(pk=item.pk) - self.assertEqual('Fred Jones', item.name) + self.assertEqual("Fred Jones", item.name) def test_fail_validation(self): item = self.model_factory() - url = reverse(self.bread.get_url_name('edit'), kwargs={'pk': item.pk}) + url = reverse(self.bread.get_url_name("edit"), kwargs={"pk": item.pk}) request = self.request_factory.post( - url, data={'name': 'this name is too much long yeah', 'age': '19'}) + url, data={"name": "this name is too much long yeah", "age": "19"} + ) request.user = self.user - self.give_permission('change') + self.give_permission("change") view = self.bread.get_edit_view() rsp = view(request, pk=item.pk) self.assertEqual(400, rsp.status_code) - self.assertTrue(rsp.context_data['bread_test_class']) + self.assertTrue(rsp.context_data["bread_test_class"]) context = rsp.context_data - form = context['form'] + form = context["form"] errors = form.errors - self.assertIn('name', errors) + self.assertIn("name", errors) def test_get(self): # Get should give you a form with the item filled in item = self.model_factory() - url = reverse(self.bread.get_url_name('edit'), kwargs={'pk': item.pk}) + url = reverse(self.bread.get_url_name("edit"), kwargs={"pk": item.pk}) request = self.request_factory.get(url) request.user = self.user - self.give_permission('change') + self.give_permission("change") view = self.bread.get_edit_view() rsp = view(request, pk=item.pk) self.assertEqual(200, rsp.status_code) - form = rsp.context_data['form'] + form = rsp.context_data["form"] self.assertFalse(form.is_bound) - self.assertEqual(item.pk, form.initial['id']) - self.assertEqual(item.name, form.initial['name']) + self.assertEqual(item.pk, form.initial["id"]) + self.assertEqual(item.name, form.initial["name"]) rsp.render() - body = rsp.content.decode('utf-8') + body = rsp.content.decode("utf-8") self.assertIn('method="POST"', body) def test_setting_form_class(self): @@ -71,7 +75,7 @@ class TestView(EditView): # bread, use a fake dispatch method that saves 'self' into a # dictionary we can access in the test. def dispatch(self, *args, **kwargs): - glob['view_object'] = self + glob["view_object"] = self class BreadTest(Bread): model = BreadTestModel @@ -81,4 +85,4 @@ class BreadTest(Bread): view_function = bread.get_edit_view() # Call the view function to invoke dispatch so we can get to the view itself view_function(None, None, None) - self.assertEqual(DummyForm, glob['view_object'].form_class) + self.assertEqual(DummyForm, glob["view_object"].form_class) diff --git a/tests/test_forms.py b/tests/test_forms.py index e1f85c0..ba20599 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -2,8 +2,8 @@ from django.core.exceptions import ValidationError from django.urls import reverse -from .models import BreadTestModel from .base import BreadTestCase +from .models import BreadTestModel class TestForm(forms.ModelForm): @@ -11,18 +11,18 @@ class TestForm(forms.ModelForm): # It only allows names that start with 'Dan' class Meta: model = BreadTestModel - fields = ['name', 'age'] + fields = ["name", "age"] def clean_name(self): - name = self.cleaned_data['name'] - if not name.startswith('Dan'): + name = self.cleaned_data["name"] + if not name.startswith("Dan"): raise ValidationError("All good names start with Dan") return name class BreadFormAddTest(BreadTestCase): extra_bread_attributes = { - 'form_class': TestForm, + "form_class": TestForm, } def setUp(self): @@ -31,50 +31,52 @@ def setUp(self): def test_new_item(self): self.model.objects.all().delete() - url = reverse(self.bread.get_url_name('add')) - request = self.request_factory.post(url, data={'name': 'Dan Jones', 'age': '19'}) + url = reverse(self.bread.get_url_name("add")) + request = self.request_factory.post( + url, data={"name": "Dan Jones", "age": "19"} + ) request.user = self.user - self.give_permission('add') + self.give_permission("add") view = self.bread.get_add_view() rsp = view(request) self.assertEqual(302, rsp.status_code) - self.assertEqual(reverse(self.bread.get_url_name('browse')), rsp['Location']) + self.assertEqual(reverse(self.bread.get_url_name("browse")), rsp["Location"]) item = self.model.objects.get() - self.assertEqual('Dan Jones', item.name) + self.assertEqual("Dan Jones", item.name) def test_fail_validation(self): self.model.objects.all().delete() - url = reverse(self.bread.get_url_name('add')) - request = self.request_factory.post(url, data={'name': 'Fred', 'age': '19'}) + url = reverse(self.bread.get_url_name("add")) + request = self.request_factory.post(url, data={"name": "Fred", "age": "19"}) request.user = self.user - self.give_permission('add') + self.give_permission("add") view = self.bread.get_add_view() rsp = view(request) self.assertEqual(400, rsp.status_code) context = rsp.context_data - form = context['form'] + form = context["form"] errors = form.errors - self.assertIn('name', errors) + self.assertIn("name", errors) def test_get(self): # Get should give you a blank form - url = reverse(self.bread.get_url_name('add')) + url = reverse(self.bread.get_url_name("add")) request = self.request_factory.get(url) request.user = self.user - self.give_permission('add') + self.give_permission("add") view = self.bread.get_add_view() rsp = view(request) self.assertEqual(200, rsp.status_code) - form = rsp.context_data['form'] + form = rsp.context_data["form"] self.assertFalse(form.is_bound) rsp.render() - body = rsp.content.decode('utf-8') + body = rsp.content.decode("utf-8") self.assertIn('method="POST"', body) class BreadFormEditTest(BreadTestCase): extra_bread_attributes = { - 'form_class': TestForm, + "form_class": TestForm, } def setUp(self): @@ -83,54 +85,54 @@ def setUp(self): def test_edit_item(self): item = self.model_factory() - url = reverse(self.bread.get_url_name('edit'), kwargs={'pk': item.pk}) - request = self.request_factory.post(url, data={'name': 'Dan Jones', 'age': '19'}) + url = reverse(self.bread.get_url_name("edit"), kwargs={"pk": item.pk}) + request = self.request_factory.post( + url, data={"name": "Dan Jones", "age": "19"} + ) request.user = self.user - self.give_permission('change') + self.give_permission("change") view = self.bread.get_edit_view() rsp = view(request, pk=item.pk) self.assertEqual(302, rsp.status_code) - self.assertEqual(reverse(self.bread.get_url_name('browse')), rsp['Location']) + self.assertEqual(reverse(self.bread.get_url_name("browse")), rsp["Location"]) item = self.model.objects.get(pk=item.pk) - self.assertEqual('Dan Jones', item.name) + self.assertEqual("Dan Jones", item.name) def test_fail_validation(self): item = self.model_factory() - url = reverse(self.bread.get_url_name('edit'), kwargs={'pk': item.pk}) - request = self.request_factory.post(url, data={'name': 'Fred', 'age': '19'}) + url = reverse(self.bread.get_url_name("edit"), kwargs={"pk": item.pk}) + request = self.request_factory.post(url, data={"name": "Fred", "age": "19"}) request.user = self.user - self.give_permission('change') + self.give_permission("change") view = self.bread.get_edit_view() rsp = view(request, pk=item.pk) self.assertEqual(400, rsp.status_code) context = rsp.context_data - form = context['form'] + form = context["form"] errors = form.errors - self.assertIn('name', errors) + self.assertIn("name", errors) def test_get(self): # Get should give you a form with the item filled in item = self.model_factory() - url = reverse(self.bread.get_url_name('edit'), kwargs={'pk': item.pk}) + url = reverse(self.bread.get_url_name("edit"), kwargs={"pk": item.pk}) request = self.request_factory.get(url) request.user = self.user - self.give_permission('change') + self.give_permission("change") view = self.bread.get_edit_view() rsp = view(request, pk=item.pk) self.assertEqual(200, rsp.status_code) - form = rsp.context_data['form'] + form = rsp.context_data["form"] self.assertFalse(form.is_bound) - self.assertEqual(item.name, form.initial['name']) + self.assertEqual(item.name, form.initial["name"]) rsp.render() - body = rsp.content.decode('utf-8') + body = rsp.content.decode("utf-8") self.assertIn('method="POST"', body) class BreadExcludeTest(BreadTestCase): # We can exclude a field from the default form - extra_bread_attributes = { - 'exclude': ['id'] - } + extra_bread_attributes = {"exclude": ["id"]} def setUp(self): super(BreadExcludeTest, self).setUp() @@ -139,14 +141,14 @@ def setUp(self): def test_get(self): # Get should give you a form with the item filled in item = self.model_factory() - url = reverse(self.bread.get_url_name('edit'), kwargs={'pk': item.pk}) + url = reverse(self.bread.get_url_name("edit"), kwargs={"pk": item.pk}) request = self.request_factory.get(url) request.user = self.user - self.give_permission('change') + self.give_permission("change") view = self.bread.get_edit_view() rsp = view(request, pk=item.pk) self.assertEqual(200, rsp.status_code) - form = rsp.context_data['form'] + form = rsp.context_data["form"] self.assertFalse(form.is_bound) - self.assertNotIn('id', form.initial) - self.assertEqual(item.name, form.initial['name']) + self.assertNotIn("id", form.initial) + self.assertEqual(item.name, form.initial["name"]) diff --git a/tests/test_pagination.py b/tests/test_pagination.py index d08fa7b..b69c674 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -1,62 +1,59 @@ -from django.urls import reverse from django.http import Http404 +from django.urls import reverse from bread.bread import BrowseView -from .base import BreadTestCase +from .base import BreadTestCase PAGE_SIZE = 5 class BreadPaginationTest(BreadTestCase): - class BrowseTestView(BrowseView): paginate_by = PAGE_SIZE - extra_bread_attributes = { - 'browse_view': BrowseTestView - } + extra_bread_attributes = {"browse_view": BrowseTestView} def setUp(self): super(BreadPaginationTest, self).setUp() [self.model_factory() for __ in range(2 * PAGE_SIZE + 1)] self.set_urls(self.bread) - self.give_permission('browse') + self.give_permission("browse") def test_get(self): - url = reverse(self.bread.get_url_name('browse')) + url = reverse(self.bread.get_url_name("browse")) request = self.request_factory.get(url) request.user = self.user rsp = self.bread.get_browse_view()(request) self.assertEqual(200, rsp.status_code) rsp.render() context = rsp.context_data - object_list = context['object_list'] + object_list = context["object_list"] self.assertEqual(PAGE_SIZE, len(object_list)) - paginator = context['paginator'] + paginator = context["paginator"] self.assertEqual(3, paginator.num_pages) # Should start with first item ordered_items = self.model.objects.all() self.assertEqual(object_list[0], ordered_items[0]) def test_get_second_page(self): - url = reverse(self.bread.get_url_name('browse')) + "?page=2" + url = reverse(self.bread.get_url_name("browse")) + "?page=2" request = self.request_factory.get(url) request.user = self.user rsp = self.bread.get_browse_view()(request) self.assertEqual(200, rsp.status_code) rsp.render() context = rsp.context_data - object_list = context['object_list'] + object_list = context["object_list"] self.assertEqual(PAGE_SIZE, len(object_list)) - paginator = context['paginator'] + paginator = context["paginator"] self.assertEqual(3, paginator.num_pages) # Should start with item with index page_size ordered_items = self.model.objects.all() self.assertEqual(object_list[0], ordered_items[PAGE_SIZE]) def test_get_page_past_the_end(self): - url = reverse(self.bread.get_url_name('browse')) + "?page=99" + url = reverse(self.bread.get_url_name("browse")) + "?page=99" request = self.request_factory.get(url) request.user = self.user with self.assertRaises(Http404): @@ -65,21 +62,21 @@ def test_get_page_past_the_end(self): def test_get_empty_list(self): self.set_urls(self.bread) self.model.objects.all().delete() - self.give_permission('browse') - url = reverse(self.bread.get_url_name('browse')) + self.give_permission("browse") + url = reverse(self.bread.get_url_name("browse")) request = self.request_factory.get(url) request.user = self.user rsp = self.bread.get_browse_view()(request) self.assertEqual(200, rsp.status_code) context = rsp.context_data - paginator = context['paginator'] + paginator = context["paginator"] self.assertEqual(1, paginator.num_pages) - self.assertEqual(0, len(context['object_list'])) + self.assertEqual(0, len(context["object_list"])) def test_post(self): self.set_urls(self.bread) - self.give_permission('browse') - url = reverse(self.bread.get_url_name('browse')) + self.give_permission("browse") + url = reverse(self.bread.get_url_name("browse")) request = self.request_factory.post(url) request.user = self.user rsp = self.bread.get_browse_view()(request) @@ -88,8 +85,8 @@ def test_post(self): def test_next_url(self): # Make sure next_url includes other query params unaltered self.set_urls(self.bread) - self.give_permission('browse') - base_url = reverse(self.bread.get_url_name('browse')) + self.give_permission("browse") + base_url = reverse(self.bread.get_url_name("browse")) # Add a query parm that needs to be preserved by the next page link url = base_url + "?test=1" request = self.request_factory.get(url) @@ -97,7 +94,7 @@ def test_next_url(self): rsp = self.bread.get_browse_view()(request) self.assertEqual(200, rsp.status_code) context = rsp.context_data - next_url = context['next_url'] + next_url = context["next_url"] # We don't know what order the query parms will end up in expected_urls = [base_url + "?test=1&page=2", base_url + "?page=2&test=1"] self.assertIn(next_url, expected_urls) diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 1032fe4..088ccfc 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -2,8 +2,8 @@ from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.models import AnonymousUser from django.core.exceptions import PermissionDenied -from django.urls import reverse from django.test import override_settings +from django.urls import reverse from .base import BreadTestCase @@ -21,33 +21,36 @@ def setUp(self): self.item = self.model_factory() url_name = self.bread.get_url_name(self.view_name) if self.expects_pk: - url = reverse(url_name, kwargs={'pk': self.item.pk}) + url = reverse(url_name, kwargs={"pk": self.item.pk}) else: url = reverse(url_name) self.request = self.request_factory.get(url) self.request.user = self.user - self.post_request = self.request_factory.post(url, {'name': 'foo'}) + self.post_request = self.request_factory.post(url, {"name": "foo"}) self.post_request.user = self.user - view_getter = getattr(self.bread, 'get_%s_view' % self.view_name) + view_getter = getattr(self.bread, "get_%s_view" % self.view_name) self.view = view_getter() - @override_settings(LOGIN_URL='/logmein') + @override_settings(LOGIN_URL="/logmein") def test_when_not_logged_in(self): # Can't do it if not logged in # but we get redirected rather than PermissionDenied self.request.user = AnonymousUser() rsp = self.view(self.request, pk=self.item.pk) - expected_url = "%s?%s=%s" % \ - (settings.LOGIN_URL, REDIRECT_FIELD_NAME, self.request.path) - self.assertEqual(expected_url, rsp['Location']) + expected_url = "%s?%s=%s" % ( + settings.LOGIN_URL, + REDIRECT_FIELD_NAME, + self.request.path, + ) + self.assertEqual(expected_url, rsp["Location"]) if self.include_post: self.post_request.user = AnonymousUser() rsp = self.view(self.post_request, pk=self.item.pk) self.assertEqual(302, rsp.status_code) - self.assertEqual(expected_url, rsp['Location']) + self.assertEqual(expected_url, rsp["Location"]) def test_access_without_permission(self): # Can't do it when logged in but no permission, get 403 @@ -65,28 +68,28 @@ def test_access_without_permission(self): class BreadBrowsePermissionTest(BreadPermissionTestMixin, BreadTestCase): - view_name = 'browse' + view_name = "browse" expects_pk = False class BreadReadPermissionTest(BreadPermissionTestMixin, BreadTestCase): - view_name = 'read' + view_name = "read" expects_pk = True class BreadEditPermissionTest(BreadPermissionTestMixin, BreadTestCase): - view_name = 'edit' + view_name = "edit" expects_pk = True include_post = True class BreadAddPermissionTest(BreadPermissionTestMixin, BreadTestCase): - view_name = 'add' + view_name = "add" expects_pk = False include_post = True class BreadDeletePermissionTest(BreadPermissionTestMixin, BreadTestCase): - view_name = 'read' + view_name = "read" expects_pk = True include_post = True diff --git a/tests/test_read.py b/tests/test_read.py index 810305b..bc5e186 100644 --- a/tests/test_read.py +++ b/tests/test_read.py @@ -1,23 +1,24 @@ -from django.urls import reverse from django import forms from django.http import Http404 +from django.urls import reverse + +from bread.bread import Bread, LabelValueReadView, ReadView -from bread.bread import LabelValueReadView, Bread, ReadView from .base import BreadTestCase -from .models import BreadLabelValueTestModel, BreadTestModel from .factories import BreadLabelValueTestModelFactory +from .models import BreadLabelValueTestModel, BreadTestModel class BreadReadTest(BreadTestCase): def setUp(self): super(BreadReadTest, self).setUp() - self.urlconf = 'bread.tests.test_read' - self.give_permission('view') + self.urlconf = "bread.tests.test_read" + self.give_permission("view") self.set_urls(self.bread) def test_read(self): item = self.model_factory() - url = reverse('read_%s' % self.model_name, kwargs={'pk': item.pk}) + url = reverse("read_%s" % self.model_name, kwargs={"pk": item.pk}) request = self.request_factory.get(url) request.user = self.user @@ -26,13 +27,13 @@ def test_read(self): self.assertEqual(200, rsp.status_code) rsp.render() - self.assertTrue(rsp.context_data['bread_test_class']) - body = rsp.content.decode('utf-8') + self.assertTrue(rsp.context_data["bread_test_class"]) + body = rsp.content.decode("utf-8") self.assertIn(item.name, body) def test_read_no_such_item(self): self.assertFalse(self.model.objects.filter(pk=999).exists()) - url = reverse('read_%s' % self.model_name, kwargs={'pk': 999}) + url = reverse("read_%s" % self.model_name, kwargs={"pk": 999}) request = self.request_factory.get(url) request.user = self.user @@ -42,9 +43,9 @@ def test_read_no_such_item(self): def test_post(self): self.set_urls(self.bread) - self.give_permission('view') + self.give_permission("view") item = self.model_factory() - url = reverse(self.bread.get_url_name('read'), kwargs={'pk': item.pk}) + url = reverse(self.bread.get_url_name("read"), kwargs={"pk": item.pk}) request = self.request_factory.post(url) request.user = self.user rsp = self.bread.get_read_view()(request) @@ -53,25 +54,30 @@ def test_post(self): class BreadLabelValueReadTest(BreadTestCase): """Exercise LabelValueReadView, particularly the 5 modes described in get_field_label_value()""" + def setUp(self): super(BreadLabelValueReadTest, self).setUp() class ReadClass(LabelValueReadView): """See LabelValueReadView.get_field_label_value() for descriptions of the modes""" + fields = [ - (None, 'id'), # Mode 1, also test of None for label. - (None, 'banana'), # Same, also test field w/explicit verbose_name - ('eman', 'name_reversed'), # Mode 2 - ('Foo', 'bar'), # Mode 3 + (None, "id"), # Mode 1, also test of None for label. + (None, "banana"), # Same, also test field w/explicit verbose_name + ("eman", "name_reversed"), # Mode 2 + ("Foo", "bar"), # Mode 3 # Mode 4 below - ('context first key', lambda context_data: sorted(context_data.keys())[0]), - ('Answer', 42), # Mode 5 - ('Model2', 'model2'), # Back through related name for one2one field + ( + "context first key", + lambda context_data: sorted(context_data.keys())[0], + ), + ("Answer", 42), # Mode 5 + ("Model2", "model2"), # Back through related name for one2one field ] class BreadTestClass(Bread): model = BreadLabelValueTestModel - base_template = 'bread/empty.html' + base_template = "bread/empty.html" read_view = ReadClass self.bread = BreadTestClass() @@ -80,14 +86,14 @@ class BreadTestClass(Bread): self.model_name = self.model._meta.model_name self.model_factory = BreadLabelValueTestModelFactory - self.urlconf = 'bread.tests.test_read' - self.give_permission('view') + self.urlconf = "bread.tests.test_read" + self.give_permission("view") self.set_urls(self.bread) def test_read(self): - item = BreadLabelValueTestModel(name='abcde') + item = BreadLabelValueTestModel(name="abcde") item.save() - url = reverse('read_%s' % self.model_name, kwargs={'pk': item.pk}) + url = reverse("read_%s" % self.model_name, kwargs={"pk": item.pk}) request = self.request_factory.get(url) request.user = self.user @@ -96,8 +102,8 @@ def test_read(self): self.assertEqual(200, rsp.status_code) rsp.render() - body = rsp.content.decode('utf-8') - self.assertIn('bar', body) + body = rsp.content.decode("utf-8") + self.assertIn("bar", body) # Test get_field_label_value() by checking the rendering of the the 5 fields of # TestLabelValueBreadReadView. @@ -107,9 +113,11 @@ def test_read(self): ": 0", ": edcba", ": bar", - ": {}".format(key), + ": {}".format( + key + ), ": 42", - ): + ): self.assertContains(rsp, expected) def test_setting_form_class(self): @@ -125,7 +133,7 @@ class TestView(ReadView): # bread, use a fake dispatch method that saves 'self' into a # dictionary we can access in the test. def dispatch(self, *args, **kwargs): - glob['view_object'] = self + glob["view_object"] = self class BreadTest(Bread): model = BreadTestModel @@ -135,4 +143,4 @@ class BreadTest(Bread): view_function = bread.get_read_view() # Call the view function to invoke dispatch so we can get to the view itself view_function(None, None, None) - self.assertEqual(DummyForm, glob['view_object'].form_class) + self.assertEqual(DummyForm, glob["view_object"].form_class) diff --git a/tests/test_search.py b/tests/test_search.py index 384d278..66461c3 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,27 +1,27 @@ # coding: utf-8 +from bread.bread import Bread, BrowseView from tests.base import BreadTestCase from tests.factories import BreadTestModelFactory -from bread.bread import BrowseView, Bread from tests.models import BreadTestModel class BrowseSearchView(BrowseView): - search_fields = ['name', 'other__text'] + search_fields = ["name", "other__text"] class BreadSearchView(Bread): # Bread view with search fields defined - base_template = 'bread/empty.html' + base_template = "bread/empty.html" browse_view = BrowseSearchView model = BreadTestModel - views = 'B' + views = "B" class BreadNoSearchView(Bread): # Default bread view - no search fields defined - base_template = 'bread/empty.html' + base_template = "bread/empty.html" model = BreadTestModel - views = 'B' + views = "B" class BreadSearchTestCase(BreadTestCase): @@ -29,62 +29,62 @@ def setUp(self): super(BreadSearchTestCase, self).setUp() self.bread = BreadSearchView() self.view = self.bread.get_browse_view() - self.give_permission('browse') + self.give_permission("browse") - self.joe = BreadTestModelFactory(name='Joe', other__text='Smith') - self.jim = BreadTestModelFactory(name='Jim', other__text='Brown') + self.joe = BreadTestModelFactory(name="Joe", other__text="Smith") + self.jim = BreadTestModelFactory(name="Jim", other__text="Brown") def get_search_results(self, q=None): data = {} if q is not None: - data['q'] = q - request = self.request_factory.get('', data=data) + data["q"] = q + request = self.request_factory.get("", data=data) request.user = self.user rsp = self.view(request) self.assertEqual(200, rsp.status_code) - return rsp.context_data['object_list'] + return rsp.context_data["object_list"] def test_no_query_parm(self): objs = self.get_search_results() self.assertEqual(BreadTestModel.objects.count(), len(objs)) def test_simple_search_direct_field(self): - objs = self.get_search_results(q='Joe') + objs = self.get_search_results(q="Joe") obj_ids = [obj.id for obj in objs] self.assertEqual([self.joe.id], obj_ids) def test_simple_search_any_field(self): # All records that match any field are returned - objs = self.get_search_results(q='i') + objs = self.get_search_results(q="i") self.assertEqual(2, len(objs)) def test_simple_search_indirect_field(self): - objs = self.get_search_results(q='Smith') + objs = self.get_search_results(q="Smith") obj_ids = [obj.id for obj in objs] self.assertEqual([self.joe.id], obj_ids) def test_multiple_terms_match(self): # A record that matches all terms is returned - objs = self.get_search_results(q='Joe Smith') + objs = self.get_search_results(q="Joe Smith") obj_ids = [obj.id for obj in objs] self.assertEqual([self.joe.id], obj_ids) def test_multiple_terms_dont_match(self): # All terms must match the record - objs = self.get_search_results(q='Joe Brown') + objs = self.get_search_results(q="Joe Brown") self.assertEqual(0, len(objs)) def test_case_insensitive(self): - objs = self.get_search_results(q='joe') + objs = self.get_search_results(q="joe") obj_ids = [obj.id for obj in objs] self.assertEqual([self.joe.id], obj_ids) def test_nonascii_search(self): # This was failing if we were also paginating - BreadTestModelFactory(name=u'قمر') - BreadTestModelFactory(name=u'قمر') + BreadTestModelFactory(name=u"قمر") + BreadTestModelFactory(name=u"قمر") try: self.bread.browse_view.paginate_by = 1 - self.get_search_results(q=u'قمر') + self.get_search_results(q=u"قمر") finally: self.bread.browse_view.paginate_by = None diff --git a/tests/test_templates.py b/tests/test_templates.py index fcf79f2..38f0061 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -2,39 +2,46 @@ class BreadTemplateResolutionTest(BreadTestCase): - def setUp(self): super(BreadTemplateResolutionTest, self).setUp() # skip 'add' since it uses 'edit' template - self.bread_names = ['browse', 'read', 'edit', 'delete'] + self.bread_names = ["browse", "read", "edit", "delete"] self.app_name = self.model._meta.app_label def test_default_template_resolution(self): for bread_name in self.bread_names: - vanilla_template_name = '%s/%s_%s.html' % (self.app_name, self.model_name, bread_name) - default_bread_template_name = 'bread/%s.html' % (bread_name, ) + vanilla_template_name = "%s/%s_%s.html" % ( + self.app_name, + self.model_name, + bread_name, + ) + default_bread_template_name = "bread/%s.html" % (bread_name,) expected_templates = [ vanilla_template_name, default_bread_template_name, ] - get_method_name = '%s_view' % (bread_name, ) + get_method_name = "%s_view" % (bread_name,) view_class = getattr(self.bread, get_method_name) view = view_class(bread=self.bread, model=self.bread.model) self.assertEqual(view.get_template_names(), expected_templates) def test_customized_template_resolution(self): - self.bread.template_name_pattern = 'mysite/bread/{view}.html' + self.bread.template_name_pattern = "mysite/bread/{view}.html" for bread_name in self.bread_names: - vanilla_template_name = '%s/%s_%s.html' % (self.app_name, self.model_name, bread_name) - custom_template_name = 'mysite/bread/%s.html' % (bread_name, ) - default_bread_template_name = 'bread/%s.html' % (bread_name, ) + vanilla_template_name = "%s/%s_%s.html" % ( + self.app_name, + self.model_name, + bread_name, + ) + custom_template_name = "mysite/bread/%s.html" % (bread_name,) + default_bread_template_name = "bread/%s.html" % (bread_name,) expected_templates = [ vanilla_template_name, custom_template_name, default_bread_template_name, ] - get_method_name = '%s_view' % (bread_name, ) + get_method_name = "%s_view" % (bread_name,) view_class = getattr(self.bread, get_method_name) view = view_class(bread=self.bread, model=self.bread.model) self.assertEqual(view.get_template_names(), expected_templates) diff --git a/tests/test_urls.py b/tests/test_urls.py index 16090e5..1ee96a7 100644 --- a/tests/test_urls.py +++ b/tests/test_urls.py @@ -2,7 +2,7 @@ class BreadURLsNamespaceTest(BreadTestCase): - url_namespace = 'testns' + url_namespace = "testns" def test_all_views_urls_with_namespace(self): # get_urls() returns the expected URL patterns @@ -10,13 +10,16 @@ def test_all_views_urls_with_namespace(self): patterns = bread.get_urls() self.assertEqual( - set([bread.browse_url_name(include_namespace=False), - bread.read_url_name(include_namespace=False), - bread.edit_url_name(include_namespace=False), - bread.add_url_name(include_namespace=False), - bread.delete_url_name(include_namespace=False), - ]), - set([x.name for x in patterns]) + set( + [ + bread.browse_url_name(include_namespace=False), + bread.read_url_name(include_namespace=False), + bread.edit_url_name(include_namespace=False), + bread.add_url_name(include_namespace=False), + bread.delete_url_name(include_namespace=False), + ] + ), + set([x.name for x in patterns]), ) self.assertTrue(bread.browse_url_name().startswith(self.url_namespace + ":")) @@ -24,34 +27,54 @@ def test_all_views_urls_with_namespace(self): self.assertTrue(bread.edit_url_name().startswith(self.url_namespace + ":")) self.assertTrue(bread.add_url_name().startswith(self.url_namespace + ":")) self.assertTrue(bread.delete_url_name().startswith(self.url_namespace + ":")) - self.assertFalse(bread.browse_url_name(include_namespace=False) - .startswith(self.url_namespace + ":")) - self.assertFalse(bread.read_url_name(include_namespace=False) - .startswith(self.url_namespace + ":")) - self.assertFalse(bread.edit_url_name(include_namespace=False) - .startswith(self.url_namespace + ":")) - self.assertFalse(bread.add_url_name(include_namespace=False) - .startswith(self.url_namespace + ":")) - self.assertFalse(bread.delete_url_name(include_namespace=False) - .startswith(self.url_namespace + ":")) + self.assertFalse( + bread.browse_url_name(include_namespace=False).startswith( + self.url_namespace + ":" + ) + ) + self.assertFalse( + bread.read_url_name(include_namespace=False).startswith( + self.url_namespace + ":" + ) + ) + self.assertFalse( + bread.edit_url_name(include_namespace=False).startswith( + self.url_namespace + ":" + ) + ) + self.assertFalse( + bread.add_url_name(include_namespace=False).startswith( + self.url_namespace + ":" + ) + ) + self.assertFalse( + bread.delete_url_name(include_namespace=False).startswith( + self.url_namespace + ":" + ) + ) browse_pattern = [ - x for x in patterns + x + for x in patterns if x.name == bread.browse_url_name(include_namespace=False) ][0].pattern - self.assertEqual('%s/' % self.bread.plural_name, str(browse_pattern)) + self.assertEqual("%s/" % self.bread.plural_name, str(browse_pattern)) read_pattern = [ - x for x in patterns + x + for x in patterns if x.name == bread.read_url_name(include_namespace=False) ][0].pattern - self.assertEqual('%s//' % self.bread.plural_name, str(read_pattern)) + self.assertEqual("%s//" % self.bread.plural_name, str(read_pattern)) edit_pattern = [ - x for x in patterns + x + for x in patterns if x.name == bread.edit_url_name(include_namespace=False) ][0].pattern - self.assertEqual('%s//edit/' % self.bread.plural_name, str(edit_pattern)) + self.assertEqual( + "%s//edit/" % self.bread.plural_name, str(edit_pattern) + ) class BreadURLsTest(BreadTestCase): @@ -61,70 +84,88 @@ def test_all_views_urls_no_namespace(self): patterns = bread.get_urls() self.assertEqual( - set([bread.browse_url_name(), - bread.read_url_name(), - bread.edit_url_name(), - bread.add_url_name(), - bread.delete_url_name(), - ]), - set([x.name for x in patterns]) + set( + [ + bread.browse_url_name(), + bread.read_url_name(), + bread.edit_url_name(), + bread.add_url_name(), + bread.delete_url_name(), + ] + ), + set([x.name for x in patterns]), ) - browse_pattern = [x for x in patterns if x.name == bread.browse_url_name()][0].pattern - self.assertEqual('%s/' % bread.plural_name, str(browse_pattern)) + browse_pattern = [x for x in patterns if x.name == bread.browse_url_name()][ + 0 + ].pattern + self.assertEqual("%s/" % bread.plural_name, str(browse_pattern)) - read_pattern = [x for x in patterns if x.name == bread.read_url_name()][0].pattern - self.assertEqual('%s//' % bread.plural_name, str(read_pattern)) + read_pattern = [x for x in patterns if x.name == bread.read_url_name()][ + 0 + ].pattern + self.assertEqual("%s//" % bread.plural_name, str(read_pattern)) - edit_pattern = [x for x in patterns if x.name == bread.edit_url_name()][0].pattern - self.assertEqual('%s//edit/' % bread.plural_name, str(edit_pattern)) + edit_pattern = [x for x in patterns if x.name == bread.edit_url_name()][ + 0 + ].pattern + self.assertEqual("%s//edit/" % bread.plural_name, str(edit_pattern)) def test_view_subset(self): # We can do bread with a subset of the BREAD views - self.bread.views = 'B' + self.bread.views = "B" url_names = [x.name for x in self.bread.get_urls()] - self.assertIn('browse_%s' % self.bread.plural_name, url_names) - self.assertNotIn('read_%s' % self.model_name, url_names) - self.assertNotIn('edit_%s' % self.model_name, url_names) - self.assertNotIn('add_%s' % self.model_name, url_names) - self.assertNotIn('delete_%s' % self.model_name, url_names) + self.assertIn("browse_%s" % self.bread.plural_name, url_names) + self.assertNotIn("read_%s" % self.model_name, url_names) + self.assertNotIn("edit_%s" % self.model_name, url_names) + self.assertNotIn("add_%s" % self.model_name, url_names) + self.assertNotIn("delete_%s" % self.model_name, url_names) - self.bread.views = 'RE' + self.bread.views = "RE" url_names = [x.name for x in self.bread.get_urls()] - self.assertNotIn('browse_%s' % self.bread.plural_name, url_names) - self.assertIn('read_%s' % self.model_name, url_names) - self.assertIn('edit_%s' % self.model_name, url_names) - self.assertNotIn('add_%s' % self.model_name, url_names) - self.assertNotIn('delete_%s' % self.model_name, url_names) + self.assertNotIn("browse_%s" % self.bread.plural_name, url_names) + self.assertIn("read_%s" % self.model_name, url_names) + self.assertIn("edit_%s" % self.model_name, url_names) + self.assertNotIn("add_%s" % self.model_name, url_names) + self.assertNotIn("delete_%s" % self.model_name, url_names) def test_url_names(self): # The xxxx_url_name methods return what we expect bread = self.bread - self.assertEqual('browse_%s' % self.bread.plural_name, bread.browse_url_name()) - self.assertEqual('read_%s' % self.model_name, bread.read_url_name()) - self.assertEqual('edit_%s' % self.model_name, bread.edit_url_name()) - self.assertEqual('add_%s' % self.model_name, bread.add_url_name()) - self.assertEqual('delete_%s' % self.model_name, bread.delete_url_name()) + self.assertEqual("browse_%s" % self.bread.plural_name, bread.browse_url_name()) + self.assertEqual("read_%s" % self.model_name, bread.read_url_name()) + self.assertEqual("edit_%s" % self.model_name, bread.edit_url_name()) + self.assertEqual("add_%s" % self.model_name, bread.add_url_name()) + self.assertEqual("delete_%s" % self.model_name, bread.delete_url_name()) def test_omit_prefix(self): bread = self.bread patterns = bread.get_urls(prefix=False) self.assertEqual( - set([bread.browse_url_name(), - bread.read_url_name(), - bread.edit_url_name(), - bread.add_url_name(), - bread.delete_url_name(), - ]), - set([x.name for x in patterns]) + set( + [ + bread.browse_url_name(), + bread.read_url_name(), + bread.edit_url_name(), + bread.add_url_name(), + bread.delete_url_name(), + ] + ), + set([x.name for x in patterns]), ) - browse_pattern = [x for x in patterns if x.name == bread.browse_url_name()][0].pattern - self.assertEqual('', str(browse_pattern)) + browse_pattern = [x for x in patterns if x.name == bread.browse_url_name()][ + 0 + ].pattern + self.assertEqual("", str(browse_pattern)) - read_pattern = [x for x in patterns if x.name == bread.read_url_name()][0].pattern - self.assertEqual('/', str(read_pattern)) + read_pattern = [x for x in patterns if x.name == bread.read_url_name()][ + 0 + ].pattern + self.assertEqual("/", str(read_pattern)) - edit_pattern = [x for x in patterns if x.name == bread.edit_url_name()][0].pattern - self.assertEqual('/edit/', str(edit_pattern)) + edit_pattern = [x for x in patterns if x.name == bread.edit_url_name()][ + 0 + ].pattern + self.assertEqual("/edit/", str(edit_pattern)) diff --git a/tests/test_utils.py b/tests/test_utils.py index 8e82546..7bdbeee 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,150 +1,181 @@ -from django.core.exceptions import ValidationError, FieldDoesNotExist +from django.core.exceptions import FieldDoesNotExist, ValidationError from django.test import TestCase -from bread.utils import get_model_field, validate_fieldspec, has_required_args, get_verbose_name -from tests.models import BreadTestModel2, BreadTestModel, BreadLabelValueTestModel +from bread.utils import ( + get_model_field, + get_verbose_name, + has_required_args, + validate_fieldspec, +) +from tests.models import BreadLabelValueTestModel, BreadTestModel, BreadTestModel2 class HasRequiredArgsTestCase(TestCase): def test_simple_no_args_function(self): def testfunc(): pass + self.assertFalse(has_required_args(testfunc)) def test_simple_1_arg_function(self): def testfunc(foo): pass + self.assertTrue(has_required_args(testfunc)) def test_simple_1_arg_function_with_default(self): def testfunc(foo=2): pass + self.assertFalse(has_required_args(testfunc)) def test_simple_2_arg_function(self): def testfunc(foo, bar): pass + self.assertTrue(has_required_args(testfunc)) def test_simple_2_arg_function_with_one_default(self): def testfunc(foo, bar=3): pass + self.assertTrue(has_required_args(testfunc)) def test_simple_2_arg_function_with_two_defaults(self): def testfunc(foo=1, bar=3): pass + self.assertFalse(has_required_args(testfunc)) def test_class_function_no_args(self): class TestClass(object): def func(self): pass + self.assertFalse(has_required_args(TestClass.func)) def test_class_function_1_arg(self): class TestClass(object): def func(self, foo): pass + self.assertTrue(has_required_args(TestClass.func)) def test_class_function_1_arg_with_default(self): class TestClass(object): def func(self, foo=2): pass + self.assertFalse(has_required_args(TestClass.func)) def test_class_function_2_args(self): class TestClass(object): def func(self, foo, bar): pass + self.assertTrue(has_required_args(TestClass.func)) def test_class_function_2_args_1_default(self): class TestClass(object): def func(self, foo, bar=2): pass + self.assertTrue(has_required_args(TestClass.func)) def test_class_function_2_args_2_defaults(self): class TestClass(object): def func(self, foo=1, bar=2): pass + self.assertFalse(has_required_args(TestClass.func)) class GetModelFieldTestCase(TestCase): def test_it(self): - obj2 = BreadTestModel2.objects.create( - text="Rhinocerous" - ) - obj1 = BreadTestModel.objects.create( - name="Rudy Vallee", other=obj2, age=72 - ) - self.assertEqual(obj1.name, get_model_field(obj1, 'name')) - self.assertEqual(obj1.name, get_model_field(obj1, 'get_name')) + obj2 = BreadTestModel2.objects.create(text="Rhinocerous") + obj1 = BreadTestModel.objects.create(name="Rudy Vallee", other=obj2, age=72) + self.assertEqual(obj1.name, get_model_field(obj1, "name")) + self.assertEqual(obj1.name, get_model_field(obj1, "get_name")) - self.assertEqual(obj2.text, get_model_field(obj1, 'other__text')) - self.assertEqual(obj2.text, get_model_field(obj1, 'other__get_text')) + self.assertEqual(obj2.text, get_model_field(obj1, "other__text")) + self.assertEqual(obj2.text, get_model_field(obj1, "other__get_text")) # Prove that we can call a dunder method. - self.assertEqual(obj1.name, get_model_field(obj1, '__str__')) + self.assertEqual(obj1.name, get_model_field(obj1, "__str__")) class ValidateFieldspecTestCase(TestCase): def test_simple_field(self): - validate_fieldspec(BreadTestModel, 'name') + validate_fieldspec(BreadTestModel, "name") def test_method_name(self): - validate_fieldspec(BreadTestModel, 'get_name') + validate_fieldspec(BreadTestModel, "get_name") def test_method_with_optional_arg(self): - validate_fieldspec(BreadTestModel, 'method2') + validate_fieldspec(BreadTestModel, "method2") def test_method_with_required_arg(self): with self.assertRaises(ValidationError): - validate_fieldspec(BreadTestModel, 'method1') + validate_fieldspec(BreadTestModel, "method1") def test_no_such_attribute(self): with self.assertRaises(ValidationError): - validate_fieldspec(BreadTestModel, 'petunias') + validate_fieldspec(BreadTestModel, "petunias") def test_get_other(self): - validate_fieldspec(BreadTestModel, 'other') + validate_fieldspec(BreadTestModel, "other") def test_field_on_other(self): - validate_fieldspec(BreadTestModel, 'other__text') + validate_fieldspec(BreadTestModel, "other__text") def test_method_on_other(self): - validate_fieldspec(BreadTestModel, 'other__get_text') + validate_fieldspec(BreadTestModel, "other__get_text") def test_no_such_attribute_on_other(self): with self.assertRaises(ValidationError): - validate_fieldspec(BreadTestModel, 'other__petunias') + validate_fieldspec(BreadTestModel, "other__petunias") class GetVerboseNameTest(TestCase): """Exercise get_verbose_name()""" + def test_with_model(self): """Ensure a model is accepted as a param""" - self.assertEqual(get_verbose_name(BreadLabelValueTestModel, 'banana'), "A Yellow Fruit") + self.assertEqual( + get_verbose_name(BreadLabelValueTestModel, "banana"), "A Yellow Fruit" + ) def test_with_instance(self): """Ensure a model instance is accepted as a param""" - self.assertEqual(get_verbose_name(BreadLabelValueTestModel(), 'banana'), "A Yellow Fruit") + self.assertEqual( + get_verbose_name(BreadLabelValueTestModel(), "banana"), "A Yellow Fruit" + ) def test_no_title_cap(self): """Ensure title cap is optional""" - self.assertEqual(get_verbose_name(BreadLabelValueTestModel, 'banana', False), - "a yellow fruit") + self.assertEqual( + get_verbose_name(BreadLabelValueTestModel, "banana", False), + "a yellow fruit", + ) def test_field_with_no_explicit_verbose_name(self): """Test behavior with a field to which we haven't given an explicit name""" - self.assertEqual(get_verbose_name(BreadLabelValueTestModel, 'id'), "Id") + self.assertEqual(get_verbose_name(BreadLabelValueTestModel, "id"), "Id") def test_failure(self): """Ensure FieldDoesNotExist is raised no matter what trash is passed as the field name""" - for field_name in ('kjasfhkjdh', u'sfasfda', None, 42, False, complex(42), lambda: None, - ValueError(), {}, [], tuple()): + for field_name in ( + "kjasfhkjdh", + u"sfasfda", + None, + 42, + False, + complex(42), + lambda: None, + ValueError(), + {}, + [], + tuple(), + ): with self.assertRaises(FieldDoesNotExist): get_verbose_name(BreadLabelValueTestModel, field_name) diff --git a/tox.ini b/tox.ini index e81fda6..26c4c12 100644 --- a/tox.ini +++ b/tox.ini @@ -1,28 +1,17 @@ [tox] -downloadcache = {toxworkdir}/_download/ -envlist = {py37,py38,py39}-django{22,30,31}, docs, py38-pep8 -whitelist_externals = /usr/bin/make +envlist = {py37,py38,py39}-django{22,30,31} + +[gh-actions] +python = + 3.6: py36 + 3.7: py37 + 3.8: py38 + 3.9: py39 [testenv] -basepython = - py37: python3.7 - py38: python3.8 - py39: python3.9 deps = factory_boy==2.3.1 django22: Django>=2.2,<3.0 - django30: DJango>=3.0,<3.1 - django31: DJango>=3.1,<3.2 -# -Wmodule so we at least see deprecation warnings -commands = {envpython} -Wmodule runtests.py {posargs} - -[testenv:docs] -basepython = python3.8 -deps = sphinx -changedir = docs -commands = /usr/bin/make html - -[testenv:py38-pep8] -basepython = python3.8 -deps = flake8 -commands = flake8 + django30: Django>=3.0,<3.1 + django31: Django>=3.1,<3.2 +commands = python -Wmodule runtests.py