Skip to content
Open
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
156 changes: 148 additions & 8 deletions contentcuration/contentcuration/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -824,7 +824,7 @@ def file_on_disk_name(instance, filename):


def generate_file_on_disk_name(checksum, filename):
""" Separated from file_on_disk_name to allow for simple way to check if has already exists """
"""Separated from file_on_disk_name to allow for simple way to check if has already exists"""
h = checksum
basename, ext = os.path.splitext(filename)
directory = os.path.join(settings.STORAGE_ROOT, h[0], h[1])
Expand All @@ -851,7 +851,7 @@ def object_storage_name(instance, filename):


def generate_object_storage_name(checksum, filename, default_ext=""):
""" Separated from file_on_disk_name to allow for simple way to check if has already exists """
"""Separated from file_on_disk_name to allow for simple way to check if has already exists"""
h = checksum
basename, actual_ext = os.path.splitext(filename)
ext = actual_ext if actual_ext else default_ext
Expand Down Expand Up @@ -1067,7 +1067,7 @@ class ChannelModelManager(models.Manager.from_queryset(ChannelModelQuerySet)):


class Channel(models.Model):
""" Permissions come from association with organizations """
"""Permissions come from association with organizations"""

id = UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=200, blank=True)
Expand Down Expand Up @@ -1231,11 +1231,55 @@ def filter_edit_queryset(cls, queryset, user):
user_id=user_id, channel_id=OuterRef("id")
)
)
queryset = queryset.annotate(edit=edit)
organization_edit = Exists(
Comment thread
nairaj2 marked this conversation as resolved.
OrganizationRole.objects.filter(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: filters status but not organization__deleted, so roles keep conferring channel edit after OrganizationViewSet.perform_destroy soft-deletes the org. Every other org queryset in this PR excludes deleted orgs (1929, 1947, 2018, 2038). Same gap in the organization_view subquery at 1272. Intended?

user_id=user_id,
organization_id=OuterRef("organization_id"),
status=ORGANIZATION_ROLE_STATUS_ACTIVE,
role__in=(
ORGANIZATION_ADMIN,
ORGANIZATION_EDITOR,
),
)
)
queryset = queryset.annotate(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: ChannelViewSet.get_queryset (viewsets/channel.py:499-517) re-annotates edit/view from the m2m tables alone, and organization_edit/organization_view are surfaced nowhere. An org admin listing channels therefore sees edit: false on channels the write endpoints will accept from them. Consider folding the org Exists into the edit annotation there so read and write agree.

edit=edit,
organization_edit=organization_edit,
)
if user.is_admin:
return queryset

return queryset.filter(edit=True)
return queryset.filter(Q(edit=True) | Q(organization_edit=True))
Comment thread
nairaj2 marked this conversation as resolved.

@classmethod
def filter_delete_queryset(cls, queryset, user):
Comment thread
nairaj2 marked this conversation as resolved.
user_id = not user.is_anonymous and user.id

if not user_id:
return queryset.none()

edit = Exists(
User.editable_channels.through.objects.filter(
user_id=user_id, channel_id=OuterRef("id")
)
)
organization_delete = Exists(
OrganizationRole.objects.filter(
user_id=user_id,
organization_id=OuterRef("organization_id"),
status=ORGANIZATION_ROLE_STATUS_ACTIVE,
role=ORGANIZATION_ADMIN,
)
)
queryset = queryset.annotate(
edit=edit,
organization_delete=organization_delete,
)

if user.is_admin:
return queryset

return queryset.filter(Q(edit=True) | Q(organization_delete=True))

@classmethod
def filter_view_queryset(cls, queryset, user):
Expand All @@ -1244,35 +1288,60 @@ def filter_view_queryset(cls, queryset, user):

if user_id:
filters = dict(user_id=user_id, channel_id=OuterRef("id"))

edit = Exists(
User.editable_channels.through.objects.filter(**filters).values(
"user_id"
)
)

view = Exists(
User.view_only_channels.through.objects.filter(**filters).values(
"user_id"
)
)

organization_view = Exists(
Comment thread
nairaj2 marked this conversation as resolved.
OrganizationRole.objects.filter(
user_id=user_id,
organization_id=OuterRef("organization_id"),
status=ORGANIZATION_ROLE_STATUS_ACTIVE,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: this filters OrganizationRole.status but not organization__deleted. Organization.filter_view_queryset (1941) and OrganizationRole.filter_view_queryset (2031) both exclude soft-deleted orgs, and perform_destroy only sets deleted=True — so after deleting an organization its channels stay visible to former members while the org itself disappears. Adding organization__deleted=False here keeps the three filters consistent.

role__in=(
ORGANIZATION_ADMIN,
ORGANIZATION_EDITOR,
ORGANIZATION_VIEWER,
),
)
)
else:
edit = boolean_val(False)
view = boolean_val(False)
organization_view = boolean_val(False)

queryset = queryset.annotate(
edit=edit,
view=view,
organization_view=organization_view,
)

if user_id and user.is_admin:
return queryset

permission_filter = Q()

if user_id:
pending_channels = Invitation.objects.filter(
email=user_email, revoked=False, declined=False, accepted=False
email=user_email,
revoked=False,
declined=False,
accepted=False,
).values_list("channel_id", flat=True)

permission_filter = (
Q(view=True) | Q(edit=True) | Q(deleted=False, id__in=pending_channels)
Q(view=True)
| Q(edit=True)
| Q(organization_view=True)
| Q(deleted=False, id__in=pending_channels)
)

return queryset.filter(permission_filter | Q(deleted=False, public=True))
Expand Down Expand Up @@ -1887,6 +1956,40 @@ class Organization(models.Model):

objects = CustomManager()

@classmethod
def filter_view_queryset(cls, queryset, user):
queryset = queryset.filter(deleted=False)

if user.is_anonymous:
return queryset.filter(public=True)

if user.is_admin:
return queryset

return queryset.filter(
Q(public=True)
| Q(
user_roles__user=user,
user_roles__status=ORGANIZATION_ROLE_STATUS_ACTIVE,
)
).distinct()

@classmethod
def filter_edit_queryset(cls, queryset, user):
queryset = queryset.filter(deleted=False)

if user.is_anonymous:
return queryset.none()

if user.is_admin:
return queryset

return queryset.filter(
user_roles__user=user,
user_roles__role=ORGANIZATION_ADMIN,
user_roles__status=ORGANIZATION_ROLE_STATUS_ACTIVE,
).distinct()

class Meta:
verbose_name = "Organization"
verbose_name_plural = "Organizations"
Expand Down Expand Up @@ -1942,6 +2045,43 @@ class OrganizationRole(models.Model):
)
updated_at = models.DateTimeField(auto_now=True, help_text="Last update timestamp")

@classmethod
def filter_view_queryset(cls, queryset, user):
queryset = queryset.filter(organization__deleted=False,).select_related(
"organization",
"user",
)

if user.is_anonymous:
return queryset.none()

if user.is_admin:
return queryset

return queryset.filter(
organization__user_roles__user=user,
organization__user_roles__status=ORGANIZATION_ROLE_STATUS_ACTIVE,
).distinct()

@classmethod
def filter_edit_queryset(cls, queryset, user):
queryset = queryset.filter(organization__deleted=False,).select_related(
"organization",
"user",
)

if user.is_anonymous:
return queryset.none()

if user.is_admin:
return queryset

return queryset.filter(
organization__user_roles__user=user,
organization__user_roles__role=ORGANIZATION_ADMIN,
organization__user_roles__status=ORGANIZATION_ROLE_STATUS_ACTIVE,
).distinct()

class Meta:
unique_together = ("user", "organization")
verbose_name = "Organization Role"
Expand Down Expand Up @@ -3729,7 +3869,7 @@ def save(self, *args, **kwargs):


class Invitation(models.Model):
""" Invitation to edit channel """
"""Invitation to edit channel"""

id = UUIDField(primary_key=True, default=uuid.uuid4)
accepted = models.BooleanField(default=False)
Expand Down
Loading