mirror of
https://github.com/Hopiu/django-notifications.git
synced 2026-03-16 21:30:24 +00:00
Merge branch 'master' into 233
This commit is contained in:
commit
82df6b6083
38 changed files with 1315 additions and 977 deletions
31
.github/workflows/test.yml
vendored
Normal file
31
.github/workflows/test.yml
vendored
Normal file
|
|
@ -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
|
||||
25
.travis.yml
25
.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
|
||||
|
|
|
|||
164
CHANGELOG.md
Normal file
164
CHANGELOG.md
Normal file
|
|
@ -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 <span class="title-ref">now</span> function according
|
||||
to the <span class="title-ref">USE\_TZ</span> 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
|
||||
147
CHANGELOG.rst
147
CHANGELOG.rst
|
|
@ -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 <https://github.com/justquick/django-activity-stream>`_ v0.4.3
|
||||
|
|
@ -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
|
||||
|
|
|
|||
518
README.md
Normal file
518
README.md
Normal file
|
|
@ -0,0 +1,518 @@
|
|||
# `django-notifications` Documentation
|
||||
|
||||
[](https://travis-ci.org/django-notifications/django-notifications)
|
||||
[](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: <http://activitystrea.ms/specs/atom/1.0/>
|
||||
|
||||
## 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<slug>\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 <strong>{{ unread_count }}</strong> 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:
|
||||
|
||||
<script src="{% static 'notifications/notify.js' %}" type="text/javascript"></script>
|
||||
{% 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 `<empty string>`) - 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 `<span>` 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 `<ul>` 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 - <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.
|
||||
|
||||
```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).
|
||||
429
README.rst
429
README.rst
|
|
@ -1,429 +0,0 @@
|
|||
``django-notifications`` Documentation
|
||||
=======================================
|
||||
|
||||
|
||||
|build-status| |coveralls|
|
||||
|
||||
`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: `<http://activitystrea.ms/specs/atom/1.0/>`_
|
||||
|
||||
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 <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``
|
||||
|
||||
::
|
||||
|
||||
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<slug>\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 <strong>{{ unread_count }}</strong> 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::
|
||||
|
||||
<script src="{% static 'notifications/notify.js' %}" type="text/javascript"></script>
|
||||
{% 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 ``<empty string>``) - 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 ``<span>`` 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 ``<ul>`` 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 <https://github.com/AlvaroLQueiroz>`_
|
||||
- `Samuel Spencer <https://github.com/LegoStormtroopr>`_
|
||||
- `Yang Yubo <https://github.com/yangyubo>`_
|
||||
- `Zhongyuan Zhang <https://github.com/zhang-z>`_
|
||||
|
||||
.. |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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
0
notifications/base/__init__.py
Normal file
0
notifications/base/__init__.py
Normal file
12
notifications/base/admin.py
Normal file
12
notifications/base/admin.py
Normal file
|
|
@ -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')
|
||||
313
notifications/base/models.py
Normal file
313
notifications/base/models.py
Normal file
|
|
@ -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::
|
||||
|
||||
<actor> <verb> <time>
|
||||
<actor> <verb> <target> <time>
|
||||
<actor> <verb> <action_object> <target> <time>
|
||||
|
||||
Examples::
|
||||
|
||||
<justquick> <reached level 60> <1 minute ago>
|
||||
<brosner> <commented on> <pinax/pinax> <2 hours ago>
|
||||
<washingtontimes> <started follow> <justquick> <8 minutes ago>
|
||||
<mitsuhiko> <closed> <issue 70> on <mitsuhiko/flask> <about 2 hours ago>
|
||||
|
||||
Unicode Representation::
|
||||
|
||||
justquick reached level 60 1 minute ago
|
||||
mitsuhiko closed issue 70 on mitsuhiko/flask 3 hours ago
|
||||
|
||||
HTML Representation::
|
||||
|
||||
<a href="http://oebfare.com/">brosner</a> commented on <a href="http://github.com/pinax/pinax">pinax/pinax</a> 2 hours ago # noqa
|
||||
|
||||
"""
|
||||
LEVELS = Choices('success', 'info', 'warning', 'error')
|
||||
level = models.CharField(choices=LEVELS, default=LEVELS.info, max_length=20)
|
||||
|
||||
recipient = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
blank=False,
|
||||
related_name='notifications',
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
unread = models.BooleanField(default=True, blank=False, db_index=True)
|
||||
|
||||
actor_content_type = models.ForeignKey(ContentType, related_name='notify_actor', on_delete=models.CASCADE)
|
||||
actor_object_id = models.CharField(max_length=255)
|
||||
actor = GenericForeignKey('actor_content_type', 'actor_object_id')
|
||||
|
||||
verb = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
|
||||
target_content_type = models.ForeignKey(
|
||||
ContentType,
|
||||
related_name='notify_target',
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
target_object_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
target = GenericForeignKey('target_content_type', 'target_object_id')
|
||||
|
||||
action_object_content_type = models.ForeignKey(ContentType, blank=True, null=True,
|
||||
related_name='notify_action_object', on_delete=models.CASCADE)
|
||||
action_object_object_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
action_object = GenericForeignKey('action_object_content_type', 'action_object_object_id')
|
||||
|
||||
timestamp = models.DateTimeField(default=timezone.now, db_index=True)
|
||||
|
||||
public = models.BooleanField(default=True, db_index=True)
|
||||
deleted = models.BooleanField(default=False, db_index=True)
|
||||
emailed = models.BooleanField(default=False, db_index=True)
|
||||
|
||||
data = JSONField(blank=True, null=True)
|
||||
objects = NotificationQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
ordering = ('-timestamp',)
|
||||
# speed up notifications count query
|
||||
index_together = ('recipient', 'unread')
|
||||
|
||||
def __str__(self):
|
||||
ctx = {
|
||||
'actor': self.actor,
|
||||
'verb': self.verb,
|
||||
'action_object': self.action_object,
|
||||
'target': self.target,
|
||||
'timesince': self.timesince()
|
||||
}
|
||||
if self.target:
|
||||
if self.action_object:
|
||||
return u'%(actor)s %(verb)s %(action_object)s on %(target)s %(timesince)s ago' % ctx
|
||||
return u'%(actor)s %(verb)s %(target)s %(timesince)s ago' % ctx
|
||||
if self.action_object:
|
||||
return u'%(actor)s %(verb)s %(action_object)s %(timesince)s ago' % ctx
|
||||
return u'%(actor)s %(verb)s %(timesince)s ago' % ctx
|
||||
|
||||
def timesince(self, now=None):
|
||||
"""
|
||||
Shortcut for the ``django.utils.timesince.timesince`` function of the
|
||||
current timestamp.
|
||||
"""
|
||||
from django.utils.timesince import timesince as timesince_
|
||||
return timesince_(self.timestamp, now)
|
||||
|
||||
@property
|
||||
def slug(self):
|
||||
return id2slug(self.id)
|
||||
|
||||
def mark_as_read(self):
|
||||
if self.unread:
|
||||
self.unread = False
|
||||
self.save()
|
||||
|
||||
def mark_as_unread(self):
|
||||
if not self.unread:
|
||||
self.unread = True
|
||||
self.save()
|
||||
|
||||
|
||||
def notify_handler(verb, **kwargs):
|
||||
"""
|
||||
Handler function to create Notification instance upon action signal call.
|
||||
"""
|
||||
# Pull the options out of kwargs
|
||||
kwargs.pop('signal', None)
|
||||
recipient = kwargs.pop('recipient')
|
||||
actor = kwargs.pop('sender')
|
||||
optional_objs = [
|
||||
(kwargs.pop(opt, None), opt)
|
||||
for opt in ('target', 'action_object')
|
||||
]
|
||||
public = bool(kwargs.pop('public', True))
|
||||
description = kwargs.pop('description', None)
|
||||
timestamp = kwargs.pop('timestamp', timezone.now())
|
||||
Notification = load_model('notifications', 'Notification')
|
||||
level = kwargs.pop('level', Notification.LEVELS.info)
|
||||
|
||||
# Check if User or Group
|
||||
if isinstance(recipient, Group):
|
||||
recipients = recipient.user_set.all()
|
||||
elif isinstance(recipient, (QuerySet, list)):
|
||||
recipients = recipient
|
||||
else:
|
||||
recipients = [recipient]
|
||||
|
||||
new_notifications = []
|
||||
|
||||
for recipient in recipients:
|
||||
newnotify = Notification(
|
||||
recipient=recipient,
|
||||
actor_content_type=ContentType.objects.get_for_model(actor),
|
||||
actor_object_id=actor.pk,
|
||||
verb=str(verb),
|
||||
public=public,
|
||||
description=description,
|
||||
timestamp=timestamp,
|
||||
level=level,
|
||||
)
|
||||
|
||||
# Set optional objects
|
||||
for obj, opt in optional_objs:
|
||||
if obj is not None:
|
||||
setattr(newnotify, '%s_object_id' % opt, obj.pk)
|
||||
setattr(newnotify, '%s_content_type' % opt,
|
||||
ContentType.objects.get_for_model(obj))
|
||||
|
||||
if kwargs and EXTRA_DATA:
|
||||
newnotify.data = kwargs.copy()
|
||||
|
||||
newnotify.save()
|
||||
new_notifications.append(newnotify)
|
||||
|
||||
return new_notifications
|
||||
|
||||
|
||||
# connect the signal
|
||||
notify.connect(notify_handler, dispatch_uid='notifications.models.notification')
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
import swapper
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
@ -33,6 +32,7 @@ class Migration(migrations.Migration):
|
|||
('target_content_type', models.ForeignKey(related_name='notify_target', blank=True, to='contenttypes.ContentType', null=True, on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
'swappable': swapper.swappable_setting('notifications', 'Notification'),
|
||||
'ordering': ('-timestamp',),
|
||||
},
|
||||
bases=(models.Model,),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import jsonfield.fields
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.utils import timezone
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.9.5 on 2016-05-04 15:20
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
|
|
|||
19
notifications/migrations/0007_add_timestamp_index.py
Normal file
19
notifications/migrations/0007_add_timestamp_index.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 2.0.9 on 2018-10-26 10:41
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('notifications', '0006_indexes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='notification',
|
||||
name='timestamp',
|
||||
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 2.1.8 on 2019-04-27 21:26
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('notifications', '0007_add_timestamp_index'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterIndexTogether(
|
||||
name='notification',
|
||||
index_together={('recipient', 'unread')},
|
||||
),
|
||||
]
|
||||
|
|
@ -1,265 +1,18 @@
|
|||
''' Django notifications models file '''
|
||||
# -*- coding: utf-8 -*-
|
||||
# pylint: disable=too-many-lines
|
||||
from distutils.version import (
|
||||
StrictVersion,
|
||||
) # pylint: disable=no-name-in-module,import-error
|
||||
from swapper import swappable_setting
|
||||
|
||||
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 django.utils.six import text_type
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
from .base.models import AbstractNotification, notify_handler # 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.
|
||||
"""
|
||||
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 Notification(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::
|
||||
|
||||
<actor> <verb> <time>
|
||||
<actor> <verb> <target> <time>
|
||||
<actor> <verb> <action_object> <target> <time>
|
||||
|
||||
Examples::
|
||||
|
||||
<justquick> <reached level 60> <1 minute ago>
|
||||
<brosner> <commented on> <pinax/pinax> <2 hours ago>
|
||||
<washingtontimes> <started follow> <justquick> <8 minutes ago>
|
||||
<mitsuhiko> <closed> <issue 70> on <mitsuhiko/flask> <about 2 hours ago>
|
||||
|
||||
Unicode Representation::
|
||||
|
||||
justquick reached level 60 1 minute ago
|
||||
mitsuhiko closed issue 70 on mitsuhiko/flask 3 hours ago
|
||||
|
||||
HTML Representation::
|
||||
|
||||
<a href="http://oebfare.com/">brosner</a> commented on <a href="http://github.com/pinax/pinax">pinax/pinax</a> 2 hours ago # noqa
|
||||
|
||||
"""
|
||||
|
||||
LEVELS = Choices('success', 'info', 'warning', 'error')
|
||||
level = models.CharField(choices=LEVELS, default=LEVELS.info, max_length=20)
|
||||
|
||||
recipient = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
blank=False,
|
||||
related_name='notifications',
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
unread = models.BooleanField(default=True, blank=False, db_index=True)
|
||||
|
||||
actor_content_type = models.ForeignKey(
|
||||
ContentType, related_name='notify_actor', on_delete=models.CASCADE
|
||||
)
|
||||
actor_object_id = models.CharField(max_length=255)
|
||||
actor = GenericForeignKey('actor_content_type', 'actor_object_id')
|
||||
|
||||
verb = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
|
||||
target_content_type = models.ForeignKey(
|
||||
ContentType,
|
||||
related_name='notify_target',
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
target_object_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
target = GenericForeignKey('target_content_type', 'target_object_id')
|
||||
|
||||
action_object_content_type = models.ForeignKey(
|
||||
ContentType,
|
||||
blank=True,
|
||||
null=True,
|
||||
related_name='notify_action_object',
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
action_object_object_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
action_object = GenericForeignKey(
|
||||
'action_object_content_type', 'action_object_object_id'
|
||||
)
|
||||
|
||||
timestamp = models.DateTimeField(default=timezone.now)
|
||||
|
||||
public = models.BooleanField(default=True, db_index=True)
|
||||
deleted = models.BooleanField(default=False, db_index=True)
|
||||
emailed = models.BooleanField(default=False, db_index=True)
|
||||
|
||||
data = JSONField(blank=True, null=True)
|
||||
objects = NotificationQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('-timestamp',)
|
||||
app_label = 'notifications'
|
||||
|
||||
def __unicode__(self):
|
||||
ctx = {
|
||||
'actor': self.actor,
|
||||
'verb': self.verb,
|
||||
'action_object': self.action_object,
|
||||
'target': self.target,
|
||||
'timesince': self.timesince(),
|
||||
}
|
||||
if self.target:
|
||||
if self.action_object:
|
||||
return (
|
||||
u'%(actor)s %(verb)s %(action_object)s on %(target)s %(timesince)s ago'
|
||||
% ctx
|
||||
)
|
||||
return u'%(actor)s %(verb)s %(target)s %(timesince)s ago' % ctx
|
||||
if self.action_object:
|
||||
return u'%(actor)s %(verb)s %(action_object)s %(timesince)s ago' % ctx
|
||||
return u'%(actor)s %(verb)s %(timesince)s ago' % ctx
|
||||
|
||||
def __str__(self): # Adds support for Python 3
|
||||
return self.__unicode__()
|
||||
|
||||
def timesince(self, now=None):
|
||||
"""
|
||||
Shortcut for the ``django.utils.timesince.timesince`` function of the
|
||||
current timestamp.
|
||||
"""
|
||||
from django.utils.timesince import timesince as timesince_
|
||||
|
||||
return timesince_(self.timestamp, now)
|
||||
class Notification(AbstractNotification):
|
||||
|
||||
class Meta(AbstractNotification.Meta):
|
||||
abstract = False
|
||||
swappable = swappable_setting('notifications', 'Notification')
|
||||
|
||||
def naturalday(self):
|
||||
"""
|
||||
Shortcut for the ``humanize``.
|
||||
Take a parameter humanize_type. This parameter control the which humanize method use.
|
||||
|
||||
Return ``today``, ``yesterday`` ,``now``, ``2 seconds ago``etc.
|
||||
"""
|
||||
from django.contrib.humanize.templatetags.humanize import naturalday
|
||||
|
|
@ -267,80 +20,4 @@ class Notification(models.Model):
|
|||
|
||||
def naturaltime(self):
|
||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||
return naturaltime(self.timestamp)
|
||||
|
||||
@property
|
||||
def slug(self):
|
||||
return id2slug(self.id)
|
||||
|
||||
def mark_as_read(self):
|
||||
if self.unread:
|
||||
self.unread = False
|
||||
self.save()
|
||||
|
||||
def mark_as_unread(self):
|
||||
if not self.unread:
|
||||
self.unread = True
|
||||
self.save()
|
||||
|
||||
|
||||
def notify_handler(verb, **kwargs):
|
||||
"""
|
||||
Handler function to create Notification instance upon action signal call.
|
||||
"""
|
||||
|
||||
# Pull the options out of kwargs
|
||||
kwargs.pop('signal', None)
|
||||
recipient = kwargs.pop('recipient')
|
||||
actor = kwargs.pop('sender')
|
||||
optional_objs = [
|
||||
(kwargs.pop(opt, None), opt) for opt in ('target', 'action_object')
|
||||
]
|
||||
public = bool(kwargs.pop('public', True))
|
||||
description = kwargs.pop('description', None)
|
||||
timestamp = kwargs.pop('timestamp', timezone.now())
|
||||
level = kwargs.pop('level', Notification.LEVELS.info)
|
||||
|
||||
# Check if User or Group
|
||||
if isinstance(recipient, Group):
|
||||
recipients = recipient.user_set.all()
|
||||
elif isinstance(recipient, (QuerySet, list)):
|
||||
recipients = recipient
|
||||
else:
|
||||
recipients = [recipient]
|
||||
|
||||
new_notifications = []
|
||||
|
||||
for recipient in recipients:
|
||||
newnotify = Notification(
|
||||
recipient=recipient,
|
||||
actor_content_type=ContentType.objects.get_for_model(actor),
|
||||
actor_object_id=actor.pk,
|
||||
verb=text_type(verb),
|
||||
public=public,
|
||||
description=description,
|
||||
timestamp=timestamp,
|
||||
level=level,
|
||||
)
|
||||
|
||||
# Set optional objects
|
||||
for obj, opt in optional_objs:
|
||||
if obj is not None:
|
||||
setattr(newnotify, '%s_object_id' % opt, obj.pk)
|
||||
setattr(
|
||||
newnotify,
|
||||
'%s_content_type' % opt,
|
||||
ContentType.objects.get_for_model(obj),
|
||||
)
|
||||
|
||||
if kwargs and EXTRA_DATA:
|
||||
newnotify.data = kwargs
|
||||
|
||||
newnotify.save()
|
||||
new_notifications.append(newnotify)
|
||||
|
||||
return new_notifications
|
||||
|
||||
|
||||
# connect the signal
|
||||
notify.connect(notify_handler, dispatch_uid='notifications.models.notification')
|
||||
return naturaltime(self.timestamp)
|
||||
|
|
@ -2,7 +2,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.dispatch import Signal
|
||||
|
||||
notify = Signal(providing_args=[ # pylint: disable=invalid-name
|
||||
'recipient', 'actor', 'verb', 'action_object', 'target', 'description',
|
||||
'timestamp', 'level'
|
||||
])
|
||||
notify = Signal()
|
||||
|
|
|
|||
|
|
@ -56,7 +56,9 @@ function fetch_api_data() {
|
|||
if (this.status === 200){
|
||||
consecutive_misfires = 0;
|
||||
var data = JSON.parse(r.responseText);
|
||||
registered_functions.forEach(function (func) { func(data); });
|
||||
for(var i = 0; i < registered_functions.length; i++) {
|
||||
registered_functions[i](data);
|
||||
}
|
||||
}else{
|
||||
consecutive_misfires++;
|
||||
}
|
||||
|
|
|
|||
1
notifications/tests/sample_notifications/__init__.py
Normal file
1
notifications/tests/sample_notifications/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
default_app_config = 'notifications.tests.sample_notifications.apps.SampleNotificationsConfig'
|
||||
10
notifications/tests/sample_notifications/admin.py
Normal file
10
notifications/tests/sample_notifications/admin.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import swapper
|
||||
from django.contrib import admin
|
||||
from notifications.base.admin import AbstractNotificationAdmin
|
||||
|
||||
Notification = swapper.load_model('notifications', 'Notification')
|
||||
|
||||
|
||||
@admin.register(Notification)
|
||||
class NotificationAdmin(AbstractNotificationAdmin):
|
||||
pass
|
||||
6
notifications/tests/sample_notifications/apps.py
Normal file
6
notifications/tests/sample_notifications/apps.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from notifications.apps import Config as NotificationConfig
|
||||
|
||||
|
||||
class SampleNotificationsConfig(NotificationConfig):
|
||||
name = 'notifications.tests.sample_notifications'
|
||||
label = 'sample_notifications'
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
# Generated by Django 3.0.5 on 2020-04-11 12:15
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import jsonfield.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('level', models.CharField(choices=[('success', 'success'), ('info', 'info'), ('warning', 'warning'), ('error', 'error')], default='info', max_length=20)),
|
||||
('unread', models.BooleanField(db_index=True, default=True)),
|
||||
('actor_object_id', models.CharField(max_length=255)),
|
||||
('verb', models.CharField(max_length=255)),
|
||||
('description', models.TextField(blank=True, null=True)),
|
||||
('target_object_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('action_object_object_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('timestamp', models.DateTimeField(db_index=True, default=django.utils.timezone.now)),
|
||||
('public', models.BooleanField(db_index=True, default=True)),
|
||||
('deleted', models.BooleanField(db_index=True, default=False)),
|
||||
('emailed', models.BooleanField(db_index=True, default=False)),
|
||||
('data', jsonfield.fields.JSONField(blank=True, null=True)),
|
||||
('details', models.CharField(blank=True, max_length=64, null=True)),
|
||||
('action_object_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notify_action_object', to='contenttypes.ContentType')),
|
||||
('actor_content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notify_actor', to='contenttypes.ContentType')),
|
||||
('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL)),
|
||||
('target_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notify_target', to='contenttypes.ContentType')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-timestamp',),
|
||||
'abstract': False,
|
||||
'index_together': {('recipient', 'unread')},
|
||||
},
|
||||
),
|
||||
]
|
||||
9
notifications/tests/sample_notifications/models.py
Normal file
9
notifications/tests/sample_notifications/models.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from django.db import models
|
||||
from notifications.base.models import AbstractNotification
|
||||
|
||||
|
||||
class Notification(AbstractNotification):
|
||||
details = models.CharField(max_length=64, blank=True, null=True)
|
||||
|
||||
class Meta(AbstractNotification.Meta):
|
||||
abstract = False
|
||||
|
|
@ -0,0 +1 @@
|
|||
from notifications.templatetags.notifications_tags import register
|
||||
22
notifications/tests/sample_notifications/tests.py
Normal file
22
notifications/tests/sample_notifications/tests.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import os
|
||||
from unittest import skipUnless
|
||||
|
||||
import swapper
|
||||
from notifications.tests.tests import AdminTest as BaseAdminTest
|
||||
from notifications.tests.tests import NotificationTest as BaseNotificationTest
|
||||
|
||||
Notification = swapper.load_model('notifications', 'Notification')
|
||||
|
||||
|
||||
@skipUnless(os.environ.get('SAMPLE_APP', False), 'Running tests on standard django-notifications models')
|
||||
class AdminTest(BaseAdminTest):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
BaseAdminTest.app_name = 'sample_notifications'
|
||||
|
||||
|
||||
@skipUnless(os.environ.get('SAMPLE_APP', False), 'Running tests on standard django-notifications models')
|
||||
class NotificationTest(BaseNotificationTest):
|
||||
pass
|
||||
|
|
@ -4,7 +4,7 @@ import os
|
|||
|
||||
|
||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
SECRET_KEY = 'secret_key' # noqa
|
||||
SECRET_KEY = 'secret_key' # noqa
|
||||
|
||||
DEBUG = True
|
||||
TESTING = True
|
||||
|
|
@ -20,20 +20,22 @@ DATABASES = {
|
|||
MIDDLEWARE_CLASSES = (
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware'
|
||||
)
|
||||
|
||||
# Django >= 2.0
|
||||
MIDDLEWARE = MIDDLEWARE_CLASSES
|
||||
|
||||
INSTALLED_APPS = (
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.sessions',
|
||||
'notifications.tests',
|
||||
'notifications',
|
||||
)
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'notifications.tests.urls'
|
||||
STATIC_URL = '/static/'
|
||||
|
|
@ -46,8 +48,11 @@ TEMPLATES = [
|
|||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'loaders' : [
|
||||
'django.template.loaders.filesystem.Loader',
|
||||
'django.template.loaders.app_directories.Loader',
|
||||
],
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
|
|
@ -66,3 +71,9 @@ DJANGO_NOTIFICATIONS_CONFIG = {
|
|||
'USE_JSONFIELD': True,
|
||||
}
|
||||
USE_TZ = True
|
||||
|
||||
if os.environ.get('SAMPLE_APP', False):
|
||||
INSTALLED_APPS.remove('notifications')
|
||||
INSTALLED_APPS.append('notifications.tests.sample_notifications')
|
||||
NOTIFICATIONS_NOTIFICATION_MODEL = 'sample_notifications.Notification'
|
||||
TEMPLATES[0]['DIRS'] += [os.path.join(BASE_DIR, '../templates')]
|
||||
|
|
|
|||
|
|
@ -8,17 +8,22 @@ Replace this with more appropriate tests for your application.
|
|||
import json
|
||||
|
||||
import pytz
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import connection
|
||||
from django.template import Context, Template
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
from django.utils import timezone
|
||||
from django.utils.timezone import localtime, utc
|
||||
|
||||
from notifications.models import Notification, notify_handler
|
||||
from notifications.base.models import notify_handler
|
||||
from notifications.signals import notify
|
||||
from notifications.utils import id2slug
|
||||
from swapper import load_model
|
||||
|
||||
Notification = load_model('notifications', 'Notification')
|
||||
|
||||
try:
|
||||
# Django >= 1.7
|
||||
|
|
@ -524,3 +529,30 @@ class TagTest(TestCase):
|
|||
context = {"user":self.to_user}
|
||||
output = u"True"
|
||||
self.tag_test(template, context, output)
|
||||
|
||||
|
||||
class AdminTest(TestCase):
|
||||
app_name = "notifications"
|
||||
def setUp(self):
|
||||
self.message_count = 10
|
||||
self.from_user = User.objects.create_user(username="from", password="pwd", email="example@example.com")
|
||||
self.to_user = User.objects.create_user(username="to", password="pwd", email="example@example.com")
|
||||
self.to_user.is_staff = True
|
||||
self.to_user.is_superuser = True
|
||||
self.to_user.save()
|
||||
for _ in range(self.message_count):
|
||||
notify.send(
|
||||
self.from_user,
|
||||
recipient=self.to_user,
|
||||
verb='commented',
|
||||
action_object=self.from_user,
|
||||
)
|
||||
|
||||
def test_list(self):
|
||||
self.client.login(username='to', password='pwd')
|
||||
|
||||
with CaptureQueriesContext(connection=connection) as context:
|
||||
response = self.client.get(reverse('admin:{0}_notification_changelist'.format(self.app_name)))
|
||||
self.assertLessEqual(len(context), 6)
|
||||
|
||||
self.assertEqual(response.status_code, 200, response.content)
|
||||
|
|
|
|||
|
|
@ -4,12 +4,22 @@ from distutils.version import StrictVersion # pylint: disable=no-name-in-module
|
|||
|
||||
from django import get_version
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.views import login
|
||||
from notifications.tests.views import (live_tester, # pylint: disable=no-name-in-module,import-error
|
||||
make_notification)
|
||||
|
||||
if StrictVersion(get_version()) >= StrictVersion('2.0'):
|
||||
if StrictVersion(get_version()) >= StrictVersion('2.1'):
|
||||
from django.urls import include, path # noqa
|
||||
from django.contrib.auth.views import LoginView
|
||||
urlpatterns = [
|
||||
path('test_make/', make_notification),
|
||||
path('test/', live_tester),
|
||||
path('login/', LoginView.as_view(), name='login'), # reverse for django login is not working
|
||||
path('admin/', admin.site.urls),
|
||||
path('', include('notifications.urls', namespace='notifications')),
|
||||
]
|
||||
elif StrictVersion(get_version()) >= StrictVersion('2.0') and StrictVersion(get_version()) < StrictVersion('2.1'):
|
||||
from django.urls import include, path # noqa
|
||||
from django.contrib.auth.views import login
|
||||
urlpatterns = [
|
||||
path('test_make/', make_notification),
|
||||
path('test/', live_tester),
|
||||
|
|
@ -19,6 +29,7 @@ if StrictVersion(get_version()) >= StrictVersion('2.0'):
|
|||
]
|
||||
else:
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib.auth.views import login
|
||||
urlpatterns = [
|
||||
url(r'^login/$', login, name='login'), # reverse for django login is not working
|
||||
url(r'^test_make/', make_notification),
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
''' Django Notifications exemple views '''
|
||||
from distutils.version import StrictVersion # pylint: disable=no-name-in-module,import-error
|
||||
''' Django Notifications example views '''
|
||||
from distutils.version import \
|
||||
StrictVersion # pylint: disable=no-name-in-module,import-error
|
||||
|
||||
from django import get_version
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.forms import model_to_dict
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.generic import ListView
|
||||
from notifications import settings
|
||||
from notifications.models import Notification
|
||||
from notifications.utils import id2slug, slug2id
|
||||
from notifications.settings import get_config
|
||||
from notifications.utils import id2slug, slug2id
|
||||
from swapper import load_model
|
||||
|
||||
Notification = load_model('notifications', 'Notification')
|
||||
|
||||
if StrictVersion(get_version()) >= StrictVersion('1.7.0'):
|
||||
from django.http import JsonResponse # noqa
|
||||
|
|
@ -123,6 +127,7 @@ def delete(request, slug=None):
|
|||
return redirect('notifications:all')
|
||||
|
||||
|
||||
@never_cache
|
||||
def live_unread_notification_count(request):
|
||||
try:
|
||||
user_is_authenticated = request.user.is_authenticated()
|
||||
|
|
@ -140,6 +145,7 @@ def live_unread_notification_count(request):
|
|||
return JsonResponse(data)
|
||||
|
||||
|
||||
@never_cache
|
||||
def live_unread_notification_list(request):
|
||||
''' Return a json with a unread notification list '''
|
||||
try:
|
||||
|
|
@ -187,6 +193,7 @@ def live_unread_notification_list(request):
|
|||
return JsonResponse(data)
|
||||
|
||||
|
||||
@never_cache
|
||||
def live_all_notification_list(request):
|
||||
''' Return a json with a unread notification list '''
|
||||
try:
|
||||
|
|
|
|||
34
setup.py
34
setup.py
|
|
@ -21,24 +21,26 @@ setup(
|
|||
name='django-notifications-hq',
|
||||
version=version,
|
||||
description='GitHub notifications alike app for Django.',
|
||||
long_description=open('README.rst').read(),
|
||||
long_description=open('README.md').read(),
|
||||
author='django-notifications team',
|
||||
author_email='yang@yangyubo.com',
|
||||
url='http://github.com/django-notifications/django-notifications',
|
||||
install_requires=[
|
||||
'django>=1.7',
|
||||
'django-model-utils>=2.0.3',
|
||||
'jsonfield>=1.0.3',
|
||||
'pytz'
|
||||
'django>=3.2',
|
||||
'django-model-utils>=3.1.0',
|
||||
'jsonfield>=2.1.0',
|
||||
'pytz',
|
||||
'swapper'
|
||||
],
|
||||
test_requires=[
|
||||
'django>=1.7',
|
||||
'django-model-utils>=2.0.3',
|
||||
'jsonfield>=1.0.3',
|
||||
'django>=3.2',
|
||||
'django-model-utils>=3.1.0',
|
||||
'jsonfield>=2.1.0',
|
||||
'pytz'
|
||||
],
|
||||
packages=[
|
||||
'notifications',
|
||||
'notifications.base',
|
||||
'notifications.templatetags',
|
||||
'notifications.migrations',
|
||||
],
|
||||
|
|
@ -53,18 +55,18 @@ setup(
|
|||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Framework :: Django',
|
||||
'Framework :: Django :: 1.10',
|
||||
'Framework :: Django :: 1.9',
|
||||
'Framework :: Django :: 1.8',
|
||||
'Framework :: Django :: 1.7',
|
||||
'Framework :: Django :: 3.2',
|
||||
'Framework :: Django :: 4.0',
|
||||
'Framework :: Django :: 4.1',
|
||||
# Specify the Python versions you support here. In particular, ensure
|
||||
# that you indicate whether you support Python 2, Python 3 or both.
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: 3.11',
|
||||
'Topic :: Utilities'
|
||||
],
|
||||
keywords='django notifications github action event stream',
|
||||
|
|
|
|||
23
tox.ini
23
tox.ini
|
|
@ -1,18 +1,23 @@
|
|||
# -- FILE: tox.ini
|
||||
[tox]
|
||||
envlist =
|
||||
py{27,33,34}-django17
|
||||
py{27,33,34,35,36}-django{18,19,110,111}
|
||||
py{34,35,36}-django200
|
||||
py{37,38,39,310,311}-django32
|
||||
py{38,39,310}-django40
|
||||
py{38,39,310,311}-django41
|
||||
|
||||
[gh-actions]
|
||||
python =
|
||||
3.7: py37
|
||||
3.8: py38
|
||||
3.9: py39
|
||||
3.10: py310
|
||||
3.11: py311
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
coverage run --branch --source=notifications manage.py test
|
||||
deps =
|
||||
coverage
|
||||
django17: Django>=1.7,<1.8
|
||||
django18: Django>=1.8,<1.9
|
||||
django19: Django>=1.9,<1.10
|
||||
django110: Django>=1.10,<1.11
|
||||
django111: Django>=1.11,<2.0
|
||||
django200: Django>=2.0,<3.0
|
||||
django32: Django>=3.2,<4.0
|
||||
django40: Django>=4.0,<4.1
|
||||
django41: Django>=4.1,<4.2
|
||||
|
|
|
|||
Loading…
Reference in a new issue