{ "info": { "author": "Ryan Castner", "author_email": "castner.rr@gmail.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Framework :: Django", "Framework :: Django :: 1.11", "Framework :: Django :: 1.8", "Framework :: Django :: 2.0", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Natural Language :: English", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6" ], "description": "=============================\nDjango Behaviors\n=============================\n\n.. image:: https://badge.fury.io/py/django-behaviors.svg\n :target: https://badge.fury.io/py/django-behaviors\n\n.. image:: https://travis-ci.org/audiolion/django-behaviors.svg?branch=master\n :target: https://travis-ci.org/audiolion/django-behaviors\n\n.. image:: https://codecov.io/gh/audiolion/django-behaviors/branch/master/graph/badge.svg\n :target: https://codecov.io/gh/audiolion/django-behaviors\n\n\nCommon behaviors for Django Models, e.g. Timestamps, Publishing, Authoring/Editing and more.\n\nInspired by Kevin Stone's `Django Model Behaviors`_.\n\nDocumentation\n=============\n\nQuickstart\n----------\n\nInstall Django Behaviors::\n\n pip install django-behaviors\n # Or, if you are going to use the Slugged behaviour\n pip install django-behaviors[slugged]\n\nAdd it to your `INSTALLED_APPS`:\n\n.. code-block:: python\n\n INSTALLED_APPS = (\n ...\n 'behaviors.apps.BehaviorsConfig',\n ...\n )\n\nFeatures\n--------\n\n``behaviors`` makes it easy to integrate common behaviors into your django models:\n\n- **Documented**, **tested**, and **easy to use**\n- **Timestamped** to add ``created`` and ``modified`` attributes to your models\n- **StoreDeleted** to add ``deleted`` attribute to your models, avoiding the record to be deleted and allow to restore it\n- **Authored** to add an ``author`` to your models\n- **Editored** to add an ``editor`` to your models\n- **Published** to add a ``publication_status`` (draft or published) to your models\n- **Released** to add a ``release_date`` to your models\n- **Slugged** to add a ``slug`` to your models (thanks @apirobot) (ensure you have `awesome-slugify` installed, see above)\n- Easily compose together multiple ``behaviors`` to get desired functionality (e.g. ``Authored`` and ``Editored``)\n- Custom ``QuerySet`` methods added as managers to your models to utilize the added fields\n- Easily compose together multiple ``queryset`` or ``manager`` to get desired functionality\n\nTable of Contents\n-----------------\n\n- `Behaviors`_\n - `Timestamped`_\n - `StoreDeleted`_\n - `Authored`_\n - `Editored`_\n - `Published`_\n - `Released`_\n - `Slugged`_\n- `Mixing in with Custom Managers`_\n- `Mixing Multiple Behaviors`_\n\nBehaviors\n---------\n\nTimestamped Behavior\n``````````````````````\n\nThe model adds a ``created`` and ``modified`` field to your model.\n\n.. code-block:: python\n\n class Timestamped(models.Model):\n \"\"\"\n An abstract behavior representing timestamping a model with``created`` and\n ``modified`` fields.\n \"\"\"\n created = models.DateTimeField(auto_now_add=True, db_index=True)\n modified = models.DateTimeField(null=True, blank=True, db_index=True)\n\n class Meta:\n abstract = True\n\n @property\n def changed(self):\n return True if self.modified else False\n\n def save(self, *args, **kwargs):\n if self.pk:\n self.modified = timezone.now()\n return super(Timestamped, self).save(*args, **kwargs)\n\n``created`` is set on the next save and is set to the current UTC time.\n``modified`` is set when the object already exists and is set to the current UTC time.\n\n``MyModel.changed`` returns a boolean representing if the object has been updated after created (the ``modified`` field has been set).\n\nHere is an example of using the model, note you do not need to add ``models.Model`` because ``Timestamped`` already inherits it.\n\n.. code-block:: python\n\n # models.py\n from behaviors.behaviors import Authored, Editored, Timestamped, Published\n\n\n class MyModel(Timestamped):\n name = models.CharField(max_length=100)\n\n\n >>> m = MyModel.objects.create(name='dj')\n >>> m.created\n '2017-02-14 17:20:19.835517+00:00'\n >>> m.modified\n None\n >>> m.changed\n False\n >>> m.save()\n >>> m.modified\n '2017-02-14 17:20:46.836395+00:00'\n >>> m.changed\n True\n\nStoreDeleted Behavior\n``````````````````````\n\nThe model add a ``deleted`` field to your model and prevent record to be deleted and allow to restore it\n\n.. code-block:: python\n\n class StoreDeleted(models.Model):\n \"\"\"\n An abstract behavior representing store deleted a model with``deleted`` field,\n avoiding the model object to be deleted and allowing you to restore it.\n \"\"\"\n deleted = models.DateTimeField(null=True, blank=True)\n\n objects = StoreDeletedQuerySet.as_manager()\n\n class Meta:\n abstract = True\n\n @property\n def is_deleted(self):\n return self.deleted != None\n\n def delete(self, *args, **kwargs):\n if not self.pk:\n raise ObjectDoesNotExist('Object must be created before it can be deleted')\n self.deleted = timezone.now()\n return super(StoreDeleted, self).save(*args, **kwargs)\n\n def restore(self, *args, **kwargs):\n if not self.pk:\n raise ObjectDoesNotExist('Object must be created before it can be restored')\n self.deleted = None\n return super(StoreDeleted, self).save(*args, **kwargs)\n\n``deleted`` is set when ``delete()`` method is called, with current UTC time.\n\nHere is an example of using the model, note you do not need to add ``models.Model`` because ``StoreDeleted`` already inherits it.\n\n.. code-block:: python\n\n # models.py\n from behaviors.behaviors import StoreDeleted\n\n\n class GreatModel(StoreDeleted):\n name = models.CharField(max_length=100)\n\n # Deleting model\n >>> gm = GreatModel.objects.create(name='Xtra')\n >>> gm.deleted\n None\n >>> gm.delete()\n >>> gm.deleted\n '2018-05-14 08:35:41.197661+00:00'\n\n # Restoring model\n >>> gm = GreatModel.objects.deleted(name='Xtra')\n >>> gm.deleted\n '2018-05-14 08:35:41.197661+00:00'\n >>> gm.restore()\n >>> gm.deleted\n None\n\n\nAuthored Behavior\n``````````````````\n\nThe authored model adds an ``author`` attribute that is a foreign key to the ``settings.AUTH_USER_MODEL`` and adds manager methods through ``objects`` and ``authors``.\n\n.. code-block:: python\n\n class Authored(models.Model):\n \"\"\"\n An abstract behavior representing adding an author to a model based on the\n AUTH_USER_MODEL setting.\n \"\"\"\n author = models.ForeignKey(\n settings.AUTH_USER_MODEL,\n related_name=\"%(app_label)s_%(class)s_author\")\n\n objects = AuthoredQuerySet.as_manager()\n authors = AuthoredQuerySet.as_manager()\n\n class Meta:\n abstract = True\n\nHere is an example of using the behavior and its ``authored_by()`` manager method:\n\n.. code-block:: python\n\n # models.py\n from behaviors.behaviors import Authored\n\n\n class MyModel(Authored):\n name = models.CharField(max_length=100)\n\n >>> m = MyModel.objects.create(author=User.objects.get(pk=2), name='tj')\n >>> m.author\n \n >>> queryset = MyModel.objects.authored_by(User.objects.get(pk=2))\n >>> queryset.count()\n 1\n\nThe author is a required field and must be provided on initial ``POST`` requests that create an object.\n\nA custom ``models.ModelForm`` is provided to automatically add the ``author``\non object creation:\n\n.. code-block:: python\n\n # forms.py\n from behaviors.forms import AuthoredModelForm\n from .models import MyModel\n\n\n class MyModelForm(AuthoredModelForm):\n class Meta:\n model = MyModel\n fields = ['name']\n\n # views.py\n from django.views.generic.edit import CreateView\n from .forms import MyModelForm\n from .models import MyModel\n\n\n class MyModelCreateView(CreateView):\n model = MyModel\n form = MyModelForm\n\n # add request to form kwargs\n def get_form_kwargs(self):\n kwargs = super(MyModelCreateView, self).get_form_kwargs()\n kwargs['request'] = self.request\n return kwargs\n\nNow when the object is created the ``author`` will be added on the call\nto ``form.save()``.\n\nIf you are using functional views or another view type you simply need\nto make sure you pass the request object along with the form.\n\n.. code-block:: python\n\n # views.py\n\n class MyModelView(View):\n template_name = \"myapp/mymodel_form.html\"\n\n def get(self, request, *args, **kwargs):\n context = {\n 'form': MyModelForm(),\n }\n return render(request, self.template_name, context=context)\n\n def post(self, request, *args, **kwargs):\n # pass in request object to the request keyword argument\n form = MyModelForm(self.request.POST, request=request)\n if form.is_valid():\n form.save()\n return reverse(..)\n context = {\n 'form': form,\n }\n return render(request, self.template_name, context=context)\n\nIf for some reason you don't want to mixin the ``AuthoredModelForm`` with your existing\nform you can just add the user like so:\n\n.. code-block:: python\n\n # ...\n if form.is_valid()\n obj = form.save(commit=False)\n obj.author = request.user\n obj.save()\n return reverse(..)\n # ...\n\nBut it isn't recommended, the ``AuthoredModelForm`` is tested and doesn't reassign the\nauthor on every save.\n\nThe ``related_name`` is set so that it will never create conflicts. Given the above example if you wanted to do a reverse foreign key lookup from the User model and ``MyModel`` was part of the ``blogs`` app it could be done like so:\n\n.. code-block:: python\n\n >>> user = User.objects.get(pk=2)\n >>> user.blogs_mymodel_author.all()\n []\n\nThat would give a list of all ``MyModel`` objects that ``user`` has ``authored``.\n\nAuthored QuerySet\n..................\n\nThe ``Authored`` behavior attaches a custom model manager to the default ``objects``\nand to the ``authors`` variables on the model it is mixed into. If you haven't overrode\nthe ``objects`` variable with a custom manager then you can use that, otherwise the\n``authors`` variable is a fallback.\n\nTo get all ``MyModel`` instances authored by people whose name starts with 'Jo'\n\n.. code-block:: python\n\n # case is insensitive so 'joe' or 'Joe' matches\n >>> MyModel.objects.authored_by('Jo')\n [, , ...]\n\n # or use the authors manager variable\n >>> MyModel.authors.authored_by('Jo')\n [, , ...]\n\nSee `Mixing in with Custom Managers`_ for details on how\nto mix in this behavior with a custom manager you have that overrides the ``objects``\ndefault manager.\n\n\nEditored Behavior\n``````````````````\n\nThe editored model adds an ``editor`` attribute that is a foreign key to the ``settings.AUTH_USER_MODEL`` and adds manager methods through ``objects`` and ``editors`` variables.\n\n\n.. code-block:: python\n\n class Editored(models.Model):\n \"\"\"\n An abstract behavior representing adding an editor to a model based on the\n AUTH_USER_MODEL setting.\n \"\"\"\n editor = models.ForeignKey(\n settings.AUTH_USER_MODEL,\n related_name=\"%(app_label)s_%(class)s_editor\",\n blank=True, null=True)\n\n objects = EditoredQuerySet.as_manager()\n editors = EditoredQuerySet.as_manager()\n\n class Meta:\n abstract = True\n\nThe ``Editored`` model is similar to the ``Authored`` model except the foreign key is **not required**. Here is an example of its usage:\n\n.. code-block:: python\n\n # models.py\n from behaviors.behaviors import Editored\n\n\n class MyModel(Editored):\n name = models.CharField(max_length=100)\n\n >>> m = MyModel.objects.create(name='pj')\n >>> m.editor\n None\n >>> m.editor = User.objects.all()[0]\n >>> m.save()\n >>> queryset = MyModel.objects.edited_by(User.objects.all()[0])\n >>> queryset.count()\n 1\n\nBy default the ``editor`` is blank and null, if a ``request`` object is supplied to the form it will assign a new editor and erase the previous editor (or the null editor).\n\nInstead of using the ``AuthoredModelForm`` use the ``EditoredModelForm`` as a mixin to\nyour form.\n\n.. code-block:: python\n\n # forms.py\n from behaviors.forms import EditoredModelForm\n from .models import MyModel\n\n\n class MyModelForm(EditoredModelForm):\n class Meta:\n model = MyModel\n fields = ['name']\n\n # views.py\n from django.views.generic.edit import CreateView, UpdateView\n from .forms import MyModelForm\n from .models import MyModel\n\n\n MyModelRequestFormMixin(object):\n # add request to form kwargs\n def get_form_kwargs(self):\n kwargs = super(MyModelCreateView, self).get_form_kwargs()\n kwargs['request'] = self.request\n return kwargs\n\n\n class MyModelCreateView(MyModelRequestFormMixin, CreateView):\n model = MyModel\n form = MyModelForm\n\n\n class MyModelUpdateView(MyModelRequestFormMixin, UpdateView):\n model = MyModel\n form = MyModelForm\n\n\nNow when the object is created or updated the ``editor`` will be updated\non the call to ``form.save()``.\n\nIf you are using functional views or another view type you simply need\nto make sure you pass the request object along with the form.\n\n.. code-block:: python\n\n # views.py\n\n class MyModelView(View):\n template_name = \"myapp/mymodel_form.html\"\n\n def get(self, request, *args, **kwargs):\n context = {\n 'form': MyModelForm(),\n }\n return render(request, self.template_name, context=context)\n\n def post(self, request, *args, **kwargs):\n # pass in request object to the request keyword argument\n form = MyModelForm(self.request.POST, request=request)\n if form.is_valid():\n form.save()\n return reverse(..)\n context = {\n 'form': form,\n }\n return render(request, self.template_name, context=context)\n\nIf for some reason you don't want to mixin the ``EditoredModelForm`` with your existing\nform you can just add the user like so:\n\n.. code-block:: python\n\n ...\n if form.is_valid()\n obj = form.save(commit=False)\n obj.editor = request.user\n obj.save()\n return reverse(..)\n ...\n\nBut it isn't recommended, the ``EditoredModelForm`` is tested and doesn't cause errors\nif request.user is invalid.\n\nThe ``related_name`` is set so that it will never create conflicts. Given the above example if you wanted to do a reverse foreign key lookup from the User model and ``MyModel`` was part of the ``blogs`` app it could be done like so:\n\n.. code-block:: python\n\n >>> user = User.objects.get(pk=2)\n >>> user.blogs_mymodel_editor.all()\n []\n\nThat would give a list of all ``MyModel`` objects that ``user`` is an ``editor``.\n\nEditored QuerySet\n..................\n\nThe ``Editored`` behavior attaches a custom model manager to the default ``objects``\nand to the ``editors`` variables on the model it is mixed into. If you haven't overrode\nthe ``objects`` variable with a custom manager then you can use that, otherwise the\n``editors`` variable is a fallback.\n\nTo get all ``MyModel`` instances edited by people whose name starts with 'Jo'\n\n.. code-block:: python\n\n # case is insensitive so 'joe' or 'Joe' matches\n >>> MyModel.objects.edited_by('Jo')\n [, , ...]\n\n # or use the editors manager variable\n >>> MyModel.editors.edited_by('Jo')\n [, , ...]\n\nSee `Mixing in with Custom Managers`_ for details on how\nto mix in this behavior with a custom manager you have that overrides the ``objects``\ndefault manager.\n\nPublished Behavior\n````````````````````\n\nThe ``Published`` behavior adds a field ``publication_status`` to your model. The status\nhas two states: 'Draft' or 'Published'.\n\n.. code-block:: python\n\n class Published(models.Model):\n \"\"\"\n An abstract behavior representing adding a publication status. A\n ``publication_status`` is set on the model with Draft or Published\n options.\n \"\"\"\n DRAFT = 'd'\n PUBLISHED = 'p'\n\n PUBLICATION_STATUS_CHOICES = (\n (DRAFT, 'Draft'),\n (PUBLISHED, 'Published'),\n )\n\n publication_status = models.CharField(\n \"Publication Status\", max_length=1,\n choices=PUBLICATION_STATUS_CHOICES, default=DRAFT)\n\n class Meta:\n abstract = True\n\n objects = PublishedQuerySet.as_manager()\n publications = PublishedQuerySet.as_manager()\n\n @property\n def draft(self):\n return self.publication_status == self.DRAFT\n\n @property\n def published(self):\n return self.publication_status == self.PUBLISHED\n\nThe class offers two properties ``draft`` and ``published`` to know object state. The ``DRAFT`` and ``PUBLISHED`` class constants will be available from the class the ``Published`` behavior is mixed into. There is also a custom manager attached to ``objects`` and ``publications`` variables to get ``published()`` or ``draft()`` querysets.\n\n.. code-block:: python\n\n # models.py\n from behaviors.behaviors import Published\n\n\n class MyModel(Published):\n name = models.CharField(max_length=100)\n\n >>> m = MyModel.objects.create(name='cj')\n >>> m.publication_status\n u'd'\n >>> m.draft\n True\n >>> m.published\n False\n >>> m.get_publication_status_display()\n u'Draft'\n >>> MyModel.objects.published().count()\n 0\n >>> MyModel.objects.draft().count()\n 1\n >>> m.publication_status = MyModel.PUBLISHED\n >>> m.save()\n >>> m.publication_status\n u'p'\n >>> m.draft\n False\n >>> m.published\n True\n >>> m.get_publication_status_display()\n u'Published'\n >>> MyModel.objects.published().count()\n 1\n >>> MyModel.PUBLISHED\n u'p'\n >>> MyModel.PUBLISHED == m.publication_status\n True\n\nThe ``publication_status`` field defaults to ``Published.DRAFT`` when you make new\nmodels unless you supply the ``Published.PUBLISHED`` attribute to the ``publication_status``\nfield.\n\n.. code-block:: python\n\n MyModel.objects.create(name='Jim-bob Cooter', publication_status=MyModel.PUBLISHED)\n\nPublished QuerySet\n...................\n\nThe ``Published`` behavior attaches to the default ``objects`` variable and\nthe ``publications`` variable as a fallback if ``objects`` is overrode.\n\n.. code-block:: python\n\n # returns all MyModel.PUBLISHED\n MyModel.objects.published()\n MyModel.publications.published()\n\n # returns all MyModel.DRAFT\n MyModel.objects.draft()\n MyModel.publications.draft()\n\n\nReleased Behavior\n``````````````````\n\nThe ``Released`` behavior adds a field ``release_date`` to your model. The field\nis **not_required**. The release date can be set with the ``release_on(datetime)`` method.\n\n.. code-block:: python\n\n class Released(models.Model):\n \"\"\"\n An abstract behavior representing a release_date for a model to\n indicate when it should be listed publically.\n \"\"\"\n release_date = models.DateTimeField(null=True, blank=True)\n\n class Meta:\n abstract = True\n\n objects = ReleasedQuerySet.as_manager()\n releases = ReleasedQuerySet.as_manager()\n\n def release_on(self, date=None):\n if not date:\n date = timezone.now()\n self.release_date = date\n self.save()\n\n @property\n def released(self):\n return self.release_date and self.release_date < timezone.now()\n\nThere is a ``released`` property added which determines if the object has been released. There is a custom manager attached to ``objects`` and ``releases`` variables to filter querysets on their release date.\n\nHere is an example of using the behavior:\n\n.. code-block:: python\n\n # models.py\n from django.utils import timezone\n from datetime import timedelta\n from behaviors.behaviors import Released\n\n\n class MyModel(Released):\n name = models.CharField(max_length=100)\n\n >>> m = MyModel.objects.create(name='rj')\n >>> m.release_date\n None\n >>> MyModel.objects.no_release_date().count()\n 1\n >>> m.release_on()\n >>> MyModel.objects.no_release_date().count()\n 0\n >>> MyModel.objects.released().count()\n 1\n >>> m.release_on(timezone.now() + timedelta(weeks=1))\n >>> MyModel.objects.not_released().count()\n 1\n >>> MyModel.objects.released().count()\n 0\n\nThe ``release_on`` method defaults to the current time so that the object is immediately\nreleased. You can also provide a date to the method to release on a certain date. ``release_on()`` just serves as a wrapper to setting and saving the date.\n\nYou can always provide a ``release_date`` on object creation:\n\n.. code-block:: python\n\n MyModel.objects.create(name='Jim-bob Cooter', release_date=timezone.now())\n\n\nReleased QuerySet\n...................\n\nThe ``Released`` behavior attaches to the default ``objects`` variable and\nthe ``releases`` variable as a fallback if ``objects`` is overrode.\n\n.. code-block:: python\n\n # returns all not released MyModel objects\n MyModel.objects.not_released()\n MyModel.releases.not_released()\n\n # returns all released MyModel objects\n MyModel.objects.released()\n MyModel.releases.released()\n\n # returns all null release date MyModel objects\n MyModel.objects.no_release_date()\n MyModel.releases.no_release_date()\n\nSlugged Behavior\n``````````````````\n\nThe ``Slugged`` behavior allows you to easily add a ``slug`` field to your model. The slug is generated on the first model creation or the next model save and is based on the ``slug_source`` attribute.\n\n**The** ``slug_source`` **property has no set default, you must add it to your model for the behavior to work.**\n\n.. code-block:: python\n\n class Slugged(models.Model):\n \"\"\"\n An abstract behavior representing adding a unique slug to a model\n based on the slug_source property.\n \"\"\"\n slug = models.SlugField(max_length=255, unique=True)\n\n class Meta:\n abstract = True\n\n def save(self, *args, **kwargs):\n if not self.slug:\n self.slug = self.generate_unique_slug()\n super(Slugged, self).save(*args, **kwargs)\n\n def get_slug(self):\n return slugify(getattr(self, \"slug_source\"), to_lower=True)\n\n def is_unique_slug(self, slug):\n qs = self.__class__.objects.filter(slug=slug)\n return not qs.exists()\n\n def generate_unique_slug(self):\n slug = self.get_slug()\n new_slug = slug\n\n iteration = 1\n while not self.is_unique_slug(new_slug):\n new_slug = \"%s-%d\" % (slug, iteration)\n iteration += 1\n\n return new_slug\n\nThe ``slug`` uses the awesome-slugify package which will preserve unicode character slugs. The ``slug`` must be unique and is guaranteed to be unique by the class appending a number ``-[0-9+]`` to the end of the slug if it is not unique. The ``unique`` field type `adds an index`_ to the ``slug`` field.\n\nAdd the ``slug_source`` property to your class when mixing in the behavior.\n\n.. code-block:: python\n\n # models.py\n from behaviors.behaviors import Slugged\n\n\n class MyModel(Slugged):\n name = models.CharField(max_length=100)\n\n # slug_source is required for the slug to be set\n @property\n def slug_source(self):\n return \"prepended-text-for-fun-{}\".format(self.name)\n\n # you can now use the slug for your get_absolute_url() method\n def get_absolute_url(self):\n return reverse('myapp:mymodel_detail', args=[self.slug])\n\n >>> m = MyModel.objects.create(name='aj')\n >>> m.slug\n 'prepended-text-for-fun-aj'\n >>> m2 = MyModel.objects.create(name='aj')\n >>> m.slug\n 'prepended-text-for-fun-aj-1'\n >>> m.get_absolute_url()\n '/myapp/prepended-text-for-fun-aj/detail'\n\nYour ``slug_source`` attribute can be a mix of any of the model data available at the time of save, generally it is some ``name`` type of field. You could also hash the primary key and/or some other data as a ``slug_source``. The ``slug`` is unique so it can be used to define the ``get_absolute_url()`` method on your model.\n\nThanks to @apirobot for sending the PR for the ``Slugged`` behavior.\n\nMixing in with Custom Managers\n------------------------------\n\nIf you have a custom manager on your model already:\n\n.. code-block:: python\n\n # models.py\n from behaviors.behaviors import Authored, Editored, Published, Timestamped\n\n from django.db import models\n\n\n class MyModelCustomManager(models.Manager):\n\n def get_queryset(self):\n return super(MyModelCustomManager).get_queryset(self)\n\n def custom_manager_method(self):\n return self.get_queryset().filter(name='Jim-bob')\n\n class MyModel(Authored):\n name = models.CharField(max_length=100)\n\n # MyModel.objects.authored_by(..) won't work\n # MyModel.authors.authored_by(..) still will\n objects = MyModelCustomManager()\n\nSimply add ``AuthoredManager`` from ``behaviors.managers`` as a mixin to\n``MyModelCustomManager`` so they can share the ``objects`` variable.\n\n.. code-block:: python\n\n # models.py\n from behaviors.behaviors import Authored, Editored, Published, Timestamped\n from behaviors.managers import AuthoredManager, EditoredManager, PublishedManager\n\n from django.db import models\n\n\n class MyModelCustomManager(AuthoredManager, models.Manager):\n\n def get_queryset(self):\n return super(MyModelCustomManager).get_queryset(self)\n\n def custom_manager_method(self):\n return self.get_queryset().filter(name='Jim-bob')\n\n class MyModel(Authored):\n name = models.CharField(max_length=100)\n\n # MyModel.objects.authored_by(..) now works\n objects = MyModelCustomManager()\n\nSimilarly if you are using a custom QuerySet and calling its ``as_manager()``\nmethod to attach it to ``objects`` you can import from ``behaviors.querysets``\nand mix it in.\n\n.. code-block:: python\n\n # models.py\n from behaviors.behaviors import Authored, Editored, Published, Timestamped\n from behaviors.querysets import AuthoredQuerySet, EditoredQuerySet, PublishedQuerySet\n\n from django.db import models\n\n\n class MyModelCustomQuerySet(AuthoredQuerySet, models.QuerySet):\n\n def custom_queryset_method(self):\n return self.filter(name='Jim-bob')\n\n class MyModel(Authored):\n name = models.CharField(max_length=100)\n\n # MyModel.objects.authored_by(..) works\n objects = MyModelCustomQuerySet.as_manager()\n\n\nMixing in Multiple Behaviors\n----------------------------\n\nMany times you will want multiple behaviors on a model. You can simply mix in\nmultiple behaviors and, if you'd like to have all their custom ``QuerySet``\nmethods work on ``objects``, provide a custom manager with all the mixins.\n\n.. code-block:: python\n\n # models.py\n from behaviors.behaviors import Authored, Editored, Published, Timestamped\n from behaviors.querysets import AuthoredQuerySet, EditoredQuerySet, PublishedQuerySet\n\n from django.db import models\n\n\n class MyModelQuerySet(AuthoredQuerySet, EditoredQuerySet, PublishedQuerySet):\n pass\n\n class MyModel(Authored, Editored, Published, Timestamped):\n name = models.CharField(max_length=100)\n\n # MyModel.objects.authored_by(..) works\n # MyModel.objects.edited_by(..) works\n # MyModel.objects.published() works\n # MyModel.objects.draft() works\n objects = MyModelQuerySet.as_manager()\n\n # you can also chain queryset methods\n >>> u = User.objects.all()[0]\n >>> u2 = User.objects.all()[1]\n >>> m = MyModel.objects.create(author=u, editor=u2)\n >>> MyModel.objects.published().authored_by(u).count()\n 1\n\n\nRunning Tests\n-------------\n\nDoes the code actually work?\n\n::\n\n source /bin/activate\n (myenv) $ pip install tox\n (myenv) $ tox\n\nCredits\n-------\n\nTools used in rendering this package:\n\n* Cookiecutter_\n* `cookiecutter-djangopackage`_\n\n.. _Cookiecutter: https://github.com/audreyr/cookiecutter\n.. _`cookiecutter-djangopackage`: https://github.com/pydanny/cookiecutter-djangopackage\n\n.. _`Timestamped`: #timestamped-behavior\n.. _`StoreDeleted`: #storedeleted-behavior\n.. _`Authored`: #authored-behavior\n.. _`Editored`: #editored-behavior\n.. _`Published`: #published-behavior\n.. _`Released`: #released-behavior\n.. _`Slugged`: #slugged-behavior\n.. _`settings.AUTH_USER_MODEL`: https://docs.djangoproject.com/en/1.10/ref/settings/#std:setting-AUTH_USER_MODEL\n.. _`Mixing in with Custom Managers`: #mixing-in-with-custom-managers\n.. _`Mixing Multiple Behaviors`: #mixing-in-multiple-behaviors\n.. _`Django Model Behaviors`: http://blog.kevinastone.com/django-model-behaviors.html\n.. _`adds an index`: https://docs.djangoproject.com/en/dev/ref/models/fields/#unique\n\n\n\n\nHistory\n=======\n\n\n0.5.0 (2019-07-01)\n------------------\n\n* Drop Django 2.0 support and Python 3.4 support as they have reached EOL\n* Add Django 2.1, 2.2 and Python 3.7 support\n* Feature: Add db_index to ``Timestamped`` behavior's ``modified`` field (thanks @kazqvaizer)\n\n0.4.1 (2018-05-17)\n------------------\n\n* Feature: Add ``StoreDeleted`` behavior to soft delete models (thanks @abekroenem)\n\n0.4.0 (2018-01-16)\n------------------\n\n* Update setup.py classifiers\n\n0.3.1 (2018-01-04)\n------------------\n\n* Add Django 2.0 and Python 3.6 Support\n\n0.3.0 (2017-03-11)\n------------------\n\n* Add ``Slugged`` behavior adding a slug to models\n* Update documentation\n\n0.2.0 (2017-02-14)\n------------------\n\n* Add ``Released`` behavior for a release date on models\n* Update documentation\n\n0.1.7 (2017-02-14)\n------------------\n\n* Remove an unused import\n* Integrate with Lintly\n\n0.1.6 (2017-02-14)\n------------------\n\n* Drop python3.3 support for Django 1.8 because 1.8 no longer supports it\n\n0.1.5 (2017-02-14)\n------------------\n\n* Fix import error for py2.7 builds\n\n0.1.4 (2017-02-14)\n------------------\n\n* Fix Syntax Error\n\n0.1.3 (2017-02-14)\n------------------\n\n* Fixed Circular Import\n\n0.1.2 (2017-02-13)\n------------------\n\n* Travis CI Fixes\n\n0.1.1 (2017-02-13)\n------------------\n\n* First release on PyPI\n* Flake8 adherence fixes\n\n0.1.0 (2017-02-13)\n------------------\n\n* First push of project\n\n\n", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/audiolion/django-behaviors", "keywords": "django-behaviors", "license": "MIT", "maintainer": "", "maintainer_email": "", "name": "django-behaviors", "package_url": "https://pypi.org/project/django-behaviors/", "platform": "", "project_url": "https://pypi.org/project/django-behaviors/", "project_urls": { "Homepage": "https://github.com/audiolion/django-behaviors" }, "release_url": "https://pypi.org/project/django-behaviors/0.5.0/", "requires_dist": null, "requires_python": "", "summary": "Common behaviors for Django Models, e.g. Timestamps, Publishing, Authoring/Editing and more.", "version": "0.5.0" }, "last_serial": 5474532, "releases": { "0.1.6": [ { "comment_text": "", "digests": { "md5": "94f58c55428c98cc960e23a7a7253720", "sha256": "6b4d53e10730830ca325f0bf7dd18b351c682b7a475aba5a6d2dbd939c8258a4" }, "downloads": -1, "filename": "django_behaviors-0.1.6-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "94f58c55428c98cc960e23a7a7253720", "packagetype": "bdist_wheel", "python_version": "3.5", "requires_python": null, "size": 11623, "upload_time": "2017-02-14T05:32:59", "url": "https://files.pythonhosted.org/packages/76/01/055f574b3af2931ab6f7570fcf22c3f5e25601f01a658f32c73e5f1b02f9/django_behaviors-0.1.6-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "c186afc1e9a65e9c8751e8a1a214a1eb", "sha256": "5de1387cfa24e4c9340252e905689abbed85c22922e86e77c6429a55a2cbf2c6" }, "downloads": -1, "filename": "django-behaviors-0.1.6.tar.gz", "has_sig": false, "md5_digest": "c186afc1e9a65e9c8751e8a1a214a1eb", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 9814, "upload_time": "2017-02-14T05:32:57", "url": "https://files.pythonhosted.org/packages/c0/76/1e12a19505f92e1fe9987276aac204f447969c4186b6a412cb8e2c19357f/django-behaviors-0.1.6.tar.gz" } ], "0.2.0": [ { "comment_text": "", "digests": { "md5": "ee34531374c3602523e280e3f8ac7c5d", "sha256": "769cc81058cce819bc9e75a3b0d85f7f350761c35180bf526e165d227524ae8d" }, "downloads": -1, "filename": "django_behaviors-0.2.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "ee34531374c3602523e280e3f8ac7c5d", "packagetype": "bdist_wheel", "python_version": "3.5", "requires_python": null, "size": 13765, "upload_time": "2017-02-14T17:48:14", "url": "https://files.pythonhosted.org/packages/27/9c/5c54e18d8514afcc25304d66f3611b1471ef5b5ae42cc01f7cf8b78be54a/django_behaviors-0.2.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "039b579bf9c4f2ac65d13bf0a76af805", "sha256": "20b6432d00c6d9368e27d519f9fe2672733b2f5ae370a836733f93cd72bf091d" }, "downloads": -1, "filename": "django-behaviors-0.2.0.tar.gz", "has_sig": false, "md5_digest": "039b579bf9c4f2ac65d13bf0a76af805", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 14294, "upload_time": "2017-02-14T17:48:11", "url": "https://files.pythonhosted.org/packages/22/f3/e4f76133b1660f13c3d96711a13f7613b828ed8caed339bc0b604b86b1a9/django-behaviors-0.2.0.tar.gz" } ], "0.3.0": [ { "comment_text": "", "digests": { "md5": "eb66f554d19a29f9c02f1fadf5a32fcc", "sha256": "9d95810d6b01cc32e441895e7f0b962947bd61b547f0271144d1c2871ee51c61" }, "downloads": -1, "filename": "django_behaviors-0.3.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "eb66f554d19a29f9c02f1fadf5a32fcc", "packagetype": "bdist_wheel", "python_version": "3.5", "requires_python": null, "size": 18185, "upload_time": "2017-03-13T03:53:21", "url": "https://files.pythonhosted.org/packages/b7/df/bf88f14f14e8fa6679495dce8816f368a69ab27d2b3762e0516fd5750264/django_behaviors-0.3.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "bc55cf29f0a3d9f11c070f70679c7398", "sha256": "639c191b3c5fd60941da61ab58b046cd0ea2add19ac7dfd1b053578e252fbced" }, "downloads": -1, "filename": "django-behaviors-0.3.0.tar.gz", "has_sig": false, "md5_digest": "bc55cf29f0a3d9f11c070f70679c7398", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 21418, "upload_time": "2017-03-13T03:53:19", "url": "https://files.pythonhosted.org/packages/d5/28/8a673b0982ae2a794a1e3992fbbdf9e5964320d85854af0c6da74aa76126/django-behaviors-0.3.0.tar.gz" } ], "0.3.1": [ { "comment_text": "", "digests": { "md5": "a0ed5c47a32b0677d74c9276063260fc", "sha256": "c905e3e868c3978be29b4ce1a5b488f082c4686a8781d66e7d0e9cd10fec951d" }, "downloads": -1, "filename": "django_behaviors-0.3.1-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "a0ed5c47a32b0677d74c9276063260fc", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 18613, "upload_time": "2018-01-04T17:27:35", "url": "https://files.pythonhosted.org/packages/e3/79/e2307f7674c8ae684a0673526c75c7f740ab761c39ca0e1efb6840e2b353/django_behaviors-0.3.1-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "f93712251e154c09880bd2b3ef433ace", "sha256": "b640907374d6b5597fc4950e188e966c0da2e696d845e9c3212dbe86f9e3f15c" }, "downloads": -1, "filename": "django-behaviors-0.3.1.tar.gz", "has_sig": false, "md5_digest": "f93712251e154c09880bd2b3ef433ace", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 21951, "upload_time": "2018-01-04T17:27:33", "url": "https://files.pythonhosted.org/packages/90/0c/4fa00017004b3ec42be16d3188b0f2abc1417a318e2c637f54f44fc3af4b/django-behaviors-0.3.1.tar.gz" } ], "0.4.0": [ { "comment_text": "", "digests": { "md5": "2c0731a45fbd8d018bade8593c4c385b", "sha256": "35ce31e2f83232e67c054cb982d5d622b250d10cd97ec8446f264e498490341e" }, "downloads": -1, "filename": "django_behaviors-0.4.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "2c0731a45fbd8d018bade8593c4c385b", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 18612, "upload_time": "2018-01-16T19:28:31", "url": "https://files.pythonhosted.org/packages/a3/e9/f7d02ef29b271bb476c1cfef28afd6aacca6dab6f2eff57bfb37252bbe14/django_behaviors-0.4.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "ed57ff21e10e3356bd4d17ff0373b866", "sha256": "974b75b224d01e4ec1d0cbe9f86cb972c73fe5e5013c5b7eec2962184b77d9ac" }, "downloads": -1, "filename": "django-behaviors-0.4.0.tar.gz", "has_sig": false, "md5_digest": "ed57ff21e10e3356bd4d17ff0373b866", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 21952, "upload_time": "2018-01-16T19:28:29", "url": "https://files.pythonhosted.org/packages/d2/91/fbdf3a1f5edc849a8f9ba8fb7ea637c74896498877b390ed852651102613/django-behaviors-0.4.0.tar.gz" } ], "0.4.1": [ { "comment_text": "", "digests": { "md5": "3a8e18d5c79bac64f5be63d932248dd9", "sha256": "4810f7d1a671ce28731d590fdcfa71a1b1bb79b8733edcfc2227a7ac608d2781" }, "downloads": -1, "filename": "django_behaviors-0.4.1-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "3a8e18d5c79bac64f5be63d932248dd9", "packagetype": "bdist_wheel", "python_version": "3.6", "requires_python": null, "size": 19765, "upload_time": "2018-05-18T13:59:07", "url": "https://files.pythonhosted.org/packages/40/ee/5e504ba460e2b796d418112208ce597314f92018602e429fbd3f550f1521/django_behaviors-0.4.1-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "2ada4985367574ce5f2f067aa8802d87", "sha256": "684e4dc9525a1971fbbf733b1396a58ab81fe9bcafbebbca9f159c5ddc806161" }, "downloads": -1, "filename": "django-behaviors-0.4.1.tar.gz", "has_sig": false, "md5_digest": "2ada4985367574ce5f2f067aa8802d87", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 24592, "upload_time": "2018-05-18T13:59:05", "url": "https://files.pythonhosted.org/packages/eb/28/81117c9a0e8efa94b47ff91d25df60e1a5e936a6481b5173eba57d2f0849/django-behaviors-0.4.1.tar.gz" } ], "0.5.0": [ { "comment_text": "", "digests": { "md5": "c95710881f02869f3693f5c382ac8cc9", "sha256": "913c235b55e20698820ca2664b624d62c7b4f6fd2834aca658fdfb22d5cb4f79" }, "downloads": -1, "filename": "django_behaviors-0.5.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "c95710881f02869f3693f5c382ac8cc9", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 20097, "upload_time": "2019-07-02T01:42:47", "url": "https://files.pythonhosted.org/packages/ad/ae/a519c109b6b69724354a146fa7c3007602434a030296ca71e1906a3f1fd8/django_behaviors-0.5.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "80d4e4e6a962fab28328acef35b93285", "sha256": "2e0180944e5529624bd543144611964189fb5e02aea43f24045f69f6704d2860" }, "downloads": -1, "filename": "django-behaviors-0.5.0.tar.gz", "has_sig": false, "md5_digest": "80d4e4e6a962fab28328acef35b93285", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 25334, "upload_time": "2019-07-02T01:42:49", "url": "https://files.pythonhosted.org/packages/70/32/46203c2e662cb8fba0642be6fbbe271dfc0bad4495a3a0e8c67e46d36c19/django-behaviors-0.5.0.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "c95710881f02869f3693f5c382ac8cc9", "sha256": "913c235b55e20698820ca2664b624d62c7b4f6fd2834aca658fdfb22d5cb4f79" }, "downloads": -1, "filename": "django_behaviors-0.5.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "c95710881f02869f3693f5c382ac8cc9", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 20097, "upload_time": "2019-07-02T01:42:47", "url": "https://files.pythonhosted.org/packages/ad/ae/a519c109b6b69724354a146fa7c3007602434a030296ca71e1906a3f1fd8/django_behaviors-0.5.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "80d4e4e6a962fab28328acef35b93285", "sha256": "2e0180944e5529624bd543144611964189fb5e02aea43f24045f69f6704d2860" }, "downloads": -1, "filename": "django-behaviors-0.5.0.tar.gz", "has_sig": false, "md5_digest": "80d4e4e6a962fab28328acef35b93285", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 25334, "upload_time": "2019-07-02T01:42:49", "url": "https://files.pythonhosted.org/packages/70/32/46203c2e662cb8fba0642be6fbbe271dfc0bad4495a3a0e8c67e46d36c19/django-behaviors-0.5.0.tar.gz" } ] }