2929from django .db import models
3030from django .db .models import Count
3131from django .db .models import Exists
32+ from django .db .models import ExpressionWrapper
3233from django .db .models import F
3334from django .db .models import Index
3435from django .db .models import JSONField
8081from contentcuration .constants import user_history
8182from contentcuration .constants .contentnode import kind_activity_map
8283from contentcuration .constants .organization_roles import ORGANIZATION_ADMIN
84+ from contentcuration .constants .organization_roles import ORGANIZATION_CHANNEL_EDIT_ROLES
8385from contentcuration .constants .organization_roles import ORGANIZATION_EDITOR
8486from contentcuration .constants .organization_roles import organization_role_choices
8587from contentcuration .constants .organization_roles import ORGANIZATION_ROLE_STATUS_ACTIVE
@@ -1006,29 +1008,63 @@ class PermissionCTE(With):
10061008 tree_id_fields = [
10071009 "channel__{}__tree_id" .format (tree_name ) for tree_name in CHANNEL_TREES
10081010 ]
1011+ channel_tree_id_fields = [
1012+ "{}__tree_id" .format (tree_name ) for tree_name in CHANNEL_TREES
1013+ ]
10091014
1010- def __init__ (self , model , user_id , ** kwargs ):
1011- queryset = model .objects .filter (user_id = user_id ).annotate (
1012- tree_id = Unnest (
1013- ArrayRemove (Array (* self .tree_id_fields ), None ),
1014- output_field = models .IntegerField (),
1015+ @classmethod
1016+ def _personal_channels (cls , model , user_id ):
1017+ return (
1018+ model .objects .filter (user_id = user_id )
1019+ .annotate (
1020+ tree_id = Unnest (
1021+ ArrayRemove (Array (* cls .tree_id_fields ), None ),
1022+ output_field = models .IntegerField (),
1023+ )
10151024 )
1025+ .values ("user_id" , "channel_id" , "tree_id" )
10161026 )
1017- super (PermissionCTE , self ).__init__ (
1018- queryset = queryset .values ("user_id" , "channel_id" , "tree_id" ), ** kwargs
1027+
1028+ @classmethod
1029+ def _organization_channels (cls , user_id , roles ):
1030+ # Organizations grant channel permissions to their members, on top of
1031+ # whatever personal editor/viewer access a user already has.
1032+ return (
1033+ Channel .objects .filter (
1034+ organization__user_roles__user_id = user_id ,
1035+ organization__user_roles__role__in = roles ,
1036+ organization__user_roles__status = ORGANIZATION_ROLE_STATUS_ACTIVE ,
1037+ )
1038+ .annotate (
1039+ user_id = Value (user_id , output_field = models .IntegerField ()),
1040+ channel_id = F ("id" ),
1041+ tree_id = Unnest (
1042+ ArrayRemove (Array (* cls .channel_tree_id_fields ), None ),
1043+ output_field = models .IntegerField (),
1044+ ),
1045+ )
1046+ .values ("user_id" , "channel_id" , "tree_id" )
10191047 )
10201048
10211049 @classmethod
10221050 def editable_channels (cls , user_id ):
1023- return PermissionCTE (
1024- User .editable_channels .through , user_id , name = "editable_channels_cte"
1051+ queryset = cls ._personal_channels (
1052+ User .editable_channels .through , user_id
1053+ ).union (
1054+ cls ._organization_channels (user_id , ORGANIZATION_CHANNEL_EDIT_ROLES ),
1055+ all = True ,
10251056 )
1057+ return PermissionCTE (queryset , name = "editable_channels_cte" )
10261058
10271059 @classmethod
10281060 def view_only_channels (cls , user_id ):
1029- return PermissionCTE (
1030- User .view_only_channels .through , user_id , name = "view_only_channels_cte"
1061+ queryset = cls ._personal_channels (
1062+ User .view_only_channels .through , user_id
1063+ ).union (
1064+ cls ._organization_channels (user_id , (ORGANIZATION_VIEWER ,)),
1065+ all = True ,
10311066 )
1067+ return PermissionCTE (queryset , name = "view_only_channels_cte" )
10321068
10331069 def exists (self , * filters ):
10341070 return Exists (self .queryset ().filter (* filters ).values ("user_id" ))
@@ -1218,6 +1254,17 @@ class Channel(models.Model):
12181254 def get_editable (cls , user , channel_id ):
12191255 return cls .filter_edit_queryset (cls .objects .all (), user ).get (id = channel_id )
12201256
1257+ @classmethod
1258+ def _organization_role_exists (cls , user_id , roles ):
1259+ return Exists (
1260+ OrganizationRole .objects .filter (
1261+ user_id = user_id ,
1262+ organization_id = OuterRef ("organization_id" ),
1263+ status = ORGANIZATION_ROLE_STATUS_ACTIVE ,
1264+ role__in = roles ,
1265+ )
1266+ )
1267+
12211268 @classmethod
12221269 def filter_edit_queryset (cls , queryset , user ):
12231270 user_id = not user .is_anonymous and user .id
@@ -1231,16 +1278,8 @@ def filter_edit_queryset(cls, queryset, user):
12311278 user_id = user_id , channel_id = OuterRef ("id" )
12321279 )
12331280 )
1234- organization_edit = Exists (
1235- OrganizationRole .objects .filter (
1236- user_id = user_id ,
1237- organization_id = OuterRef ("organization_id" ),
1238- status = ORGANIZATION_ROLE_STATUS_ACTIVE ,
1239- role__in = (
1240- ORGANIZATION_ADMIN ,
1241- ORGANIZATION_EDITOR ,
1242- ),
1243- )
1281+ organization_edit = cls ._organization_role_exists (
1282+ user_id , ORGANIZATION_CHANNEL_EDIT_ROLES
12441283 )
12451284 queryset = queryset .annotate (
12461285 edit = edit ,
@@ -1263,13 +1302,10 @@ def filter_delete_queryset(cls, queryset, user):
12631302 user_id = user_id , channel_id = OuterRef ("id" )
12641303 )
12651304 )
1266- organization_delete = Exists (
1267- OrganizationRole .objects .filter (
1268- user_id = user_id ,
1269- organization_id = OuterRef ("organization_id" ),
1270- status = ORGANIZATION_ROLE_STATUS_ACTIVE ,
1271- role = ORGANIZATION_ADMIN ,
1272- )
1305+ # Deleting a channel is restricted to organization admins, unlike
1306+ # editing it, which organization editors may also do.
1307+ organization_delete = cls ._organization_role_exists (
1308+ user_id , (ORGANIZATION_ADMIN ,)
12731309 )
12741310 queryset = queryset .annotate (
12751311 edit = edit ,
@@ -1289,40 +1325,26 @@ def filter_view_queryset(cls, queryset, user):
12891325 if user_id :
12901326 filters = dict (user_id = user_id , channel_id = OuterRef ("id" ))
12911327
1292- edit = Exists (
1293- User .editable_channels .through .objects .filter (** filters ).values (
1294- "user_id"
1295- )
1328+ # Fold organization grants into edit/view (viewer-only for view; an org editor/admin is admitted via the edit check instead).
1329+ edit = ExpressionWrapper (
1330+ Q (Exists (User .editable_channels .through .objects .filter (** filters )))
1331+ | Q (
1332+ cls ._organization_role_exists (
1333+ user_id , ORGANIZATION_CHANNEL_EDIT_ROLES
1334+ )
1335+ ),
1336+ output_field = models .BooleanField (),
12961337 )
1297-
1298- view = Exists (
1299- User .view_only_channels .through .objects .filter (** filters ).values (
1300- "user_id"
1301- )
1302- )
1303-
1304- organization_view = Exists (
1305- OrganizationRole .objects .filter (
1306- user_id = user_id ,
1307- organization_id = OuterRef ("organization_id" ),
1308- status = ORGANIZATION_ROLE_STATUS_ACTIVE ,
1309- role__in = (
1310- ORGANIZATION_ADMIN ,
1311- ORGANIZATION_EDITOR ,
1312- ORGANIZATION_VIEWER ,
1313- ),
1314- )
1338+ view = ExpressionWrapper (
1339+ Q (Exists (User .view_only_channels .through .objects .filter (** filters )))
1340+ | Q (cls ._organization_role_exists (user_id , (ORGANIZATION_VIEWER ,))),
1341+ output_field = models .BooleanField (),
13151342 )
13161343 else :
13171344 edit = boolean_val (False )
13181345 view = boolean_val (False )
1319- organization_view = boolean_val (False )
13201346
1321- queryset = queryset .annotate (
1322- edit = edit ,
1323- view = view ,
1324- organization_view = organization_view ,
1325- )
1347+ queryset = queryset .annotate (edit = edit , view = view )
13261348
13271349 if user_id and user .is_admin :
13281350 return queryset
@@ -1338,10 +1360,7 @@ def filter_view_queryset(cls, queryset, user):
13381360 ).values_list ("channel_id" , flat = True )
13391361
13401362 permission_filter = (
1341- Q (view = True )
1342- | Q (edit = True )
1343- | Q (organization_view = True )
1344- | Q (deleted = False , id__in = pending_channels )
1363+ Q (view = True ) | Q (edit = True ) | Q (deleted = False , id__in = pending_channels )
13451364 )
13461365
13471366 return queryset .filter (permission_filter | Q (deleted = False , public = True ))
0 commit comments