├── .coveragerc ├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.rst ├── MANIFEST.in ├── README.rst ├── author ├── __init__.py ├── backends.py ├── conf.py ├── decorators.py ├── locale │ ├── cs_CZ │ │ └── LC_MESSAGES │ │ │ └── django.po │ └── pt_BR │ │ └── LC_MESSAGES │ │ └── django.po ├── middlewares.py └── receivers.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── autocmd └── __init__.py ├── blog ├── __init__.py ├── admin.py ├── fixtures │ └── test.yaml ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ ├── 404.html │ └── blog │ │ ├── entry_confirm_delete.html │ │ ├── entry_detail.html │ │ ├── entry_form.html │ │ └── entry_list.html ├── tests │ ├── __init__.py │ ├── test_backends.py │ ├── test_models.py │ ├── test_receivers.py │ ├── test_settings.py │ └── test_views.py ├── urls.py └── views.py ├── manage.py ├── runtests.py ├── settings.py ├── static └── css │ ├── image │ ├── edit.png │ └── trash.png │ ├── style.css │ └── style.scss ├── templates ├── 404.html ├── base.html ├── blog │ ├── entry_confirm_delete.html │ ├── entry_detail.html │ ├── entry_form.html │ └── entry_list.html └── registration │ ├── logged_out.html │ └── login.html └── urls.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | include = author* 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | indent_style = space 11 | indent_size = 4 12 | 13 | [*.py] 14 | max_line_length = 100 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ '**' ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | env: 17 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 18 | 19 | # Cancels any in-progress runs when code is pushed to the same PR/branch 20 | # At that point in time the running workflow results are irrelevant because there is new code to test against 21 | concurrency: 22 | group: ${{ github.workflow }}-${{ github.ref }} 23 | cancel-in-progress: true 24 | 25 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 26 | jobs: 27 | # This workflow contains a single job called "build" 28 | tests: 29 | # The type of runner that the job will run on 30 | runs-on: ubuntu-latest 31 | 32 | strategy: 33 | matrix: 34 | DJANGO_VERSION: [ '2.2.*', '3.0.*', '3.1.*', '3.2.*', '4.0.*', '4.1.*'] 35 | python-version: ['3.7', '3.8', '3.9', '3.10'] 36 | exclude: 37 | - DJANGO_VERSION: '4.1.*' 38 | python-version: '3.7' 39 | - DJANGO_VERSION: '4.0.*' 40 | python-version: '3.7' 41 | - DJANGO_VERSION: '3.1.*' 42 | python-version: '3.10' 43 | - DJANGO_VERSION: '3.0.*' 44 | python-version: '3.10' 45 | - DJANGO_VERSION: '2.2.*' 46 | python-version: '3.10' 47 | fail-fast: false 48 | 49 | services: 50 | postgres: 51 | image: postgres 52 | env: 53 | POSTGRES_PASSWORD: postgres 54 | options: >- 55 | --health-cmd pg_isready 56 | --health-interval 10s 57 | --health-timeout 5s 58 | --health-retries 5 59 | ports: 60 | - 5432:5432 61 | 62 | steps: 63 | - uses: actions/checkout@v2 64 | 65 | - name: Set up Python ${{ matrix.python-version }} 66 | uses: actions/setup-python@v2 67 | with: 68 | python-version: ${{ matrix.python-version }} 69 | - uses: actions/cache@v2 70 | with: 71 | path: ~/.cache/pip 72 | key: ${{ hashFiles('setup.py') }}-${{ matrix.DJANGO_VERSION }} 73 | 74 | - name: Install 75 | run: | 76 | pip install setuptools --upgrade 77 | pip install Django==${{ matrix.DJANGO_VERSION }} 78 | pip install pyaml 79 | pip install mock 80 | pip install coverage coveralls codecov 81 | env: 82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 83 | 84 | - name: Testing 85 | run: | 86 | python -Wall -W error::DeprecationWarning -m coverage run tests/runtests.py 87 | coveralls --service=github 88 | coverage xml && codecov 89 | env: 90 | POSTGRES_HOST: localhost 91 | POSTGRES_PORT: 5432 92 | DB_ENGINE: ${{ matrix.DB_ENGINE }} 93 | lint: 94 | runs-on: ubuntu-latest 95 | steps: 96 | - uses: actions/checkout@v2 97 | - name: Install 98 | run: | 99 | pip install flake8 flake8-import-order flake8-blind-except flake8-tidy-imports flake8-comprehensions 100 | pip install isort black mypy 101 | - name: Running Flake8 102 | run: flake8 --application-import-names=author 103 | - name: Running isort 104 | run: python -m isort . --check-only --diff 105 | - name: Running mypy 106 | run: mypy author --ignore-missing-imports 107 | - name: Running black 108 | run: black --check . 109 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # PyPI 2 | /build 3 | /dist 4 | /*.egg-info 5 | 6 | # miniblog 7 | *.db 8 | .sass-cache/ 9 | 10 | #locale 11 | *.mo 12 | 13 | *.pyc 14 | *.swp 15 | *~ 16 | 17 | # IDE 18 | .idea 19 | 20 | .coverage 21 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 1.2.0 (2022-04-19) 5 | ------------------- 6 | * Switch to using lazy settings object 7 | * Update middleware to Django's current style 8 | 9 | 1.1.0 (2022-04-15) 10 | ------------------- 11 | * Make possible to set ``instance._change_updated_by`` option to allow not saving ``updated_by`` choice 12 | 13 | 1.0.4 (2022-01-19) 14 | ------------------- 15 | * Don't include tests in PyPI package 16 | 17 | 1.0.3 (2022-01-15) 18 | ------------------- 19 | * Update to Django 4.0 20 | * Fix tests and drop support for Django versions < 2.2 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-exclude tests * 2 | recursive-include author/locale *.po *.mo 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ************* 2 | django-author 3 | ************* 4 | .. image:: https://github.com/lambdalisue/django-author/actions/workflows/main.yml/badge.svg 5 | :target: https://github.com/lambdalisue/django-author/actions/workflows/main.yml 6 | .. image:: https://coveralls.io/repos/github/lambdalisue/django-author/badge.svg?branch=master 7 | :target: https://coveralls.io/github/lambdalisue/django-author?branch=master 8 | 9 | Update author and updated_by fields of models automatically 10 | 11 | This library is used for updating ``author`` and ``updated_by`` fields automatically 12 | with ``request.user`` when the model has created/changed. 13 | 14 | Also if you are too lazy to write ``author = models.ForeignKey(User, _('author'), related_name ...)`` to every model, 15 | just add ``@with_author`` decorator to the top of class makes you happy. 16 | 17 | 18 | Install 19 | ============== 20 | This library is on PyPI so you can install it with:: 21 | 22 | pip install django-author 23 | 24 | or from github:: 25 | 26 | pip install git+https://github.com/lambdalisue/django-author.git 27 | 28 | 29 | Usage 30 | ========== 31 | 32 | 1. Add ``'author'`` to your ``INSTALLED_APPS`` on settings.py 33 | 34 | 2. Add ``'author.middlewares.AuthorDefaultBackendMiddleware'`` to your ``MIDDLEWARE_CLASSES`` 35 | if you use default author backend 36 | 37 | 3. Add ``author`` and ``updated_by`` field to models which you want to have ``author`` and ``updated_by`` fields manually 38 | or use ``@with_author`` decorator like below:: 39 | 40 | from django.db import models 41 | from author.decorators import with_author 42 | 43 | @with_author 44 | class Entry(models.Model): 45 | title = models.CharField('title', max_length=50) 46 | body = models.TextField('body') 47 | 48 | 4. Done. Now you have automatically updated ``author`` and ``updated_by`` fields 49 | 50 | If you are in truble, see ``author_test`` directory for usage sample. 51 | 52 | 5. If you want to forbid modification of the ``updated_by`` field for some actions, just set ``instance._change_updated_by = False`` before calling ``save()``. 53 | 54 | 55 | Settings 56 | ================ 57 | 58 | AUTHOR_BACKEND 59 | Class or string path of backend. the backend is used to determine user when object is created/updated. 60 | 61 | AUTHOR_CREATED_BY_FIELD_NAME 62 | A name of field. the setting also interfer the name of field created by ``@with_author`` decorator. default is 'author' 63 | 64 | AUTHOR_UPDATED_BY_FIELD_NAME 65 | A name of field. the setting also interfer the name of field created by ``@with_author`` decorator. default is 'updated_by' 66 | 67 | AUTHOR_DO_NOT_UPDATE_WHILE_USER_IS_NONE 68 | Do not update ``author`` or ``updated_by`` field when detected user is None. default is True 69 | 70 | AUTHOR_MODELS 71 | Check signals for only these models. default is None 72 | 73 | AUTHOR_IGNORE_MODELS 74 | Do not check signals for these models. default is ['auth.user', 'auth.group', 'auth.permission', 'contenttype.contenttype'] 75 | 76 | 77 | Backend 78 | ============== 79 | The default backend use ``thread_locals`` storategy to get current request in signal call. 80 | 81 | If you want to change the strategy or whatever, create your own backend. 82 | 83 | A backend is a class which have ``get_user`` method to determine current user. 84 | 85 | AuthorDefaultBackend 86 | Default backend. This backend return None when no request found or AnonymousUser create/update object. 87 | 88 | AuthorSystemUserBackend 89 | System user backend. This backend return system user when no request found or AnonymousUser create/update object. 90 | 91 | system user is determined with ``get_system_user`` method and default is ``User.objects.get(pk=1)`` 92 | -------------------------------------------------------------------------------- /author/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim: set fileencoding=utf8: 3 | """ 4 | initialization django-object-permission 5 | 6 | Add this backend to your ``AUTHENTICATION_BACKENDS`` like below:: 7 | 8 | AUTHENTICATION_BACKENDS = ( 9 | 'django.contrib.auth.backends.ModelBackend', 10 | 'object_permission.backends.ObjectPermBackend', 11 | ) 12 | 13 | AUTHOR: 14 | lambdalisue[Ali su ae] (lambdalisue@hashnote.net) 15 | 16 | Copyright: 17 | Copyright 2011 Alisue allright reserved. 18 | 19 | License: 20 | Licensed under the Apache License, Version 2.0 (the "License"); 21 | you may not use this file except in compliance with the License. 22 | You may obtain a copy of the License at 23 | 24 | http://www.apache.org/licenses/LICENSE-2.0 25 | 26 | Unliss required by applicable law or agreed to in writing, software 27 | distributed under the License is distrubuted on an "AS IS" BASICS, 28 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | See the License for the specific language governing permissions and 30 | limitations under the License. 31 | """ 32 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 33 | from importlib import import_module 34 | 35 | from django.core.exceptions import ImproperlyConfigured 36 | 37 | from . import receivers 38 | from .conf import settings 39 | 40 | 41 | def load_backend(path): 42 | """load author backend from string path""" 43 | i = path.rfind(".") 44 | module, attr = path[:i], path[i + 1 :] 45 | try: 46 | mod = import_module(module) 47 | except ImportError as e: 48 | raise ImproperlyConfigured( 49 | 'Error importing author backend %s: "%s"' % (path, e) 50 | ) 51 | except ValueError: 52 | raise ImproperlyConfigured( 53 | "Error importing author backend. Is AUTHOR_BACKEND a correctly defined?", 54 | ) 55 | try: 56 | cls = getattr(mod, attr) 57 | except AttributeError: 58 | raise ImproperlyConfigured( 59 | 'Module "%s" does not define a "%s" author backend' % (module, attr), 60 | ) 61 | return cls 62 | 63 | 64 | def get_backend_class(): 65 | """get author backend""" 66 | from .backends import AuthorDefaultBackend 67 | 68 | backend = getattr(settings, "AUTHOR_BACKEND", AuthorDefaultBackend) 69 | try: 70 | is_backend_string = isinstance(backend, basestring) 71 | except NameError: 72 | is_backend_string = isinstance(backend, str) 73 | if is_backend_string: 74 | backend = load_backend(backend) 75 | 76 | if isinstance(backend, object) and hasattr(backend, "get_user"): 77 | return backend 78 | else: 79 | raise ImproperlyConfigured( 80 | 'Error author backend must have "get_user" method Please define it in %s.' 81 | % backend, 82 | ) 83 | 84 | 85 | def get_backend(): 86 | backend_class = get_backend_class() 87 | return backend_class() 88 | 89 | 90 | # Register receivers 91 | receivers.register() 92 | -------------------------------------------------------------------------------- /author/backends.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim: set fileencoding=utf8: 3 | """ 4 | backends for django-author 5 | 6 | AUTHOR: 7 | lambdalisue[Ali su ae] (lambdalisue@hashnote.net) 8 | 9 | Copyright: 10 | Copyright 2011 Alisue allright reserved. 11 | 12 | License: 13 | Licensed under the Apache License, Version 2.0 (the "License"); 14 | you may not use this file except in compliance with the License. 15 | You may obtain a copy of the License at 16 | 17 | http://www.apache.org/licenses/LICENSE-2.0 18 | 19 | Unliss required by applicable law or agreed to in writing, software 20 | distributed under the License is distrubuted on an "AS IS" BASICS, 21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | See the License for the specific language governing permissions and 23 | limitations under the License. 24 | """ 25 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 26 | from django.contrib import auth 27 | from django.core.exceptions import ImproperlyConfigured 28 | 29 | from .conf import settings 30 | from .middlewares import get_request 31 | 32 | 33 | class AuthorDefaultBackend(object): 34 | """Author default backend 35 | 36 | Get current user from request stored in thread_locals. 37 | Return None when request.user is not detected include request.user 38 | is AnonymousUser 39 | 40 | """ 41 | 42 | def __init__(self): 43 | 44 | required_middleware = "author.middlewares.AuthorDefaultBackendMiddleware" 45 | 46 | middlewares = settings.MIDDLEWARE 47 | 48 | if required_middleware not in middlewares: 49 | raise ImproperlyConfigured( 50 | 'Error "%s" is not found in MIDDLEWARE_CLASSES nor MIDDLEWARE. ' 51 | "It is required to use AuthorDefaultBackend" % required_middleware, 52 | ) 53 | 54 | def _get_user_model(self): 55 | """get user model class""" 56 | return auth.get_user_model() 57 | 58 | def _get_request(self): 59 | """get current request""" 60 | return get_request() 61 | 62 | def get_user(self): 63 | """get current user""" 64 | request = self._get_request() 65 | if request and getattr(request, "user", None): 66 | if isinstance(request.user, self._get_user_model()): 67 | return request.user 68 | # AnonymousUser 69 | return None 70 | 71 | 72 | class AuthorSystemUserBackend(AuthorDefaultBackend): 73 | """Author System user backend 74 | 75 | Get current user from request stored in thread_locals. 76 | Return System user when request.user is not detected include request.user 77 | is AnonymousUser 78 | 79 | System user is detected with ``get_system_user`` 80 | 81 | """ 82 | 83 | def _get_filter_kwargs(self): 84 | """get kwargs for filtering user""" 85 | return {"pk": 1} 86 | 87 | def get_system_user(self): 88 | """get system user""" 89 | user_model = self._get_user_model() 90 | user = user_model._default_manager.get(**self._get_filter_kwargs()) 91 | return user 92 | 93 | def get_user(self): 94 | """get current user""" 95 | request = self._get_request() 96 | if request and getattr(request, "user", None): 97 | if isinstance(request.user, self._get_user_model()): 98 | return request.user 99 | # AnonymousUser 100 | return self.get_system_user() 101 | -------------------------------------------------------------------------------- /author/conf.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings as dj_settings 2 | 3 | 4 | DEFAULTS = { 5 | "AUTHOR_MODELS": None, 6 | "AUTHOR_DO_NOT_UPDATE_WHILE_USER_IS_NONE": True, 7 | "AUTHOR_IGNORE_MODELS": [ 8 | "auth.user", 9 | "auth.group", 10 | "auth.permission", 11 | "contenttypes.contenttype", 12 | ], 13 | "AUTHOR_UPDATED_BY_FIELD_NAME": "updated_by", 14 | "AUTHOR_CREATED_BY_FIELD_NAME": "author", 15 | } 16 | 17 | 18 | class Settings: 19 | """ 20 | Lazy settings wrapper, for use in app-specific conf.py files 21 | """ 22 | 23 | def __init__(self, defaults): 24 | """ 25 | Constructor 26 | 27 | :param defaults: default values for settings, will be return if 28 | not overridden in the project settings 29 | :type defaults: dict 30 | """ 31 | self.defaults = defaults 32 | 33 | def __getattr__(self, name): 34 | """ 35 | Return the setting with the specified name, from the project settings 36 | (if overridden), else from the default values passed in during 37 | construction. 38 | 39 | :param name: name of the setting to return 40 | :type name: str 41 | :return: the named setting 42 | :raises: AttributeError -- if the named setting is not found 43 | """ 44 | if hasattr(dj_settings, name): 45 | return getattr(dj_settings, name) 46 | 47 | if name in self.defaults: 48 | return self.defaults[name] 49 | 50 | raise AttributeError("'{name}' setting not found".format(name=name)) 51 | 52 | 53 | settings = Settings(DEFAULTS) 54 | -------------------------------------------------------------------------------- /author/decorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim: set fileencoding=utf8: 3 | """ 4 | decorators for django-author 5 | 6 | 7 | AUTHOR: 8 | lambdalisue[Ali su ae] (lambdalisue@hashnote.net) 9 | 10 | Copyright: 11 | Copyright 2011 Alisue allright reserved. 12 | 13 | License: 14 | Licensed under the Apache License, Version 2.0 (the "License"); 15 | you may not use this file except in compliance with the License. 16 | You may obtain a copy of the License at 17 | 18 | http://www.apache.org/licenses/LICENSE-2.0 19 | 20 | Unliss required by applicable law or agreed to in writing, software 21 | distributed under the License is distrubuted on an "AS IS" BASICS, 22 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 23 | See the License for the specific language governing permissions and 24 | limitations under the License. 25 | """ 26 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 27 | from django.db import models 28 | from django.utils.translation import gettext_lazy as _ 29 | 30 | from .conf import settings 31 | 32 | 33 | def with_author(cls): 34 | """Decorator to add created_by/updated_by field to particular model""" 35 | verbose_name_plural = cls._meta.object_name 36 | created_by = models.ForeignKey( 37 | settings.AUTH_USER_MODEL, 38 | verbose_name=_("author"), 39 | related_name="%s_create" % verbose_name_plural.lower(), 40 | null=True, 41 | blank=True, 42 | on_delete=models.SET_NULL, 43 | ) 44 | updated_by = models.ForeignKey( 45 | settings.AUTH_USER_MODEL, 46 | verbose_name=_("last updated by"), 47 | related_name="%s_update" % verbose_name_plural.lower(), 48 | null=True, 49 | blank=True, 50 | on_delete=models.SET_NULL, 51 | ) 52 | 53 | if not hasattr(cls, settings.AUTHOR_CREATED_BY_FIELD_NAME): 54 | cls.add_to_class(settings.AUTHOR_CREATED_BY_FIELD_NAME, created_by) 55 | if not hasattr(cls, settings.AUTHOR_UPDATED_BY_FIELD_NAME): 56 | cls.add_to_class(settings.AUTHOR_UPDATED_BY_FIELD_NAME, updated_by) 57 | 58 | return cls 59 | -------------------------------------------------------------------------------- /author/locale/cs_CZ/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Django author package 2 | # Copyright (C) 2013 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Petr Dlouhý , 2013. 6 | # 7 | #, fuzzy 8 | msgid "" 9 | msgstr "" 10 | "Project-Id-Version: PACKAGE VERSION\n" 11 | "Report-Msgid-Bugs-To: \n" 12 | "POT-Creation-Date: 2013-10-19 23:49+0200\n" 13 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 | "Last-Translator: Petr Dlouhý \n" 15 | "Language-Team: LANGUAGE \n" 16 | "Language: \n" 17 | "MIME-Version: 1.0\n" 18 | "Content-Type: text/plain; charset=UTF-8\n" 19 | "Content-Transfer-Encoding: 8bit\n" 20 | 21 | #: decorators.py:44 22 | msgid "author" 23 | msgstr "autor" 24 | 25 | #: decorators.py:45 26 | msgid "last updated by" 27 | msgstr "naposledy upravil" 28 | -------------------------------------------------------------------------------- /author/locale/pt_BR/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Django author package 2 | # Copyright (C) 2014 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # 5 | # Translators: 6 | # Fabio C. Barrionuevo da Luz 2014. 7 | # 8 | #, fuzzy 9 | msgid "" 10 | msgstr "" 11 | "Project-Id-Version: Django Author\n" 12 | "Report-Msgid-Bugs-To: \n" 13 | "POT-Creation-Date: 2014-04-22 17:40+0000\n" 14 | "PO-Revision-Date: 2014-04-22 17:40+0000\n" 15 | "Last-Translator: Fabio C. Barrionuevo da Luz \n" 16 | "Language-Team: Brazilian Portuguese\n" 17 | "MIME-Version: 1.0\n" 18 | "Content-Type: text/plain; charset=UTF-8\n" 19 | "Content-Transfer-Encoding: 8bit\n" 20 | "Language: pt_BR\n" 21 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 22 | 23 | #: decorators.py:44 24 | msgid "author" 25 | msgstr "autor" 26 | 27 | #: decorators.py:45 28 | msgid "last updated by" 29 | msgstr "última atualização realizada por" 30 | -------------------------------------------------------------------------------- /author/middlewares.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim: set fileencoding=utf8: 3 | """ 4 | middlewares for django-author 5 | 6 | 7 | AUTHOR: 8 | lambdalisue[Ali su ae] (lambdalisue@hashnote.net) 9 | 10 | Copyright: 11 | Copyright 2011 Alisue allright reserved. 12 | 13 | License: 14 | Licensed under the Apache License, Version 2.0 (the "License"); 15 | you may not use this file except in compliance with the License. 16 | You may obtain a copy of the License at 17 | 18 | http://www.apache.org/licenses/LICENSE-2.0 19 | 20 | Unliss required by applicable law or agreed to in writing, software 21 | distributed under the License is distrubuted on an "AS IS" BASICS, 22 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 23 | See the License for the specific language governing permissions and 24 | limitations under the License. 25 | """ 26 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 27 | from threading import local 28 | 29 | 30 | __all__ = ["get_request", "AuthorDefaultBackendMiddleware"] 31 | _thread_locals = local() 32 | 33 | 34 | def get_request(): 35 | """Get request stored in current thread""" 36 | return getattr(_thread_locals, "request", None) 37 | 38 | 39 | class AuthorDefaultBackendMiddleware: 40 | def __init__(self, get_response): 41 | self.get_response = get_response 42 | 43 | def __call__(self, request): 44 | _thread_locals.request = request 45 | response = self.get_response(request) 46 | _thread_locals.request = None 47 | return response 48 | -------------------------------------------------------------------------------- /author/receivers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim: set fileencoding=utf8: 3 | """ 4 | signal receivers for django-author 5 | 6 | 7 | AUTHOR: 8 | lambdalisue[Ali su ae] (lambdalisue@hashnote.net) 9 | 10 | Copyright: 11 | Copyright 2011 Alisue allright reserved. 12 | 13 | License: 14 | Licensed under the Apache License, Version 2.0 (the "License"); 15 | you may not use this file except in compliance with the License. 16 | You may obtain a copy of the License at 17 | 18 | http://www.apache.org/licenses/LICENSE-2.0 19 | 20 | Unliss required by applicable law or agreed to in writing, software 21 | distributed under the License is distrubuted on an "AS IS" BASICS, 22 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 23 | See the License for the specific language governing permissions and 24 | limitations under the License. 25 | """ 26 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 27 | import logging 28 | 29 | from django.db.models.signals import pre_save 30 | 31 | from .conf import settings 32 | 33 | 34 | logger = logging.getLogger(__name__) 35 | 36 | 37 | def pre_save_callback(sender, instance, **kwargs): 38 | from . import get_backend 39 | 40 | if ( 41 | hasattr(instance._meta, "app_label") 42 | and "%s.%s" % (instance._meta.app_label, instance._meta.model_name) 43 | in settings.AUTHOR_IGNORE_MODELS 44 | ): 45 | return 46 | if not hasattr(instance, settings.AUTHOR_CREATED_BY_FIELD_NAME): 47 | return 48 | # get current user via author backend 49 | user = get_backend().get_user() 50 | if settings.AUTHOR_DO_NOT_UPDATE_WHILE_USER_IS_NONE and user is None: 51 | return 52 | if getattr(instance, settings.AUTHOR_CREATED_BY_FIELD_NAME) is None: 53 | setattr(instance, settings.AUTHOR_CREATED_BY_FIELD_NAME, user) 54 | if not getattr( 55 | instance, "_change_updated_by", True 56 | ): # User forbid to modify updated_by field 57 | return 58 | if hasattr(instance, settings.AUTHOR_UPDATED_BY_FIELD_NAME): 59 | setattr(instance, settings.AUTHOR_UPDATED_BY_FIELD_NAME, user) 60 | 61 | 62 | def register(): 63 | if settings.AUTHOR_MODELS: 64 | for model in settings.AUTHOR_MODELS: 65 | app_label, model = model.split(".", 1) 66 | from django.contrib.contenttypes.models import ContentType 67 | 68 | ct = ContentType.objects.get_by_natural_key(app_label, model) 69 | pre_save.connect(pre_save_callback, sender=ct.model_class()) 70 | else: 71 | pre_save.connect(pre_save_callback) 72 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [zest.releaser] 2 | current_version = 1.0.3 3 | commit = True 4 | tag = True 5 | python-file-with-version = ./setup.py 6 | 7 | [flake8] 8 | max-line-length = 100 9 | max-complexity = 10 10 | ignore = 11 | E203, # space befor : in [] of array created by black 12 | W503, # line break before binary operator 13 | exclude = 14 | *migrations/*, 15 | env, 16 | build, 17 | .venv, 18 | enable-extensions = import-order, blind-except 19 | import-order-style = pep8 20 | 21 | [isort] 22 | multi_line_output = 3 23 | lines_after_imports = 2 24 | profile = black 25 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Author: Alisue 5 | # Last Change: 18-Mar-2011. 6 | # 7 | import os 8 | 9 | from setuptools import Command, find_packages, setup 10 | from setuptools.command.sdist import sdist as original_sdist 11 | 12 | 13 | version = "1.2.0" 14 | 15 | 16 | class compile_messages(Command): 17 | description = ( 18 | "re-compile local message files ('.po' to '.mo'). " "it require django-admin.py" 19 | ) 20 | user_options = [] 21 | 22 | def initialize_options(self): 23 | self.cwd = None 24 | 25 | def finalize_options(self): 26 | self.cwd = os.getcwd() 27 | 28 | def run(self): 29 | compile_messages.compile_messages() 30 | 31 | @classmethod 32 | def compile_messages(cls): 33 | """ 34 | Compile '.po' into '.mo' via 'django-admin.py' thus the function 35 | require the django to be installed. 36 | It return True when the process successfully end, otherwise it print 37 | error messages and return False. 38 | https://docs.djangoproject.com/en/dev/ref/django-admin/#compilemessages 39 | """ 40 | try: 41 | import django # noqa 42 | except ImportError: 43 | print( 44 | "####################################################\n" 45 | "Django is not installed.\nIt will not be possible to " 46 | "compile the locale files during installation of " 47 | "django-inspectional-registration.\nPlease, install " 48 | "Django first. Done so, install the django-registration" 49 | "-inspectional\n" 50 | "####################################################\n" 51 | ) 52 | return False 53 | else: 54 | original_cwd = os.getcwd() 55 | BASE = os.path.abspath(os.path.dirname(__file__)) 56 | root = os.path.join(BASE, "author") 57 | os.chdir(root) 58 | os.system("django-admin.py compilemessages") 59 | os.chdir(original_cwd) 60 | return True 61 | 62 | 63 | class sdist(original_sdist): 64 | """ 65 | Run 'sdist' command but make sure that the message files are latest by 66 | running 'compile_messages' before 'sdist' 67 | """ 68 | 69 | def run(self): 70 | compile_messages.compile_messages() 71 | original_sdist.run(self) 72 | 73 | 74 | def read(filename): 75 | import os.path 76 | 77 | return open(os.path.join(os.path.dirname(__file__), filename)).read() 78 | 79 | 80 | setup( 81 | name="django-author", 82 | version=version, 83 | description="Add special User ForeignKey fields which update automatically", 84 | long_description=read("README.rst"), 85 | classifiers=[ 86 | "Framework :: Django", 87 | "Intended Audience :: Developers", 88 | "License :: OSI Approved :: MIT License", 89 | "Programming Language :: Python", 90 | "Topic :: Internet :: WWW/HTTP", 91 | ], 92 | keywords="django author object universal", 93 | author="Alisue", 94 | author_email="lambdalisue@hashnote.net", 95 | url=r"https://github.com/lambdalisue/django-author", 96 | download_url=r"https://github.com/lambdalisue/django-author/tarball/master", 97 | license="MIT", 98 | packages=find_packages(), 99 | include_package_data=True, 100 | zip_safe=True, 101 | install_requires=[ 102 | "setuptools-git", 103 | ], 104 | test_suite="tests.runtests.runtests", 105 | ) 106 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf8: 2 | """ 3 | short module explanation 4 | 5 | 6 | AUTHOR: 7 | lambdalisue[Ali su ae] (lambdalisue@hashnote.net) 8 | 9 | Copyright: 10 | Copyright 2011 Alisue allright reserved. 11 | 12 | License: 13 | Licensed under the Apache License, Version 2.0 (the "License"); 14 | you may not use this file except in compliance with the License. 15 | You may obtain a copy of the License at 16 | 17 | http://www.apache.org/licenses/LICENSE-2.0 18 | 19 | Unliss required by applicable law or agreed to in writing, software 20 | distributed under the License is distrubuted on an "AS IS" BASICS, 21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | See the License for the specific language governing permissions and 23 | limitations under the License. 24 | """ 25 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 26 | -------------------------------------------------------------------------------- /tests/autocmd/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: set fileencoding=utf8: 3 | """ 4 | short module explanation 5 | 6 | 7 | AUTHOR: 8 | lambdalisue[Ali su ae] (lambdalisue@hashnote.net) 9 | 10 | Copyright: 11 | Copyright 2011 Alisue allright reserved. 12 | 13 | License: 14 | Licensed under the Apache License, Version 2.0 (the "License"); 15 | you may not use this file except in compliance with the License. 16 | You may obtain a copy of the License at 17 | 18 | http://www.apache.org/licenses/LICENSE-2.0 19 | 20 | Unliss required by applicable law or agreed to in writing, software 21 | distributed under the License is distrubuted on an "AS IS" BASICS, 22 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 23 | See the License for the specific language governing permissions and 24 | limitations under the License. 25 | """ 26 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 27 | from django.conf import settings 28 | from django.db import models 29 | 30 | 31 | try: 32 | from django.contrib.auth.management import create_superuser 33 | except ImportError: 34 | 35 | def create_superuser(*args, **kwargs): 36 | pass 37 | 38 | 39 | try: 40 | from django.db.models.signals import post_migrate 41 | except ImportError: 42 | from django.db.models.signals import post_syncdb as post_migrate 43 | # from django.contrib.auth import models as auth_models 44 | 45 | settings.AUTO_CREATE_USER = getattr(settings, "AUTO_CREATE_USER", True) 46 | 47 | if settings.DEBUG and settings.AUTO_CREATE_USER: 48 | # From http://stackoverflow.com/questions/1466827/ -- 49 | # 50 | # Prevent interactive question about wanting a superuser created. (This code 51 | # has to go in this otherwise empty "models" module so that it gets processed by 52 | # the "syncdb" command during database creation.) 53 | # 54 | # Create our own test user automatically. 55 | def create_testuser(app, created_models, verbosity, **kwargs): 56 | USERNAME = getattr(settings, "QWERT_AUTO_CREATE_USERNAME", "admin") 57 | PASSWORD = getattr(settings, "QWERT_AUTO_CREATE_PASSWORD", "admin") 58 | EMAIL = getattr(settings, "QWERT_AUTO_CREATE_EMAIL", "x@x.com") 59 | 60 | if getattr(settings, "QWERT_AUTO_CREATE_USER", None): 61 | User = models.get_model(*settings.QWERT_AUTO_CREATE_USER.rsplit(".", 1)) 62 | else: 63 | from django.contrib.auth.models import User 64 | 65 | try: 66 | User.objects.get(username=USERNAME) 67 | except User.DoesNotExist: 68 | if verbosity > 0: 69 | print("*" * 80) 70 | print( 71 | "Creating test user -- login: %s, password: %s" 72 | % (USERNAME, PASSWORD) 73 | ) 74 | print("*" * 80) 75 | assert User.objects.create_superuser(USERNAME, EMAIL, PASSWORD) 76 | else: 77 | if verbosity > 0: 78 | print( 79 | "Test user already exists. -- login: %s, password: %s" 80 | % (USERNAME, PASSWORD) 81 | ) 82 | 83 | post_migrate.disconnect( 84 | create_superuser, 85 | sender="django.contrib.auth.models", 86 | dispatch_uid="django.contrib.auth.management.create_superuser", 87 | ) 88 | post_migrate.connect( 89 | create_testuser, 90 | sender="django.contrib.auth.models", 91 | dispatch_uid="common.models.create_testuser", 92 | ) 93 | -------------------------------------------------------------------------------- /tests/blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdalisue/django-author/210e45b2468bd64e3ca34c90d1b8abc1acd65e37/tests/blog/__init__.py -------------------------------------------------------------------------------- /tests/blog/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # admin.py 3 | 4 | from django.contrib import admin 5 | 6 | from .models import Entry 7 | 8 | 9 | admin.site.register(Entry, admin.ModelAdmin) 10 | -------------------------------------------------------------------------------- /tests/blog/fixtures/test.yaml: -------------------------------------------------------------------------------- 1 | - model: auth.user 2 | pk: 1 3 | fields: 4 | username: admin 5 | email: admin@test.com 6 | password: sha1$588be$494b93325fbfd299a39691b4dddef29ac0ed3008 7 | is_active: true 8 | is_superuser: true 9 | is_staff: true 10 | - model: auth.user 11 | pk: 2 12 | fields: 13 | username: foo 14 | email: foo@test.com 15 | password: sha1$588be$494b93325fbfd299a39691b4dddef29ac0ed3008 16 | is_active: true 17 | is_superuser: false 18 | is_staff: false 19 | - model: auth.user 20 | pk: 3 21 | fields: 22 | username: bar 23 | email: bar@test.com 24 | password: sha1$588be$494b93325fbfd299a39691b4dddef29ac0ed3008 25 | is_active: true 26 | is_superuser: false 27 | is_staff: false 28 | - model: blog.entry 29 | pk: 1 30 | fields: 31 | title: foo 32 | body: foo 33 | created_at: 2000-01-01 34 | updated_at: 2000-01-01 35 | - model: blog.entry 36 | pk: 2 37 | fields: 38 | title: bar 39 | body: bar 40 | created_at: 2000-01-01 41 | updated_at: 2000-01-01 42 | - model: blog.entry 43 | pk: 3 44 | fields: 45 | title: hoge 46 | body: hoge 47 | created_at: 2000-01-01 48 | updated_at: 2000-01-01 49 | -------------------------------------------------------------------------------- /tests/blog/forms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: set fileencoding=utf8: 3 | """ 4 | Mini blog forms 5 | 6 | 7 | AUTHOR: 8 | lambdalisue[Ali su ae] (lambdalisue@hashnote.net) 9 | 10 | Copyright: 11 | Copyright 2011 Alisue allright reserved. 12 | 13 | License: 14 | Licensed under the Apache License, Version 2.0 (the "License"); 15 | you may not use this file except in compliance with the License. 16 | You may obtain a copy of the License at 17 | 18 | http://www.apache.org/licenses/LICENSE-2.0 19 | 20 | Unliss required by applicable law or agreed to in writing, software 21 | distributed under the License is distrubuted on an "AS IS" BASICS, 22 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 23 | See the License for the specific language governing permissions and 24 | limitations under the License. 25 | """ 26 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 27 | from django import forms 28 | 29 | from .models import Entry 30 | 31 | 32 | class EntryForm(forms.ModelForm): 33 | class Meta: 34 | model = Entry 35 | fields = ("title", "body") 36 | -------------------------------------------------------------------------------- /tests/blog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="Entry", 17 | fields=[ 18 | ( 19 | "id", 20 | models.AutoField( 21 | verbose_name="ID", 22 | serialize=False, 23 | auto_created=True, 24 | primary_key=True, 25 | ), 26 | ), 27 | ( 28 | "title", 29 | models.CharField(unique=True, max_length=50, verbose_name="title"), 30 | ), 31 | ("body", models.TextField(verbose_name="body")), 32 | ( 33 | "created_at", 34 | models.DateTimeField( 35 | auto_now_add=True, verbose_name="date and time created" 36 | ), 37 | ), 38 | ( 39 | "updated_at", 40 | models.DateTimeField( 41 | auto_now=True, verbose_name="date and time updated" 42 | ), 43 | ), 44 | ( 45 | "author", 46 | models.ForeignKey( 47 | related_name="entry_create", 48 | verbose_name="author", 49 | blank=True, 50 | to=settings.AUTH_USER_MODEL, 51 | null=True, 52 | on_delete=models.SET_NULL, 53 | ), 54 | ), 55 | ( 56 | "updated_by", 57 | models.ForeignKey( 58 | related_name="entry_update", 59 | verbose_name="last updated by", 60 | blank=True, 61 | to=settings.AUTH_USER_MODEL, 62 | null=True, 63 | on_delete=models.SET_NULL, 64 | ), 65 | ), 66 | ], 67 | options={}, 68 | bases=(models.Model,), 69 | ), 70 | ] 71 | -------------------------------------------------------------------------------- /tests/blog/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdalisue/django-author/210e45b2468bd64e3ca34c90d1b8abc1acd65e37/tests/blog/migrations/__init__.py -------------------------------------------------------------------------------- /tests/blog/models.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf8: 2 | """ 3 | Mini blog models 4 | 5 | AUTHOR: 6 | lambdalisue[Ali su ae] (lambdalisue@hashnote.net) 7 | 8 | Copyright: 9 | Copyright 2011 Alisue allright reserved. 10 | 11 | License: 12 | Licensed under the Apache License, Version 2.0 (the "License"); 13 | you may not use this file except in compliance with the License. 14 | You may obtain a copy of the License at 15 | 16 | http://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unliss required by applicable law or agreed to in writing, software 19 | distributed under the License is distrubuted on an "AS IS" BASICS, 20 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | See the License for the specific language governing permissions and 22 | limitations under the License. 23 | """ 24 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 25 | __VERSION__ = "0.1.0" 26 | 27 | from django.db import models 28 | from django.urls import reverse 29 | from django.utils.translation import gettext_lazy as _ 30 | 31 | from author.decorators import with_author 32 | 33 | 34 | # This class decorator (@with_author) is all you need to add `author` and `updated_by` field 35 | # to particular model. 36 | # If you are using Python under 2.4, use the following code insted:: 37 | # 38 | # class Entry(models.Mode): 39 | # # brabrabra 40 | # Entry = with_author(Entry) 41 | # 42 | @with_author 43 | class Entry(models.Model): 44 | """mini blog entry model 45 | 46 | >>> entry = Entry() 47 | 48 | # Attribute test 49 | >>> assert hasattr(entry, 'title') 50 | >>> assert hasattr(entry, 'body') 51 | >>> assert hasattr(entry, 'created_at') 52 | >>> assert hasattr(entry, 'updated_at') 53 | 54 | # Function test 55 | >>> assert callable(getattr(entry, '__unicode__')) 56 | >>> assert callable(getattr(entry, 'get_absolute_url')) 57 | """ 58 | 59 | title = models.CharField(_("title"), max_length=50, unique=True) 60 | body = models.TextField(_("body")) 61 | 62 | created_at = models.DateTimeField( 63 | _("date and time created"), 64 | auto_now_add=True, 65 | ) 66 | updated_at = models.DateTimeField( 67 | _("date and time updated"), 68 | auto_now=True, 69 | ) 70 | 71 | def __unicode__(self): 72 | return self.title 73 | 74 | def get_absolute_url(self): 75 | return reverse("blog-entry-detail", None, (), {"slug": self.title}) 76 | 77 | def clean(self): 78 | """custom validation""" 79 | from django.core.exceptions import ValidationError 80 | 81 | if self.title in ("create", "update", "delete"): 82 | raise ValidationError( 83 | """The title cannot be 'create', 'update' or 'delete'""" 84 | ) 85 | -------------------------------------------------------------------------------- /tests/blog/templates/404.html: -------------------------------------------------------------------------------- 1 | 404 - Page Not Found 2 | -------------------------------------------------------------------------------- /tests/blog/templates/blog/entry_confirm_delete.html: -------------------------------------------------------------------------------- 1 | Hello 2 | -------------------------------------------------------------------------------- /tests/blog/templates/blog/entry_detail.html: -------------------------------------------------------------------------------- 1 | Hello 2 | -------------------------------------------------------------------------------- /tests/blog/templates/blog/entry_form.html: -------------------------------------------------------------------------------- 1 | Hello 2 | -------------------------------------------------------------------------------- /tests/blog/templates/blog/entry_list.html: -------------------------------------------------------------------------------- 1 | Hello 2 | -------------------------------------------------------------------------------- /tests/blog/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf8: 2 | """ 3 | short module explanation 4 | 5 | 6 | AUTHOR: 7 | lambdalisue[Ali su ae] (lambdalisue@hashnote.net) 8 | 9 | Copyright: 10 | Copyright 2011 Alisue allright reserved. 11 | 12 | License: 13 | Licensed under the Apache License, Version 2.0 (the "License"); 14 | you may not use this file except in compliance with the License. 15 | You may obtain a copy of the License at 16 | 17 | http://www.apache.org/licenses/LICENSE-2.0 18 | 19 | Unliss required by applicable law or agreed to in writing, software 20 | distributed under the License is distrubuted on an "AS IS" BASICS, 21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | See the License for the specific language governing permissions and 23 | limitations under the License. 24 | """ 25 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 26 | -------------------------------------------------------------------------------- /tests/blog/tests/test_backends.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf8: 2 | """ 3 | Unittest module of models 4 | 5 | 6 | AUTHOR: 7 | Petr Dlouhý (petr.dlouhy@email.cz) 8 | 9 | Copyright: 10 | Copyright 2011 Alisue allright reserved. 11 | 12 | License: 13 | Licensed under the Apache License, Version 2.0 (the "License"); 14 | you may not use this file except in compliance with the License. 15 | You may obtain a copy of the License at 16 | 17 | http://www.apache.org/licenses/LICENSE-2.0 18 | 19 | Unliss required by applicable law or agreed to in writing, software 20 | distributed under the License is distrubuted on an "AS IS" BASICS, 21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | See the License for the specific language governing permissions and 23 | limitations under the License. 24 | """ 25 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 26 | 27 | import settings 28 | from blog import models 29 | from django.contrib.auth.hashers import make_password 30 | from django.contrib.auth.models import User 31 | from django.core.exceptions import ImproperlyConfigured 32 | from django.test import TestCase 33 | from django.test.utils import override_settings 34 | 35 | 36 | class AuthorBackendTestCase(TestCase): 37 | @override_settings( 38 | MIDDLEWARE=settings.BASE_MIDDLEWARES, 39 | MIDDLEWARE_CLASSES=settings.BASE_MIDDLEWARES, 40 | ) 41 | def test_improperly_configured(self): 42 | """Test, that if author backend is missing, it throws error""" 43 | entry = models.Entry(title="foo", body="bar") 44 | with self.assertRaisesRegex( 45 | ImproperlyConfigured, 46 | r'Error "author.middlewares.AuthorDefaultBackendMiddleware" is not found ' 47 | "in MIDDLEWARE_CLASSES nor MIDDLEWARE. It is required to use AuthorDefaultBackend", 48 | ): 49 | entry.save() 50 | 51 | 52 | @override_settings( 53 | AUTHOR_BACKEND="author.backends.AuthorSystemUserBackend", 54 | ) 55 | class AuthorSystemUserBackendTestCase(TestCase): 56 | def test_save(self): 57 | """Test that AuthorSystemBackend saves with default user""" 58 | user = User.objects.create(pk=1) 59 | entry = models.Entry(title="foo", body="bar") 60 | entry.save() 61 | self.assertEqual(entry.author, user) 62 | 63 | def test_with_request(self): 64 | admin = User.objects.create( 65 | pk=1, username="admin", password=make_password("password") 66 | ) 67 | try: # Django >= 1.9 68 | self.client.force_login(admin) 69 | except AttributeError: 70 | assert self.client.login(username="admin", password="password") 71 | response = self.client.post( 72 | "/create/", 73 | { 74 | "title": "barbar", 75 | "body": "barbar", 76 | }, 77 | ) 78 | # if post success, redirect occur 79 | self.assertEqual(response.status_code, 302) 80 | 81 | entry = models.Entry.objects.get(title="barbar") 82 | self.assertEqual(entry.author, admin) 83 | self.assertEqual(entry.updated_by, admin) 84 | 85 | 86 | class AuthorBackendSettingsTestCase(TestCase): 87 | @override_settings( 88 | AUTHOR_BACKEND="author.backends.FooBackend", 89 | ) 90 | def test_unexistent_backend(self): 91 | entry = models.Entry(title="foo", body="bar") 92 | with self.assertRaisesRegex( 93 | ImproperlyConfigured, 94 | 'Module "author.backends" does not define a "FooBackend" author backend', 95 | ): 96 | entry.save() 97 | 98 | @override_settings( 99 | AUTHOR_BACKEND=1234, 100 | ) 101 | def test_wrong_class(self): 102 | entry = models.Entry(title="foo", body="bar") 103 | with self.assertRaisesRegex( 104 | ImproperlyConfigured, 105 | 'Error author backend must have "get_user" method Please define it in 1234', 106 | ): 107 | entry.save() 108 | 109 | @override_settings( 110 | AUTHOR_BACKEND="foo", 111 | ) 112 | def test_error_importing(self): 113 | entry = models.Entry(title="foo", body="bar") 114 | with self.assertRaisesRegex( 115 | ImproperlyConfigured, 116 | r'Error importing author backend foo: "No module named \'?fo\'?', 117 | ): 118 | entry.save() 119 | 120 | @override_settings( 121 | AUTHOR_BACKEND=".", 122 | ) 123 | def test_value_error(self): 124 | entry = models.Entry(title="foo", body="bar") 125 | with self.assertRaisesRegex( 126 | ImproperlyConfigured, 127 | "Error importing author backend. Is AUTHOR_BACKEND a correctly defined?", 128 | ): 129 | entry.save() 130 | -------------------------------------------------------------------------------- /tests/blog/tests/test_models.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf8: 2 | """ 3 | Unittest module of models 4 | 5 | 6 | AUTHOR: 7 | lambdalisue[Ali su ae] (lambdalisue@hashnote.net) 8 | 9 | Copyright: 10 | Copyright 2011 Alisue allright reserved. 11 | 12 | License: 13 | Licensed under the Apache License, Version 2.0 (the "License"); 14 | you may not use this file except in compliance with the License. 15 | You may obtain a copy of the License at 16 | 17 | http://www.apache.org/licenses/LICENSE-2.0 18 | 19 | Unliss required by applicable law or agreed to in writing, software 20 | distributed under the License is distrubuted on an "AS IS" BASICS, 21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | See the License for the specific language governing permissions and 23 | limitations under the License. 24 | """ 25 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 26 | 27 | from blog import models 28 | from django.test import TestCase 29 | 30 | 31 | class EntryModelTestCase(TestCase): 32 | def test_creation(self): 33 | """blog.Entry: creation works correctly""" 34 | entry = models.Entry(title="foo", body="bar") 35 | entry.full_clean() 36 | self.assertEqual(entry.title, "foo") 37 | self.assertEqual(entry.body, "bar") 38 | 39 | entry.save() 40 | entry = models.Entry.objects.get(pk=entry.pk) 41 | self.assertEqual(entry.title, "foo") 42 | self.assertEqual(entry.body, "bar") 43 | 44 | def test_modification(self): 45 | """blog.Entry: modification works correctly""" 46 | entry = models.Entry(title="foo", body="bar") 47 | entry.full_clean() 48 | entry.save() 49 | 50 | entry.title = "foofoo" 51 | entry.body = "barbar" 52 | entry.full_clean() 53 | entry.save() 54 | entry = models.Entry.objects.get(pk=entry.pk) 55 | self.assertEqual(entry.title, "foofoo") 56 | self.assertEqual(entry.body, "barbar") 57 | 58 | def test_validation(self): 59 | """blog.Entry: validation works correctly""" 60 | from django.core.exceptions import ValidationError 61 | 62 | entry = models.Entry(title="foo", body="bar") 63 | entry.full_clean() 64 | entry.save() 65 | 66 | entry.title = "" 67 | self.assertRaises(ValidationError, entry.full_clean) 68 | 69 | entry.body = "" 70 | self.assertRaises(ValidationError, entry.full_clean) 71 | 72 | entry.title = "*" * 100 73 | self.assertRaises(ValidationError, entry.full_clean) 74 | 75 | entry.title = "!#$%&()" 76 | self.assertRaises(ValidationError, entry.full_clean) 77 | 78 | def test_deletion(self): 79 | """blog.Entry: deletion works correctly""" 80 | entry = models.Entry(title="foo", body="bar") 81 | entry.full_clean() 82 | entry.save() 83 | 84 | num = models.Entry.objects.all().count() 85 | entry.delete() 86 | self.assertEqual(models.Entry.objects.all().count(), num - 1) 87 | 88 | def test_with_author(self): 89 | """blog.Entry: with_author works correctly""" 90 | entry = models.Entry(title="foo", body="bar") 91 | entry.full_clean() 92 | entry.save() 93 | 94 | # None for AnonymousUser (AuthorDefaultBackend) 95 | self.assertEqual(entry.author, None) 96 | self.assertEqual(entry.updated_by, None) 97 | -------------------------------------------------------------------------------- /tests/blog/tests/test_receivers.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf8: 2 | """ 3 | Unittest module of receivers 4 | 5 | 6 | AUTHOR: 7 | lambdalisue[Ali su ae] (lambdalisue@hashnote.net) 8 | 9 | Copyright: 10 | Copyright 2011 Alisue allright reserved. 11 | 12 | License: 13 | Licensed under the Apache License, Version 2.0 (the "License"); 14 | you may not use this file except in compliance with the License. 15 | You may obtain a copy of the License at 16 | 17 | http://www.apache.org/licenses/LICENSE-2.0 18 | 19 | Unliss required by applicable law or agreed to in writing, software 20 | distributed under the License is distrubuted on an "AS IS" BASICS, 21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | See the License for the specific language governing permissions and 23 | limitations under the License. 24 | """ 25 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 26 | 27 | from blog import models 28 | from django.contrib.auth.models import User 29 | from django.db.models.signals import pre_save 30 | from django.test import TestCase 31 | from django.test.utils import override_settings 32 | from mock import patch 33 | 34 | from author import receivers 35 | 36 | 37 | class TestPreSaveCallback(TestCase): 38 | @patch("author.backends.AuthorDefaultBackend.get_user") 39 | @override_settings( 40 | AUTHOR_IGNORE_MODELS=["blog.entry"], 41 | ) 42 | def test_ignore_models(self, get_user): 43 | """blog.Entry: setting author on ignored models doesn't do anything""" 44 | user = User.objects.create() 45 | get_user.return_value = user 46 | instance = models.Entry.objects.create() 47 | receivers.pre_save_callback(None, instance) 48 | self.assertEqual(instance.author, None) 49 | self.assertEqual(instance.updated_by, None) 50 | 51 | @patch("author.backends.AuthorDefaultBackend.get_user") 52 | @override_settings( 53 | AUTHOR_CREATED_BY_FIELD_NAME="foo", 54 | ) 55 | def test_ignore_models_created_field(self, get_user): 56 | """blog.Entry: setting author on nonexistent field doesn't do anything""" 57 | user = User.objects.create() 58 | get_user.return_value = user 59 | instance = models.Entry.objects.create() 60 | receivers.pre_save_callback(None, instance) 61 | self.assertEqual(instance.author, None) 62 | self.assertEqual(instance.updated_by, None) 63 | 64 | def test_callback_no_user(self): 65 | """blog.Entry: setting author is ignored""" 66 | instance = models.Entry.objects.create() 67 | receivers.pre_save_callback(None, instance) 68 | self.assertEqual(instance.author, None) 69 | self.assertEqual(instance.updated_by, None) 70 | 71 | @patch("author.backends.AuthorDefaultBackend.get_user") 72 | def test_callback(self, get_user): 73 | """blog.Entry: callbacks runned""" 74 | user = User.objects.create() 75 | get_user.return_value = user 76 | instance = models.Entry.objects.create() 77 | receivers.pre_save_callback(None, instance) 78 | self.assertEqual(instance.author, user) 79 | self.assertEqual(instance.updated_by, user) 80 | 81 | @patch("author.backends.AuthorDefaultBackend.get_user") 82 | def test_callback_no_updated_by(self, get_user): 83 | """blog.Entry: callbacks runned""" 84 | user = User.objects.create() 85 | get_user.return_value = user 86 | instance = models.Entry.objects.create() 87 | instance._change_updated_by = False 88 | receivers.pre_save_callback(None, instance) 89 | self.assertEqual(instance.author, user) 90 | self.assertEqual(instance.updated_by, None) 91 | 92 | 93 | class TestBlankSettingsTestCase(TestCase): 94 | @override_settings( 95 | AUTHOR_MODELS=["auth.user"], 96 | ) 97 | def test_author_models_settings_blank(self): 98 | """blog.Entry: callbacks are not created""" 99 | pre_save.disconnect(receivers.pre_save_callback) 100 | self.assertEqual(pre_save._live_receivers(models.Entry), []) 101 | receivers.register() 102 | self.assertEqual(pre_save._live_receivers(models.Entry), []) 103 | 104 | 105 | class TestSettingsTestCase(TestCase): 106 | @override_settings( 107 | AUTHOR_MODELS=["blog.entry"], 108 | ) 109 | def test_author_models_settings(self): 110 | """blog.Entry: callbacks are created""" 111 | self.assertEqual(pre_save._live_receivers(models.Entry), []) 112 | receivers.register() 113 | self.assertEqual( 114 | pre_save._live_receivers(models.Entry)[0], receivers.pre_save_callback 115 | ) 116 | -------------------------------------------------------------------------------- /tests/blog/tests/test_settings.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf8: 2 | """ 3 | Unittest module of settings 4 | 5 | 6 | AUTHOR: 7 | Petr Dlouhý (petr.dlouhy@email.cz) 8 | 9 | Copyright: 10 | Copyright 2011 Alisue allright reserved. 11 | 12 | License: 13 | Licensed under the Apache License, Version 2.0 (the "License"); 14 | you may not use this file except in compliance with the License. 15 | You may obtain a copy of the License at 16 | 17 | http://www.apache.org/licenses/LICENSE-2.0 18 | 19 | Unliss required by applicable law or agreed to in writing, software 20 | distributed under the License is distrubuted on an "AS IS" BASICS, 21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | See the License for the specific language governing permissions and 23 | limitations under the License. 24 | """ 25 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 26 | 27 | from django.test import TestCase 28 | 29 | from author.conf import settings 30 | 31 | 32 | class AuthorSettingsTestCase(TestCase): 33 | """Test the lazy settings provide defaults and access to django settings""" 34 | 35 | def test_a_django_setting(self): 36 | """Test the lazy settings fallback to django settings""" 37 | time_zone = settings.TIME_ZONE 38 | self.assertEqual(time_zone, "America/Chicago") 39 | 40 | def test_author_created_by_field(self): 41 | """Test the default author created by field name""" 42 | field_name = settings.AUTHOR_CREATED_BY_FIELD_NAME 43 | self.assertEqual(field_name, "author") 44 | 45 | def test_author_updated_by_field(self): 46 | """Test the default author updated by field name""" 47 | field_name = settings.AUTHOR_UPDATED_BY_FIELD_NAME 48 | self.assertEqual(field_name, "updated_by") 49 | 50 | def test_author_not_update(self): 51 | """Test the default author 'do not updated while user is none' setting""" 52 | not_update = settings.AUTHOR_DO_NOT_UPDATE_WHILE_USER_IS_NONE 53 | self.assertTrue(not_update) 54 | 55 | def test_author_models(self): 56 | """Test the default author models""" 57 | author_models = settings.AUTHOR_MODELS 58 | self.assertIsNone(author_models) 59 | 60 | def test_author_ignore_models(self): 61 | """Test the default ignored models""" 62 | ignore_models = settings.AUTHOR_IGNORE_MODELS 63 | expected_ignored = [ 64 | "auth.user", 65 | "auth.group", 66 | "auth.permission", 67 | "contenttypes.contenttype", 68 | ] 69 | self.assertEqual(ignore_models, expected_ignored) 70 | 71 | def test_unknown_setting(self): 72 | """Test an unknown setting""" 73 | with self.assertRaises(AttributeError): 74 | _ = settings.UNKNOWN_SETTING 75 | -------------------------------------------------------------------------------- /tests/blog/tests/test_views.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf8: 2 | """ 3 | Unittest module of ... 4 | 5 | 6 | AUTHOR: 7 | lambdalisue[Ali su ae] (lambdalisue@hashnote.net) 8 | 9 | Copyright: 10 | Copyright 2011 Alisue allright reserved. 11 | 12 | License: 13 | Licensed under the Apache License, Version 2.0 (the "License"); 14 | you may not use this file except in compliance with the License. 15 | You may obtain a copy of the License at 16 | 17 | http://www.apache.org/licenses/LICENSE-2.0 18 | 19 | Unliss required by applicable law or agreed to in writing, software 20 | distributed under the License is distrubuted on an "AS IS" BASICS, 21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | See the License for the specific language governing permissions and 23 | limitations under the License. 24 | """ 25 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 26 | from django.test import TestCase 27 | 28 | 29 | class EntryViewTestCase(TestCase): 30 | fixtures = ["test.yaml"] 31 | 32 | def test_list(self): 33 | response = self.client.get("/") 34 | self.assertEqual(response.status_code, 200) 35 | 36 | def test_detail(self): 37 | response = self.client.get("/foo/") 38 | self.assertEqual(response.status_code, 200) 39 | 40 | def test_create(self): 41 | response = self.client.get("/create/") 42 | self.assertEqual(response.status_code, 200) 43 | 44 | def test_update(self): 45 | response = self.client.get("/update/1/") 46 | self.assertEqual(response.status_code, 200) 47 | 48 | def test_delete(self): 49 | response = self.client.get("/delete/1/") 50 | self.assertEqual(response.status_code, 200) 51 | 52 | def test_with_author(self): 53 | from blog import models 54 | from django.contrib.auth.models import User 55 | 56 | response = self.client.post( 57 | "/create/", 58 | { 59 | "title": "foo", 60 | "body": "foo", 61 | }, 62 | ) 63 | self.assertEqual(response.status_code, 200) 64 | 65 | entry = models.Entry.objects.get(title="foo") 66 | self.assertEqual(entry.author, None) 67 | self.assertEqual(entry.updated_by, None) 68 | 69 | try: # Django >= 1.9 70 | self.client.force_login(User.objects.get(username="admin")) 71 | except AttributeError: 72 | assert self.client.login(username="admin", password="password") 73 | response = self.client.post( 74 | "/create/", 75 | { 76 | "title": "barbar", 77 | "body": "barbar", 78 | }, 79 | ) 80 | # if post success, redirect occur 81 | self.assertEqual(response.status_code, 302) 82 | 83 | admin = User.objects.get(username="admin") 84 | entry = models.Entry.objects.get(title="barbar") 85 | self.assertEqual(entry.author, admin) 86 | self.assertEqual(entry.updated_by, admin) 87 | 88 | self.client.logout() 89 | try: # Django >= 1.9 90 | self.client.force_login(User.objects.get(username="foo")) 91 | except AttributeError: 92 | assert self.client.login(username="foo", password="password") 93 | response = self.client.post( 94 | "/update/%d/" % entry.pk, 95 | { 96 | "title": "barbarbar", 97 | "body": "barbarbar", 98 | }, 99 | ) 100 | # if post success, redirect occur 101 | self.assertEqual(response.status_code, 302) 102 | 103 | foo = User.objects.get(username="foo") 104 | entry = models.Entry.objects.get(pk=entry.pk) 105 | self.assertEqual(entry.author, admin) 106 | self.assertEqual(entry.updated_by, foo) 107 | 108 | def test_failed_request(self): 109 | """ 110 | Test for problem when two consequent tests failed. 111 | If the request is created, then user is logged off 112 | and another object is created in the same session, it failed. 113 | """ 114 | from blog import models 115 | from django.contrib.auth.models import User 116 | 117 | try: # Django >= 1.9 118 | self.client.force_login(User.objects.get(username="admin")) 119 | except AttributeError: 120 | assert self.client.login(username="admin", password="password") 121 | response = self.client.post( 122 | "/create/", 123 | { 124 | "title": "barbar", 125 | "body": "barbar", 126 | }, 127 | ) 128 | # if post success, redirect occur 129 | self.assertEqual(response.status_code, 302) 130 | 131 | self.client.logout() 132 | User.objects.get(username="admin").delete() 133 | models.Entry.objects.create(title="barbar1") 134 | entry = models.Entry.objects.get(title="barbar1") 135 | self.assertEqual(entry.author, None) 136 | self.assertEqual(entry.updated_by, None) 137 | -------------------------------------------------------------------------------- /tests/blog/urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: set fileencoding=utf8: 3 | """ 4 | Mini blog urls 5 | 6 | 7 | AUTHOR: 8 | lambdalisue[Ali su ae] (lambdalisue@hashnote.net) 9 | 10 | Copyright: 11 | Copyright 2011 Alisue allright reserved. 12 | 13 | License: 14 | Licensed under the Apache License, Version 2.0 (the "License"); 15 | you may not use this file except in compliance with the License. 16 | You may obtain a copy of the License at 17 | 18 | http://www.apache.org/licenses/LICENSE-2.0 19 | 20 | Unliss required by applicable law or agreed to in writing, software 21 | distributed under the License is distrubuted on an "AS IS" BASICS, 22 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 23 | See the License for the specific language governing permissions and 24 | limitations under the License. 25 | """ 26 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 27 | from django.urls import path 28 | 29 | from . import views 30 | 31 | 32 | urlpatterns = ( 33 | path("", views.EntryListView.as_view(), name="blog-entry-list"), 34 | path( 35 | "create/", 36 | views.EntryCreateView.as_view(), 37 | name="blog-entry-create", 38 | ), 39 | path( 40 | "update//", 41 | views.EntryUpdateView.as_view(), 42 | name="blog-entry-update", 43 | ), 44 | path( 45 | "delete//", 46 | views.EntryDeleteView.as_view(), 47 | name="blog-entry-delete", 48 | ), 49 | path( 50 | "/", 51 | views.EntryDetailView.as_view(), 52 | name="blog-entry-detail", 53 | ), 54 | ) 55 | -------------------------------------------------------------------------------- /tests/blog/views.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: set fileencoding=utf8: 3 | """ 4 | Mini blog views 5 | 6 | 7 | AUTHOR: 8 | lambdalisue[Ali su ae] (lambdalisue@hashnote.net) 9 | 10 | Copyright: 11 | Copyright 2011 Alisue allright reserved. 12 | 13 | License: 14 | Licensed under the Apache License, Version 2.0 (the "License"); 15 | you may not use this file except in compliance with the License. 16 | You may obtain a copy of the License at 17 | 18 | http://www.apache.org/licenses/LICENSE-2.0 19 | 20 | Unliss required by applicable law or agreed to in writing, software 21 | distributed under the License is distrubuted on an "AS IS" BASICS, 22 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 23 | See the License for the specific language governing permissions and 24 | limitations under the License. 25 | """ 26 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 27 | from django.views.generic import ( 28 | CreateView, 29 | DeleteView, 30 | DetailView, 31 | ListView, 32 | UpdateView, 33 | ) 34 | 35 | 36 | try: # Django >= 1.10 37 | from django.urls import reverse 38 | except ImportError: 39 | from django.core.urlresolvers import reverse 40 | 41 | from .forms import EntryForm 42 | from .models import Entry 43 | 44 | 45 | class EntryListView(ListView): 46 | model = Entry 47 | 48 | 49 | class EntryDetailView(DetailView): 50 | model = Entry 51 | slug_field = "title" 52 | 53 | 54 | class EntryCreateView(CreateView): 55 | form_class = EntryForm 56 | model = Entry 57 | 58 | 59 | class EntryUpdateView(UpdateView): 60 | form_class = EntryForm 61 | model = Entry 62 | 63 | 64 | class EntryDeleteView(DeleteView): 65 | model = Entry 66 | 67 | def get_success_url(self): 68 | return reverse("blog-entry-list") 69 | -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | 6 | if __name__ == "__main__": 7 | os.environ.setdefault( 8 | "DJANGO_SETTINGS_MODULE", 9 | "settings", 10 | ) 11 | 12 | from django.core.management import execute_from_command_line 13 | 14 | execute_from_command_line(sys.argv) 15 | -------------------------------------------------------------------------------- /tests/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # vim: set fileencoding=utf8: 3 | """ 4 | short module explanation 5 | 6 | 7 | AUTHOR: 8 | lambdalisue[Ali su ae] (lambdalisue@hashnote.net) 9 | 10 | Copyright: 11 | Copyright 2011 Alisue allright reserved. 12 | 13 | License: 14 | Licensed under the Apache License, Version 2.0 (the "License"); 15 | you may not use this file except in compliance with the License. 16 | You may obtain a copy of the License at 17 | 18 | http://www.apache.org/licenses/LICENSE-2.0 19 | 20 | Unliss required by applicable law or agreed to in writing, software 21 | distributed under the License is distrubuted on an "AS IS" BASICS, 22 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 23 | See the License for the specific language governing permissions and 24 | limitations under the License. 25 | """ 26 | __AUTHOR__ = "lambdalisue (lambdalisue@hashnote.net)" 27 | import os 28 | import sys 29 | 30 | 31 | os.environ["DJANGO_SETTINGS_MODULE"] = "settings" 32 | test_dir = os.path.dirname(__file__) 33 | sys.path.insert(0, test_dir) 34 | 35 | import django # noqa 36 | from django.conf import settings # noqa 37 | from django.test.utils import get_runner # noqa 38 | 39 | 40 | def runtests(verbosity=1, interactive=True): 41 | TestRunner = get_runner(settings) 42 | test_runner = TestRunner(verbosity=verbosity, interactive=interactive) 43 | django.setup() 44 | failures = test_runner.run_tests([]) 45 | sys.exit(bool(failures)) 46 | 47 | 48 | if __name__ == "__main__": 49 | runtests() 50 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for weblog project. 2 | import os 3 | import sys 4 | 5 | 6 | ROOT = os.path.dirname(__file__) 7 | 8 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 9 | 10 | 11 | DEBUG = True 12 | 13 | ADMINS = ( 14 | # ('Your Name', 'your_email@example.com'), 15 | ) 16 | 17 | MANAGERS = ADMINS 18 | 19 | DATABASES = { 20 | "default": { 21 | "ENGINE": "django.db.backends.sqlite3", 22 | "NAME": os.path.join(ROOT, "database.db"), 23 | "USER": "", 24 | "PASSWORD": "", 25 | "HOST": "", 26 | "PORT": "", 27 | }, 28 | } 29 | 30 | # Local time zone for this installation. Choices can be found here: 31 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 32 | # although not all choices may be available on all operating systems. 33 | # On Unix systems, a value of None will cause Django to use the same 34 | # timezone as the operating system. 35 | # If running in a Windows environment this must be set to the same as your 36 | # system time zone. 37 | TIME_ZONE = "America/Chicago" 38 | 39 | # Language code for this installation. All choices can be found here: 40 | # http://www.i18nguy.com/unicode/language-identifiers.html 41 | LANGUAGE_CODE = "en-us" 42 | 43 | SITE_ID = 1 44 | 45 | # If you set this to False, Django will make some optimizations so as not 46 | # to load the internationalization machinery. 47 | USE_I18N = True 48 | 49 | # If you set this to False, Django will not format dates, numbers and 50 | # calendars according to the current locale 51 | USE_L10N = True 52 | 53 | # Absolute filesystem path to the directory that will hold user-uploaded files. 54 | # Example: "/home/media/media.lawrence.com/media/" 55 | MEDIA_ROOT = "" 56 | 57 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 58 | # trailing slash. 59 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 60 | MEDIA_URL = "" 61 | 62 | # Absolute path to the directory static files should be collected to. 63 | # Don't put anything in this directory yourself; store your static files 64 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 65 | # Example: "/home/media/media.lawrence.com/static/" 66 | STATIC_ROOT = "" 67 | 68 | # URL prefix for static files. 69 | # Example: "http://media.lawrence.com/static/" 70 | STATIC_URL = "/static/" 71 | 72 | # URL prefix for admin static files -- CSS, JavaScript and images. 73 | # Make sure to use a trailing slash. 74 | # Examples: "http://foo.com/static/admin/", "/static/admin/". 75 | ADMIN_MEDIA_PREFIX = "/static/admin/" 76 | 77 | # Additional locations of static files 78 | STATICFILES_DIRS = ( 79 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 80 | # Always use forward slashes, even on Windows. 81 | # Don't forget to use absolute paths, not relative paths. 82 | os.path.join(ROOT, "static"), 83 | ) 84 | 85 | # List of finder classes that know how to find static files in 86 | # various locations. 87 | STATICFILES_FINDERS = ( 88 | "django.contrib.staticfiles.finders.FileSystemFinder", 89 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 90 | ) 91 | 92 | # Make this unique, and don't share it with anybody. 93 | SECRET_KEY = "4et6(22#@lgie4wogk)6um6^jklpkk0!z-l%uj&kvs*u2xrvfj%" 94 | 95 | TEMPLATES = [ 96 | { 97 | "BACKEND": "django.template.backends.django.DjangoTemplates", 98 | "DIRS": [ 99 | os.path.join(ROOT, "templates"), 100 | ], 101 | "APP_DIRS": True, 102 | "OPTIONS": { 103 | "debug": DEBUG, 104 | "context_processors": ( 105 | "django.contrib.auth.context_processors.auth", 106 | "django.contrib.messages.context_processors.messages", 107 | ), 108 | }, 109 | }, 110 | ] 111 | 112 | BASE_MIDDLEWARES = [ 113 | "django.middleware.common.CommonMiddleware", 114 | "django.contrib.sessions.middleware.SessionMiddleware", 115 | "django.middleware.csrf.CsrfViewMiddleware", 116 | "django.contrib.auth.middleware.AuthenticationMiddleware", 117 | "django.contrib.messages.middleware.MessageMiddleware", 118 | ] 119 | 120 | 121 | MIDDLEWARE_CLASSES = [] 122 | MIDDLEWARE_CLASSES += BASE_MIDDLEWARES 123 | MIDDLEWARE_CLASSES.append("author.middlewares.AuthorDefaultBackendMiddleware") 124 | 125 | MIDDLEWARE = [] 126 | MIDDLEWARE += BASE_MIDDLEWARES 127 | MIDDLEWARE.append("author.middlewares.AuthorDefaultBackendMiddleware") 128 | 129 | ROOT_URLCONF = "urls" 130 | 131 | INSTALLED_APPS = ( 132 | "django.contrib.auth", 133 | "django.contrib.contenttypes", 134 | "django.contrib.sessions", 135 | "django.contrib.sites", 136 | "django.contrib.messages", 137 | "django.contrib.staticfiles", 138 | "django.contrib.admin", 139 | "autocmd", 140 | "blog", 141 | "author", 142 | ) 143 | 144 | FIXTURE_DIRS = (os.path.join(ROOT, "fixtures"),) 145 | 146 | LOGIN_REDIRECT_URL = "/" 147 | 148 | # A sample logging configuration. The only tangible logging 149 | # performed by this configuration is to send an email to 150 | # the site admins on every HTTP 500 error. 151 | # See http://docs.djangoproject.com/en/dev/topics/logging for 152 | # more details on how to customize your logging configuration. 153 | LOGGING = { 154 | "version": 1, 155 | "disable_existing_loggers": False, 156 | "handlers": { 157 | "mail_admins": { 158 | "level": "ERROR", 159 | "class": "django.utils.log.AdminEmailHandler", 160 | }, 161 | }, 162 | "loggers": { 163 | "django.request": { 164 | "handlers": ["mail_admins"], 165 | "level": "ERROR", 166 | "propagate": True, 167 | }, 168 | }, 169 | } 170 | 171 | TEST_RUNNER = "django.test.runner.DiscoverRunner" 172 | -------------------------------------------------------------------------------- /tests/static/css/image/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdalisue/django-author/210e45b2468bd64e3ca34c90d1b8abc1acd65e37/tests/static/css/image/edit.png -------------------------------------------------------------------------------- /tests/static/css/image/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lambdalisue/django-author/210e45b2468bd64e3ca34c90d1b8abc1acd65e37/tests/static/css/image/trash.png -------------------------------------------------------------------------------- /tests/static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #3d3d3d; } 3 | 4 | a { 5 | color: #2f4f4f; 6 | text-decoration: none; } 7 | a:hover { 8 | color: #778899; } 9 | 10 | div#container { 11 | position: relative; 12 | width: 980px; 13 | margin: 20px auto; } 14 | 15 | header { 16 | margin: 0 0 50px 0; } 17 | header h1 { 18 | font-family: Georgia, sans-serif; 19 | font-style: normal; 20 | letter-spacing: 0.1em; 21 | word-spacing: 0.6em; 22 | line-height: 1.2em; 23 | text-align: center; } 24 | header h1 a { 25 | color: #3d3d3d; } 26 | header p { 27 | font-size: 1.2em; 28 | font-family: Arial, serif; 29 | text-align: center; 30 | color: #6d6d6d; } 31 | header nav { 32 | position: absolute; 33 | top: 0; 34 | right: 0; } 35 | 36 | section#contents { 37 | border-top: 1px solid #dedede; 38 | border-bottom: 1px solid #dedede; 39 | margin: 20px 0; } 40 | section#contents > h1 { 41 | display: none; } 42 | section#contents:after { 43 | visibility: hidden; 44 | display: block; 45 | font-size: 0; 46 | content: " "; 47 | clear: both; 48 | height: 0; } 49 | section#contents article.entry { 50 | position: relative; 51 | float: left; 52 | width: 460px; 53 | height: 200px; 54 | padding: 10px; 55 | overflow: hidden; } 56 | section#contents article.entry h1 { 57 | display: inline-block; } 58 | section#contents article.entry nav { 59 | display: inline-block; } 60 | section#contents article.entry nav ul { 61 | margin: 0; 62 | padding: 0; } 63 | section#contents article.entry nav ul li { 64 | display: inline-block; } 65 | section#contents article.entry nav ul li a { 66 | display: block; 67 | width: 16px; 68 | height: 16px; 69 | text-indent: -10000px; } 70 | section#contents article.entry nav ul li.entry-update a { 71 | background: url(image/edit.png) no-repeat; } 72 | section#contents article.entry nav ul li.entry-delete a { 73 | background: url(image/trash.png) no-repeat; } 74 | section#contents article.entry p.subscription { 75 | font-size: smaller; 76 | color: #ababab; } 77 | section#contents article#entry { 78 | height: auto; } 79 | 80 | * html section#content { 81 | zoom: 1; } 82 | 83 | /* IE6 */ 84 | *:first-child + html section#content { 85 | zoom: 1; } 86 | 87 | /* IE7 */ 88 | footer { 89 | margin: 30px 0 0 0; 90 | font-size: 0.9em; 91 | color: #ababab; 92 | text-align: center; } 93 | -------------------------------------------------------------------------------- /tests/static/css/style.scss: -------------------------------------------------------------------------------- 1 | body { 2 | color: #3d3d3d; 3 | } 4 | a { 5 | color: #2f4f4f; 6 | text-decoration: none; 7 | &:hover { 8 | color: #778899; 9 | } 10 | } 11 | 12 | div#container{ 13 | position: relative; 14 | width: 980px; 15 | margin: 20px auto; 16 | } 17 | 18 | header{ 19 | margin: 0 0 50px 0; 20 | 21 | h1 { 22 | font-family: Georgia, sans-serif; 23 | font-style: normal; 24 | letter-spacing: 0.1em; 25 | word-spacing: 0.6em; 26 | line-height: 1.2em; 27 | text-align: center; 28 | 29 | a { 30 | color: #3d3d3d; 31 | } 32 | } 33 | p { 34 | font-size: 1.2em; 35 | font-family: Arial, serif; 36 | text-align: center; 37 | color: #6d6d6d; 38 | } 39 | 40 | nav { 41 | position: absolute; 42 | top: 0; 43 | right: 0; 44 | } 45 | } 46 | 47 | section#contents{ 48 | border-top: 1px solid #dedede; 49 | border-bottom: 1px solid #dedede; 50 | margin: 20px 0; 51 | 52 | > h1{ 53 | display: none; 54 | } 55 | &:after{ 56 | visibility: hidden; 57 | display: block; 58 | font-size: 0; 59 | content: " "; 60 | clear: both; 61 | height: 0; 62 | } 63 | article.entry{ 64 | position: relative; 65 | float: left; 66 | width: 460px; 67 | height: 200px; 68 | padding: 10px; 69 | overflow: hidden; 70 | 71 | h1 { 72 | display: inline-block; 73 | } 74 | nav { 75 | display: inline-block; 76 | ul { 77 | margin: 0; 78 | padding: 0; 79 | 80 | li { 81 | display: inline-block; 82 | 83 | a { 84 | display: block; 85 | width: 16px; 86 | height: 16px; 87 | text-indent: -10000px; 88 | } 89 | &.entry-update a { 90 | background: url(image/edit.png) no-repeat; 91 | } 92 | &.entry-delete a { 93 | background: url(image/trash.png) no-repeat; 94 | } 95 | } 96 | } 97 | } 98 | p.subscription{ 99 | font-size: smaller; 100 | color: #ababab; 101 | } 102 | } 103 | article#entry{ 104 | height: auto; 105 | } 106 | } 107 | * html section#content 108 | { zoom: 1; } /* IE6 */ 109 | *:first-child+html section#content 110 | { zoom: 1; } /* IE7 */ 111 | 112 | footer{ 113 | margin: 30px 0 0 0; 114 | font-size: 0.9em; 115 | color: #ababab; 116 | text-align: center; 117 | } 118 | -------------------------------------------------------------------------------- /tests/templates/404.html: -------------------------------------------------------------------------------- 1 | 404 - Page Not Found 2 | -------------------------------------------------------------------------------- /tests/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Mini blog{% endblock %} 6 | 9 | 10 | 11 | 12 |
13 |
14 | 18 | 28 |
29 |
30 | {% block contents %} 31 | {% endblock %} 32 |
33 |
34 |

Copyright © 2011 Alisue, hashnote.net

35 |
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /tests/templates/blog/entry_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block contents %} 4 |

Delete '{{ object }}'

5 | 6 |
7 | {{ object.body|linebreaks|truncatewords_html:100 }} 8 |
9 | 10 |

Are you sure to delete this entry?

11 |
{% csrf_token %} 12 | {{ form.as_p }} 13 |

cancel

14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /tests/templates/blog/entry_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block contents %} 4 |
5 |

{{ object.title }}

6 | {% if user.is_authenticated %} 7 | 13 | {% endif %} 14 | {% if object.author == object.updated_by %} 15 |

Written by {{ object.author }}

16 | {% else %} 17 |

Originally written by {{ object.author }} and updated by {{ object.updated_by|default:'Anonymous' }}

18 | {% endif %} 19 | 20 |
21 | {{ object.body|linebreaks }} 22 |
23 |
24 | {% endblock %} 25 | 26 | -------------------------------------------------------------------------------- /tests/templates/blog/entry_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block contents %} 4 |

{% if object %}{{ object }}{% else %}Create new entry{% endif %}

5 | 6 |
{% csrf_token %} 7 | {{ form.as_p }} 8 |

9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /tests/templates/blog/entry_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block contents %} 4 |

Entry list

5 | 6 |
7 | {% for object in object_list %} 8 |
9 |

{{ object.title }}

10 | {% if user.is_authenticated %} 11 | 17 | {% endif %} 18 | 19 | {% if object.author == object.updated_by %} 20 |

Written by {{ object.author }}

21 | {% else %} 22 |

Originally written by {{ object.author }} and updated by {{ object.updated_by|default:'Anonymous' }}

23 | {% endif %} 24 | 25 |
26 | {{ object.body|linebreaks|truncatewords_html:50 }} 27 |
28 |
29 | {% endfor %} 30 |
31 | {% endblock %} 32 | 33 | -------------------------------------------------------------------------------- /tests/templates/registration/logged_out.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block contents %} 4 |

Logged out

5 |

You are successfully logged out

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /tests/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block contents %} 4 |

Login

5 |
{% csrf_token %} 6 | {{ form.as_p }} 7 |

8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth import login, logout 3 | from django.urls import include, path 4 | 5 | 6 | admin.autodiscover() 7 | 8 | urlpatterns = ( 9 | path("admin/", admin.site.urls), 10 | path( 11 | "registration/login/", 12 | login, 13 | name="login", 14 | ), 15 | path( 16 | "registration/logout/", 17 | logout, 18 | name="logout", 19 | ), 20 | path("", include("blog.urls")), 21 | ) 22 | --------------------------------------------------------------------------------