fairdm.contrib.contributors.models#

Models for people, organisations, their identifiers and their credits on research objects.

Module Contents#

Classes#

Contributor

A person or organisation credited on projects, datasets, samples or measurements.

Person

An individual contributor who can also sign in as a user.

Affiliation

A person’s membership of an organisation, with time bounds and a verification state.

Organization

A contributor that represents a group of people, such as a university, institute, company or agency.

Contribution

A contributor’s credit on a project, dataset, sample or measurement, based on the DataCite contributor schema.

ContributorIdentifier

An external identifier of a contributor, such as an ORCID iD or a ROR id.

ClaimMethod

The ways a profile can be claimed.

ClaimingAuditLogManager

Manager with filters for the claiming audit log.

ClaimingAuditLog

Immutable audit trail of profile claiming events, created once and never updated.

Functions#

contributor_permissions_default

Return the empty default permissions dict, which migration 0001 references.

forwards

Keep one primary email address for each user with several, preferring the one matching the user’s email.

Data#

logger

ORCID_PATTERN

The form of an ORCID iD, without its address.

ROR_PATTERN

The form of a ROR ID, without its address.

OrganizationMember

CONTRIBUTION_UNIQUE_PAIRING_MESSAGE

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.PolymorphicModel

A 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:Person and :class:Organization as its concrete types, and :class:Contribution links 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(…)’

‘JSONField(…)’

lang#

‘JSONField(…)’

last_synced#

‘DateField(…)’

synced_data#

‘JSONField(…)’

config#

‘JSONField(…)’

added#

‘DateTimeField(…)’

modified#

‘DateTimeField(…)’

tracker#

‘FieldTracker(…)’

class Meta#
ordering#

[‘name’]

verbose_name#

‘_(…)’

verbose_name_plural#

‘_(…)’

‘contributors’

save(*args, **kwargs)#

Stamp last_synced when synced_data has changed, then save.

static base_class()#

Return Contributor, which PolymorphicMixin needs 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 prefetches identifiers costs 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, which PolymorphicMixin needs 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_model or a subclass.

Subclass rows share their base row’s primary key, so the ids are also base_model primary keys. Credits on samples and measurements are stored under the concrete subclass’s content type, which a reverse GenericRelation from 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 Person or Organization.

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.

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 not http or https is 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_projects or :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 carries kind (project, dataset, sample or measurement). Projects and datasets also carry record; 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 Counter of 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, sample or measurement.

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.Contributor

An 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

class Meta#

Bases: django.contrib.auth.models.AbstractUser.Meta

constraints#

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_in says. 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_state alone cannot say that, because an account made with createsuperuser is 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_claimed and email.

The precedence is inactive, then claimed, then invited when there is an email, then ghost. The PersonQuerySet state 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 prefetches affiliations__organization costs 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 prefetches groups costs 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 prefetches affiliations__organization costs no query per person.

Returns: current, the primary affiliation first and the rest by organization name, and past, 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) and links.

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.”) or initials_family (“J. Doe”). Any other value is treated as given_family.

Returns: The formatted name, or name when 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: orcid or orcid_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.Model

A 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. Setting end_date ends the rights the type confers, as AffiliationQuerySet.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.IntegerChoices

Verification 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(…)’

class Meta#
verbose_name#

‘_(…)’

verbose_name_plural#

‘_(…)’

‘affiliations’

constraints#

None

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.Contributor

A 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.

class Meta#
verbose_name#

‘_(…)’

verbose_name_plural#

‘_(…)’

‘organizations’

indexes#

None

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_data after 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 person selected.

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 person loaded: 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) and children (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), profile and links.

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.OrderedModel

A 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.LifecycleModelMixin

An 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(…)’

related#

‘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.TextChoices

The 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.Manager

Manager 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 success is false.

by_method(method: str)#

Filter to one claiming method.

Args: method: A :class:ClaimMethod value.

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.Model

Immutable 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(…)’

class Meta#
verbose_name#

‘_(…)’

verbose_name_plural#

‘_(…)’

ordering#

[‘-timestamp’]

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.