Repository navigation
Expand file tree
/
Copy pathnode.py
More file actions
1512 lines (1276 loc) · 55.1 KB
/
Copy pathnode.py
File metadata and controls
1512 lines (1276 loc) · 55.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
import os
import logging
from rest_framework import status as http_status
import math
from collections import defaultdict
from flask import request
from django.apps import apps
from django.utils import timezone
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.db.models import Q, OuterRef, Subquery
from framework import status
from framework.utils import iso8601format
from framework.flask import redirect # VOL-aware redirect
from framework.auth.decorators import must_be_logged_in, collect_auth
from website.ember_osf_web.decorators import ember_flag_is_active
from api.waffle.utils import flag_is_active, storage_i18n_flag_active, storage_usage_flag_active
from framework.exceptions import HTTPError, PermissionsError
from osf.models.nodelog import NodeLog
from osf.utils.functional import rapply
from osf.utils.registrations import strip_registered_meta_comments
from osf.utils import sanitize
from osf import features
from website import language
from website.util import rubeus
from website.ember_osf_web.views import use_ember_app
from osf.exceptions import NodeStateError
from website.project import new_node, new_private_link
from website.project.decorators import (
must_be_contributor_or_public_but_not_anonymized,
must_be_contributor_or_public,
must_be_valid_project,
must_have_permission,
must_not_be_registration,
must_not_be_retracted_registration,
)
from osf.utils.tokens import process_token_or_pass
from website.util.rubeus import collect_addon_js
from website.project.model import has_anonymous_link, NodeUpdateError, validate_title
from website.project.forms import NewNodeForm
from website.project.utils import sizeof_fmt
from website.project.metadata.utils import serialize_meta_schemas
from addons.wiki.models import WikiPage
from osf.models import AbstractNode, Collection, Contributor, Guid, PrivateLink, Node, NodeRelation, Preprint
from osf.models.licenses import serialize_node_license_record
from osf.utils.sanitize import strip_html
from osf.utils.permissions import ADMIN, READ, WRITE, CREATOR_PERMISSIONS, ADMIN_NODE
from osf.models import RdmTimestampGrantPattern
from website import settings
from website.views import find_bookmark_collection, validate_page_num
from website.views import serialize_node_summary, get_storage_region_list
from website.profile import utils
from addons.mendeley.provider import MendeleyCitationsProvider
from addons.zotero.provider import ZoteroCitationsProvider
from addons.wiki.utils import serialize_wiki_widget
from addons.wiki.models import WikiVersion
from addons.dataverse.utils import serialize_dataverse_widget
from addons.forward.utils import serialize_forward_widget
from addons.jupyterhub.utils import serialize_jupyterhub_widget
from addons.iqbrims.utils import serialize_iqbrims_widget
from addons.myscreen.utils import serialize_myscreen_widget
from admin.rdm_addons.utils import validate_rdm_addons_allowed
from api.base import settings as api_settings
from website.util import quota
from osf.models.project_storage_type import ProjectStorageType
from nii.mapcore_api import MAPCoreException
from nii.mapcore import (mapcore_sync_is_enabled,
mapcore_log_error,
mapcore_sync_rdm_project_or_map_group,
mapcore_sync_map_group)
r_strip_html = lambda collection: rapply(collection, strip_html)
logger = logging.getLogger(__name__)
@must_be_valid_project
@must_have_permission(WRITE)
def edit_node(auth, node, **kwargs):
post_data = request.json
edited_field = post_data.get('name')
value = post_data.get('value', '')
new_val = None
if edited_field == 'title':
try:
node.set_title(value, auth=auth)
except ValidationError as e:
raise HTTPError(
http_status.HTTP_400_BAD_REQUEST,
data=dict(message_long=str(e))
)
new_val = node.title
elif edited_field == 'description':
node.set_description(value, auth=auth)
new_val = node.description
elif edited_field == 'category':
node.category = new_val = value
try:
node.save()
except ValidationError as e:
raise HTTPError(
http_status.HTTP_400_BAD_REQUEST,
data=dict(message_long=str(e))
)
if mapcore_sync_is_enabled():
mapcore_sync_map_group(auth.user, node)
return {
'status': 'success',
'newValue': new_val # Used by x-editable widget to reflect changes made by sanitizer
}
##############################################################################
# New Project
##############################################################################
@must_be_logged_in
def project_new(**kwargs):
return {}
@must_be_logged_in
def project_new_post(auth, **kwargs):
user = auth.user
data = request.get_json()
title = strip_html(data.get('title'))
title = title.strip()
category = data.get('category', 'project')
template = data.get('template')
description = strip_html(data.get('description'))
campaign = data.get('campaign', None)
new_project = {}
if template:
original_node = AbstractNode.load(template)
changes = {
'title': title,
'category': category,
'template_node': original_node,
}
if description:
changes['description'] = description
project = original_node.use_as_template(
auth=auth,
changes={
template: changes,
}
)
else:
try:
project = new_node(category, title, user, description, campaign=campaign)
except ValidationError as e:
raise HTTPError(
http_status.HTTP_400_BAD_REQUEST,
data=dict(message_long=str(e))
)
new_project = _view_project(project, auth)
return {
'projectUrl': project.url,
'newNode': new_project['node'] if new_project else None
}, http_status.HTTP_201_CREATED
@must_be_logged_in
@must_be_valid_project
def project_new_from_template(auth, node, **kwargs):
new_node = node.use_as_template(
auth=auth,
changes=dict(),
)
return {'url': new_node.url}, http_status.HTTP_201_CREATED, None
##############################################################################
# New Node
##############################################################################
@must_be_valid_project
@must_have_permission(WRITE)
@must_not_be_registration
def project_new_node(auth, node, **kwargs):
"""
There's an APIv2 endpoint that does this same thing!
If you make changes here, see if they need to be made there.
"""
form = NewNodeForm(request.form)
user = auth.user
if form.validate():
try:
new_component = new_node(
title=strip_html(form.title.data),
user=user,
category=form.category.data,
parent=node,
)
except ValidationError as e:
raise HTTPError(
http_status.HTTP_400_BAD_REQUEST,
data=dict(message_long=str(e))
)
redirect_url = node.url
message = (
'Your component was created successfully. You can keep working on the project page below, '
'or go to the new <u><a href={component_url}>component</a></u>.'
).format(component_url=new_component.url)
if form.inherit_contributors.data and node.has_permission(user, WRITE):
for contributor in node.contributors:
# Using permission property off of Contributor model to get contributor permissions - not group member perms
perm = CREATOR_PERMISSIONS if contributor._id == user._id else Contributor.objects.get(user_id=contributor.id, node_id=node.id).permission
if contributor._id == user._id and not contributor.is_registered:
new_component.add_unregistered_contributor(
fullname=contributor.fullname, email=contributor.email,
permissions=perm, auth=auth, existing_user=contributor
)
else:
new_component.add_contributor(contributor, permissions=perm, auth=auth)
for group in node.osf_groups:
if group.is_manager(user):
new_component.add_osf_group(group, group.get_permission_to_node(node), auth=auth)
new_component.save()
redirect_url = new_component.url + 'contributors/'
message = (
'Your component was created successfully. You can edit the contributor permissions below, '
'work on your <u><a href={component_url}>component</a></u> or return to the <u> '
'<a href="{project_url}">project page</a></u>.'
).format(component_url=new_component.url, project_url=node.url)
status.push_status_message(message, kind='info', trust=True)
return {
'status': 'success',
}, 201, None, redirect_url
else:
# TODO: This function doesn't seem to exist anymore?
status.push_errors_to_status(form.errors)
raise HTTPError(http_status.HTTP_400_BAD_REQUEST, redirect_url=node.url)
@must_be_logged_in
@must_be_valid_project
def project_before_fork(auth, node, **kwargs):
user = auth.user
prompts = node.callback('before_fork', user=user)
if node.has_pointers_recursive:
prompts.append(
language.BEFORE_FORK_HAS_POINTERS.format(
category=node.project_or_component
)
)
return {'prompts': prompts}
@must_be_logged_in
@must_be_valid_project
def project_before_template(auth, node, **kwargs):
prompts = []
for addon in node.get_addons():
if 'node' in addon.config.configs:
if addon.to_json(auth.user)['addon_full_name']:
prompts.append(addon.to_json(auth.user)['addon_full_name'])
return {'prompts': prompts, 'isRegistration': node.is_registration}
@must_be_valid_project
@must_be_contributor_or_public_but_not_anonymized
@must_not_be_registration
def node_registrations(auth, node, **kwargs):
if request.path.startswith('/project/'):
return redirect('/{}/registrations/'.format(node._id))
return use_ember_app()
@must_be_valid_project
@must_be_contributor_or_public_but_not_anonymized
@must_not_be_retracted_registration
def node_forks(auth, node, **kwargs):
if request.path.startswith('/project/'):
return redirect('/' + node._id + '/forks/')
return use_ember_app()
@must_be_valid_project
@must_not_be_retracted_registration
@must_be_logged_in
@must_have_permission(READ)
@ember_flag_is_active(features.EMBER_PROJECT_SETTINGS)
def node_setting(auth, node, **kwargs):
if node.is_registration and flag_is_active(request, features.EMBER_REGISTRIES_DETAIL_PAGE):
# Registration settings page obviated during redesign
return redirect(node.url)
auth.user.update_affiliated_institutions_by_email_domain()
auth.user.save()
ret = _view_project(node, auth, primary=True)
ret['include_wiki_settings'] = WikiPage.objects.include_wiki_settings(node)
ret['wiki_enabled'] = 'wiki' in node.get_addon_names()
ret['comments'] = {
'level': node.comment_level,
}
addon_settings = {}
for addon in ['forward']:
addon_config = apps.get_app_config('addons_{}'.format(addon))
config = addon_config.to_json()
config['template_lookup'] = addon_config.template_lookup
config['addon_icon_url'] = addon_config.icon_url
config['node_settings_template'] = os.path.basename(addon_config.node_settings_template)
addon_settings[addon] = config
ret['addon_settings'] = addon_settings
ret['categories'] = settings.NODE_CATEGORY_MAP
ret['categories'].update({
'project': 'Project'
})
ret['group'] = node.group.name if node.group is not None else None
ret['can_delete'] = True if node.group is None else False
return ret
@must_be_valid_project
@must_not_be_registration
@must_be_logged_in
@must_have_permission(WRITE)
def node_addons(auth, node, **kwargs):
ret = _view_project(node, auth, primary=True)
addon_settings = serialize_addons(node, auth)
ret['addon_capabilities'] = settings.ADDON_CAPABILITIES
# If an addon is default you cannot connect/disconnect so we don't have to load it.
ret['addon_settings'] = [addon for addon in addon_settings]
# Addons can have multiple categories, but we only want a set of unique ones being used.
ret['addon_categories'] = set([item for addon in addon_settings for item in addon['categories']])
# The page only needs to load enabled addons and it refreshes when a new addon is being enabled.
ret['addon_js'] = collect_node_config_js([addon for addon in addon_settings if addon['enabled']])
try:
timestamp_pattern = RdmTimestampGrantPattern.objects.get(node_guid=node._id)
ret['timestamp_pattern_division'] = timestamp_pattern.timestamp_pattern_division
except ObjectDoesNotExist:
timestamp_pattern = None
return ret
def get_timestamp_pattern_division(auth, node, **kwargs):
try:
timestamp_pattern = RdmTimestampGrantPattern.objects.get(node_guid=node._id)
timestamp_pattern_division = timestamp_pattern.timestamp_pattern_division
except ObjectDoesNotExist:
timestamp_pattern_division = None
return timestamp_pattern_division
def serialize_addons(node, auth):
addon_settings = []
addons_available = [addon for addon in settings.ADDONS_AVAILABLE
if addon not in settings.SYSTEM_ADDED_ADDONS['node']
and addon.short_name not in ('wiki', 'forward', 'twofactor')]
### forced Admin Settings
from admin.rdm_addons.utils import update_with_rdm_addon_settings
owners_addons_available = sorted([
owners_addon
for owners_addon in settings.ADDONS_AVAILABLE
if 'node' in owners_addon.owners
and owners_addon.short_name not in settings.SYSTEM_ADDED_ADDONS['node']
and owners_addon.short_name not in ['wiki', 'forward', 'twofactor']
], key=lambda owners_addon: owners_addon.full_name.lower())
rdm_addon_settings = [{'addon_short_name': owners_addon.short_name} for owners_addon in owners_addons_available]
update_with_rdm_addon_settings(rdm_addon_settings, auth.user)
addons_allowed = [
addon['addon_short_name']
for addon in rdm_addon_settings
if (addon['is_allowed'] and not addon['is_forced'])
or (addon['is_allowed'] and addon['is_forced'] and addon['has_user_external_accounts'])
]
for addon in addons_available:
addon_config = apps.get_app_config('addons_{}'.format(addon.short_name))
config = addon_config.to_json()
config['template_lookup'] = addon_config.template_lookup
config['addon_icon_url'] = addon_config.icon_url
config['node_settings_template'] = os.path.basename(addon_config.node_settings_template)
config['addon_short_name'] = addon.short_name
config['addon_full_name'] = addon.full_name
config['categories'] = addon.categories
config['enabled'] = node.has_addon(addon.short_name)
config['default'] = addon.short_name in settings.ADDONS_DEFAULT
if node.has_addon(addon.short_name):
node_json = node.get_addon(addon.short_name).to_json(auth.user)
config.update(node_json)
if addon.short_name in addons_allowed:
addon_settings.append(config)
addon_settings = sorted(addon_settings, key=lambda addon: addon['full_name'].lower())
return addon_settings
def collect_node_config_js(addons):
"""Collect webpack bundles for each of the addons' node-cfg.js modules. Return
the URLs for each of the JS modules to be included on the node addons config page.
:param list addons: List of node's addon config records.
"""
js_modules = []
for addon in addons:
source_path = os.path.join(
settings.ADDON_PATH,
addon['short_name'],
'static',
'node-cfg.js',
)
if os.path.exists(source_path):
asset_path = os.path.join(
'/',
'static',
'public',
'js',
addon['short_name'],
'node-cfg.js',
)
js_modules.append(asset_path)
return js_modules
@must_have_permission(WRITE)
@must_not_be_registration
def node_choose_addons(auth, node, **kwargs):
config = request.json
try:
for addon_name, enabled in config.items():
if enabled:
validate_rdm_addons_allowed(auth, addon_name)
except PermissionsError as e:
raise HTTPError(
http_status.HTTP_403_FORBIDDEN,
data=dict(message_long=str(e))
)
node.config_addons(request.json, auth)
@must_be_valid_project
@must_not_be_retracted_registration
@must_have_permission(READ)
@ember_flag_is_active(features.EMBER_PROJECT_CONTRIBUTORS)
def node_contributors(auth, node, **kwargs):
ret = _view_project(node, auth, primary=True)
ret['contributors'] = utils.serialize_contributors(node.contributors, node)
ret['access_requests'] = utils.serialize_access_requests(node)
ret['adminContributors'] = utils.serialize_contributors(node.parent_admin_contributors, node, admin=True)
return ret
@must_have_permission(ADMIN)
def configure_comments(node, **kwargs):
comment_level = request.json.get('commentLevel')
if not comment_level:
node.comment_level = None
elif comment_level in ['public', 'private']:
node.comment_level = comment_level
else:
raise HTTPError(http_status.HTTP_400_BAD_REQUEST)
node.save()
@must_have_permission(ADMIN)
@must_not_be_registration
def configure_requests(node, **kwargs):
access_requests_enabled = request.get_json().get('accessRequestsEnabled')
auth = kwargs.get('auth', None)
node.set_access_requests_enabled(access_requests_enabled, auth, save=True)
return {'access_requests_enabled': access_requests_enabled}, 200
##############################################################################
# View Project
##############################################################################
@process_token_or_pass
@must_be_valid_project(retractions_valid=True)
@must_be_contributor_or_public
@ember_flag_is_active(features.EMBER_PROJECT_DETAIL)
def view_project(auth, node, **kwargs):
primary = '/api/v1' not in request.path
ret = _view_project(node, auth,
primary=primary,
embed_contributors=True,
embed_descendants=True
)
ret['addon_capabilities'] = settings.ADDON_CAPABILITIES
# Collect the URIs to the static assets for addons that have widgets
ret['addon_widget_js'] = list(collect_addon_js(
node,
filename='widget-cfg.js',
config_entry='widget'
))
ret.update(rubeus.collect_addon_assets(node))
access_request = node.requests.filter(creator=auth.user).exclude(machine_state='accepted')
ret['user']['access_request_state'] = access_request.get().machine_state if access_request else None
addons_widget_data = {
'wiki': None,
'mendeley': None,
'zotero': None,
'forward': None,
'dataverse': None,
'jupyterhub': None,
# widget: ここから
'myscreen': None
# widget: ここまで
}
if 'wiki' in ret['addons']:
addons_widget_data['wiki'] = serialize_wiki_widget(node)
if 'dataverse' in ret['addons']:
addons_widget_data['dataverse'] = serialize_dataverse_widget(node)
if 'forward' in ret['addons']:
addons_widget_data['forward'] = serialize_forward_widget(node)
if 'zotero' in ret['addons']:
node_addon = node.get_addon('zotero')
zotero_widget_data = ZoteroCitationsProvider().widget(node_addon)
addons_widget_data['zotero'] = zotero_widget_data
if 'mendeley' in ret['addons']:
node_addon = node.get_addon('mendeley')
mendeley_widget_data = MendeleyCitationsProvider().widget(node_addon)
addons_widget_data['mendeley'] = mendeley_widget_data
if 'jupyterhub' in ret['addons']:
addons_widget_data['jupyterhub'] = serialize_jupyterhub_widget(node)
if 'iqbrims' in ret['addons']:
addons_widget_data['iqbrims'] = serialize_iqbrims_widget(node)
# widget: ここから
if 'myscreen' in ret['addons']:
addons_widget_data['myscreen'] = serialize_myscreen_widget(node)
# widget: ここまで
ret.update({'addons_widget_data': addons_widget_data})
try:
projectStorageType = ProjectStorageType.objects.get(node=node)
ret['isCustomStorageLocation'] = projectStorageType.storage_type == ProjectStorageType.CUSTOM_STORAGE
except Exception as err:
logger.critical(err)
ret['isCustomStorageLocation'] = False
return ret
@process_token_or_pass
@must_be_valid_project(retractions_valid=True)
@must_be_contributor_or_public
def token_action(auth, node, **kwargs):
return redirect(node.url)
# Reorder components
@must_be_valid_project
@must_not_be_registration
@must_have_permission(WRITE)
def project_reorder_components(node, **kwargs):
"""Reorders the components in a project's component list.
:param-json list new_list: List of strings that include node GUIDs.
"""
ordered_guids = request.get_json().get('new_list', [])
node_relations = (
node.node_relations
.select_related('child')
.filter(child__is_deleted=False)
)
deleted_node_relation_ids = list(
node.node_relations.select_related('child')
.filter(child__is_deleted=True)
.values_list('pk', flat=True)
)
if len(ordered_guids) > len(node_relations):
raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data=dict(message_long='Too many node IDs'))
# Ordered NodeRelation pks, sorted according the order of guids passed in the request payload
new_node_relation_ids = [
each.id for each in sorted(node_relations,
key=lambda nr: ordered_guids.index(nr.child._id))
]
if len(node_relations) == len(ordered_guids):
node.set_noderelation_order(new_node_relation_ids + deleted_node_relation_ids)
node.save()
return {'nodes': ordered_guids}
logger.error('Got invalid node list in reorder components')
raise HTTPError(http_status.HTTP_400_BAD_REQUEST)
@must_be_valid_project
@must_be_contributor_or_public
@must_not_be_retracted_registration
def project_statistics(auth, node, **kwargs):
if request.path.startswith('/project/'):
return redirect('/' + node._id + '/analytics/')
return use_ember_app()
###############################################################################
# Make Private/Public
###############################################################################
@must_be_valid_project
@must_have_permission(ADMIN)
def project_set_privacy(auth, node, **kwargs):
permissions = kwargs.get('permissions')
if permissions is None:
raise HTTPError(http_status.HTTP_400_BAD_REQUEST)
try:
node.set_privacy(permissions, auth)
except NodeStateError as e:
raise HTTPError(http_status.HTTP_400_BAD_REQUEST, data=dict(
message_short="Can't change privacy",
message_long=str(e)
))
return {
'status': 'success',
'permissions': permissions,
}
@must_be_valid_project
@must_have_permission(WRITE)
def update_node(auth, node, **kwargs):
# in node.update() method there is a key list node.WRITABLE_WHITELIST only allow user to modify
# category, title, and description which can be edited by write permission contributor
data = r_strip_html(request.get_json())
try:
updated_field_names = node.update(data, auth=auth)
except NodeUpdateError as e:
raise HTTPError(400, data=dict(
message_short='Failed to update attribute \'{0}\''.format(e.key),
message_long=e.reason
))
# Need to cast tags to a string to make them JSON-serialiable
updated_fields_dict = {
key: getattr(node, key) if key != 'tags' else [str(tag) for tag in node.tags]
for key in updated_field_names
if key != 'logs' and key != 'modified' and key != 'last_logged'
}
return {'updated_fields': updated_fields_dict}
@must_be_valid_project
@must_have_permission(ADMIN)
@must_not_be_registration
def component_remove(auth, node, **kwargs):
"""Remove component, and recursively remove its children. If node has a
parent, add log and redirect to parent; else redirect to user dashboard.
"""
try:
node.remove_node(auth)
except NodeStateError as e:
raise HTTPError(
http_status.HTTP_400_BAD_REQUEST,
data={
'message_short': 'Error',
'message_long': 'Could not delete component: ' + str(e)
},
)
message = '{} has been successfully deleted.'.format(
node.project_or_component.capitalize()
)
id = '{}_deleted'.format(node.project_or_component)
status.push_status_message(message, kind='success', trust=False, id=id)
parent = node.parent_node
if parent and parent.can_view(auth):
redirect_url = node.parent_node.url
else:
redirect_url = '/dashboard/'
return {
'url': redirect_url,
}
@must_be_valid_project
@must_have_permission(ADMIN)
def remove_private_link(*args, **kwargs):
link_id = request.json['private_link_id']
try:
link = PrivateLink.objects.get(_id=link_id)
except PrivateLink.DoesNotExist:
raise HTTPError(http_status.HTTP_404_NOT_FOUND)
link.is_deleted = True
link.deleted = timezone.now()
link.save()
for node in link.nodes.all():
log_dict = {
'project': node.parent_id,
'node': node._id,
'user': kwargs.get('auth').user._id,
'anonymous_link': link.anonymous,
}
node.add_log(
NodeLog.VIEW_ONLY_LINK_REMOVED,
log_dict,
auth=kwargs.get('auth', None)
)
# TODO: Split into separate functions
def _render_addons(addons):
widgets = {}
configs = {}
js = []
css = []
for addon in addons:
configs[addon.config.short_name] = addon.config.to_json()
js.extend(addon.config.include_js.get('widget', []))
css.extend(addon.config.include_css.get('widget', []))
js.extend(addon.config.include_js.get('files', []))
css.extend(addon.config.include_css.get('files', []))
return widgets, configs, js, css
def _should_show_wiki_widget(node, user):
has_wiki = bool(node.get_addon('wiki'))
wiki_page = WikiVersion.objects.get_for_node(node, 'home')
if node.has_permission(user, WRITE) and not node.is_registration:
return has_wiki
else:
return has_wiki and wiki_page and wiki_page.html(node)
def _view_project(node, auth, primary=False,
embed_contributors=False, embed_descendants=False,
embed_registrations=False, embed_forks=False):
"""Build a JSON object containing everything needed to render
project.view.mako.
"""
node = AbstractNode.objects.filter(pk=node.pk).include('contributor__user__guids').get()
if node.is_deleted:
raise HTTPError(http_status.HTTP_410_GONE)
user = auth.user
if mapcore_sync_is_enabled():
try:
mapcore_sync_rdm_project_or_map_group(auth.user, node)
except MAPCoreException as e:
# Do not call redirect() here
if settings.DEBUG_MODE:
import traceback
emsg = '<pre>{}</pre>'.format(
traceback.format_exc())
else:
emsg = str(e)
mapcore_log_error('{}: {}'.format(
e.__class__.__name__, emsg))
raise HTTPError(http_status.HTTP_503_SERVICE_UNAVAILABLE, data={
'message_short': 'mAP Core API Error',
'message_long': emsg
})
else:
# for CloudGateway API v1
if node.group is not None and user.cggroups_sync is not None \
and not user.cggroups_sync.filter(name=node.group.name).exists():
from nii import project_sync
project_sync.project_sync_one(node, None)
user.cggroups_sync.add(node.group) # checked
user.save()
in_bookmark_collection = False
bookmark_collection_id = ''
parent = node.find_readable_antecedent(auth)
if user:
bookmark_collection = find_bookmark_collection(user)
if bookmark_collection:
bookmark_collection_id = bookmark_collection._id
in_bookmark_collection = bookmark_collection.guid_links.filter(_id=node._id).exists()
view_only_link = auth.private_key or request.args.get('view_only', '').strip('/')
anonymous = has_anonymous_link(node, auth)
addons = list(node.get_addons())
widgets, configs, js, css = _render_addons(addons)
redirect_url = node.url + '?view_only=None'
disapproval_link = ''
if (node.is_pending_registration and node.has_permission(user, ADMIN)):
disapproval_link = node.root.registration_approval.stashed_urls.get(user._id, {}).get('reject', '')
if (node.is_pending_embargo and node.has_permission(user, ADMIN)):
disapproval_link = node.root.embargo.stashed_urls.get(user._id, {}).get('reject', '')
# Before page load callback; skip if not primary call
if primary:
for addon in addons:
messages = addon.before_page_load(node, user) or []
for message in messages:
status.push_status_message(message, kind='info', dismissible=False, trust=True)
NodeRelation = apps.get_model('osf.NodeRelation')
max_quota, used_quota = quota.get_quota_info(
node.creator, quota.get_project_storage_type(node)
)
is_registration = node.is_registration
timestamp_pattern = get_timestamp_pattern_division(auth, node)
data = {
'node': {
'disapproval_link': disapproval_link,
'id': node._primary_key,
'title': sanitize.unescape_entities(node.title),
'category': node.category_display,
'category_short': node.category,
'used_quota': used_quota,
'max_quota': max_quota * api_settings.SIZE_UNIT_GB,
'threshold': api_settings.WARNING_THRESHOLD,
'node_type': node.project_or_component,
'description': node.description or '',
'license': serialize_node_license_record(node.license),
'url': node.url,
'api_url': node.api_url,
'absolute_url': node.absolute_url,
'redirect_url': redirect_url,
'display_absolute_url': node.display_absolute_url,
'update_url': node.api_url_for('update_node'),
'in_dashboard': in_bookmark_collection,
'is_public': node.is_public,
'is_archiving': node.archiving,
'date_created': iso8601format(node.created),
'date_modified': iso8601format(node.last_logged) if node.last_logged else '',
'tags': list(node.tags.filter(system=False).values_list('name', flat=True)),
'children': node.nodes_active.exists(),
'child_exists': Node.objects.get_children(node, active=True).exists(),
'is_supplemental_project': node.has_linked_published_preprints,
'is_registration': is_registration,
'is_pending_registration': node.is_pending_registration if is_registration else False,
'is_retracted': node.is_retracted if is_registration else False,
'is_pending_retraction': node.is_pending_retraction if is_registration else False,
'retracted_justification': getattr(node.root.retraction, 'justification', None) if is_registration else None,
'date_retracted': iso8601format(getattr(node.root.retraction, 'date_retracted', None)) if is_registration else '',
'embargo_end_date': node.embargo_end_date.strftime('%A, %b %d, %Y') if is_registration and node.embargo_end_date else '',
'is_pending_embargo': node.is_pending_embargo if is_registration else False,
'is_embargoed': node.is_embargoed if is_registration else False,
'is_pending_embargo_termination': is_registration and node.is_pending_embargo_termination,
'registered_from_url': node.registered_from.url if is_registration else '',
'registered_date': iso8601format(node.registered_date) if is_registration else '',
'root_id': node.root._id if node.root else None,
'registered_meta': strip_registered_meta_comments(node.registered_meta),
'registered_schemas': serialize_meta_schemas(list(node.registered_schema.all())) if is_registration else False,
'is_fork': node.is_fork,
'is_collected': node.is_collected,
'collections': serialize_collections(node.collecting_metadata_list, auth),
'forked_from_id': node.forked_from._primary_key if node.is_fork else '',
'forked_from_display_absolute_url': node.forked_from.display_absolute_url if node.is_fork else '',
'forked_date': iso8601format(node.forked_date) if node.is_fork else '',
'fork_count': node.forks.exclude(type='osf.registration').filter(is_deleted=False).count(),
'private_links': [x.to_json() for x in node.private_links_active],
'link': view_only_link,
'templated_count': node.templated_list.count(),
'linked_nodes_count': NodeRelation.objects.filter(child=node, is_node_link=True).exclude(parent__type='osf.collection').count(),
'anonymous': anonymous,
'comment_level': node.comment_level,
'has_comments': node.comment_set.exists(),
'identifiers': {
'doi': node.get_identifier_value('doi'),
'ark': node.get_identifier_value('ark'),
},
'visible_preprints': serialize_preprints(node, user),
'institutions': get_affiliated_institutions(node) if node else [],
'has_draft_registrations': node.has_active_draft_registrations,
'timestamp_pattern_division': timestamp_pattern,
'access_requests_enabled': node.access_requests_enabled,
'storage_location': node.osfstorage_region.name,
'waterbutler_url': node.osfstorage_region.waterbutler_url,
'mfr_url': node.osfstorage_region.mfr_url,
'groups': list(node.osf_groups.values_list('name', flat=True)),
},
'parent_node': {
'exists': parent is not None,
'id': parent._primary_key if parent else '',
'title': parent.title if parent else '',
'category': parent.category_display if parent else '',
'url': parent.url if parent else '',
'api_url': parent.api_url if parent else '',
'absolute_url': parent.absolute_url if parent else '',
'registrations_url': parent.web_url_for('node_registrations', _guid=True) if parent else '',
'is_public': parent.is_public if parent else '',
'is_contributor_or_group_member': parent.is_contributor_or_group_member(user) if parent else '',
'is_contributor': parent.is_contributor(user) if parent else '',
'can_view': parent.can_view(auth) if parent else False,
},
'user': {
'is_contributor_or_group_member': node.is_contributor_or_group_member(user),
'is_contributor': node.is_contributor(user),
'is_admin': node.has_permission(user, ADMIN),
'is_admin_parent_contributor': parent.is_admin_parent(user, include_group_admin=False) if parent else False,
'is_admin_parent_contributor_or_group_member': parent.is_admin_parent(user) if parent else False,
'can_edit': node.has_permission(user, WRITE),
'can_edit_tags': node.has_permission(user, WRITE),
'has_read_permissions': node.has_permission(user, READ),
'permissions': node.get_permissions(user) if user else [],
'id': user._id if user else None,
'username': user.username if user else None,
'fullname': user.fullname if user else '',
'can_comment': node.can_comment(auth),
'show_wiki_widget': _should_show_wiki_widget(node, user),
'dashboard_id': bookmark_collection_id,
'institutions': get_affiliated_institutions(user) if user else [],
},
# TODO: Namespace with nested dicts
'addons_enabled': [each.short_name for each in addons],
'addons': configs,
'addon_widgets': widgets,
'addon_widget_js': js,
'addon_widget_css': css,
'node_categories': [
{'value': key, 'display_name': value}
for key, value in list(settings.NODE_CATEGORY_MAP.items())
]
}
# Default should be at top of list for UI and for the project overview page the default region
# for a component is that of the it's parent node.
region_list = get_storage_region_list(user, node=node)
data.update({'storage_regions': region_list})
data.update({'storage_flag_is_active': storage_i18n_flag_active()})
if storage_usage_flag_active():
storage_usage = node.storage_usage
if storage_usage:
data['node']['storage_usage'] = sizeof_fmt(storage_usage)
if embed_contributors and not anonymous:
data['node']['contributors'] = utils.serialize_visible_contributors(node)
else:
data['node']['contributors'] = list(node.contributors.values_list('guids___id', flat=True))
if embed_descendants:
descendants, all_readable = _get_readable_descendants(auth=auth, node=node)
data['user']['can_sort'] = all_readable
data['node']['descendants'] = [
serialize_node_summary(node=each, auth=auth, primary=not node.has_node_link_to(each), show_path=False)
for each in descendants
]
if embed_registrations:
data['node']['registrations'] = [
serialize_node_summary(node=each, auth=auth, show_path=False)
for each in node.registrations_all.order_by('-registered_date').exclude(is_deleted=True)
]
if embed_forks:
data['node']['forks'] = [
serialize_node_summary(node=each, auth=auth, show_path=False)
for each in node.forks.exclude(type='osf.registration').exclude(is_deleted=True).order_by('-forked_date')
]
return data
def get_affiliated_institutions(obj):
ret = []
for institution in obj.affiliated_institutions.all():
ret.append({
'name': institution.name,
'logo_path': institution.logo_path,