diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..37d6f0f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +# Based on +# https://pypi.org/project/tox-gh-actions/ + +--- +name: Test the application using Tox. + +on: + - push + - pull_request + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + + steps: + - name: apt update + run: sudo apt update + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox tox-gh-actions + - name: Test with tox + run: tox -v diff --git a/.travis.yml b/.travis.yml index 92f1f96..8b07556 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,15 @@ language: python + python: - - "2.7" - - "3.4" - "3.5" - "3.6" + - "3.7" + - "3.8" + env: - - DJANGO=1.7 - - DJANGO=1.8 - - DJANGO=1.9 - - DJANGO=1.10 - - DJANGO=1.11 - - DJANGO=2.0 + - DJANGO=2.2 + - DJANGO=3.0 + install: # command to install dependencies - "pip install coveralls" @@ -18,17 +17,15 @@ install: - pip install -q Django==$DJANGO - "pip install ." # command to run tests + script: + - SAMPLE_APP=1 coverage run --branch --source=notifications manage.py test - coverage run --branch --source=notifications manage.py test + matrix: exclude: - - python: "2.7" - env: DJANGO=2.0 - python: "3.5" - env: DJANGO=1.7 - - python: "3.6" - env: DJANGO=1.7 - + env: DJANGO=3.0 after_success: - coveralls diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d074b52 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,164 @@ +# Changelog + +## 1.7.0 + + - Added support for Django 3.2 and Django 4.0 + - Fixed bug on IE11 for using `forEach` in notify.js + +## 1.6.0 + + - Added support to Django up to version 3.0 + - Added `AbstractNotification` model + - Added prefetch for actor field in admin + - Added never\_cache to some views to avoid no-update bug + +## 1.5 + +Now all configs for the app are made inside the dictionary +*DJANGO\_NOTIFICATION\_CONFIG* in *settings.py*. + +Default configs: `` `Python DJANGO_NOTIFICATION_CONFIG = { +'PAGINATE_BY': 20, 'USE_JSONFIELD': False, 'SOFT_DELETE': False, +'NUM_TO_FETCH': 10, } ``\` + + - Improve code quality. (@AlvaroLQueiroz) + - Improve url patterns and remove duplicated code. (@julianogouveia) + - Added a view for show all notifications. \#205 (@AlvaroLQueiroz) + - Added a new tag to verify if an user has unread notifications. \#164 + (@AlvaroLQueiroz) + - Improve documentation. (@pandabearcoder) + - Fix pagination in list views. \#69 (@AlvaroLQueiroz) + - Improve test matrix. (@AlvaroLQueiroz) + +## 1.4 + + - Adds support for django 2.0.0 (@jphamcsp and @nemesisdesign). + - Adds database index for some fields (@nemesisdesign). + - Changes the ID-based selection to a class-based selection in the + methods + \_\_[live\_notify\_badge](THIS%20VERSION%20HAS%20BREAKING%20CHANGES__:) + and \_\_live\_notify\_list\_\_ (@AlvaroLQueiroz). + - Now extra data and slug are returned on + \_\_live\_unread\_notification\_list\_\_ API (@AlvaroLQueiroz). + - Fix documentation issues (@archatas, @yaoelvon and @AlvaroLQueiroz). + +## 1.3 + + - Redirect to unread view after mark as read. (@osminogin) + - Django 1.10 compability. (@osminogin) + - Django Admin overhead reduction by removing the need to carry all + recipients users. (@theromis) + - Added option to mark as read in + \_\_live\_unread\_notification\_list\_\_ endpoint. (@osminogin) + - Fixed parameter name error in README.rst: there is no + \_\_api\_url\_name\_\_ parameter, the correct name is + \_\_api\_name\_\_ (@ikkebr) + - Added \_\_sent()\_\_, \_\_unsent()\_\_, \_\_mark\_as\_sent()\_\_ and + \_\_mark\_as\_unsent()\_\_ methods in the queryset. (@theromis) + - \_\_notify.send()\_\_ now returns the list of saved Notifications + instances. (@satyanash) + - Now \_\_recipient\_\_ can be a User queryset. (@AlvaroLQueiroz) + - Fix XMLHttpRequest onready event handler. (@AlvaroLQueiroz) + +## 1.2 + + - Django 1.9 template tag compatibility: due to `register.simple_tag` + automatically espacing `unsafe_html` in Django 1.9, it is now + recommended to use format\_html (@ikkebr) + - Fixed parameter name error in README.rst: there is no to\_fetch + parameter, the correct name is fetch (@ikkebr) + - Add missing migration (@marcgibbons) + - Minor documentation correction (@tkwon, @zhang-z) + - Return updated count in QuerySet (@zhang-z) + +## 1.1 + + - Custom now() invocation got overlooked by PR \#113 (@yangyuvo) + - Added sentinals for unauthenticated users, preventing a 500 error + (@LegoStormtroopr) + - Fix: Mark All As read fails if soft-deleted \#126 (@zhang-z) + +## 1.0 + +The first major version that requires Django 1.7+. + + - Drop support for Django 1.6 and below (@zhang-z) + - Django 1.9 compability (@illing2005) + - Now depends on Django built-in migration facility, + "south\_migrations" dependence was removed (@zhang-z) + - Make django-notification compatible with django-model-utils \>= 2.4 + ( \#87, \#88, \#90 ) (@zhang-z) + - Fix a RemovedInDjango110Warning in unittest (@zhang-z) + - Fix pep8 & use setuptools (@areski) + - Fix typo- in doc (@areski, @zhang-z) + - Add app\_name in urls.py (@zhang-z) + - Use Django's vendored copy of six (@funkybob) + - Tidy with flake8 (@funkybob) + - Remove custom now() function (@funkybob, @yangyubo) + - notify.send() accepts User or Group (@Evidlo) + +## 0.8.0 + +0.8 is the last major version supports Django 1.4\~1.6, version 0.8.0 +will go into bugfix mode, no new features will be accepted. + + - Bugfixes for live-updater, and added a live tester page + (@LegoStormtroopr) + - Class-based classes (@alazaro) + - Fixed urls in tests (@alazaro) + - Added app\_label to Notification model in order to fix a Django 1.9 + deprecation warning (@Heldroe) + - django-model-utils compatible issue (must \>=2.0.3 and \<2.4) + (@zhang-z) + - Reliable setup.py versioning (@yangyubo) + +## 0.7.1 + + - Able to pass level when adding notification (@Arthur) + - Fix deprecation notice in Django 1.8 (@ashokfernandez) + - Fix Python 3 support for notification model (@philroche) + - Bugfix for wrong user unread notification count (@Geeknux) + - A simple javascript API for live-updating specific fields within a + django template (@LegoStormtroopr) + - Add missing migration for Notification model (@shezadkhan137) + +## 0.7.0 + + - Add filters and displays to Django model Admin + - Support Django 1.8, compatible with both django-south (django \< + 1.7) and built-in schema migration (django \>= 1.7) + - Compatible with Python 3 + - Test fixtures, and integrated with travis-ci + +## 0.6.2 + + - Fix README.rst reStructuredText syntax format + - Use relative imports + - Add contributors to AUTHORS.txt + +## 0.6.1 + + - Add support for custom user model + - mark\_as\_unread + - Require django-model-utils \>= 2.0.3 + - Use different now function according + to the USE\_TZ setting + +## 0.6.0 + + - Improve documentation + - Add unicode support at admin panel or shell + +## 0.5.5 + +Support for arbitrary data attribute. + +## 0.5.1 + +Fix package descriptions and doc links. + +## 0.5 + +First version based on +[django-activity-stream](https://github.com/justquick/django-activity-stream) +v0.4.3 diff --git a/CHANGELOG.rst b/CHANGELOG.rst deleted file mode 100644 index 023ed2a..0000000 --- a/CHANGELOG.rst +++ /dev/null @@ -1,147 +0,0 @@ -Changelog -========= - -1.5 ----- -__THIS VERSION HAS BREAKING CHANGES__: -Now all configs for the app are made inside the dictionary *DJANGO_NOTIFICATION_CONFIG* in *settings.py*. - -Default configs: -```Python -DJANGO_NOTIFICATION_CONFIG = { - 'PAGINATE_BY': 20, - 'USE_JSONFIELD': False, - 'SOFT_DELETE': False, - 'NUM_TO_FETCH': 10, -} -``` - -- Improve code quality. (@AlvaroLQueiroz) -- Improve url patterns and remove duplicated code. (@julianogouveia) -- Added a view for show all notifications. #205 (@AlvaroLQueiroz) -- Added a new tag to verify if an user has unread notifications. #164 (@AlvaroLQueiroz) -- Improve documentation. (@pandabearcoder) -- Fix pagination in list views. #69 (@AlvaroLQueiroz) -- Improve test matrix. (@AlvaroLQueiroz) - -1.4 ----- - -- Adds support for django 2.0.0 (@jphamcsp and @nemesisdesign). -- Adds database index for some fields (@nemesisdesign). -- Changes the ID-based selection to a class-based selection in the methods __live_notify_badge__ and __live_notify_list__ (@AlvaroLQueiroz). -- Now extra data and slug are returned on __live_unread_notification_list__ API (@AlvaroLQueiroz). -- Fix documentation issues (@archatas, @yaoelvon and @AlvaroLQueiroz). - -1.3 ------ - -- Redirect to unread view after mark as read. (@osminogin) -- Django 1.10 compability. (@osminogin) -- Django Admin overhead reduction by removing the need to carry all recipients users. (@theromis) -- Added option to mark as read in __live_unread_notification_list__ endpoint. (@osminogin) -- Fixed parameter name error in README.rst: there is no __api_url_name__ parameter, the correct name is __api_name__ (@ikkebr) -- Added __sent()__, __unsent()__, __mark_as_sent()__ and __mark_as_unsent()__ methods in the queryset. (@theromis) -- __notify.send()__ now returns the list of saved Notifications instances. (@satyanash) -- Now __recipient__ can be a User queryset. (@AlvaroLQueiroz) -- Fix XMLHttpRequest onready event handler. (@AlvaroLQueiroz) - -1.2 ------ - -- Django 1.9 template tag compatibility: due to ``register.simple_tag`` automatically espacing ``unsafe_html`` in Django 1.9, it is now recommended to use format_html (@ikkebr) -- Fixed parameter name error in README.rst: there is no to_fetch parameter, the correct name is fetch (@ikkebr) -- Add missing migration (@marcgibbons) -- Minor documentation correction (@tkwon, @zhang-z) -- Return updated count in QuerySet (@zhang-z) - -1.1 ------ - -- Custom now() invocation got overlooked by PR #113 (@yangyuvo) -- Added sentinals for unauthenticated users, preventing a 500 error (@LegoStormtroopr) -- Fix: Mark All As read fails if soft-deleted #126 (@zhang-z) - -1.0 ------ - -The first major version that requires Django 1.7+. - -- Drop support for Django 1.6 and below (@zhang-z) -- Django 1.9 compability (@illing2005) -- Now depends on Django built-in migration facility, "south_migrations" dependence was removed (@zhang-z) -- Make django-notification compatible with django-model-utils >= 2.4 ( #87, #88, #90 ) (@zhang-z) -- Fix a RemovedInDjango110Warning in unittest (@zhang-z) -- Fix pep8 & use setuptools (@areski) -- Fix typo- in doc (@areski, @zhang-z) -- Add app_name in urls.py (@zhang-z) -- Use Django's vendored copy of six (@funkybob) -- Tidy with flake8 (@funkybob) -- Remove custom now() function (@funkybob, @yangyubo) -- notify.send() accepts User or Group (@Evidlo) - -0.8.0 ------ - -0.8 is the last major version supports Django 1.4~1.6, version 0.8.0 will go into bugfix mode, no new features will be accepted. - -- Bugfixes for live-updater, and added a live tester page (@LegoStormtroopr) -- Class-based classes (@alazaro) -- Fixed urls in tests (@alazaro) -- Added app_label to Notification model in order to fix a Django 1.9 deprecation warning (@Heldroe) -- django-model-utils compatible issue (must >=2.0.3 and <2.4) (@zhang-z) -- Reliable setup.py versioning (@yangyubo) - -0.7.1 ------ - -- Able to pass level when adding notification (@Arthur) -- Fix deprecation notice in Django 1.8 (@ashokfernandez) -- Fix Python 3 support for notification model (@philroche) -- Bugfix for wrong user unread notification count (@Geeknux) -- A simple javascript API for live-updating specific fields within a django template (@LegoStormtroopr) -- Add missing migration for Notification model (@shezadkhan137) - -0.7.0 ------ - -- Add filters and displays to Django model Admin -- Support Django 1.8, compatible with both django-south (django < 1.7) and built-in schema migration (django >= 1.7) -- Compatible with Python 3 -- Test fixtures, and integrated with travis-ci - -0.6.2 ------ - -- Fix README.rst reStructuredText syntax format -- Use relative imports -- Add contributors to AUTHORS.txt - -0.6.1 ------ - -- Add support for custom user model -- mark_as_unread -- Require django-model-utils >= 2.0.3 -- Use different `now` function according to the `USE_TZ` setting - -0.6.0 ------ - -- Improve documentation -- Add unicode support at admin panel or shell - -0.5.5 ------ - -Support for arbitrary data attribute. - -0.5.1 ------ - -Fix package descriptions and doc links. - -0.5 ---- - -First version based on `django-activity-stream `_ v0.4.3 diff --git a/MANIFEST.in b/MANIFEST.in index 66ef6b8..c633a41 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ -include MANIFEST.in README.rst AUTHORS.txt LICENSE.txt CHANGELOG.rst +include MANIFEST.in README.md AUTHORS.txt LICENSE.txt CHANGELOG.md recursive-include notifications *.py *.html *.txt *.po diff --git a/README.md b/README.md new file mode 100644 index 0000000..8fa5b54 --- /dev/null +++ b/README.md @@ -0,0 +1,518 @@ +# `django-notifications` Documentation + +[![build-status](https://travis-ci.org/django-notifications/django-notifications.svg)](https://travis-ci.org/django-notifications/django-notifications) +[![Coverage Status](https://coveralls.io/repos/github/django-notifications/django-notifications/badge.svg?branch=master)](https://coveralls.io/github/django-notifications/django-notifications?branch=master) + + +[django-notifications](https://github.com/django-notifications/django-notifications) is a GitHub notification alike app for Django, it was derived from [django-activity-stream](https://github.com/justquick/django-activity-stream) + +The major difference between `django-notifications` and `django-activity-stream`: + +- `django-notifications` is for building something like Github "Notifications" +- While `django-activity-stream` is for building Github "News Feed" + +Notifications are actually actions events, which are categorized by four main components. + +- `Actor`. The object that performed the activity. +- `Verb`. The verb phrase that identifies the action of the activity. +- `Action Object`. *(Optional)* The object linked to the action + itself. +- `Target`. *(Optional)* The object to which the activity was + performed. + +`Actor`, `Action Object` and `Target` are `GenericForeignKeys` to any +arbitrary Django object. An action is a description of an action that +was performed (`Verb`) at some instant in time by some `Actor` on some +optional `Target` that results in an `Action Object` getting +created/updated/deleted. + +For example: [justquick](https://github.com/justquick/) `(actor)` +*closed* `(verb)` [issue +2](https://github.com/justquick/django-activity-stream/issues/2) +`(action_object)` on +[activity-stream](https://github.com/justquick/django-activity-stream/) +`(target)` 12 hours ago + +Nomenclature of this specification is based on the Activity Streams +Spec: + +## Requirements + +- Python 3.7, 3.8, 3.9, 3.10, 3.11 +- Django 3.2, 4.0, 4.1 + +## Installation + +Installation is easy using `pip` and will install all required +libraries. +```bash +$ pip install django-notifications-hq +``` +or get it from source + +```bash +$ git clone https://github.com/django-notifications/django-notifications +$ cd django-notifications +$ python setup.py sdist +$ pip install dist/django-notifications-hq* +``` + +Note that [django-model-utils](http://pypi.python.org/pypi/django-model-utils) +will be installed: this is required for the pass-through QuerySet manager. + +Then to add the Django Notifications to your project add the app +`notifications` to your `INSTALLED_APPS` and urlconf. + +The app should go somewhere after all the apps that are going to be +generating notifications like `django.contrib.auth` + +```python +INSTALLED_APPS = ( + 'django.contrib.auth', + ... + 'notifications', + ... +) +``` + +Add the notifications urls to your urlconf: + +```python +import notifications.urls + +urlpatterns = [ + ... + url('^inbox/notifications/', include(notifications.urls, namespace='notifications')), + ... +] +``` + +The method of installing these urls, importing rather than using +`'notifications.urls'`, is required to ensure that the urls are +installed in the `notifications` namespace. + +To run schema migration, execute +`python manage.py migrate notifications`. + +## Generating Notifications + +Generating notifications is probably best done in a separate signal. + +```python +from django.db.models.signals import post_save +from notifications.signals import notify +from myapp.models import MyModel + +def my_handler(sender, instance, created, **kwargs): + notify.send(instance, verb='was saved') + +post_save.connect(my_handler, sender=MyModel) +``` +To generate an notification anywhere in your code, simply import the +notify signal and send it with your actor, recipient, and verb. + +```python +from notifications.signals import notify + +notify.send(user, recipient=user, verb='you reached level 10') +``` + +The complete syntax is. + +```python +notify.send(actor, recipient, verb, action_object, target, level, description, public, timestamp, **kwargs) +``` + +Arguments: + +- **actor**: An object of any type. (Required) Note: Use + **sender** instead of **actor** if you intend to use keyword + arguments +- **recipient**: A **Group** or a **User QuerySet** or a list of + **User**. (Required) +- **verb**: An string. (Required) +- **action\_object**: An object of any type. (Optional) +- **target**: An object of any type. (Optional) +- **level**: One of Notification.LEVELS (\'success\', \'info\', + \'warning\', \'error\') (default=info). (Optional) +- **description**: An string. (Optional) +- **public**: An boolean (default=True). (Optional) +- **timestamp**: An tzinfo (default=timezone.now()). (Optional) + +### Extra data + +You can attach arbitrary data to your notifications by doing the +following: + +- Add to your settings.py: + `DJANGO_NOTIFICATIONS_CONFIG = { 'USE_JSONFIELD': True}` + +Then, any extra arguments you pass to `notify.send(...)` will be +attached to the `.data` attribute of the notification object. These will +be serialised using the JSONField\'s serialiser, so you may need to take +that into account: using only objects that will be serialised is a good +idea. + +### Soft delete + +By default, `delete/(?P\d+)/` deletes specified notification +record from DB. You can change this behaviour to \"mark +`Notification.deleted` field as `True`\" by: + +- Add to your settings.py: + `DJANGO_NOTIFICATIONS_CONFIG = { 'SOFT_DELETE': True}` + +With this option, QuerySet methods `unread` and `read` contain one more +filter: `deleted=False`. Meanwhile, QuerySet methods `deleted`, +`active`, `mark_all_as_deleted`, `mark_all_as_active` are turned on. See +more details in QuerySet methods section. + +## API + +### QuerySet methods + +Using `django-model-utils`, we get the ability to add queryset methods +to not only the manager, but to all querysets that will be used, +including related objects. This enables us to do things like: + +```python + Notification.objects.unread() +``` + +which returns all unread notifications. To do this for a single user, we +can do: + +```python + user = User.objects.get(pk=pk) + user.notifications.unread() +``` + +There are some other QuerySet methods, too. + +#### `qs.unsent()` + +Return all of the unsent notifications, filtering the current queryset. +(emailed=False) + +#### `qs.sent()` + +Return all of the sent notifications, filtering the current queryset. +(emailed=True) + +#### `qs.unread()` + +Return all of the unread notifications, filtering the current queryset. +When `SOFT_DELETE=True`, this filter contains `deleted=False`. + +#### `qs.read()` + +Return all of the read notifications, filtering the current queryset. +When `SOFT_DELETE=True`, this filter contains `deleted=False`. + +#### `qs.mark_all_as_read()` \| `qs.mark_all_as_read(recipient)` + +Mark all of the unread notifications in the queryset (optionally also +filtered by `recipient`) as read. + +#### `qs.mark_all_as_unread()` \| `qs.mark_all_as_unread(recipient)` + +Mark all of the read notifications in the queryset (optionally also +filtered by `recipient`) as unread. + +#### `qs.mark_as_sent()` \| `qs.mark_as_sent(recipient)` + +Mark all of the unsent notifications in the queryset (optionally also +filtered by `recipient`) as sent. + +#### `qs.mark_as_unsent()` \| `qs.mark_as_unsent(recipient)` + +Mark all of the sent notifications in the queryset (optionally also +filtered by `recipient`) as unsent. + +#### `qs.deleted()` + +Return all notifications that have `deleted=True`, filtering the current +queryset. Must be used with `SOFT_DELETE=True`. + +#### `qs.active()` + +Return all notifications that have `deleted=False`, filtering the +current queryset. Must be used with `DELETE=True`. + +#### `qs.mark_all_as_deleted()` \| `qs.mark_all_as_deleted(recipient)` + +Mark all notifications in the queryset (optionally also filtered by +`recipient`) as `deleted=True`. Must be used with `DELETE=True`. + +#### `qs.mark_all_as_active()` \| `qs.mark_all_as_active(recipient)` + +Mark all notifications in the queryset (optionally also filtered by +`recipient`) as `deleted=False`. Must be used with `SOFT_DELETE=True`. + +### Model methods + +#### `obj.timesince([datetime])` + +A wrapper for Django\'s `timesince` function. + +#### `obj.mark_as_read()` + +Mark the current object as read. + +### Template tags + +Put `{% load notifications\_tags %}` in the template before +you actually use notification tags. + +### `notifications_unread` + +```python + {% notifications_unread %} +``` + +Give the number of unread notifications for a user, or nothing (an empty +string) for an anonymous user. + +Storing the count in a variable for further processing is advised, such +as: + +```python + {% notifications_unread as unread_count %} + ... + {% if unread_count %} + You have {{ unread_count }} unread notifications. + {% endif %} +``` + +## Live-updater API + +To ensure users always have the most up-to-date notifications, +`django-notifications` includes a simple javascript API for +updating specific fields within a django template. + +There are two possible API calls that can be made: + +1. `api/unread_count/` that returns a javascript object with 1 key: + `unread_count` eg: + + {"unread_count":1} + +2. `api/unread_list/` that returns a javascript object with 2 keys: + `unread_count` and `unread_list` eg: + + { + "unread_count":1, + "unread_list":[--list of json representations of notifications--] + } + + Representations of notifications are based on the django method: + `model_to_dict` + + Query string arguments: + + - **max** - maximum length of unread list. + - **mark\_as\_read** - mark notification in list as read. + + For example, get `api/unread_list/?max=3&mark_as_read=true` returns + 3 notifications and mark them read (remove from list on next + request). + +### How to use: + +1. Put `{% load notifications_tags %}` in the template before you + actually use notification tags. + +2. In the area where you are loading javascript resources add the + following tags in the order below: + + + {% register_notify_callbacks callbacks='fill_notification_list,fill_notification_badge' %} + + `register_notify_callbacks` takes the following arguments: + + 1. `badge_class` (default `live_notify_badge`) - The identifier + `class` of the element to show the unread count, + that will be periodically updated. + 2. `menu_class` (default `live_notify_list`) - The identifier + `class` of the element to insert a list of unread + items, that will be periodically updated. + 3. `refresh_period` (default `15`) - How often to fetch unread + items from the server (integer in seconds). + 4. `fetch` (default `5`) - How many notifications to fetch each + time. + 5. `callbacks` (default ``) - A comma-separated list + of javascript functions to call each period. + 6. `api_name` (default `list`) - The name of the API to call (this + can be either `list` or `count`). + +3. To insert a live-updating unread count, use the following template: + + {% live_notify_badge %} + + `live_notify_badge` takes the following arguments: + + - `badge_class` (default `live_notify_badge`) - The identifier + `class` for the `` element that will be created to show + the unread count. + +4. To insert a live-updating unread list, use the following template: + + {% live_notify_list %} + + `live_notify_list` takes the following arguments: + + - `list_class` (default `live_notify_list`) - The identifier + `class` for the `
    ` element that will be created to insert + the list of notifications into. + +### Using the live-updater with bootstrap + +The Live-updater can be incorporated into bootstrap with minimal code. + +To create a live-updating bootstrap badge containing the unread count, +simply use the template tag: + + {% live_notify_badge badge_class="badge" %} + +To create a live-updating bootstrap dropdown menu containing a selection +of recent unread notifications, simply use the template tag: + + {% live_notify_list list_class="dropdown-menu" %} + +### Customising the display of notifications using javascript callbacks + +While the live notifier for unread counts should suit most use cases, +users may wish to alter how unread notifications are shown. + +The `callbacks` argument of the `register_notify_callbacks` dictates +which javascript functions are called when the unread api call is made. + +To add a custom javascript callback, simply add this to the list, like +so: + + {% register_notify_callbacks callbacks='fill_notification_badge,my_special_notification_callback' %} + +The above would cause the callback to update the unread count badge, and +would call the custom function +`my_special_notification_callback`. All callback +functions are passed a single argument by convention called +`data`, which contains the entire result from the API. + +For example, the below function would get the recent list of unread +messages and log them to the console: + +```javascript +function my_special_notification_callback(data) { + for (var i=0; i < data.unread_list.length; i++) { + msg = data.unread_list[i]; + console.log(msg); + } +} +``` + +### Testing the live-updater + +1. Clone the repo +2. Run `./manage.py runserver` +3. Browse to `yourserverip/test/` +4. Click \'Make a notification\' and a new notification should appear + in the list in 5-10 seconds. + +## Serializing the django-notifications Model + +See here - + +In this example the target object can be of type Foo or Bar and the +appropriate serializer will be used. + +```python +class GenericNotificationRelatedField(serializers.RelatedField): + + def to_representation(self, value): + if isinstance(value, Foo): + serializer = FooSerializer(value) + if isinstance(value, Bar): + serializer = BarSerializer(value) + + return serializer.data + + +class NotificationSerializer(serializers.Serializer): + recipient = PublicUserSerializer(User, read_only=True) + unread = serializers.BooleanField(read_only=True) + target = GenericNotificationRelatedField(read_only=True) +``` + +Thanks to @DaWy + +### `AbstractNotification` model + +In case you need to customize the notification model in order to add +field or customised features that depend on your application, you can +inherit and extend the `AbstractNotification` model, example: + +```python +#In your_app/models.py + +from django.db import models +from notifications.base.models import AbstractNotification + + +class Notification(AbstractNotification): + # custom field example + category = models.ForeignKey('myapp.Category', + on_delete=models.CASCADE) + + class Meta(AbstractNotification.Meta): + abstract = False +``` + +You will require to define `NOTIFICATIONS_NOTIFICATION_MODEL` setting in +`setting.py` as follows: + +```python +# In your_project/settings.py + +NOTIFICATIONS_NOTIFICATION_MODEL = 'your_app.Notification' +``` + +## Notes + +### Email Notification + +Sending email to users has not been integrated into this library. So for +now you need to implement it if needed. There is a reserved field +`Notification.emailed` to make it easier. + +### Sample App + +A sample app has been implemented in +`notifications/tests/sample_notifications` that extends +`django-notifications` with the sole purpose of testing its +extensibility. You can run the SAMPLE APP by setting the environment +variable `SAMPLE_APP` as follows + +```bash +export SAMPLE_APP=1 +# Run the Django development server with sample_notifications app installed +python manage.py runserver +# Unset SAMPLE_APP to remove sample_notifications app from list of INSTALLED_APPS +unset SAMPLE_APP +``` + +## `django-notifications` Team + +Core contributors (in alphabetical order): + +- [Alvaro Leonel](https://github.com/AlvaroLQueiroz) +- [Federico Capoano](https://github.com/nemesisdesign) +- [Samuel Spencer](https://github.com/LegoStormtroopr) +- [Yang Yubo](https://github.com/yangyubo) +- [YPCrumble](https://github.com/YPCrumble) +- [Zhongyuan Zhang](https://github.com/zhang-z) + +## Contribute + +We are looking for contributors, for anyone who\'d like to contribute +and willing to put time and energy on this project, please contact [Yang +Yubo](https://github.com/yangyubo). diff --git a/README.rst b/README.rst deleted file mode 100644 index 32048b4..0000000 --- a/README.rst +++ /dev/null @@ -1,429 +0,0 @@ -``django-notifications`` Documentation -======================================= - - -|build-status| |coveralls| - -`django-notifications `_ is a GitHub notification alike app for Django, it was derived from `django-activity-stream `_ - -The major difference between ``django-notifications`` and ``django-activity-stream``: - -* ``django-notifications`` is for building something like Github "Notifications" -* While ``django-activity-stream`` is for building Github "News Feed" - -Notifications are actually actions events, which are categorized by four main components. - -* ``Actor``. The object that performed the activity. -* ``Verb``. The verb phrase that identifies the action of the activity. -* ``Action Object``. *(Optional)* The object linked to the action itself. -* ``Target``. *(Optional)* The object to which the activity was performed. - -``Actor``, ``Action Object`` and ``Target`` are ``GenericForeignKeys`` to any arbitrary Django object. -An action is a description of an action that was performed (``Verb``) at some instant in time by some ``Actor`` on some optional ``Target`` that results in an ``Action Object`` getting created/updated/deleted. - -For example: `justquick `_ ``(actor)`` *closed* ``(verb)`` `issue 2 `_ ``(action_object)`` on `activity-stream `_ ``(target)`` 12 hours ago - -Nomenclature of this specification is based on the Activity Streams Spec: ``_ - -Requirements -============ - -- Python 2.7, 3.4, 3.5, 3.6 -- Django 1.7, 1.8, 1.9, 1.10, 1.11, 2.0 - -Installation -============ - -Installation is easy using ``pip`` and will install all required libraries. - -:: - - $ pip install django-notifications-hq - -or get it from source - -:: - - $ git clone https://github.com/django-notifications/django-notifications - $ cd django-notifications - $ python setup.py sdist - $ pip install dist/django-notifications-hq*a - -Note that `django-model-utils `_ will be installed: this is required for the pass-through QuerySet manager. - -Then to add the Django Notifications to your project add the app ``notifications`` to your ``INSTALLED_APPS`` and urlconf. - -The app should go somewhere after all the apps that are going to be generating notifications like ``django.contrib.auth`` - -:: - - INSTALLED_APPS = ( - 'django.contrib.auth', - ... - 'notifications', - ... - ) - -Add the notifications urls to your urlconf:: - - import notifications.urls - - urlpatterns = [ - ... - url('^inbox/notifications/', include(notifications.urls, namespace='notifications')), - ... - ] - -The method of installing these urls, importing rather than using ``'notifications.urls'``, is required to ensure that the urls are installed in the ``notifications`` namespace. - -To run schema migration, execute ``python manage.py migrate notifications``. - -Generating Notifications -========================= - -Generating notifications is probably best done in a separate signal. - -:: - - from django.db.models.signals import post_save - from notifications.signals import notify - from myapp.models import MyModel - - def my_handler(sender, instance, created, **kwargs): - notify.send(instance, verb='was saved') - - post_save.connect(my_handler, sender=MyModel) - -To generate an notification anywhere in your code, simply import the notify signal and send it with your actor, recipient, and verb. - -:: - - from notifications.signals import notify - - notify.send(user, recipient=user, verb='you reached level 10') - -The complete syntax is. - -:: - - notify.send(actor, recipient, verb, action_object, target, level, description, public, timestamp, **kwargs) - -Arguments: - * **actor**: An object of any type. (Required) Note: Use **sender** instead of **actor** if you intend to use keyword arguments - * **recipient**: A **Group** or a **User QuerySet** or a list of **User**. (Required) - * **verb**: An string. (Required) - * **action_object**: An object of any type. (Optional) - * **target**: An object of any type. (Optional) - * **level**: One of Notification.LEVELS ('success', 'info', 'warning', 'error') (default=info). (Optional) - * **description**: An string. (Optional) - * **public**: An boolean (default=True). (Optional) - * **timestamp**: An tzinfo (default=timezone.now()). (Optional) - -Extra data ----------- - -You can attach arbitrary data to your notifications by doing the following: - -* Add to your settings.py: ``DJANGO_NOTIFICATIONS_CONFIG = { 'USE_JSONFIELD': True}`` - -Then, any extra arguments you pass to ``notify.send(...)`` will be attached to the ``.data`` attribute of the notification object. -These will be serialised using the JSONField's serialiser, so you may need to take that into account: using only objects that will be serialised is a good idea. - -Soft delete ------------ - -By default, ``delete/(?P\d+)/`` deletes specified notification record from DB. -You can change this behaviour to "mark ``Notification.deleted`` field as ``True``" by: - -* Add to your settings.py: ``DJANGO_NOTIFICATIONS_CONFIG = { 'SOFT_DELETE': True}`` - -With this option, QuerySet methods ``unread`` and ``read`` contain one more filter: ``deleted=False``. -Meanwhile, QuerySet methods ``deleted``, ``active``, ``mark_all_as_deleted``, ``mark_all_as_active`` are turned on. -See more details in QuerySet methods section. - -API -==== - -QuerySet methods ------------------ - -Using ``django-model-utils``, we get the ability to add queryset methods to not only the manager, but to all querysets that will be used, including related objects. This enables us to do things like:: - - Notification.objects.unread() - -which returns all unread notifications. To do this for a single user, we can do:: - - user = User.objects.get(pk=pk) - user.notifications.unread() - -There are some other QuerySet methods, too. - -``qs.unsent()`` -~~~~~~~~~~~~~~~ - -Return all of the unsent notifications, filtering the current queryset. (emailed=False) - -``qs.sent()`` -~~~~~~~~~~~~~~~ - -Return all of the sent notifications, filtering the current queryset. (emailed=True) - -``qs.unread()`` -~~~~~~~~~~~~~~~ - -Return all of the unread notifications, filtering the current queryset. -When ``SOFT_DELETE=True``, this filter contains ``deleted=False``. - -``qs.read()`` -~~~~~~~~~~~~~~~ - -Return all of the read notifications, filtering the current queryset. -When ``SOFT_DELETE=True``, this filter contains ``deleted=False``. - - -``qs.mark_all_as_read()`` | ``qs.mark_all_as_read(recipient)`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Mark all of the unread notifications in the queryset (optionally also filtered by ``recipient``) as read. - - -``qs.mark_all_as_unread()`` | ``qs.mark_all_as_unread(recipient)`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Mark all of the read notifications in the queryset (optionally also filtered by ``recipient``) as unread. - -``qs.mark_as_sent()`` | ``qs.mark_as_sent(recipient)`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Mark all of the unsent notifications in the queryset (optionally also filtered by ``recipient``) as sent. - - -``qs.mark_as_unsent()`` | ``qs.mark_as_unsent(recipient)`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Mark all of the sent notifications in the queryset (optionally also filtered by ``recipient``) as unsent. - -``qs.deleted()`` -~~~~~~~~~~~~~~~~ - -Return all notifications that have ``deleted=True``, filtering the current queryset. -Must be used with ``SOFT_DELETE=True``. - -``qs.active()`` -~~~~~~~~~~~~~~~ - -Return all notifications that have ``deleted=False``, filtering the current queryset. -Must be used with ``DELETE=True``. - -``qs.mark_all_as_deleted()`` | ``qs.mark_all_as_deleted(recipient)`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Mark all notifications in the queryset (optionally also filtered by ``recipient``) as ``deleted=True``. -Must be used with ``DELETE=True``. - -``qs.mark_all_as_active()`` | ``qs.mark_all_as_active(recipient)`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Mark all notifications in the queryset (optionally also filtered by ``recipient``) as ``deleted=False``. -Must be used with ``SOFT_DELETE=True``. - - -Model methods -------------- - -``obj.timesince([datetime])`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A wrapper for Django's ``timesince`` function. - -``obj.mark_as_read()`` -~~~~~~~~~~~~~~~~~~~~~~ - -Mark the current object as read. - - -Template tags -------------- - -Put `{% load notifications_tags %}` in the template before you actually use notification tags. - - -``notifications_unread`` -~~~~~~~~~~~~~~~~~~~~~~~~ - -:: - - {% notifications_unread %} - -Give the number of unread notifications for a user, or nothing (an empty string) for an anonymous user. - -Storing the count in a variable for further processing is advised, such as:: - - {% notifications_unread as unread_count %} - ... - {% if unread_count %} - You have {{ unread_count }} unread notifications. - {% endif %} - -Live-updater API -================ - -To ensure users always have the most up-to-date notifications, `django-notifications` includes a simple javascript API -for updating specific fields within a django template. - -There are two possible API calls that can be made: - -1. ``api/unread_count/`` that returns a javascript object with 1 key: ``unread_count`` eg:: - - {"unread_count":1} - -#. ``api/unread_list/`` that returns a javascript object with 2 keys: `unread_count` and `unread_list` eg:: - - { - "unread_count":1, - "unread_list":[--list of json representations of notifications--] - } - - Representations of notifications are based on the django method: ``model_to_dict`` - - Query string arguments: - - - **max** - maximum length of unread list. - - **mark_as_read** - mark notification in list as read. - - For example, get ``api/unread_list/?max=3&mark_as_read=true`` returns 3 notifications and mark them read (remove from list on next request). - - -How to use: ------------ - -1. Put ``{% load notifications_tags %}`` in the template before you actually use notification tags. -2. In the area where you are loading javascript resources add the following tags in the order below:: - - - {% register_notify_callbacks callbacks='fill_notification_list,fill_notification_badge' %} - - ``register_notify_callbacks`` takes the following arguments: - - 1. ``badge_class`` (default ``live_notify_badge``) - The identifier `class` of the element to show the unread count, that will be periodically updated. - #. ``menu_class`` (default ``live_notify_list``) - The identifier `class` of the element to insert a list of unread items, that will be periodically updated. - #. ``refresh_period`` (default ``15``) - How often to fetch unread items from the server (integer in seconds). - #. ``fetch`` (default ``5``) - How many notifications to fetch each time. - #. ``callbacks`` (default ````) - A comma-separated list of javascript functions to call each period. - #. ``api_name`` (default ``list``) - The name of the API to call (this can be either ``list`` or ``count``). - -3. To insert a live-updating unread count, use the following template:: - - {% live_notify_badge %} - - ``live_notify_badge`` takes the following arguments: - - 1. ``badge_class`` (default ``live_notify_badge``) - The identifier ``class`` for the ```` element that will be created to show the unread count. - -4. To insert a live-updating unread list, use the following template:: - - {% live_notify_list %} - - ``live_notify_list`` takes the following arguments: - - 1. ``list_class`` (default ``live_notify_list``) - The identifier ``class`` for the ``
      `` element that will be created to insert the list of notifications into. - -Using the live-updater with bootstrap -------------------------------------- - -The Live-updater can be incorporated into bootstrap with minimal code. - -To create a live-updating bootstrap badge containing the unread count, simply use the template tag:: - - {% live_notify_badge badge_class="badge" %} - -To create a live-updating bootstrap dropdown menu containing a selection of recent unread notifications, simply use the template tag:: - - {% live_notify_list list_class="dropdown-menu" %} - -Customising the display of notifications using javascript callbacks -------------------------------------------------------------------- - -While the live notifier for unread counts should suit most use cases, users may wish to alter how -unread notifications are shown. - -The ``callbacks`` argument of the ``register_notify_callbacks`` dictates which javascript functions are called when -the unread api call is made. - -To add a custom javascript callback, simply add this to the list, like so:: - - {% register_notify_callbacks callbacks='fill_notification_badge,my_special_notification_callback' %} - -The above would cause the callback to update the unread count badge, and would call the custom function `my_special_notification_callback`. -All callback functions are passed a single argument by convention called `data`, which contains the entire result from the API. - -For example, the below function would get the recent list of unread messages and log them to the console:: - - function my_special_notification_callback(data) { - for (var i=0; i < data.unread_list.length; i++) { - msg = data.unread_list[i]; - console.log(msg); - } - } - -Testing the live-updater ------------------------- - -1. Clone the repo -2. Run `./manage.py runserver` -3. Browse to `yourserverip/test/` -4. Click 'Make a notification' and a new notification should appear in the list in 5-10 seconds. - -Serializing the django-notifications Model -========================================== - -See here - http://www.django-rest-framework.org/api-guide/relations/#generic-relationships - -In this example the target object can be of type Foo or Bar and the appropriate serializer will be used. - -:: - - class GenericNotificationRelatedField(serializers.RelatedField): - - def to_representation(self, value): - if isinstance(value, Foo): - serializer = FooSerializer(value) - if isinstance(value, Bar): - serializer = BarSerializer(value) - - return serializer.data - - - class NotificationSerializer(serializers.Serializer): - recipient = PublicUserSerializer(User, read_only=True) - unread = serializers.BooleanField(read_only=True) - target = GenericNotificationRelatedField(read_only=True) - -Thanks to @DaWy - -Notes -===== - -Email Notification ------------------- - -Sending email to users has not been integrated into this library. So for now you need to implement it if needed. There is a reserved field `Notification.emailed` to make it easier. - - -``django-notifications`` Team -============================== - -Core contributors (in alphabetical order): - -- `Alvaro Leonel `_ -- `Samuel Spencer `_ -- `Yang Yubo `_ -- `Zhongyuan Zhang `_ - -.. |build-status| image:: https://travis-ci.org/django-notifications/django-notifications.svg - :target: https://travis-ci.org/django-notifications/django-notifications - -.. |coveralls| image:: https://coveralls.io/repos/django-notifications/django-notifications/badge.png?branch=master - :alt: Code coverage on coveralls - :scale: 100% - :target: https://coveralls.io/r/django-notifications/django-notifications?branch=master diff --git a/notifications/__init__.py b/notifications/__init__.py index 6a254b8..55a4735 100644 --- a/notifications/__init__.py +++ b/notifications/__init__.py @@ -8,6 +8,6 @@ """ # PEP 386-compliant version number: N.N[.N]+[{a|b|c|rc}N[.N]+][.postN][.devN] -__version__ = '1.4.0' +__version__ = '1.7.0' default_app_config = 'notifications.apps.Config' # pylint: disable=invalid-name diff --git a/notifications/admin.py b/notifications/admin.py index 72b9e5d..3d9c172 100644 --- a/notifications/admin.py +++ b/notifications/admin.py @@ -1,14 +1,21 @@ ''' Django notifications admin file ''' # -*- coding: utf-8 -*- from django.contrib import admin -from .models import Notification +from notifications.base.admin import AbstractNotificationAdmin +from swapper import load_model + +Notification = load_model('notifications', 'Notification') -class NotificationAdmin(admin.ModelAdmin): +class NotificationAdmin(AbstractNotificationAdmin): raw_id_fields = ('recipient',) list_display = ('recipient', 'actor', 'level', 'target', 'unread', 'public') list_filter = ('level', 'unread', 'public', 'timestamp',) + def get_queryset(self, request): + qs = super(NotificationAdmin, self).get_queryset(request) + return qs.prefetch_related('actor') + admin.site.register(Notification, NotificationAdmin) diff --git a/notifications/apps.py b/notifications/apps.py index 2a1a387..1480c3a 100644 --- a/notifications/apps.py +++ b/notifications/apps.py @@ -5,6 +5,7 @@ from django.apps import AppConfig class Config(AppConfig): name = "notifications" + default_auto_field = 'django.db.models.AutoField' def ready(self): super(Config, self).ready() diff --git a/notifications/base/__init__.py b/notifications/base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/notifications/base/admin.py b/notifications/base/admin.py new file mode 100644 index 0000000..29c20c2 --- /dev/null +++ b/notifications/base/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin + + +class AbstractNotificationAdmin(admin.ModelAdmin): + raw_id_fields = ('recipient',) + list_display = ('recipient', 'actor', + 'level', 'target', 'unread', 'public') + list_filter = ('level', 'unread', 'public', 'timestamp',) + + def get_queryset(self, request): + qs = super(AbstractNotificationAdmin, self).get_queryset(request) + return qs.prefetch_related('actor') diff --git a/notifications/base/models.py b/notifications/base/models.py new file mode 100644 index 0000000..43f9ee3 --- /dev/null +++ b/notifications/base/models.py @@ -0,0 +1,313 @@ +# -*- coding: utf-8 -*- +# pylint: disable=too-many-lines +from distutils.version import \ + StrictVersion # pylint: disable=no-name-in-module,import-error + +from django import get_version +from django.conf import settings +from django.contrib.auth.models import Group +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ImproperlyConfigured +from django.db import models +from django.db.models.query import QuerySet +from django.utils import timezone +from jsonfield.fields import JSONField +from model_utils import Choices + +from notifications import settings as notifications_settings +from notifications.signals import notify +from notifications.utils import id2slug +from swapper import load_model + +if StrictVersion(get_version()) >= StrictVersion('1.8.0'): + from django.contrib.contenttypes.fields import GenericForeignKey # noqa +else: + from django.contrib.contenttypes.generic import GenericForeignKey # noqa + + +EXTRA_DATA = notifications_settings.get_config()['USE_JSONFIELD'] + + +def is_soft_delete(): + return notifications_settings.get_config()['SOFT_DELETE'] + + +def assert_soft_delete(): + if not is_soft_delete(): + # msg = """To use 'deleted' field, please set 'SOFT_DELETE'=True in settings. + # Otherwise NotificationQuerySet.unread and NotificationQuerySet.read do NOT filter by 'deleted' field. + # """ + msg = 'REVERTME' + raise ImproperlyConfigured(msg) + + +class NotificationQuerySet(models.query.QuerySet): + ''' Notification QuerySet ''' + def unsent(self): + return self.filter(emailed=False) + + def sent(self): + return self.filter(emailed=True) + + def unread(self, include_deleted=False): + """Return only unread items in the current queryset""" + if is_soft_delete() and not include_deleted: + return self.filter(unread=True, deleted=False) + + # When SOFT_DELETE=False, developers are supposed NOT to touch 'deleted' field. + # In this case, to improve query performance, don't filter by 'deleted' field + return self.filter(unread=True) + + def read(self, include_deleted=False): + """Return only read items in the current queryset""" + if is_soft_delete() and not include_deleted: + return self.filter(unread=False, deleted=False) + + # When SOFT_DELETE=False, developers are supposed NOT to touch 'deleted' field. + # In this case, to improve query performance, don't filter by 'deleted' field + return self.filter(unread=False) + + def mark_all_as_read(self, recipient=None): + """Mark as read any unread messages in the current queryset. + + Optionally, filter these by recipient first. + """ + # We want to filter out read ones, as later we will store + # the time they were marked as read. + qset = self.unread(True) + if recipient: + qset = qset.filter(recipient=recipient) + + return qset.update(unread=False) + + def mark_all_as_unread(self, recipient=None): + """Mark as unread any read messages in the current queryset. + + Optionally, filter these by recipient first. + """ + qset = self.read(True) + + if recipient: + qset = qset.filter(recipient=recipient) + + return qset.update(unread=True) + + def deleted(self): + """Return only deleted items in the current queryset""" + assert_soft_delete() + return self.filter(deleted=True) + + def active(self): + """Return only active(un-deleted) items in the current queryset""" + assert_soft_delete() + return self.filter(deleted=False) + + def mark_all_as_deleted(self, recipient=None): + """Mark current queryset as deleted. + Optionally, filter by recipient first. + """ + assert_soft_delete() + qset = self.active() + if recipient: + qset = qset.filter(recipient=recipient) + + return qset.update(deleted=True) + + def mark_all_as_active(self, recipient=None): + """Mark current queryset as active(un-deleted). + Optionally, filter by recipient first. + """ + assert_soft_delete() + qset = self.deleted() + if recipient: + qset = qset.filter(recipient=recipient) + + return qset.update(deleted=False) + + def mark_as_unsent(self, recipient=None): + qset = self.sent() + if recipient: + qset = qset.filter(recipient=recipient) + return qset.update(emailed=False) + + def mark_as_sent(self, recipient=None): + qset = self.unsent() + if recipient: + qset = qset.filter(recipient=recipient) + return qset.update(emailed=True) + + +class AbstractNotification(models.Model): + """ + Action model describing the actor acting out a verb (on an optional + target). + Nomenclature based on http://activitystrea.ms/specs/atom/1.0/ + + Generalized Format:: + +