class BadgeInstanceSerializerV2(DetailSerializerV2, OriginalJsonSerializerMixin): openBadgeId = serializers.URLField(source='jsonld_id', read_only=True) createdAt = DateTimeWithUtcZAtEndField(source='created_at', read_only=True, default_timezone=pytz.utc) createdBy = EntityRelatedFieldV2(source='cached_creator', read_only=True) badgeclass = EntityRelatedFieldV2(source='cached_badgeclass', required=False, queryset=BadgeClass.cached) badgeclassOpenBadgeId = CachedUrlHyperlinkedRelatedField( source='badgeclass_jsonld_id', view_name='badgeclass_json', lookup_field='entity_id', queryset=BadgeClass.cached, required=False) badgeclassName = serializers.CharField(write_only=True, required=False) issuer = EntityRelatedFieldV2(source='cached_issuer', required=False, queryset=Issuer.cached) issuerOpenBadgeId = serializers.URLField(source='issuer_jsonld_id', read_only=True) image = serializers.FileField(read_only=True) recipient = BadgeRecipientSerializerV2(source='*', required=False) issuedOn = DateTimeWithUtcZAtEndField(source='issued_on', required=False, default_timezone=pytz.utc) narrative = MarkdownCharField(required=False, allow_null=True) evidence = EvidenceItemSerializerV2(source='evidence_items', many=True, required=False) revoked = HumanReadableBooleanField(read_only=True) revocationReason = serializers.CharField(source='revocation_reason', read_only=True) acceptance = serializers.CharField(read_only=True) expires = DateTimeWithUtcZAtEndField(source='expires_at', required=False, allow_null=True, default_timezone=pytz.utc) notify = HumanReadableBooleanField(write_only=True, required=False, default=False) allowDuplicateAwards = serializers.BooleanField(write_only=True, required=False, default=True) extensions = serializers.DictField(source='extension_items', required=False, validators=[BadgeExtensionValidator()]) class Meta(DetailSerializerV2.Meta): model = BadgeInstance apispec_definition = ('Assertion', { 'properties': OrderedDict([ ('entityId', { 'type': "string", 'format': "string", 'description': "Unique identifier for this Assertion", 'readOnly': True, }), ('entityType', { 'type': "string", 'format': "string", 'description': "\"Assertion\"", 'readOnly': True, }), ('openBadgeId', { 'type': "string", 'format': "url", 'description': "URL of the OpenBadge compliant json", 'readOnly': True, }), ('createdAt', { 'type': 'string', 'format': 'ISO8601 timestamp', 'description': "Timestamp when the Assertion was created", 'readOnly': True, }), ('createdBy', { 'type': 'string', 'format': 'entityId', 'description': "BadgeUser who created the Assertion", 'readOnly': True, }), ('badgeclass', { 'type': 'string', 'format': 'entityId', 'description': "BadgeClass that issued this Assertion", 'required': False, }), ('badgeclassOpenBadgeId', { 'type': 'string', 'format': 'url', 'description': "URL of the BadgeClass to award", 'required': False, }), ('badgeclassName', { 'type': 'string', 'format': 'string', 'description': "Name of BadgeClass to create assertion against, case insensitive", 'required': False, }), ('revoked', { 'type': 'boolean', 'description': "True if this Assertion has been revoked", 'readOnly': True, }), ('revocationReason', { 'type': 'string', 'format': "string", 'description': "Short description of why the Assertion was revoked", 'readOnly': True, }), ('acceptance', { 'type': 'string', 'description': "Recipient interaction with Assertion. One of: Unaccepted, Accepted, or Rejected", 'readOnly': True, }), ('image', { 'type': 'string', 'format': 'url', 'description': "URL to the baked assertion image", 'readOnly': True, }), ('issuedOn', { 'type': 'string', 'format': 'ISO8601 timestamp', 'description': "Timestamp when the Assertion was issued", 'required': False, }), ('narrative', { 'type': 'string', 'format': 'markdown', 'description': "Markdown narrative of the achievement", 'required': False, }), ('evidence', { 'type': 'array', 'items': { '$ref': '#/definitions/AssertionEvidence' }, 'description': "List of evidence associated with the achievement", 'required': False, }), ('recipient', { 'type': 'object', '$ref': '#/definitions/BadgeRecipient', 'description': "Recipient that was issued the Assertion", 'required': False, }), ('expires', { 'type': 'string', 'format': 'ISO8601 timestamp', 'description': "Timestamp when the Assertion expires", 'required': False, }), ]) }) def validate_issuedOn(self, value): if value > timezone.now(): raise serializers.ValidationError( "Only issuedOn dates in the past are acceptable.") if value.year < 1583: raise serializers.ValidationError( "Only issuedOn dates after the introduction of the Gregorian calendar are allowed." ) return value def update(self, instance, validated_data): updateable_fields = [ 'evidence_items', 'expires_at', 'extension_items', 'hashed', 'issued_on', 'narrative', 'recipient_identifier', 'recipient_type' ] for field_name in updateable_fields: if field_name in validated_data: setattr(instance, field_name, validated_data.get(field_name)) instance.rebake(save=True) return instance def validate(self, data): request = self.context.get('request', None) expected_issuer = self.context.get('kwargs', {}).get('issuer') badgeclass_identifiers = [ 'badgeclass_jsonld_id', 'badgeclassName', 'cached_badgeclass', 'badgeclass' ] badge_instance_properties = list(data.keys()) if 'badgeclass' in self.context: badge_instance_properties.append('badgeclass') if sum( [el in badgeclass_identifiers for el in badge_instance_properties]) > 1: raise serializers.ValidationError( 'Multiple badge class identifiers. Exactly one of the following badge class identifiers are allowed: badgeclass, badgeclassName, or badgeclassOpenBadgeId' ) if request and request.method != 'PUT': # recipient and badgeclass are only required on create, ignored on update if 'recipient_identifier' not in data: raise serializers.ValidationError( {'recipient': ["This field is required"]}) if 'cached_badgeclass' in data: # included badgeclass in request data['badgeclass'] = data.pop('cached_badgeclass') elif 'badgeclass' in self.context: # badgeclass was passed in context data['badgeclass'] = self.context.get('badgeclass') elif 'badgeclass_jsonld_id' in data: data['badgeclass'] = data.pop('badgeclass_jsonld_id') elif 'badgeclassName' in data: name = data.pop('badgeclassName') matches = BadgeClass.objects.filter(name=name, issuer=expected_issuer) len_matches = len(matches) if len_matches == 1: data['badgeclass'] = matches.first() elif len_matches == 0: raise serializers.ValidationError( "No matching BadgeClass found with name {}".format( name)) else: raise serializers.ValidationError( "Could not award; {} BadgeClasses with name {}".format( len_matches, name)) else: raise serializers.ValidationError( {"badgeclass": ["This field is required"]}) allow_duplicate_awards = data.pop('allowDuplicateAwards') if allow_duplicate_awards is False: previous_awards = BadgeInstance.objects.filter( recipient_identifier=data['recipient_identifier'], badgeclass=data['badgeclass']).filter( revoked=False).filter( Q(expires_at__isnull=True) | Q(expires_at__gt=timezone.now())) if previous_awards.exists(): raise serializers.ValidationError( "A previous award of this badge already exists for this recipient." ) if expected_issuer and data[ 'badgeclass'].issuer_id != expected_issuer.id: raise serializers.ValidationError({ "badgeclass": ["Could not find matching badgeclass for this issuer."] }) if 'badgeclass' in data: data['issuer'] = data['badgeclass'].issuer return data
class BadgeInstanceSerializerV2(DetailSerializerV2, OriginalJsonSerializerMixin): openBadgeId = serializers.URLField(source='jsonld_id', read_only=True) createdAt = serializers.DateTimeField(source='created_at', read_only=True) createdBy = EntityRelatedFieldV2(source='cached_creator', read_only=True) badgeclass = EntityRelatedFieldV2(source='cached_badgeclass', required=False, queryset=BadgeClass.cached) badgeclassOpenBadgeId = CachedUrlHyperlinkedRelatedField( source='badgeclass_jsonld_id', view_name='badgeclass_json', lookup_field='entity_id', queryset=BadgeClass.cached, required=False) issuer = EntityRelatedFieldV2(source='cached_issuer', required=False, queryset=Issuer.cached) issuerOpenBadgeId = serializers.URLField(source='issuer_jsonld_id', read_only=True) image = serializers.FileField(read_only=True) recipient = BadgeRecipientSerializerV2(source='*', required=False) issuedOn = serializers.DateTimeField(source='issued_on', required=False) narrative = MarkdownCharField(required=False, allow_null=True) evidence = EvidenceItemSerializerV2(source='evidence_items', many=True, required=False) revoked = HumanReadableBooleanField(read_only=True) revocationReason = serializers.CharField(source='revocation_reason', read_only=True) expires = serializers.DateTimeField(source='expires_at', required=False, allow_null=True) notify = HumanReadableBooleanField(write_only=True, required=False, default=False) extensions = serializers.DictField(source='extension_items', required=False, validators=[BadgeExtensionValidator()]) class Meta(DetailSerializerV2.Meta): model = BadgeInstance apispec_definition = ('Assertion', { 'properties': OrderedDict([ ('entityId', { 'type': "string", 'format': "string", 'description': "Unique identifier for this Assertion", }), ('entityType', { 'type': "string", 'format': "string", 'description': "\"Assertion\"", }), ('openBadgeId', { 'type': "string", 'format': "url", 'description': "URL of the OpenBadge compliant json", }), ('createdAt', { 'type': 'string', 'format': 'ISO8601 timestamp', 'description': "Timestamp when the Assertion was created", }), ('createdBy', { 'type': 'string', 'format': 'entityId', 'description': "BadgeUser who created the Assertion", }), ('badgeclass', { 'type': 'string', 'format': 'entityId', 'description': "BadgeClass that issued this Assertion", }), ('badgeclassOpenBadgeId', { 'type': 'string', 'format': 'url', 'description': "URL of the BadgeClass to award", }), ('revoked', { 'type': 'boolean', 'description': "True if this Assertion has been revoked", }), ('revocationReason', { 'type': 'string', 'format': "string", 'description': "Short description of why the Assertion was revoked", }), ('image', { 'type': 'string', 'format': 'url', 'description': "URL to the baked assertion image", }), ('issuedOn', { 'type': 'string', 'format': 'ISO8601 timestamp', 'description': "Timestamp when the Assertion was issued", }), ('narrative', { 'type': 'string', 'format': 'markdown', 'description': "Markdown narrative of the achievement", }), ('evidence', { 'type': 'array', 'items': { '$ref': '#/definitions/AssertionEvidence' }, 'description': "List of evidence associated with the achievement" }), ('recipient', { 'type': 'object', 'properties': OrderedDict([ ('identity', { 'type': 'string', 'format': 'string', 'description': 'Either the hash of the identity or the plaintext value' }), ('type', { 'type': 'string', 'enum': [ c[0] for c in BadgeInstance.RECIPIENT_TYPE_CHOICES ], 'description': "Type of identifier used to identify recipient" }), ('hashed', { 'type': 'boolean', 'description': "Whether or not the identity value is hashed." }), ('plaintextIdentity', { 'type': 'string', 'description': "The plaintext identity" }), ]), 'description': "Recipient that was issued the Assertion" }), ('expires', { 'type': 'string', 'format': 'ISO8601 timestamp', 'description': "Timestamp when the Assertion expires", }), ]) }) def update(self, instance, validated_data): updateable_fields = [ 'evidence_items', 'expires_at', 'extension_items', 'hashed', 'issued_on', 'narrative', 'recipient_identifier', 'recipient_type' ] for field_name in updateable_fields: if field_name in validated_data: setattr(instance, field_name, validated_data.get(field_name)) instance.save() instance.rebake() return instance def validate(self, data): request = self.context.get('request', None) if request and request.method != 'PUT': # recipient and badgeclass are only required on create, ignored on update if 'recipient_identifier' not in data: raise serializers.ValidationError( {'recipient_identifier': ["This field is required"]}) if 'cached_badgeclass' in data: # included badgeclass in request data['badgeclass'] = data.pop('cached_badgeclass') elif 'badgeclass' in self.context: # badgeclass was passed in context data['badgeclass'] = self.context.get('badgeclass') elif 'badgeclass_jsonld_id' in data: data['badgeclass'] = data.pop('badgeclass_jsonld_id') else: raise serializers.ValidationError( {"badgeclass": ["This field is required"]}) expected_issuer = self.context.get('kwargs', {}).get('issuer') if expected_issuer and data['badgeclass'].issuer != expected_issuer: raise serializers.ValidationError({ "badgeclass": ["Could not find matching badgeclass for this issuer."] }) if 'badgeclass' in data: data['issuer'] = data['badgeclass'].issuer return data