Skip to content

Commit

Permalink
feat: Shareable resource (#1104)
Browse files Browse the repository at this point in the history
  • Loading branch information
josebui authored Feb 19, 2024
1 parent a95a868 commit e0f0896
Show file tree
Hide file tree
Showing 20 changed files with 704 additions and 23 deletions.
6 changes: 6 additions & 0 deletions terraso_backend/apps/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
Landscape,
LandscapeDevelopmentStrategy,
LandscapeGroup,
SharedResource,
TaxonomyTerm,
User,
UserPreference,
Expand Down Expand Up @@ -69,3 +70,8 @@ class TaxonomyTermAdmin(admin.ModelAdmin):
@admin.register(LandscapeDevelopmentStrategy)
class LandscapeDevelopmentStrategyAdmin(admin.ModelAdmin):
list_display = ("id", "landscape")


@admin.register(SharedResource)
class SharedResourceAdmin(admin.ModelAdmin):
list_display = ("id", "share_uuid", "share_access")
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Copyright © 2024 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 5.0 on 2024-01-08 15:59

import uuid

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


def fill_shared_uuid(apps, schema_editor):
SharedResource = apps.get_model("core", "SharedResource")
shared_resources = SharedResource.objects.all()
for shared_resource in shared_resources:
shared_resource.share_uuid = uuid.uuid4()
SharedResource.objects.bulk_update(shared_resources, ["share_uuid"])


class Migration(migrations.Migration):
dependencies = [
("core", "0048_group_membership_list"),
]

operations = [
migrations.AddField(
model_name="sharedresource",
name="share_access",
field=models.CharField(
choices=[
("no", "No share access"),
("all", "Anyone with the link"),
("target_members", "Only tagert members"),
],
default="no",
max_length=32,
),
),
migrations.AddField(
model_name="sharedresource",
name="share_uuid",
field=models.UUIDField(default=uuid.uuid4),
preserve_default=False,
),
migrations.RunPython(fill_shared_uuid),
migrations.AddConstraint(
model_name="sharedresource",
constraint=models.UniqueConstraint(
condition=models.Q(("deleted_at__isnull", True)),
fields=("share_uuid",),
name="unique_share_uuid",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright © 2024 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/.

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("core", "0049_sharedresource_share_fields"),
]

operations = [
migrations.RunSQL(
sql="UPDATE core_sharedresource SET share_access ='target_members' WHERE share_access='no';"
),
migrations.AlterField(
model_name="sharedresource",
name="share_access",
field=models.CharField(
choices=[
("all", "Anyone with the link"),
("target_members", "Only target members"),
],
default="target_members",
max_length=32,
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright © 2024 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/.

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("core", "0050_sharedresource_remove_none"),
]

operations = [
migrations.RunSQL(
sql="UPDATE core_sharedresource SET share_access ='members' WHERE share_access='target_members';"
),
]
55 changes: 55 additions & 0 deletions terraso_backend/apps/core/models/shared_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
#
# 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/.
import uuid

from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.translation import gettext_lazy as _

from apps.core.models import BaseModel

Expand All @@ -26,6 +30,15 @@ class SharedResource(BaseModel):
Target represents the resource that is receiving the shared resource (Example: Landscape).
"""

SHARE_ACCESS_ALL = "all"
SHARE_ACCESS_MEMBERS = "members"
DEFAULT_SHARE_ACCESS = SHARE_ACCESS_MEMBERS

SHARE_ACCESS_TYPES = (
(SHARE_ACCESS_ALL, _("Anyone with the link")),
(SHARE_ACCESS_MEMBERS, _("Only members")),
)

source = GenericForeignKey("source_content_type", "source_object_id")
source_content_type = models.ForeignKey(
ContentType, on_delete=models.CASCADE, related_name="source_content_type"
Expand All @@ -37,3 +50,45 @@ class SharedResource(BaseModel):
ContentType, on_delete=models.CASCADE, related_name="target_content_type"
)
target_object_id = models.UUIDField()
share_uuid = models.UUIDField(default=uuid.uuid4)
share_access = models.CharField(
max_length=32,
choices=SHARE_ACCESS_TYPES,
default=DEFAULT_SHARE_ACCESS,
)

class Meta:
constraints = (
models.UniqueConstraint(
fields=("share_uuid",),
condition=models.Q(deleted_at__isnull=True),
name="unique_share_uuid",
),
)

def get_download_url(self):
return f"{settings.API_ENDPOINT}/shared-data/download/{self.share_uuid}"

def get_share_url(self):
from apps.core.models import Group, Landscape

target = self.target
entity = (
"groups"
if isinstance(target, Group)
else "landscapes" if isinstance(target, Landscape) else None
)
if not entity:
return None
slug = target.slug
share_uuid = self.share_uuid
return f"{settings.WEB_CLIENT_URL}/{entity}/{slug}/download/{share_uuid}"

@classmethod
def get_share_access_from_text(cls, share_access):
if not share_access:
return cls.SHARE_ACCESS_MEMBERS
lowered = share_access.lower()
if lowered == cls.SHARE_ACCESS_ALL:
return cls.SHARE_ACCESS_ALL
return cls.SHARE_ACCESS_MEMBERS
17 changes: 17 additions & 0 deletions terraso_backend/apps/core/permission_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,22 @@ def allowed_to_delete_group_membership(user, obj):
return validate_delete_membership(user, group, membership)


@rules.predicate
def allowed_to_change_shared_resource(user, shared_resource):
from apps.shared_data.permission_rules import is_target_manager

target = shared_resource.target
source = shared_resource.source

if source.created_by == user:
return True

if is_target_manager(user, target):
return True

return False


rules.add_rule("allowed_group_managers_count", allowed_group_managers_count)
rules.add_rule("allowed_to_update_preferences", allowed_to_update_preferences)
rules.add_rule("allowed_to_change_landscape", allowed_to_change_landscape)
Expand All @@ -222,3 +238,4 @@ def allowed_to_delete_group_membership(user, obj):
rules.add_rule("allowed_landscape_managers_count", allowed_landscape_managers_count)
rules.add_rule("allowed_to_change_group_membership", allowed_to_change_group_membership)
rules.add_rule("allowed_to_delete_group_membership", allowed_to_delete_group_membership)
rules.add_rule("allowed_to_change_shared_resource", allowed_to_change_shared_resource)
4 changes: 4 additions & 0 deletions terraso_backend/apps/graphql/schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
LandscapeMembershipDeleteMutation,
LandscapeMembershipSaveMutation,
)
from .shared_resources import SharedResourceRelayNode, SharedResourceUpdateMutation
from .sites import (
SiteAddMutation,
SiteDeleteMutation,
Expand Down Expand Up @@ -139,6 +140,8 @@ class Query(graphene.ObjectType):
site = TerrasoRelayNode.Field(SiteNode)
sites = DjangoFilterConnectionField(SiteNode, required=True)
audit_logs = DjangoFilterConnectionField(AuditLogNode)
shared_resource = SharedResourceRelayNode.Field()
from .shared_resources import resolve_shared_resource


# All mutations should inherit from BaseWriteMutation or BaseDeleteMutation
Expand Down Expand Up @@ -202,6 +205,7 @@ class Mutations(graphene.ObjectType):
delete_landscape_membership = LandscapeMembershipDeleteMutation.Field()
save_group_membership = GroupMembershipSaveMutation.Field()
delete_group_membership = GroupMembershipDeleteMutation.Field()
update_shared_resource = SharedResourceUpdateMutation.Field()


schema = graphene.Schema(query=Query, mutation=Mutations)
39 changes: 33 additions & 6 deletions terraso_backend/apps/graphql/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ type Query {
"""Ordering"""
orderBy: String
): AuditLogNodeConnection
sharedResource(shareUuid: String!): SharedResourceNode
}

type GroupNode implements Node {
Expand Down Expand Up @@ -430,8 +431,27 @@ type SharedResourceNodeEdge {

type SharedResourceNode implements Node {
id: ID!
shareUuid: UUID!
shareAccess: CoreSharedResourceShareAccessChoices!
source: SourceNode
target: TargetNode
downloadUrl: String
shareUrl: String
}

"""
Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects
in fields, resolvers and input.
"""
scalar UUID

"""An enumeration."""
enum CoreSharedResourceShareAccessChoices {
"""Anyone with the link"""
ALL

"""Only members"""
MEMBERS
}

union SourceNode = VisualizationConfigNode | DataEntryNode
Expand Down Expand Up @@ -507,12 +527,6 @@ type VisualizationConfigNodeEdge {
cursor: String!
}

"""
Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects
in fields, resolvers and input.
"""
scalar UUID

union OwnerNode = GroupNode | LandscapeNode

union TargetNode = GroupNode | LandscapeNode
Expand Down Expand Up @@ -1676,6 +1690,7 @@ type Mutations {
deleteLandscapeMembership(input: LandscapeMembershipDeleteMutationInput!): LandscapeMembershipDeleteMutationPayload!
saveGroupMembership(input: GroupMembershipSaveMutationInput!): GroupMembershipSaveMutationPayload!
deleteGroupMembership(input: GroupMembershipDeleteMutationInput!): GroupMembershipDeleteMutationPayload!
updateSharedResource(input: SharedResourceUpdateMutationInput!): SharedResourceUpdateMutationPayload!
}

type GroupAddMutationPayload {
Expand Down Expand Up @@ -2477,3 +2492,15 @@ input GroupMembershipDeleteMutationInput {
groupSlug: String!
clientMutationId: String
}

type SharedResourceUpdateMutationPayload {
errors: GenericScalar
sharedResource: SharedResourceNode
clientMutationId: String
}

input SharedResourceUpdateMutationInput {
id: ID!
shareAccess: String!
clientMutationId: String
}
Loading

0 comments on commit e0f0896

Please sign in to comment.