Skip to content

Commit

Permalink
swappable device model (#239)
Browse files Browse the repository at this point in the history
* add swapper

* add tests and enable swapper

* more versions of python

* add databases

* fix tests

* migrate to mariadb

* fix test model

* add tastypie test

* update docs

* fix tests for swapped models

* add tests for admin panel

* cleanup tests

* add test device swap owner

* fix links

* remove pyest.ini

* fix docs

* add notes for tests

* removed redundant model

* split api tests

* remove comment code

* update docs

* update to create default device only when required

* add tests for swapped model

---------

Co-authored-by: Mojca Rojko <[email protected]>
  • Loading branch information
Akay7 and xtrinch authored May 15, 2024
1 parent 99a6399 commit 887b440
Show file tree
Hide file tree
Showing 24 changed files with 399 additions and 30 deletions.
82 changes: 82 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,74 @@ logins on the same device, you do not wish the old user to receive messages whil
Via DRF, any creation of device with an already existing registration ID will be transformed into an update.
If done manually, you are responsible for deleting the old device entry.

Using custom FCMDevice model
----------------------------

If there's a need to store additional information or change type of fields in the FCMDevice model.
You could simple override this model. To do this, inherit your model from the AbstractFCMDevice class.

In your ``your_app/models.py``:

.. code-block:: python
import uuid
from django.db import models
from fcm_django.models import AbstractFCMDevice
class CustomDevice(AbstractFCMDevice):
# fields could be overwritten
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# could be added new fields
updated_at = models.DateTimeField(auto_now=True)
In your ``settings.py``:

.. code-block:: python
FCM_DJANGO_FCMDEVICE_MODEL = "your_app.CustomDevice"
In the DB will be two tables one that was created by this package and other your own. New data will appears only in your own table.
If you don't want default table appears in the DB then you should remove ``fcm_django`` out of ``INSTALLED_APPS`` at ``settings.py``:

.. code-block:: python
INSTALLED_APPS = (
...
# "fcm_django", - remove this line
"your_app", # your app should appears
...
)
After setup your own ``Model`` don't forget to create ``migrations`` for your app and call ``migrate`` command.

After removing ``"fcm_django"`` out of ``INSTALLED_APPS``. You will need to re-register the Device in order to see it in the admin panel.
This can be accomplished as follows at ``your_app/admin.py``:

.. code-block:: python
from django.contrib import admin
from fcm_django.admin import DeviceAdmin
from your_app.models import CustomDevice
admin.site.unregister(CustomDevice)
admin.site.register(CustomDevice, DeviceAdmin)
If you choose to move forward with swapped models then:

1. On existed project you have to keep in mind there are required manual work to move data from one table to anther.
2. If there's any tables with FK to swapped model then you have to deal with them on your own.

Note: This functionality based on `Swapper <https://pypi.org/project/swapper/>`_ that based on functionality
that allow to use a `custom User model <https://docs.djangoproject.com/en/4.2/topics/auth/customizing/#substituting-a-custom-user-model>`_.
So this functionality have the same limitations.
The most is important limitation it is that is difficult to start out with a default (non-swapped) model
and then later to switch to a swapped implementation without doing some migration hacking.

Python 3 support
----------------
- ``fcm-django`` is fully compatible with Python 3.7+
Expand Down Expand Up @@ -407,3 +475,17 @@ Contributing

To setup the development environment, simply do ``pip install -r requirements_dev.txt``
To manually run the pre-commit hook, run `pre-commit run --all-files`.

Because there's possibility to use swapped models therefore tests contains two config files:

1. with default settings and non swapped models ``settings/default.py``
2. and with overwritten settings only that required by swapper - ``settings/swap.py``

To run tests locally you could use ``pytest``, and if you need to check migrations on different DB then you have to specify environment variable ``DATABASE_URL`` ie

.. code-block:: console
export DATABASE_URL=postgres://postgres:[email protected]:5432/postgres
export DJANGO_SETTINGS_MODULE=tests.settings.default
# or export DJANGO_SETTINGS_MODULE=tests.settings.swap
pytest
90 changes: 90 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,14 @@ Sending messages in bulk
# Or (send_message parameters include: messages, dry_run, app)
FCMDevice.objects.send_message(Message(...))
Sending messages raises all the errors that ``firebase-admin`` raises, so make sure
they are caught and dealt with in your application code:

- ``FirebaseError`` – If an error occurs while sending the message to the FCM service.
- ``ValueError`` – If the input arguments are invalid.

For more info, see https://firebase.google.com/docs/reference/admin/python/firebase_admin.messaging#firebase_admin.messaging.BatchResponse

Subscribing or Unsubscribing Users to topic
-------------------------------------------

Expand Down Expand Up @@ -372,6 +380,74 @@ logins on the same device, you do not wish the old user to receive messages whil
Via DRF, any creation of device with an already existing registration ID will be transformed into an update.
If done manually, you are responsible for deleting the old device entry.

Using custom FCMDevice model
----------------------------

If there's a need to store additional information or change type of fields in the FCMDevice model.
You could simple override this model. To do this, inherit your model from the AbstractFCMDevice class.

In your ``your_app/models.py``:

.. code-block:: python
import uuid
from django.db import models
from fcm_django.models import AbstractFCMDevice
class CustomDevice(AbstractFCMDevice):
# fields could be overwritten
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# could be added new fields
updated_at = models.DateTimeField(auto_now=True)
In your ``settings.py``:

.. code-block:: python
FCM_DJANGO_FCMDEVICE_MODEL = "your_app.CustomDevice"
In the DB will be two tables one that was created by this package and other your own. New data will appears only in your own table.
If you don't want default table appears in the DB then you should remove ``fcm_django`` out of ``INSTALLED_APPS`` at ``settings.py``:

.. code-block:: python
INSTALLED_APPS = (
...
# "fcm_django", - remove this line
"your_app", # your app should appears
...
)
After setup your own ``Model`` don't forget to create ``migrations`` for your app and call ``migrate`` command.

After removing ``"fcm_django"`` out of ``INSTALLED_APPS``. You will need to re-register the Device in order to see it in the admin panel.
This can be accomplished as follows at ``your_app/admin.py``:

.. code-block:: python
from django.contrib import admin
from fcm_django.admin import DeviceAdmin
from your_app.models import CustomDevice
admin.site.unregister(CustomDevice)
admin.site.register(CustomDevice, DeviceAdmin)
If you choose to move forward with swapped models then:

1. On existed project you have to keep in mind there are required manual work to move data from one table to anther.
2. If there's any tables with FK to swapped model then you have to deal with them on your own.

Note: This functionality based on `Swapper <https://pypi.org/project/swapper/>`_ that based on functionality
that allow to use a `custom User model <https://docs.djangoproject.com/en/4.2/topics/auth/customizing/#substituting-a-custom-user-model>`_.
So this functionality have the same limitations.
The most is important limitation it is that is difficult to start out with a default (non-swapped) model
and then later to switch to a swapped implementation without doing some migration hacking.

Python 3 support
----------------
- ``fcm-django`` is fully compatible with Python 3.7+
Expand Down Expand Up @@ -399,3 +475,17 @@ Contributing

To setup the development environment, simply do ``pip install -r requirements_dev.txt``
To manually run the pre-commit hook, run `pre-commit run --all-files`.

Because there's possibility to use swapped models therefore tests contains two config files:

1. with default settings and non swapped models ``settings/default.py``
2. and with overwritten settings only that required by swapper - ``settings/swap.py``

To run tests locally you could use ``pytest``, and if you need to check migrations on different DB then you have to specify environment variable ``DATABASE_URL`` ie

.. code-block:: console
export DATABASE_URL=postgres://postgres:[email protected]:5432/postgres
export DJANGO_SETTINGS_MODULE=tests.settings.default
# or export DJANGO_SETTINGS_MODULE=tests.settings.swap
pytest
5 changes: 4 additions & 1 deletion fcm_django/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import List, Tuple, Union

import swapper
from django.apps import apps
from django.contrib import admin, messages
from django.utils.translation import gettext_lazy as _
Expand All @@ -12,11 +13,13 @@
TopicManagementResponse,
)

from fcm_django.models import FCMDevice, FirebaseResponseDict, fcm_error_list
from fcm_django.models import FirebaseResponseDict, fcm_error_list
from fcm_django.settings import FCM_DJANGO_SETTINGS as SETTINGS

User = apps.get_model(*SETTINGS["USER_MODEL"].split("."))

FCMDevice = swapper.load_model("fcm_django", "fcmdevice")


class DeviceAdmin(admin.ModelAdmin):
list_display = (
Expand Down
4 changes: 3 additions & 1 deletion fcm_django/api/rest_framework.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import swapper
from django.db.models import Q
from rest_framework import permissions, status
from rest_framework.mixins import CreateModelMixin
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, Serializer, ValidationError
from rest_framework.viewsets import GenericViewSet, ModelViewSet

from fcm_django.models import FCMDevice
from fcm_django.settings import FCM_DJANGO_SETTINGS as SETTINGS

FCMDevice = swapper.load_model("fcm_django", "fcmdevice")


# Serializers
class DeviceSerializerMixin(ModelSerializer):
Expand Down
3 changes: 2 additions & 1 deletion fcm_django/api/tastypie.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import swapper
from tastypie.authentication import BasicAuthentication
from tastypie.authorization import Authorization
from tastypie.resources import ModelResource

from fcm_django.models import FCMDevice
FCMDevice = swapper.load_model("fcm_django", "fcmdevice")


class FCMDeviceResource(ModelResource):
Expand Down
4 changes: 4 additions & 0 deletions fcm_django/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from copy import copy
from typing import List, NamedTuple, Sequence, Union

import swapper
from django.db import models
from django.utils.translation import gettext_lazy as _
from firebase_admin import messaging
Expand Down Expand Up @@ -392,3 +393,6 @@ class Meta:
indexes = [
models.Index(fields=["registration_id", "user"]),
]

app_label = "fcm_django"
swappable = swapper.swappable_setting("fcm_django", "fcmdevice")
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[tool.pytest.ini_options]
pythonpath = ["."]
DJANGO_SETTINGS_MODULE= "tests.testing_settings"
DJANGO_SETTINGS_MODULE = "tests.settings.default"
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ Django>=3.2
django-tastypie>=0.14.0
djangorestframework>=3.9.2
firebase-admin>=6.2,<7
swapper>=1.3.0
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"fcm_django/migrations",
],
python_requires=">=3.7",
install_requires=["firebase-admin>=6.2,<7", "Django"],
install_requires=["firebase-admin>=6.2,<7", "Django", "swapper"],
author=fcm_django.__author__,
author_email=fcm_django.__email__,
classifiers=CLASSIFIERS,
Expand Down
13 changes: 4 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from unittest.mock import sentinel

import pytest
import swapper
from firebase_admin.exceptions import FirebaseError
from firebase_admin.messaging import Message
from pytest_mock import MockerFixture

from fcm_django.models import DeviceType, FCMDevice
from fcm_django.models import DeviceType

FCMDevice = swapper.load_model("fcm_django", "fcmdevice")


@pytest.fixture
Expand Down Expand Up @@ -59,11 +62,3 @@ def mock_firebase_send(mocker: MockerFixture, firebase_message_id_send):
mock = mocker.patch("fcm_django.models.messaging.send")
mock.return_value = firebase_message_id_send
return mock


@pytest.fixture
def mock_fcm_device_deactivate(mocker: MockerFixture):
return mocker.patch(
"fcm_django.models.FCMDevice.deactivate_devices_with_error_result",
autospec=True,
)
Empty file added tests/settings/__init__.py
Empty file.
5 changes: 0 additions & 5 deletions tests/testing_settings.py → tests/settings/base.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import dj_database_url
from firebase_admin import initialize_app

SECRET_KEY = "ToP SeCrEt"


INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"fcm_django",
]
MIDDLEWARE = [
"django.contrib.sessions.middleware.SessionMiddleware",
Expand All @@ -19,7 +16,6 @@
]

DATABASES = {"default": dj_database_url.config(default="sqlite://")}

USE_TZ = True
ROOT_URLCONF = "tests.urls"
TEMPLATES = [
Expand All @@ -37,4 +33,3 @@
},
},
]
FIREBASE_APP = initialize_app()
7 changes: 7 additions & 0 deletions tests/settings/default.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .base import *

INSTALLED_APPS += [
"fcm_django",
]

IS_SWAP = False # Only to distinguish which model is used in the tests
9 changes: 9 additions & 0 deletions tests/settings/swap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .base import *

INSTALLED_APPS += [
"tests.swapped_models",
]

FCM_DJANGO_FCMDEVICE_MODEL = "swapped_models.CustomDevice"

IS_SWAP = True # Only to distinguish which model is used in the tests
Empty file.
8 changes: 8 additions & 0 deletions tests/swapped_models/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.contrib import admin

from fcm_django.admin import DeviceAdmin

from .models import CustomDevice

admin.site.unregister(CustomDevice)
admin.site.register(CustomDevice, DeviceAdmin)
Loading

0 comments on commit 887b440

Please sign in to comment.