diff --git a/.coveragerc b/.coveragerc index e702339..fc19d7a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,5 +1,10 @@ +[paths] +source = publisher + [run] -include = */publisher/* +source = + publisher + publisher_tests [report] exclude_lines = @@ -19,6 +24,7 @@ exclude_lines = if __name__ == .__main__.: omit = - */tests/* + *migrations* + .tox/* -show_missing = True \ No newline at end of file +show_missing = True diff --git a/.gitignore b/.gitignore index b83038e..da977d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ *.py[cod] +.cache + +# coverage report: +reports # C extensions *.so @@ -51,4 +55,4 @@ docs/_build # Virtualenv env/ -venv/ \ No newline at end of file +venv/ diff --git a/.travis.yml b/.travis.yml index 3ceab0a..1375254 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,40 +1,54 @@ -sudo: false -language: python +# Config file for automatic testing at travis-ci.org +dist: trusty +language: python python: - - "3.6" - - "3.5" - - "3.4" - - "3.3" - - "2.7" + - 3.6 +sudo: false + +cache: + directories: + - $HOME/.pip-accel + - $HOME/.cache/pip env: - - DJANGO="Django>=1.8,<1.9" - - DJANGO="Django>=1.9,<1.10" - - DJANGO="Django>=1.10,<1.11" - - DJANGO="Django>=1.11,<2.0" + matrix: + # see output from: `tox -l` + - TOXENV=py27-dj18 + - TOXENV=py34-dj18 + - TOXENV=py35-dj18 + - TOXENV=pypy-dj18 + - TOXENV=py27-dj19 + - TOXENV=py27-dj110 + - TOXENV=py27-dj111 + - TOXENV=py34-dj19 + - TOXENV=py34-dj110 + - TOXENV=py34-dj111 + - TOXENV=py35-dj19 + - TOXENV=py35-dj110 + - TOXENV=py35-dj111 + - TOXENV=pypy-dj19 + - TOXENV=pypy-dj110 + - TOXENV=pypy-dj111 + - TOXENV=py36-dj111 + matrix: - exclude: - - env: DJANGO="Django>=1.8,<1.9" - python: "3.6" - - env: DJANGO="Django>=1.8,<1.9" - python: "3.5" - - env: DJANGO="Django>=1.9,<1.10" - python: "3.3" - - env: DJANGO="Django>=1.10,<1.11" - python: "3.3" - - env: DJANGO="Django>=1.11,<2.0" - python: "3.3" - -before_install: pip install --upgrade pip - - -# command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors + fast_finish: true + +before_install: + # work around https://github.com/travis-ci/travis-ci/issues/8363 + - pyenv global system 3.5 + - travis_retry pip install --upgrade pip + install: - - pip install $DJANGO - - pip install -r requirements/dev.txt + # install only the needed package to run tox + # tox will install all needed packages + - travis_retry pip install tox tox-travis python-creole docutils + +script: + - travis_retry ./setup.py tox -# command to run tests, e.g. python setup.py test -script: coverage run --source=publisher tests/manage.py test myapp -after_script: coveralls \ No newline at end of file +after_success: + # https://github.com/codecov/codecov-bash + - bash <(curl -s https://codecov.io/bash) diff --git a/AUTHORS.rst b/AUTHORS.rst index a71b07b..311d405 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -2,8 +2,13 @@ Credits ======= -Development Lead ----------------- +Development Lead (current) +-------------------------- + +* Jens Diemer + +Development Lead (old) +---------------------- * Lee Solway * Ashley Wilson diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 29bcb6e..ec7b990 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -3,7 +3,7 @@ Contributing ============ Contributions are welcome, and they are greatly appreciated! Every -little bit helps, and credit will always be given. +little bit helps, and credit will always be given. You can contribute in many ways: @@ -13,7 +13,7 @@ Types of Contributions Report Bugs ~~~~~~~~~~~ -Report bugs at https://github.com/jp74/django-model-publisher/issues. +Report bugs at https://github.com/wearehoods/django-ya-model-publisher/issues. If you are reporting a bug, please include: @@ -36,14 +36,14 @@ is open to whoever wants to implement it. Write Documentation ~~~~~~~~~~~~~~~~~~~ -django-model-publisher could always use more documentation, whether as part of the -official django-model-publisher docs, in docstrings, or even on the web in blog posts, +django-ya-model-publisher could always use more documentation, whether as part of the +official django-ya-model-publisher docs, in docstrings, or even on the web in blog posts, articles, and such. Submit Feedback ~~~~~~~~~~~~~~~ -The best way to send feedback is to file an issue at https://github.com/jp74/django-model-publisher/issues. +The best way to send feedback is to file an issue at https://github.com/wearehoods/django-ya-model-publisher/issues. If you are proposing a feature: @@ -55,17 +55,17 @@ If you are proposing a feature: Get Started! ------------ -Ready to contribute? Here's how to set up `django-model-publisher` for local development. +Ready to contribute? Here's how to set up `django-ya-model-publisher` for local development. -1. Fork the `django-model-publisher` repo on GitHub. +1. Fork the `django-ya-model-publisher` repo on GitHub. 2. Clone your fork locally:: - $ git clone git@github.com:your_name_here/django-model-publisher.git + $ git clone git@github.com:your_name_here/django-ya-model-publisher.git 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: - $ mkvirtualenv django-model-publisher - $ cd django-model-publisher/ + $ mkvirtualenv django-ya-model-publisher + $ cd django-ya-model-publisher/ $ python setup.py develop 4. Create a branch for local development:: @@ -81,7 +81,7 @@ tests, including testing other Python versions with tox:: $ python tests/manage.py test myapp $ tox -To get flake8 and tox, just pip install them into your virtualenv. +To get flake8 and tox, just pip install them into your virtualenv. 6. Commit your changes and push your branch to GitHub:: @@ -100,8 +100,8 @@ Before you submit a pull request, check that it meets these guidelines: 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. -3. The pull request should work for Python 2.6, 2.7, and 3.3, and for PyPy. Check - https://travis-ci.org/jp74/django-model-publisher/pull_requests +3. The pull request should work for Python 2.6, 2.7, and 3.3, and for PyPy. Check + https://travis-ci.org/wearehoods/django-ya-model-publisher/pull_requests and make sure that the tests pass for all supported Python versions. Tips @@ -109,4 +109,4 @@ Tips To run a subset of tests:: - $ python -m unittest tests.test_publisher \ No newline at end of file + $ python -m unittest tests.test_publisher diff --git a/LICENSE b/LICENSE index 32a95e4..1143886 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ Copyright (c) 2014, JP74 +Copyright (c) 2017, Jens Diemer All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -7,6 +8,6 @@ Redistribution and use in source and binary forms, with or without modification, * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -* Neither the name of django-model-publisher nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Neither the name of django-ya-model-publisher nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in index 5f31475..e3ddbfc 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ include AUTHORS.rst include CONTRIBUTING.rst -include HISTORY.rst include LICENSE -include README.rst -recursive-include publisher *.html *.png *.gif *js *.css *jpg *jpeg *svg *py \ No newline at end of file +include README.creole +recursive-include publisher *.html *.png *js *.css *.py +recursive-include publisher_tests *.py diff --git a/Makefile b/Makefile index 5254208..d0a8c0f 100644 --- a/Makefile +++ b/Makefile @@ -19,37 +19,41 @@ clean-build: rm -fr *.egg-info clean-pyc: - find . -name '*.pyc' -exec rm -f {} + - find . -name '*.pyo' -exec rm -f {} + + find . -type d -name "__pycache__" | xargs --no-run-if-empty rm -Rf + find . -type f -name "*.py[co]" -delete find . -name '*~' -exec rm -f {} + lint: - flake8 django-model-publisher tests + flake8 django-ya-model-publisher tests test: - python tests/manage.py test myapp + python setup.py test -test-all: - tox +tox: + python setup.py tox + +dev_install: clean + pip install -U pip + pip install -r requirements/dev.txt + pip install -e . coverage: - coverage run --source django-model-publisher setup.py test + coverage run --source django-ya-model-publisher setup.py test coverage report -m coverage html open htmlcov/index.html docs: - rm -f docs/django-model-publisher.rst + rm -f docs/django-ya-model-publisher.rst rm -f docs/modules.rst - sphinx-apidoc -o docs/ django-model-publisher + sphinx-apidoc -o docs/ django-ya-model-publisher $(MAKE) -C docs clean $(MAKE) -C docs html open docs/_build/html/index.html -release: clean - python setup.py sdist upload - python setup.py bdist_wheel upload +publish: clean + python setup.py publish sdist: clean python setup.py sdist - ls -l dist \ No newline at end of file + ls -l dist diff --git a/README.creole b/README.creole new file mode 100644 index 0000000..f3cabd2 --- /dev/null +++ b/README.creole @@ -0,0 +1,72 @@ += Django (yet another) Model Publisher + +Django model mixins and utilities for a standard publisher workflow. + +This is a fork of [[https://github.com/andersinno/django-model-publisher-ai|andersinno/django-model-publisher-ai]] witch is a fork of the origin [[https://github.com/wearehoods/django-ya-model-publisher|wearehoods/django-ya-model-publisher]]. + +| {{https://travis-ci.org/wearehoods/django-ya-model-publisher.svg|Build Status on travis-ci.org}} | [[https://travis-ci.org/wearehoods/django-ya-model-publisher/|travis-ci.org/wearehoods/django-ya-model-publisher]] | +| {{https://codecov.io/github/wearehoods/django-ya-model-publisher/coverage.svg|Coverage Status on codecov.io}} | [[https://codecov.io/gh/wearehoods/django-ya-model-publisher|codecov.io/gh/wearehoods/django-ya-model-publisher]] | + + +== Features + +* Django CMS placeholders support. +* Hvad/Parler support. +* Restrict user access to publish functions with user permissions. + + +== Roadmap + +* Implement a "request/reject/accept publishing" workflow with a shot messages and logging + + +== Django compatibility + +|= django-ya-model-publisher |= django version |= python | +| v0.4.x | 1.8, 1.9, 1.10, 1.11 | 2.7, 3.4, 3.5, 3.6 | + +Note: See travis/tox config files for current test matrix + + +== run tests + +run tests via //py.test// with current python/environment: +{{{ +$ make test +or +$ ./setup.py test +or +$ python tests/manage.py test myapp +}}} + +run test via //tox// e.g.: +{{{ +$ make tox +or +$ ./setup.py tox +or +$ tox +}}} + + +== history + +* v0.4.1 - 14.11.2017 +** Refactor test run setup +** bugfix project name +* v0.4.0.dev1 - 14.11.2017 +** Just create the fork and apply all pull requests from [[https://github.com/andersinno/django-model-publisher-ai/pull/14|andersinno/django-model-publisher-ai/pull/14]] + + +== links == + +| Homepage | http://github.com/wearehoods/django-ya-model-publisher +| PyPi.org | https://pypi.org/project/django-ya-model-publisher/ +| PyPi (legacy) | http://pypi.python.org/pypi/django-ya-model-publisher/ + + +== donation == + +* [[https://www.paypal.me/JensDiemer|paypal.me/JensDiemer]] +* [[https://flattr.com/submit/auto?uid=jedie&url=https%3A%2F%2Fgithub.com%2Fwearehoods%2Fdjango-ya-model-publisher%2F|Flattr This!]] +* Send [[http://www.bitcoin.org/|Bitcoins]] to [[https://blockexplorer.com/address/1823RZ5Md1Q2X5aSXRC5LRPcYdveCiVX6F|1823RZ5Md1Q2X5aSXRC5LRPcYdveCiVX6F]] diff --git a/README.rst b/README.rst deleted file mode 100644 index 9b3e9b3..0000000 --- a/README.rst +++ /dev/null @@ -1,39 +0,0 @@ -========================= -Django Model Publisher AI -========================= - -Django model mixins and utilities for a standard publisher workflow. - -This is a fork of `django-model-publisher -`_. - -``django-model-publisher-ai`` supports `Django`_ 1.8 through 1.11 (latest bugfix -release in each series only) on Python 2.7, 3.3 (Django 1.8 only), 3.4, and 3.5. - -.. _Django: http://www.djangoproject.com/ - -This app is available on `PyPI`_. - -.. _PyPI: https://pypi.python.org/pypi/django-model-publisher-ai/ - - -Getting Help -============ - -Documentation for django-model-publisher is available at https://django-model-publisher.readthedocs.org. - - -Quickstart -========== - -Install django-model-publisher-ai: - -``pip install django-model-publisher-ai`` - - -Features -======== - -- Django CMS placeholders support. -- Hvad/Parler support. -- Restrict user access to publish functions with user permissions. diff --git a/docs/conf.py b/docs/conf.py index 17075b9..b8c3680 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,8 +46,8 @@ master_doc = 'index' # General information about the project. -project = u'django-model-publisher' -copyright = u'2014, JP74' +project = u'django-ya-model-publisher' +copyright = u'2014, JP74 ; 2017, Jens Diemer' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -173,7 +173,7 @@ #html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'django-model-publisherdoc' +htmlhelp_basename = 'django-ya-model-publisherdoc' # -- Options for LaTeX output -------------------------------------------------- @@ -192,8 +192,13 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'django-model-publisher.tex', u'django-model-publisher Documentation', - u'JP74', 'manual'), + ( + 'index', + 'django-ya-model-publisher.tex', + u'django-ya-model-publisher Documentation', + u'see AUTHORS.rst', + 'manual' + ), ] # The name of an image file (relative to this directory) to place at the top of @@ -222,8 +227,13 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'django-model-publisher', u'django-model-publisher Documentation', - [u'JP74'], 1) + ( + 'index', + 'django-ya-model-publisher', + u'django-ya-model-publisher Documentation', + [u'see AUTHORS.rst'], + 1 + ) ] # If true, show URL addresses after external links. @@ -236,9 +246,15 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'django-model-publisher', u'django-model-publisher Documentation', - u'JP74', 'django-model-publisher', 'One line description of project.', - 'Miscellaneous'), + ( + 'index', + 'django-ya-model-publisher', + u'django-ya-model-publisher Documentation', + u'see AUTHORS.rst', + 'django-ya-model-publisher', + 'Handy mixin/abstract class for providing a "publisher workflow" to arbitrary Django models.', + 'Miscellaneous' + ), ] # Documents to append as an appendix to all manuals. @@ -251,4 +267,4 @@ #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False \ No newline at end of file +#texinfo_no_detailmenu = False diff --git a/docs/handling_relations.rst b/docs/handling_relations.rst index f430a18..255da3d 100644 --- a/docs/handling_relations.rst +++ b/docs/handling_relations.rst @@ -3,7 +3,7 @@ Handling relations ================== -django-model-publisher does not provide any mechanism to publish related models, as it can be quite specific. Implementing classes wishing to do so will have to override the ``clone_relations()`` method from ``PublisherModelBase``, which takes two arguments: ``src_obj`` (the draft instance), and ``dst_obj`` (the published instance). +django-ya-model-publisher does not provide any mechanism to publish related models, as it can be quite specific. Implementing classes wishing to do so will have to override the ``clone_relations()`` method from ``PublisherModelBase``, which takes two arguments: ``src_obj`` (the draft instance), and ``dst_obj`` (the published instance). Here's a simple example which maintains the relations with a many to many model:: diff --git a/docs/index.rst b/docs/index.rst index 4d4c54a..bbb8a5d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,7 +3,7 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to django-model-publisher's documentation! +Welcome to django-ya-model-publisher's documentation! ================================================================= Contents: diff --git a/docs/installation.rst b/docs/installation.rst index 789cadc..a9dba4c 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -2,9 +2,9 @@ Installation ============ -Install django-model-publisher (using pip):: +Install django-ya-model-publisher (using pip):: - pip install django-model-publisher + pip install django-ya-model-publisher Add it to installed apps in your settings:: diff --git a/docs/readme.rst b/docs/readme.rst index 6b2b3ec..ec29988 100644 --- a/docs/readme.rst +++ b/docs/readme.rst @@ -1 +1,113 @@ -.. include:: ../README.rst \ No newline at end of file +==================================== +Django (yet another) Model Publisher +==================================== + +Django model mixins and utilities for a standard publisher workflow. + +This is a fork of `andersinno/django-model-publisher-ai `_ witch is a fork of the origin `wearehoods/django-ya-model-publisher `_. + ++---------------------------------+-------------------------------------------------------+ +| |Build Status on travis-ci.org| | `travis-ci.org/wearehoods/django-ya-model-publisher`_ | ++---------------------------------+-------------------------------------------------------+ +| |Coverage Status on codecov.io| | `codecov.io/gh/wearehoods/django-ya-model-publisher`_ | ++---------------------------------+-------------------------------------------------------+ + +.. |Build Status on travis-ci.org| image:: https://travis-ci.org/wearehoods/django-ya-model-publisher.svg +.. _travis-ci.org/wearehoods/django-ya-model-publisher: https://travis-ci.org/wearehoods/django-ya-model-publisher/ +.. |Coverage Status on codecov.io| image:: https://codecov.io/github/wearehoods/django-ya-model-publisher/coverage.svg +.. _codecov.io/gh/wearehoods/django-ya-model-publisher: https://codecov.io/gh/wearehoods/django-ya-model-publisher + +-------- +Features +-------- + +* Django CMS placeholders support. + +* Hvad/Parler support. + +* Restrict user access to publish functions with user permissions. + +------- +Roadmap +------- + +* Implement a "request/reject/accept publishing" workflow with a shot messages and logging + +-------------------- +Django compatibility +-------------------- + ++---------------------------+----------------------+--------------------+ +| django-ya-model-publisher | django version | python | ++===========================+======================+====================+ +| v0.4.x | 1.8, 1.9, 1.10, 1.11 | 2.7, 3.4, 3.5, 3.6 | ++---------------------------+----------------------+--------------------+ + +Note: See travis/tox config files for current test matrix + +--------- +run tests +--------- + +run tests via *py.test* with current python/environment: + +:: + + $ make test + or + $ ./setup.py test + or + $ python tests/manage.py test myapp + +run test via *tox* e.g.: + +:: + + $ make tox + or + $ ./setup.py tox + or + $ tox + +------- +history +------- + +* v0.4.1 - 14.11.2017 + + * Refactor test run setup + + * bugfix project name + +* v0.4.0.dev1 - 14.11.2017 + + * Just create the fork and apply all pull requests from `andersinno/django-model-publisher-ai/pull/14 `_ + +----- +links +----- + ++---------------+-----------------------------------------------------------+ +| Homepage | `http://github.com/wearehoods/django-ya-model-publisher`_ | ++---------------+-----------------------------------------------------------+ +| PyPi.org | `https://pypi.org/project/django-ya-model-publisher/`_ | ++---------------+-----------------------------------------------------------+ +| PyPi (legacy) | `http://pypi.python.org/pypi/django-ya-model-publisher/`_ | ++---------------+-----------------------------------------------------------+ + +.. _http://github.com/wearehoods/django-ya-model-publisher: http://github.com/wearehoods/django-ya-model-publisher +.. _https://pypi.org/project/django-ya-model-publisher/: https://pypi.org/project/django-ya-model-publisher/ +.. _http://pypi.python.org/pypi/django-ya-model-publisher/: http://pypi.python.org/pypi/django-ya-model-publisher/ + +-------- +donation +-------- + +* `paypal.me/JensDiemer `_ + +* `Flattr This! `_ + +* Send `Bitcoins `_ to `1823RZ5Md1Q2X5aSXRC5LRPcYdveCiVX6F `_ + + +*(This file is automatically generated by python-creole from ``/README.creole``)* \ No newline at end of file diff --git a/publisher/__init__.py b/publisher/__init__.py index e1424ed..58f3ace 100644 --- a/publisher/__init__.py +++ b/publisher/__init__.py @@ -1 +1 @@ -__version__ = '0.3.1' +from .version import __version__ diff --git a/publisher/admin.py b/publisher/admin.py index 9c3127e..65a3630 100644 --- a/publisher/admin.py +++ b/publisher/admin.py @@ -160,7 +160,7 @@ def publisher_publish(self, obj): def get_queryset(self, request): # hack! We need request.user to check user publish perms self.request = request - qs = self.model.publisher_manager.drafts() + qs = self.model.objects.drafts() ordering = self.get_ordering(request) if ordering: qs = qs.order_by(*ordering) diff --git a/publisher/managers.py b/publisher/managers.py index 814505b..7e15579 100644 --- a/publisher/managers.py +++ b/publisher/managers.py @@ -1,24 +1,64 @@ from django.db import models +from django.db.models import Q +from django.utils import timezone from .signals import publisher_pre_delete from .middleware import get_draft_status -class PublisherManager(models.Manager): - - def contribute_to_class(self, model, name): - super(PublisherManager, self).contribute_to_class(model, name) - models.signals.pre_delete.connect(publisher_pre_delete, model) - +class PublisherQuerySet(models.QuerySet): def drafts(self): from .models import PublisherModelBase return self.filter(publisher_is_draft=PublisherModelBase.STATE_DRAFT) def published(self): + """ + Note: will ignore start/end date! + Use self.visible() to get all publicly accessible entries. + """ from .models import PublisherModelBase - return self.filter(publisher_is_draft=PublisherModelBase.STATE_PUBLISHED) + return self.filter( + publisher_is_draft=PublisherModelBase.STATE_PUBLISHED, + ) + + def visible(self): + """ + Filter all publicly accessible entries. + """ + from .models import PublisherModelBase + return self.filter( + Q(publication_start_date__isnull=True) | Q(publication_start_date__lte=timezone.now()), + Q(publication_end_date__isnull=True) | Q(publication_end_date__gt=timezone.now()), + publisher_is_draft=PublisherModelBase.STATE_PUBLISHED, + ) + + +class BasePublisherManager(models.Manager): + def get_queryset(self): + return PublisherQuerySet(self.model, using=self._db) + + def contribute_to_class(self, model, name): + super(BasePublisherManager, self).contribute_to_class(model, name) + models.signals.pre_delete.connect(publisher_pre_delete, model) def current(self): if get_draft_status(): return self.drafts() return self.published() + + +PublisherManager = BasePublisherManager.from_queryset(PublisherQuerySet) + +try: + from parler.managers import TranslatableManager, TranslatableQuerySet +except ImportError: + pass +else: + class PublisherParlerQuerySet(PublisherQuerySet, TranslatableQuerySet): + pass + + class BasePublisherParlerManager(PublisherManager, TranslatableManager): + def get_queryset(self): + return PublisherParlerQuerySet(self.model, using=self._db) + + PublisherParlerManager = BasePublisherParlerManager.from_queryset(PublisherParlerQuerySet) diff --git a/publisher/models.py b/publisher/models.py index 0d7967a..a595beb 100644 --- a/publisher/models.py +++ b/publisher/models.py @@ -1,6 +1,10 @@ +import logging + from django.utils import timezone from django.db import models from django.core.exceptions import ObjectDoesNotExist +from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone from .managers import PublisherManager from .utils import assert_draft @@ -12,6 +16,8 @@ publisher_post_unpublish, ) +log = logging.getLogger(__name__) + class PublisherModelBase(models.Model): STATE_PUBLISHED = False @@ -33,6 +39,23 @@ class PublisherModelBase(models.Model): publisher_published_at = models.DateTimeField(null=True, editable=False) + publication_start_date = models.DateTimeField( + _("publication start date"), + null=True, blank=True, db_index=True, + help_text=_( + "Published content will only be visible from this point in time." + " Leave blank if always visible." + ) + ) + publication_end_date = models.DateTimeField( + _("publication end date"), + null=True, blank=True, db_index=True, + help_text=_( + "When to expire the published version." + " Leave empty to never expire." + ), + ) + publisher_fields = ( 'publisher_linked', 'publisher_is_draft', @@ -58,8 +81,31 @@ def is_draft(self): @property def is_published(self): + """ + Note: will ignore start/end date! + Use self.is_visible() if you want to know if this entry should be publicly accessible. + """ return self.publisher_is_draft == self.STATE_PUBLISHED + @property + def hidden_by_end_date(self): + if not self.publication_end_date: + return False + return self.publication_end_date <= timezone.now() + + @property + def hidden_by_start_date(self): + if not self.publication_start_date: + return False + return self.publication_start_date >= timezone.now() + + @property + def is_visible(self): + """ + Is this entry publicly available? + """ + return self.is_published and (not self.hidden_by_end_date) and (not self.hidden_by_start_date) + @property def is_dirty(self): if not self.is_draft: @@ -84,10 +130,12 @@ def is_dirty(self): @assert_draft def publish(self): if not self.is_draft: - return + log.info("Don't publish %s because it's not the daft version!", self) + return self if not self.is_dirty: - return + log.info("Don't publish %s because it's not dirty!", self) + return self publisher_pre_publish.send(sender=self.__class__, instance=self) @@ -132,10 +180,14 @@ def publish(self): publisher_publish_pre_save_draft.send(sender=draft_obj.__class__, instance=draft_obj) - draft_obj.save(suppress_modified=True) + self._suppress_modified=True # Don't update self.publisher_modified_at + draft_obj.save() + self._suppress_modified=False publisher_post_publish.send(sender=draft_obj.__class__, instance=draft_obj) + return publish_obj + @assert_draft def patch_placeholders(self, draft_obj): try: @@ -244,33 +296,37 @@ def get_placeholder_fields(self, obj=None): try: from cms.models.placeholdermodel import Placeholder + from cms.models.fields import PlaceholderField except ImportError: return placeholder_fields if obj is None: obj = self - model_fields = obj.__class__._meta.get_all_field_names() + model_fields = obj.__class__._meta.get_fields() for field in model_fields: - if field in self.publisher_ignore_fields: + if field.name in self.publisher_ignore_fields: continue try: - placeholder = getattr(obj, field) - if isinstance(placeholder, Placeholder): - placeholder_fields.append(field) - except (ObjectDoesNotExist, AttributeError): + if isinstance(field, (Placeholder, PlaceholderField)): + placeholder_fields.append(field.name) + except (ObjectDoesNotExist, AttributeError) as err: continue return placeholder_fields - def update_modified_at(self): - self.publisher_modified_at = timezone.now() + _suppress_modified=False + + def save(self, **kwargs): + if self._suppress_modified is False: + self.publisher_modified_at = timezone.now() + + super(PublisherModelBase, self).save(**kwargs) class PublisherModel(PublisherModelBase): - objects = models.Manager() - publisher_manager = PublisherManager() + objects = PublisherManager() class Meta: abstract = True @@ -278,8 +334,49 @@ class Meta: ('can_publish', 'Can publish'), ) - def save(self, suppress_modified=False, **kwargs): - if suppress_modified is False: - self.update_modified_at() - super(PublisherModel, self).save(**kwargs) +try: + from .managers import PublisherParlerManager + from parler.models import TranslatableModelMixin +except ImportError: + pass +else: + class PublisherParlerModel(TranslatableModelMixin, PublisherModelBase): + objects = PublisherParlerManager() + + class Meta(PublisherModel.Meta): + abstract = True + + try: + from aldryn_translation_tools.models import TranslatedAutoSlugifyMixin + except ImportError: + pass + else: + class PublisherParlerAutoSlugifyModel(TranslatedAutoSlugifyMixin, PublisherParlerModel): + + def _get_slug_queryset(self, *args, **kwargs): + """ + The slug must be only unique for drafts + """ + qs = super(PublisherParlerAutoSlugifyModel, self)._get_slug_queryset() + qs = qs.filter(publisher_is_draft=PublisherModelBase.STATE_DRAFT) + return qs + + def save(self, **kwargs): + """ + Set new slug by TranslatedAutoSlugifyMixin only on drafts + see also: + https://github.com/andersinno/django-model-publisher-ai/issues/8 + """ + if self.publisher_is_draft: + # code from TranslatedAutoSlugifyMixin.save(): + slug = self._get_existing_slug() + if not slug or self._slug_exists(slug): + slug = self.make_new_slug(slug=slug) + setattr(self, self.slug_field_name, slug) + + # NOTE: We call PublisherParlerModel.save() here: + super(PublisherParlerModel, self).save(**kwargs) + + class Meta(PublisherParlerModel.Meta): + abstract = True diff --git a/publisher/version.py b/publisher/version.py new file mode 100644 index 0000000..455e715 --- /dev/null +++ b/publisher/version.py @@ -0,0 +1,4 @@ + +# https://packaging.python.org/tutorials/distributing-packages/#choosing-a-versioning-scheme + +__version__ = '0.4.1' diff --git a/publisher/views.py b/publisher/views.py index 91df19c..ec75f43 100644 --- a/publisher/views.py +++ b/publisher/views.py @@ -1,8 +1,6 @@ from django.views.generic import ListView from django.views.generic.detail import DetailView -from .middleware import get_draft_status - class PublisherViewMixin(object): @@ -10,7 +8,7 @@ class Meta: abstract = True def get_queryset(self): - return self.model.objects.filter(publisher_is_draft=get_draft_status()).all() + return self.model.objects.visible() class PublisherDetailView(PublisherViewMixin, DetailView): diff --git a/tests/__init__.py b/publisher_tests/__init__.py similarity index 100% rename from tests/__init__.py rename to publisher_tests/__init__.py diff --git a/tests/manage.py b/publisher_tests/manage.py similarity index 100% rename from tests/manage.py rename to publisher_tests/manage.py diff --git a/tests/myapp/__init__.py b/publisher_tests/myapp/__init__.py similarity index 100% rename from tests/myapp/__init__.py rename to publisher_tests/myapp/__init__.py diff --git a/tests/myapp/admin.py b/publisher_tests/myapp/admin.py similarity index 100% rename from tests/myapp/admin.py rename to publisher_tests/myapp/admin.py diff --git a/publisher_tests/myapp/models.py b/publisher_tests/myapp/models.py new file mode 100644 index 0000000..3902c0d --- /dev/null +++ b/publisher_tests/myapp/models.py @@ -0,0 +1,43 @@ +from django.db import models + +from publisher.managers import PublisherManager +from publisher.models import PublisherModel + + +try: + import parler +except ImportError: + parler=None + + +try: + import aldryn_translation_tools +except ImportError as err: + aldryn_translation_tools=None + + +class PublisherTestModel(PublisherModel): + title = models.CharField(max_length=100) + objects = PublisherManager() + + +if parler is not None: + from parler.models import TranslatedFields + from publisher.models import PublisherParlerModel + + class PublisherParlerTestModel(PublisherParlerModel): + translations = TranslatedFields( + title=models.CharField(max_length=100) + ) + + +if aldryn_translation_tools is not None: + from publisher.models import PublisherParlerAutoSlugifyModel + + class PublisherParlerAutoSlugifyTestModel(PublisherParlerAutoSlugifyModel): + slug_source_field_name = "title" # TranslatedAutoSlugifyMixin options + + translations = TranslatedFields( + title=models.CharField(max_length=100), + slug=models.SlugField(max_length=255, db_index=True, blank=True), + ) diff --git a/publisher_tests/myapp/tests.py b/publisher_tests/myapp/tests.py new file mode 100644 index 0000000..4a3c88d --- /dev/null +++ b/publisher_tests/myapp/tests.py @@ -0,0 +1,723 @@ +import datetime +import unittest + +from django import test +from django.core.cache import cache +from django.utils import timezone + +from mock import MagicMock + +from publisher.utils import NotDraftException +from publisher.signals import publisher_post_publish, publisher_post_unpublish +from publisher.middleware import PublisherMiddleware, get_draft_status + +from myapp.models import PublisherTestModel + + +try: + import parler + from parler.managers import TranslatableQuerySet +except ImportError: + PARLER_INSTALLED=False +else: + PARLER_INSTALLED = True + from myapp.models import PublisherParlerTestModel + + +TRANSLATION_TOOLS_INSTALLED=False +if PARLER_INSTALLED: + try: + import aldryn_translation_tools + except ImportError as err: + pass + else: + TRANSLATION_TOOLS_INSTALLED = True + from myapp.models import PublisherParlerAutoSlugifyTestModel + + +class PublisherTest(test.TestCase): + + def test_creating_model_creates_only_one_record(self): + PublisherTestModel.objects.create(title='Test model') + count = PublisherTestModel.objects.count() + self.assertEqual(count, 1) + + def test_new_models_are_draft(self): + instance = PublisherTestModel(title='Test model') + self.assertTrue(instance.is_draft) + + def test_editing_a_record_does_not_create_a_duplicate(self): + instance = PublisherTestModel.objects.create(title='Test model') + instance.title = 'Updated test model' + instance.save() + count = PublisherTestModel.objects.count() + self.assertEqual(count, 1) + + def test_editing_a_draft_does_not_update_published_record(self): + title = 'Test model' + instance = PublisherTestModel.objects.create(title=title) + instance.publish() + instance.title = 'Updated test model' + instance.save() + published_instance = PublisherTestModel.objects.published().get() + self.assertEqual(published_instance.title, title) + + def test_publishing_creates_new_record(self): + instance = PublisherTestModel.objects.create(title='Test model') + instance.publish() + + published = PublisherTestModel.objects.published().count() + drafts = PublisherTestModel.objects.drafts().count() + + self.assertEqual(published, 1) + self.assertEqual(drafts, 1) + + def test_unpublishing_deletes_published_record(self): + instance = PublisherTestModel.objects.create(title='Test model') + instance.publish() + instance.unpublish() + + published = PublisherTestModel.objects.published().count() + drafts = PublisherTestModel.objects.drafts().count() + + self.assertEqual(published, 0) + self.assertEqual(drafts, 1) + + def test_unpublished_record_can_be_republished(self): + instance = PublisherTestModel.objects.create(title='Test model') + instance.publish() + instance.unpublish() + instance.publish() + + published = PublisherTestModel.objects.published().count() + drafts = PublisherTestModel.objects.drafts().count() + + self.assertEqual(published, 1) + self.assertEqual(drafts, 1) + + def test_published_date_is_set_to_none_for_new_records(self): + draft = PublisherTestModel(title='Test model') + self.assertEqual(draft.publisher_published_at, None) + + def test_published_date_is_updated_when_publishing(self): + now = timezone.now() + draft = PublisherTestModel.objects.create(title='Test model') + draft.publish() + draft = PublisherTestModel.objects.drafts().get() + published = PublisherTestModel.objects.drafts().get() + + self.assertGreaterEqual(draft.publisher_published_at, now) + self.assertGreaterEqual(published.publisher_published_at, now) + self.assertEqual(draft.publisher_published_at, published.publisher_published_at) + + def test_published_date_is_not_changed_when_publishing_twice(self): + published_date = datetime.datetime(1970, 1, 1, 0, 0, tzinfo=timezone.utc) + draft = PublisherTestModel.objects.create(title='Test model') + draft.publish() + published = PublisherTestModel.objects.drafts().get() + draft.publisher_published_at = published_date + draft.save() + published.publisher_published_at = published_date + published.save() + + draft.publish() + draft = PublisherTestModel.objects.drafts().get() + published = PublisherTestModel.objects.drafts().get() + self.assertEqual(draft.publisher_published_at, published_date) + self.assertEqual(published.publisher_published_at, published_date) + + def test_published_date_is_set_to_none_when_unpublished(self): + draft = PublisherTestModel.objects.create(title='Test model') + draft.publish() + draft.unpublish() + self.assertIsNone(draft.publisher_published_at) + + def test_published_date_is_set_when_republished(self): + now = timezone.now() + draft = PublisherTestModel.objects.create(title='Test model') + draft.publish() + draft.unpublish() + draft.publish() + self.assertGreaterEqual(draft.publisher_published_at, now) + + def test_deleting_draft_also_deletes_published_record(self): + instance = PublisherTestModel.objects.create(title='Test model') + instance.publish() + instance.delete() + + published = PublisherTestModel.objects.published().count() + drafts = PublisherTestModel.objects.drafts().count() + + self.assertEqual(published, 0) + self.assertEqual(drafts, 0) + + def test_delete_published_does_not_delete_draft(self): + obj = PublisherTestModel.objects.create(title='Test model') + obj.publish() + + published = PublisherTestModel.objects.published().get() + published.delete() + + published = PublisherTestModel.objects.published().count() + drafts = PublisherTestModel.objects.drafts().count() + + self.assertEqual(published, 0) + self.assertEqual(drafts, 1) + + def test_reverting_reverts_draft_from_published_record(self): + title = 'Test model' + instance = PublisherTestModel.objects.create(title=title) + instance.publish() + instance.title = 'Updated test model' + instance.save() + revert_instance = instance.revert_to_public() + self.assertEqual(title, revert_instance.title) + + def test_only_draft_records_can_be_published_or_reverted(self): + draft = PublisherTestModel.objects.create(title='Test model') + draft.publish() + + published = PublisherTestModel.objects.published().get() + self.assertRaises(NotDraftException, published.publish) + self.assertRaises(NotDraftException, published.unpublish) + self.assertRaises(NotDraftException, published.revert_to_public) + + def test_published_signal(self): + # Check the signal was sent. These get lost if they don't reference self. + self.got_signal = False + self.signal_sender = None + self.signal_instance = None + + def handle_signal(sender, instance, **kwargs): + self.got_signal = True + self.signal_sender = sender + self.signal_instance = instance + + publisher_post_publish.connect(handle_signal) + + # call the function + instance = PublisherTestModel.objects.create(title='Test model') + instance.publish() + + self.assertTrue(self.got_signal) + self.assertEqual(self.signal_sender, PublisherTestModel) + self.assertEqual(self.signal_instance, instance) + + def test_unpublished_signal(self): + # Check the signal was sent. These get lost if they don't reference self. + self.got_signal = False + self.signal_sender = None + self.signal_instance = None + + def handle_signal(sender, instance, **kwargs): + self.got_signal = True + self.signal_sender = sender + self.signal_instance = instance + + publisher_post_unpublish.connect(handle_signal) + + # Call the function. + instance = PublisherTestModel.objects.create(title='Test model') + instance.publish() + instance.unpublish() + + self.assertTrue(self.got_signal) + self.assertEqual(self.signal_sender, PublisherTestModel) + self.assertEqual(self.signal_instance, instance) + + def test_unpublished_signal_is_sent_when_deleting(self): + self.got_signal = False + self.signal_sender = None + self.signal_instance = None + + def handle_signal(sender, instance, **kwargs): + self.got_signal = True + self.signal_sender = sender + self.signal_instance = instance + + publisher_post_unpublish.connect(handle_signal) + + # Call the function. + instance = PublisherTestModel.objects.create(title='Test model') + instance.publish() + instance.delete() + + self.assertTrue(self.got_signal) + self.assertEqual(self.signal_sender, PublisherTestModel) + self.assertEqual(self.signal_instance, instance) + + def test_middleware_detects_published_when_logged_out(self): + + class MockUser(object): + is_staff = False + + def is_authenticated(self): + return False + + class MockRequest(object): + user = MockUser() + GET = {'edit': '1'} + + mock_request = MockRequest() + self.assertFalse(PublisherMiddleware.is_draft(mock_request)) + + def test_middleware_detects_published_when_user_edit_parameter_is_missing(self): + + class MockUser(object): + is_staff = True + + def is_authenticated(self): + return True + + class MockRequest(object): + user = MockUser() + GET = {} + + mock_request = MockRequest() + self.assertFalse(PublisherMiddleware.is_draft(mock_request)) + + def test_middleware_detects_published_when_user_is_not_staff(self): + + class MockUser(object): + is_staff = False + + def is_authenticated(self): + return True + + class MockRequest(object): + user = MockUser() + GET = {'edit': '1'} + + mock_request = MockRequest() + self.assertFalse(PublisherMiddleware.is_draft(mock_request)) + + def test_middleware_detects_draft_when_user_is_staff_and_edit_parameter_is_present(self): + + class MockUser(object): + is_staff = True + + def is_authenticated(self): + return True + + class MockRequest(object): + user = MockUser() + GET = {'edit': '1'} + + mock_request = MockRequest() + self.assertTrue(PublisherMiddleware.is_draft(mock_request)) + + def test_middleware_get_draft_status_shortcut_defaults_to_false(self): + self.assertFalse(get_draft_status()) + + def test_middleware_get_draft_status_shortcut_returns_true_in_draft_mode(self): + # Mock the request process to initialise the middleware, but force the middleware to go in + # draft mode. + middleware = PublisherMiddleware() + middleware.is_draft = MagicMock(return_value=True) + middleware.process_request(None) + draft_status = get_draft_status() + PublisherMiddleware.process_response(None, None) + + self.assertTrue(draft_status) + + def test_middleware_get_draft_status_shortcut_does_not_change_draft_status(self): + # The get_draft_status() shortcut shouldn't change the value returned by + # PublisherMiddleware.get_draft_status(). + middleware = PublisherMiddleware() + middleware.is_draft = MagicMock(return_value=True) + middleware.process_request(None) + expected_draft_status = PublisherMiddleware.get_draft_status() + draft_status = get_draft_status() + PublisherMiddleware.process_response(None, None) + + self.assertTrue(expected_draft_status, draft_status) + + def test_middleware_forgets_current_draft_status_after_request(self): + middleware = PublisherMiddleware() + middleware.is_draft = MagicMock(return_value=True) + middleware.process_request(None) + PublisherMiddleware.process_response(None, None) + + self.assertFalse(get_draft_status()) + + def test_model_properties(self): + draft_obj = PublisherTestModel.objects.create(title="one") + + self.assertEqual(draft_obj.is_draft, True) + self.assertEqual(draft_obj.is_published, False) + self.assertEqual(draft_obj.is_dirty, True) + + publish_obj = draft_obj.publish() + + self.assertEqual(publish_obj.title, "one") + self.assertEqual(publish_obj.is_draft, False) + self.assertEqual(publish_obj.is_published, True) + self.assertEqual(publish_obj.is_dirty, False) + + self.assertEqual(draft_obj.title, "one") + self.assertEqual(draft_obj.is_draft, True) + self.assertEqual(draft_obj.is_published, False) # FIXME: Should this not be True ?!? + self.assertEqual(draft_obj.is_dirty, False) + + draft_obj.title="two" + draft_obj.save() + + self.assertEqual(publish_obj.title, "one") + self.assertEqual(publish_obj.is_draft, False) + self.assertEqual(publish_obj.is_published, True) + self.assertEqual(publish_obj.is_dirty, False) # FIXME: Should this not be True ?!? + + self.assertEqual(draft_obj.title, "two") + self.assertEqual(draft_obj.is_draft, True) + self.assertEqual(draft_obj.is_published, False) # FIXME: Should this not be True ?!? + self.assertEqual(draft_obj.is_dirty, True) + + publish_obj = draft_obj.publish() + + self.assertEqual(publish_obj.title, "two") + self.assertEqual(publish_obj.is_draft, False) + self.assertEqual(publish_obj.is_published, True) + self.assertEqual(publish_obj.is_dirty, False) + + self.assertEqual(draft_obj.title, "two") + self.assertEqual(draft_obj.is_draft, True) + self.assertEqual(draft_obj.is_published, False) # FIXME: Should this not be True ?!? + self.assertEqual(draft_obj.is_dirty, False) + + def test_publication_start_date(self): + yesterday = timezone.now() - datetime.timedelta(days=1) + tomorrow = timezone.now() + datetime.timedelta(days=1) + + instance = PublisherTestModel.objects.create(title='Test model') + instance.publish() + + # No publication_start_date set: + + published = PublisherTestModel.objects.published() + self.assertEqual(published.count(), 1) + # Check model instance + obj = published[0] + self.assertEqual(obj.publication_start_date, None) + self.assertEqual(obj.publication_end_date, None) + self.assertEqual(obj.is_published, True) + self.assertEqual(obj.hidden_by_end_date, False) + self.assertEqual(obj.hidden_by_start_date, False) + self.assertEqual(obj.is_visible, True) + + # Hidden, because publication_start_date is in the future: + + instance.publication_start_date = tomorrow + instance.save() + instance.publish() + + published = PublisherTestModel.objects.published() + self.assertEqual(published.count(), 1) + + visible = PublisherTestModel.objects.visible() + self.assertEqual(visible.count(), 0) + + count = PublisherTestModel.objects.all().count() + self.assertEqual(count, 2) # draft + published + + draft = PublisherTestModel.objects.drafts()[0] + self.assertEqual(draft.publication_start_date, tomorrow) + + # Check model instance + obj = PublisherTestModel.objects.filter(publisher_is_draft=PublisherTestModel.STATE_PUBLISHED)[0] + self.assertEqual(obj.publication_start_date, tomorrow) + self.assertEqual(obj.publication_end_date, None) + self.assertEqual(obj.is_published, True) + self.assertEqual(obj.hidden_by_end_date, False) + self.assertEqual(obj.hidden_by_start_date, True) + self.assertEqual(obj.is_visible, False) + + # Visible, because publication_start_date is in the past: + + instance.publication_start_date = yesterday + instance.save() + instance.publish() + + published = PublisherTestModel.objects.published() + self.assertEqual(published.count(), 1) + + visible = PublisherTestModel.objects.visible() + self.assertEqual(visible.count(), 1) + + # Check model instance + obj = published[0] + self.assertEqual(obj.publication_start_date, yesterday) + self.assertEqual(obj.publication_end_date, None) + self.assertEqual(obj.is_published, True) + self.assertEqual(obj.hidden_by_end_date, False) + self.assertEqual(obj.hidden_by_start_date, False) + self.assertEqual(obj.is_visible, True) + + def test_publication_end_date(self): + yesterday = timezone.now() - datetime.timedelta(days=1) + tomorrow = timezone.now() + datetime.timedelta(days=1) + + instance = PublisherTestModel.objects.create(title='Test model') + instance.publish() + + # No publication_end_date set: + published = PublisherTestModel.objects.published() + self.assertEqual(published.count(), 1) + + visible = PublisherTestModel.objects.visible() + self.assertEqual(visible.count(), 1) + + # Check model instance + obj = published[0] + self.assertEqual(obj.publication_start_date, None) + self.assertEqual(obj.publication_end_date, None) + self.assertEqual(obj.is_published, True) + self.assertEqual(obj.hidden_by_end_date, False) + self.assertEqual(obj.hidden_by_start_date, False) + self.assertEqual(obj.is_visible, True) + + # Hidden, because publication_end_date is in the past: + instance.publication_end_date = yesterday + instance.save() + instance.publish() + + published = PublisherTestModel.objects.published() + self.assertEqual(published.count(), 1) + + visible = PublisherTestModel.objects.visible() + self.assertEqual(visible.count(), 0) + + count = PublisherTestModel.objects.all().count() + self.assertEqual(count, 2) # draft + published + + draft = PublisherTestModel.objects.drafts()[0] + self.assertEqual(draft.publication_start_date, None) + self.assertEqual(draft.publication_end_date, yesterday) + + # Check model instance + obj = PublisherTestModel.objects.filter(publisher_is_draft=PublisherTestModel.STATE_PUBLISHED)[0] + self.assertEqual(obj.publication_start_date, None) + self.assertEqual(obj.publication_end_date, yesterday) + self.assertEqual(obj.is_published, True) + self.assertEqual(obj.hidden_by_end_date, True) + self.assertEqual(obj.hidden_by_start_date, False) + self.assertEqual(obj.is_visible, False) + + # Visible, because publication_end_date is in the future: + instance.publication_end_date = tomorrow + instance.save() + instance.publish() + + published = PublisherTestModel.objects.published() + self.assertEqual(published.count(), 1) + + visible = PublisherTestModel.objects.visible() + self.assertEqual(visible.count(), 1) + + # Check model instance + obj = published[0] + self.assertEqual(obj.publication_start_date, None) + self.assertEqual(obj.publication_end_date, tomorrow) + self.assertEqual(obj.is_published, True) + self.assertEqual(obj.hidden_by_end_date, False) + self.assertEqual(obj.hidden_by_start_date, False) + self.assertEqual(obj.is_visible, True) + + +@unittest.skipIf(PARLER_INSTALLED != True, 'Django-Parler is not installed') +class PublisherParlerTest(test.TestCase): + + def test_queryset_subclass(self): + queryset = PublisherParlerTestModel.objects.all() + self.assertTrue(issubclass(queryset.__class__, TranslatableQuerySet)) + + def test_creation(self): + x = PublisherParlerTestModel.objects.create(title='english title') + x.create_translation('de', title='deutsche Titel') + self.assertEqual(sorted(x.get_available_languages()), ['de', 'en']) + + def test_creating_instance(self): + instance = PublisherParlerTestModel() + instance.set_current_language('en') + instance.title = 'The english title' + instance.save() + instance.set_current_language('de') + instance.title = 'Der deutsche Titel' + instance.save() + + instance = PublisherParlerTestModel.objects.get(pk=instance.pk) + + self.assertEqual(sorted(instance.get_available_languages()), ['de', 'en']) + + count = PublisherParlerTestModel.objects.count() + self.assertEqual(count, 1) + + count = PublisherParlerTestModel.objects.drafts().count() + self.assertEqual(count, 1) + + count = PublisherParlerTestModel.objects.published().count() + self.assertEqual(count, 0) + + count = PublisherParlerTestModel.objects.visible().count() + self.assertEqual(count, 0) + + count = PublisherParlerTestModel.objects.language(language_code='en').count() + self.assertEqual(count, 1) + + queryset = PublisherParlerTestModel.objects.active_translations('en') + queryset = queryset.drafts() + count = queryset.count() + self.assertEqual(count, 1) + + queryset = PublisherParlerTestModel.objects.active_translations('en') + queryset = queryset.published() + count = queryset.count() + self.assertEqual(count, 0) + + queryset = PublisherParlerTestModel.objects.active_translations('de') + queryset = queryset.drafts() + count = queryset.count() + self.assertEqual(count, 1) + + def test_publish(self): + instance = PublisherParlerTestModel.objects.create() + instance.publish() + + count = PublisherParlerTestModel.objects.drafts().count() + self.assertEqual(count, 1) + + count = PublisherParlerTestModel.objects.published().count() + self.assertEqual(count, 1) + + count = PublisherParlerTestModel.objects.visible().count() + self.assertEqual(count, 1) + + + +@unittest.skipIf(TRANSLATION_TOOLS_INSTALLED != True, 'aldryn_translation_tools is not installed') +class PublisherParlerAutoSlugifyTest(test.TestCase): + def tearDown(self): + # Parler cache must be cleared, otherwise some test failed. + # Maybe a other way is to set PARLER_ENABLE_CACHING=False in settings + cache.clear() + + def _create_draft(self): + instance = PublisherParlerAutoSlugifyTestModel.objects.language('de').create(title='Der deutsche Titel') + instance.set_current_language('en') + instance.title = 'The english title' + instance.save() + instance = PublisherParlerAutoSlugifyTestModel.objects.get(pk=instance.pk) + return instance + + def assert_instance(self, instance): + instance.set_current_language('de') + self.assertEqual(instance.title, 'Der deutsche Titel') + self.assertEqual(instance.slug, "der-deutsche-titel") + + instance.set_current_language('en') + self.assertEqual(instance.title, 'The english title') + self.assertEqual(instance.slug, "the-english-title") + + # FIXME: Will fail in some cases: + # self.assertEqual(sorted(instance.get_available_languages()), ['de', 'en']) + + def test_slug_creation(self): + instance = self._create_draft() + self.assert_instance(instance) + + def test_publish(self): + instance = self._create_draft() + self.assert_instance(instance) + + count = PublisherParlerAutoSlugifyTestModel.objects.drafts().count() + self.assertEqual(count, 1) + + count = PublisherParlerAutoSlugifyTestModel.objects.published().count() + self.assertEqual(count, 0) + + count = PublisherParlerAutoSlugifyTestModel.objects.visible().count() + self.assertEqual(count, 0) + + instance.publish() + + count = PublisherParlerAutoSlugifyTestModel.objects.drafts().count() + self.assertEqual(count, 1) + + count = PublisherParlerAutoSlugifyTestModel.objects.published().count() + self.assertEqual(count, 1) + + count = PublisherParlerAutoSlugifyTestModel.objects.visible().count() + self.assertEqual(count, 1) + + count = PublisherParlerAutoSlugifyTestModel.objects.count() + self.assertEqual(count, 2) + + def test_model_properties(self): + draft_obj = PublisherParlerAutoSlugifyTestModel.objects.create(title="one") + + self.assertEqual(draft_obj.is_draft, True) + self.assertEqual(draft_obj.is_published, False) + self.assertEqual(draft_obj.is_visible, False) + self.assertEqual(draft_obj.is_dirty, True) + + publish_obj = draft_obj.publish() + + self.assertEqual(publish_obj.title, "one") + self.assertEqual(publish_obj.is_draft, False) + self.assertEqual(publish_obj.is_published, True) + self.assertEqual(publish_obj.is_visible, True) + self.assertEqual(publish_obj.is_dirty, False) + + self.assertEqual(draft_obj.title, "one") + self.assertEqual(draft_obj.is_draft, True) + self.assertEqual(draft_obj.is_published, False) # FIXME: Should this not be True ?!? + self.assertEqual(draft_obj.is_visible, False) # FIXME: Should this not be True ?!? + self.assertEqual(draft_obj.is_dirty, False) + + draft_obj.title="two" + draft_obj.save() + + self.assertEqual(publish_obj.title, "one") + self.assertEqual(publish_obj.is_draft, False) + self.assertEqual(publish_obj.is_published, True) + self.assertEqual(publish_obj.is_visible, True) + self.assertEqual(publish_obj.is_dirty, False) # FIXME: Should this not be True ?!? + + self.assertEqual(draft_obj.title, "two") + self.assertEqual(draft_obj.is_draft, True) + self.assertEqual(draft_obj.is_published, False) # FIXME: Should this not be True ?!? + self.assertEqual(draft_obj.is_visible, False) # FIXME: Should this not be True ?!? + self.assertEqual(draft_obj.is_dirty, True) + + publish_obj = draft_obj.publish() + + self.assertEqual(publish_obj.title, "two") + self.assertEqual(publish_obj.is_draft, False) + self.assertEqual(publish_obj.is_published, True) + self.assertEqual(publish_obj.is_visible, True) + self.assertEqual(publish_obj.is_dirty, False) + + self.assertEqual(draft_obj.title, "two") + self.assertEqual(draft_obj.is_draft, True) + self.assertEqual(draft_obj.is_published, False) # FIXME: Should this not be True ?!? + self.assertEqual(draft_obj.is_visible, False) # FIXME: Should this not be True ?!? + self.assertEqual(draft_obj.is_dirty, False) + + def test_delete(self): + for no in range(10): + title = "%i" % no + instance = PublisherParlerAutoSlugifyTestModel.objects.create(title=title) + instance.publish() + + count = PublisherParlerAutoSlugifyTestModel.objects.drafts().count() + self.assertEqual(count, 10) + + count = PublisherParlerAutoSlugifyTestModel.objects.published().count() + self.assertEqual(count, 10) + + count = PublisherParlerAutoSlugifyTestModel.objects.count() + self.assertEqual(count, 20) + + PublisherParlerAutoSlugifyTestModel.objects.all().delete() + count = PublisherParlerAutoSlugifyTestModel.objects.count() + self.assertEqual(count, 0) + diff --git a/tests/settings.py b/publisher_tests/settings.py similarity index 53% rename from tests/settings.py rename to publisher_tests/settings.py index a131bf9..5f87a95 100644 --- a/tests/settings.py +++ b/publisher_tests/settings.py @@ -20,6 +20,10 @@ 'django.contrib.messages', 'django.contrib.sessions', 'django.contrib.staticfiles', + + 'django.contrib.sites', # django-cms will import sites models + 'menus', # django-cms will import menu models + 'publisher', 'myapp', ) @@ -58,3 +62,42 @@ ROOT_URLCONF = 'urls' USE_TZ = True + +# https://docs.djangoproject.com/en/1.8/ref/settings/#std:setting-LANGUAGE_CODE +LANGUAGE_CODE = "en" + +# http://docs.django-cms.org/en/latest/reference/configuration.html#std:setting-CMS_LANGUAGES +CMS_LANGUAGES = { + 1: [ + { + "code": "de", + "fallbacks": ["en"], + "hide_untranslated": True, + "name": "German", + "public": True, + "redirect_on_fallback": False, + }, + { + "code": "en", + "fallbacks": ["de"], + "hide_untranslated": True, + "name": "English", + "public": True, + "redirect_on_fallback": False, + }, + ], + "default": { # all SITE_ID"s + "fallbacks": [LANGUAGE_CODE], + "redirect_on_fallback": False, + "public": True, + "hide_untranslated": True, + }, +} + +# https://docs.djangoproject.com/en/1.8/ref/settings/#languages +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGES = tuple([(d["code"], d["name"]) for d in CMS_LANGUAGES[1]]) + +# http://django-parler.readthedocs.org/en/latest/quickstart.html#configuration +PARLER_DEFAULT_LANGUAGE_CODE = LANGUAGE_CODE +PARLER_LANGUAGES = CMS_LANGUAGES \ No newline at end of file diff --git a/tests/urls.py b/publisher_tests/urls.py similarity index 100% rename from tests/urls.py rename to publisher_tests/urls.py diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..e66f29f --- /dev/null +++ b/pytest.ini @@ -0,0 +1,46 @@ +# +# pytest config file +# +# http://doc.pytest.org/en/latest/customize.html#builtin-configuration-file-options +# https://pytest-django.readthedocs.io/en/latest/ +# + +[pytest] +DJANGO_SETTINGS_MODULE=publisher_tests.settings +testpaths = publisher_tests +python_files = tests.py test_*.py *_tests.py +# http://doc.pytest.org/en/latest/customize.html#confval-norecursedirs +#cache_dir=/tmp +norecursedirs = + .* + __pycache__ +addopts = + --verbose + --reuse-db + #--create-db + #--nomigrations + --showlocals + #--trace-config + #--doctest-modules + --cov-report xml:reports/coverage.xml + --cov-report html:reports/ + --cov-config pytest.ini + --no-cov-on-fail + # Do not cut tracebacks (somethimes helpfull): + #--full-trace + # exit after 2 failures: + --maxfail=2 + # per-test capturing method: one of fd|sys|no: + #--capture=no + # run the last failures first: + --failed-first + +# coverage +[run] +source = src +branch = True +data_file = /tmp/.coverage +omit = + */migrations/* + #*/publisher_tests/* + *settings.py diff --git a/requirements/dev.txt b/requirements/dev.txt index 1abe723..53e05de 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -3,4 +3,12 @@ mock coverage flake8 tox -coveralls +pytest +pytest-django +pytest-cov +docutils +python-creole +wheel +twine +aldryn_translation_tools +django-parler diff --git a/setup.cfg b/setup.cfg index 0edf785..616860b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,11 @@ [flake8] exclude = env,venv,.tox,docs/conf.py max-line-length = 99 +ignore = E731 + +[wheel] +universal = 1 + +[aliases] + -[bdist_wheel] -universal = 1 \ No newline at end of file diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 8a1f8f2..f0245d0 --- a/setup.py +++ b/setup.py @@ -1,47 +1,284 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +from __future__ import unicode_literals, print_function, absolute_import + import os +import shutil import sys +import distutils +import subprocess + +from setuptools import setup, find_packages + + +PACKAGE_ROOT = os.path.dirname(os.path.abspath(__file__)) + + +def read(*args): + return open(os.path.join(PACKAGE_ROOT, *args)).read() + + +__version__="" +exec(read('publisher', 'version.py')) + + +class ToxTestCommand(distutils.cmd.Command): + """Distutils command to run tests via tox with 'python setup.py test'. + + Please note that in this package configuration tox uses the dependencies in + ``requirements/dev.txt``, the list of dependencies in ``tests_require`` in + ``setup.py`` is ignored! + + See https://docs.python.org/3/distutils/apiref.html#creating-a-new-distutils-command + for more documentation on custom distutils commands. + """ + description = "Run tests via 'tox'." + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + self.announce("Running tests with 'tox'...", level=distutils.log.INFO) + return subprocess.call(['tox']) + + +class TestCommand(distutils.cmd.Command): + """ + Distutils command to run tests via py.test. + """ + description = "Run tests via 'py.test'." + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + self.announce("Running tests...", level=distutils.log.INFO) + return subprocess.call(['pytest', 'publisher_tests']) + + +# convert creole to ReSt on-the-fly, see also: +# https://code.google.com/p/python-creole/wiki/UseInSetup +try: + from creole.setup_utils import get_long_description +except ImportError as err: + if "check" in sys.argv or "register" in sys.argv or "sdist" in sys.argv or "--long-description" in sys.argv: + raise ImportError("%s - Please install python-creole >= v0.8 - e.g.: pip install python-creole" % err) + long_description = None +else: + long_description = get_long_description(PACKAGE_ROOT) + docs_readme=os.path.join(PACKAGE_ROOT, "docs", "readme.rst") + if os.path.isfile(docs_readme): + with open(docs_readme, "w") as f: + f.write(long_description) + f.write("\n\n\n*(This file is automatically generated by python-creole from ``/README.creole``)*") + print("Updated: %s" % docs_readme) + + +if "publish" in sys.argv: + """ + 'publish' helper for setup.py -from setuptools import setup + Build and upload to PyPi, if... + ... __version__ doesn't contains "dev" + ... we are on git 'master' branch + ... git repository is 'clean' (no changed files) -import publisher + Upload with "twine", git tag the current version and git push --tag -version = publisher.__version__ + The cli arguments will be pass to 'twine'. So this is possible: + * Display 'twine' help page...: ./setup.py publish --help + * use testpypi................: ./setup.py publish --repository=test -if sys.argv[-1] == 'publish': - os.system('python setup.py sdist upload') - print("You probably want to also tag the version now:") - print(" git tag -a %s -m 'version %s'" % (version, version)) - print(" git push --tags") - sys.exit() + TODO: Look at: https://github.com/zestsoftware/zest.releaser -readme = open('README.rst').read() + Source: https://github.com/jedie/python-code-snippets/blob/master/CodeSnippets/setup_publish.py + copyleft 2015-2016 Jens Diemer - GNU GPL v2+ + """ + if sys.version_info[0] == 2: + input = raw_input + import_error = False + try: + # Test if wheel is installed, otherwise the user will only see: + # error: invalid command 'bdist_wheel' + import wheel + except ImportError as err: + print("\nError: %s" % err) + print("\nMaybe https://pypi.python.org/pypi/wheel is not installed or virtualenv not activated?!?") + print("e.g.:") + print(" ~/your/env/$ source bin/activate") + print(" ~/your/env/$ pip install wheel") + import_error = True + + try: + import twine + except ImportError as err: + print("\nError: %s" % err) + print("\nMaybe https://pypi.python.org/pypi/twine is not installed or virtualenv not activated?!?") + print("e.g.:") + print(" ~/your/env/$ source bin/activate") + print(" ~/your/env/$ pip install twine") + import_error = True + + if import_error: + sys.exit(-1) + + def verbose_check_output(*args): + """ 'verbose' version of subprocess.check_output() """ + call_info = "Call: %r" % " ".join(args) + try: + output = subprocess.check_output(args, universal_newlines=True, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as err: + print("\n***ERROR:") + print(err.output) + raise + return call_info, output + + def verbose_check_call(*args): + """ 'verbose' version of subprocess.check_call() """ + print("\tCall: %r\n" % " ".join(args)) + subprocess.check_call(args, universal_newlines=True) + + def confirm(txt): + print("\n%s" % txt) + if input("\nPublish anyhow? (Y/N)").lower() not in ("y", "j"): + print("Bye.") + sys.exit(-1) + + if "dev" in __version__: + confirm("WARNING: Version contains 'dev': v%s\n" % __version__) + + print("\nCheck if we are on 'master' branch:") + call_info, output = verbose_check_output("git", "branch", "--no-color") + print("\t%s" % call_info) + if "* master" in output: + print("OK") + else: + confirm("\nNOTE: It seems you are not on 'master':\n%s" % output) + + print("\ncheck if if git repro is clean:") + call_info, output = verbose_check_output("git", "status", "--porcelain") + print("\t%s" % call_info) + if output == "": + print("OK") + else: + print("\n *** ERROR: git repro not clean:") + print(output) + sys.exit(-1) + + print("\ncheck if pull is needed") + verbose_check_call("git", "fetch", "--all") + call_info, output = verbose_check_output("git", "log", "HEAD..origin/master", "--oneline") + print("\t%s" % call_info) + if output == "": + print("OK") + else: + print("\n *** ERROR: git repro is not up-to-date:") + print(output) + sys.exit(-1) + verbose_check_call("git", "push") + + print("\nCleanup old builds:") + def rmtree(path): + path = os.path.abspath(path) + if os.path.isdir(path): + print("\tremove tree:", path) + shutil.rmtree(path) + rmtree("./dist") + rmtree("./build") + + print("\nbuild but don't upload...") + log_filename="build.log" + with open(log_filename, "a") as log: + call_info, output = verbose_check_output( + sys.executable or "python", + "setup.py", "sdist", "bdist_wheel", "bdist_egg" + ) + print("\t%s" % call_info) + log.write(call_info) + log.write(output) + print("Build output is in log file: %r" % log_filename) + + git_tag="v%s" % __version__ + + print("\ncheck git tag") + call_info, output = verbose_check_output("git", "log", "HEAD..origin/master", "--oneline") + if git_tag in output: + print("\n *** ERROR: git tag %r already exists!" % git_tag) + print(output) + sys.exit(-1) + else: + print("OK") + + print("\nUpload with twine:") + twine_args = sys.argv[1:] + twine_args.remove("publish") + twine_args.insert(1, "dist/*") + print("\ttwine upload command args: %r" % " ".join(twine_args)) + from twine.commands.upload import main as twine_upload + twine_upload(twine_args) + + print("\ngit tag version") + verbose_check_call("git", "tag", git_tag) + + print("\ngit push tag to server") + verbose_check_call("git", "push", "--tags") + + sys.exit(0) + + +classifiers=[ + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + 'Development Status :: 3 - Alpha', + + 'Environment :: Web Environment', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: Content Management System', + 'Topic :: Software Development :: Libraries :: Python Modules', +] + + +# https://packaging.python.org/tutorials/distributing-packages/ setup( - name='django-model-publisher-ai', - version=version, + name='django-ya-model-publisher', + version=__version__, description="""Handy mixin/abstract class for providing a "publisher workflow" to arbitrary Django models.""", - long_description=readme, - author='JP74', - author_email='opensource@jp74.com', - url='https://github.com/andersinno/django-model-publisher-ai', + long_description=long_description, + author='Jens Diemer', + author_email='model-ya-publisher@jensdiemer.de', + url='https://github.com/wearehoods/django-ya-model-publisher', packages=[ 'publisher', ], include_package_data=True, license="BSD", zip_safe=False, - keywords='django-model-publisher', - classifiers=[ - 'Development Status :: 4 - Beta', - 'Framework :: Django', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Natural Language :: English', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - ], + keywords='publisher django cms parler workflow model-publisher', + python_requires='>=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4', + classifiers=classifiers, + cmdclass={ + 'test': TestCommand, + 'tox': ToxTestCommand, + } ) diff --git a/tests/myapp/models.py b/tests/myapp/models.py deleted file mode 100644 index 45eb10b..0000000 --- a/tests/myapp/models.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.db import models - -from publisher.managers import PublisherManager -from publisher.models import PublisherModel - - -class PublisherTestModel(PublisherModel): - title = models.CharField(max_length=100) - - publisher_manager = PublisherManager() diff --git a/tests/myapp/tests.py b/tests/myapp/tests.py deleted file mode 100644 index eacb9bf..0000000 --- a/tests/myapp/tests.py +++ /dev/null @@ -1,318 +0,0 @@ -import datetime - -from django import test -from django.utils import timezone - -from mock import MagicMock - -from publisher.utils import NotDraftException -from publisher.signals import publisher_post_publish, publisher_post_unpublish -from publisher.middleware import PublisherMiddleware, get_draft_status - -from myapp.models import PublisherTestModel - - -class PublisherTest(test.TestCase): - - def test_creating_model_creates_only_one_record(self): - PublisherTestModel.publisher_manager.create(title='Test model') - count = PublisherTestModel.publisher_manager.count() - self.assertEqual(count, 1) - - def test_new_models_are_draft(self): - instance = PublisherTestModel(title='Test model') - self.assertTrue(instance.is_draft) - - def test_editing_a_record_does_not_create_a_duplicate(self): - instance = PublisherTestModel.publisher_manager.create(title='Test model') - instance.title = 'Updated test model' - instance.save() - count = PublisherTestModel.publisher_manager.count() - self.assertEqual(count, 1) - - def test_editing_a_draft_does_not_update_published_record(self): - title = 'Test model' - instance = PublisherTestModel.publisher_manager.create(title=title) - instance.publish() - instance.title = 'Updated test model' - instance.save() - published_instance = PublisherTestModel.publisher_manager.published().get() - self.assertEqual(published_instance.title, title) - - def test_publishing_creates_new_record(self): - instance = PublisherTestModel.publisher_manager.create(title='Test model') - instance.publish() - - published = PublisherTestModel.publisher_manager.published().count() - drafts = PublisherTestModel.publisher_manager.drafts().count() - - self.assertEqual(published, 1) - self.assertEqual(drafts, 1) - - def test_unpublishing_deletes_published_record(self): - instance = PublisherTestModel.publisher_manager.create(title='Test model') - instance.publish() - instance.unpublish() - - published = PublisherTestModel.publisher_manager.published().count() - drafts = PublisherTestModel.publisher_manager.drafts().count() - - self.assertEqual(published, 0) - self.assertEqual(drafts, 1) - - def test_unpublished_record_can_be_republished(self): - instance = PublisherTestModel.publisher_manager.create(title='Test model') - instance.publish() - instance.unpublish() - instance.publish() - - published = PublisherTestModel.publisher_manager.published().count() - drafts = PublisherTestModel.publisher_manager.drafts().count() - - self.assertEqual(published, 1) - self.assertEqual(drafts, 1) - - def test_published_date_is_set_to_none_for_new_records(self): - draft = PublisherTestModel(title='Test model') - self.assertEqual(draft.publisher_published_at, None) - - def test_published_date_is_updated_when_publishing(self): - now = timezone.now() - draft = PublisherTestModel.publisher_manager.create(title='Test model') - draft.publish() - draft = PublisherTestModel.publisher_manager.drafts().get() - published = PublisherTestModel.publisher_manager.drafts().get() - - self.assertGreaterEqual(draft.publisher_published_at, now) - self.assertGreaterEqual(published.publisher_published_at, now) - self.assertEqual(draft.publisher_published_at, published.publisher_published_at) - - def test_published_date_is_not_changed_when_publishing_twice(self): - published_date = datetime.datetime(1970, 1, 1, 0, 0, tzinfo=timezone.utc) - draft = PublisherTestModel.publisher_manager.create(title='Test model') - draft.publish() - published = PublisherTestModel.publisher_manager.drafts().get() - draft.publisher_published_at = published_date - draft.save() - published.publisher_published_at = published_date - published.save() - - draft.publish() - draft = PublisherTestModel.publisher_manager.drafts().get() - published = PublisherTestModel.publisher_manager.drafts().get() - self.assertEqual(draft.publisher_published_at, published_date) - self.assertEqual(published.publisher_published_at, published_date) - - def test_published_date_is_set_to_none_when_unpublished(self): - draft = PublisherTestModel.publisher_manager.create(title='Test model') - draft.publish() - draft.unpublish() - self.assertIsNone(draft.publisher_published_at) - - def test_published_date_is_set_when_republished(self): - now = timezone.now() - draft = PublisherTestModel.publisher_manager.create(title='Test model') - draft.publish() - draft.unpublish() - draft.publish() - self.assertGreaterEqual(draft.publisher_published_at, now) - - def test_deleting_draft_also_deletes_published_record(self): - instance = PublisherTestModel.publisher_manager.create(title='Test model') - instance.publish() - instance.delete() - - published = PublisherTestModel.publisher_manager.published().count() - drafts = PublisherTestModel.publisher_manager.drafts().count() - - self.assertEqual(published, 0) - self.assertEqual(drafts, 0) - - def test_delete_published_does_not_delete_draft(self): - obj = PublisherTestModel.publisher_manager.create(title='Test model') - obj.publish() - - published = PublisherTestModel.publisher_manager.published().get() - published.delete() - - published = PublisherTestModel.publisher_manager.published().count() - drafts = PublisherTestModel.publisher_manager.drafts().count() - - self.assertEqual(published, 0) - self.assertEqual(drafts, 1) - - def test_reverting_reverts_draft_from_published_record(self): - title = 'Test model' - instance = PublisherTestModel.publisher_manager.create(title=title) - instance.publish() - instance.title = 'Updated test model' - instance.save() - revert_instance = instance.revert_to_public() - self.assertEqual(title, revert_instance.title) - - def test_only_draft_records_can_be_published_or_reverted(self): - draft = PublisherTestModel.publisher_manager.create(title='Test model') - draft.publish() - - published = PublisherTestModel.publisher_manager.published().get() - self.assertRaises(NotDraftException, published.publish) - self.assertRaises(NotDraftException, published.unpublish) - self.assertRaises(NotDraftException, published.revert_to_public) - - def test_published_signal(self): - # Check the signal was sent. These get lost if they don't reference self. - self.got_signal = False - self.signal_sender = None - self.signal_instance = None - - def handle_signal(sender, instance, **kwargs): - self.got_signal = True - self.signal_sender = sender - self.signal_instance = instance - - publisher_post_publish.connect(handle_signal) - - # call the function - instance = PublisherTestModel.publisher_manager.create(title='Test model') - instance.publish() - - self.assertTrue(self.got_signal) - self.assertEqual(self.signal_sender, PublisherTestModel) - self.assertEqual(self.signal_instance, instance) - - def test_unpublished_signal(self): - # Check the signal was sent. These get lost if they don't reference self. - self.got_signal = False - self.signal_sender = None - self.signal_instance = None - - def handle_signal(sender, instance, **kwargs): - self.got_signal = True - self.signal_sender = sender - self.signal_instance = instance - - publisher_post_unpublish.connect(handle_signal) - - # Call the function. - instance = PublisherTestModel.publisher_manager.create(title='Test model') - instance.publish() - instance.unpublish() - - self.assertTrue(self.got_signal) - self.assertEqual(self.signal_sender, PublisherTestModel) - self.assertEqual(self.signal_instance, instance) - - def test_unpublished_signal_is_sent_when_deleting(self): - self.got_signal = False - self.signal_sender = None - self.signal_instance = None - - def handle_signal(sender, instance, **kwargs): - self.got_signal = True - self.signal_sender = sender - self.signal_instance = instance - - publisher_post_unpublish.connect(handle_signal) - - # Call the function. - instance = PublisherTestModel.publisher_manager.create(title='Test model') - instance.publish() - instance.delete() - - self.assertTrue(self.got_signal) - self.assertEqual(self.signal_sender, PublisherTestModel) - self.assertEqual(self.signal_instance, instance) - - def test_middleware_detects_published_when_logged_out(self): - - class MockUser(object): - is_staff = False - - def is_authenticated(self): - return False - - class MockRequest(object): - user = MockUser() - GET = {'edit': '1'} - - mock_request = MockRequest() - self.assertFalse(PublisherMiddleware.is_draft(mock_request)) - - def test_middleware_detects_published_when_user_edit_parameter_is_missing(self): - - class MockUser(object): - is_staff = True - - def is_authenticated(self): - return True - - class MockRequest(object): - user = MockUser() - GET = {} - - mock_request = MockRequest() - self.assertFalse(PublisherMiddleware.is_draft(mock_request)) - - def test_middleware_detects_published_when_user_is_not_staff(self): - - class MockUser(object): - is_staff = False - - def is_authenticated(self): - return True - - class MockRequest(object): - user = MockUser() - GET = {'edit': '1'} - - mock_request = MockRequest() - self.assertFalse(PublisherMiddleware.is_draft(mock_request)) - - def test_middleware_detects_draft_when_user_is_staff_and_edit_parameter_is_present(self): - - class MockUser(object): - is_staff = True - - def is_authenticated(self): - return True - - class MockRequest(object): - user = MockUser() - GET = {'edit': '1'} - - mock_request = MockRequest() - self.assertTrue(PublisherMiddleware.is_draft(mock_request)) - - def test_middleware_get_draft_status_shortcut_defaults_to_false(self): - self.assertFalse(get_draft_status()) - - def test_middleware_get_draft_status_shortcut_returns_true_in_draft_mode(self): - # Mock the request process to initialise the middleware, but force the middleware to go in - # draft mode. - middleware = PublisherMiddleware() - middleware.is_draft = MagicMock(return_value=True) - middleware.process_request(None) - draft_status = get_draft_status() - PublisherMiddleware.process_response(None, None) - - self.assertTrue(draft_status) - - def test_middleware_get_draft_status_shortcut_does_not_change_draft_status(self): - # The get_draft_status() shortcut shouldn't change the value returned by - # PublisherMiddleware.get_draft_status(). - middleware = PublisherMiddleware() - middleware.is_draft = MagicMock(return_value=True) - middleware.process_request(None) - expected_draft_status = PublisherMiddleware.get_draft_status() - draft_status = get_draft_status() - PublisherMiddleware.process_response(None, None) - - self.assertTrue(expected_draft_status, draft_status) - - def test_middleware_forgets_current_draft_status_after_request(self): - middleware = PublisherMiddleware() - middleware.is_draft = MagicMock(return_value=True) - middleware.process_request(None) - PublisherMiddleware.process_response(None, None) - - self.assertFalse(get_draft_status()) diff --git a/tox.ini b/tox.ini index b058463..96e3c95 100644 --- a/tox.ini +++ b/tox.ini @@ -1,23 +1,29 @@ +# Tox https://github.com/tox-dev/tox is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + [tox] envlist = - {py27,py33,py34,pypy}-{dj18} + {py27,py34,py35,pypy}-{dj18} {py27,py34,py35,pypy}-{dj19,dj110,dj111} {py36}-{dj111} [testenv] -changedir = {toxinidir}/tests -commands = python {toxinidir}/tests/manage.py test myapp +changedir = {toxinidir} basepython = py27: python2.7 - py33: python3.3 py34: python3.4 py35: python3.5 py36: python3.6 pypy: pypy deps = - mock - django_nose dj18: Django>=1.8,<1.9 dj19: Django>=1.9,<1.10 dj110: Django>=1.10,<1.11 - dj111: Django>=1.11,<2.0 \ No newline at end of file + dj111: Django>=1.11,<2.0 +commands = + python --version + make dev_install + pip freeze + py.test -v --cov=publisher --cov-report=html --cov-report=term-missing --cov-append --basetemp={envtmpdir} {toxinidir}/publisher_tests