class NodeContributorsSerializer(JSONAPISerializer): """ Separate from UserSerializer due to necessity to override almost every field as read only """ non_anonymized_fields = ['bibliographic', 'permission'] filterable_fields = frozenset(['id', 'bibliographic', 'permission']) id = IDField(source='_id', required=True) type = TypeField() bibliographic = ser.BooleanField( help_text= 'Whether the user will be included in citations for this node or not.', default=True) permission = ser.ChoiceField( choices=osf_permissions.PERMISSIONS, required=False, allow_null=True, default=osf_permissions.reduce_permissions( osf_permissions.DEFAULT_CONTRIBUTOR_PERMISSIONS), help_text= 'User permission level. Must be "read", "write", or "admin". Defaults to "write".' ) links = LinksField({'self': 'get_absolute_url'}) users = RelationshipField(related_view='users:user-detail', related_view_kwargs={'user_id': '<pk>'}, always_embed=True) class Meta: type_ = 'contributors' def get_absolute_url(self, obj): node_id = self.context['request'].parser_context['kwargs']['node_id'] return absolute_reverse('nodes:node-contributor-detail', kwargs={ 'node_id': node_id, 'user_id': obj._id })
class UserIdentitiesSerializer(JSONAPISerializer): id = IDField(source='_id', read_only=True) type = TypeField() external_id = ser.CharField(read_only=True) status = ser.CharField(read_only=True) links = LinksField({ 'self': 'get_absolute_url', }) def get_absolute_url(self, obj): return absolute_reverse( 'users:user-identities-detail', kwargs={ 'user_id': self.context['request'].parser_context['kwargs']['user_id'], 'version': self.context['request'].parser_context['kwargs']['version'], 'identity_id': obj['_id'], }, ) class Meta: type_ = 'external-identities'
class NodeLinksSerializer(JSONAPISerializer): id = IDField(source='_id', read_only=True) type = TypeField() target_node_id = ser.CharField(source='node._id', help_text='The ID of the node that this Node Link points to') # TODO: We don't show the title because the current user may not have access to this node. We may want to conditionally # include this field in the future. # title = ser.CharField(read_only=True, source='node.title', help_text='The title of the node that this Node Link ' # 'points to') class Meta: type_ = 'node_links' links = LinksField({ 'html': 'get_absolute_url', }) def get_absolute_url(self, obj): pointer_node = Node.load(obj.node._id) return pointer_node.absolute_url def create(self, validated_data): request = self.context['request'] user = request.user auth = Auth(user) node = self.context['view'].get_node() pointer_node = Node.load(validated_data['node']['_id']) if not pointer_node: raise exceptions.NotFound('Node not found.') try: pointer = node.add_pointer(pointer_node, auth, save=True) return pointer except ValueError: raise exceptions.ValidationError('Node link to node {} already in list'.format(pointer_node._id)) def update(self, instance, validated_data): pass
class LicenseSerializer(JSONAPISerializer): filterable_fields = frozenset([ 'name', 'id', ]) non_anonymized_fields = ['type'] id = IDField(source='_id', read_only=True) type = TypeField() name = ser.CharField(required=True, help_text='License name') text = ser.CharField(required=True, help_text='Full text of the license') url = ser.URLField(required=False, help_text='URL for the license') required_fields = ser.ListField(source='properties', read_only=True, help_text='Fields required for this license (provided to help front-end validators)') links = LinksField({'self': 'get_absolute_url'}) class Meta: type_ = 'licenses' def get_absolute_url(self, obj): return absolute_reverse('licenses:license-detail', kwargs={ 'license_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version'] })
class UserSettingsSerializer(JSONAPISerializer): id = IDField(source='_id', read_only=True) type = TypeField() two_factor_enabled = ser.SerializerMethodField() subscribe_osf_general_email = ser.SerializerMethodField() subscribe_osf_help_email = ser.SerializerMethodField() def get_two_factor_enabled(self, obj): try: two_factor = TwoFactorUserSettings.objects.get(owner_id=obj.id) return not two_factor.deleted except TwoFactorUserSettings.DoesNotExist: return False def get_subscribe_osf_general_email(self, obj): return obj.mailchimp_mailing_lists.get(MAILCHIMP_GENERAL_LIST, False) def get_subscribe_osf_help_email(self, obj): return obj.osf_mailing_lists.get(OSF_HELP_LIST, False) links = LinksField({ 'self': 'get_absolute_url', }) def get_absolute_url(self, obj): return absolute_reverse( 'users:user_settings', kwargs={ 'user_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version'], }, ) class Meta: type_ = 'user_settings'
class AlertSerializer(JSONAPISerializer): filterable_fields = frozenset(['location', 'id']) id = IDField(source='_id') type = TypeField() location = ser.CharField(max_length=255) links = LinksField({'self': 'get_absolute_url'}) def get_absolute_url(self, obj): return obj.absolute_api_v2_url class Meta: type_ = 'alerts' def create(self, validated_data): Alert = apps.get_model('osf.DismissedAlert') alert = Alert(**validated_data) try: alert.save() except ValidationError as e: raise InvalidModelValueError(detail=e.messages[0]) return alert
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 = ser.DateTimeField(read_only=True) date_modified = ser.DateTimeField(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(read_only=True, required=False) 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 = RelationshipField( related_view='licenses:license-detail', related_view_kwargs={'license_id': '<node_license.node_license._id>'}, ) children = RelationshipField( related_view='nodes:node-children', related_view_kwargs={'node_id': '<pk>'}, related_meta={'count': 'get_node_count'}, ) comments = RelationshipField( related_view='nodes:node-comments', related_view_kwargs={'node_id': '<pk>'}, related_meta={'unread': 'get_unread_comments_count'}, filter={'target': '<pk>'}) contributors = RelationshipField( related_view='nodes:node-contributors', related_view_kwargs={'node_id': '<pk>'}, related_meta={'count': 'get_contrib_count'}, ) files = RelationshipField(related_view='nodes:node-providers', related_view_kwargs={'node_id': '<pk>'}) wikis = RelationshipField(related_view='nodes:node-wikis', related_view_kwargs={'node_id': '<pk>'}) 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_id>'}) 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': '<pk>'}) node_links = RelationshipField( related_view='nodes:node-pointers', related_view_kwargs={'node_id': '<pk>'}, related_meta={'count': 'get_pointers_count'}, ) parent = RelationshipField( related_view='nodes:node-detail', related_view_kwargs={'node_id': '<parent_node._id>'}, filter_key='parent_node') draft_registrations = HideIfRegistration( RelationshipField(related_view='nodes:node-draft-registrations', related_view_kwargs={'node_id': '<pk>'})) registrations = HideIfRegistration( RelationshipField(related_view='nodes:node-registrations', related_view_kwargs={'node_id': '<pk>'}, related_meta={'count': 'get_registration_count'})) affiliated_institutions = RelationshipField( related_view='nodes:node-institutions', related_view_kwargs={'node_id': '<pk>'}, self_view='nodes:node-relationships-institutions', self_view_kwargs={'node_id': '<pk>'}) 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': '<pk>'}, related_meta={'count': 'get_logs_count'}) linked_nodes = RelationshipField( related_view='nodes:linked-nodes', related_view_kwargs={'node_id': '<pk>'}, related_meta={'count': 'get_node_links_count'}, self_view='nodes:node-pointer-relationship', self_view_kwargs={'node_id': '<pk>'}, self_meta={'count': 'get_node_links_count'}) view_only_links = RelationshipField( related_view='nodes:node-view-only-links', related_view_kwargs={'node_id': '<pk>'}, ) 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 len(obj.logs) def get_node_count(self, obj): auth = get_user_auth(self.context['request']) nodes = [ node for node in obj.nodes if node.can_view(auth) and node.primary and not node.is_deleted ] return len(nodes) 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 len(obj.nodes_pointer) def get_node_links_count(self, obj): count = 0 auth = get_user_auth(self.context['request']) for pointer in obj.nodes_pointer: if not pointer.node.is_deleted and not pointer.node.is_collection and pointer.node.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 if 'template_from' in validated_data: template_from = validated_data.pop('template_from') template_node = Node.load(key=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 ValidationValueError as e: raise InvalidModelValueError(detail=e.message) 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.contributors: if contributor is not user: contributors.append({ 'user': contributor, 'permissions': parent.get_permissions(contributor), 'visible': parent.get_visible(contributor) }) 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, Node), 'node must be a Node' auth = get_user_auth(self.context['request']) old_tags = set([tag._id for tag in node.tags]) if 'tags' in validated_data: current_tags = set(validated_data.pop('tags', [])) elif self.partial: current_tags = set(old_tags) else: current_tags = set() for new_tag in (current_tags - old_tags): node.add_tag(new_tag, auth=auth) for deleted_tag in (old_tags - current_tags): node.remove_tag(deleted_tag, auth=auth) if validated_data: try: node.update(validated_data, auth=auth) except ValidationValueError 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 BaseFileSerializer(JSONAPISerializer): filterable_fields = frozenset([ 'id', 'name', '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 = VersionedDateTimeField( 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.IntegerField(help_text='Latest file version', read_only=True, source='current_version_number') delete_allowed = ser.BooleanField(read_only=True, required=False) parent_folder = RelationshipField( related_view='files:file-detail', related_view_kwargs={'file_id': '<parent._id>'}, help_text='The folder in which this file exists', ) files = NodeFileHyperLinkField( related_view=lambda node: disambiguate_files_related_view(node), view_lambda_argument='target', related_view_kwargs=lambda filenode: disambiguate_files_related_view_kwargs(filenode), kind='folder', ) versions = NodeFileHyperLinkField( related_view='files:file-versions', related_view_kwargs={'file_id': '<_id>'}, kind='file', ) comments = HideIfPreprint( FileRelationshipField( related_view='nodes:node-comments', related_view_kwargs={'node_id': '<target._id>'}, related_meta={'unread': 'get_unread_comments_count'}, filter={'target': 'get_file_guid'}, )) metadata_records = FileRelationshipField( related_view='files:metadata-records', related_view_kwargs={'file_id': '<_id>'}, ) links = LinksField({ 'info': Link('files:file-detail', kwargs={'file_id': '<_id>'}), 'move': WaterbutlerLink(), 'upload': WaterbutlerLink(), 'delete': WaterbutlerLink(), 'download': 'get_download_link', 'render': 'get_render_link', 'html': 'absolute_url', 'new_folder': WaterbutlerLink(must_be_folder=True, kind='folder'), }) def absolute_url(self, obj): if obj.is_file: return furl.furl(settings.DOMAIN).set( path=(obj.target._id, 'files', obj.provider, obj.path.lstrip('/')), ).url def get_download_link(self, obj): if obj.is_file: return get_file_download_link( obj, view_only=self.context['request'].query_params.get( 'view_only')) def get_render_link(self, obj): if obj.is_file: mfr_url = get_mfr_url(obj.target, obj.provider) download_url = self.get_download_link(obj) return get_file_render_link(mfr_url, download_url) class Meta: type_ = 'files' def get_size(self, obj): if obj.versions.exists(): self.size = obj.versions.first().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 external_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 created of the latest version. mod_dt = obj.versions.first().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.last().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.first().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) if isinstance(obj.target, AbstractNode): return obj.target.can_comment(auth) return False 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.target, 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, BaseFileNode), 'Instance must be a BaseFileNode' 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(BaseFileSerializer, 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 FileMetadataRecordSerializer(JSONAPISerializer): id = IDField(source='_id', required=True) type = TypeField() metadata = ser.DictField() file = RelationshipField( related_view='files:file-detail', related_view_kwargs={'file_id': '<file._id>'}, ) schema = RelationshipField( related_view='schemas:file-metadata-schema-detail', related_view_kwargs={'schema_id': '<schema._id>'}, ) links = LinksField({ 'download': 'get_download_link', 'self': 'get_absolute_url', }) def validate_metadata(self, value): schema = from_json(self.instance.serializer.osf_schema) try: jsonschema.validate(value, schema) except jsonschema.ValidationError as e: if e.relative_schema_path[0] == 'additionalProperties': error_message = e.message else: error_message = 'Your response of {} for the field {} was invalid.'.format( e.instance, e.absolute_path[0], ) raise InvalidModelValueError(detail=error_message, meta={'metadata_schema': schema}) return value def update(self, record, validated_data): if validated_data: user = self.context['request'].user proposed_metadata = validated_data.pop('metadata') record.update(proposed_metadata, user) return record def get_download_link(self, obj): return absolute_reverse( 'files:metadata-record-download', kwargs={ 'file_id': obj.file._id, 'record_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version'], }, ) def get_absolute_url(self, obj): return obj.absolute_api_v2_url class Meta: @staticmethod def get_type(request): return get_kebab_snake_case_field(request.version, 'metadata-records')
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 = ser.DateTimeField(read_only=True) date_modified = ser.DateTimeField(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': '<pk>'}, 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': '<pk>'}, related_meta={'count': 'get_node_links_count'}, self_view='collections:collection-node-pointer-relationship', self_view_kwargs={'collection_id': '<pk>'}) class Meta: type_ = 'collections' def get_absolute_url(self, obj): return absolute_reverse('collections:collection-detail', kwargs={'collection_id': obj._id}) def get_node_links_count(self, obj): count = 0 auth = get_user_auth(self.context['request']) for pointer in obj.nodes_pointer: if not pointer.node.is_deleted and not pointer.node.is_collection and pointer.node.can_view( auth): count += 1 return count def create(self, validated_data): node = Node(**validated_data) node.is_collection = True node.category = '' try: node.save() except ValidationValueError as e: raise InvalidModelValueError(detail=e.message) except NodeStateError: 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 ValidationValueError as e: raise InvalidModelValueError(detail=e.message) except PermissionsError: raise exceptions.PermissionDenied return node
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 = VersionedDateTimeField( source='created', 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 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 = ser.CharField(required=False, allow_blank=True, help_text='For bibliographic citations') date_registered = ser.DateTimeField(read_only=True) # Social Fields are broken out to get around DRF complex object bug and to make API updating more user friendly. gitHub = DevOnly( AllowMissing(ser.CharField(required=False, source='social.github', allow_blank=True, help_text='GitHub Handle'), required=False, source='social.github')) scholar = DevOnly( AllowMissing(ser.CharField(required=False, source='social.scholar', allow_blank=True, help_text='Google Scholar Account'), required=False, source='social.scholar')) personal_website = DevOnly( AllowMissing(ser.URLField(required=False, source='social.personal', allow_blank=True, help_text='Personal Website'), required=False, source='social.personal')) twitter = DevOnly( AllowMissing(ser.CharField(required=False, source='social.twitter', allow_blank=True, help_text='Twitter Handle'), required=False, source='social.twitter')) linkedIn = DevOnly( AllowMissing(ser.CharField(required=False, source='social.linkedIn', allow_blank=True, help_text='LinkedIn Account'), required=False, source='social.linkedIn')) impactStory = DevOnly( AllowMissing(ser.CharField(required=False, source='social.impactStory', allow_blank=True, help_text='ImpactStory Account'), required=False, source='social.impactStory')) orcid = DevOnly( AllowMissing(ser.CharField(required=False, source='social.orcid', allow_blank=True, help_text='ORCID'), required=False, source='social.orcid')) researcherId = DevOnly( AllowMissing(ser.CharField(required=False, source='social.researcherId', allow_blank=True, help_text='ResearcherId Account'), required=False, source='social.researcherId')) researchGate = DevOnly( AllowMissing(ser.CharField(required=False, source='social.researchGate', allow_blank=True, help_text='ResearchGate Account'), required=False, source='social.researchGate')) academiaInstitution = DevOnly( AllowMissing(ser.CharField(required=False, source='social.academiaInstitution', allow_blank=True, help_text='AcademiaInstitution Field'), required=False, source='social.academiaInstitution')) academiaProfileID = DevOnly( AllowMissing(ser.CharField(required=False, source='social.academiaProfileID', allow_blank=True, help_text='AcademiaProfileID Field'), required=False, source='social.academiaProfileID')) baiduScholar = DevOnly( AllowMissing(ser.CharField(required=False, source='social.baiduScholar', allow_blank=True, help_text='Baidu Scholar Account'), required=False, source='social.baiduScholar')) links = LinksField({ 'html': 'absolute_url', 'profile_image': 'profile_image_url', }) nodes = RelationshipField( related_view='users:user-nodes', related_view_kwargs={'user_id': '<pk>'}, ) institutions = 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 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}) 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
class UserSerializer(JSONAPISerializer): filterable_fields = frozenset([ 'full_name', 'given_name', 'middle_names', 'family_name', 'id', 'uid' ]) non_anonymized_fields = ['type'] id = IDField(source='_id', read_only=True) uid = 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', max_length=186) 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(VersionedDateTimeField(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 = 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>'}, )) preprints = HideIfDisabled( RelationshipField( related_view='users:user-preprints', 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 UserSerializer(JSONAPISerializer): filterable_fields = frozenset([ 'full_name', 'given_name', 'middle_names', 'family_name', 'id', ]) writeable_method_fields = frozenset([ 'accepted_terms_of_service', ]) 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', max_length=186) 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(VersionedDateTimeField(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 = SocialField(required=False, min_version='2.10') employment = JSONAPIListField(required=False, source='jobs') education = JSONAPIListField(required=False, source='schools') can_view_reviews = ShowIfCurrentUser( ser.SerializerMethodField( help_text= 'Whether the current user has the `view_submissions` permission to ANY reviews provider.' )) accepted_terms_of_service = ShowIfCurrentUser(ser.SerializerMethodField()) 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', 'count': 'get_node_count', }, )) quickfiles = HideIfDisabled( QuickFilesRelationshipField( related_view='users:user-quickfiles', related_view_kwargs={'user_id': '<_id>'}, related_meta={'count': 'get_quickfiles_count'}, )) registrations = HideIfDisabled( RelationshipField( related_view='users:user-registrations', related_view_kwargs={'user_id': '<_id>'}, related_meta={'count': 'get_registration_count'}, )) 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>'}, related_meta={'count': 'get_institutions_count'}, )) preprints = HideIfDisabled( RelationshipField( related_view='users:user-preprints', related_view_kwargs={'user_id': '<_id>'}, related_meta={'count': 'get_preprint_count'}, )) emails = ShowIfCurrentUser( RelationshipField( related_view='users:user-emails', related_view_kwargs={'user_id': '<_id>'}, )) default_region = ShowIfCurrentUser( RegionRelationshipField( related_view='regions:region-detail', related_view_kwargs={'region_id': 'get_default_region_id'}, read_only=False, )) settings = ShowIfCurrentUser( RelationshipField( related_view='users:user_settings', related_view_kwargs={'user_id': '<_id>'}, read_only=True, )) 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_node_count(self, obj): auth = get_user_auth(self.context['request']) if obj != auth.user: return default_node_list_permission_queryset( user=auth.user, model_cls=Node).filter(contributor__user__id=obj.id).count() return default_node_list_queryset(model_cls=Node).filter( contributor__user__id=obj.id).count() def get_quickfiles_count(self, obj): return QuickFilesNode.objects.get( contributor__user__id=obj.id).files.filter( type='osf.osfstoragefile').count() def get_registration_count(self, obj): auth = get_user_auth(self.context['request']) user_registration = default_node_list_queryset( model_cls=Registration).filter(contributor__user__id=obj.id) return user_registration.can_view( user=auth.user, private_link=auth.private_link).count() def get_preprint_count(self, obj): auth_user = get_user_auth(self.context['request']).user user_preprints_query = Preprint.objects.filter( _contributors__guids___id=obj._id).exclude(machine_state='initial') return Preprint.objects.can_view(user_preprints_query, auth_user, allow_contribs=False).count() def get_institutions_count(self, obj): return obj.affiliated_institutions.count() def get_can_view_reviews(self, obj): group_qs = AbstractProviderGroupObjectPermission.objects.filter( group__user=obj, permission__codename='view_submissions') return group_qs.exists( ) or obj.abstractprovideruserobjectpermission_set.filter( permission__codename='view_submissions') def get_default_region_id(self, obj): try: # use the annotated value if possible region_id = obj.default_region except AttributeError: # use computed property if region annotation does not exist region_id = obj.osfstorage_region._id return region_id def get_accepted_terms_of_service(self, obj): return bool(obj.accepted_terms_of_service) def profile_image_url(self, user): size = self.context['request'].query_params.get('profile_image_size') return user.profile_image_url(size=size) def validate_employment(self, value): validate_user_json(value, 'employment-schema.json') return value def validate_education(self, value): validate_user_json(value, 'education-schema.json') return value def validate_social(self, value): schema = from_json('social-schema.json') try: jsonschema.validate(value, schema) except jsonschema.ValidationError as e: raise InvalidModelValueError(e) return value 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(): instance.social[key] = val elif 'accepted_terms_of_service' == attr: if value and not instance.accepted_terms_of_service: instance.accepted_terms_of_service = timezone.now() elif 'region_id' == attr: region_id = validated_data.get('region_id') user_settings = instance._settings_model( 'osfstorage').objects.get(owner=instance) user_settings.default_region_id = region_id user_settings.save() instance.default_region = self.context['request'].data[ 'default_region'] 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 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 = ser.DateTimeField(read_only=True) date_modified = ser.DateTimeField(read_only=True) links = LinksField({}) node_links = DevOnly(RelationshipField( related_view='collections:node-pointers', related_view_kwargs={'collection_id': '<pk>'}, related_meta={'count': 'get_node_links_count'} )) # TODO: Add a self link to this when it's available linked_nodes = DevOnly(RelationshipField( related_view='collections:linked-nodes', related_view_kwargs={'collection_id': '<pk>'}, related_meta={'count': 'get_node_links_count'} )) class Meta: type_ = 'collections' def get_absolute_url(self, obj): return obj.absolute_url def get_node_links_count(self, obj): return len(obj.nodes_pointer) def create(self, validated_data): node = Node(**validated_data) node.is_folder = True node.category = '' try: node.save() except ValidationValueError as e: raise InvalidModelValueError(detail=e.message) 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 ValidationValueError as e: raise InvalidModelValueError(detail=e.message) except PermissionsError: raise exceptions.PermissionDenied return node
class GuidSerializer(JSONAPISerializer): class Meta: type_ = 'guids' filterable_fields = tuple() id = IDField(source='_id', read_only=True) type = TypeField() referent = RelationshipField( related_view=get_related_view, related_view_kwargs=get_related_view_kwargs, related_meta={ 'type': 'get_type', }, ) links = LinksField({ 'self': 'get_absolute_url', 'html': 'get_absolute_html_url', }) def get_type(self, guid): return get_type(guid.referent) def get_absolute_url(self, obj): return absolute_reverse( 'guids:guid-detail', kwargs={ 'guids': obj._id, 'version': self.context['request'].parser_context['kwargs']['version'], }, ) def get_absolute_html_url(self, obj): if not isinstance(obj.referent, BaseFileNode): return obj.referent.absolute_url return urljoin(website_settings.DOMAIN, '/{}/'.format(obj._id)) def to_representation(self, obj): if self.context['view'].kwargs.get('is_embedded'): # Force the referent to serialize instead. obj = obj.referent if isinstance(obj, Collection): view_kwargs = {'collection_id': obj._id} else: view_kwargs = {'node_id': obj._id} ser = resolve( reverse( get_related_view(obj), kwargs={ 'version': self.context['view'].kwargs.get('version', '2'), **view_kwargs, }, ), ).func.cls.serializer_class(context=self.context, ) [ser.context.update({k: v}) for k, v in self.context.items()] return ser.to_representation(obj) return super().to_representation(obj)
class UserSettingsSerializer(JSONAPISerializer): id = IDField(source='_id', read_only=True) type = TypeField() two_factor_enabled = ser.SerializerMethodField() two_factor_confirmed = ser.SerializerMethodField(read_only=True) subscribe_osf_general_email = ser.SerializerMethodField() subscribe_osf_help_email = ser.SerializerMethodField() deactivation_requested = ser.BooleanField(source='requested_deactivation', required=False) secret = ser.SerializerMethodField(read_only=True) def to_representation(self, instance): self.context['twofactor_addon'] = instance.get_addon('twofactor') return super(UserSettingsSerializer, self).to_representation(instance) def get_two_factor_enabled(self, obj): try: two_factor = TwoFactorUserSettings.objects.get(owner_id=obj.id) return not two_factor.deleted except TwoFactorUserSettings.DoesNotExist: return False def get_two_factor_confirmed(self, obj): two_factor_addon = self.context['twofactor_addon'] if two_factor_addon and two_factor_addon.is_confirmed: return True return False def get_secret(self, obj): two_factor_addon = self.context['twofactor_addon'] if two_factor_addon and not two_factor_addon.is_confirmed: return two_factor_addon.totp_secret_b32 def get_subscribe_osf_general_email(self, obj): return obj.mailchimp_mailing_lists.get(MAILCHIMP_GENERAL_LIST, False) def get_subscribe_osf_help_email(self, obj): return obj.osf_mailing_lists.get(OSF_HELP_LIST, False) links = LinksField({ 'self': 'get_absolute_url', 'export': 'get_export_link', }) def get_export_link(self, obj): return absolute_reverse( 'users:user-account-export', kwargs={ 'user_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version'], }, ) def get_absolute_url(self, obj): return absolute_reverse( 'users:user_settings', kwargs={ 'user_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version'], }, ) class Meta: type_ = 'user_settings'
class ApiOAuth2PersonalTokenSerializer(JSONAPISerializer): """Serialize data about a registered personal access token""" id = IDField(source='_id', read_only=True, help_text='The object ID for this token (automatically generated)') type = TypeField() name = ser.CharField( help_text='A short, descriptive name for this token', required=True, ) owner = ser.CharField( help_text='The user who owns this token', read_only=True, # Don't let user register a token in someone else's name source='owner._id', ) scopes = ser.CharField( help_text='Governs permissions associated with this token', required=True, ) token_id = ser.CharField(read_only=True, allow_blank=True) class Meta: type_ = 'tokens' links = LinksField({ 'html': 'absolute_url', }) def absolute_url(self, obj): return obj.absolute_url def get_absolute_url(self, obj): return obj.absolute_api_v2_url def to_representation(self, obj, envelope='data'): data = super(ApiOAuth2PersonalTokenSerializer, self).to_representation(obj, envelope=envelope) # Make sure users only see token_id on create if not self.context['request'].method == 'POST': if 'data' in data: data['data']['attributes'].pop('token_id') else: data['attributes'].pop('token_id') return data def create(self, validated_data): validate_requested_scopes(validated_data) instance = ApiOAuth2PersonalToken(**validated_data) instance.save() return instance def update(self, instance, validated_data): validate_requested_scopes(validated_data) assert isinstance(instance, ApiOAuth2PersonalToken), 'instance must be an ApiOAuth2PersonalToken' instance.deactivate(save=False) # This will cause CAS to revoke the existing token but still allow it to be used in the future, new scopes will be updated properly at that time. instance.reload() for attr, value in validated_data.iteritems(): if attr == 'token_id': # Do not allow user to update token_id continue else: setattr(instance, attr, value) instance.save() return instance
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() 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 = ser.DateTimeField( 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) 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'}) 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_size(self, obj): if obj.versions: return obj.versions[-1].size return None def get_date_modified(self, obj): mod_dt = None if obj.provider == 'osfstorage' and obj.versions: # 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[-1].date_created elif obj.provider != 'osfstorage' and obj.history: mod_dt = obj.history[-1].get('modified', None) 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: creat_dt = obj.versions[0].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) return creat_dt and creat_dt.replace(tzinfo=pytz.utc) def get_extra(self, obj): metadata = {} if obj.provider == 'osfstorage' and obj.versions: metadata = obj.versions[-1].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), } return extras 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([tag._id for tag in instance.tags]) 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 PreprintSerializer(TaxonomizableSerializerMixin, MetricsSerializerMixin, JSONAPISerializer): filterable_fields = frozenset([ 'id', 'date_created', 'date_modified', 'date_published', 'original_publication_date', 'provider', 'is_published', 'subjects', 'reviews_state', 'node_is_public', ]) available_metrics = frozenset([ 'downloads', 'views', ]) id = IDField(source='_id', read_only=True) type = TypeField() date_created = VersionedDateTimeField(source='created', read_only=True) date_modified = VersionedDateTimeField(source='modified', read_only=True) date_published = VersionedDateTimeField(read_only=True) original_publication_date = VersionedDateTimeField(required=False, allow_null=True) doi = ser.CharField(source='article_doi', required=False, allow_null=True) title = ser.CharField(required=True, max_length=512) description = ser.CharField(required=False, allow_blank=True, allow_null=True) is_published = NoneIfWithdrawal(ser.BooleanField(required=False)) is_preprint_orphan = NoneIfWithdrawal(ser.BooleanField(read_only=True)) license_record = NodeLicenseSerializer(required=False, source='license') tags = JSONAPIListField(child=NodeTagField(), required=False) node_is_public = ser.BooleanField( read_only=True, source='node__is_public', help_text='Is supplementary project public?') preprint_doi_created = NoneIfWithdrawal( VersionedDateTimeField(read_only=True)) date_withdrawn = VersionedDateTimeField(read_only=True, allow_null=True) withdrawal_justification = HideIfNotWithdrawal( ser.CharField(required=False, read_only=True, allow_blank=True)) current_user_permissions = ser.SerializerMethodField( help_text='List of strings representing the permissions ' 'for the current user on this preprint.', ) public = ser.BooleanField(source='is_public', required=False, read_only=True) contributors = RelationshipField( related_view='preprints:preprint-contributors', related_view_kwargs={'preprint_id': '<_id>'}, ) reviews_state = ser.CharField(source='machine_state', read_only=True, max_length=15) date_last_transitioned = NoneIfWithdrawal( VersionedDateTimeField(read_only=True)) citation = NoneIfWithdrawal( RelationshipField( related_view='preprints:preprint-citation', related_view_kwargs={'preprint_id': '<_id>'}, )) identifiers = NoneIfWithdrawal( RelationshipField( related_view='preprints:identifier-list', related_view_kwargs={'preprint_id': '<_id>'}, )) node = NoneIfWithdrawal( NodeRelationshipField( related_view='nodes:node-detail', related_view_kwargs={'node_id': '<node._id>'}, read_only=False, many=False, self_view='preprints:preprint-node-relationship', self_view_kwargs={'preprint_id': '<_id>'}, )) license = PreprintLicenseRelationshipField( related_view='licenses:license-detail', related_view_kwargs={'license_id': '<license.node_license._id>'}, read_only=False, ) provider = PreprintProviderRelationshipField( related_view='providers:preprint-providers:preprint-provider-detail', related_view_kwargs={'provider_id': '<provider._id>'}, read_only=False, ) files = NoneIfWithdrawal( RelationshipField( related_view='preprints:preprint-storage-providers', related_view_kwargs={'preprint_id': '<_id>'}, )) primary_file = NoneIfWithdrawal( PrimaryFileRelationshipField( related_view='files:file-detail', related_view_kwargs={'file_id': '<primary_file._id>'}, read_only=False, )) review_actions = RelationshipField( related_view='preprints:preprint-review-action-list', related_view_kwargs={'preprint_id': '<_id>'}, ) requests = NoneIfWithdrawal( RelationshipField( related_view='preprints:preprint-request-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_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://doi.org/{}'.format( obj.article_doi) if obj.article_doi else None def get_current_user_permissions(self, obj): user = self.context['request'].user all_perms = ['read', 'write', 'admin'] user_perms = [] for p in all_perms: if obj.has_permission(user, p): user_perms.append(p) return user_perms def get_preprint_doi_url(self, obj): doi = None doi_identifier = obj.get_identifier('doi') if doi_identifier: doi = doi_identifier.value # if a preprint hasn't been published yet, don't show the DOI prematurely elif obj.is_published: client = obj.get_doi_client() doi = client.build_doi(preprint=obj) if client else None return 'https://doi.org/{}'.format(doi) if doi else None def update(self, preprint, validated_data): assert isinstance( preprint, Preprint), 'You must specify a valid preprint to be updated' auth = get_user_auth(self.context['request']) if not preprint.has_permission(auth.user, osf_permissions.WRITE): raise exceptions.PermissionDenied( detail= 'User must have admin or write permissions 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( 'preprints:preprint-review-action-list', kwargs={ 'version': self.context['request'].parser_context['kwargs'] ['version'], 'preprint_id': preprint._id, }, ), )) 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_preprint = True old_tags = set(preprint.tags.values_list('name', flat=True)) if 'tags' in validated_data: current_tags = set(validated_data.pop('tags', [])) elif self.partial: current_tags = set(old_tags) else: current_tags = set() for new_tag in (current_tags - old_tags): preprint.add_tag(new_tag, auth=auth) for deleted_tag in (old_tags - current_tags): preprint.remove_tag(deleted_tag, auth=auth) if 'node' in validated_data: node = validated_data.pop('node', None) self.set_field(preprint.set_supplemental_node, node, auth) save_preprint = True if 'subjects' in validated_data: subjects = validated_data.pop('subjects', None) self.set_field(preprint.set_subjects, subjects, auth) save_preprint = True if 'title' in validated_data: title = validated_data['title'] self.set_field(preprint.set_title, title, auth) save_preprint = True if 'description' in validated_data: description = validated_data['description'] self.set_field(preprint.set_description, description, auth) save_preprint = True if 'article_doi' in validated_data: preprint.article_doi = validated_data['article_doi'] save_preprint = 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'] or None 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.set_privacy('public', log=False, save=True) if save_preprint: preprint.save() if recently_published: for author in preprint.contributors: if author != auth.user: project_signals.contributor_added.send( preprint, 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=str(e)) except (ValueError, ValidationError, NodeStateError) as e: raise exceptions.ValidationError(detail=e.message)
class ApiOAuth2PersonalTokenSerializer(JSONAPISerializer): """Serialize data about a registered personal access token""" def __init__(self, *args, **kwargs): super(ApiOAuth2PersonalTokenSerializer, self).__init__(*args, **kwargs) request = kwargs['context']['request'] # Dynamically adding scopes field here, depending on the version if expect_scopes_as_relationships(request): field = TokenScopesRelationshipField( related_view='tokens:token-scopes-list', related_view_kwargs={'_id': '<_id>'}, always_embed=True, read_only=False, ) self.fields['scopes'] = field self.fields['owner'] = RelationshipField( related_view='users:user-detail', related_view_kwargs={'user_id': '<owner._id>'}, ) # Making scopes embeddable self.context['embed']['scopes'] = self.context[ 'view']._get_embed_partial('scopes', field) else: self.fields['scopes'] = ser.SerializerMethodField() self.fields['owner'] = ser.SerializerMethodField() id = IDField( source='_id', read_only=True, help_text='The object ID for this token (automatically generated)') type = TypeField() name = ser.CharField( help_text='A short, descriptive name for this token', required=True, ) token_id = ser.CharField(read_only=True, allow_blank=True) class Meta: type_ = 'tokens' links = LinksField({ 'html': 'absolute_url', }) def get_owner(self, obj): return obj.owner._id def get_scopes(self, obj): return ' '.join([scope.name for scope in obj.scopes.all()]) def absolute_url(self, obj): return obj.absolute_url def get_absolute_url(self, obj): return obj.absolute_api_v2_url def to_representation(self, obj, envelope='data'): data = super(ApiOAuth2PersonalTokenSerializer, self).to_representation(obj, envelope=envelope) # Make sure users only see token_id on create if not self.context['request'].method == 'POST': if 'data' in data: data['data']['attributes'].pop('token_id') else: data['attributes'].pop('token_id') return data def create(self, validated_data): scopes = validate_requested_scopes(validated_data.pop('scopes', None)) if not scopes: raise exceptions.ValidationError( 'Cannot create a token without scopes.') instance = ApiOAuth2PersonalToken(**validated_data) try: instance.save() except ValidationError as e: detail = format_validation_error(e) raise exceptions.ValidationError(detail=detail) for scope in scopes: instance.scopes.add(scope) return instance def update(self, instance, validated_data): scopes = validate_requested_scopes(validated_data.pop('scopes', None)) assert isinstance(instance, ApiOAuth2PersonalToken ), 'instance must be an ApiOAuth2PersonalToken' instance.deactivate( save=False ) # This will cause CAS to revoke the existing token but still allow it to be used in the future, new scopes will be updated properly at that time. instance.reload() for attr, value in validated_data.items(): if attr == 'token_id': # Do not allow user to update token_id continue else: setattr(instance, attr, value) if scopes: update_scopes(instance, scopes) try: instance.save() except ValidationError as e: detail = format_validation_error(e) raise exceptions.ValidationError(detail=detail) return instance
class CollectionSubmissionSerializer(TaxonomizableSerializerMixin, JSONAPISerializer): class Meta: type_ = 'collected-metadata' filterable_fields = frozenset([ 'id', 'collected_type', 'date_created', 'date_modified', 'subjects', 'status', ]) id = IDField(source='guid._id', read_only=True) type = TypeField() creator = RelationshipField( related_view='users:user-detail', related_view_kwargs={'user_id': '<creator._id>'}, ) collection = RelationshipField( related_view='collections:collection-detail', related_view_kwargs={'collection_id': '<collection._id>'}, ) guid = RelationshipField( related_view='guids:guid-detail', related_view_kwargs={'guids': '<guid._id>'}, always_embed=True, ) collected_type = ser.CharField(required=False) status = ser.CharField(required=False) volume = ser.CharField(required=False) issue = ser.CharField(required=False) program_area = ser.CharField(required=False) def get_absolute_url(self, obj): return absolute_reverse( 'collected-metadata:collected-metadata-detail', kwargs={ 'collection_id': obj.collection._id, 'cgm_id': obj.guid._id, 'version': self.context['request'].parser_context['kwargs']['version'], }, ) def update(self, obj, validated_data): if validated_data and 'subjects' in validated_data: auth = get_user_auth(self.context['request']) subjects = validated_data.pop('subjects', None) try: obj.set_subjects(subjects, auth) except PermissionsError as e: raise exceptions.PermissionDenied(detail=str(e)) except (ValueError, NodeStateError) as e: raise exceptions.ValidationError(detail=str(e)) if 'status' in validated_data: obj.status = validated_data.pop('status') if 'collected_type' in validated_data: obj.collected_type = validated_data.pop('collected_type') if 'volume' in validated_data: obj.volume = validated_data.pop('volume') if 'issue' in validated_data: obj.issue = validated_data.pop('issue') if 'program_area' in validated_data: obj.program_area = validated_data.pop('program_area') obj.save() return obj
class UserAccountExportSerializer(BaseAPISerializer): type = TypeField() class Meta: type_ = 'user-account-export-form'
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 = VersionedDateTimeField(source='created', read_only=True) date_modified = VersionedDateTimeField(source='modified', read_only=True) bookmarks = ser.BooleanField(read_only=False, default=False, source='is_bookmark_collection') is_promoted = ser.BooleanField(read_only=True, default=False) is_public = ser.BooleanField(read_only=False, default=False) status_choices = ser.ListField( child=ser.CharField(max_length=127), default=list(), ) collected_type_choices = ser.ListField( child=ser.CharField(max_length=127), default=list(), ) volume_choices = ser.ListField( child=ser.CharField(max_length=127), default=list(), ) issue_choices = ser.ListField( child=ser.CharField(max_length=127), default=list(), ) program_area_choices = ser.ListField( child=ser.CharField(max_length=127), default=list(), ) links = LinksField({}) provider = CollectionProviderRelationshipField( related_view='providers:collection-providers:collection-provider-detail', related_view_kwargs={'provider_id': '<provider._id>'}, read_only=True, ) 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>'}, ) linked_preprints = RelationshipField( related_view='collections:linked-preprints', related_view_kwargs={'collection_id': '<_id>'}, self_view='collections:collection-preprint-pointer-relationship', self_view_kwargs={'collection_id': '<_id>'}, related_meta={'count': 'get_preprint_links_count'}, ) 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): auth = get_user_auth(self.context['request']) node_ids = obj.guid_links.all().values_list('_id', flat=True) return Node.objects.filter(guids___id__in=node_ids, is_deleted=False).can_view(user=auth.user, private_link=auth.private_link).count() def get_registration_links_count(self, obj): auth = get_user_auth(self.context['request']) registration_ids = obj.guid_links.all().values_list('_id', flat=True) return Registration.objects.filter(guids___id__in=registration_ids, is_deleted=False).can_view(user=auth.user, private_link=auth.private_link).count() def get_preprint_links_count(self, obj): auth = get_user_auth(self.context['request']) return self.context['view'].collection_preprints(obj, auth.user).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, collection, validated_data): """Update instance with the validated data. """ assert isinstance(collection, Collection), 'collection must be a Collection' if validated_data: for key, value in validated_data.items(): if key == 'title' and collection.is_bookmark_collection: raise InvalidModelValueError('Bookmark collections cannot be renamed.') setattr(collection, key, value) try: collection.save() except ValidationError as e: raise InvalidModelValueError(detail=e.messages[0]) return collection
class UserEmailsSerializer(JSONAPISerializer): filterable_fields = frozenset([ 'confirmed', 'verified', 'primary', ]) id = IDField(read_only=True) type = TypeField() email_address = ser.CharField(source='address') confirmed = ser.BooleanField( read_only=True, help_text='User has clicked the confirmation link in an email.') verified = ser.BooleanField( required=False, help_text= 'User has verified adding the email on the OSF, i.e. via a modal.') primary = ser.BooleanField(required=False) is_merge = ser.BooleanField( read_only=True, required=False, help_text='This unconfirmed email is already confirmed to another user.' ) links = LinksField({ 'self': 'get_absolute_url', 'resend_confirmation': 'get_resend_confirmation_url', }) def get_absolute_url(self, obj): user = self.context['request'].user return absolute_reverse( 'users:user-email-detail', kwargs={ 'user_id': user._id, 'email_id': obj.id, 'version': self.context['request'].parser_context['kwargs']['version'], }, ) def get_resend_confirmation_url(self, obj): if not obj.confirmed: url = self.get_absolute_url(obj) return '{}?resend_confirmation=true'.format(url) class Meta: type_ = 'user_emails' def create(self, validated_data): user = self.context['request'].user address = validated_data['address'] is_merge = Email.objects.filter(address=address).exists() if address in user.unconfirmed_emails or address in user.emails.all( ).values_list('address', flat=True): raise Conflict( 'This user already has registered with the email address {}'. format(address)) try: token = user.add_unconfirmed_email(address) user.save() if CONFIRM_REGISTRATIONS_BY_EMAIL: send_confirm_email(user, email=address) user.email_last_sent = timezone.now() user.save() except ValidationError as e: raise exceptions.ValidationError(e.args[0]) except BlacklistedEmailError: raise exceptions.ValidationError( 'This email address domain is blacklisted.') return UserEmail(email_id=token, address=address, confirmed=False, verified=False, primary=False, is_merge=is_merge) def update(self, instance, validated_data): user = self.context['request'].user primary = validated_data.get('primary', None) verified = validated_data.get('verified', None) if primary and instance.confirmed: user.username = instance.address user.save() elif primary and not instance.confirmed: raise exceptions.ValidationError( 'You cannot set an unconfirmed email address as your primary email address.' ) if verified and not instance.verified: if not instance.confirmed: raise exceptions.ValidationError( 'You cannot verify an email address that has not been confirmed by a user.' ) user.confirm_email(token=instance.id, merge=instance.is_merge) instance.verified = True instance.is_merge = False new_email = Email.objects.get(address=instance.address, user=user) instance.id = hashids.encode(new_email.id) user.save() return instance
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 ModeratorSerializer(JSONAPISerializer): filterable_fields = frozenset([ 'full_name', 'id', 'permission_group' ]) id = IDField(source='_id', required=False, allow_null=True) type = TypeField() full_name = ser.CharField(source='fullname', required=False, label='Full name', help_text='Display name used in the general user interface', max_length=186) permission_group = ser.CharField(required=True) email = ser.EmailField(required=False, write_only=True, validators=[validate_email]) class Meta: type_ = 'moderators' def get_absolute_url(self, obj): return absolute_reverse('moderators:provider-moderator-detail', kwargs={ 'provider_id': self.context['request'].parser_context['kwargs']['version'], 'moderator_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version']}) def create(self, validated_data): auth = get_user_auth(self.context['request']) user_id = validated_data.pop('_id', '') address = validated_data.pop('email', '') context = { 'referrer': auth.user } if user_id and address: raise ValidationError('Cannot specify both "id" and "email".') user = None if user_id: user = OSFUser.load(user_id) elif address: try: email = Email.objects.get(address=address.lower()) except Email.DoesNotExist: full_name = validated_data.pop('fullname', '') if not full_name: raise ValidationError('"full_name" is required when adding a moderator via email.') user = OSFUser.create_unregistered(full_name, email=address) user.add_unconfirmed_email(user.username) user.save() context['confirmation_url'] = user.get_confirmation_url(user.username) else: user = email.user else: raise ValidationError('Must specify either "id" or "email".') if not user: raise ValidationError('Unable to find specified user.') context['user'] = user provider = self.context['provider'] if bool(get_perms(user, provider)): raise ValidationError('Specified user is already a moderator.') context['provider'] = provider if 'confirmation_url' in context: template = mails.CONFIRM_EMAIL_MODERATION(provider) else: template = mails.MODERATOR_ADDED(provider) perm_group = validated_data.pop('permission_group', '') if perm_group not in GROUPS: raise ValidationError('Unrecognized permission_group') context['role'] = 'an admin' if perm_group == 'admin' else 'a {}'.format(perm_group) context['notification_url'] = '{}settings/notifications'.format(DOMAIN) provider.add_to_group(user, perm_group) setattr(user, 'permission_group', perm_group) # Allows reserialization mails.send_mail( user.username, template, mimetype='html', **context ) return user def update(self, instance, validated_data): provider = self.context['provider'] perm_group = validated_data.get('permission_group') if perm_group == instance.permission_group: return instance try: provider.remove_from_group(instance, instance.permission_group, unsubscribe=False) except ValueError as e: raise ValidationError(e.message) provider.add_to_group(instance, perm_group) setattr(instance, 'permission_group', perm_group) return instance
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 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([ 'title', 'description', 'public', 'tags', 'category', 'date_created', 'date_modified', 'registration' ]) id = IDField(source='_id', read_only=True) type = TypeField() category_choices = Node.CATEGORY_MAP.keys() category_choices_string = ', '.join(["'{}'".format(choice) 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 = ser.DateTimeField(read_only=True) date_modified = ser.DateTimeField(read_only=True) registration = ser.BooleanField(read_only=True, source='is_registration') fork = ser.BooleanField(read_only=True, source='is_fork') collection = DevOnly(ser.BooleanField(read_only=True, source='is_folder')) dashboard = ser.BooleanField(read_only=True, source='is_dashboard') tags = JSONAPIListField(child=NodeTagField(), required=False) # 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_url'}) # TODO: When we have osf_permissions.ADMIN permissions, make this writable for admins children = RelationshipField( related_view='nodes:node-children', related_view_kwargs={'node_id': '<pk>'}, related_meta={'count': 'get_node_count'}, ) comments = RelationshipField( related_view='nodes:node-comments', related_view_kwargs={'node_id': '<pk>'}, related_meta={'unread': 'get_unread_comments_count'}) contributors = RelationshipField( related_view='nodes:node-contributors', related_view_kwargs={'node_id': '<pk>'}, related_meta={'count': 'get_contrib_count'}, ) files = RelationshipField( related_view='nodes:node-providers', related_view_kwargs={'node_id': '<pk>'} ) forked_from = RelationshipField( related_view='nodes:node-detail', related_view_kwargs={'node_id': '<forked_from_id>'} ) node_links = DevOnly(RelationshipField( related_view='nodes:node-pointers', related_view_kwargs={'node_id': '<pk>'}, related_meta={'count': 'get_pointers_count'}, )) parent = RelationshipField( related_view='nodes:node-detail', related_view_kwargs={'node_id': '<parent_id>'} ) registrations = DevOnly(HideIfRegistration(RelationshipField( related_view='nodes:node-registrations', related_view_kwargs={'node_id': '<pk>'}, related_meta={'count': 'get_registration_count'} ))) logs = RelationshipField( related_view='nodes:node-logs', related_view_kwargs={'node_id': '<pk>'}, ) class Meta: type_ = 'nodes' def get_absolute_url(self, obj): return obj.absolute_url # TODO: See if we can get the count filters into the filter rather than the serializer. def get_user_auth(self, request): user = request.user if user.is_anonymous(): auth = Auth(None) else: auth = Auth(user) return auth def get_node_count(self, obj): auth = self.get_user_auth(self.context['request']) nodes = [node for node in obj.nodes if node.can_view(auth) and node.primary and not node.is_deleted] return len(nodes) def get_contrib_count(self, obj): return len(obj.contributors) def get_registration_count(self, obj): auth = self.get_user_auth(self.context['request']) registrations = [node for node in obj.node__registrations if node.can_view(auth)] return len(registrations) def get_pointers_count(self, obj): return len(obj.nodes_pointer) def get_unread_comments_count(self, obj): auth = self.get_user_auth(self.context['request']) user = auth.user return Comment.find_unread(user=user, node=obj) def create(self, validated_data): node = Node(**validated_data) try: node.save() except ValidationValueError as e: raise InvalidModelValueError(detail=e.message) 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), 'node must be a Node' auth = self.get_user_auth(self.context['request']) old_tags = set([tag._id for tag in node.tags]) if 'tags' in validated_data: current_tags = set(validated_data.get('tags')) del validated_data['tags'] elif self.partial: current_tags = set(old_tags) else: current_tags = set() for new_tag in (current_tags - old_tags): node.add_tag(new_tag, auth=auth) for deleted_tag in (old_tags - current_tags): node.remove_tag(deleted_tag, auth=auth) if validated_data: try: node.update(validated_data, auth=auth) except ValidationValueError as e: raise InvalidModelValueError(detail=e.message) except PermissionsError: raise exceptions.PermissionDenied return node
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 = ser.DateTimeField(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) 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'] })