From 595a260f2302d253ef4ecf0a93e55c61578650e1 Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 6 Feb 2025 15:22:04 +0000 Subject: [PATCH] Fix issue #192: [Claude] User groups --- ..._group_usergroup_group_members_and_more.py | 77 +++++++++++++ photo/models.py | 32 ++++++ photo/tests/test_database/test_group.py | 102 ++++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 photo/migrations/0005_group_usergroup_group_members_and_more.py create mode 100644 photo/tests/test_database/test_group.py diff --git a/photo/migrations/0005_group_usergroup_group_members_and_more.py b/photo/migrations/0005_group_usergroup_group_members_and_more.py new file mode 100644 index 0000000..2714fce --- /dev/null +++ b/photo/migrations/0005_group_usergroup_group_members_and_more.py @@ -0,0 +1,77 @@ +# Generated by Django 4.2.8 on 2025-02-06 15:21 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + ("photo", "0004_alter_contest_prize"), + ] + + operations = [ + migrations.CreateModel( + name="Group", + fields=[ + ("is_deleted", models.BooleanField(default=False)), + ( + "id", + models.UUIDField( + default=uuid.uuid4, primary_key=True, serialize=False + ), + ), + ("name", models.TextField()), + ("description", models.TextField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="UserGroup", + fields=[ + ("is_deleted", models.BooleanField(default=False)), + ( + "id", + models.UUIDField( + default=uuid.uuid4, primary_key=True, serialize=False + ), + ), + ("role", models.TextField()), + ("joined_at", models.DateTimeField(auto_now_add=True)), + ( + "group", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="photo.group" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.AddField( + model_name="group", + name="members", + field=models.ManyToManyField( + related_name="user_groups", + through="photo.UserGroup", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddConstraint( + model_name="usergroup", + constraint=models.UniqueConstraint( + condition=models.Q(("is_deleted", False)), + fields=("user", "group"), + name="unique_user_group", + ), + ), + ] diff --git a/photo/models.py b/photo/models.py index 85d4a52..6e9b886 100644 --- a/photo/models.py +++ b/photo/models.py @@ -64,6 +64,38 @@ class Meta: abstract = True +class Group(SoftDeleteModel): + id = models.UUIDField(primary_key=True, default=uuid.uuid4) + name = models.TextField() + description = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + members = models.ManyToManyField( + "User", + through="UserGroup", + related_name="user_groups", + ) + + def __str__(self): + return self.name + + +class UserGroup(SoftDeleteModel): + id = models.UUIDField(primary_key=True, default=uuid.uuid4) + user = models.ForeignKey("User", on_delete=models.CASCADE) + group = models.ForeignKey("Group", on_delete=models.CASCADE) + role = models.TextField() + joined_at = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["user", "group"], + condition=models.Q(is_deleted=False), + name="unique_user_group", + ) + ] + + class User(AbstractUser, SoftDeleteModel): id = models.UUIDField(primary_key=True, default=uuid.uuid4) email = models.TextField(unique=True) diff --git a/photo/tests/test_database/test_group.py b/photo/tests/test_database/test_group.py new file mode 100644 index 0000000..e4f7953 --- /dev/null +++ b/photo/tests/test_database/test_group.py @@ -0,0 +1,102 @@ +import pytest +from django.db.utils import IntegrityError +from django.forms import ValidationError + +from photo.models import Group, User, UserGroup +from photo.tests.factories import UserFactory + + +@pytest.mark.django_db +class TestGroup: + def test_create_group(self): + group = Group.objects.create( + name="Test Group", + description="A test group", + ) + assert group.name == "Test Group" + assert group.description == "A test group" + assert group.created_at is not None + + def test_add_user_to_group(self): + user = UserFactory() + group = Group.objects.create(name="Test Group") + user_group = UserGroup.objects.create( + user=user, + group=group, + role="member", + ) + assert user_group.user == user + assert user_group.group == group + assert user_group.role == "member" + assert user_group.joined_at is not None + + def test_add_multiple_users_to_group(self): + users = [UserFactory() for _ in range(3)] + group = Group.objects.create(name="Test Group") + + for user in users: + UserGroup.objects.create( + user=user, + group=group, + role="member", + ) + + assert group.members.count() == 3 + for user in users: + assert user in group.members.all() + + def test_user_cannot_join_group_twice(self): + user = UserFactory() + group = Group.objects.create(name="Test Group") + UserGroup.objects.create( + user=user, + group=group, + role="member", + ) + + with pytest.raises(IntegrityError): + UserGroup.objects.create( + user=user, + group=group, + role="admin", + ) + + def test_user_can_be_in_multiple_groups(self): + user = UserFactory() + group1 = Group.objects.create(name="Group 1") + group2 = Group.objects.create(name="Group 2") + + UserGroup.objects.create( + user=user, + group=group1, + role="member", + ) + UserGroup.objects.create( + user=user, + group=group2, + role="admin", + ) + + assert user.user_groups.count() == 2 + assert group1 in user.user_groups.all() + assert group2 in user.user_groups.all() + + def test_soft_delete_group(self): + group = Group.objects.create(name="Test Group") + group.delete() + assert group.is_deleted + assert not Group.objects.filter(name="Test Group").exists() + assert Group.all_objects.filter(name="Test Group").exists() + + def test_soft_delete_user_group(self): + user = UserFactory() + group = Group.objects.create(name="Test Group") + user_group = UserGroup.objects.create( + user=user, + group=group, + role="member", + ) + user_group.delete() + assert user_group.is_deleted + assert not UserGroup.objects.filter(user=user, group=group).exists() + assert UserGroup.all_objects.filter(user=user, group=group).exists()