Skip to content

Commit

Permalink
Allow editing ws user invites (#419)
Browse files Browse the repository at this point in the history
Missing: Show invited date
  • Loading branch information
justuswilhelm authored Feb 22, 2024
2 parents 0474a9e + 4e8119f commit 7943d4a
Show file tree
Hide file tree
Showing 14 changed files with 290 additions and 181 deletions.
7 changes: 6 additions & 1 deletion backend/projectify/user/services/user_invite.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from typing import Optional

from django.db import transaction
from django.utils.timezone import now

from projectify.user.models import User, UserInvite
from projectify.user.selectors.user import user_find_by_email
Expand Down Expand Up @@ -52,14 +53,18 @@ def user_invite_redeem(*, user_invite: UserInvite, user: User) -> None:

# Add user to workspaces for any outstanding invites
qs = WorkspaceUserInvite.objects.filter(
# This plausibly keeps us from redeeming the same invite twice
user_invite__user=user,
redeemed=False,
)
for invite in qs:
workspace = invite.workspace
workspace_add_user(
workspace=workspace, user=user, role=WorkspaceUserRoles.OBSERVER
)
invite.redeem()
invite.redeemed = True
invite.redeemed_when = now()
invite.save()


@transaction.atomic
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Copyright (C) 2024 JWP Consulting GK
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Annotate ws user invite with redeemd_when dt field."""
# Generated by Django 4.2.7 on 2024-02-22 05:41

from django.db import migrations, models


class Migration(migrations.Migration):
"""Migration."""

dependencies = [
("workspace", "0056_alter_label_options"),
]

operations = [
migrations.AddField(
model_name="workspaceuserinvite",
name="redeemed_when",
field=models.DateTimeField(
blank=True,
default=None,
editable=False,
help_text="When has this invite been redeemed?",
null=True,
),
),
]
59 changes: 13 additions & 46 deletions backend/projectify/workspace/models/workspace_user_invite.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Copyright (C) 2023 JWP Consulting GK
# Copyright (C) 2023-2024 JWP Consulting GK
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
Expand All @@ -15,73 +15,40 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Contains workspace user invite qs / manager / model."""
from typing import (
TYPE_CHECKING,
ClassVar,
Self,
cast,
)

from django.db import (
models,
)
from django.db import models
from django.utils.translation import gettext_lazy as _

from projectify.lib.models import BaseModel
from projectify.user.models import UserInvite

from .types import (
Pks,
)

if TYPE_CHECKING:
from projectify.user.models import UserInvite # noqa: F401
from projectify.workspace.models import Workspace # noqa: F401


class WorkspaceUserInviteQuerySet(models.QuerySet["WorkspaceUserInvite"]):
"""QuerySet for WorkspaceUserInvite."""

def filter_by_workspace_pks(self, workspace_pks: Pks) -> Self:
"""Filter by workspace pks."""
return self.filter(workspace__pk__in=workspace_pks)

def filter_by_redeemed(self, redeemed: bool = True) -> Self:
"""Filter by redeemed workspace user invites."""
return self.filter(redeemed=redeemed)
from ..models import Workspace


class WorkspaceUserInvite(BaseModel):
"""UserInvites belonging to this workspace."""

user_invite = models.ForeignKey["UserInvite"](
user_invite = models.ForeignKey[UserInvite](
"user.UserInvite",
on_delete=models.CASCADE,
)
workspace = models.ForeignKey["Workspace"](
workspace = models.ForeignKey[Workspace](
"Workspace",
on_delete=models.CASCADE,
)
# TODO make this a datetimefield with default null
# TODO use redeemed_when only
redeemed = models.BooleanField(
default=False,
# TODO this should then say "When has this invite been redeemed?"
help_text=_("Has this invite been redeemed?"),
)

objects: ClassVar[WorkspaceUserInviteQuerySet] = cast( # type: ignore[assignment]
WorkspaceUserInviteQuerySet, WorkspaceUserInviteQuerySet.as_manager()
redeemed_when = models.DateTimeField(
blank=True,
null=True,
editable=False,
default=None,
help_text=_("When has this invite been redeemed?"),
)

def redeem(self) -> None:
"""
Redeem invite.
Save.
"""
assert not self.redeemed
self.redeemed = True
self.save()

class Meta:
"""Meta."""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from projectify.premail.email import EmailAddress
from projectify.user.models import User, UserInvite
from projectify.user.services.user_invite import user_invite_create
from projectify.workspace.services.signals import send_workspace_change_signal

from ..emails import WorkspaceUserInviteEmail
from ..exceptions import UserAlreadyAdded, UserAlreadyInvited
Expand Down Expand Up @@ -162,6 +163,7 @@ def workspace_user_invite_create(
)
email_to_send.send()

send_workspace_change_signal(workspace)
return workspace_user_invite


Expand All @@ -182,3 +184,4 @@ def workspace_user_invite_delete(
)
case WorkspaceUserInvite() as workspace_user_invite:
workspace_user_invite.delete()
send_workspace_change_signal(workspace)
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Copyright (C) 2023 JWP Consulting GK
# Copyright (C) 2023-2024 JWP Consulting GK
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
Expand All @@ -17,62 +17,15 @@
"""Test WorkspaceUserInvite model."""
import pytest

from ... import (
models,
from projectify.workspace.models.workspace_user_invite import (
WorkspaceUserInvite,
)


@pytest.mark.django_db
class TestWorkspaceUserInviteQuerySet:
"""Test WorkspaceUserInviteQuerySet."""

def test_filter_by_workspace_pks(
self,
workspace: models.Workspace,
workspace_user_invite: models.WorkspaceUserInvite,
) -> None:
"""Test filter_by_workspace_pks."""
qs = models.WorkspaceUserInvite.objects.filter_by_workspace_pks(
[workspace.pk],
)
assert list(qs) == [workspace_user_invite]

def test_filter_by_redeemed(
self,
workspace: models.Workspace,
workspace_user_invite: models.WorkspaceUserInvite,
) -> None:
"""Test filter_by_redeemed."""
qs = models.WorkspaceUserInvite.objects.filter_by_redeemed(False)
assert qs.count() == 1
workspace_user_invite.redeem()
assert qs.count() == 0
qs = models.WorkspaceUserInvite.objects.filter_by_redeemed(True)
assert qs.count() == 1


@pytest.mark.django_db
class TestWorkspaceUserInvite:
"""Test workspace user invite."""

def test_factory(
self, workspace_user_invite: models.WorkspaceUserInvite
) -> None:
def test_factory(self, workspace_user_invite: WorkspaceUserInvite) -> None:
"""Test factory."""
assert workspace_user_invite

def test_redeem(
self, workspace_user_invite: models.WorkspaceUserInvite
) -> None:
"""Test redeeming."""
workspace_user_invite.redeem()
workspace_user_invite.refresh_from_db()
assert workspace_user_invite.redeemed

def test_redeeming_twice(
self, workspace_user_invite: models.WorkspaceUserInvite
) -> None:
"""Test redeeming twice."""
workspace_user_invite.redeem()
with pytest.raises(AssertionError):
workspace_user_invite.redeem()
22 changes: 21 additions & 1 deletion backend/projectify/workspace/test/test_consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@
workspace_user_delete,
workspace_user_update,
)
from ..services.workspace_user_invite import workspace_user_invite_create
from ..services.workspace_user_invite import (
workspace_user_invite_create,
workspace_user_invite_delete,
)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -326,6 +329,23 @@ async def test_workspace_user_life_cycle(
who=user,
)
await expect_message(workspace_communicator, workspace)

# Now we invite someone without an account:
await database_sync_to_async(workspace_user_invite_create)(
workspace=workspace,
email_or_user="doesnotexist@example.com",
who=user,
)
await expect_message(workspace_communicator, workspace)

# And we remove their invitation
await database_sync_to_async(workspace_user_invite_delete)(
workspace=workspace,
email="doesnotexist@example.com",
who=user,
)
await expect_message(workspace_communicator, workspace)

# With only one remaining user, we call workspace_delete instead
await database_sync_to_async(workspace_delete)(
workspace=workspace,
Expand Down
Loading

0 comments on commit 7943d4a

Please sign in to comment.