fairdm.contrib.contributors.models#
Models for people, organisations, their identifiers and their credits on research objects.
Module Contents#
Classes#
A person or organisation credited on projects, datasets, samples or measurements. |
|
An individual contributor who can also sign in as a user. |
|
A person’s membership of an organisation, with time bounds and a verification state. |
|
A contributor that represents a group of people, such as a university, institute, company or agency. |
|
A contributor’s credit on a project, dataset, sample or measurement, based on the DataCite contributor schema. |
|
An external identifier of a contributor, such as an ORCID iD or a ROR id. |
|
The ways a profile can be claimed. |
|
Manager with filters for the claiming audit log. |
|
Immutable audit trail of profile claiming events, created once and never updated. |
Functions#
Return the empty default permissions dict, which migration 0001 references. |
|
Keep one primary email address for each user with several, preferring the one matching the user’s email. |
Data#
The form of an ORCID iD, without its address. |
|
The form of a ROR ID, without its address. |
|
API#
- logger#
‘getLogger(…)’
- ORCID_PATTERN#
‘^\d{4}-\d{4}-\d{4}-\d{3}[0-9X]$’
The form of an ORCID iD, without its address.
- ROR_PATTERN#
‘^0[a-z0-9]{6}[0-9]{2}$’
The form of a ROR ID, without its address.
- contributor_permissions_default() dict#
Return the empty default permissions dict, which migration 0001 references.
Returns: An empty dict.
- class Contributor(*args, **kwargs)#
Bases:
fairdm.utils.models.PolymorphicMixin,fairdm.db.models.PolymorphicModelA person or organisation credited on projects, datasets, samples or measurements.
Holds the public information needed for attribution and publication, aligned with the DataCite contributor schema. The model is polymorphic, with :class:
Personand :class:Organizationas its concrete types, and :class:Contributionlinks a contributor to a research object.Attributes: uuid: Public identifier. image: Profile image. name: Preferred name. alternative_names: Other names by which the contributor is known. profile: Free-text description. links: URLs of related online resources. lang: ISO 639-1 language codes. last_synced: When the contributor was last synced with an external provider. synced_data: Raw data from the external provider. config: General-purpose configuration data. added: When the record was created. modified: When the record was last modified. tracker: Tracks field changes, used to stamp
last_synced. is_organization: Whether the concrete type is :class:Organization.Initialization
- is_organization#
False
- uuid#
‘ShortUUIDField(…)’
- image#
‘ThumbnailerImageField(…)’
- name#
‘CharField(…)’
- alternative_names#
‘JSONField(…)’
- profile#
‘TextField(…)’
- links#
‘JSONField(…)’
- lang#
‘JSONField(…)’
- last_synced#
‘DateField(…)’
- synced_data#
‘JSONField(…)’
- config#
‘JSONField(…)’
- added#
‘DateTimeField(…)’
- modified#
‘DateTimeField(…)’
- tracker#
‘FieldTracker(…)’
- save(*args, **kwargs)#
Stamp
last_syncedwhensynced_datahas changed, then save.
- static base_class()#
Return
Contributor, whichPolymorphicMixinneeds as the base class.
- get_absolute_url()#
Return the URL of the contributor’s overview page.
- get_update_url()#
Return the URL of the page where the contributor’s profile is edited.
- is_editable_by(user) bool#
Say whether a user may edit this contributor’s profile in the portal.
The editing page asks on every request and the overview pages ask before they offer an edit action. A contributor nobody may edit is the default; each subclass names who may.
Args: user: The user, or an anonymous user for a visitor.
Returns: True when the user may open and save the editing page.
- get_identifier_icon()#
Return the icon for the contributor’s default identifier scheme.
Returns: The rendered icon.
- get_default_identifier()#
Return the contributor’s identifier of the default scheme.
Reads
identifiers.all(), so a listing that prefetchesidentifierscosts no query per contributor.Returns: The identifier, or None when there is none.
- property default_identifier#
The contributor’s identifier of the default scheme, or None.
- property primary_organization#
The organisation the contributor is shown with: None here, a person overrides it.
- profile_image()#
Return the URL of the profile image, or of the brand icon when there is none.
Returns: The image URL.
- get_initials()#
Return initials from the first letter of the first two words in the name.
Returns: Up to two capital letters, or an empty string when there is no name.
- type_of()#
Contributor, whichPolymorphicMixinneeds as the base type.
- type()#
Return the name of the concrete model, such as
person.Returns: The model name of the polymorphic content type.
- credited_object_ids(base_model)#
Return the object ids of this contributor’s credits on
base_modelor a subclass.Subclass rows share their base row’s primary key, so the ids are also
base_modelprimary keys. Credits on samples and measurements are stored under the concrete subclass’s content type, which a reverseGenericRelationfrom the base cannot match.Args: base_model: The polymorphic base model, such as
Sample.Returns: The credited objects’ ids.
- property projects#
The projects this contributor is credited on.
- property datasets#
The datasets this contributor is credited on.
- get_public_projects()#
List the public projects this contributor is credited on.
The one source behind a contributor’s page: its figure, its card and its Projects tab. A subclass that has more projects to show overrides it.
Returns: The projects, as a queryset.
- get_public_datasets()#
List the public datasets this contributor is credited on, outside private projects.
The one source behind a contributor’s page: its figure, its card and its Datasets tab. A subclass that has more datasets to show overrides it.
Returns: The datasets, as a queryset.
- property samples#
The samples this contributor is credited on, whatever their concrete type.
- property measurements#
The measurements this contributor is credited on, whatever their concrete type.
- get_credit_counts()#
Count this contributor’s credits for each kind of research output.
Returns: A map of each credited model’s plural verbose name to its count.
- to_datacite()#
Export the contributor as a DataCite 4.4 creator or contributor object.
Returns: The DataCite metadata.
- to_schema_org()#
Export the contributor as a Schema.org
PersonorOrganization.Returns: The JSON-LD metadata.
- to_public_schema_org()#
Export the contributor as Schema.org metadata that is safe to publish on a page.
Returns: The JSON-LD metadata without the email address, which a public page never shows.
- get_links_display()#
List the contributor’s links, each with the site it points at.
Returns: One
{"url", "host"}entry per web link, in the order recorded. A link that is nothttporhttpsis left out, so a page never draws a link that runs a script.
- get_language_names()#
Name the contributor’s languages in the active language.
Returns: One name per recorded language code.
- get_visible_contributions(user)#
List the contributor’s credits on records a profile may name, newest first.
A project or dataset counts only when it is one of :meth:
get_public_projectsor :meth:get_public_datasets, whoever is asking. A sample or measurement counts when its own dataset lets the user see it and that dataset’s project, where it has one, is public. Each contribution returned carrieskind(project,dataset,sampleormeasurement). Projects and datasets also carryrecord; samples and measurements are resolved to ids and not loaded, as no card lists them.Args: user: The user, or an anonymous user for a visitor.
Returns: The contributions, with their roles prefetched.
- get_role_counts(contributions=None)#
Count how many credits each contribution role is held on.
Args: contributions: The credits to count over, such as those from :meth:
get_visible_contributions. Defaults to every credit.Returns: A
Counterof role labels.
- get_recent_contributions(limit: int = 5)#
Return the contributor’s most recent contributions.
Args: limit: Maximum number of contributions to return.
Returns: Contributions, newest first.
- get_contributions_by_type(model_name: str)#
Return the contributor’s contributions to one type of object.
Args: model_name: The model name, such as
project,dataset,sampleormeasurement.Returns: Contributions to that type.
Example: >>> person.get_contributions_by_type(“project”) <QuerySet [<Contribution: John Doe: [‘ContactPerson’]>]>
- has_contribution_to(obj) bool#
Check whether this contributor is credited on an object.
Args: obj: A Project, Dataset, Sample or Measurement instance.
Returns: True when the contributor is credited on the object.
Example: >>> person.has_contribution_to(my_project) True
- get_collaborators(limit: int | None = None, contributions=None)#
Return other contributors credited on the same objects, most frequent first.
Args: limit: Maximum number of collaborators to return. Defaults to all. contributions: The credits to look for collaborators on, such as those from :meth:
get_visible_contributions. Defaults to every credit.Returns: Contributors annotated with
collaboration_count.Example: >>> person.get_collaborators(limit=5) <QuerySet [<Person: Jane Smith>, <Person: Bob Wilson>, …]>
- add_to(obj, roles=None)#
Credit the contributor on an object, adding roles to any already recorded.
A person credited for the first time starts at the view level.
Args: obj: A project, dataset, sample or measurement. roles: Addresses of contributor role concepts.
Returns: The contribution, created if it did not exist.
- class Person(*args, **kwargs)#
Bases:
django.contrib.auth.models.AbstractUser,fairdm.contrib.contributors.models.ContributorAn individual contributor who can also sign in as a user.
A person may be a ghost (no email), invited (email, unclaimed), claimed, or inactive. See :attr:
account_state.Attributes: DEFAULT_IDENTIFIER: The address of the identifier type shown by default. DEFAULT_IDENTIFIER_ICON: The icon of that identifier type. objects: The person manager. email: Login address, null for an unclaimed profile created for attribution alone. is_claimed: Whether the person has claimed the account. USERNAME_FIELD: The field used to sign in, which is the email. REQUIRED_FIELDS: Fields required by
createsuperuser, which is none. username: Removed, as people sign in by email.Initialization
- DEFAULT_IDENTIFIER#
None
- DEFAULT_IDENTIFIER_ICON#
‘ORCID’
- objects#
‘UserManager(…)’
- email#
‘EmailField(…)’
- is_claimed#
‘BooleanField(…)’
- USERNAME_FIELD#
‘email’
- REQUIRED_FIELDS#
[]
- username#
None
- save(*args, **kwargs)#
Fill a blank name from the first and last names, then save.
- is_editable_by(user) bool#
Say whether a user may edit this profile: the person it describes, or a community manager.
A community manager may edit it only while nobody can sign in to it and keep it themselves, as :meth:
can_sign_insays. A superuser, an administrator and the holder of any other portal role get no right from that alone.Args: user: The user, or an anonymous user for a visitor.
Returns: True when the user is this person with an active account, or an active community manager and the person has no active account.
- can_sign_in() bool#
Say whether this person’s account is in use or can be: active, and claimed or signed in.
account_statealone cannot say that, because an account made withcreatesuperuseris active and in use without being marked claimed.Returns: True when the account is active and the person has claimed it or has signed in.
- property account_state: fairdm.contrib.contributors.choices.AccountState#
The person’s account state, derived from
is_active,is_claimedandemail.The precedence is inactive, then claimed, then invited when there is an email, then ghost. The
PersonQuerySetstate filters mirror this order.Returns: Exactly one of INACTIVE, CLAIMED, INVITED or GHOST.
- clean()#
Normalise the email and validate it, the links and the ORCID iD.
- orcid()#
Return the person’s ORCID identifier.
Returns: The identifier, or None when there is none.
- get_provider(provider: str)#
Return the person’s social account for a provider.
Args: provider: The allauth provider id, such as
orcid.Returns: The social account, or None when there is none.
- primary_affiliation()#
Return the person’s primary affiliation when it is verified and has not ended.
A primary affiliation that is still pending, or has an end date, is not returned, so nothing that reads this names an organization the person’s page does not. Reads
affiliations.all(), so a listing that prefetchesaffiliations__organizationcosts no query per person.Returns: The primary affiliation, or None when there is none that is verified and current.
- property primary_organization#
The organisation of the verified, current primary affiliation, or None.
- property portal_roles#
The labels of the portal roles the person holds, in declaration order.
An inactive person holds none, as on the portal’s team page. Reads
groups.all(), so a listing that prefetchesgroupscosts no query per person.
- get_initials()#
Return the initials of the given and family names, else of the preferred name.
Returns: Up to two capital letters, or an empty string when there is no name.
- property member_since#
When the person’s account was created, or None for a profile nobody has claimed.
- get_affiliation_history()#
Split the person’s verified affiliations into current and past.
Reads
affiliations.all(), so a listing that prefetchesaffiliations__organizationcosts no query per person.Returns:
current, the primary affiliation first and the rest by organization name, andpast, most recently ended first. Each affiliation has its organization loaded.
- get_profile_completeness()#
Say which parts of a complete profile the person has filled in.
Returns: A flag for each of
image,orcid(connected by signing in with ORCID),profile,primary_affiliation(verified and not ended, as the header shows it) andlinks.
- current_affiliations()#
Return the person’s verified affiliations that have not ended.
Returns: Affiliations with no end date and a type of member or above.
- property given#
The person’s first name.
- property family#
The person’s last name.
- get_full_name_display(name_format: str = 'given_family') str#
Format the person’s name.
Args: name_format: One of
given_family(“John Doe”),family_given(“Doe, John”),family_initial(“Doe, J.”) orinitials_family(“J. Doe”). Any other value is treated asgiven_family.Returns: The formatted name, or
namewhen the first and last names are both empty.
- property orcid_is_authenticated#
Whether the person has signed in with ORCID.
Reads
socialaccount_set.all(), so a listing that prefetches it costs no query per person.
- icon()#
Return the icon name, showing whether the ORCID iD is authenticated.
Returns:
orcidororcid_unauthenticated.
- classmethod from_orcid(orcid_id)#
Create or update a person from ORCID data and schedule a full sync after commit.
Args: orcid_id: The ORCID iD, such as
0000-0002-1825-0097.Returns: The created or updated person.
- as_geojson()#
Return the primary affiliation’s organisation location as a GeoJSON feature.
Returns: The feature as a JSON string, or None when there is no located affiliation.
- get_location_display()#
Return the city and country of the organisation the profile names as primary.
That is the verified primary affiliation that has not ended, the one the page header shows. Reads
affiliations.all(), so a prefetch costs no query per person.Returns: The location text, or None when there is no such affiliation.
- class Affiliation(*args, **kwargs)#
Bases:
fairdm.db.models.ModelA person’s membership of an organisation, with time bounds and a verification state.
The type runs from pending (declared, unverified) through member and admin to owner, which maps to
manage_organization. Settingend_dateends the rights the type confers, asAffiliationQuerySet.owners()reads only current affiliations.Attributes: objects: The affiliation manager. tracker: Tracks field changes. person: The affiliated person. organization: The organisation. type: The verification state and role, from 0 to 3. is_primary: Whether this is the person’s primary affiliation for citation. start_date: When the affiliation began, with variable precision. end_date: When it ended. Null means it is still active.
Initialization
- objects#
‘AffiliationManager(…)’
- tracker#
‘FieldTracker(…)’
- class MembershipType(*args, **kwds)#
Bases:
fairdm.db.models.IntegerChoicesVerification state and role within an organisation.
Initialization
- PENDING#
(0,)
- MEMBER#
(1,)
- ADMIN#
(2,)
- OWNER#
(3,)
- person#
‘ForeignKey(…)’
- organization#
‘ForeignKey(…)’
- type#
‘IntegerField(…)’
- is_primary#
‘BooleanField(…)’
- start_date#
‘PartialDateField(…)’
- end_date#
‘PartialDateField(…)’
- property start_display#
The start of the affiliation, as precisely as it was recorded, or an empty string.
- property end_display#
The end of the affiliation, as precisely as it was recorded, or an empty string.
- clean()#
Refuse a second membership of the same organisation with a readable message.
- save(*args, **kwargs)#
Demote the person’s other primary affiliation in the same transaction, then save.
- OrganizationMember#
None
- class Organization(*args, **kwargs)#
Bases:
fairdm.contrib.contributors.models.ContributorA contributor that represents a group of people, such as a university, institute, company or agency.
An organisation can have members and sub-organisations, such as departments or research groups.
Attributes: DEFAULT_IDENTIFIER: The address of the identifier type shown by default. DEFAULT_IDENTIFIER_ICON: The icon of that identifier type. type: The kind of institution, from ROR’s organisation types. members: The people affiliated with the organisation. parent: The organisation this one is part of. city: The city where the organisation is based. country: The country where the organisation is based. location: The point where the organisation is, in WGS 84.
Initialization
- DEFAULT_IDENTIFIER#
None
- DEFAULT_IDENTIFIER_ICON#
‘ROR’
- is_organization#
True
- objects#
‘(…)’
- type#
‘CharField(…)’
- members#
‘ManyToManyField(…)’
- parent#
‘ForeignKey(…)’
- city#
‘CharField(…)’
- country#
‘CountryField(…)’
- location#
‘PointField(…)’
- property lat#
The location’s latitude, or None without a location.
- property lon#
The location’s longitude, or None without a location.
- clean()#
Refuse a parent that would make the organization part of itself, then validate the links and the ROR identifier.
- update_identifier()#
Create the ROR identifier from
synced_dataafter the organisation is created.
- classmethod from_ror(ror, commit=True)#
Create or update an organisation from a ROR record and, when committing, schedule a full sync after commit.
Args: ror: The ROR identifier, such as
https://ror.org/04aj4c181. commit: Whether to schedule the full sync. The organisation is saved either way.Returns: The created or updated organisation.
- icon()#
Return the icon name.
Returns:
organization.
- get_memberships()#
Return the organisation’s affiliations with their people loaded.
Returns: Affiliations with
personselected.
- get_public_projects()#
List the public projects the organisation owns or is credited on, none twice.
Returns: The projects, as a queryset.
- get_public_datasets()#
List the public datasets the organisation is credited on or that sit in its projects.
A dataset in a private project is left out, whoever owns the project, and none is listed twice.
Returns: The datasets, as a queryset.
- get_current_memberships()#
List the organisation’s verified current members, the people who run it first.
Returns: Affiliations with
personloaded: the owner, then administrators, then members, each group by name.
- has_member(user) bool#
Check whether a user is a verified current member.
Args: user: The user, or an anonymous user for a visitor.
Returns: True when the user holds a current affiliation of type member or above.
- is_managed_by(user) bool#
Check whether a user keeps this organisation’s record: its owner or an administrator.
Args: user: The user, or an anonymous user for a visitor.
Returns: True when the user holds a current affiliation of type administrator or owner.
- is_editable_by(user) bool#
Say whether a user may edit this organization’s profile.
The owner and the administrators with a current affiliation may, and so may a community manager, while their account is active. A superuser, the holder of any other portal role and an ordinary member get no right from that alone.
Args: user: The user, or an anonymous user for a visitor.
Returns: True when the user is active and keeps this organization’s record or is a community manager.
- get_descendant_ids()#
Find every organization beneath this one, at any depth.
Walks the chain downward a level at a time, so a loop already stored ends when it comes back to an organization it has seen.
Returns: The primary keys of the sub-organizations, their own sub-organizations and so on. Empty for an organization that is not saved yet.
- get_hierarchy()#
Place the organisation among the organisations around it.
Returns:
parent(or None),siblings(the parent’s sub-organisations, this one included, by name; empty without a parent) andchildren(this organisation’s direct sub-organisations, by name).
- get_record_completeness()#
Say which parts of a complete organisation record are filled in.
Returns: A flag for each of
ror,image,type,location(city and country),profileandlinks.
- owner()#
Return the organisation’s current owner, derived through
AffiliationQuerySet.owners().Returns: The owner, or None when there is none.
- transfer_ownership(new_owner)#
Make an existing member the owner, demoting each current owner to admin, atomically.
Only affiliation records change, as management rights are derived from them. Ended affiliations are left as they are.
Args: new_owner: The person to become owner. Must be an active, claimed account with a current affiliation of member type or above.
Raises: ValidationError: The person is not a member, holds a pending or ended affiliation, or is an unclaimed or deactivated account.
- as_geojson()#
Return the organisation’s location as a GeoJSON feature.
Returns: The feature as a JSON string, or None when there is no location.
- get_location_display()#
Return the organisation’s city and country.
Returns: The location text, or None when both are empty.
- property summary#
The organisation’s type and place, joined by a middle dot, or an empty string.
- get_initials()#
Return the organisation’s acronym when its name starts with one, else its initials.
“GFZ Helmholtz Centre” gives “GFZ” and “University of Potsdam” gives “UP”.
Returns: The acronym or up to two capital letters, or an empty string when there is no name.
- CONTRIBUTION_UNIQUE_PAIRING_MESSAGE#
‘_(…)’
- class Contribution(*args, **kwargs)#
Bases:
django_lifecycle.mixins.LifecycleModelMixin,ordered_model.models.OrderedModelA contributor’s credit on a project, dataset, sample or measurement, based on the DataCite contributor schema.
Attributes: objects: The contribution manager. content_type: The type of the credited object. object_id: The id of the credited object. content_object: The credited object. contributor: The person or organisation credited. roles: The roles held on this credit. affiliation: The organisation a person is credited from on this record, or None. It is kept with the record and does not follow the person’s profile. level: What a person may do on the credited object. Empty for an organisation.
Initialization
- objects#
‘ContributionManager(…)’
- content_type#
‘ForeignKey(…)’
- object_id#
‘CharField(…)’
- content_object#
‘GenericForeignKey(…)’
- contributor#
‘ForeignKey(…)’
- roles#
‘MultiConceptField(…)’
- affiliation#
‘ForeignKey(…)’
- level#
‘PositiveSmallIntegerField(…)’
- class Meta#
- verbose_name#
‘_(…)’
- verbose_name_plural#
‘_(…)’
- constraints#
None
- indexes#
None
- ordering#
[‘object_id’, ‘order’]
- clean()#
Refuse a duplicate credit and any stored role outside the roles vocabulary.
- static starting_level(contributor)#
Return the level a contributor holds when first credited on a record.
Args: contributor: The person or organization being credited.
Returns: The view level for a person, as for one added from the Contributors tab, and None for an organization, which holds no level.
- classmethod add_to(contributor, obj, roles=None, affiliation=None)#
Credit a contributor on an object, adding roles to any already recorded.
A person credited for the first time starts at the view level. A sample or measurement held as its base class is credited under its real type, which is where its contributors and their levels are read.
Args: contributor: The person or organisation to credit. obj: The project, dataset, sample or measurement. roles: Addresses of contributor role concepts. affiliation: The organisation to set on a newly created credit.
Returns: The contribution, created if it did not exist.
- save(*args, **kwargs)#
Refuse to credit a superuser outside debug mode, then save.
- is_person()#
Check whether the contributor is a person.
Returns: True when the contributor is a
Person.
- get_absolute_url()#
Return the URL of the contributor’s profile.
- get_update_url()#
Return the URL of the edit page for the credited object’s contributors.
- class ContributorIdentifier(*args, **kwargs)#
Bases:
fairdm.core.abstract.AbstractIdentifier,django_lifecycle.mixins.LifecycleModelMixinAn external identifier of a contributor, such as an ORCID iD or a ROR id.
Types are drawn from the contributor identifier collection, so a person cannot hold a specimen identifier such as an IGSN.
Attributes: type: The kind of identifier, from the contributor identifier types. related: The contributor the identifier belongs to.
Initialization
- type#
‘ConceptField(…)’
‘ForeignKey(…)’
- property resolver_url#
The address the identifier resolves to, or None for a type with no resolver.
- clean()#
Refuse a second identifier of the same type, with a message naming the type.
- dispatch_sync_task()#
Queue a sync with the external provider once the transaction commits.
- class ClaimMethod(*args, **kwds)#
Bases:
fairdm.db.models.TextChoicesThe ways a profile can be claimed.
Initialization
- ORCID#
(‘orcid’,)
- EMAIL#
(‘email’,)
- TOKEN#
(‘token’,)
- ADMIN_MERGE#
(‘admin_merge’,)
- ADMIN_MANUAL#
(‘admin_manual’,)
- class ClaimingAuditLogManager#
Bases:
fairdm.db.models.ManagerManager with filters for the claiming audit log.
- for_person(pk)#
Filter to events where the person was the source or the target.
Args: pk: The person’s primary key.
Returns: Matching log entries.
- failures()#
Filter to failed claim attempts.
Returns: Log entries where
successis false.
- by_method(method: str)#
Filter to one claiming method.
Args: method: A :class:
ClaimMethodvalue.Returns: Matching log entries.
- recent(days: int = 30)#
Filter to events within the last few days.
Args: days: How many days back to include.
Returns: Matching log entries.
- class ClaimingAuditLog(*args, **kwargs)#
Bases:
fairdm.db.models.ModelImmutable audit trail of profile claiming events, created once and never updated.
Attributes: objects: The audit log manager. timestamp: When the event happened. method: How the profile was claimed. source_person: The unclaimed person being claimed. target_person: The resulting claimed person. initiated_by: The admin who started the claim, when admin-driven. ip_address: The requester’s IP address. success: Whether the claim succeeded. failure_reason: Why the claim failed. details: Extra structured details.
Initialization
- objects#
‘ClaimingAuditLogManager(…)’
- timestamp#
‘DateTimeField(…)’
- method#
‘CharField(…)’
- source_person#
‘ForeignKey(…)’
- target_person#
‘ForeignKey(…)’
- initiated_by#
‘ForeignKey(…)’
- ip_address#
‘GenericIPAddressField(…)’
- success#
‘BooleanField(…)’
- failure_reason#
‘CharField(…)’
- details#
‘JSONField(…)’
- save(*args, **kwargs)#
Refuse to update an existing entry, then save.
- forwards()#
Keep one primary email address for each user with several, preferring the one matching the user’s email.