class ShareUser(AbstractBaseUser, PermissionsMixin): id = models.AutoField(primary_key=True) username = models.TextField( _('username'), unique=True, help_text=_('Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.'), validators=[ validators.MaxLengthValidator(64), validators.RegexValidator( r'^[\w.@+-]+$', _('Enter a valid username. This value may contain only ' 'letters, numbers ' 'and @/./+/-/_ characters.') ), ], error_messages={ 'unique': _("A user with that username already exists."), }, ) first_name = models.TextField(_('first name'), validators=[validators.MaxLengthValidator(64)], blank=True) last_name = models.TextField(_('last name'), validators=[validators.MaxLengthValidator(64)], blank=True) email = models.EmailField(_('email address'), blank=True) gravatar = ShareURLField(blank=True) time_zone = models.TextField(validators=[validators.MaxLengthValidator(100)], blank=True) locale = models.TextField(validators=[validators.MaxLengthValidator(100)], blank=True) is_staff = models.BooleanField( _('staff status'), default=False, help_text=_('Designates whether the user can log into this admin site.'), ) is_active = models.BooleanField( _('active'), default=True, help_text=_( 'Designates whether this user should be treated as active. ' 'Unselect this instead of deleting accounts.' ), ) date_joined = models.DateTimeField(_('date joined'), default=timezone.now) robot = models.TextField(validators=[validators.MaxLengthValidator(40)], blank=True) long_title = models.TextField(validators=[validators.MaxLengthValidator(100)], blank=True) home_page = ShareURLField(blank=True) def get_short_name(self): return self.robot if self.is_robot else self.username def get_full_name(self): return '{} {}'.format(self.first_name, self.last_name) @property def is_robot(self): return self.robot != '' objects = ShareUserManager() USERNAME_FIELD = 'username' class Meta: verbose_name = _('Share user') verbose_name_plural = _('Share users')
class Venue(ShareObject): name = models.TextField(blank=True) venue_type = ShareURLField(blank=True) location = ShareURLField(blank=True) community_identifier = ShareURLField(blank=True) def __str__(self): return self.name
class Award(ShareObject): # ScholarlyArticle has an award object # it's just a text field, I assume our 'description' covers it. award = ShareURLField(blank=True) description = models.TextField(blank=True) url = ShareURLField(blank=True) entities = ShareManyToManyField('Entity', through='ThroughAwardEntities') def __str__(self): return self.description
class AbstractCreativeWork(ShareObject, metaclass=TypedShareObjectMeta): title = models.TextField(blank=True) description = models.TextField(blank=True) contributors = ShareManyToManyField(Person, through='Contributor') awards = ShareManyToManyField(Award, through='ThroughAwards') venues = ShareManyToManyField(Venue, through='ThroughVenues') links = ShareManyToManyField('Link', through='ThroughLinks') funders = ShareManyToManyField('Funder', through='Association') publishers = ShareManyToManyField('Publisher', through='Association') institutions = ShareManyToManyField('Institution', through='Association') organizations = ShareManyToManyField('Organization', through='Association') subject = ShareForeignKey(Tag, related_name='subjected_%(class)s', null=True) # Note: Null allows inserting of None but returns it as an empty string tags = ShareManyToManyField(Tag, related_name='tagged_%(class)s', through='ThroughTags') date_created = models.DateTimeField(null=True, db_index=True) date_published = models.DateTimeField(null=True, db_index=True) date_updated = models.DateTimeField(null=True, db_index=True) free_to_read_type = ShareURLField(blank=True, db_index=True) free_to_read_date = models.DateTimeField(null=True, db_index=True) rights = models.TextField(blank=True, null=True, db_index=True) language = models.TextField(blank=True, null=True, db_index=True) def __str__(self): return self.title
class Person(ShareObject): family_name = models.TextField(blank=True, db_index=True) # last given_name = models.TextField(blank=True, db_index=True) # first additional_name = models.TextField(blank=True, db_index=True) # can be used for middle suffix = models.TextField(blank=True, db_index=True) emails = ShareManyToManyField(Email, through='PersonEmail') affiliations = ShareManyToManyField('Entity', through='Affiliation') # this replaces "authority_id" and "other_identifiers" in the diagram identifiers = ShareManyToManyField(Identifier, through='ThroughIdentifiers') location = models.TextField(blank=True) url = ShareURLField(blank=True) def __str__(self): return self.get_full_name() def get_full_name(self): return ' '.join(x for x in [ self.given_name, self.family_name, self.additional_name, self.suffix ] if x) class Meta: verbose_name_plural = 'People' index_together = (('family_name', 'given_name', 'additional_name', 'suffix'))
class AgentIdentifier(ShareObject): """Unique identifier (in IRI form) for an agent.""" uri = ShareURLField(unique=True) host = models.TextField(editable=False) scheme = models.TextField(editable=False) agent = ShareForeignKey('AbstractAgent', related_name='identifiers') # objects = FilteredEmailsManager() # objects_unfiltered = models.Manager() @classmethod def normalize(self, node, graph): try: ret = IRILink().execute(node.attrs['uri']) except InvalidIRI as e: logger.warning('Discarding invalid identifier %s with error %s', node.attrs['uri'], e) graph.remove(node) return if node.attrs['uri'] != ret['IRI']: logger.debug('Normalized %s to %s', node.attrs['uri'], ret['IRI']) node.attrs = { 'uri': ret['IRI'], 'host': ret['authority'], 'scheme': ret['scheme'], } def __repr__(self): return '<{}({}, {})>'.format(self.__class__.__name__, self.uri, self.agent_id) class Disambiguation: all = ('uri', )
class Subject(ShareObject): name = models.TextField() is_deleted = models.BooleanField(default=False) uri = ShareURLField(null=True, blank=True) taxonomy = models.ForeignKey(SubjectTaxonomy, editable=False, on_delete=models.CASCADE) parent = ShareForeignKey('Subject', blank=True, null=True, related_name='children') central_synonym = ShareForeignKey('Subject', blank=True, null=True, related_name='custom_synonyms') @classmethod def normalize(cls, node, graph): edge = node.related('central_synonym') if edge and edge.related and edge.related.id == node.id: graph.remove_edge(edge) def save(self, *args, **kwargs): if self.id is not None and self.parent is not None: new_lineage = self.parent.lineage() if self in new_lineage: raise CyclicalTaxonomyError( 'Making {} a child of {} would cause a cycle!'.format( self, self.parent)) return super().save(*args, **kwargs) def lineage(self): query = ''' WITH RECURSIVE lineage_chain(id, parent, depth, path, cycle) AS ( SELECT id, parent_id, 1, ARRAY[id], false FROM {table} WHERE id = %(id)s UNION SELECT {table}.id, {table}.parent_id, lineage_chain.depth + 1, path || {table}.id, {table}.id = ANY(path) FROM lineage_chain JOIN {table} ON lineage_chain.parent = {table}.id WHERE NOT cycle ) SELECT {table}.* FROM {table} INNER JOIN lineage_chain ON {table}.id = lineage_chain.id ORDER BY lineage_chain.depth DESC '''.format(table=self._meta.db_table) lineage = list( self._meta.model.objects.raw(query, params={'id': self.id})) if lineage[0].parent is not None: raise CyclicalTaxonomyError( 'Subject taxonomy cycle! {}'.format(lineage)) return lineage def __str__(self): return self.name class Meta: unique_together = (('name', 'taxonomy'), ('uri', 'taxonomy')) class Disambiguation: all = ('name', 'central_synonym')
class Award(ShareObject): # ScholarlyArticle has an award object # it's just a text field, I assume our 'description' covers it. name = models.TextField(blank=True) description = models.TextField(blank=True) date = models.DateTimeField(blank=True, null=True) award_amount = models.PositiveIntegerField(blank=True, null=True) uri = ShareURLField(unique=True, blank=True, null=True) def __str__(self): return self.description matching_criteria = MatchByAttrs('uri')
class AbstractCreativeWork(ShareObject, metaclass=TypedShareObjectMeta): title = models.TextField(blank=True, help_text='') description = models.TextField(blank=True, help_text='') is_deleted = models.BooleanField( default=False, help_text= _('Determines whether or not this record will be discoverable via search.' )) date_published = models.DateTimeField(null=True) date_updated = models.DateTimeField(null=True) free_to_read_type = ShareURLField(blank=True) free_to_read_date = models.DateTimeField(null=True) rights = models.TextField(blank=True, null=True) language = models.TextField( blank=True, null=True, help_text= _('The ISO 3166-1 alpha-2 country code indicating the language of this record.' )) subjects = ShareManyToManyField(Subject, related_name='subjected_works', through='ThroughSubjects') tags = ShareManyToManyField(Tag, related_name='tagged_works', through='ThroughTags') related_agents = ShareManyToManyField('AbstractAgent', through='AbstractAgentWorkRelation') related_works = ShareManyToManyField('AbstractCreativeWork', through='AbstractWorkRelation', through_fields=('subject', 'related'), symmetrical=False) @classmethod def normalize(self, node, graph): for k, v in tuple(node.attrs.items()): if isinstance(v, str): node.attrs[k] = strip_whitespace(v) if node.attrs[k] == 'null': node.attrs[k] = '' class Disambiguation: any = ('identifiers', ) class Meta: db_table = 'share_creativework' def __str__(self): return self.title
class WorkIdentifier(ShareObject): """ Unique identifier (in IRI form) for a creative work. """ uri = ShareURLField(unique=True) host = models.TextField(editable=False) scheme = models.TextField( editable=False, help_text= _('A prefix to URI indicating how the following data should be interpreted.' )) creative_work = ShareForeignKey('AbstractCreativeWork', related_name='identifiers') # objects = FilteredEmailsManager() # objects_unfiltered = models.Manager() @classmethod def normalize(self, node, graph): try: ret = IRILink().execute(node.attrs['uri']) except InvalidIRI as e: logger.warning('Discarding invalid identifier %s with error %s', node.attrs['uri'], e) graph.remove(node) return if ret['authority'] in {'issn', 'orcid.org' } or ret['scheme'] in {'mailto'}: logger.warning( 'Discarding %s %s as an invalid identifier for works', ret['authority'], ret['IRI']) graph.remove(node) return if node.attrs['uri'] != ret['IRI']: logger.debug('Normalized %s to %s', node.attrs['uri'], ret['IRI']) node.attrs = { 'uri': ret['IRI'], 'host': ret['authority'], 'scheme': ret['scheme'], } def __repr__(self): return '<{}({}, {})>'.format(self.__class__.__name__, self.uri, self.creative_work_id) class Disambiguation: all = ('uri', )
class AgentIdentifier(ShareObject): """Unique identifier (in IRI form) for an agent.""" uri = ShareURLField(unique=True) host = models.TextField(blank=True) scheme = models.TextField(blank=True) agent = ShareForeignKey('AbstractAgent', related_name='identifiers') # objects = FilteredEmailsManager() # objects_unfiltered = models.Manager() def __repr__(self): return '<{}({}, {})>'.format(self.__class__.__name__, self.uri, self.agent_id) matching_criteria = MatchByAttrs('uri')
class Entity(ShareObject, metaclass=TypedShareObjectMeta): url = ShareURLField(blank=True) name = models.TextField() location = models.TextField(blank=True) affiliations = ShareManyToManyField('Person', through='Affiliation') class Meta: verbose_name_plural = 'Entities' index_together = (( 'type', 'name', )) def __str__(self): return self.name
class WorkIdentifier(ShareObject): """ Unique identifier (in IRI form) for a creative work. """ uri = ShareURLField(unique=True) host = models.TextField(blank=True) scheme = models.TextField(blank=True, help_text=_('A prefix to URI indicating how the following data should be interpreted.')) creative_work = ShareForeignKey('AbstractCreativeWork', related_name='identifiers') # objects = FilteredEmailsManager() # objects_unfiltered = models.Manager() def __repr__(self): return '<{}({}, {})>'.format(self.__class__.__name__, self.uri, self.creative_work_id) matching_criteria = MatchByAttrs('uri')
class Award(ShareObject): # ScholarlyArticle has an award object # it's just a text field, I assume our 'description' covers it. name = models.TextField(blank=True) description = models.TextField(blank=True) date = models.DateTimeField(blank=True, null=True) award_amount = models.PositiveIntegerField(blank=True, null=True) uri = ShareURLField(unique=True, blank=True, null=True) @classmethod def normalize(self, node, graph): for k, v in tuple(node.attrs.items()): if isinstance(v, str): node.attrs[k] = strip_whitespace(v) def __str__(self): return self.description class Disambiguation: any = ('uri', )
class AbstractCreativeWork(ShareObject, metaclass=TypedShareObjectMeta): title = models.TextField(blank=True) description = models.TextField(blank=True) # Used to determine if something should be surfaced in ES or not # this may need to be renamed later is_deleted = models.BooleanField(default=False) contributors = ShareManyToManyField(Person, through='Contributor') awards = ShareManyToManyField(Award, through='ThroughAwards') venues = ShareManyToManyField(Venue, through='ThroughVenues') links = ShareManyToManyField('Link', through='ThroughLinks') funders = ShareManyToManyField('Funder', through='Association') publishers = ShareManyToManyField('Publisher', through='Association') institutions = ShareManyToManyField('Institution', through='Association') organizations = ShareManyToManyField('Organization', through='Association') subjects = ShareManyToManyField(Subject, related_name='subjected_%(class)s', through='ThroughSubjects') # Note: Null allows inserting of None but returns it as an empty string tags = ShareManyToManyField(Tag, related_name='tagged_%(class)s', through='ThroughTags') date_published = models.DateTimeField(null=True, db_index=True) date_updated = models.DateTimeField(null=True, db_index=True) free_to_read_type = ShareURLField(blank=True, db_index=True) free_to_read_date = models.DateTimeField(null=True, db_index=True) rights = models.TextField(blank=True, null=True, db_index=True) language = models.TextField(blank=True, null=True, db_index=True) def __str__(self): return self.title
class Institution(Entity): # TODO: ScholarlyArticle says this should be an Organization isni = ShareURLField(blank=True) ringgold = ShareURLField(blank=True)
class AbstractCreativeWork(ShareObject, metaclass=TypedShareObjectMeta): title = models.TextField(blank=True, help_text='') description = models.TextField(blank=True, help_text='') is_deleted = models.BooleanField( default=False, help_text= _('Determines whether or not this record will be discoverable via search.' )) date_published = models.DateTimeField(blank=True, null=True) date_updated = models.DateTimeField(blank=True, null=True) free_to_read_type = ShareURLField(blank=True) free_to_read_date = models.DateTimeField(blank=True, null=True) rights = models.TextField(blank=True, null=True) language = models.TextField( blank=True, null=True, help_text= _('The ISO 3166-1 alpha-2 country code indicating the language of this record.' )) subjects = ShareManyToManyField(Subject, related_name='subjected_works', through='ThroughSubjects') tags = ShareManyToManyField(Tag, related_name='tagged_works', through='ThroughTags') related_agents = ShareManyToManyField('AbstractAgent', through='AbstractAgentWorkRelation') related_works = ShareManyToManyField('AbstractCreativeWork', through='AbstractWorkRelation', through_fields=('subject', 'related'), symmetrical=False) @classmethod def normalize(self, node, graph): for k, v in tuple(node.attrs.items()): if isinstance(v, str): node.attrs[k] = strip_whitespace(v) if node.attrs[k] == 'null': node.attrs[k] = '' class Disambiguation: any = ('identifiers', ) class Meta(ShareObject.Meta): db_table = 'share_creativework' verbose_name_plural = 'Creative Works' def defrankenize(self, *_, im_really_sure_about_this=False): if not im_really_sure_about_this: raise ValueError('You have to be really sure about this') logger.info('Defrankenizing %r', self) with transaction.atomic(): logger.info('Removing relations') for field in AbstractCreativeWork._meta.get_fields(): if not field.one_to_many or field.name in ('changes', 'versions'): continue logger.warning('Removing all %s', field.related_name) relation = getattr(self, field.get_accessor_name()) num_deleted, stats = Change.objects.filter( id__in=relation.values_list('change_id', flat=True)).delete() logger.warning('Deleted %d changes to remove %s', num_deleted, field.related_name) assert num_deleted == stats.pop('share.Change', 0) if stats: logger.error('Unexpectedly removed other rows, %r', stats) raise ValueError( 'Unexpectedly removed other rows, {!r}'.format(stats)) logger.info('Relations removed') self.administrative_change(is_deleted=True, title='Defrankenized work') def __str__(self): return self.title
class Identifier(ShareObject): # https://twitter.com/berniethoughts/ url = ShareURLField() # https://twitter.com/ base_url = ShareURLField()
class Funder(Entity): # TODO: ScholarlyArticle says this should be a DiscourseElement # http://purl.org/spar/deo/DiscourseElement # many fields are missing but seem extraneous to our purpose funder_region = ShareURLField(blank=True) community_identifier = ShareURLField(blank=True)