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
85+ from contentcuration .constants .organization_roles import ORGANIZATION_CHANNEL_VIEW_ROLES
8386from contentcuration .constants .organization_roles import ORGANIZATION_EDITOR
8487from contentcuration .constants .organization_roles import organization_role_choices
8588from contentcuration .constants .organization_roles import ORGANIZATION_ROLE_STATUS_ACTIVE
@@ -1006,29 +1009,63 @@ class PermissionCTE(With):
10061009 tree_id_fields = [
10071010 "channel__{}__tree_id" .format (tree_name ) for tree_name in CHANNEL_TREES
10081011 ]
1012+ channel_tree_id_fields = [
1013+ "{}__tree_id" .format (tree_name ) for tree_name in CHANNEL_TREES
1014+ ]
10091015
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 (),
1016+ @classmethod
1017+ def _personal_channels (cls , model , user_id ):
1018+ return (
1019+ model .objects .filter (user_id = user_id )
1020+ .annotate (
1021+ tree_id = Unnest (
1022+ ArrayRemove (Array (* cls .tree_id_fields ), None ),
1023+ output_field = models .IntegerField (),
1024+ )
10151025 )
1026+ .values ("user_id" , "channel_id" , "tree_id" )
10161027 )
1017- super (PermissionCTE , self ).__init__ (
1018- queryset = queryset .values ("user_id" , "channel_id" , "tree_id" ), ** kwargs
1028+
1029+ @classmethod
1030+ def _organization_channels (cls , user_id , roles ):
1031+ # Organizations grant channel permissions to their members, on top of
1032+ # whatever personal editor/viewer access a user already has.
1033+ return (
1034+ Channel .objects .filter (
1035+ organization__user_roles__user_id = user_id ,
1036+ organization__user_roles__role__in = roles ,
1037+ organization__user_roles__status = ORGANIZATION_ROLE_STATUS_ACTIVE ,
1038+ )
1039+ .annotate (
1040+ user_id = Value (user_id , output_field = models .IntegerField ()),
1041+ channel_id = F ("id" ),
1042+ tree_id = Unnest (
1043+ ArrayRemove (Array (* cls .channel_tree_id_fields ), None ),
1044+ output_field = models .IntegerField (),
1045+ ),
1046+ )
1047+ .values ("user_id" , "channel_id" , "tree_id" )
10191048 )
10201049
10211050 @classmethod
10221051 def editable_channels (cls , user_id ):
1023- return PermissionCTE (
1024- User .editable_channels .through , user_id , name = "editable_channels_cte"
1052+ queryset = cls ._personal_channels (
1053+ User .editable_channels .through , user_id
1054+ ).union (
1055+ cls ._organization_channels (user_id , ORGANIZATION_CHANNEL_EDIT_ROLES ),
1056+ all = True ,
10251057 )
1058+ return PermissionCTE (queryset , name = "editable_channels_cte" )
10261059
10271060 @classmethod
10281061 def view_only_channels (cls , user_id ):
1029- return PermissionCTE (
1030- User .view_only_channels .through , user_id , name = "view_only_channels_cte"
1062+ queryset = cls ._personal_channels (
1063+ User .view_only_channels .through , user_id
1064+ ).union (
1065+ cls ._organization_channels (user_id , ORGANIZATION_CHANNEL_VIEW_ROLES ),
1066+ all = True ,
10311067 )
1068+ return PermissionCTE (queryset , name = "view_only_channels_cte" )
10321069
10331070 def exists (self , * filters ):
10341071 return Exists (self .queryset ().filter (* filters ).values ("user_id" ))
@@ -1218,6 +1255,17 @@ class Channel(models.Model):
12181255 def get_editable (cls , user , channel_id ):
12191256 return cls .filter_edit_queryset (cls .objects .all (), user ).get (id = channel_id )
12201257
1258+ @classmethod
1259+ def _organization_role_exists (cls , user_id , roles ):
1260+ return Exists (
1261+ OrganizationRole .objects .filter (
1262+ user_id = user_id ,
1263+ organization_id = OuterRef ("organization_id" ),
1264+ status = ORGANIZATION_ROLE_STATUS_ACTIVE ,
1265+ role__in = roles ,
1266+ )
1267+ )
1268+
12211269 @classmethod
12221270 def filter_edit_queryset (cls , queryset , user ):
12231271 user_id = not user .is_anonymous and user .id
@@ -1231,16 +1279,8 @@ def filter_edit_queryset(cls, queryset, user):
12311279 user_id = user_id , channel_id = OuterRef ("id" )
12321280 )
12331281 )
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- )
1282+ organization_edit = cls ._organization_role_exists (
1283+ user_id , ORGANIZATION_CHANNEL_EDIT_ROLES
12441284 )
12451285 queryset = queryset .annotate (
12461286 edit = edit ,
@@ -1263,13 +1303,10 @@ def filter_delete_queryset(cls, queryset, user):
12631303 user_id = user_id , channel_id = OuterRef ("id" )
12641304 )
12651305 )
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- )
1306+ # Deleting a channel is restricted to organization admins, unlike
1307+ # editing it, which organization editors may also do.
1308+ organization_delete = cls ._organization_role_exists (
1309+ user_id , (ORGANIZATION_ADMIN ,)
12731310 )
12741311 queryset = queryset .annotate (
12751312 edit = edit ,
@@ -1301,29 +1338,40 @@ def filter_view_queryset(cls, queryset, user):
13011338 )
13021339 )
13031340
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- )
1341+ organization_edit = cls ._organization_role_exists (
1342+ user_id , ORGANIZATION_CHANNEL_EDIT_ROLES
1343+ )
1344+ organization_view = cls ._organization_role_exists (
1345+ user_id , ORGANIZATION_CHANNEL_VIEW_ROLES
13151346 )
13161347 else :
13171348 edit = boolean_val (False )
13181349 view = boolean_val (False )
1350+ organization_edit = boolean_val (False )
13191351 organization_view = boolean_val (False )
13201352
13211353 queryset = queryset .annotate (
13221354 edit = edit ,
13231355 view = view ,
1356+ organization_edit = organization_edit ,
13241357 organization_view = organization_view ,
13251358 )
13261359
1360+ # Fold organization grants into edit/view here, in the model layer,
1361+ # so every consumer of this queryset sees combined permissions
1362+ # rather than just the personal editor/viewer grants.
1363+ queryset = queryset .annotate (
1364+ edit = ExpressionWrapper (
1365+ Q (edit = True ) | Q (organization_edit = True ),
1366+ output_field = models .BooleanField (),
1367+ ),
1368+ ).annotate (
1369+ view = ExpressionWrapper (
1370+ Q (view = True ) | Q (organization_view = True ),
1371+ output_field = models .BooleanField (),
1372+ ),
1373+ )
1374+
13271375 if user_id and user .is_admin :
13281376 return queryset
13291377
@@ -1338,10 +1386,7 @@ def filter_view_queryset(cls, queryset, user):
13381386 ).values_list ("channel_id" , flat = True )
13391387
13401388 permission_filter = (
1341- Q (view = True )
1342- | Q (edit = True )
1343- | Q (organization_view = True )
1344- | Q (deleted = False , id__in = pending_channels )
1389+ Q (view = True ) | Q (edit = True ) | Q (deleted = False , id__in = pending_channels )
13451390 )
13461391
13471392 return queryset .filter (permission_filter | Q (deleted = False , public = True ))
0 commit comments