diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index faff9b0..618c1ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,15 +13,15 @@ jobs: fail-fast: false matrix: lint-command: - - "bandit -r hijack -x hijack/tests" + - "bandit -r mailauth -x tests" - "black --check --diff ." - "flake8 ." - "isort --check-only --diff ." - "pydocstyle ." runs-on: ubuntu-latest steps: - - uses: actions/setup-python@v2.3.1 - - uses: actions/checkout@v2.4.0 + - uses: actions/setup-python@v3 + - uses: actions/checkout@v3 with: cache: 'pip' cache-dependency-path: 'requirements.txt' @@ -32,24 +32,24 @@ jobs: runs-on: ubuntu-latest steps: - run: sudo apt install -y gettext - - uses: actions/setup-python@v2.3.1 - - uses: actions/setup-node@v2.5.1 + - uses: actions/setup-python@v3 + - uses: actions/setup-node@v3.1.1 with: node-version: 'lts/*' - - uses: actions/checkout@v2.4.0 + - uses: actions/checkout@v3 - name: Install Python dependencies run: python -m pip install --upgrade pip setuptools wheel twine readme-renderer - run: python setup.py sdist bdist_wheel - run: python -m twine check dist/* - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: path: dist/* docs: runs-on: ubuntu-latest steps: - - uses: actions/setup-python@v2.3.1 - - uses: actions/checkout@v2.4.0 + - uses: actions/setup-python@v3 + - uses: actions/checkout@v3 with: cache: 'pip' cache-dependency-path: 'requirements.txt' @@ -69,11 +69,11 @@ jobs: - "3.10" steps: - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2.3.1 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - run: python -m pip install --upgrade pip setuptools wheel - - uses: actions/checkout@v2.4.0 + - uses: actions/checkout@v3 - run: python setup.py test - name: Codecov run: | @@ -90,11 +90,11 @@ jobs: python-version: ["3.10"] steps: - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2.3.1 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - run: python -m pip install --upgrade pip setuptools wheel - - uses: actions/checkout@v2.4.0 + - uses: actions/checkout@v3 - run: python -m pip install -e ".[${{ matrix.extras }}]" - run: python setup.py test - name: Codecov @@ -110,9 +110,7 @@ jobs: matrix: python-version: ["3.10"] django-version: - - "2.2a1" - - "3.2a1" - - "4.0a1" + - "4.0" services: postgres: image: postgres @@ -124,12 +122,12 @@ jobs: options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2.3.1 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} - run: python -m pip install --upgrade pip setuptools wheel - - uses: actions/checkout@v2.4.0 - - run: python -m pip install "psycopg2-binary<2.9" Django~=${{ matrix.django-version }} + - uses: actions/checkout@v3 + - run: python -m pip install "psycopg2-binary<2.9" Django~=${{ matrix.django-version }}a - run: python setup.py test env: DB_PORT: ${{ job.services.postgres.ports[5432] }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ffad020..42e9ab3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,8 +9,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.4.0 - - uses: actions/setup-python@v2.3.1 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 - name: Install Python dependencies run: python -m pip install --upgrade pip setuptools wheel twine - name: Build dist packages diff --git a/docs/customizing.rst b/docs/customizing.rst index 9626e68..0feb8ca 100644 --- a/docs/customizing.rst +++ b/docs/customizing.rst @@ -118,7 +118,9 @@ API documentation :members: .. autoattribute:: mailauth.contrib.user.models.AbstractEmailUser.email + :noindex: .. autoattribute:: mailauth.contrib.user.models.AbstractEmailUser.session_salt + :noindex: .. autoclass:: mailauth.contrib.user.models.EmailUser :members: diff --git a/docs/index.rst b/docs/index.rst index 70a773f..0a08e0a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,6 +10,7 @@ All Contents usage templates + privacy customizing settings contributing diff --git a/docs/privacy.rst b/docs/privacy.rst new file mode 100644 index 0000000..e180d4b --- /dev/null +++ b/docs/privacy.rst @@ -0,0 +1,40 @@ +Privacy +======== + +Anonymization +------------- + +User privacy is important, not only to meet local regulations, but also to +protect your users and allow them to exercise their rights. However, +it's not always practical to delete users, especially if they have dependent +objects, that are relevant for statistical analysis. + +Anonymization is a process of removing the user's personal data whilst keeping +related data intact. This is done by using the ``anomymize`` method. + + + +.. automethod:: mailauth.contrib.user.models.AbstractEmailUser.anonymize + :noindex: + +This method may be overwritten to provide anonymization for you custom user model. + +Related objects may also listen to the anonymize signal. + +.. autoclass:: mailauth.contrib.user.signals.anonymize + +All those methods can be conveniently triggered via the ``anonymize`` admin action. + +.. autoclass:: mailauth.contrib.user.admin.AnonymizableAdminMixin + :members: + +Liability Waiver +---------------- + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mailauth/contrib/admin/__init__.py b/mailauth/contrib/admin/__init__.py index f811b22..e69de29 100644 --- a/mailauth/contrib/admin/__init__.py +++ b/mailauth/contrib/admin/__init__.py @@ -1,4 +0,0 @@ -import django - -if django.VERSION < (4, 0): - default_app_config = "mailauth.contrib.admin.apps.MailAuthAdmin" diff --git a/mailauth/contrib/admin/locale/de/LC_MESSAGES/django.po b/mailauth/contrib/admin/locale/de/LC_MESSAGES/django.po index 9fd6104..dd37b9a 100644 --- a/mailauth/contrib/admin/locale/de/LC_MESSAGES/django.po +++ b/mailauth/contrib/admin/locale/de/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-04-12 18:14+0200\n" +"POT-Creation-Date: 2022-04-14 16:57+0200\n" "PO-Revision-Date: 2019-04-12 18:15+0200\n" "Last-Translator: Johannes Hoppe \n" "Language-Team: \n" @@ -65,6 +65,6 @@ msgstr "" msgid "Resend login email" msgstr "Login E-Mail erneut senden" -#: views.py:16 +#: views.py:17 msgid "Log in" msgstr "Anmelden" diff --git a/mailauth/contrib/user/__init__.py b/mailauth/contrib/user/__init__.py index f6b481d..e69de29 100644 --- a/mailauth/contrib/user/__init__.py +++ b/mailauth/contrib/user/__init__.py @@ -1,4 +0,0 @@ -import django - -if django.VERSION < (4, 0): - default_app_config = "mailauth.contrib.user.apps.AuthConfig" diff --git a/mailauth/contrib/user/admin.py b/mailauth/contrib/user/admin.py index 217e9c0..a3ac665 100644 --- a/mailauth/contrib/user/admin.py +++ b/mailauth/contrib/user/admin.py @@ -1,12 +1,50 @@ from django.contrib import admin from django.contrib.auth.models import Group, Permission +from django.utils.translation import gettext_lazy as _, ngettext from . import models +class AnonymizableAdminMixin: + """ + Mixin for admin classes that provides a `anonymize` action. + + This mixin calls the `anonymize` method of all user model instances. + """ + + actions = ["anonymize"] + + @admin.action( + permissions=["anonymize"], + description=_("Anonymize selected %(verbose_name_plural)s"), + ) + def anonymize(self, request, queryset): + count = queryset.count() + for user in queryset.iterator(): + user.anonymize() + + self.message_user( + request, + ngettext( + "%(count)s %(obj_name)s has successfully been anonymized.", + "%(count)s %(obj_name)s have successfully been anonymized.", + count, + ) + % { + "count": count, + "obj_name": self.model._meta.verbose_name_plural + if count > 1 + else self.model._meta.verbose_name, + }, + fail_silently=True, + ) + + def has_anonymize_permission(self, request, obj=None): + return request.user.has_perm(f"{self.opts.app_label}.anonymize", obj=obj) + + @admin.register(models.EmailUser) -class EmailUserAdmin(admin.ModelAdmin): - app_label = "asdf" +class EmailUserAdmin(AnonymizableAdminMixin, admin.ModelAdmin): list_display = ("email", "first_name", "last_name", "is_staff") list_filter = ("is_staff", "is_superuser", "is_active", "groups") search_fields = ("first_name", "last_name", "email") diff --git a/mailauth/contrib/user/locale/de/LC_MESSAGES/django.po b/mailauth/contrib/user/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..fce2313 --- /dev/null +++ b/mailauth/contrib/user/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,35 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Johannes Hoppe , 2019. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-04-14 16:56+0200\n" +"PO-Revision-Date: 2022-04-14 15:38+0200\n" +"Last-Translator: Johannes Maron \n" +"Language-Team: \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.0.1\n" + +#: admin.py:19 +#, python-format +msgid "Anonymize selected %(verbose_name_plural)s" +msgstr "Ausgewählte %(verbose_name_plural)s anonymisieren" + +#: admin.py:29 +#, python-format +msgid "%(count)s %(obj_name)s has successfully been anonymized." +msgid_plural "%(count)s %(obj_name)s have successfully been anonymized." +msgstr[0] "%(count)s %(obj_name)s wurde erfolgreich anonymisiert." +msgstr[1] "%(count)s %(obj_name)s wurden erfolgreich anonymisiert." + +#: models.py:56 +msgid "email address" +msgstr "E-Mail-Adresse" diff --git a/mailauth/contrib/user/migrations/0005_emailuser_email_hash_alter_emailuser_email.py b/mailauth/contrib/user/migrations/0005_emailuser_email_hash_alter_emailuser_email.py new file mode 100644 index 0000000..c9220c3 --- /dev/null +++ b/mailauth/contrib/user/migrations/0005_emailuser_email_hash_alter_emailuser_email.py @@ -0,0 +1,38 @@ +from django.db import migrations, models + +try: + from django.contrib.postgres.fields import CIEmailField +except ImportError: + CIEmailField = models.EmailField + + +class Migration(migrations.Migration): + + dependencies = [ + ("mailauth_user", "0004_auto_20200812_0722"), + ] + + operations = [ + # add new permissions + migrations.AlterModelOptions( + name="emailuser", + options={ + "permissions": [("anonymize", "Can anonymize user")], + "verbose_name": "user", + "verbose_name_plural": "users", + }, + ), + # email is now nullable + migrations.AlterField( + model_name="emailuser", + name="email", + field=CIEmailField( + blank=True, + db_index=True, + max_length=254, + null=True, + unique=True, + verbose_name="email address", + ), + ), + ] diff --git a/mailauth/contrib/user/models.py b/mailauth/contrib/user/models.py index 42d5749..fc310c4 100644 --- a/mailauth/contrib/user/models.py +++ b/mailauth/contrib/user/models.py @@ -5,6 +5,8 @@ from django.utils.crypto import get_random_string, salted_hmac from django.utils.translation import gettext_lazy as _ +from . import signals + try: from django.contrib.postgres.fields import CIEmailField except ImportError: @@ -50,15 +52,17 @@ class AbstractEmailUser(AbstractUser): username = None password = None - email = CIEmailField(_("email address"), unique=True, db_index=True) - """The field is unique and case insensitive to serve as a better username.""" + email = CIEmailField( + _("email address"), blank=True, null=True, unique=True, db_index=True + ) + """Unique and case insensitive to serve as a better username.""" - # Salt for the session hash replacing the password in this function. session_salt = models.CharField( max_length=12, editable=False, default=_get_session_salt, ) + """Salt for the session hash replacing the password in this function.""" def has_usable_password(self): return False @@ -67,6 +71,9 @@ def has_usable_password(self): class Meta(AbstractUser.Meta): abstract = True + permissions = [ + ("anonymize", "Can anonymize user"), + ] def _legacy_get_session_auth_hash(self): # RemovedInDjango40Warning: pre-Django 3.1 hashes will be invalid. @@ -92,6 +99,31 @@ def get_session_auth_hash(self): algorithm=algorithm, ).hexdigest() + def anonymize(self, commit=True): + """ + Anonymize the user data for privacy purposes. + + This method will erase the email address, first and last name. + You may overwrite this method to add additional fields to anonymize:: + + class MyUser(AbstractEmailUser): + def anonymize(self, commit=True): + super().anonymize(commit=False) # do not commit yet + self.phone_number = None + if commit: + self.save() + """ + self.email = None + self.first_name = "" + self.last_name = "" + update_fields = ["email", "first_name", "last_name"] + if commit: + self.save(update_fields=update_fields) + signals.anonymize.send( + sender=self.__class__, instance=self, update_fields=tuple(update_fields) + ) + return update_fields + delattr(AbstractEmailUser, "password") diff --git a/mailauth/contrib/user/signals.py b/mailauth/contrib/user/signals.py new file mode 100644 index 0000000..3bfa2a1 --- /dev/null +++ b/mailauth/contrib/user/signals.py @@ -0,0 +1,19 @@ +from django.dispatch import Signal + +anonymize = Signal() +""" +Signal that is emitted when a user and all their data should be anonymized. + +Usage:: + + from django.dispatch import receiver + from mailauth.contrib.user.models import EmailUser + from mailauth.contrib.user.signals import anonymize + + + @receiver(anonymize, sender=EmailUser) + def anonymize_user(sender, instance, update_fields, **kwargs): + # Do something with related user data + instance.related_model.delete() + +""" diff --git a/mailauth/contrib/wagtail/__init__.py b/mailauth/contrib/wagtail/__init__.py index 8e775e4..e69de29 100644 --- a/mailauth/contrib/wagtail/__init__.py +++ b/mailauth/contrib/wagtail/__init__.py @@ -1,4 +0,0 @@ -import django - -if django.VERSION < (4, 0): - default_app_config = "mailauth.contrib.wagtail.apps.MailAuthWagtail" diff --git a/mailauth/signing.py b/mailauth/signing.py index b492f79..ff174ef 100644 --- a/mailauth/signing.py +++ b/mailauth/signing.py @@ -1,17 +1,6 @@ -import django from django.contrib.auth import get_user_model from django.core import signing -if django.VERSION >= (4, 0): - b62_encode = signing.b62_encode - b62_decode = signing.b62_decode -else: - from django.utils import baseconv - - b62_encode = baseconv.base62.encode - b62_decode = baseconv.base62.decode - - __all__ = ( "UserDoesNotExist", "UserSigner", @@ -39,7 +28,7 @@ def to_timestamp(value): """ if value is None: return "" - return b62_encode(int(value.timestamp())) + return signing.b62_encode(int(value.timestamp())) def sign(self, user): """ @@ -60,7 +49,7 @@ def sign(self, user): def _make_hash_value(self, user): last_login = self.to_timestamp(user.last_login) - user_pk = b62_encode(user.pk) + user_pk = signing.b62_encode(user.pk) return self.sep.join((user_pk, last_login)) def unsign(self, value, max_age=None, single_use=True): @@ -94,7 +83,7 @@ def unsign(self, value, max_age=None, single_use=True): """ result = super().unsign(value, max_age=max_age) user_pk, last_login = result.rsplit(self.sep, 2) - user_pk = b62_decode(user_pk) + user_pk = signing.b62_decode(user_pk) try: user = get_user_model()._default_manager.get(pk=user_pk) except get_user_model().DoesNotExist as e: diff --git a/mailauth/views.py b/mailauth/views.py index ecc24c5..c82cc2c 100644 --- a/mailauth/views.py +++ b/mailauth/views.py @@ -43,11 +43,11 @@ def get_initial(self): } -INTERNAL_LOGIN_URL_TOKEN = "login-token" +INTERNAL_LOGIN_URL_TOKEN = "login-token" # nosec class LoginTokenView(DjangoLoginView): - """Authenticate a user via a access token.""" + """Authenticate a user via an access token.""" redirect_field_name = REDIRECT_FIELD_NAME diff --git a/requirements.txt b/requirements.txt index d296f6d..b62c63b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,7 @@ Django>=2.2 Wagtail>=2.8 -bandit==1.7.1 -black==21.12b0 +bandit==1.7.4 +black==22.3.0 flake8==4.0.1 isort==5.10.1 -msgcheck==3.1 pydocstyle==6.1.1 diff --git a/setup.cfg b/setup.cfg index 89a5c8b..00a18d0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,8 +21,6 @@ classifier = Topic :: Internet :: WWW/HTTP Topic :: Internet Framework :: Django - Framework :: Django :: 2.2 - Framework :: Django :: 3.2 Framework :: Django :: 4.0 keywords = django, otp, password, email @@ -30,7 +28,7 @@ keywords = django, otp, password, email include_package_data = True packages = find: install_requires = - django>=2.2 + django>=4.0 setup_requires = setuptools_scm sphinx diff --git a/tests/conftest.py b/tests/conftest.py index 532a7f9..9194bfc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,3 @@ -import django import pytest from django.contrib.auth import get_user_model from django.utils import timezone @@ -37,8 +36,6 @@ def admin_user(db): @pytest.fixture() def signature(): """Return a signature matching the user fixture.""" - if django.VERSION < (3, 1): - return "LZ:173QUS:1Hjptg:umUR9iKN1rxDezT-dZGwqcqsM5Y" return "LZ:173QUS:1Hjptg:6oq5DS1NJ7SxJ1o-CpfgaqrImVaRpkcHrzV9yltwcHM" diff --git a/tests/contrib/auth/test_admin.py b/tests/contrib/auth/test_admin.py new file mode 100644 index 0000000..35ada72 --- /dev/null +++ b/tests/contrib/auth/test_admin.py @@ -0,0 +1,71 @@ +from unittest.mock import Mock + +import pytest +from django.contrib import admin +from django.contrib.auth.models import Permission + +from mailauth.contrib.user.admin import AnonymizableAdminMixin +from mailauth.contrib.user.models import EmailUser + + +class TestAnonymizableAdminMixin: + def test_anonymize__none(self, rf): + class MyUserModel(EmailUser): + class Meta: + app_label = "test" + verbose_name = "singular" + verbose_name_plural = "plural" + + class MyModelAdmin(AnonymizableAdminMixin, admin.ModelAdmin): + pass + + request = rf.get("/") + MyModelAdmin(MyUserModel, admin.site).anonymize( + request, MyUserModel.objects.none() + ) + + @pytest.mark.django_db + def test_anonymize__one(self, rf, user, monkeypatch): + class MyModelAdmin(AnonymizableAdminMixin, admin.ModelAdmin): + pass + + monkeypatch.setattr(EmailUser, "anonymize", Mock()) + + request = rf.get("/") + MyModelAdmin(type(user), admin.site).anonymize( + request, type(user).objects.all() + ) + assert EmailUser.anonymize.was_called_once_with(user) + + @pytest.mark.django_db + def test_anonymize__many(self, rf, user, monkeypatch): + class MyModelAdmin(AnonymizableAdminMixin, admin.ModelAdmin): + pass + + monkeypatch.setattr(EmailUser, "anonymize", Mock()) + + request = rf.get("/") + MyModelAdmin(type(user), admin.site).anonymize( + request, type(user).objects.all() + ) + assert EmailUser.anonymize.was_called_once_with(user) + + def test_has_anonymize_permission(self, rf, user): + class MyModelAdmin(AnonymizableAdminMixin, admin.ModelAdmin): + pass + + user.is_staff = True + user.save() + request = rf.get("/") + request.user = user + assert not MyModelAdmin(type(user), admin.site).has_anonymize_permission( + request + ) + + permission = Permission.objects.get( + codename="anonymize", + ) + user.user_permissions.add(permission) + del user._perm_cache + del user._user_perm_cache + assert MyModelAdmin(type(user), admin.site).has_anonymize_permission(request) diff --git a/tests/contrib/auth/test_models.py b/tests/contrib/auth/test_models.py index 4a003cb..f757d1b 100644 --- a/tests/contrib/auth/test_models.py +++ b/tests/contrib/auth/test_models.py @@ -1,4 +1,3 @@ -import django import pytest from django.core.exceptions import FieldDoesNotExist @@ -29,14 +28,12 @@ def test_get_session_auth_hash__unique(self, db): assert spiderman.get_session_auth_hash() != ironman.get_session_auth_hash() - @pytest.mark.skipif(django.VERSION < (3, 1), reason="requires Django 3.1 or higher") def test_legacy_get_session_auth_hash__default(self, db): user = EmailUser(email="spiderman@avengers.com") assert user.session_salt assert user._legacy_get_session_auth_hash() - @pytest.mark.skipif(django.VERSION < (3, 1), reason="requires Django 3.1 or higher") def test_legacy_get_session_auth_hash__value_error(self, db): user = EmailUser(email="spiderman@avengers.com", session_salt=None) @@ -45,7 +42,6 @@ def test_legacy_get_session_auth_hash__value_error(self, db): assert "'session_salt' must be set" in str(e.value) - @pytest.mark.skipif(django.VERSION < (3, 1), reason="requires Django 3.1 or higher") def test_legacy_get_session_auth_hash__unique(self, db): spiderman = EmailUser(email="spiderman@avengers.com") ironman = EmailUser(email="ironman@avengers.com") diff --git a/tests/contrib/auth/test_signals.py b/tests/contrib/auth/test_signals.py new file mode 100644 index 0000000..ef8c280 --- /dev/null +++ b/tests/contrib/auth/test_signals.py @@ -0,0 +1,20 @@ +from unittest.mock import Mock + +import pytest +from django.dispatch import receiver + +from mailauth.contrib.user.signals import anonymize + + +@pytest.mark.django_db +def test_anonymize(user): + handler = Mock() + receiver(anonymize, sender=user.__class__)(handler) + handler.assert_not_called() + user.anonymize() + handler.assert_called_once_with( + signal=anonymize, + sender=user.__class__, + instance=user, + update_fields=("email", "first_name", "last_name"), + ) diff --git a/tests/test_models.py b/tests/test_models.py index 439fcd1..06144e9 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -20,3 +20,22 @@ def test_email__ci_unique(self, db): models.EmailUser.objects.create_user("IronMan@avengers.com") with pytest.raises(IntegrityError): models.EmailUser.objects.create_user("ironman@avengers.com") + + @pytest.mark.django_db + def test_anonymize(self): + user = models.EmailUser.objects.create_user( + email="ironman@avengers.com", first_name="Tony", last_name="Stark" + ) + assert user.anonymize() == ["email", "first_name", "last_name"] + assert not user.first_name + assert not user.last_name + assert not user.email + + def test_anonymize__no_commit(self): + user = models.EmailUser( + email="ironman@avengers.com", first_name="Tony", last_name="Stark" + ) + user.anonymize(commit=False) + assert not user.first_name + assert not user.last_name + assert not user.email diff --git a/tests/testapp/settings.py b/tests/testapp/settings.py index bacba2d..75329b0 100644 --- a/tests/testapp/settings.py +++ b/tests/testapp/settings.py @@ -13,7 +13,6 @@ import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) -import django BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -125,10 +124,6 @@ TIME_ZONE = "UTC" USE_I18N = True - -if django.VERSION < (4, 0): - USE_L10N = True - USE_TZ = True