From b5e25125b33da771260a68dddb670ebc21a0edf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Wed, 30 Mar 2022 16:57:46 +0200 Subject: [PATCH 1/6] Add .pre-commit-config.yaml and run it. --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 12 +- .gitignore | 1 - .pre-commit-config.yaml | 62 ++ ChangeLog | 2 - LICENSE | 2 +- invitations/__init__.py | 2 +- invitations/adapters.py | 94 +-- invitations/admin.py | 17 +- invitations/app_settings.py | 45 +- invitations/apps.py | 4 +- invitations/base_invitation.py | 28 +- invitations/exceptions.py | 5 +- invitations/forms.py | 46 +- invitations/managers.py | 4 +- invitations/migrations/0001_initial.py | 47 +- .../migrations/0002_auto_20151126_0426.py | 25 +- .../migrations/0003_auto_20151126_1523.py | 13 +- invitations/models.py | 70 +-- .../templates/invitations/forms/_invite.html | 4 +- .../invitations/messages/invite_accepted.txt | 2 +- invitations/urls.py | 21 +- invitations/utils.py | 9 +- invitations/views.py | 104 ++-- manage.py | 20 +- setup.cfg | 2 +- setup.py | 48 +- test_allauth_settings.py | 30 +- test_settings.py | 67 ++- test_urls.py | 15 +- tests/allauth/test_allauth.py | 175 +++--- tests/basic/tests.py | 544 +++++++++++------- tests/conftest.py | 41 +- tests/migrations/0001_initial.py | 22 +- 34 files changed, 924 insertions(+), 661 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dde96b5..c52985f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,4 +35,4 @@ jobs: with: user: jazzband password: ${{ secrets.JAZZBAND_RELEASE_KEY }} - repository_url: https://jazzband.co/projects/django-invitations/upload \ No newline at end of file + repository_url: https://jazzband.co/projects/django-invitations/upload diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 53bc39a..47ff6fd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,7 @@ name: Test on: [push, pull_request] - + jobs: build: name: build (Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }}) @@ -20,12 +20,12 @@ jobs: with: python-version: ${{ matrix.python-version }} - + - name: Get pip cache dir id: pip-cache run: | echo "::set-output name=dir::$(pip cache dir)" - + - name: Cache uses: actions/cache@v2 with: @@ -34,17 +34,17 @@ jobs: ${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.cfg') }}-${{ hashFiles('**/tox.ini') }} restore-keys: | ${{ matrix.python-version }}-v1- - + - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install --upgrade tox tox-gh-actions - + - name: Tox tests run: tox -v env: DJANGO: ${{ matrix.django-version }} - + - name: Upload coverage uses: codecov/codecov-action@v1 with: diff --git a/.gitignore b/.gitignore index 1637c41..83f560f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,3 @@ git-push.bat .python-version .coverage /.idea/ - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..74a9208 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,62 @@ +default_language_version: + python: python3 +repos: + - repo: https://github.com/psf/black + rev: 22.1.0 + hooks: + - id: black + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: check-ast + - id: check-merge-conflict + - id: check-case-conflict + - id: detect-private-key + - id: check-added-large-files + - id: check-json + - id: check-symlinks + - id: check-toml + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/pycqa/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + additional_dependencies: + - flake8-bugbear + - flake8-comprehensions + - flake8-tidy-imports + args: [--max-line-length=88] + - repo: https://github.com/asottile/reorder_python_imports + rev: v3.0.1 + hooks: + - id: reorder-python-imports + args: + - --py37-plus + - --application-directories=.:src + exclude: migrations/ + - repo: https://github.com/asottile/pyupgrade + rev: v2.31.1 + hooks: + - id: pyupgrade + args: + - --py37-plus + exclude: migrations/ + - repo: https://github.com/adamchainz/django-upgrade + rev: 1.4.0 + hooks: + - id: django-upgrade + args: + - --target-version=3.2 + - repo: https://github.com/asottile/yesqa + rev: v1.3.0 + hooks: + - id: yesqa + - repo: https://github.com/asottile/add-trailing-comma + rev: v2.2.1 + hooks: + - id: add-trailing-comma + - repo: https://github.com/hadialqattan/pycln + rev: v1.2.5 + hooks: + - id: pycln diff --git a/ChangeLog b/ChangeLog index 4f1b666..ae02704 100644 --- a/ChangeLog +++ b/ChangeLog @@ -346,5 +346,3 @@ x.x (yyyy-mm-dd) - Template path. [bee_keeper] - Name changes. [bee_keeper] - - diff --git a/LICENSE b/LICENSE index 70566f2..ef7e7ef 100644 --- a/LICENSE +++ b/LICENSE @@ -671,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read -. \ No newline at end of file +. diff --git a/invitations/__init__.py b/invitations/__init__.py index 26e4845..584387a 100644 --- a/invitations/__init__.py +++ b/invitations/__init__.py @@ -1 +1 @@ -default_app_config = 'invitations.apps.Config' +default_app_config = "invitations.apps.Config" diff --git a/invitations/adapters.py b/invitations/adapters.py index c02984a..32988d9 100644 --- a/invitations/adapters.py +++ b/invitations/adapters.py @@ -1,73 +1,69 @@ from django.conf import settings from django.contrib import messages from django.contrib.sites.models import Site -from django.core.mail import EmailMessage, EmailMultiAlternatives +from django.core.mail import EmailMessage +from django.core.mail import EmailMultiAlternatives from django.template import TemplateDoesNotExist from django.template.loader import render_to_string +from django.utils.encoding import force_str from .app_settings import app_settings from .utils import import_attribute -try: - from django.utils.encoding import force_str as force_text -except ImportError: - try: - from django.utils.encoding import force_text - except ImportError: - from django.utils.encoding import force_unicode as force_text - # Code credits here to django-allauth -class BaseInvitationsAdapter(object): +class BaseInvitationsAdapter: def stash_verified_email(self, request, email): - request.session['account_verified_email'] = email + request.session["account_verified_email"] = email def unstash_verified_email(self, request): - ret = request.session.get('account_verified_email') - request.session['account_verified_email'] = None + ret = request.session.get("account_verified_email") + request.session["account_verified_email"] = None return ret def format_email_subject(self, subject): prefix = app_settings.EMAIL_SUBJECT_PREFIX if prefix is None: site = Site.objects.get_current() - prefix = "[{name}] ".format(name=site.name) - return prefix + force_text(subject) + prefix = f"[{site.name}] " + return prefix + force_str(subject) def render_mail(self, template_prefix, email, context): """ Renders an e-mail to `email`. `template_prefix` identifies the e-mail that is to be sent, e.g. "account/email/email_confirmation" """ - subject = render_to_string('{0}_subject.txt'.format(template_prefix), - context) + subject = render_to_string(f"{template_prefix}_subject.txt", context) # remove superfluous line breaks subject = " ".join(subject.splitlines()).strip() subject = self.format_email_subject(subject) bodies = {} - for ext in ['html', 'txt']: + for ext in ["html", "txt"]: try: - template_name = '{0}_message.{1}'.format(template_prefix, ext) - bodies[ext] = render_to_string(template_name, - context).strip() + template_name = f"{template_prefix}_message.{ext}" + bodies[ext] = render_to_string(template_name, context).strip() except TemplateDoesNotExist: - if ext == 'txt' and not bodies: + if ext == "txt" and not bodies: # We need at least one body raise - if 'txt' in bodies: - msg = EmailMultiAlternatives(subject, - bodies['txt'], - settings.DEFAULT_FROM_EMAIL, - [email]) - if 'html' in bodies: - msg.attach_alternative(bodies['html'], 'text/html') + if "txt" in bodies: + msg = EmailMultiAlternatives( + subject, + bodies["txt"], + settings.DEFAULT_FROM_EMAIL, + [email], + ) + if "html" in bodies: + msg.attach_alternative(bodies["html"], "text/html") else: - msg = EmailMessage(subject, - bodies['html'], - settings.DEFAULT_FROM_EMAIL, - [email]) - msg.content_subtype = 'html' # Main content is now text/html + msg = EmailMessage( + subject, + bodies["html"], + settings.DEFAULT_FROM_EMAIL, + [email], + ) + msg.content_subtype = "html" # Main content is now text/html return msg def send_mail(self, template_prefix, email, context): @@ -75,8 +71,9 @@ def send_mail(self, template_prefix, email, context): msg.send() def is_open_for_signup(self, request): - if hasattr(request, 'session') and request.session.get( - 'account_verified_email'): + if hasattr(request, "session") and request.session.get( + "account_verified_email", + ): return True elif app_settings.INVITATION_ONLY is True: # Site is ONLY open for invites @@ -92,32 +89,39 @@ def clean_email(self, email): """ return email - def add_message(self, request, level, message_template, - message_context=None, extra_tags=''): + def add_message( + self, + request, + level, + message_template, + message_context=None, + extra_tags="", + ): """ Wrapper of `django.contrib.messages.add_message`, that reads the message text from a template. """ - if 'django.contrib.messages' in settings.INSTALLED_APPS: + if "django.contrib.messages" in settings.INSTALLED_APPS: try: if message_context is None: message_context = {} - message = render_to_string(message_template, - message_context).strip() + message = render_to_string(message_template, message_context).strip() if message: - messages.add_message(request, level, message, - extra_tags=extra_tags) + messages.add_message(request, level, message, extra_tags=extra_tags) except TemplateDoesNotExist: pass def get_invitations_adapter(): # Compatibility with legacy allauth only version. - LEGACY_ALLAUTH = hasattr(settings, 'ACCOUNT_ADAPTER') and \ - settings.ACCOUNT_ADAPTER == 'invitations.models.InvitationsAdapter' + LEGACY_ALLAUTH = ( + hasattr(settings, "ACCOUNT_ADAPTER") + and settings.ACCOUNT_ADAPTER == "invitations.models.InvitationsAdapter" + ) if LEGACY_ALLAUTH: # defer to allauth from allauth.account.adapter import get_adapter + return get_adapter() else: # load an adapter from elsewhere diff --git a/invitations/admin.py b/invitations/admin.py index 5235a96..e903215 100644 --- a/invitations/admin.py +++ b/invitations/admin.py @@ -1,7 +1,8 @@ from django.contrib import admin -from .utils import (get_invitation_admin_add_form, - get_invitation_admin_change_form, get_invitation_model) +from .utils import get_invitation_admin_add_form +from .utils import get_invitation_admin_change_form +from .utils import get_invitation_model Invitation = get_invitation_model() InvitationAdminAddForm = get_invitation_admin_add_form() @@ -9,16 +10,16 @@ class InvitationAdmin(admin.ModelAdmin): - list_display = ('email', 'sent', 'accepted') + list_display = ("email", "sent", "accepted") def get_form(self, request, obj=None, **kwargs): if obj: - kwargs['form'] = InvitationAdminChangeForm + kwargs["form"] = InvitationAdminChangeForm else: - kwargs['form'] = InvitationAdminAddForm - kwargs['form'].user = request.user - kwargs['form'].request = request - return super(InvitationAdmin, self).get_form(request, obj, **kwargs) + kwargs["form"] = InvitationAdminAddForm + kwargs["form"].user = request.user + kwargs["form"].request = request + return super().get_form(request, obj, **kwargs) admin.site.register(Invitation, InvitationAdmin) diff --git a/invitations/app_settings.py b/invitations/app_settings.py index d2b59d1..25e47a6 100644 --- a/invitations/app_settings.py +++ b/invitations/app_settings.py @@ -1,34 +1,34 @@ from django.conf import settings -class AppSettings(object): - +class AppSettings: def __init__(self, prefix): self.prefix = prefix def _setting(self, name, dflt): from django.conf import settings + return getattr(settings, self.prefix + name, dflt) @property def INVITATION_EXPIRY(self): - """ How long before the invitation expires """ - return self._setting('INVITATION_EXPIRY', 3) + """How long before the invitation expires""" + return self._setting("INVITATION_EXPIRY", 3) @property def INVITATION_ONLY(self): - """ Signup is invite only """ - return self._setting('INVITATION_ONLY', False) + """Signup is invite only""" + return self._setting("INVITATION_ONLY", False) @property def CONFIRM_INVITE_ON_GET(self): - """ Simple get request confirms invite """ - return self._setting('CONFIRM_INVITE_ON_GET', True) + """Simple get request confirms invite""" + return self._setting("CONFIRM_INVITE_ON_GET", True) @property def ACCEPT_INVITE_AFTER_SIGNUP(self): - """ Accept the invitation after the user finished signup. """ - return self._setting('ACCEPT_INVITE_AFTER_SIGNUP', False) + """Accept the invitation after the user finished signup.""" + return self._setting("ACCEPT_INVITE_AFTER_SIGNUP", False) @property def GONE_ON_ACCEPT_ERROR(self): @@ -36,28 +36,27 @@ def GONE_ON_ACCEPT_ERROR(self): If an invalid/expired/previously accepted key is provided, return a HTTP 410 GONE response. """ - return self._setting('GONE_ON_ACCEPT_ERROR', True) + return self._setting("GONE_ON_ACCEPT_ERROR", True) @property def ALLOW_JSON_INVITES(self): - """ Exposes json endpoint for mass invite creation """ - return self._setting('ALLOW_JSON_INVITES', False) + """Exposes json endpoint for mass invite creation""" + return self._setting("ALLOW_JSON_INVITES", False) @property def SIGNUP_REDIRECT(self): - """ Where to redirect on email confirm of invite """ - return self._setting('SIGNUP_REDIRECT', 'account_signup') + """Where to redirect on email confirm of invite""" + return self._setting("SIGNUP_REDIRECT", "account_signup") @property def LOGIN_REDIRECT(self): - """ Where to redirect on an expired or already accepted invite """ - return self._setting('LOGIN_REDIRECT', settings.LOGIN_URL) + """Where to redirect on an expired or already accepted invite""" + return self._setting("LOGIN_REDIRECT", settings.LOGIN_URL) @property def ADAPTER(self): - """ The adapter, setting ACCOUNT_ADAPTER overrides this default """ - return self._setting( - 'ADAPTER', 'invitations.adapters.BaseInvitationsAdapter') + """The adapter, setting ACCOUNT_ADAPTER overrides this default""" + return self._setting("ADAPTER", "invitations.adapters.BaseInvitationsAdapter") @property def EMAIL_MAX_LENGTH(self): @@ -88,15 +87,15 @@ def INVITE_FORM(self): def ADMIN_ADD_FORM(self): return self._setting( "ADMIN_ADD_FORM", - "invitations.forms.InvitationAdminAddForm" + "invitations.forms.InvitationAdminAddForm", ) @property def ADMIN_CHANGE_FORM(self): return self._setting( "ADMIN_CHANGE_FORM", - "invitations.forms.InvitationAdminChangeForm" + "invitations.forms.InvitationAdminChangeForm", ) -app_settings = AppSettings('INVITATIONS_') +app_settings = AppSettings("INVITATIONS_") diff --git a/invitations/apps.py b/invitations/apps.py index 31ab087..457f960 100644 --- a/invitations/apps.py +++ b/invitations/apps.py @@ -4,5 +4,5 @@ class Config(AppConfig): """Config.""" - name = 'invitations' - label = 'invitations' + name = "invitations" + label = "invitations" diff --git a/invitations/base_invitation.py b/invitations/base_invitation.py index 15f0589..5e95edd 100644 --- a/invitations/base_invitation.py +++ b/invitations/base_invitation.py @@ -6,11 +6,15 @@ class AbstractBaseInvitation(models.Model): - accepted = models.BooleanField(verbose_name=_('accepted'), default=False) - key = models.CharField(verbose_name=_('key'), max_length=64, unique=True) - sent = models.DateTimeField(verbose_name=_('sent'), null=True) + accepted = models.BooleanField(verbose_name=_("accepted"), default=False) + key = models.CharField(verbose_name=_("key"), max_length=64, unique=True) + sent = models.DateTimeField(verbose_name=_("sent"), null=True) inviter = models.ForeignKey( - settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.CASCADE) # noqa + settings.AUTH_USER_MODEL, + null=True, + blank=True, + on_delete=models.CASCADE, + ) objects = BaseInvitationManager() @@ -19,21 +23,13 @@ class Meta: @classmethod def create(cls, email, inviter=None, **kwargs): - raise NotImplementedError( - 'You should implement the create method class' - ) + raise NotImplementedError("You should implement the create method class") def key_expired(self): - raise NotImplementedError( - 'You should implement the key_expired method' - ) + raise NotImplementedError("You should implement the key_expired method") def send_invitation(self, request, **kwargs): - raise NotImplementedError( - 'You should implement the send_invitation method' - ) + raise NotImplementedError("You should implement the send_invitation method") def __str__(self): - raise NotImplementedError( - 'You should implement the __str__ method' - ) + raise NotImplementedError("You should implement the __str__ method") diff --git a/invitations/exceptions.py b/invitations/exceptions.py index 3c6cd26..33b05b4 100644 --- a/invitations/exceptions.py +++ b/invitations/exceptions.py @@ -1,13 +1,16 @@ class AlreadyInvited(Exception): """User has a valid, pending invitation""" + pass class AlreadyAccepted(Exception): """User has already accepted an invitation""" + pass class UserRegisteredEmail(Exception): - """This email is already registered by a site user """ + """This email is already registered by a site user""" + pass diff --git a/invitations/forms.py b/invitations/forms.py index efa61c9..e33c269 100644 --- a/invitations/forms.py +++ b/invitations/forms.py @@ -3,20 +3,19 @@ from django.utils.translation import gettext_lazy as _ from .adapters import get_invitations_adapter -from .exceptions import AlreadyAccepted, AlreadyInvited, UserRegisteredEmail +from .exceptions import AlreadyAccepted +from .exceptions import AlreadyInvited +from .exceptions import UserRegisteredEmail from .utils import get_invitation_model Invitation = get_invitation_model() -class CleanEmailMixin(object): - +class CleanEmailMixin: def validate_invitation(self, email): - if Invitation.objects.all_valid().filter( - email__iexact=email, accepted=False): + if Invitation.objects.all_valid().filter(email__iexact=email, accepted=False): raise AlreadyInvited - elif Invitation.objects.filter( - email__iexact=email, accepted=True): + elif Invitation.objects.filter(email__iexact=email, accepted=True): raise AlreadyAccepted elif get_user_model().objects.filter(email__iexact=email): raise UserRegisteredEmail @@ -28,19 +27,19 @@ def clean_email(self): email = get_invitations_adapter().clean_email(email) errors = { - "already_invited": _("This e-mail address has already been" - " invited."), - "already_accepted": _("This e-mail address has already" - " accepted an invite."), + "already_invited": _("This e-mail address has already been" " invited."), + "already_accepted": _( + "This e-mail address has already" " accepted an invite.", + ), "email_in_use": _("An active user is using this e-mail address"), } try: self.validate_invitation(email) - except(AlreadyInvited): + except (AlreadyInvited): raise forms.ValidationError(errors["already_invited"]) - except(AlreadyAccepted): + except (AlreadyAccepted): raise forms.ValidationError(errors["already_accepted"]) - except(UserRegisteredEmail): + except (UserRegisteredEmail): raise forms.ValidationError(errors["email_in_use"]) return email @@ -50,8 +49,9 @@ class InviteForm(forms.Form, CleanEmailMixin): email = forms.EmailField( label=_("E-mail"), required=True, - widget=forms.TextInput( - attrs={"type": "email", "size": "30"}), initial="") + widget=forms.TextInput(attrs={"type": "email", "size": "30"}), + initial="", + ) def save(self, email): return Invitation.create(email=email) @@ -61,17 +61,18 @@ class InvitationAdminAddForm(forms.ModelForm, CleanEmailMixin): email = forms.EmailField( label=_("E-mail"), required=True, - widget=forms.TextInput(attrs={"type": "email", "size": "30"})) + widget=forms.TextInput(attrs={"type": "email", "size": "30"}), + ) def save(self, *args, **kwargs): - cleaned_data = super(InvitationAdminAddForm, self).clean() + cleaned_data = super().clean() email = cleaned_data.get("email") - params = {'email': email} + params = {"email": email} if cleaned_data.get("inviter"): - params['inviter'] = cleaned_data.get("inviter") + params["inviter"] = cleaned_data.get("inviter") instance = Invitation.create(**params) instance.send_invitation(self.request) - super(InvitationAdminAddForm, self).save(*args, **kwargs) + super().save(*args, **kwargs) return instance class Meta: @@ -80,7 +81,6 @@ class Meta: class InvitationAdminChangeForm(forms.ModelForm): - class Meta: model = Invitation - fields = '__all__' + fields = "__all__" diff --git a/invitations/managers.py b/invitations/managers.py index 91334cd..7856558 100644 --- a/invitations/managers.py +++ b/invitations/managers.py @@ -8,7 +8,6 @@ class BaseInvitationManager(models.Manager): - def all_expired(self): return self.filter(self.expired_q()) @@ -16,8 +15,7 @@ def all_valid(self): return self.exclude(self.expired_q()) def expired_q(self): - sent_threshold = timezone.now() - timedelta( - days=app_settings.INVITATION_EXPIRY) + sent_threshold = timezone.now() - timedelta(days=app_settings.INVITATION_EXPIRY) q = Q(accepted=True) | Q(sent__lt=sent_threshold) return q diff --git a/invitations/migrations/0001_initial.py b/invitations/migrations/0001_initial.py index dcf311a..0d174df 100644 --- a/invitations/migrations/0001_initial.py +++ b/invitations/migrations/0001_initial.py @@ -7,22 +7,47 @@ class Migration(migrations.Migration): - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Invitation', + name="Invitation", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('email', models.EmailField(unique=True, max_length=75, verbose_name='e-mail address')), - ('accepted', models.BooleanField(default=False, verbose_name='accepted')), - ('created', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created')), - ('key', models.CharField(unique=True, max_length=64, verbose_name='key')), - ('sent', models.DateTimeField(null=True, verbose_name='sent')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "email", + models.EmailField( + unique=True, + max_length=75, + verbose_name="e-mail address", + ), + ), + ( + "accepted", + models.BooleanField(default=False, verbose_name="accepted"), + ), + ( + "created", + models.DateTimeField( + default=django.utils.timezone.now, + verbose_name="created", + ), + ), + ( + "key", + models.CharField(unique=True, max_length=64, verbose_name="key"), + ), + ("sent", models.DateTimeField(null=True, verbose_name="sent")), ], - options={ - }, + options={}, bases=(models.Model,), ), ] diff --git a/invitations/migrations/0002_auto_20151126_0426.py b/invitations/migrations/0002_auto_20151126_0426.py index 386a4af..3284304 100644 --- a/invitations/migrations/0002_auto_20151126_0426.py +++ b/invitations/migrations/0002_auto_20151126_0426.py @@ -5,26 +5,33 @@ from django.conf import settings from django.db import migrations, models -EMAIL_MAX_LENGTH = getattr(settings, 'INVITATIONS_EMAIL_MAX_LENGTH', 254) - +EMAIL_MAX_LENGTH = getattr(settings, "INVITATIONS_EMAIL_MAX_LENGTH", 254) class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('invitations', '0001_initial'), + ("invitations", "0001_initial"), ] operations = [ migrations.AddField( - model_name='invitation', - name='inviter', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True, on_delete=django.db.models.deletion.CASCADE), + model_name="invitation", + name="inviter", + field=models.ForeignKey( + to=settings.AUTH_USER_MODEL, + null=True, + on_delete=django.db.models.deletion.CASCADE, + ), ), migrations.AlterField( - model_name='invitation', - name='email', - field=models.EmailField(unique=True, max_length=EMAIL_MAX_LENGTH, verbose_name='e-mail address'), + model_name="invitation", + name="email", + field=models.EmailField( + unique=True, + max_length=EMAIL_MAX_LENGTH, + verbose_name="e-mail address", + ), ), ] diff --git a/invitations/migrations/0003_auto_20151126_1523.py b/invitations/migrations/0003_auto_20151126_1523.py index 8a7c976..7e80744 100644 --- a/invitations/migrations/0003_auto_20151126_1523.py +++ b/invitations/migrations/0003_auto_20151126_1523.py @@ -9,13 +9,18 @@ class Migration(migrations.Migration): dependencies = [ - ('invitations', '0002_auto_20151126_0426'), + ("invitations", "0002_auto_20151126_0426"), ] operations = [ migrations.AlterField( - model_name='invitation', - name='inviter', - field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=django.db.models.deletion.CASCADE), + model_name="invitation", + name="inviter", + field=models.ForeignKey( + blank=True, + to=settings.AUTH_USER_MODEL, + null=True, + on_delete=django.db.models.deletion.CASCADE, + ), ), ] diff --git a/invitations/models.py b/invitations/models.py index c163310..2e1b94a 100644 --- a/invitations/models.py +++ b/invitations/models.py @@ -2,6 +2,7 @@ from django.conf import settings from django.contrib.sites.models import Site + try: from django.urls import reverse except ImportError: @@ -18,47 +19,45 @@ class Invitation(AbstractBaseInvitation): - email = models.EmailField(unique=True, verbose_name=_('e-mail address'), - max_length=app_settings.EMAIL_MAX_LENGTH) - created = models.DateTimeField(verbose_name=_('created'), - default=timezone.now) + email = models.EmailField( + unique=True, + verbose_name=_("e-mail address"), + max_length=app_settings.EMAIL_MAX_LENGTH, + ) + created = models.DateTimeField(verbose_name=_("created"), default=timezone.now) @classmethod def create(cls, email, inviter=None, **kwargs): key = get_random_string(64).lower() instance = cls._default_manager.create( - email=email, - key=key, - inviter=inviter, - **kwargs) + email=email, key=key, inviter=inviter, **kwargs + ) return instance def key_expired(self): - expiration_date = ( - self.sent + datetime.timedelta( - days=app_settings.INVITATION_EXPIRY)) + expiration_date = self.sent + datetime.timedelta( + days=app_settings.INVITATION_EXPIRY, + ) return expiration_date <= timezone.now() def send_invitation(self, request, **kwargs): - current_site = kwargs.pop('site', Site.objects.get_current()) - invite_url = reverse('invitations:accept-invite', - args=[self.key]) + current_site = kwargs.pop("site", Site.objects.get_current()) + invite_url = reverse("invitations:accept-invite", args=[self.key]) invite_url = request.build_absolute_uri(invite_url) ctx = kwargs - ctx.update({ - 'invite_url': invite_url, - 'site_name': current_site.name, - 'email': self.email, - 'key': self.key, - 'inviter': self.inviter, - }) - - email_template = 'invitations/email/email_invite' - - get_invitations_adapter().send_mail( - email_template, - self.email, - ctx) + ctx.update( + { + "invite_url": invite_url, + "site_name": current_site.name, + "email": self.email, + "key": self.key, + "inviter": self.inviter, + }, + ) + + email_template = "invitations/email/email_invite" + + get_invitations_adapter().send_mail(email_template, self.email, ctx) self.sent = timezone.now() self.save() @@ -66,23 +65,24 @@ def send_invitation(self, request, **kwargs): sender=self.__class__, instance=self, invite_url_sent=invite_url, - inviter=self.inviter) + inviter=self.inviter, + ) def __str__(self): - return "Invite: {0}".format(self.email) + return f"Invite: {self.email}" # here for backwards compatibility, historic allauth adapter -if hasattr(settings, 'ACCOUNT_ADAPTER'): - if settings.ACCOUNT_ADAPTER == 'invitations.models.InvitationsAdapter': +if hasattr(settings, "ACCOUNT_ADAPTER"): + if settings.ACCOUNT_ADAPTER == "invitations.models.InvitationsAdapter": from allauth.account.adapter import DefaultAccountAdapter from allauth.account.signals import user_signed_up class InvitationsAdapter(DefaultAccountAdapter): - def is_open_for_signup(self, request): - if hasattr(request, 'session') and request.session.get( - 'account_verified_email'): + if hasattr(request, "session") and request.session.get( + "account_verified_email", + ): return True elif app_settings.INVITATION_ONLY is True: # Site is ONLY open for invites diff --git a/invitations/templates/invitations/forms/_invite.html b/invitations/templates/invitations/forms/_invite.html index 56f2bfe..8345754 100644 --- a/invitations/templates/invitations/forms/_invite.html +++ b/invitations/templates/invitations/forms/_invite.html @@ -14,5 +14,5 @@

{% trans "Invite" %}

{% endfor %} {{ success_message }} - - \ No newline at end of file + + diff --git a/invitations/templates/invitations/messages/invite_accepted.txt b/invitations/templates/invitations/messages/invite_accepted.txt index a55e7db..92f63be 100644 --- a/invitations/templates/invitations/messages/invite_accepted.txt +++ b/invitations/templates/invitations/messages/invite_accepted.txt @@ -1,4 +1,4 @@ {% load i18n %} {% autoescape off %} {% blocktrans %}Invitation to - {{ email }} - has been accepted{% endblocktrans %} -{% endautoescape %} \ No newline at end of file +{% endautoescape %} diff --git a/invitations/urls.py b/invitations/urls.py index 75ffa56..f6aa85c 100644 --- a/invitations/urls.py +++ b/invitations/urls.py @@ -2,14 +2,17 @@ from . import views -app_name = 'invitations' +app_name = "invitations" urlpatterns = [ - re_path(r'^send-invite/$', views.SendInvite.as_view(), - name='send-invite'), - - re_path(r'^send-json-invite/$', views.SendJSONInvite.as_view(), - name='send-json-invite'), - - re_path(r'^accept-invite/(?P\w+)/?$', views.AcceptInvite.as_view(), - name='accept-invite'), + re_path(r"^send-invite/$", views.SendInvite.as_view(), name="send-invite"), + re_path( + r"^send-json-invite/$", + views.SendJSONInvite.as_view(), + name="send-json-invite", + ), + re_path( + r"^accept-invite/(?P\w+)/?$", + views.AcceptInvite.as_view(), + name="accept-invite", + ), ] diff --git a/invitations/utils.py b/invitations/utils.py index 3fa4928..3f58d50 100644 --- a/invitations/utils.py +++ b/invitations/utils.py @@ -11,7 +11,7 @@ def import_attribute(path): assert isinstance(path, str) - pkg, attr = path.rsplit('.', 1) + pkg, attr = path.rsplit(".", 1) ret = getattr(importlib.import_module(pkg), attr) return ret @@ -36,11 +36,10 @@ def get_invitation_model(): try: return django_apps.get_model(path) except ValueError: - raise ImproperlyConfigured( - "path must be of the form 'app_label.model_name'" - ) + raise ImproperlyConfigured("path must be of the form 'app_label.model_name'") except LookupError: raise ImproperlyConfigured( "path refers to model '%s' that\ - has not been installed" % app_settings.INVITATION_MODEL + has not been installed" + % app_settings.INVITATION_MODEL, ) diff --git a/invitations/views.py b/invitations/views.py index ab0b505..0a8e746 100644 --- a/invitations/views.py +++ b/invitations/views.py @@ -4,31 +4,36 @@ from django.contrib.auth.decorators import login_required from django.core.exceptions import ValidationError from django.core.validators import validate_email -from django.http import Http404, HttpResponse +from django.http import Http404 +from django.http import HttpResponse from django.shortcuts import redirect from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ -from django.views.generic import FormView, View +from django.views.generic import FormView +from django.views.generic import View from django.views.generic.detail import SingleObjectMixin from .adapters import get_invitations_adapter from .app_settings import app_settings -from .exceptions import AlreadyAccepted, AlreadyInvited, UserRegisteredEmail +from .exceptions import AlreadyAccepted +from .exceptions import AlreadyInvited +from .exceptions import UserRegisteredEmail from .forms import CleanEmailMixin from .signals import invite_accepted -from .utils import get_invitation_model, get_invite_form +from .utils import get_invitation_model +from .utils import get_invite_form Invitation = get_invitation_model() InviteForm = get_invite_form() class SendInvite(FormView): - template_name = 'invitations/forms/_invite.html' + template_name = "invitations/forms/_invite.html" form_class = InviteForm @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): - return super(SendInvite, self).dispatch(request, *args, **kwargs) + return super().dispatch(request, *args, **kwargs) def form_valid(self, form): email = form.cleaned_data["email"] @@ -42,58 +47,56 @@ def form_valid(self, form): return self.form_invalid(form) return self.render_to_response( self.get_context_data( - success_message=_('%(email)s has been invited') % { - "email": email})) + success_message=_("%(email)s has been invited") % {"email": email}, + ), + ) def form_invalid(self, form): return self.render_to_response(self.get_context_data(form=form)) class SendJSONInvite(View): - http_method_names = [u'post'] + http_method_names = ["post"] @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): if app_settings.ALLOW_JSON_INVITES: - return super(SendJSONInvite, self).dispatch( - request, *args, **kwargs) + return super().dispatch(request, *args, **kwargs) else: raise Http404 def post(self, request, *args, **kwargs): status_code = 400 invitees = json.loads(request.body.decode()) - response = {'valid': [], 'invalid': []} + response = {"valid": [], "invalid": []} if isinstance(invitees, list): for invitee in invitees: try: validate_email(invitee) CleanEmailMixin().validate_invitation(invitee) invite = Invitation.create(invitee) - except(ValueError, KeyError): + except (ValueError, KeyError): pass - except(ValidationError): - response['invalid'].append({ - invitee: 'invalid email'}) - except(AlreadyAccepted): - response['invalid'].append({ - invitee: 'already accepted'}) - except(AlreadyInvited): - response['invalid'].append( - {invitee: 'pending invite'}) - except(UserRegisteredEmail): - response['invalid'].append( - {invitee: 'user registered email'}) + except (ValidationError): + response["invalid"].append({invitee: "invalid email"}) + except (AlreadyAccepted): + response["invalid"].append({invitee: "already accepted"}) + except (AlreadyInvited): + response["invalid"].append({invitee: "pending invite"}) + except (UserRegisteredEmail): + response["invalid"].append({invitee: "user registered email"}) else: invite.send_invitation(request) - response['valid'].append({invitee: 'invited'}) + response["valid"].append({invitee: "invited"}) - if response['valid']: + if response["valid"]: status_code = 201 return HttpResponse( json.dumps(response), - status=status_code, content_type='application/json') + status=status_code, + content_type="application/json", + ) class AcceptInvite(SingleObjectMixin, View): @@ -114,10 +117,10 @@ def post(self, *args, **kwargs): # Compatibility with older versions: return an HTTP 410 GONE if there # is an error. # Error conditions are: no key, expired key or # previously accepted key. - if app_settings.GONE_ON_ACCEPT_ERROR and \ - (not invitation or - (invitation and (invitation.accepted or - invitation.key_expired()))): + if app_settings.GONE_ON_ACCEPT_ERROR and ( + not invitation + or (invitation and (invitation.accepted or invitation.key_expired())) + ): return HttpResponse(status=410) # No invitation was found. @@ -126,7 +129,8 @@ def post(self, *args, **kwargs): get_invitations_adapter().add_message( self.request, messages.ERROR, - 'invitations/messages/invite_invalid.txt') + "invitations/messages/invite_invalid.txt", + ) return redirect(app_settings.LOGIN_REDIRECT) # The invitation was previously accepted, redirect to the login @@ -135,8 +139,9 @@ def post(self, *args, **kwargs): get_invitations_adapter().add_message( self.request, messages.ERROR, - 'invitations/messages/invite_already_accepted.txt', - {'email': invitation.email}) + "invitations/messages/invite_already_accepted.txt", + {"email": invitation.email}, + ) # Redirect to login since there's hopefully an account already. return redirect(app_settings.LOGIN_REDIRECT) @@ -145,20 +150,22 @@ def post(self, *args, **kwargs): get_invitations_adapter().add_message( self.request, messages.ERROR, - 'invitations/messages/invite_expired.txt', - {'email': invitation.email}) + "invitations/messages/invite_expired.txt", + {"email": invitation.email}, + ) # Redirect to sign-up since they might be able to register anyway. return redirect(self.get_signup_redirect()) # The invitation is valid. # Mark it as accepted now if ACCEPT_INVITE_AFTER_SIGNUP is False. if not app_settings.ACCEPT_INVITE_AFTER_SIGNUP: - accept_invitation(invitation=invitation, - request=self.request, - signal_sender=self.__class__) + accept_invitation( + invitation=invitation, + request=self.request, + signal_sender=self.__class__, + ) - get_invitations_adapter().stash_verified_email( - self.request, invitation.email) + get_invitations_adapter().stash_verified_email(self.request, invitation.email) return redirect(self.get_signup_redirect()) @@ -183,16 +190,19 @@ def accept_invitation(invitation, request, signal_sender): get_invitations_adapter().add_message( request, messages.SUCCESS, - 'invitations/messages/invite_accepted.txt', - {'email': invitation.email}) + "invitations/messages/invite_accepted.txt", + {"email": invitation.email}, + ) def accept_invite_after_signup(sender, request, user, **kwargs): invitation = Invitation.objects.filter(email__iexact=user.email).first() if invitation: - accept_invitation(invitation=invitation, - request=request, - signal_sender=Invitation) + accept_invitation( + invitation=invitation, + request=request, + signal_sender=Invitation, + ) if app_settings.ACCEPT_INVITE_AFTER_SIGNUP: diff --git a/manage.py b/manage.py index 3095287..e74a25b 100755 --- a/manage.py +++ b/manage.py @@ -1,8 +1,22 @@ #!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" import os -os.environ['DJANGO_SETTINGS_MODULE'] = 'test_settings' +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?", + ) from exc + execute_from_command_line(sys.argv) -from django.core import management if __name__ == "__main__": - management.execute_from_command_line() + main() diff --git a/setup.cfg b/setup.cfg index 224a779..b88034e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ [metadata] -description-file = README.md \ No newline at end of file +description-file = README.md diff --git a/setup.py b/setup.py index cfcfe0e..e3d5c1b 100644 --- a/setup.py +++ b/setup.py @@ -1,32 +1,32 @@ -from setuptools import setup, find_packages +from setuptools import find_packages +from setuptools import setup setup( - name='django-invitations', + name="django-invitations", packages=find_packages(), - package_data={'invitations': ['templates/*.*']}, + package_data={"invitations": ["templates/*.*"]}, include_package_data=True, zip_safe=False, - version='1.9.3', - description='Generic invitations app with support for django-allauth', - author='https://github.com/bee-keeper', - author_email='none@none.com', - url='https://github.com/bee-keeper/django-invitations.git', - download_url='https://github.com/' - 'bee-keeper/django-invitations/tarball/1.9.3', - keywords=['django', 'invitation', 'django-allauth', 'invite'], - license='GPL-3.0-only', + version="1.9.3", + description="Generic invitations app with support for django-allauth", + author="https://github.com/bee-keeper", + author_email="none@none.com", + url="https://github.com/bee-keeper/django-invitations.git", + download_url="https://github.com/" "bee-keeper/django-invitations/tarball/1.9.3", + keywords=["django", "invitation", "django-allauth", "invite"], + license="GPL-3.0-only", classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Environment :: Web Environment', - 'Topic :: Internet', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'License :: OSI Approved :: GPL-3.0-only', - 'Framework :: Django', + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + "Environment :: Web Environment", + "Topic :: Internet", + "Programming Language :: Python", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "License :: OSI Approved :: GPL-3.0-only", + "Framework :: Django", ], ) diff --git a/test_allauth_settings.py b/test_allauth_settings.py index 1403af0..aa4b767 100644 --- a/test_allauth_settings.py +++ b/test_allauth_settings.py @@ -1,22 +1,22 @@ -from test_settings import * +from test_settings import * # noqa: F401, F403 INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.admin', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'allauth', - 'allauth.account', - 'allauth.socialaccount', - 'invitations', - 'tests', + "django.contrib.auth", + "django.contrib.admin", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "allauth", + "allauth.account", + "allauth.socialaccount", + "invitations", + "tests", ) AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - 'allauth.account.auth_backends.AuthenticationBackend', + "django.contrib.auth.backends.ModelBackend", + "allauth.account.auth_backends.AuthenticationBackend", ) -ACCOUNT_ADAPTER = 'invitations.models.InvitationsAdapter' +ACCOUNT_ADAPTER = "invitations.models.InvitationsAdapter" diff --git a/test_settings.py b/test_settings.py index d64b685..ea06c9b 100644 --- a/test_settings.py +++ b/test_settings.py @@ -1,30 +1,29 @@ -# -*- coding: utf-8 -*- import django -SECRET_KEY = 'not_empty' +SECRET_KEY = "not_empty" SITE_ID = 1 DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:', - } + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", + }, } -ROOT_URLCONF = 'test_urls' +ROOT_URLCONF = "test_urls" if django.VERSION >= (1, 8): TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages' + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, @@ -41,27 +40,25 @@ ) MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' +EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.admin', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'tests', - 'invitations', -) -AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', + "django.contrib.auth", + "django.contrib.admin", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "tests", + "invitations", ) +AUTHENTICATION_BACKENDS = ("django.contrib.auth.backends.ModelBackend",) diff --git a/test_urls.py b/test_urls.py index f36fe53..57f1c2b 100644 --- a/test_urls.py +++ b/test_urls.py @@ -1,15 +1,14 @@ -from django.conf.urls import url, include -from django.contrib import admin from django.conf import settings +from django.contrib import admin +from django.urls import include +from django.urls import path admin.autodiscover() urlpatterns = [ - url(r'^invitations/', include('invitations.urls')), - url(r'^admin/', admin.site.urls), + path("invitations/", include("invitations.urls")), + path("admin/", admin.site.urls), ] -if 'allauth' in settings.INSTALLED_APPS: - urlpatterns.append( - url(r'^accounts/', include('allauth.urls')) - ) +if "allauth" in settings.INSTALLED_APPS: + urlpatterns.append(path("accounts/", include("allauth.urls"))) diff --git a/tests/allauth/test_allauth.py b/tests/allauth/test_allauth.py index 8bf7209..193ba74 100644 --- a/tests/allauth/test_allauth.py +++ b/tests/allauth/test_allauth.py @@ -19,63 +19,89 @@ class TestAllAuthIntegrationAcceptAfterSignup: client = Client() adapter = get_invitations_adapter() - @pytest.mark.parametrize('method', [ - ('get'), - ('post'), - ]) + @pytest.mark.parametrize( + "method", + [ + ("get"), + ("post"), + ], + ) def test_accept_invite_accepted_invitation_after_signup( - self, settings, method, sent_invitation_by_user_a, user_a): + self, + settings, + method, + sent_invitation_by_user_a, + user_a, + ): settings.INVITATIONS_ACCEPT_INVITE_AFTER_SIGNUP = True client_with_method = getattr(self.client, method) resp = client_with_method( - reverse('invitations:accept-invite', - kwargs={'key': sent_invitation_by_user_a.key} - ), follow=True) + reverse( + "invitations:accept-invite", + kwargs={"key": sent_invitation_by_user_a.key}, + ), + follow=True, + ) assert resp.status_code == 200 - invite = Invitation.objects.get(email='email@example.com') + invite = Invitation.objects.get(email="email@example.com") assert invite.inviter == user_a assert invite.accepted is False - assert resp.request['PATH_INFO'] == reverse('account_signup') - form = resp.context_data['form'] - assert 'email@example.com' == form.fields['email'].initial + assert resp.request["PATH_INFO"] == reverse("account_signup") + form = resp.context_data["form"] + assert "email@example.com" == form.fields["email"].initial resp = self.client.post( - reverse('account_signup'), - {'email': 'email@example.com', - 'username': 'username', - 'password1': 'password', - 'password2': 'password' - }) - invite = Invitation.objects.get(email='email@example.com') + reverse("account_signup"), + { + "email": "email@example.com", + "username": "username", + "password1": "password", + "password2": "password", + }, + ) + invite = Invitation.objects.get(email="email@example.com") assert invite.accepted is True - @pytest.mark.parametrize('method', [ - ('get'), - ('post'), - ]) + @pytest.mark.parametrize( + "method", + [ + ("get"), + ("post"), + ], + ) def test_invite_accepted_after_signup_with_altered_case_email( - self, settings, method, sent_invitation_by_user_a, user_a): + self, + settings, + method, + sent_invitation_by_user_a, + user_a, + ): settings.INVITATIONS_ACCEPT_INVITE_AFTER_SIGNUP = True client_with_method = getattr(self.client, method) resp = client_with_method( - reverse('invitations:accept-invite', - kwargs={'key': sent_invitation_by_user_a.key} - ), follow=True) + reverse( + "invitations:accept-invite", + kwargs={"key": sent_invitation_by_user_a.key}, + ), + follow=True, + ) - invite = Invitation.objects.get(email='email@example.com') + invite = Invitation.objects.get(email="email@example.com") assert invite.accepted is False - form = resp.context_data['form'] - assert 'email@example.com' == form.fields['email'].initial + form = resp.context_data["form"] + assert "email@example.com" == form.fields["email"].initial resp = self.client.post( - reverse('account_signup'), - {'email': 'EMAIL@EXAMPLE.COM', - 'username': 'username', - 'password1': 'password', - 'password2': 'password' - }) - invite = Invitation.objects.get(email='email@example.com') + reverse("account_signup"), + { + "email": "EMAIL@EXAMPLE.COM", + "username": "username", + "password1": "password", + "password2": "password", + }, + ) + invite = Invitation.objects.get(email="email@example.com") assert invite.accepted is True @@ -83,58 +109,67 @@ class TestAllAuthIntegration: client = Client() adapter = get_invitations_adapter() - @pytest.mark.parametrize('method', [ - ('get'), - ('post'), - ]) + @pytest.mark.parametrize( + "method", + [ + ("get"), + ("post"), + ], + ) def test_accept_invite_allauth( - self, method, settings, user_a, sent_invitation_by_user_a): + self, + method, + settings, + user_a, + sent_invitation_by_user_a, + ): client_with_method = getattr(self.client, method) resp = client_with_method( reverse( - 'invitations:accept-invite', - kwargs={'key': sent_invitation_by_user_a.key} - ), follow=True) - invite = Invitation.objects.get(email='email@example.com') + "invitations:accept-invite", + kwargs={"key": sent_invitation_by_user_a.key}, + ), + follow=True, + ) + invite = Invitation.objects.get(email="email@example.com") assert invite.accepted assert invite.inviter == user_a - assert resp.request['PATH_INFO'] == reverse('account_signup') + assert resp.request["PATH_INFO"] == reverse("account_signup") - form = resp.context_data['form'] - assert 'email@example.com' == form.fields['email'].initial - messages = resp.context['messages'] + form = resp.context_data["form"] + assert "email@example.com" == form.fields["email"].initial + messages = resp.context["messages"] message_text = [message.message for message in messages] - assert ( - 'Invitation to - email@example.com - has been accepted' in - message_text - ) + assert "Invitation to - email@example.com - has been accepted" in message_text resp = self.client.post( - reverse('account_signup'), - {'email': 'email@example.com', - 'username': 'username', - 'password1': 'password', - 'password2': 'password' - }) - - allauth_email_obj = EmailAddress.objects.get( - email='email@example.com') + reverse("account_signup"), + { + "email": "email@example.com", + "username": "username", + "password1": "password", + "password2": "password", + }, + ) + + allauth_email_obj = EmailAddress.objects.get(email="email@example.com") assert allauth_email_obj.verified is True def test_fetch_adapter(self): assert isinstance(self.adapter, InvitationsAdapter) def test_allauth_signup_open(self): - signup_request = RequestFactory().get(reverse( - 'account_signup', urlconf='allauth.account.urls')) + signup_request = RequestFactory().get( + reverse("account_signup", urlconf="allauth.account.urls"), + ) assert self.adapter.is_open_for_signup(signup_request) is True @pytest.mark.django_db def test_allauth_adapter_invitations_only(self, settings): settings.INVITATIONS_INVITATION_ONLY = True - signup_request = RequestFactory().get(reverse( - 'account_signup', urlconf='allauth.account.urls')) + signup_request = RequestFactory().get( + reverse("account_signup", urlconf="allauth.account.urls"), + ) assert self.adapter.is_open_for_signup(signup_request) is False - response = self.client.get( - reverse('account_signup')) - assert 'Sign Up Closed' in response.content.decode('utf8') + response = self.client.get(reverse("account_signup")) + assert "Sign Up Closed" in response.content.decode("utf8") diff --git a/tests/basic/tests.py b/tests/basic/tests.py index 4eb4d60..f288f5d 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -1,12 +1,13 @@ import datetime -import re import json -from mock import patch +import re +from unittest.mock import patch from django.test import Client from django.test.client import RequestFactory from django.test.utils import override_settings from django.utils import timezone + try: from django.urls import reverse except ImportError: @@ -17,56 +18,54 @@ import pytest from freezegun import freeze_time -from invitations.adapters import ( - BaseInvitationsAdapter, get_invitations_adapter) +from invitations.adapters import BaseInvitationsAdapter, get_invitations_adapter from invitations.app_settings import app_settings from invitations.views import AcceptInvite, SendJSONInvite from invitations.forms import InviteForm -from .. models import ExampleSwappableInvitation from invitations.utils import get_invitation_model Invitation = get_invitation_model() class TestInvitationModel: - - @freeze_time('2015-07-30 12:00:06') + @freeze_time("2015-07-30 12:00:06") def test_create_invitation(self, invitation_a): - assert invitation_a.email == 'email@example.com' + assert invitation_a.email == "email@example.com" assert invitation_a.key assert invitation_a.accepted is False assert invitation_a.created == datetime.datetime.now() def test_invitation_key_expiry(self, invitation_a): invitation_a.sent = timezone.now() - datetime.timedelta( - days=app_settings.INVITATION_EXPIRY, minutes=1) + days=app_settings.INVITATION_EXPIRY, + minutes=1, + ) assert invitation_a.key_expired() is True invitation_a.sent = timezone.now() - datetime.timedelta( - days=app_settings.INVITATION_EXPIRY, minutes=-1) + days=app_settings.INVITATION_EXPIRY, + minutes=-1, + ) assert invitation_a.key_expired() is False class TestInvitationsAdapter: - def test_fetch_adapter(self): adapter = get_invitations_adapter() assert isinstance(adapter, BaseInvitationsAdapter) def test_email_subject_prefix_settings_with_site(self): adapter = get_invitations_adapter() - with patch('invitations.adapters.Site') as MockSite: - MockSite.objects.get_current.return_value.name = 'Foo.com' - result = adapter.format_email_subject('Bar') - assert result == '[Foo.com] Bar' + with patch("invitations.adapters.Site") as MockSite: + MockSite.objects.get_current.return_value.name = "Foo.com" + result = adapter.format_email_subject("Bar") + assert result == "[Foo.com] Bar" - @override_settings( - INVITATIONS_EMAIL_SUBJECT_PREFIX='' - ) + @override_settings(INVITATIONS_EMAIL_SUBJECT_PREFIX="") def test_email_subject_prefix_settings_with_custom_override(self): adapter = get_invitations_adapter() - result = adapter.format_email_subject('Bar') - assert result == 'Bar' + result = adapter.format_email_subject("Bar") + assert result == "Bar" class TestInvitationsSendView: @@ -75,65 +74,76 @@ class TestInvitationsSendView: @pytest.mark.django_db def test_auth(self): response = self.client.post( - reverse('invitations:send-invite'), {'email': 'valid@example.com'}, - follow=True) + reverse("invitations:send-invite"), + {"email": "valid@example.com"}, + follow=True, + ) assert response.status_code == 404 - @pytest.mark.parametrize('email, error', [ - ('invalid@example', 'Enter a valid email address'), - ('invited@example.com', 'This e-mail address has already been'), - ('flobble@example.com', 'An active user is'), - ]) - def test_invalid_form_submissions( - self, user_a, user_b, invitation_b, email, error): - self.client.login(username='flibble', password='password') - resp = self.client.post( - reverse('invitations:send-invite'), {'email': email}) + @pytest.mark.parametrize( + "email, error", + [ + ("invalid@example", "Enter a valid email address"), + ("invited@example.com", "This e-mail address has already been"), + ("flobble@example.com", "An active user is"), + ], + ) + def test_invalid_form_submissions(self, user_a, user_b, invitation_b, email, error): + self.client.login(username="flibble", password="password") + resp = self.client.post(reverse("invitations:send-invite"), {"email": email}) - form = resp.context_data['form'] - assert error in form.errors['email'][0] + form = resp.context_data["form"] + assert error in form.errors["email"][0] - @freeze_time('2015-07-30 12:00:06') + @freeze_time("2015-07-30 12:00:06") def test_valid_form_submission(self, user_a): - self.client.login(username='flibble', password='password') + self.client.login(username="flibble", password="password") resp = self.client.post( - reverse('invitations:send-invite'), {'email': 'email@example.com'}) - invitation = Invitation.objects.get(email='email@example.com') + reverse("invitations:send-invite"), + {"email": "email@example.com"}, + ) + invitation = Invitation.objects.get(email="email@example.com") assert resp.status_code == 200 - assert 'success_message' in resp.context_data.keys() + assert "success_message" in resp.context_data.keys() assert invitation.sent == datetime.datetime.now() assert len(mail.outbox) == 1 - assert mail.outbox[0].to[0] == 'email@example.com' - assert 'Invitation to join example.com' in mail.outbox[0].subject - url = re.search( - "(?P/invitations/[^\s]+)", mail.outbox[0].body).group("url") + assert mail.outbox[0].to[0] == "email@example.com" + assert "Invitation to join example.com" in mail.outbox[0].subject + url = re.search(r"(?P/invitations/[^\s]+)", mail.outbox[0].body).group( + "url", + ) assert url == reverse( - 'invitations:accept-invite', kwargs={'key': invitation.key}) + "invitations:accept-invite", + kwargs={"key": invitation.key}, + ) - @override_settings( - INVITATION_MODEL='ExampleSwappableInvitation' - ) - @freeze_time('2015-07-30 12:00:06') + @override_settings(INVITATION_MODEL="ExampleSwappableInvitation") + @freeze_time("2015-07-30 12:00:06") def test_valid_form_submission_with_swapped_model(self, user_a): - self.client.login(username='flibble', password='password') + self.client.login(username="flibble", password="password") resp = self.client.post( - reverse('invitations:send-invite'), {'email': 'email@example.com'}) - invitation = Invitation.objects.get(email='email@example.com') + reverse("invitations:send-invite"), + {"email": "email@example.com"}, + ) + invitation = Invitation.objects.get(email="email@example.com") assert resp.status_code == 200 - assert 'success_message' in resp.context_data.keys() + assert "success_message" in resp.context_data.keys() assert invitation.sent == datetime.datetime.now() assert len(mail.outbox) == 1 - assert mail.outbox[0].to[0] == 'email@example.com' - assert 'Invitation to join example.com' in mail.outbox[0].subject - url = re.search( - "(?P/invitations/[^\s]+)", mail.outbox[0].body).group("url") + assert mail.outbox[0].to[0] == "email@example.com" + assert "Invitation to join example.com" in mail.outbox[0].subject + url = re.search(r"(?P/invitations/[^\s]+)", mail.outbox[0].body).group( + "url", + ) assert url == reverse( - 'invitations:accept-invite', kwargs={'key': invitation.key}) + "invitations:accept-invite", + kwargs={"key": invitation.key}, + ) @pytest.mark.django_db @@ -143,128 +153,187 @@ class TestInvitationsAcceptView: def test_accept_invite_get_is_404(self, settings, invitation_b): settings.INVITATIONS_CONFIRM_INVITE_ON_GET = False resp = self.client.get( - reverse( - 'invitations:accept-invite', - kwargs={'key': invitation_b.key}), - follow=True) + reverse("invitations:accept-invite", kwargs={"key": invitation_b.key}), + follow=True, + ) assert resp.status_code == 404 - @pytest.mark.parametrize('method', [ - ('get'), - ('post'), - ]) + @pytest.mark.parametrize( + "method", + [ + ("get"), + ("post"), + ], + ) def test_accept_invite_invalid_key(self, method): client_with_method = getattr(self.client, method) resp = client_with_method( - reverse('invitations:accept-invite', kwargs={'key': 'invalidKey'}), - follow=True) + reverse("invitations:accept-invite", kwargs={"key": "invalidKey"}), + follow=True, + ) assert resp.status_code == 410 - @pytest.mark.parametrize('method', [ - ('get'), - ('post'), - ]) + @pytest.mark.parametrize( + "method", + [ + ("get"), + ("post"), + ], + ) def test_accept_invite_invalid_key_error_disabled(self, settings, method): settings.INVITATIONS_GONE_ON_ACCEPT_ERROR = False - settings.INVITATIONS_LOGIN_REDIRECT = '/login-url/' + settings.INVITATIONS_LOGIN_REDIRECT = "/login-url/" client_with_method = getattr(self.client, method) resp = client_with_method( - reverse('invitations:accept-invite', kwargs={'key': 'invalidKey'}), - follow=True) - assert resp.request['PATH_INFO'] == '/login-url/' - - @pytest.mark.parametrize('method', [ - ('get'), - ('post'), - ]) + reverse("invitations:accept-invite", kwargs={"key": "invalidKey"}), + follow=True, + ) + assert resp.request["PATH_INFO"] == "/login-url/" + + @pytest.mark.parametrize( + "method", + [ + ("get"), + ("post"), + ], + ) def test_accept_invite_accepted_key(self, accepted_invitation, method): client_with_method = getattr(self.client, method) resp = client_with_method( - reverse('invitations:accept-invite', - kwargs={'key': accepted_invitation.key}), follow=True) + reverse( + "invitations:accept-invite", + kwargs={"key": accepted_invitation.key}, + ), + follow=True, + ) assert resp.status_code == 410 - @pytest.mark.parametrize('method', [ - ('get'), - ('post'), - ]) + @pytest.mark.parametrize( + "method", + [ + ("get"), + ("post"), + ], + ) def test_accept_invite_accepted_key_error_disabled( - self, settings, accepted_invitation, method): + self, + settings, + accepted_invitation, + method, + ): settings.INVITATIONS_GONE_ON_ACCEPT_ERROR = False - settings.INVITATIONS_LOGIN_REDIRECT = '/login-url/' + settings.INVITATIONS_LOGIN_REDIRECT = "/login-url/" client_with_method = getattr(self.client, method) resp = client_with_method( - reverse('invitations:accept-invite', - kwargs={'key': accepted_invitation.key}), follow=True) - assert resp.request['PATH_INFO'] == '/login-url/' - - @pytest.mark.parametrize('method', [ - ('get'), - ('post'), - ]) + reverse( + "invitations:accept-invite", + kwargs={"key": accepted_invitation.key}, + ), + follow=True, + ) + assert resp.request["PATH_INFO"] == "/login-url/" + + @pytest.mark.parametrize( + "method", + [ + ("get"), + ("post"), + ], + ) def test_accept_invite_expired_key( - self, settings, sent_invitation_by_user_a, method): + self, + settings, + sent_invitation_by_user_a, + method, + ): settings.INVITATIONS_INVITATION_EXPIRY = 0 client_with_method = getattr(self.client, method) resp = client_with_method( - reverse('invitations:accept-invite', - kwargs={'key': sent_invitation_by_user_a.key} - ), follow=True) + reverse( + "invitations:accept-invite", + kwargs={"key": sent_invitation_by_user_a.key}, + ), + follow=True, + ) assert resp.status_code == 410 - @pytest.mark.parametrize('method', [ - ('get'), - ('post'), - ]) + @pytest.mark.parametrize( + "method", + [ + ("get"), + ("post"), + ], + ) def test_accept_invite_expired_key_error_disabled( - self, sent_invitation_by_user_a, method, settings): + self, + sent_invitation_by_user_a, + method, + settings, + ): settings.INVITATIONS_INVITATION_EXPIRY = 0 settings.INVITATIONS_GONE_ON_ACCEPT_ERROR = False - settings.INVITATIONS_SIGNUP_REDIRECT = '/signup-url/' + settings.INVITATIONS_SIGNUP_REDIRECT = "/signup-url/" client_with_method = getattr(self.client, method) resp = client_with_method( - reverse('invitations:accept-invite', - kwargs={'key': sent_invitation_by_user_a.key} - ), follow=True) - assert resp.request['PATH_INFO'] == '/signup-url/' - - @pytest.mark.parametrize('method', [ - ('get'), - ('post'), - ]) - def test_accept_invite( - self, settings, sent_invitation_by_user_a, user_a, method): - settings.INVITATIONS_SIGNUP_REDIRECT = '/non-existent-url/' + reverse( + "invitations:accept-invite", + kwargs={"key": sent_invitation_by_user_a.key}, + ), + follow=True, + ) + assert resp.request["PATH_INFO"] == "/signup-url/" + + @pytest.mark.parametrize( + "method", + [ + ("get"), + ("post"), + ], + ) + def test_accept_invite(self, settings, sent_invitation_by_user_a, user_a, method): + settings.INVITATIONS_SIGNUP_REDIRECT = "/non-existent-url/" client_with_method = getattr(self.client, method) resp = client_with_method( - reverse('invitations:accept-invite', - kwargs={'key': sent_invitation_by_user_a.key} - ), follow=True) - invite = Invitation.objects.get(email='email@example.com') + reverse( + "invitations:accept-invite", + kwargs={"key": sent_invitation_by_user_a.key}, + ), + follow=True, + ) + invite = Invitation.objects.get(email="email@example.com") assert invite.accepted is True assert invite.inviter == user_a - assert resp.request['PATH_INFO'] == '/non-existent-url/' + assert resp.request["PATH_INFO"] == "/non-existent-url/" def test_signup_redirect(self, settings, sent_invitation_by_user_a): - settings.INVITATIONS_SIGNUP_REDIRECT = '/non-existent-url/' + settings.INVITATIONS_SIGNUP_REDIRECT = "/non-existent-url/" resp = self.client.post( - reverse('invitations:accept-invite', - kwargs={'key': sent_invitation_by_user_a.key} - ), follow=True) - invite = Invitation.objects.get(email='email@example.com') + reverse( + "invitations:accept-invite", + kwargs={"key": sent_invitation_by_user_a.key}, + ), + follow=True, + ) + invite = Invitation.objects.get(email="email@example.com") assert invite.accepted is True - assert resp.request['PATH_INFO'] == '/non-existent-url/' + assert resp.request["PATH_INFO"] == "/non-existent-url/" class TestInvitationSignals: client = Client() - @patch('invitations.signals.invite_url_sent.send') + @patch("invitations.signals.invite_url_sent.send") def test_invite_url_sent_triggered_correctly( - self, mock_signal, sent_invitation_by_user_a, user_a): - invite_url = reverse('invitations:accept-invite', - args=[sent_invitation_by_user_a.key]) - request = RequestFactory().get('/') + self, + mock_signal, + sent_invitation_by_user_a, + user_a, + ): + invite_url = reverse( + "invitations:accept-invite", + args=[sent_invitation_by_user_a.key], + ) + request = RequestFactory().get("/") invite_url = request.build_absolute_uri(invite_url) sent_invitation_by_user_a.send_invitation(request) @@ -279,38 +348,50 @@ def test_invite_url_sent_triggered_correctly( sender=Invitation, ) - @override_settings( - INVITATIONS_SIGNUP_REDIRECT='/non-existent-url/' - ) - @patch('invitations.signals.invite_accepted.send') + @override_settings(INVITATIONS_SIGNUP_REDIRECT="/non-existent-url/") + @patch("invitations.signals.invite_accepted.send") def test_invite_invite_accepted_triggered_correctly( - self, mock_signal, sent_invitation_by_user_a): - request = RequestFactory().get('/') + self, + mock_signal, + sent_invitation_by_user_a, + ): + request = RequestFactory().get("/") sent_invitation_by_user_a.send_invitation(request) self.client.post( - reverse('invitations:accept-invite', - kwargs={'key': sent_invitation_by_user_a.key} - ), follow=True) + reverse( + "invitations:accept-invite", + kwargs={"key": sent_invitation_by_user_a.key}, + ), + follow=True, + ) assert mock_signal.called assert mock_signal.call_count == 1 - assert mock_signal.call_args[1]['email'] == 'email@example.com' - assert mock_signal.call_args[1]['sender'] == AcceptInvite + assert mock_signal.call_args[1]["email"] == "email@example.com" + assert mock_signal.call_args[1]["sender"] == AcceptInvite class TestInvitationsForm: - - @pytest.mark.parametrize('email, form_validity, errors', [ - ('bogger@example.com', True, None), - ('accepted@example.com', False, 'has already accepted an invite'), - ('pending@example.com', False, 'has already been invited'), - ('flobble@example.com', False, 'active user is using this'), - ]) + @pytest.mark.parametrize( + "email, form_validity, errors", + [ + ("bogger@example.com", True, None), + ("accepted@example.com", False, "has already accepted an invite"), + ("pending@example.com", False, "has already been invited"), + ("flobble@example.com", False, "active user is using this"), + ], + ) def test_form( - self, email, form_validity, errors, - accepted_invitation, pending_invitation, user_b): - form = InviteForm(data={'email': email}) + self, + email, + form_validity, + errors, + accepted_invitation, + pending_invitation, + user_b, + ): + form = InviteForm(data={"email": email}) if errors: assert errors in str(form.errors) else: @@ -320,86 +401,106 @@ def test_form( @pytest.mark.django_db class TestInvitationsManager: - def test_managers( - self, sent_invitation_by_user_a, accepted_invitation, - expired_invitation, invitation_b): - valid = Invitation.objects.all_valid().values_list( - 'email', flat=True) - expired = Invitation.objects.all_expired().values_list( - 'email', flat=True) - expected_valid = ['email@example.com', 'invited@example.com'] - expected_expired = ['accepted@example.com', 'expired@example.com'] + self, + sent_invitation_by_user_a, + accepted_invitation, + expired_invitation, + invitation_b, + ): + valid = Invitation.objects.all_valid().values_list("email", flat=True) + expired = Invitation.objects.all_expired().values_list("email", flat=True) + expected_valid = ["email@example.com", "invited@example.com"] + expected_expired = ["accepted@example.com", "expired@example.com"] assert sorted(valid) == sorted(expected_valid) assert sorted(expired) == sorted(expected_expired) def test_delete_all(self): - valid = Invitation.objects.all_valid().values_list( - 'email', flat=True) + valid = Invitation.objects.all_valid().values_list("email", flat=True) Invitation.objects.delete_expired_confirmations() - remaining_invites = Invitation.objects.all().values_list( - 'email', flat=True) + remaining_invites = Invitation.objects.all().values_list("email", flat=True) assert sorted(valid) == sorted(remaining_invites) class TestInvitationsJSON: client = Client() - @pytest.mark.parametrize('data, expected, status_code', [ - (['accepted@example.com'], - {u'valid': [], - u'invalid': [{u'accepted@example.com': u'already accepted'}]}, - 400), - (['xample.com'], - {u'valid': [], u'invalid': [{u'xample.com': u'invalid email'}]}, - 400), - ('xample.com', - {u'valid': [], u'invalid': []}, - 400), - (['pending@example.com'], - {u'valid': [], - u'invalid': [{u'pending@example.com': u'pending invite'}]}, - 400), - (['flobble@example.com'], - {u'valid': [], - u'invalid': [{u'flobble@example.com': u'user registered email'}]}, - 400), - (['example@example.com'], - {u'valid': [{u'example@example.com': u'invited'}], - u'invalid': []}, - 201), - ]) + @pytest.mark.parametrize( + "data, expected, status_code", + [ + ( + ["accepted@example.com"], + { + "valid": [], + "invalid": [{"accepted@example.com": "already accepted"}], + }, + 400, + ), + ( + ["xample.com"], + {"valid": [], "invalid": [{"xample.com": "invalid email"}]}, + 400, + ), + ("xample.com", {"valid": [], "invalid": []}, 400), + ( + ["pending@example.com"], + {"valid": [], "invalid": [{"pending@example.com": "pending invite"}]}, + 400, + ), + ( + ["flobble@example.com"], + { + "valid": [], + "invalid": [{"flobble@example.com": "user registered email"}], + }, + 400, + ), + ( + ["example@example.com"], + {"valid": [{"example@example.com": "invited"}], "invalid": []}, + 201, + ), + ], + ) def test_post( - self, settings, data, expected, status_code, - user_a, accepted_invitation, - pending_invitation, user_b): + self, + settings, + data, + expected, + status_code, + user_a, + accepted_invitation, + pending_invitation, + user_b, + ): settings.INVITATIONS_ALLOW_JSON_INVITES = True - self.client.login(username='flibble', password='password') + self.client.login(username="flibble", password="password") response = self.client.post( - reverse('invitations:send-json-invite'), + reverse("invitations:send-json-invite"), data=json.dumps(data), - content_type='application/json') + content_type="application/json", + ) assert response.status_code == status_code assert json.loads(response.content.decode()) == expected def test_json_setting(self, user_a): - self.client.login(username='flibble', password='password') + self.client.login(username="flibble", password="password") response = self.client.post( - reverse('invitations:send-json-invite'), - data=json.dumps(['example@example.com']), - content_type='application/json') + reverse("invitations:send-json-invite"), + data=json.dumps(["example@example.com"]), + content_type="application/json", + ) assert response.status_code == 404 - @override_settings( - INVITATIONS_ALLOW_JSON_INVITES=True - ) + @override_settings(INVITATIONS_ALLOW_JSON_INVITES=True) def test_anonymous_get(self): request = RequestFactory().get( - reverse('invitations:send-json-invite'), - content_type='application/json') + reverse("invitations:send-json-invite"), + content_type="application/json", + ) request.user = AnonymousUser() response = SendJSONInvite.as_view()(request) @@ -408,8 +509,9 @@ def test_anonymous_get(self): def test_authenticated_get(self, settings, user_a): settings.INVITATIONS_ALLOW_JSON_INVITES = True request = RequestFactory().get( - reverse('invitations:send-json-invite'), - content_type='application/json') + reverse("invitations:send-json-invite"), + content_type="application/json", + ) request.user = user_a response = SendJSONInvite.as_view()(request) @@ -420,26 +522,26 @@ class TestInvitationsAdmin: client = Client() def test_admin_form_add(self, super_user): - self.client.login(username='flibble', password='password') + self.client.login(username="flibble", password="password") response = self.client.post( - reverse('admin:invitations_invitation_add'), - {'email': 'valid@example.com', 'inviter': super_user.id}, - follow=True) - invite = Invitation.objects.get(email='valid@example.com') + reverse("admin:invitations_invitation_add"), + {"email": "valid@example.com", "inviter": super_user.id}, + follow=True, + ) + invite = Invitation.objects.get(email="valid@example.com") assert response.status_code == 200 assert invite.sent assert invite.inviter == super_user def test_admin_form_change(self, super_user, invitation_b): - self.client.login(username='flibble', password='password') + self.client.login(username="flibble", password="password") response = self.client.get( - reverse('admin:invitations_invitation_change', - args=(invitation_b.id,)), - follow=True) + reverse("admin:invitations_invitation_change", args=(invitation_b.id,)), + follow=True, + ) assert response.status_code == 200 - fields = list(response.context_data['adminform'].form.fields.keys()) - expected_fields = ['accepted', - 'key', 'sent', 'inviter', 'email', 'created'] + fields = list(response.context_data["adminform"].form.fields.keys()) + expected_fields = ["accepted", "key", "sent", "inviter", "email", "created"] assert fields == expected_fields diff --git a/tests/conftest.py b/tests/conftest.py index 373d52e..35ba370 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,61 +1,56 @@ import datetime import pytest - from django.contrib.auth import get_user_model from django.utils import timezone - from freezegun import freeze_time -from invitations.utils import get_invitation_model from invitations.app_settings import app_settings +from invitations.utils import get_invitation_model Invitation = get_invitation_model() @pytest.fixture def invitation_a(db): - freezer = freeze_time('2015-07-30 12:00:06') + freezer = freeze_time("2015-07-30 12:00:06") freezer.start() - inivitation = Invitation.create('email@example.com') + inivitation = Invitation.create("email@example.com") freezer.stop() return inivitation @pytest.fixture def invitation_b(db): - return Invitation.create('invited@example.com') + return Invitation.create("invited@example.com") @pytest.fixture def user_a(db): - return get_user_model().objects.create_user( - username='flibble', - password='password' - ) + return get_user_model().objects.create_user(username="flibble", password="password") @pytest.fixture def user_b(db): return get_user_model().objects.create_user( - username='flobble', - password='password', - email='flobble@example.com' + username="flobble", + password="password", + email="flobble@example.com", ) @pytest.fixture def super_user(db): return get_user_model().objects.create_superuser( - username='flibble', - password='password', - email='mrflibble@example.com' + username="flibble", + password="password", + email="mrflibble@example.com", ) @pytest.fixture def sent_invitation_by_user_a(db, user_a): - invite = Invitation.create('email@example.com', inviter=user_a) + invite = Invitation.create("email@example.com", inviter=user_a) invite.sent = timezone.now() invite.save() return invite @@ -63,7 +58,7 @@ def sent_invitation_by_user_a(db, user_a): @pytest.fixture def accepted_invitation(db, user_a): - invite = Invitation.create('accepted@example.com', inviter=user_a) + invite = Invitation.create("accepted@example.com", inviter=user_a) invite.sent = timezone.now() invite.accepted = True invite.save() @@ -72,17 +67,19 @@ def accepted_invitation(db, user_a): @pytest.fixture def pending_invitation(db, user_a): - invite = Invitation.create('pending@example.com', inviter=user_a) + invite = Invitation.create("pending@example.com", inviter=user_a) invite.sent = timezone.now() - datetime.timedelta( - days=app_settings.INVITATION_EXPIRY - 1) + days=app_settings.INVITATION_EXPIRY - 1, + ) invite.save() return invite @pytest.fixture def expired_invitation(db, user_a): - invite = Invitation.create('expired@example.com') + invite = Invitation.create("expired@example.com") invite.sent = timezone.now() - datetime.timedelta( - days=app_settings.INVITATION_EXPIRY + 1) + days=app_settings.INVITATION_EXPIRY + 1, + ) invite.save() return invite diff --git a/tests/migrations/0001_initial.py b/tests/migrations/0001_initial.py index 184b69f..d7a6feb 100644 --- a/tests/migrations/0001_initial.py +++ b/tests/migrations/0001_initial.py @@ -11,19 +11,29 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('invitations', '0003_auto_20151126_1523'), + ("invitations", "0003_auto_20151126_1523"), ] operations = [ migrations.CreateModel( - name='ExampleSwappableInvitation', + name="ExampleSwappableInvitation", fields=[ - ('invitation_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='invitations.Invitation')), - ('additonal_field', models.CharField(blank=True, max_length=255)), + ( + "invitation_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="invitations.Invitation", + ), + ), + ("additonal_field", models.CharField(blank=True, max_length=255)), ], options={ - 'abstract': False, + "abstract": False, }, - bases=('invitations.invitation',), + bases=("invitations.invitation",), ), ] From fdf105643244412bed518fb80a56e1d1adb04e2a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 30 Mar 2022 15:44:14 +0000 Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- invitations/adapters.py | 2 +- invitations/models.py | 1 - invitations/views.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/invitations/adapters.py b/invitations/adapters.py index ff1ba5d..c10960e 100644 --- a/invitations/adapters.py +++ b/invitations/adapters.py @@ -24,7 +24,7 @@ def format_email_subject(self, subject, context): prefix = app_settings.EMAIL_SUBJECT_PREFIX if prefix is None: site_name = context["site_name"] - prefix = "[{name}] ".format(name=site_name) + prefix = f"[{site_name}] " return prefix + force_text(subject) def render_mail(self, template_prefix, email, context): diff --git a/invitations/models.py b/invitations/models.py index 8d42d14..053b1c1 100644 --- a/invitations/models.py +++ b/invitations/models.py @@ -1,7 +1,6 @@ import datetime from django.conf import settings - from django.contrib.sites.shortcuts import get_current_site diff --git a/invitations/views.py b/invitations/views.py index fd703b9..e539fcb 100644 --- a/invitations/views.py +++ b/invitations/views.py @@ -184,7 +184,7 @@ def get_queryset(self): def accept_invitation(invitation, request, signal_sender): invitation.accepted = True invitation.save() - + invite_accepted.send( sender=signal_sender, email=invitation.email, From 15d1b3332127a7877622aac70bae4d42919eff5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Wed, 30 Mar 2022 18:02:23 +0200 Subject: [PATCH 3/6] Ignore migrations when doing pre-commit. --- .pre-commit-config.yaml | 1 + invitations/migrations/0001_initial.py | 47 +++++-------------- .../migrations/0002_auto_20151126_0426.py | 25 ++++------ .../migrations/0003_auto_20151126_1523.py | 13 ++--- 4 files changed, 25 insertions(+), 61 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 74a9208..afc21a0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ default_language_version: python: python3 +exclude: ^.*\b(migrations)\b.*$ repos: - repo: https://github.com/psf/black rev: 22.1.0 diff --git a/invitations/migrations/0001_initial.py b/invitations/migrations/0001_initial.py index 0d174df..dcf311a 100644 --- a/invitations/migrations/0001_initial.py +++ b/invitations/migrations/0001_initial.py @@ -7,47 +7,22 @@ class Migration(migrations.Migration): - dependencies = [] + dependencies = [ + ] operations = [ migrations.CreateModel( - name="Invitation", + name='Invitation', fields=[ - ( - "id", - models.AutoField( - verbose_name="ID", - serialize=False, - auto_created=True, - primary_key=True, - ), - ), - ( - "email", - models.EmailField( - unique=True, - max_length=75, - verbose_name="e-mail address", - ), - ), - ( - "accepted", - models.BooleanField(default=False, verbose_name="accepted"), - ), - ( - "created", - models.DateTimeField( - default=django.utils.timezone.now, - verbose_name="created", - ), - ), - ( - "key", - models.CharField(unique=True, max_length=64, verbose_name="key"), - ), - ("sent", models.DateTimeField(null=True, verbose_name="sent")), + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('email', models.EmailField(unique=True, max_length=75, verbose_name='e-mail address')), + ('accepted', models.BooleanField(default=False, verbose_name='accepted')), + ('created', models.DateTimeField(default=django.utils.timezone.now, verbose_name='created')), + ('key', models.CharField(unique=True, max_length=64, verbose_name='key')), + ('sent', models.DateTimeField(null=True, verbose_name='sent')), ], - options={}, + options={ + }, bases=(models.Model,), ), ] diff --git a/invitations/migrations/0002_auto_20151126_0426.py b/invitations/migrations/0002_auto_20151126_0426.py index 3284304..386a4af 100644 --- a/invitations/migrations/0002_auto_20151126_0426.py +++ b/invitations/migrations/0002_auto_20151126_0426.py @@ -5,33 +5,26 @@ from django.conf import settings from django.db import migrations, models -EMAIL_MAX_LENGTH = getattr(settings, "INVITATIONS_EMAIL_MAX_LENGTH", 254) +EMAIL_MAX_LENGTH = getattr(settings, 'INVITATIONS_EMAIL_MAX_LENGTH', 254) + class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("invitations", "0001_initial"), + ('invitations', '0001_initial'), ] operations = [ migrations.AddField( - model_name="invitation", - name="inviter", - field=models.ForeignKey( - to=settings.AUTH_USER_MODEL, - null=True, - on_delete=django.db.models.deletion.CASCADE, - ), + model_name='invitation', + name='inviter', + field=models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True, on_delete=django.db.models.deletion.CASCADE), ), migrations.AlterField( - model_name="invitation", - name="email", - field=models.EmailField( - unique=True, - max_length=EMAIL_MAX_LENGTH, - verbose_name="e-mail address", - ), + model_name='invitation', + name='email', + field=models.EmailField(unique=True, max_length=EMAIL_MAX_LENGTH, verbose_name='e-mail address'), ), ] diff --git a/invitations/migrations/0003_auto_20151126_1523.py b/invitations/migrations/0003_auto_20151126_1523.py index 7e80744..8a7c976 100644 --- a/invitations/migrations/0003_auto_20151126_1523.py +++ b/invitations/migrations/0003_auto_20151126_1523.py @@ -9,18 +9,13 @@ class Migration(migrations.Migration): dependencies = [ - ("invitations", "0002_auto_20151126_0426"), + ('invitations', '0002_auto_20151126_0426'), ] operations = [ migrations.AlterField( - model_name="invitation", - name="inviter", - field=models.ForeignKey( - blank=True, - to=settings.AUTH_USER_MODEL, - null=True, - on_delete=django.db.models.deletion.CASCADE, - ), + model_name='invitation', + name='inviter', + field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=django.db.models.deletion.CASCADE), ), ] From 19ef3bc737564f379c479b85d7b465d6692c2fb1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 30 Mar 2022 18:31:31 +0000 Subject: [PATCH 4/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- invitations/__init__.py | 1 - tests/basic/tests.py | 1 - 2 files changed, 2 deletions(-) diff --git a/invitations/__init__.py b/invitations/__init__.py index 8b13789..e69de29 100644 --- a/invitations/__init__.py +++ b/invitations/__init__.py @@ -1 +0,0 @@ - diff --git a/tests/basic/tests.py b/tests/basic/tests.py index e47638f..f288f5d 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -1,7 +1,6 @@ import datetime import json import re - from unittest.mock import patch from django.test import Client From 462d9c7c1c797539ea5881a6bc2afb1532e38fd2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 30 Mar 2022 18:31:31 +0000 Subject: [PATCH 5/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .pre-commit-config.yaml | 2 +- invitations/adapters.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index afc21a0..df5a074 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ default_language_version: exclude: ^.*\b(migrations)\b.*$ repos: - repo: https://github.com/psf/black - rev: 22.1.0 + rev: 22.3.0 hooks: - id: black - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/invitations/adapters.py b/invitations/adapters.py index c10960e..55dc50a 100644 --- a/invitations/adapters.py +++ b/invitations/adapters.py @@ -25,7 +25,7 @@ def format_email_subject(self, subject, context): if prefix is None: site_name = context["site_name"] prefix = f"[{site_name}] " - return prefix + force_text(subject) + return prefix + force_str(subject) def render_mail(self, template_prefix, email, context): """ From cff5b8be6a4d026542e45b796549f913b9a9f1b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Wed, 30 Mar 2022 20:46:49 +0200 Subject: [PATCH 6/6] Fix test after fixing site. --- tests/basic/tests.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/basic/tests.py b/tests/basic/tests.py index f288f5d..57b5cd5 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -56,15 +56,13 @@ def test_fetch_adapter(self): def test_email_subject_prefix_settings_with_site(self): adapter = get_invitations_adapter() - with patch("invitations.adapters.Site") as MockSite: - MockSite.objects.get_current.return_value.name = "Foo.com" - result = adapter.format_email_subject("Bar") - assert result == "[Foo.com] Bar" + result = adapter.format_email_subject("Bar", context={"site_name": "Foo.com"}) + assert result == "[Foo.com] Bar" @override_settings(INVITATIONS_EMAIL_SUBJECT_PREFIX="") def test_email_subject_prefix_settings_with_custom_override(self): adapter = get_invitations_adapter() - result = adapter.format_email_subject("Bar") + result = adapter.format_email_subject("Bar", context={"site_name": "Foo.com"}) assert result == "Bar"