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

feat: Shareable resource #1104

Merged
merged 20 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading