Skip to content

Commit 1f24bb1

Browse files
committed
add bulk add_contributor_registered_or_not method
1 parent c2191f8 commit 1f24bb1

File tree

2 files changed

+131
-103
lines changed

2 files changed

+131
-103
lines changed

api/nodes/serializers.py

Lines changed: 72 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,104 +1226,80 @@ def create(self, validated_data):
12261226
node = self.context['resource']
12271227
auth = Auth(request.user)
12281228

1229-
with transaction.atomic():
1230-
results = []
1231-
registered_items = []
1232-
unregistered_items = []
1233-
1234-
for item in validated_data:
1235-
if item.get('_id') and not item.get('user', {}).get('email'):
1236-
registered_items.append(item)
1237-
else:
1238-
unregistered_items.append(item)
1239-
1240-
def _perm(item):
1241-
return osf_permissions.get_contributor_proposed_permissions(item)
1242-
1243-
if registered_items:
1244-
# Load users once and build a mapping to reuse
1245-
user_map = {}
1246-
for item in registered_items:
1247-
user_id = item.get('_id')
1248-
if user_id and user_id not in user_map:
1249-
user = OSFUser.load(user_id)
1250-
if not user:
1251-
raise exceptions.NotFound(detail=f"User {user_id} not found")
1252-
user_map[user_id] = user
1253-
1254-
contrib_dicts = []
1255-
for item in registered_items:
1256-
user = user_map[item.get('_id')]
1257-
contrib_dicts.append({
1258-
'user': user,
1259-
'permissions': _perm(item),
1260-
'visible': item.get('bibliographic'),
1261-
})
1262-
contribs = node.add_contributors(contrib_dicts, auth=auth, log=True, save=True)
1263-
results.extend(contribs)
1264-
1265-
child_to_items = {}
1266-
for item in registered_items:
1267-
child_nodes = item.get('child_nodes')
1268-
if child_nodes:
1269-
for child_id in child_nodes:
1270-
child_to_items.setdefault(child_id, []).append(item)
1271-
1272-
for child_id, items in child_to_items.items():
1273-
child = AbstractNode.load(child_id)
1274-
if not child:
1275-
continue
1276-
child_contribs = []
1277-
for item in items:
1278-
child_contribs.append({
1279-
'user': user_map[item.get('_id')],
1280-
'permissions': _perm(item),
1281-
'visible': item.get('bibliographic'),
1282-
})
1283-
child.add_contributors(child_contribs, auth=auth, log=True, save=True)
1284-
1285-
for item in unregistered_items:
1286-
id = item.get('_id')
1229+
def _perm(item):
1230+
return osf_permissions.get_contributor_proposed_permissions(item)
1231+
1232+
send_email_default = request.GET.get('send_email') or self.context['default_email']
1233+
# Preload users once and pass through to the bulk method (also reused for children)
1234+
user_ids = {item.get('_id') for item in validated_data if item.get('_id')}
1235+
user_map = {}
1236+
if user_ids:
1237+
for u in OSFUser.objects.filter(guids___id__in=user_ids):
1238+
user_map[u._id] = u
1239+
1240+
payload = []
1241+
for item in validated_data:
1242+
uid = item.get('_id')
1243+
user = user_map.get(uid)
1244+
email = item.get('user', {}).get('email', None)
1245+
full_name = item.get('full_name') or (user.fullname if user and not user.is_registered else None)
1246+
if not uid and not full_name:
1247+
raise exceptions.ValidationError(detail='A user ID or full name must be provided to add a contributor.')
1248+
payload.append({
1249+
'user_id': uid,
1250+
'user': user,
1251+
'email': email,
1252+
'full_name': full_name,
1253+
'send_email': send_email_default,
1254+
'permissions': _perm(item),
1255+
'bibliographic': item.get('bibliographic'),
1256+
'index': item.get('_order') if '_order' in item else None,
1257+
})
1258+
1259+
try:
1260+
contribs = node.add_contributors_registered_or_not(payload, auth=auth, save=True)
1261+
except ValidationError as e:
1262+
raise exceptions.ValidationError(detail=e.messages[0])
1263+
except ValueError as e:
1264+
raise exceptions.NotFound(detail=e.args[0])
1265+
1266+
child_to_items = {}
1267+
for item in validated_data:
1268+
child_nodes = item.get('child_nodes')
1269+
if child_nodes:
1270+
for child_id in child_nodes:
1271+
child_to_items.setdefault(child_id, []).append(item)
1272+
1273+
for child_id, items in child_to_items.items():
1274+
child = AbstractNode.load(child_id)
1275+
if not child:
1276+
continue
1277+
child_payload = []
1278+
for item in items:
1279+
uid = item.get('_id')
1280+
user = user_map.get(uid)
12871281
email = item.get('user', {}).get('email', None)
1288-
full_name = item.get('full_name')
1289-
bibliographic = item.get('bibliographic')
1290-
index = item.get('_order') if '_order' in item else None
1291-
send_email = request.GET.get('send_email') or self.context['default_email']
1292-
permissions = _perm(item)
1282+
full_name = item.get('full_name') or (user.fullname if user and not user.is_registered else None)
1283+
if not uid and not full_name:
1284+
raise exceptions.ValidationError(detail='A user ID or full name must be provided to add a contributor.')
1285+
child_payload.append({
1286+
'user_id': uid,
1287+
'user': user,
1288+
'email': email,
1289+
'full_name': full_name,
1290+
'send_email': send_email_default,
1291+
'permissions': _perm(item),
1292+
'bibliographic': item.get('bibliographic'),
1293+
'index': item.get('_order') if '_order' in item else None,
1294+
})
1295+
try:
1296+
child.add_contributors_registered_or_not(child_payload, auth=auth, save=True)
1297+
except ValidationError as e:
1298+
raise exceptions.ValidationError(detail=e.messages[0])
1299+
except ValueError as e:
1300+
raise exceptions.NotFound(detail=e.args[0])
12931301

1294-
try:
1295-
contributor_obj = node.add_contributor_registered_or_not(
1296-
auth=auth,
1297-
user_id=id,
1298-
email=email,
1299-
full_name=full_name,
1300-
send_email=send_email,
1301-
permissions=permissions,
1302-
bibliographic=bibliographic,
1303-
index=index,
1304-
save=True,
1305-
)
1306-
results.append(contributor_obj)
1307-
child_nodes = item.get('child_nodes')
1308-
if child_nodes:
1309-
for child in AbstractNode.objects.filter(guids___id__in=child_nodes):
1310-
child.add_contributor_registered_or_not(
1311-
auth=auth,
1312-
user_id=id,
1313-
email=email,
1314-
full_name=full_name,
1315-
send_email=send_email,
1316-
permissions=permissions,
1317-
bibliographic=bibliographic,
1318-
index=index,
1319-
save=True,
1320-
)
1321-
except ValidationError as e:
1322-
raise exceptions.ValidationError(detail=e.messages[0])
1323-
except ValueError as e:
1324-
raise exceptions.NotFound(detail=e.args[0])
1325-
1326-
return results
1302+
return contribs
13271303

13281304

13291305
class NodeContributorsCreateSerializer(NodeContributorsSerializer):

osf/models/mixins.py

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,7 +1507,8 @@ def add_contributors(self, contributors, auth=None, log=True, save=False):
15071507
return self.contributor_set.filter(user__in=users)
15081508

15091509
def add_unregistered_contributor(self, fullname, email, auth, send_email=None,
1510-
visible=True, permissions=None, save=True, existing_user=None):
1510+
visible=True, permissions=None, save=True, existing_user=None,
1511+
log=True):
15111512
"""Add a non-registered contributor to the project.
15121513
15131514
:param str fullname: The full name of the person.
@@ -1555,7 +1556,7 @@ def add_unregistered_contributor(self, fullname, email, auth, send_email=None,
15551556

15561557
self.add_contributor(
15571558
contributor, permissions=permissions, auth=auth,
1558-
visible=visible, send_email=send_email, log=True, save=False
1559+
visible=visible, send_email=send_email, log=log, save=False
15591560
)
15601561
self._add_related_source_tags(contributor)
15611562
if save:
@@ -1564,12 +1565,13 @@ def add_unregistered_contributor(self, fullname, email, auth, send_email=None,
15641565

15651566
def add_contributor_registered_or_not(self, auth, user_id=None,
15661567
full_name=None, email=None, send_email=None,
1567-
permissions=None, bibliographic=True, index=None, save=True):
1568+
permissions=None, bibliographic=True, index=None, save=True,
1569+
user=None, log=True):
15681570
OSFUser = apps.get_model('osf.OSFUser')
15691571
send_email = send_email or self.contributor_email_template
15701572

15711573
if user_id:
1572-
contributor = OSFUser.load(user_id)
1574+
contributor = user or OSFUser.load(user_id)
15731575
if not contributor:
15741576
raise ValueError(f'User with id {user_id} was not found.')
15751577

@@ -1578,7 +1580,7 @@ def add_contributor_registered_or_not(self, auth, user_id=None,
15781580

15791581
if contributor.is_registered:
15801582
contributor = self.add_contributor(contributor=contributor, auth=auth, visible=bibliographic,
1581-
permissions=permissions, send_email=send_email, save=save)
1583+
permissions=permissions, send_email=send_email, save=save, log=log)
15821584
else:
15831585
if not full_name:
15841586
raise ValueError(
@@ -1588,7 +1590,7 @@ def add_contributor_registered_or_not(self, auth, user_id=None,
15881590
contributor = self.add_unregistered_contributor(
15891591
fullname=full_name, email=contributor.username, auth=auth,
15901592
send_email=send_email, permissions=permissions,
1591-
visible=bibliographic, existing_user=contributor, save=save
1593+
visible=bibliographic, existing_user=contributor, save=save, log=log
15921594
)
15931595

15941596
else:
@@ -1604,12 +1606,13 @@ def add_contributor_registered_or_not(self, auth, user_id=None,
16041606
send_email=send_email,
16051607
permissions=permissions,
16061608
save=save,
1609+
log=log,
16071610
)
16081611
else:
16091612
contributor = self.add_unregistered_contributor(
16101613
fullname=full_name, email=email, auth=auth,
16111614
send_email=send_email, permissions=permissions,
1612-
visible=bibliographic, save=save
1615+
visible=bibliographic, save=save, log=log
16131616
)
16141617

16151618
auth.user.email_last_sent = timezone.now()
@@ -1621,6 +1624,55 @@ def add_contributor_registered_or_not(self, auth, user_id=None,
16211624
contributor_obj = self.contributor_set.get(user=contributor)
16221625
return contributor_obj
16231626

1627+
def add_contributors_registered_or_not(self, contributors, auth=None, log=True, save=False):
1628+
"""Add multiple contributors using the unified registered-or-not path.
1629+
1630+
Each item should be a dictionary with keys compatible with
1631+
`add_contributor_registered_or_not`, e.g.:
1632+
{
1633+
'user_id': '<user guid>',
1634+
'user': '<OSFUser>' or None,
1635+
'email': '<email>' or None,
1636+
'full_name': '<full name>' or None,
1637+
'send_email': '<email preference>' or None,
1638+
'permissions': <permission string>,
1639+
'bibliographic': <bool>,
1640+
'index': <int or None>,
1641+
}
1642+
"""
1643+
results = []
1644+
1645+
for item in contributors:
1646+
contributor_obj = self.add_contributor_registered_or_not(
1647+
auth=auth,
1648+
user_id=item.get('user_id'),
1649+
user=item.get('user'),
1650+
full_name=item.get('full_name'),
1651+
email=item.get('email'),
1652+
send_email=item.get('send_email') or getattr(self, 'contributor_email_template', None),
1653+
permissions=item.get('permissions'),
1654+
bibliographic=item.get('bibliographic', True),
1655+
index=item.get('index'),
1656+
save=False,
1657+
log=False,
1658+
)
1659+
results.append(contributor_obj)
1660+
1661+
if log and results:
1662+
params = self.log_params
1663+
params['contributors'] = [c.user._id for c in results]
1664+
self.add_log(
1665+
action=self.log_class.CONTRIB_ADDED,
1666+
params=params,
1667+
auth=auth,
1668+
save=False,
1669+
)
1670+
1671+
if save:
1672+
self.save()
1673+
1674+
return results
1675+
16241676
def replace_contributor(self, old, new):
16251677
"""
16261678
Replacing unregistered contributor with a verified user

0 commit comments

Comments
 (0)