class DraftRegistrationSerializer(JSONAPISerializer): id = IDField(source='_id', read_only=True) type = TypeField() registration_supplement = ser.CharField(source='registration_schema._id', required=True) registration_metadata = ser.DictField(required=False) datetime_initiated = DateByVersion(read_only=True) datetime_updated = DateByVersion(read_only=True) branched_from = RelationshipField( related_view='nodes:node-detail', related_view_kwargs={'node_id': '<branched_from._id>'}) initiator = RelationshipField( related_view='users:user-detail', related_view_kwargs={'user_id': '<initiator._id>'}, ) registration_schema = RelationshipField( related_view='metaschemas:metaschema-detail', related_view_kwargs={'metaschema_id': '<registration_schema._id>'}) links = LinksField({'html': 'get_absolute_url'}) def get_absolute_url(self, obj): return obj.absolute_url def create(self, validated_data): node = validated_data.pop('node') initiator = validated_data.pop('initiator') metadata = validated_data.pop('registration_metadata', None) schema_id = validated_data.pop('registration_schema').get('_id') schema = get_object_or_error(MetaSchema, schema_id, self.context['request']) if schema.schema_version != LATEST_SCHEMA_VERSION or not schema.active: raise exceptions.ValidationError( 'Registration supplement must be an active schema.') draft = DraftRegistration.create_from_node(node=node, user=initiator, schema=schema) reviewer = is_prereg_admin_not_project_admin(self.context['request'], draft) if metadata: try: # Required fields are only required when creating the actual registration, not updating the draft. draft.validate_metadata(metadata=metadata, reviewer=reviewer, required_fields=False) except ValidationError as e: raise exceptions.ValidationError(e.message) draft.update_metadata(metadata) draft.save() return draft class Meta: type_ = 'draft_registrations'
class NodeForksSerializer(NodeSerializer): category_choices = settings.NODE_CATEGORY_MAP.items() category_choices_string = ', '.join( ["'{}'".format(choice[0]) for choice in category_choices]) title = ser.CharField(required=False) category = ser.ChoiceField(read_only=True, choices=category_choices, help_text='Choices: ' + category_choices_string) forked_date = DateByVersion(read_only=True) def create(self, validated_data): node = validated_data.pop('node') fork_title = validated_data.pop('title', None) request = self.context['request'] auth = get_user_auth(request) fork = node.fork_node(auth, title=fork_title) try: fork.save() except ValidationError as e: raise InvalidModelValueError(detail=e.message) return fork
class NodeLogSerializer(JSONAPISerializer): filterable_fields = frozenset(['action', 'date']) non_anonymized_fields = [ 'id', 'date', 'action', ] id = ser.CharField(read_only=True, source='_id') date = DateByVersion(read_only=True) action = ser.CharField(read_only=True) params = ser.SerializerMethodField(read_only=True) links = LinksField({'self': 'get_absolute_url'}) class Meta: type_ = 'logs' node = RelationshipField( related_view=lambda n: 'registrations:registration-detail' if getattr(n, 'is_registration', False) else 'nodes:node-detail', related_view_kwargs={'node_id': '<node._id>'}, ) original_node = RelationshipField( related_view=lambda n: 'registrations:registration-detail' if getattr(n, 'is_registration', False) else 'nodes:node-detail', related_view_kwargs={'node_id': '<original_node._id>'}, ) user = RelationshipField( related_view='users:user-detail', related_view_kwargs={'user_id': '<user._id>'}, ) # This would be a node_link, except that data isn't stored in the node log params linked_node = HideIfNotNodePointerLog( RelationshipField( related_view='nodes:node-detail', related_view_kwargs={'node_id': '<params.pointer.id>'})) linked_registration = HideIfNotRegistrationPointerLog( RelationshipField( related_view='registrations:registration-detail', related_view_kwargs={'node_id': '<params.pointer.id>'})) template_node = RelationshipField( related_view='nodes:node-detail', related_view_kwargs={'node_id': '<params.template_node.id>'}) def get_absolute_url(self, obj): return obj.absolute_url def get_params(self, obj): if obj.action == 'osf_storage_folder_created' and obj.params.get( 'urls'): obj.params.pop('urls') return NodeLogParamsSerializer(obj.params, context=self.context, read_only=True).data
class FileVersionSerializer(JSONAPISerializer): filterable_fields = frozenset([ 'id', 'size', 'identifier', 'content_type', ]) id = ser.CharField(read_only=True, source='identifier') size = ser.IntegerField(read_only=True, help_text='The size of this file at this version') content_type = ser.CharField(read_only=True, help_text='The mime type of this file at this verison') date_created = DateByVersion(source='created', read_only=True, help_text='The date that this version was created') links = LinksField({ 'self': 'self_url', 'html': 'absolute_url' }) class Meta: type_ = 'file_versions' def self_url(self, obj): return absolute_reverse('files:version-detail', kwargs={ 'version_id': obj.identifier, 'file_id': self.context['view'].kwargs['file_id'], 'version': self.context['request'].parser_context['kwargs']['version'] }) def absolute_url(self, obj): fobj = self.context['view'].get_file() return furl.furl(settings.DOMAIN).set( path=(fobj.node._id, 'files', fobj.provider, fobj.path.lstrip('/')), query={fobj.version_identifier: obj.identifier} # TODO this can probably just be changed to revision or version ).url def get_absolute_url(self, obj): return self.self_url(obj)
class RegistrationSerializer(BaseRegistrationSerializer): """ Overrides BaseRegistrationSerializer to add draft_registration, registration_choice, and lift_embargo fields """ draft_registration = ser.CharField(write_only=True) registration_choice = ser.ChoiceField(write_only=True, choices=['immediate', 'embargo']) lift_embargo = DateByVersion(write_only=True, default=None, input_formats=['%Y-%m-%dT%H:%M:%S'])
class ViewOnlyLinkDetailSerializer(JSONAPISerializer): key = ser.CharField(read_only=True) id = IDField(source='_id', read_only=True) date_created = DateByVersion(read_only=True) anonymous = ser.BooleanField(required=False) name = ser.CharField(required=False) nodes = RelationshipField( related_view='view-only-links:view-only-link-nodes', related_view_kwargs={'link_id': '<_id>'}, self_view='view-only-links:view-only-link-nodes-relationships', self_view_kwargs={'link_id': '<_id>'}) creator = RelationshipField( related_view='users:user-detail', related_view_kwargs={'user_id': '<creator._id>'}, ) def get_absolute_url(self, obj): return absolute_reverse( 'nodes:node-view-only-link-detail', kwargs={ 'link_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version'] }) class Meta: type_ = 'view-only-links'
class NodeViewOnlyLinkSerializer(JSONAPISerializer): filterable_fields = frozenset([ 'anonymous', 'name', 'date_created' ]) key = ser.CharField(read_only=True) id = IDField(source='_id', read_only=True) date_created = DateByVersion(read_only=True) anonymous = ser.BooleanField(required=False, default=False) name = ser.CharField(required=False, default='Shared project link') links = LinksField({ 'self': 'get_absolute_url' }) creator = RelationshipField( related_view='users:user-detail', related_view_kwargs={'user_id': '<creator._id>'}, ) nodes = RelationshipField( related_view='view-only-links:view-only-link-nodes', related_view_kwargs={'link_id': '<_id>'}, self_view='view-only-links:view-only-link-nodes-relationships', self_view_kwargs={'link_id': '<_id>'} ) def create(self, validated_data): name = validated_data.pop('name') user = get_user_auth(self.context['request']).user anonymous = validated_data.pop('anonymous') node = self.context['view'].get_node() try: view_only_link = new_private_link( name=name, user=user, nodes=[node], anonymous=anonymous ) except ValidationValueError: raise exceptions.ValidationError('Invalid link name.') return view_only_link def get_absolute_url(self, obj): return absolute_reverse( 'nodes:node-view-only-link-detail', kwargs={ 'link_id': obj._id, 'node_id': self.context['request'].parser_context['kwargs']['node_id'], 'version': self.context['request'].parser_context['kwargs']['version'] } ) class Meta: type_ = 'view_only_links'
class ApiOAuth2ApplicationSerializer(ApiOAuthApplicationBaseSerializer): """Serialize data about a registered OAuth2 application""" id = IDField( source='client_id', read_only=True, help_text='The client ID for this application (automatically generated)' ) type = TypeField() name = ser.CharField( help_text='A short, descriptive name for this application', required=True) description = ser.CharField( help_text= 'An optional description displayed to all users of this application', required=False, allow_blank=True) home_url = ser.CharField( help_text="The full URL to this application's homepage.", required=True, validators=[URLValidator()], label='Home URL') callback_url = ser.CharField( help_text= 'The callback URL for this application (refer to OAuth documentation)', required=True, validators=[URLValidator()], label='Callback URL') owner = ser.CharField( help_text='The id of the user who owns this application', read_only= True, # Don't let user register an application in someone else's name source='owner._id') date_created = DateByVersion( help_text= 'The date this application was generated (automatically filled in)', read_only=True) def create(self, validated_data): instance = ApiOAuth2Application(**validated_data) instance.save() return instance def update(self, instance, validated_data): assert isinstance( instance, ApiOAuth2Application), 'instance must be an ApiOAuth2Application' for attr, value in validated_data.iteritems(): setattr(instance, attr, value) instance.save() return instance
class CitationSerializer(JSONAPISerializer): filterable_fields = frozenset(['title', 'short_title', 'summary', 'id']) id = ser.CharField(source='_id', required=True) title = ser.CharField(max_length=200) date_parsed = DateByVersion( read_only=True, help_text='Datetime the csl file was last parsed') short_title = ser.CharField(max_length=500) summary = ser.CharField(max_length=200) def get_absolute_url(self, obj): return obj.get_absolute_url() class Meta: type_ = 'citation-styles'
class NodeLogSerializer(JSONAPISerializer): filterable_fields = frozenset(['action', 'date']) non_anonymized_fields = [ 'id', 'date', 'action', ] id = ser.CharField(read_only=True, source='_id') date = DateByVersion(read_only=True) action = ser.CharField(read_only=True) params = NodeLogParamsSerializer(read_only=True) links = LinksField({'self': 'get_absolute_url'}) class Meta: type_ = 'logs' node = RelationshipField( related_view=lambda n: 'registrations:registration-detail' if getattr(n, 'is_registration', False) else 'nodes:node-detail', related_view_kwargs={'node_id': '<node._id>'}, ) original_node = RelationshipField( related_view=lambda n: 'registrations:registration-detail' if getattr(n, 'is_registration', False) else 'nodes:node-detail', related_view_kwargs={'node_id': '<original_node._id>'}, ) user = RelationshipField( related_view='users:user-detail', related_view_kwargs={'user_id': '<user._id>'}, ) # This would be a node_link, except that data isn't stored in the node log params linked_node = RelationshipField( related_view='nodes:node-detail', related_view_kwargs={'node_id': '<params.pointer.id>'} ) template_node = RelationshipField( related_view='nodes:node-detail', related_view_kwargs={'node_id': '<params.template_node.id>'} ) def get_absolute_url(self, obj): return obj.absolute_url
class PreprintSerializer(JSONAPISerializer): filterable_fields = frozenset([ 'id', 'date_created', 'date_modified', 'date_published', 'provider', 'is_published', ]) id = IDField(source='_id', read_only=True) subjects = JSONAPIListField(child=JSONAPIListField(child=TaxonomyField()), allow_null=True, required=False) date_created = DateByVersion(read_only=True) date_modified = DateByVersion(read_only=True) date_published = DateByVersion(read_only=True) doi = ser.CharField(source='article_doi', required=False, allow_null=True) is_published = ser.BooleanField(required=False) is_preprint_orphan = ser.BooleanField(read_only=True) license_record = NodeLicenseSerializer(required=False, source='license') node = NodeRelationshipField(related_view='nodes:node-detail', related_view_kwargs={'node_id': '<node._id>'}, read_only=False) license = PreprintLicenseRelationshipField( related_view='licenses:license-detail', related_view_kwargs={'license_id': '<license.node_license._id>'}, read_only=False) provider = PreprintProviderRelationshipField( related_view='preprint_providers:preprint_provider-detail', related_view_kwargs={'provider_id': '<provider._id>'}, read_only=False) primary_file = PrimaryFileRelationshipField( related_view='files:file-detail', related_view_kwargs={'file_id': '<primary_file._id>'}, lookup_url_kwarg='file_id', read_only=False) links = LinksField({ 'self': 'get_preprint_url', 'html': 'get_absolute_html_url', 'doi': 'get_doi_url' }) class Meta: type_ = 'preprints' def get_preprint_url(self, obj): return absolute_reverse( 'preprints:preprint-detail', kwargs={ 'preprint_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version'] }) def get_absolute_url(self, obj): return self.get_preprint_url(obj) def get_doi_url(self, obj): return 'https://dx.doi.org/{}'.format( obj.article_doi) if obj.article_doi else None def update(self, preprint, validated_data): assert isinstance( preprint, PreprintService), 'You must specify a valid preprint to be updated' assert isinstance( preprint.node, Node ), 'You must specify a preprint with a valid node to be updated.' auth = get_user_auth(self.context['request']) if not preprint.node.has_permission(auth.user, 'admin'): raise exceptions.PermissionDenied( detail='User must be an admin to update a preprint.') save_node = False save_preprint = False recently_published = False primary_file = validated_data.pop('primary_file', None) if primary_file: self.set_field(preprint.set_primary_file, primary_file, auth) save_node = True if 'subjects' in validated_data: subjects = validated_data.pop('subjects', None) self.set_field(preprint.set_subjects, subjects, auth) save_preprint = True if 'article_doi' in validated_data: preprint.node.preprint_article_doi = validated_data['article_doi'] save_node = True published = validated_data.pop('is_published', None) if published is not None: self.set_field(preprint.set_published, published, auth) save_preprint = True recently_published = published if 'license_type' in validated_data or 'license' in validated_data: license_details = get_license_details(preprint, validated_data) self.set_field(preprint.set_preprint_license, license_details, auth) save_preprint = True if save_node: try: preprint.node.save() except ValidationValueError as e: # Raised from invalid DOI raise exceptions.ValidationError(detail=e.message) if save_preprint: preprint.save() # Send preprint confirmation email signal to new authors on preprint! -- only when published # TODO: Some more thought might be required on this; preprints made from existing # nodes will send emails making it seem like a new node. if recently_published: for author in preprint.node.contributors: if author != auth.user: project_signals.contributor_added.send( preprint.node, contributor=author, auth=auth, email_template='preprint') return preprint def set_field(self, func, val, auth, save=False): try: func(val, auth, save=save) except PermissionsError as e: raise exceptions.PermissionDenied(detail=e.message) except ValueError as e: raise exceptions.ValidationError(detail=e.message) except NodeStateError as e: raise exceptions.ValidationError(detail=e.message)
class NodeSerializer(JSONAPISerializer): # TODO: If we have to redo this implementation in any of the other serializers, subclass ChoiceField and make it # handle blank choices properly. Currently DRF ChoiceFields ignore blank options, which is incorrect in this # instance filterable_fields = frozenset([ 'id', 'title', 'description', 'public', 'tags', 'category', 'date_created', 'date_modified', 'root', 'parent', 'contributors', 'preprint' ]) non_anonymized_fields = [ 'id', 'title', 'description', 'category', 'date_created', 'date_modified', 'registration', 'tags', 'public', 'license', 'links', 'children', 'comments', 'contributors', 'files', 'node_links', 'parent', 'root', 'logs', 'wikis' ] id = IDField(source='_id', read_only=True) type = TypeField() category_choices = settings.NODE_CATEGORY_MAP.items() category_choices_string = ', '.join( ["'{}'".format(choice[0]) for choice in category_choices]) title = ser.CharField(required=True) description = ser.CharField(required=False, allow_blank=True, allow_null=True) category = ser.ChoiceField(choices=category_choices, help_text='Choices: ' + category_choices_string) date_created = DateByVersion(source='created', read_only=True) date_modified = DateByVersion(source='last_logged', read_only=True) registration = ser.BooleanField(read_only=True, source='is_registration') preprint = ser.BooleanField(read_only=True, source='is_preprint') fork = ser.BooleanField(read_only=True, source='is_fork') collection = ser.BooleanField(read_only=True, source='is_collection') tags = JSONAPIListField(child=NodeTagField(), required=False) node_license = NodeLicenseSerializer(required=False, source='license') template_from = ser.CharField( required=False, allow_blank=False, allow_null=False, help_text= 'Specify a node id for a node you would like to use as a template for the ' 'new node. Templating is like forking, except that you do not copy the ' 'files, only the project structure. Some information is changed on the top ' 'level project by submitting the appropriate fields in the request body, ' 'and some information will not change. By default, the description will ' 'be cleared and the project will be made private.') current_user_can_comment = ser.SerializerMethodField( help_text='Whether the current user is allowed to post comments') current_user_permissions = ser.SerializerMethodField( help_text='List of strings representing the permissions ' 'for the current user on this node.') # Public is only write-able by admins--see update method public = ser.BooleanField( source='is_public', required=False, help_text='Nodes that are made public will give read-only access ' 'to everyone. Private nodes require explicit read ' 'permission. Write and admin access are the same for ' 'public and private nodes. Administrators on a parent ' 'node have implicit read permissions for all child nodes') links = LinksField({'html': 'get_absolute_html_url'}) # TODO: When we have osf_permissions.ADMIN permissions, make this writable for admins license = NodeLicenseRelationshipField( related_view='licenses:license-detail', related_view_kwargs={'license_id': '<license.node_license._id>'}, read_only=False) children = RelationshipField( related_view='nodes:node-children', related_view_kwargs={'node_id': '<_id>'}, related_meta={'count': 'get_node_count'}, ) comments = RelationshipField( related_view='nodes:node-comments', related_view_kwargs={'node_id': '<_id>'}, related_meta={'unread': 'get_unread_comments_count'}, filter={'target': '<_id>'}) contributors = RelationshipField( related_view='nodes:node-contributors', related_view_kwargs={'node_id': '<_id>'}, related_meta={'count': 'get_contrib_count'}, ) files = RelationshipField(related_view='nodes:node-providers', related_view_kwargs={'node_id': '<_id>'}) wikis = RelationshipField(related_view='nodes:node-wikis', related_view_kwargs={'node_id': '<_id>'}) forked_from = RelationshipField( related_view=lambda n: 'registrations:registration-detail' if getattr(n, 'is_registration', False) else 'nodes:node-detail', related_view_kwargs={'node_id': '<forked_from_guid>'}) template_node = RelationshipField( related_view='nodes:node-detail', related_view_kwargs={'node_id': '<template_node._id>'}) forks = RelationshipField(related_view='nodes:node-forks', related_view_kwargs={'node_id': '<_id>'}) node_links = ShowIfVersion(RelationshipField( related_view='nodes:node-pointers', related_view_kwargs={'node_id': '<_id>'}, related_meta={'count': 'get_pointers_count'}, help_text= 'This feature is deprecated as of version 2.1. Use linked_nodes instead.' ), min_version='2.0', max_version='2.0') parent = RelationshipField( related_view='nodes:node-detail', related_view_kwargs={'node_id': '<parent_node._id>'}, filter_key='parent_node') identifiers = RelationshipField(related_view='nodes:identifier-list', related_view_kwargs={'node_id': '<_id>'}) draft_registrations = HideIfRegistration( RelationshipField(related_view='nodes:node-draft-registrations', related_view_kwargs={'node_id': '<_id>'})) registrations = HideIfRegistration( RelationshipField(related_view='nodes:node-registrations', related_view_kwargs={'node_id': '<_id>'}, related_meta={'count': 'get_registration_count'})) affiliated_institutions = RelationshipField( related_view='nodes:node-institutions', related_view_kwargs={'node_id': '<_id>'}, self_view='nodes:node-relationships-institutions', self_view_kwargs={'node_id': '<_id>'}) root = RelationshipField(related_view='nodes:node-detail', related_view_kwargs={'node_id': '<root._id>'}) logs = RelationshipField(related_view='nodes:node-logs', related_view_kwargs={'node_id': '<_id>'}, related_meta={'count': 'get_logs_count'}) linked_nodes = RelationshipField( related_view='nodes:linked-nodes', related_view_kwargs={'node_id': '<_id>'}, related_meta={'count': 'get_node_links_count'}, self_view='nodes:node-pointer-relationship', self_view_kwargs={'node_id': '<_id>'}, self_meta={'count': 'get_node_links_count'}) linked_registrations = RelationshipField( related_view='nodes:linked-registrations', related_view_kwargs={'node_id': '<_id>'}, related_meta={'count': 'get_registration_links_count'}, self_view='nodes:node-registration-pointer-relationship', self_view_kwargs={'node_id': '<_id>'}, self_meta={'count': 'get_node_links_count'}) view_only_links = RelationshipField( related_view='nodes:node-view-only-links', related_view_kwargs={'node_id': '<_id>'}, ) citation = RelationshipField(related_view='nodes:node-citation', related_view_kwargs={'node_id': '<_id>'}) preprints = HideIfRegistration( RelationshipField(related_view='nodes:node-preprints', related_view_kwargs={'node_id': '<_id>'})) def get_current_user_permissions(self, obj): user = self.context['request'].user if user.is_anonymous: return ['read'] permissions = obj.get_permissions(user=user) if not permissions: permissions = ['read'] return permissions def get_current_user_can_comment(self, obj): user = self.context['request'].user auth = Auth(user if not user.is_anonymous else None) return obj.can_comment(auth) class Meta: type_ = 'nodes' def get_absolute_url(self, obj): return obj.get_absolute_url() # TODO: See if we can get the count filters into the filter rather than the serializer. def get_logs_count(self, obj): return obj.logs.count() def get_node_count(self, obj): auth = get_user_auth(self.context['request']) user_id = getattr(auth.user, 'id', None) with connection.cursor() as cursor: cursor.execute( ''' WITH RECURSIVE parents AS ( SELECT parent_id, child_id FROM osf_noderelation WHERE child_id = %s AND is_node_link IS FALSE UNION ALL SELECT osf_noderelation.parent_id, parents.parent_id AS child_id FROM parents JOIN osf_noderelation ON parents.PARENT_ID = osf_noderelation.child_id WHERE osf_noderelation.is_node_link IS FALSE ), has_admin AS (SELECT * FROM osf_contributor WHERE (node_id IN (SELECT parent_id FROM parents) OR node_id = %s) AND user_id = %s AND admin IS TRUE LIMIT 1) SELECT DISTINCT COUNT(child_id) FROM osf_noderelation JOIN osf_abstractnode ON osf_noderelation.child_id = osf_abstractnode.id JOIN osf_contributor ON osf_abstractnode.id = osf_contributor.node_id LEFT JOIN osf_privatelink_nodes ON osf_abstractnode.id = osf_privatelink_nodes.abstractnode_id LEFT JOIN osf_privatelink ON osf_privatelink_nodes.privatelink_id = osf_privatelink.id WHERE parent_id = %s AND is_node_link IS FALSE AND osf_abstractnode.is_deleted IS FALSE AND ( osf_abstractnode.is_public OR (TRUE IN (SELECT TRUE FROM has_admin)) OR (osf_contributor.user_id = %s AND osf_contributor.read IS TRUE) OR (osf_privatelink.key = %s AND osf_privatelink.is_deleted = FALSE) ); ''', [obj.id, obj.id, user_id, obj.id, user_id, auth.private_key]) return int(cursor.fetchone()[0]) def get_contrib_count(self, obj): return len(obj.contributors) def get_registration_count(self, obj): auth = get_user_auth(self.context['request']) registrations = [ node for node in obj.registrations_all if node.can_view(auth) ] return len(registrations) def get_pointers_count(self, obj): return obj.linked_nodes.count() def get_node_links_count(self, obj): count = 0 auth = get_user_auth(self.context['request']) for pointer in obj.linked_nodes.filter(is_deleted=False).exclude( type='osf.collection').exclude(type='osf.registration'): if pointer.can_view(auth): count += 1 return count def get_registration_links_count(self, obj): count = 0 auth = get_user_auth(self.context['request']) for pointer in obj.linked_nodes.filter( is_deleted=False, type='osf.registration').exclude(type='osf.collection'): if pointer.can_view(auth): count += 1 return count def get_unread_comments_count(self, obj): user = get_user_auth(self.context['request']).user node_comments = Comment.find_n_unread(user=user, node=obj, page='node') return {'node': node_comments} def create(self, validated_data): request = self.context['request'] user = request.user Node = apps.get_model('osf.Node') tag_instances = [] if 'tags' in validated_data: tags = validated_data.pop('tags') for tag in tags: tag_instance, created = Tag.objects.get_or_create( name=tag, defaults=dict(system=False)) tag_instances.append(tag_instance) if 'template_from' in validated_data: template_from = validated_data.pop('template_from') template_node = Node.load(template_from) if template_node is None: raise exceptions.NotFound if not template_node.has_permission( user, 'read', check_parent=False): raise exceptions.PermissionDenied validated_data.pop('creator') changed_data = {template_from: validated_data} node = template_node.use_as_template(auth=get_user_auth(request), changes=changed_data) else: node = Node(**validated_data) try: node.save() except ValidationError as e: raise InvalidModelValueError(detail=e.messages[0]) if len(tag_instances): for tag in tag_instances: node.tags.add(tag) if is_truthy(request.GET.get('inherit_contributors') ) and validated_data['parent'].has_permission( user, 'write'): auth = get_user_auth(request) parent = validated_data['parent'] contributors = [] for contributor in parent.contributor_set.exclude(user=user): contributors.append({ 'user': contributor.user, 'permissions': parent.get_permissions(contributor.user), 'visible': contributor.visible }) if not contributor.user.is_registered: node.add_unregistered_contributor( fullname=contributor.user.fullname, email=contributor.user.email, auth=auth, permissions=parent.get_permissions(contributor.user), existing_user=contributor.user) node.add_contributors(contributors, auth=auth, log=True, save=True) return node def update(self, node, validated_data): """Update instance with the validated data. Requires the request to be in the serializer context. """ assert isinstance(node, AbstractNode), 'node must be a Node' auth = get_user_auth(self.context['request']) # Update tags if 'tags' in validated_data: new_tags = set(validated_data.pop('tags', [])) node.update_tags(new_tags, auth=auth) if validated_data: if 'license_type' in validated_data or 'license' in validated_data: license_details = get_license_details(node, validated_data) validated_data['node_license'] = license_details try: node.update(validated_data, auth=auth) except ValidationError as e: raise InvalidModelValueError(detail=e.message) except PermissionsError: raise exceptions.PermissionDenied except NodeUpdateError as e: raise exceptions.ValidationError(detail=e.reason) except NodeStateError as e: raise InvalidModelValueError(detail=e.message) return node
class PreprintSerializer(JSONAPISerializer): filterable_fields = frozenset([ 'id', 'date_created', 'date_modified', 'date_published', 'original_publication_date', 'provider', 'is_published', 'subjects', 'reviews_state', 'node_is_public', ]) id = IDField(source='_id', read_only=True) subjects = ser.SerializerMethodField() date_created = DateByVersion(read_only=True) date_modified = DateByVersion(read_only=True) date_published = DateByVersion(read_only=True) original_publication_date = DateByVersion(required=False) doi = ser.CharField(source='article_doi', required=False, allow_null=True) is_published = ser.BooleanField(required=False) is_preprint_orphan = ser.BooleanField(read_only=True) license_record = NodeLicenseSerializer(required=False, source='license') title = ser.CharField(source='node.title', required=False) description = ser.CharField(required=False, allow_blank=True, allow_null=True, source='node.description') tags = JSONAPIListField(child=NodeTagField(), required=False, source='node.tags') node_is_public = ser.BooleanField(read_only=True, source='node__is_public') contributors = RelationshipField( related_view='nodes:node-contributors', related_view_kwargs={'node_id': '<node._id>'}, ) reviews_state = ser.CharField(read_only=True, max_length=15) date_last_transitioned = DateByVersion(read_only=True) citation = RelationshipField(related_view='preprints:preprint-citation', related_view_kwargs={'preprint_id': '<_id>'}) identifiers = RelationshipField( related_view='preprints:identifier-list', related_view_kwargs={'preprint_id': '<_id>'}) node = NodeRelationshipField(related_view='nodes:node-detail', related_view_kwargs={'node_id': '<node._id>'}, read_only=False) license = PreprintLicenseRelationshipField( related_view='licenses:license-detail', related_view_kwargs={'license_id': '<license.node_license._id>'}, read_only=False) provider = PreprintProviderRelationshipField( related_view='preprint_providers:preprint_provider-detail', related_view_kwargs={'provider_id': '<provider._id>'}, read_only=False) files = RelationshipField(related_view='nodes:node-providers', related_view_kwargs={'node_id': '<_id>'}) primary_file = PrimaryFileRelationshipField( related_view='files:file-detail', related_view_kwargs={'file_id': '<primary_file._id>'}, lookup_url_kwarg='file_id', read_only=False) actions = RelationshipField(related_view='preprints:preprint-action-list', related_view_kwargs={'preprint_id': '<_id>'}) links = LinksField({ 'self': 'get_preprint_url', 'html': 'get_absolute_html_url', 'doi': 'get_article_doi_url', 'preprint_doi': 'get_preprint_doi_url' }) class Meta: type_ = 'preprints' def get_subjects(self, obj): return [[TaxonomyField().to_representation(subj) for subj in hier] for hier in obj.subject_hierarchy] def get_preprint_url(self, obj): return absolute_reverse( 'preprints:preprint-detail', kwargs={ 'preprint_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version'] }) def get_absolute_url(self, obj): return self.get_preprint_url(obj) def get_article_doi_url(self, obj): return 'https://dx.doi.org/{}'.format( obj.article_doi) if obj.article_doi else None def get_preprint_doi_url(self, obj): doi_identifier = obj.get_identifier('doi') return 'https://dx.doi.org/{}'.format( doi_identifier.value) if doi_identifier else None def run_validation(self, *args, **kwargs): # Overrides construtor for validated_data to allow writes to a SerializerMethodField # Validation for `subjects` happens in the model _validated_data = super(PreprintSerializer, self).run_validation(*args, **kwargs) if 'subjects' in self.initial_data: _validated_data['subjects'] = self.initial_data['subjects'] return _validated_data def update(self, preprint, validated_data): assert isinstance( preprint, PreprintService), 'You must specify a valid preprint to be updated' assert isinstance( preprint.node, Node ), 'You must specify a preprint with a valid node to be updated.' auth = get_user_auth(self.context['request']) if not preprint.node.has_permission(auth.user, 'admin'): raise exceptions.PermissionDenied( detail='User must be an admin to update a preprint.') published = validated_data.pop('is_published', None) if published and preprint.provider.is_reviewed: raise Conflict( '{} uses a moderation workflow, so preprints must be submitted for review instead of published directly. Submit a preprint by creating a `submit` Action at {}' .format( preprint.provider.name, absolute_reverse( 'actions:create-action', kwargs={ 'version': self.context['request'].parser_context['kwargs'] ['version'] }))) save_node = False save_preprint = False recently_published = False primary_file = validated_data.pop('primary_file', None) if primary_file: self.set_field(preprint.set_primary_file, primary_file, auth) save_node = True old_tags = set(preprint.node.tags.values_list('name', flat=True)) if validated_data.get('node') and 'tags' in validated_data['node']: current_tags = set(validated_data['node'].pop('tags', [])) elif self.partial: current_tags = set(old_tags) else: current_tags = set() for new_tag in (current_tags - old_tags): preprint.node.add_tag(new_tag, auth=auth) for deleted_tag in (old_tags - current_tags): preprint.node.remove_tag(deleted_tag, auth=auth) if 'node' in validated_data: preprint.node.update(fields=validated_data.pop('node')) save_node = True if 'subjects' in validated_data: subjects = validated_data.pop('subjects', None) self.set_field(preprint.set_subjects, subjects, auth) save_preprint = True if 'article_doi' in validated_data: preprint.node.preprint_article_doi = validated_data['article_doi'] save_node = True if 'license_type' in validated_data or 'license' in validated_data: license_details = get_license_details(preprint, validated_data) self.set_field(preprint.set_preprint_license, license_details, auth) save_preprint = True if 'original_publication_date' in validated_data: preprint.original_publication_date = validated_data[ 'original_publication_date'] save_preprint = True if published is not None: if not preprint.primary_file: raise exceptions.ValidationError( detail= 'A valid primary_file must be set before publishing a preprint.' ) self.set_field(preprint.set_published, published, auth) save_preprint = True recently_published = published preprint.node.set_privacy('public') save_node = True if save_node: try: preprint.node.save() except ValidationError as e: # Raised from invalid DOI raise exceptions.ValidationError(detail=e.messages[0]) if save_preprint: preprint.save() # Send preprint confirmation email signal to new authors on preprint! -- only when published # TODO: Some more thought might be required on this; preprints made from existing # nodes will send emails making it seem like a new node. if recently_published: for author in preprint.node.contributors: if author != auth.user: project_signals.contributor_added.send( preprint.node, contributor=author, auth=auth, email_template='preprint') return preprint def set_field(self, func, val, auth, save=False): try: func(val, auth) except PermissionsError as e: raise exceptions.PermissionDenied(detail=e.message) except (ValueError, ValidationError, NodeStateError) as e: raise exceptions.ValidationError(detail=e.message)
class CollectionSerializer(JSONAPISerializer): filterable_fields = frozenset([ 'title', 'date_created', 'date_modified', ]) id = IDField(source='_id', read_only=True) type = TypeField() title = ser.CharField(required=True) date_created = DateByVersion(read_only=True) date_modified = DateByVersion(read_only=True) bookmarks = ser.BooleanField(read_only=False, default=False, source='is_bookmark_collection') links = LinksField({}) node_links = RelationshipField( related_view='collections:node-pointers', related_view_kwargs={'collection_id': '<_id>'}, related_meta={'count': 'get_node_links_count'} ) # TODO: Add a self link to this when it's available linked_nodes = RelationshipField( related_view='collections:linked-nodes', related_view_kwargs={'collection_id': '<_id>'}, related_meta={'count': 'get_node_links_count'}, self_view='collections:collection-node-pointer-relationship', self_view_kwargs={'collection_id': '<_id>'} ) linked_registrations = RelationshipField( related_view='collections:linked-registrations', related_view_kwargs={'collection_id': '<_id>'}, related_meta={'count': 'get_registration_links_count'}, self_view='collections:collection-registration-pointer-relationship', self_view_kwargs={'collection_id': '<_id>'} ) class Meta: type_ = 'collections' def get_absolute_url(self, obj): return absolute_reverse('collections:collection-detail', kwargs={ 'collection_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version'] }) def get_node_links_count(self, obj): count = 0 auth = get_user_auth(self.context['request']) for pointer in obj.linked_nodes.filter(is_deleted=False, type='osf.node'): if pointer.can_view(auth): count += 1 return count def get_registration_links_count(self, obj): count = 0 auth = get_user_auth(self.context['request']) for pointer in obj.linked_nodes.filter(is_deleted=False, type='osf.registration'): if pointer.can_view(auth): count += 1 return count def create(self, validated_data): node = Collection(**validated_data) node.category = '' try: node.save() except ValidationError as e: raise InvalidModelValueError(detail=e.messages[0]) except IntegrityError: raise ser.ValidationError('Each user cannot have more than one Bookmark collection.') return node def update(self, node, validated_data): """Update instance with the validated data. Requires the request to be in the serializer context. """ assert isinstance(node, Node), 'collection must be a Node' auth = get_user_auth(self.context['request']) if validated_data: try: node.update(validated_data, auth=auth) except ValidationError as e: raise InvalidModelValueError(detail=e.messages[0]) except PermissionsError: raise exceptions.PermissionDenied return node
class UserSerializer(JSONAPISerializer): filterable_fields = frozenset( ['full_name', 'given_name', 'middle_names', 'family_name', 'id']) non_anonymized_fields = ['type'] id = IDField(source='_id', read_only=True) type = TypeField() full_name = ser.CharField( source='fullname', required=True, label='Full name', help_text='Display name used in the general user interface') given_name = ser.CharField(required=False, allow_blank=True, help_text='For bibliographic citations') middle_names = ser.CharField(required=False, allow_blank=True, help_text='For bibliographic citations') family_name = ser.CharField(required=False, allow_blank=True, help_text='For bibliographic citations') suffix = HideIfDisabled( ser.CharField(required=False, allow_blank=True, help_text='For bibliographic citations')) date_registered = HideIfDisabled(DateByVersion(read_only=True)) active = HideIfDisabled( ser.BooleanField(read_only=True, source='is_active')) timezone = HideIfDisabled( ser.CharField(required=False, help_text="User's timezone, e.g. 'Etc/UTC")) locale = HideIfDisabled( ser.CharField(required=False, help_text="User's locale, e.g. 'en_US'")) social = ListDictField(required=False) can_view_reviews = ShowIfCurrentUser( ser.SerializerMethodField( help_text= 'Whether the current user has the `view_submissions` permission to ANY reviews provider.' )) links = HideIfDisabled( LinksField({ 'html': 'absolute_url', 'profile_image': 'profile_image_url', })) nodes = HideIfDisabled( RelationshipField( related_view='users:user-nodes', related_view_kwargs={'user_id': '<_id>'}, related_meta={'projects_in_common': 'get_projects_in_common'}, )) quickfiles = HideIfDisabled( QuickFilesRelationshipField( related_view='users:user-quickfiles', related_view_kwargs={'user_id': '<_id>'}, )) registrations = DevOnly( HideIfDisabled( RelationshipField( related_view='users:user-registrations', related_view_kwargs={'user_id': '<_id>'}, ))) institutions = HideIfDisabled( RelationshipField( related_view='users:user-institutions', related_view_kwargs={'user_id': '<_id>'}, self_view='users:user-institutions-relationship', self_view_kwargs={'user_id': '<_id>'}, )) actions = ShowIfCurrentUser( RelationshipField( related_view='users:user-action-list', related_view_kwargs={'user_id': '<_id>'}, )) class Meta: type_ = 'users' def get_projects_in_common(self, obj): user = get_user_auth(self.context['request']).user if obj == user: return user.contributor_to.count() return obj.n_projects_in_common(user) def absolute_url(self, obj): if obj is not None: return obj.absolute_url return None def get_absolute_url(self, obj): return absolute_reverse( 'users:user-detail', kwargs={ 'user_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version'] }) def get_can_view_reviews(self, obj): group_qs = GroupObjectPermission.objects.filter( group__user=obj, permission__codename='view_submissions') return group_qs.exists() or obj.userobjectpermission_set.filter( permission__codename='view_submissions') def profile_image_url(self, user): size = self.context['request'].query_params.get('profile_image_size') return user.profile_image_url(size=size) def update(self, instance, validated_data): assert isinstance(instance, OSFUser), 'instance must be a User' for attr, value in validated_data.items(): if 'social' == attr: for key, val in value.items(): # currently only profileWebsites are a list, the rest of the social key only has one value if key == 'profileWebsites': instance.social[key] = val else: if len(val) > 1: raise InvalidModelValueError( detail= '{} only accept a list of one single value'. format(key)) instance.social[key] = val[0] else: setattr(instance, attr, value) try: instance.save() except ValidationValueError as e: raise InvalidModelValueError(detail=e.message) except ValidationError as e: raise InvalidModelValueError(e) return instance
class WikiSerializer(JSONAPISerializer): filterable_fields = frozenset([ 'name', 'date_modified' ]) id = IDField(source='_id', read_only=True) type = TypeField() name = ser.CharField(source='page_name') kind = ser.SerializerMethodField() size = ser.SerializerMethodField() path = ser.SerializerMethodField() materialized_path = ser.SerializerMethodField(method_name='get_path') date_modified = DateByVersion(source='date') content_type = ser.SerializerMethodField() current_user_can_comment = ser.SerializerMethodField(help_text='Whether the current user is allowed to post comments') extra = ser.SerializerMethodField(help_text='Additional metadata about this wiki') user = RelationshipField( related_view='users:user-detail', related_view_kwargs={'user_id': '<user._id>'} ) # LinksField.to_representation adds link to "self" links = LinksField({ 'info': Link('wikis:wiki-detail', kwargs={'wiki_id': '<_id>'}), 'download': 'get_wiki_content' }) class Meta: type_ = 'wikis' def get_absolute_url(self, obj): return obj.get_absolute_url() def get_path(self, obj): return '/{}'.format(obj._id) def get_kind(self, obj): return 'file' def get_size(self, obj): return sys.getsizeof(obj.content) def get_current_user_can_comment(self, obj): user = self.context['request'].user auth = Auth(user if not user.is_anonymous else None) return obj.node.can_comment(auth) def get_content_type(self, obj): return 'text/markdown' def get_extra(self, obj): return { 'version': obj.version } def get_wiki_content(self, obj): return absolute_reverse('wikis:wiki-content', kwargs={ 'wiki_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version'] })
class CommentSerializer(JSONAPISerializer): filterable_fields = frozenset( ['deleted', 'date_created', 'date_modified', 'page', 'target']) id = IDField(source='_id', read_only=True) type = TypeField() content = AuthorizedCharField(source='get_content', required=True) page = ser.CharField(read_only=True) target = TargetField(link_type='related', meta={'type': 'get_target_type'}) user = RelationshipField(related_view='users:user-detail', related_view_kwargs={'user_id': '<user._id>'}) reports = RelationshipField(related_view='comments:comment-reports', related_view_kwargs={'comment_id': '<pk>'}) date_created = DateByVersion(read_only=True) date_modified = DateByVersion(read_only=True) modified = ser.BooleanField(read_only=True, default=False) deleted = ser.BooleanField(read_only=True, source='is_deleted', default=False) is_abuse = ser.SerializerMethodField( help_text='If the comment has been reported or confirmed.') is_ham = ser.SerializerMethodField( help_text='Comment has been confirmed as ham.') has_report = ser.SerializerMethodField( help_text='If the user reported this comment.') has_children = ser.SerializerMethodField( help_text='Whether this comment has any replies.') can_edit = ser.SerializerMethodField( help_text='Whether the current user can edit this comment.') # LinksField.to_representation adds link to "self" links = LinksField({}) class Meta: type_ = 'comments' def get_is_ham(self, obj): if obj.spam_status == SpamStatus.HAM: return True return False def get_has_report(self, obj): user = self.context['request'].user if user.is_anonymous(): return False return user._id in obj.reports and not obj.reports[user._id].get( 'retracted', True) def get_is_abuse(self, obj): if obj.spam_status == SpamStatus.FLAGGED or obj.spam_status == SpamStatus.SPAM: return True return False def get_can_edit(self, obj): user = self.context['request'].user if user.is_anonymous(): return False return obj.user._id == user._id and obj.node.can_comment(Auth(user)) def get_has_children(self, obj): return Comment.find(Q('target', 'eq', Guid.load(obj._id))).count() > 0 def get_absolute_url(self, obj): return absolute_reverse( 'comments:comment-detail', kwargs={ 'comment_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version'] }) def update(self, comment, validated_data): assert isinstance(comment, Comment), 'comment must be a Comment' auth = Auth(self.context['request'].user) if validated_data: if validated_data.get('is_deleted', None) is False and comment.is_deleted: try: comment.undelete(auth, save=True) except PermissionsError: raise PermissionDenied( 'Not authorized to undelete this comment.') elif validated_data.get('is_deleted', None) is True and not comment.is_deleted: try: comment.delete(auth, save=True) except PermissionsError: raise PermissionDenied( 'Not authorized to delete this comment.') elif 'get_content' in validated_data: content = validated_data.pop('get_content') try: comment.edit(content, auth=auth, save=True) except PermissionsError: raise PermissionDenied( 'Not authorized to edit this comment.') except ValidationValueError as err: raise ValidationError(err.args[0]) return comment def get_target_type(self, obj): if not getattr(obj.referent, 'target_type', None): raise InvalidModelValueError(source={ 'pointer': '/data/relationships/target/links/related/meta/type' }, detail='Invalid comment target type.') return obj.referent.target_type def sanitize_data(self): ret = super(CommentSerializer, self).sanitize_data() content = self.validated_data.get('get_content', None) if content: ret['get_content'] = bleach.clean(content) return ret
class BaseRegistrationSerializer(NodeSerializer): title = ser.CharField(read_only=True) description = ser.CharField(read_only=True) category_choices = NodeSerializer.category_choices category_choices_string = NodeSerializer.category_choices_string category = HideIfWithdrawal( ser.ChoiceField(read_only=True, choices=category_choices, help_text='Choices: ' + category_choices_string)) date_modified = DateByVersion(read_only=True) fork = HideIfWithdrawal(ser.BooleanField(read_only=True, source='is_fork')) collection = HideIfWithdrawal( ser.BooleanField(read_only=True, source='is_collection')) node_license = HideIfWithdrawal(NodeLicenseSerializer(read_only=True)) tags = HideIfWithdrawal( JSONAPIListField(child=NodeTagField(), read_only=True)) public = HideIfWithdrawal( ser.BooleanField( source='is_public', required=False, help_text='Nodes that are made public will give read-only access ' 'to everyone. Private nodes require explicit read ' 'permission. Write and admin access are the same for ' 'public and private nodes. Administrators on a parent ' 'node have implicit read permissions for all child nodes')) current_user_permissions = HideIfWithdrawal( ser.SerializerMethodField( help_text='List of strings representing the permissions ' 'for the current user on this node.')) pending_embargo_approval = HideIfWithdrawal( ser.BooleanField( read_only=True, source='is_pending_embargo', help_text= 'The associated Embargo is awaiting approval by project admins.')) pending_registration_approval = HideIfWithdrawal( ser.BooleanField( source='is_pending_registration', read_only=True, help_text= 'The associated RegistrationApproval is awaiting approval by project admins.' )) pending_withdrawal = HideIfWithdrawal( ser.BooleanField( source='is_pending_retraction', read_only=True, help_text= 'The registration is awaiting withdrawal approval by project admins.' )) withdrawn = ser.BooleanField( source='is_retracted', read_only=True, help_text='The registration has been withdrawn.') date_registered = DateByVersion(source='registered_date', read_only=True, help_text='Date time of registration.') date_withdrawn = DateByVersion( source='retraction.date_retracted', read_only=True, help_text='Date time of when this registration was retracted.') embargo_end_date = HideIfWithdrawal( ser.SerializerMethodField( help_text='When the embargo on this registration will be lifted.')) withdrawal_justification = ser.CharField(source='retraction.justification', read_only=True) template_from = HideIfWithdrawal( ser.CharField( read_only=True, allow_blank=False, allow_null=False, help_text= 'Specify a node id for a node you would like to use as a template for the ' 'new node. Templating is like forking, except that you do not copy the ' 'files, only the project structure. Some information is changed on the top ' 'level project by submitting the appropriate fields in the request body, ' 'and some information will not change. By default, the description will ' 'be cleared and the project will be made private.')) registration_supplement = ser.SerializerMethodField() registered_meta = HideIfWithdrawal( ser.SerializerMethodField( help_text= 'A dictionary with supplemental registration questions and responses.' )) registered_by = HideIfWithdrawal( RelationshipField( related_view='users:user-detail', related_view_kwargs={'user_id': '<registered_user._id>'})) registered_from = HideIfWithdrawal( RelationshipField( related_view='nodes:node-detail', related_view_kwargs={'node_id': '<registered_from._id>'})) children = HideIfWithdrawal( RelationshipField( related_view='registrations:registration-children', related_view_kwargs={'node_id': '<_id>'}, related_meta={'count': 'get_node_count'}, )) comments = HideIfWithdrawal( RelationshipField(related_view='registrations:registration-comments', related_view_kwargs={'node_id': '<_id>'}, related_meta={'unread': 'get_unread_comments_count'}, filter={'target': '<_id>'})) contributors = RelationshipField( related_view='registrations:registration-contributors', related_view_kwargs={'node_id': '<_id>'}, related_meta={'count': 'get_contrib_count'}) files = HideIfWithdrawal( RelationshipField(related_view='registrations:registration-providers', related_view_kwargs={'node_id': '<_id>'})) wikis = HideIfWithdrawal( RelationshipField( related_view='registrations:registration-wikis', related_view_kwargs={'node_id': '<_id>'}, )) forked_from = HideIfWithdrawal( RelationshipField( related_view=lambda n: 'registrations:registration-detail' if getattr(n, 'is_registration', False) else 'nodes:node-detail', related_view_kwargs={'node_id': '<forked_from_id>'})) template_node = HideIfWithdrawal( RelationshipField( related_view='nodes:node-detail', related_view_kwargs={'node_id': '<template_node._id>'})) license = HideIfWithdrawal( RelationshipField( related_view='licenses:license-detail', related_view_kwargs={ 'license_id': '<node_license.node_license._id>' }, )) logs = HideIfWithdrawal( RelationshipField( related_view='registrations:registration-logs', related_view_kwargs={'node_id': '<_id>'}, )) forks = HideIfWithdrawal( RelationshipField(related_view='registrations:registration-forks', related_view_kwargs={'node_id': '<_id>'})) node_links = ShowIfVersion(HideIfWithdrawal( RelationshipField( related_view='registrations:registration-pointers', related_view_kwargs={'node_id': '<_id>'}, related_meta={'count': 'get_pointers_count'}, help_text= 'This feature is deprecated as of version 2.1. Use linked_nodes instead.' )), min_version='2.0', max_version='2.0') parent = HideIfWithdrawal( RelationshipField(related_view='registrations:registration-detail', related_view_kwargs={'node_id': '<parent_node._id>'}, filter_key='parent_node')) root = HideIfWithdrawal( RelationshipField(related_view='registrations:registration-detail', related_view_kwargs={'node_id': '<root._id>'})) affiliated_institutions = HideIfWithdrawal( RelationshipField( related_view='registrations:registration-institutions', related_view_kwargs={'node_id': '<_id>'})) registration_schema = RelationshipField( related_view='metaschemas:metaschema-detail', related_view_kwargs={'metaschema_id': '<registered_schema_id>'}) registrations = HideIfRegistration( RelationshipField(related_view='nodes:node-registrations', related_view_kwargs={'node_id': '<_id>'})) draft_registrations = HideIfRegistration( RelationshipField(related_view='nodes:node-draft-registrations', related_view_kwargs={'node_id': '<_id>'})) preprints = HideIfWithdrawal( HideIfRegistration( RelationshipField(related_view='nodes:node-preprints', related_view_kwargs={'node_id': '<_id>'}))) identifiers = HideIfWithdrawal( RelationshipField(related_view='registrations:identifier-list', related_view_kwargs={'node_id': '<_id>'})) linked_nodes = HideIfWithdrawal( RelationshipField(related_view='registrations:linked-nodes', related_view_kwargs={'node_id': '<_id>'}, related_meta={'count': 'get_node_links_count'}, self_view='registrations:node-pointer-relationship', self_view_kwargs={'node_id': '<_id>'})) linked_registrations = HideIfWithdrawal( RelationshipField( related_view='registrations:linked-registrations', related_view_kwargs={'node_id': '<_id>'}, related_meta={'count': 'get_registration_links_count'}, self_view='registrations:node-registration-pointer-relationship', self_view_kwargs={'node_id': '<_id>'})) view_only_links = HideIfWithdrawal( RelationshipField( related_view='registrations:registration-view-only-links', related_view_kwargs={'node_id': '<_id>'}, related_meta={'count': 'get_view_only_links_count'}, )) citation = HideIfWithdrawal( RelationshipField(related_view='registrations:registration-citation', related_view_kwargs={'node_id': '<_id>'})) links = LinksField({ 'self': 'get_registration_url', 'html': 'get_absolute_html_url' }) def get_registration_url(self, obj): return absolute_reverse( 'registrations:registration-detail', kwargs={ 'node_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version'] }) def get_absolute_url(self, obj): return self.get_registration_url(obj) def create(self, validated_data): auth = get_user_auth(self.context['request']) draft = validated_data.pop('draft') registration_choice = validated_data.pop('registration_choice', 'immediate') embargo_lifted = validated_data.pop('lift_embargo', None) reviewer = is_prereg_admin_not_project_admin(self.context['request'], draft) try: draft.validate_metadata(metadata=draft.registration_metadata, reviewer=reviewer, required_fields=True) except ValidationValueError as e: raise exceptions.ValidationError(e.message) registration = draft.register(auth, save=True) if registration_choice == 'embargo': if not embargo_lifted: raise exceptions.ValidationError( 'lift_embargo must be specified.') embargo_end_date = embargo_lifted.replace(tzinfo=pytz.utc) try: registration.embargo_registration(auth.user, embargo_end_date) except ValidationError as err: raise exceptions.ValidationError(err.message) else: try: registration.require_approval(auth.user) except NodeStateError as err: raise exceptions.ValidationError(err) registration.save() return registration def get_registered_meta(self, obj): if obj.registered_meta: meta_values = obj.registered_meta.values()[0] try: return json.loads(meta_values) except TypeError: return meta_values except ValueError: return meta_values return None def get_embargo_end_date(self, obj): if obj.embargo_end_date: return obj.embargo_end_date return None def get_registration_supplement(self, obj): if obj.registered_schema: schema = obj.registered_schema.first() if schema is None: return None return schema.name return None def get_current_user_permissions(self, obj): return NodeSerializer.get_current_user_permissions(self, obj) def update(self, registration, validated_data): is_public = validated_data.get('is_public', False) if is_public: auth = Auth(self.context['request'].user) try: registration.update(validated_data, auth=auth) except NodeUpdateError as err: raise exceptions.ValidationError(err.reason) except NodeStateError as err: raise exceptions.ValidationError(err.message) else: raise exceptions.ValidationError( 'Registrations can only be turned from private to public.') return registration class Meta: type_ = 'registrations'
class FileSerializer(JSONAPISerializer): filterable_fields = frozenset([ 'id', 'name', 'node', 'kind', 'path', 'materialized_path', 'size', 'provider', 'last_touched', 'tags', ]) id = IDField(source='_id', read_only=True) type = TypeField() guid = ser.SerializerMethodField( read_only=True, method_name='get_file_guid', help_text='OSF GUID for this file (if one has been assigned)') checkout = CheckoutField() name = ser.CharField( read_only=True, help_text='Display name used in the general user interface') kind = ser.CharField(read_only=True, help_text='Either folder or file') path = ser.CharField( read_only=True, help_text='The unique path used to reference this object') size = ser.SerializerMethodField( read_only=True, help_text='The size of this file at this version') provider = ser.CharField( read_only=True, help_text='The Add-on service this file originates from') materialized_path = ser.CharField( read_only=True, help_text= 'The Unix-style path of this object relative to the provider root') last_touched = DateByVersion( read_only=True, help_text= 'The last time this file had information fetched about it via the OSF') date_modified = ser.SerializerMethodField( read_only=True, help_text='Timestamp when the file was last modified') date_created = ser.SerializerMethodField( read_only=True, help_text='Timestamp when the file was created') extra = ser.SerializerMethodField( read_only=True, help_text='Additional metadata about this file') tags = JSONAPIListField(child=FileTagField(), required=False) current_user_can_comment = ser.SerializerMethodField( help_text='Whether the current user is allowed to post comments') current_version = ser.SerializerMethodField( help_text='Latest file version') delete_allowed = ser.BooleanField(read_only=True, required=False) files = NodeFileHyperLinkField(related_view='nodes:node-files', related_view_kwargs={ 'node_id': '<node._id>', 'path': '<path>', 'provider': '<provider>' }, kind='folder') versions = NodeFileHyperLinkField(related_view='files:file-versions', related_view_kwargs={'file_id': '<_id>'}, kind='file') comments = FileCommentRelationshipField( related_view='nodes:node-comments', related_view_kwargs={'node_id': '<node._id>'}, related_meta={'unread': 'get_unread_comments_count'}, filter={'target': 'get_file_guid'}) node = RelationshipField(related_view='nodes:node-detail', related_view_kwargs={'node_id': '<node._id>'}, help_text='The project that this file belongs to') links = LinksField({ 'info': Link('files:file-detail', kwargs={'file_id': '<_id>'}), 'move': WaterbutlerLink(), 'upload': WaterbutlerLink(), 'delete': WaterbutlerLink(), 'download': WaterbutlerLink(must_be_file=True), 'new_folder': WaterbutlerLink(must_be_folder=True, kind='folder'), }) class Meta: type_ = 'files' def get_current_version(self, obj): if obj.history: return len(obj.history) return 1 def get_size(self, obj): if obj.versions.exists(): self.size = obj.versions.last().size return self.size return None def get_date_modified(self, obj): mod_dt = None if obj.provider == 'osfstorage' and obj.versions.exists(): # Each time an osfstorage file is added or uploaded, a new version object is created with its # date_created equal to the time of the update. The date_modified is the modified date # from the backend the file is stored on. This field refers to the modified date on osfstorage, # so prefer to use the date_created of the latest version. mod_dt = obj.versions.last().date_created elif obj.provider != 'osfstorage' and obj.history: mod_dt = obj.history[-1].get('modified', None) if self.context['request'].version >= '2.2' and obj.is_file and mod_dt: return datetime.strftime(mod_dt, '%Y-%m-%dT%H:%M:%S.%fZ') return mod_dt and mod_dt.replace(tzinfo=pytz.utc) def get_date_created(self, obj): creat_dt = None if obj.provider == 'osfstorage' and obj.versions.exists(): creat_dt = obj.versions.first().date_created elif obj.provider != 'osfstorage' and obj.history: # Non-osfstorage files don't store a created date, so instead get the modified date of the # earliest entry in the file history. creat_dt = obj.history[0].get('modified', None) if self.context[ 'request'].version >= '2.2' and obj.is_file and creat_dt: return datetime.strftime(creat_dt, '%Y-%m-%dT%H:%M:%S.%fZ') return creat_dt and creat_dt.replace(tzinfo=pytz.utc) def get_extra(self, obj): metadata = {} if obj.provider == 'osfstorage' and obj.versions.exists(): metadata = obj.versions.last().metadata elif obj.provider != 'osfstorage' and obj.history: metadata = obj.history[-1].get('extra', {}) extras = {} extras['hashes'] = { # mimic waterbutler response 'md5': metadata.get('md5', None), 'sha256': metadata.get('sha256', None), } if obj.provider == 'osfstorage' and obj.is_file: extras['downloads'] = obj.get_download_count() return extras def get_current_user_can_comment(self, obj): user = self.context['request'].user auth = Auth(user if not user.is_anonymous else None) return obj.node.can_comment(auth) def get_unread_comments_count(self, obj): user = self.context['request'].user if user.is_anonymous: return 0 return Comment.find_n_unread(user=user, node=obj.node, page='files', root_id=obj.get_guid()._id) def user_id(self, obj): # NOTE: obj is the user here, the meta field for # Hyperlinks is weird if obj: return obj._id return None def update(self, instance, validated_data): assert isinstance(instance, FileNode), 'Instance must be a FileNode' if instance.provider != 'osfstorage' and 'tags' in validated_data: raise Conflict( 'File service provider {} does not support tags on the OSF.'. format(instance.provider)) auth = get_user_auth(self.context['request']) old_tags = set(instance.tags.values_list('name', flat=True)) if 'tags' in validated_data: current_tags = set(validated_data.pop('tags', [])) else: current_tags = set(old_tags) for new_tag in (current_tags - old_tags): instance.add_tag(new_tag, auth=auth) for deleted_tag in (old_tags - current_tags): instance.remove_tag(deleted_tag, auth=auth) for attr, value in validated_data.items(): if attr == 'checkout': user = self.context['request'].user instance.check_in_or_out(user, value) else: setattr(instance, attr, value) instance.save() return instance def is_valid(self, **kwargs): return super(FileSerializer, self).is_valid(clean_html=False, **kwargs) def get_file_guid(self, obj): if obj: guid = obj.get_guid() if guid: return guid._id return None def get_absolute_url(self, obj): return api_v2_url('files/{}/'.format(obj._id))
class UserSerializer(JSONAPISerializer): filterable_fields = frozenset( ['full_name', 'given_name', 'middle_names', 'family_name', 'id']) non_anonymized_fields = ['type'] id = IDField(source='_id', read_only=True) type = TypeField() full_name = ser.CharField( source='fullname', required=True, label='Full name', help_text='Display name used in the general user interface') given_name = ser.CharField(required=False, allow_blank=True, help_text='For bibliographic citations') middle_names = ser.CharField(required=False, allow_blank=True, help_text='For bibliographic citations') family_name = ser.CharField(required=False, allow_blank=True, help_text='For bibliographic citations') suffix = HideIfDisabled( ser.CharField(required=False, allow_blank=True, help_text='For bibliographic citations')) date_registered = HideIfDisabled(DateByVersion(read_only=True)) active = HideIfDisabled( ser.BooleanField(read_only=True, source='is_active')) # Social Fields are broken out to get around DRF complex object bug and to make API updating more user friendly. github = DevOnly( HideIfDisabled( AllowMissing(ser.CharField(required=False, source='social.github', allow_blank=True, help_text='GitHub Handle'), required=False, source='social.github'))) scholar = DevOnly( HideIfDisabled( AllowMissing(ser.CharField(required=False, source='social.scholar', allow_blank=True, help_text='Google Scholar Account'), required=False, source='social.scholar'))) personal_website = DevOnly( HideIfDisabled( AllowMissing(ser.URLField(required=False, source='social.personal', allow_blank=True, help_text='Personal Website'), required=False, source='social.personal'))) twitter = DevOnly( HideIfDisabled( AllowMissing(ser.CharField(required=False, source='social.twitter', allow_blank=True, help_text='Twitter Handle'), required=False, source='social.twitter'))) linkedin = DevOnly( HideIfDisabled( AllowMissing(ser.CharField(required=False, source='social.linkedIn', allow_blank=True, help_text='LinkedIn Account'), required=False, source='social.linkedIn'))) impactstory = DevOnly( HideIfDisabled( AllowMissing(ser.CharField(required=False, source='social.impactStory', allow_blank=True, help_text='ImpactStory Account'), required=False, source='social.impactStory'))) orcid = DevOnly( HideIfDisabled( AllowMissing(ser.CharField(required=False, source='social.orcid', allow_blank=True, help_text='ORCID'), required=False, source='social.orcid'))) researcherid = DevOnly( HideIfDisabled( AllowMissing(ser.CharField(required=False, source='social.researcherId', allow_blank=True, help_text='ResearcherId Account'), required=False, source='social.researcherId'))) researchgate = DevOnly( HideIfDisabled( AllowMissing(ser.CharField(required=False, source='social.researchGate', allow_blank=True, help_text='ResearchGate Account'), required=False, source='social.researchGate'))) academia_institution = DevOnly( HideIfDisabled( AllowMissing(ser.CharField(required=False, source='social.academiaInstitution', allow_blank=True, help_text='AcademiaInstitution Field'), required=False, source='social.academiaInstitution'))) academia_profile_id = DevOnly( HideIfDisabled( AllowMissing(ser.CharField(required=False, source='social.academiaProfileID', allow_blank=True, help_text='AcademiaProfileID Field'), required=False, source='social.academiaProfileID'))) baiduscholar = DevOnly( HideIfDisabled( AllowMissing(ser.CharField(required=False, source='social.baiduScholar', allow_blank=True, help_text='Baidu Scholar Account'), required=False, source='social.baiduScholar'))) ssrn = DevOnly( HideIfDisabled( AllowMissing(ser.CharField(required=False, source='social.ssrn', allow_blank=True, help_text='SSRN Account'), required=False, source='social.ssrn'))) timezone = HideIfDisabled( ser.CharField(required=False, help_text="User's timezone, e.g. 'Etc/UTC")) locale = HideIfDisabled( ser.CharField(required=False, help_text="User's locale, e.g. 'en_US'")) links = HideIfDisabled( LinksField({ 'html': 'absolute_url', 'profile_image': 'profile_image_url', })) nodes = HideIfDisabled( RelationshipField( related_view='users:user-nodes', related_view_kwargs={'user_id': '<pk>'}, related_meta={'projects_in_common': 'get_projects_in_common'}, )) registrations = DevOnly( HideIfDisabled( RelationshipField( related_view='users:user-registrations', related_view_kwargs={'user_id': '<pk>'}, ))) institutions = HideIfDisabled( RelationshipField( related_view='users:user-institutions', related_view_kwargs={'user_id': '<pk>'}, self_view='users:user-institutions-relationship', self_view_kwargs={'user_id': '<pk>'}, )) class Meta: type_ = 'users' def get_projects_in_common(self, obj): user = get_user_auth(self.context['request']).user if obj == user: return len(user.contributor_to) return len(obj.get_projects_in_common(user, primary_keys=True)) def absolute_url(self, obj): if obj is not None: return obj.absolute_url return None def get_absolute_url(self, obj): return absolute_reverse( 'users:user-detail', kwargs={ 'user_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version'] }) def profile_image_url(self, user): size = self.context['request'].query_params.get('profile_image_size') return user.profile_image_url(size=size) def update(self, instance, validated_data): assert isinstance(instance, User), 'instance must be a User' for attr, value in validated_data.items(): if 'social' == attr: for key, val in value.items(): instance.social[key] = val else: setattr(instance, attr, value) try: instance.save() except ValidationValueError as e: raise InvalidModelValueError(detail=e.message) return instance