Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Landscape membership list #946

Closed
wants to merge 34 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b961c36
refactor: Added membership_list to landscape
josebui Oct 5, 2023
613e4d4
refactor: Create manager when landscape created
josebui Oct 6, 2023
4033bc7
fix: Fixed manager test
josebui Oct 6, 2023
1e81edb
refactor: User landscape manager
josebui Oct 6, 2023
412ec33
refactor: Removed default landscape group from tests and GQL
josebui Oct 6, 2023
70e4743
refactor: (WIP) landscape add membership tests
josebui Oct 6, 2023
824216c
fix: Update landsacpe membership test
josebui Oct 10, 2023
d45fb2c
refactor: Delete landscape membership
josebui Oct 10, 2023
17274d7
fix: delete and duplicated tests
josebui Oct 10, 2023
0bf3e15
test: Added non members landscape membership test
josebui Oct 10, 2023
6879646
fix: Anonymous user membership data
josebui Oct 10, 2023
582557d
fix: Optional membership_list for landscapes
josebui Oct 10, 2023
2e71137
refactor: Migration to copy landscape membership data to new model
josebui Oct 10, 2023
03f0931
fix: Removed landscape default group from GQL
josebui Oct 10, 2023
3e63f96
fix: Added enroll method to membership list node
josebui Oct 11, 2023
a98f13b
feat: Shared data files issue
josebui Oct 11, 2023
118a339
fix: Renamed migration
josebui Oct 30, 2023
f9a9609
fix: Return landscape on membership delete
josebui Oct 30, 2023
41de1b9
fix: Shared data landscape membership fixes
josebui Oct 30, 2023
ccf22ea
fix: Added landscape managers count
josebui Oct 30, 2023
e9a80b0
fix: User can join landscape
josebui Oct 31, 2023
a178128
fix: Added membership email filter to landscapes query
josebui Oct 31, 2023
1fe84e2
fix: Added membership list to landscape admin
josebui Oct 31, 2023
8deeda3
fix: Prefetch Landscape membership list data
josebui Nov 1, 2023
b9d550d
fix: Landscape membership count visible for anonymous users
josebui Nov 1, 2023
35b862b
fix: Removed more default group references
josebui Nov 1, 2023
dfbf5e4
fix: Added distinct for landscapes query
josebui Nov 1, 2023
ae54343
Update terraso_backend/apps/graphql/schema/landscapes.py
josebui Nov 7, 2023
63405d3
Update terraso_backend/apps/graphql/schema/landscapes.py
josebui Nov 7, 2023
f6fe2bd
Update terraso_backend/apps/graphql/schema/landscapes.py
josebui Nov 7, 2023
e4aecc0
Update terraso_backend/tests/graphql/mutations/test_landscape_members…
josebui Nov 7, 2023
64de363
Update terraso_backend/apps/collaboration/graphql/memberships.py
josebui Nov 7, 2023
52a15aa
fix: Fixed imports
josebui Nov 7, 2023
e939b42
fix: Linter fix
josebui Nov 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions terraso_backend/apps/collaboration/graphql/memberships.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class Meta:
fields = (
"memberships",
"membership_type",
"enroll_method",
)
interfaces = (relay.Node,)
connection_class = TerrasoConnection
Expand All @@ -61,9 +62,15 @@ def resolve_account_membership(self, info):
user = info.context.user
if user.is_anonymous:
return None
if hasattr(self, "account_memberships"):
if len(self.account_memberships):
return self.account_memberships[0]
return None
return self.memberships.filter(user=user).first()

def resolve_memberships_count(self, info):
if hasattr(self, "memberships_count"):
return self.memberships_count
user = info.context.user
if user.is_anonymous:
return 0
Expand Down
14 changes: 12 additions & 2 deletions terraso_backend/apps/collaboration/models/memberships.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,16 @@ class MembershipList(BaseModel):
default=DEFAULT_MEMERBSHIP_TYPE,
)

def default_validation_func(self):
return False
Comment on lines +57 to +58
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: what is this for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@paulschreiber The function using this default is the fucntion to save memberships, since the collaboration app doesn't have all the context to enforce the validation a function can be passed from the app using it to add the needed validation, if no function is passed this will be used by default


def save_membership(
self, user_email, user_role, membership_status, validation_func, membership_class=None
self,
user_email,
user_role,
membership_status,
validation_func=default_validation_func,
membership_class=None,
):
membership_class = membership_class or Membership
user = User.objects.filter(email__iexact=user_email).first()
Expand All @@ -71,6 +79,8 @@ def save_membership(
"user_role": user_role,
"membership_status": membership_status,
"current_membership": membership,
"user_exists": user_exists,
"user_email": user_email,
}
):
raise ValidationError("User cannot request membership")
Expand Down Expand Up @@ -119,7 +129,7 @@ def approved_members(self):
return User.objects.filter(id__in=approved_memberships_user_ids)

def has_role(self, user: User, role: str) -> bool:
return self.memberships.by_role(role).filter(id=user.id).exists()
return self.memberships.by_role(role).filter(user=user).exists()

def is_approved_member(self, user: User) -> bool:
return self.approved_members.filter(id=user.id).exists()
Expand Down
28 changes: 1 addition & 27 deletions terraso_backend/apps/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
# along with this program. If not, see https://www.gnu.org/licenses/.

from django.contrib import admin
from django.urls import reverse
from django.utils.safestring import mark_safe

from .models import (
Group,
Expand Down Expand Up @@ -52,38 +50,14 @@ def get_queryset(self, request):
@admin.register(Landscape)
class LandscapeAdmin(admin.ModelAdmin):
list_display = ("name", "slug", "location", "website", "created_at")

readonly_fields = ("default_group",)

def default_group(self, obj):
group = obj.get_default_group()
url = reverse("admin:core_landscapedefaultgroup_change", args=[group.pk])
return mark_safe(f'<a href="{url}">{group}</a>')

default_group.short_description = "Default Group"
raw_id_fields = ("membership_list",)


class LandscapeDefaultGroup(Group):
class Meta:
proxy = True


@admin.register(LandscapeDefaultGroup)
class LandscapeDefaultGroupAdmin(admin.ModelAdmin):
list_display = ("name", "slug", "website", "created_at")
inlines = [MembershipInline]

def get_queryset(self, request):
qs = super().get_queryset(request)
landscape_group_ids = [
values[0]
for values in LandscapeGroup.objects.filter(
is_default_landscape_group=True
).values_list("group__id")
]
return qs.filter(id__in=landscape_group_ids)


@admin.register(LandscapeGroup)
class LandscapeGroupAdmin(admin.ModelAdmin):
list_display = ("landscape", "group")
Expand Down
20 changes: 20 additions & 0 deletions terraso_backend/apps/core/landscape_collaboration_roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright © 2023 Technology Matters
#
# 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/.


ROLE_MANAGER = "manager"
ROLE_MEMBER = "member"

ALL_ROLES = [ROLE_MANAGER, ROLE_MEMBER]
10 changes: 3 additions & 7 deletions terraso_backend/apps/core/management/commands/loadsampledata.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# 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/.

from apps.collaboration.models import MembershipList
from apps.core.models import Group, Landscape, LandscapeGroup

from ._base_airtable import BaseAirtableCommand
Expand Down Expand Up @@ -47,13 +48,8 @@ def handle(self, *args, **kwargs):
name=landscape_name, defaults=model_data
)

# Creates Landscape default group
default_group, _ = Group.objects.update_or_create(name=f"{landscape_name} Group")
landscape_group, _ = LandscapeGroup.objects.update_or_create(
landscape=landscape,
group=default_group,
defaults={"is_default_landscape_group": True},
)
# Creates Landscape membership list
membership_list, _ = MembershipList.objects.update_or_create(landscape=landscape)

# Creates Partnership group
partnership_name = landscape_data.get("Landscape Partnership Name")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright © 2023 Technology Matters
#
# 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/.

# Generated by Django 4.2.6 on 2023-10-10 20:45

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models

import apps.core.models.commons


def copy_memberships(apps, schema_editor):
Landscape = apps.get_model("core", "Landscape")
MembershipList = apps.get_model("collaboration", "MembershipList")
Membership = apps.get_model("collaboration", "Membership")
landscapes = Landscape.objects.all()
for landscape in landscapes:
if not landscape.membership_list:
landscape.membership_list = MembershipList.objects.create(
enroll_method="both",
membership_type="open",
)
landscape.save()
default_group = landscape.associated_groups.filter(is_default_landscape_group=True).first()
if not default_group:
print(f"no default group for landscape {landscape.name}")
continue
current_memberships = default_group.group.memberships.filter(
deleted_at__isnull=True
).distinct("user")
for membership in current_memberships:
Membership.objects.create(
membership_list=landscape.membership_list,
user=membership.user,
user_role=membership.user_role,
membership_status=membership.membership_status,
)


class Migration(migrations.Migration):
dependencies = [
("collaboration", "0005_change_collaborator_to_editor"),
("core", "0046_shared_resource"),
]

operations = [
migrations.AddField(
model_name="landscape",
name="membership_list",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="landscape",
to="collaboration.membershiplist",
),
),
migrations.RunPython(copy_memberships),
]
56 changes: 23 additions & 33 deletions terraso_backend/apps/core/models/landscapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import structlog
from dirtyfields import DirtyFieldsMixin
from django.apps import apps
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models, transaction

Expand All @@ -23,6 +24,7 @@
calculate_geojson_centroid,
calculate_geojson_feature_area,
)
from apps.core.landscape_collaboration_roles import ROLE_MANAGER
from apps.core.models.taxonomy_terms import TaxonomyTerm

from .commons import BaseModel, SlugModel, validate_name
Expand Down Expand Up @@ -64,6 +66,12 @@ class Landscape(SlugModel, DirtyFieldsMixin):
related_name="created_landscapes",
)
groups = models.ManyToManyField(Group, through="LandscapeGroup")
membership_list = models.ForeignKey(
"collaboration.MembershipList",
on_delete=models.CASCADE,
related_name="landscape",
null=True,
)

area_types = models.JSONField(blank=True, null=True)
taxonomy_terms = models.ManyToManyField(TaxonomyTerm, blank=True)
Expand Down Expand Up @@ -101,6 +109,9 @@ class Meta(SlugModel.Meta):
_unique_fields = ["name"]
abstract = False

def full_clean(self, *args, **kwargs):
super().full_clean(*args, **kwargs, exclude=["membership_list"])

def save(self, *args, **kwargs):
dirty_fields = self.get_dirty_fields()
if self.area_polygon and "area_polygon" in dirty_fields:
Expand All @@ -110,52 +121,31 @@ def save(self, *args, **kwargs):
self.center_coordinates = calculate_geojson_centroid(self.area_polygon)

with transaction.atomic():
MembershipList = apps.get_model("collaboration", "MembershipList")
Membership = apps.get_model("collaboration", "Membership")
creating = not Landscape.objects.filter(pk=self.pk).exists()

super().save(*args, **kwargs)

if creating and self.created_by:
group = Group(
name="Group {}".format(self.slug),
description="",
created_by=self.created_by,
self.membership_list = MembershipList.objects.create(
enroll_method=MembershipList.ENROLL_METHOD_BOTH,
membership_type=MembershipList.MEMBERSHIP_TYPE_OPEN,
)
group.save()
landscape_group = LandscapeGroup(
group=group, landscape=self, is_default_landscape_group=True
self.membership_list.save_membership(
self.created_by.email, ROLE_MANAGER, Membership.APPROVED
)
landscape_group.save()

super().save(*args, **kwargs)

def delete(self, *args, **kwargs):
default_group = self.get_default_group()
membership_list = self.membership_list

with transaction.atomic():
ret = super().delete(*args, **kwargs)
# default group should be deleted as well
if default_group is not None:
default_group.delete()
if membership_list is not None:
membership_list.delete()

return ret

def get_default_group(self):
"""
A default Group in a Landscape is that Group where any
individual (associated or not with other Groups) is added when
associating directly with a Landscape.
"""
try:
# associated_groups is the related_name defined on
# LandscapeGroup relationship with Landscape. It returns a
# queryset of LandscapeGroup
landscape_group = self.associated_groups.get(is_default_landscape_group=True)
except LandscapeGroup.DoesNotExist:
logger.error(
"Landscape has no default group, but it must have", extra={"landscape_id": self.pk}
)
return None

return landscape_group.group

def __str__(self):
return self.name

Expand Down
7 changes: 4 additions & 3 deletions terraso_backend/apps/core/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
from django.utils.translation import gettext_lazy as _
from safedelete.models import SOFT_DELETE_CASCADE, SafeDeleteManager, SafeDeleteModel

from apps.core import landscape_collaboration_roles

NOTIFICATION_KEY_GROUP = "group_notifications"
NOTIFICATION_KEY_STORY_MAP = "story_map_notifications"
NOTIFICATION_KEY_LANGUAGE = "language"
Expand Down Expand Up @@ -99,10 +101,9 @@ def save(self, *args, **kwargs):

def is_landscape_manager(self, landscape_id):
return (
self.memberships.managers_only()
self.collaboration_memberships.by_role(landscape_collaboration_roles.ROLE_MANAGER)
.filter(
group__associated_landscapes__is_default_landscape_group=True,
group__associated_landscapes__landscape__pk=landscape_id,
membership_list__landscape__pk=landscape_id,
)
.exists()
)
Expand Down
Loading