@@ -1208,11 +1208,7 @@ def tree(self, request, pk, *args, **kwargs):
12081208 @transaction .atomic
12091209 def duplicate (self , request , * args , ** kwargs ):
12101210 """
1211- Duplicate a document and store the links to attached files in the duplicated
1212- document to allow cross-access.
1213-
1214- Optionally duplicates accesses if `with_accesses` is set to true
1215- in the payload.
1211+ Duplicate a document, alongside its descendants if requested.
12161212 """
12171213 # Get document while checking permissions
12181214 document_to_duplicate = self .get_object ()
@@ -1221,8 +1217,43 @@ def duplicate(self, request, *args, **kwargs):
12211217 data = request .data , partial = True
12221218 )
12231219 serializer .is_valid (raise_exception = True )
1220+ user = request .user
1221+
1222+ duplicated_document = self ._duplicate_document (
1223+ document_to_duplicate = document_to_duplicate ,
1224+ serializer = serializer ,
1225+ user = user ,
1226+ )
1227+
1228+ return drf_response .Response (
1229+ {"id" : str (duplicated_document .id )}, status = status .HTTP_201_CREATED
1230+ )
1231+
1232+ def _duplicate_document (
1233+ self ,
1234+ document_to_duplicate ,
1235+ serializer ,
1236+ user ,
1237+ new_parent = None ,
1238+ ):
1239+ """
1240+ Duplicate a document and store the links to attached files in the duplicated
1241+ document to allow cross-access.
1242+
1243+ Optionally duplicates accesses if `with_accesses` is set to true
1244+ in the payload.
1245+
1246+ Optionally duplicates sub-documents if `with_descendants` is set to true in
1247+ the payload. In this case, the whole subtree of the document will be duplicated,
1248+ and the links to attached files will be stored in all duplicated documents.
1249+
1250+ The `with_accesses` option will also be applied to all duplicated documents
1251+ if `with_descendants` is set to true.
1252+ """
12241253 with_accesses = serializer .validated_data .get ("with_accesses" , False )
1225- user_role = document_to_duplicate .get_role (request .user )
1254+ with_descendants = serializer .validated_data .get ("with_descendants" , False )
1255+
1256+ user_role = document_to_duplicate .get_role (user )
12261257 is_owner_or_admin = user_role in models .PRIVILEGED_ROLES
12271258
12281259 base64_yjs_content = document_to_duplicate .content
@@ -1241,68 +1272,106 @@ def duplicate(self, request, *args, **kwargs):
12411272 extracted_attachments & set (document_to_duplicate .attachments )
12421273 )
12431274 title = capfirst (_ ("copy of {title}" ).format (title = document_to_duplicate .title ))
1244- if not document_to_duplicate .is_root () and choices .RoleChoices .get_priority (
1245- user_role
1246- ) < choices .RoleChoices .get_priority (models .RoleChoices .EDITOR ):
1247- duplicated_document = models .Document .add_root (
1248- creator = self .request .user ,
1275+ # If parent_duplicate is provided we must add the duplicated document as a child
1276+ if new_parent is not None :
1277+ duplicated_document = new_parent .add_child (
12491278 title = title ,
12501279 content = base64_yjs_content ,
12511280 attachments = attachments ,
12521281 duplicated_from = document_to_duplicate ,
1282+ creator = user ,
12531283 ** link_kwargs ,
12541284 )
1255- models .DocumentAccess .objects .create (
1256- document = duplicated_document ,
1257- user = self .request .user ,
1258- role = models .RoleChoices .OWNER ,
1259- )
1260- return drf_response .Response (
1261- {"id" : str (duplicated_document .id )}, status = status .HTTP_201_CREATED
1262- )
12631285
1264- duplicated_document = document_to_duplicate .add_sibling (
1265- "right" ,
1266- title = title ,
1267- content = base64_yjs_content ,
1268- attachments = attachments ,
1269- duplicated_from = document_to_duplicate ,
1270- creator = request .user ,
1271- ** link_kwargs ,
1272- )
1273-
1274- # Always add the logged-in user as OWNER for root documents
1275- if document_to_duplicate .is_root ():
1276- accesses_to_create = [
1277- models .DocumentAccess (
1278- document = duplicated_document ,
1279- user = request .user ,
1280- role = models .RoleChoices .OWNER ,
1281- )
1282- ]
1283-
1284- # If accesses should be duplicated, add other users' accesses as per original document
1286+ # Handle access duplication for this child
12851287 if with_accesses and is_owner_or_admin :
12861288 original_accesses = models .DocumentAccess .objects .filter (
12871289 document = document_to_duplicate
1288- ).exclude (user = request . user )
1290+ ).exclude (user = user )
12891291
1290- accesses_to_create . extend (
1292+ accesses_to_create = [
12911293 models .DocumentAccess (
12921294 document = duplicated_document ,
12931295 user_id = access .user_id ,
12941296 team = access .team ,
12951297 role = access .role ,
12961298 )
12971299 for access in original_accesses
1298- )
1300+ ]
12991301
1300- # Bulk create all the duplicated accesses
1301- models .DocumentAccess .objects .bulk_create (accesses_to_create )
1302+ if accesses_to_create :
1303+ models .DocumentAccess .objects .bulk_create (accesses_to_create )
13021304
1303- return drf_response .Response (
1304- {"id" : str (duplicated_document .id )}, status = status .HTTP_201_CREATED
1305- )
1305+ elif not document_to_duplicate .is_root () and choices .RoleChoices .get_priority (
1306+ user_role
1307+ ) < choices .RoleChoices .get_priority (models .RoleChoices .EDITOR ):
1308+ duplicated_document = models .Document .add_root (
1309+ creator = user ,
1310+ title = title ,
1311+ content = base64_yjs_content ,
1312+ attachments = attachments ,
1313+ duplicated_from = document_to_duplicate ,
1314+ ** link_kwargs ,
1315+ )
1316+ models .DocumentAccess .objects .create (
1317+ document = duplicated_document ,
1318+ user = user ,
1319+ role = models .RoleChoices .OWNER ,
1320+ )
1321+ else :
1322+ duplicated_document = document_to_duplicate .add_sibling (
1323+ "right" ,
1324+ title = title ,
1325+ content = base64_yjs_content ,
1326+ attachments = attachments ,
1327+ duplicated_from = document_to_duplicate ,
1328+ creator = user ,
1329+ ** link_kwargs ,
1330+ )
1331+
1332+ # Always add the logged-in user as OWNER for root documents
1333+ if document_to_duplicate .is_root ():
1334+ accesses_to_create = [
1335+ models .DocumentAccess (
1336+ document = duplicated_document ,
1337+ user = user ,
1338+ role = models .RoleChoices .OWNER ,
1339+ )
1340+ ]
1341+
1342+ # If accesses should be duplicated,
1343+ # add other users' accesses as per original document
1344+ if with_accesses and is_owner_or_admin :
1345+ original_accesses = models .DocumentAccess .objects .filter (
1346+ document = document_to_duplicate
1347+ ).exclude (user = user )
1348+
1349+ accesses_to_create .extend (
1350+ models .DocumentAccess (
1351+ document = duplicated_document ,
1352+ user_id = access .user_id ,
1353+ team = access .team ,
1354+ role = access .role ,
1355+ )
1356+ for access in original_accesses
1357+ )
1358+
1359+ # Bulk create all the duplicated accesses
1360+ models .DocumentAccess .objects .bulk_create (accesses_to_create )
1361+
1362+ if with_descendants :
1363+ for child in document_to_duplicate .get_children ().filter (
1364+ ancestors_deleted_at__isnull = True
1365+ ):
1366+ # When duplicating descendants, attach duplicates under the duplicated_document
1367+ self ._duplicate_document (
1368+ document_to_duplicate = child ,
1369+ serializer = serializer ,
1370+ user = user ,
1371+ new_parent = duplicated_document ,
1372+ )
1373+
1374+ return duplicated_document
13061375
13071376 def _search_simple (self , request , text ):
13081377 """
0 commit comments