Contributor models#
This page lists the models, fields, managers, methods and helper modules of fairdm.contrib.contributors that describe people and organizations: Contributor, Person, Organization, Affiliation, Contribution and ContributorIdentifier. It also covers the profile helper functions, the profile editing forms and the transform classes. It does not cover the contribution levels and the permission checks built on them, which are in Contribution levels and record access, the methods that change who is credited on a record, which are in Crediting, or the claiming functions, which are in Profile claiming. The reasons behind the design are in How contributor accounts and credits work.
Every class below is imported from fairdm.contrib.contributors.models unless a section says otherwise. The generated Python API holds the docstrings.
Contributor#
Contributor is the polymorphic base of Person and Organization. It has a table of its own. A query on Contributor returns each row as its concrete class.
Field |
Type |
Default |
What it holds |
What it accepts |
|---|---|---|---|---|
|
|
Generated on first save |
The public identifier. Unique across both concrete types and never changed. Not editable |
Not set by hand |
|
|
|
The profile image. The stored source is resized to at most 1200 by 1200 pixels as WebP |
An image file |
|
|
None, required |
The name the contributor is publicly known by |
Text |
|
|
|
Other names the contributor is known by |
A list of strings |
|
|
|
A free-text description |
Text |
|
|
|
Online resources about the contributor |
A list of web addresses. |
|
|
|
The contributor’s languages |
A list of ISO 639-1 codes, checked by |
|
|
|
The date |
Not set by hand |
|
|
|
The raw record from the external provider. Not editable |
Not set by hand |
|
|
|
A general-purpose store. Nothing in FairDM reads it or enforces anything from it |
Any JSON value |
|
|
Set once, on creation |
When the row was created |
Not set by hand |
|
|
Set on every save |
When the row was last saved. Indexed |
Not set by hand |
Contributor.Meta orders rows by name and sets default_related_name to contributors.
Attribute |
Value |
What it does |
|---|---|---|
|
|
Lets a template branch on the concrete type |
|
|
The identifier type that |
Member |
Returns |
What it does |
|---|---|---|
|
|
The address of the overview page |
|
|
The address of the profile editing page |
|
|
Whether |
|
|
The identifier of type |
|
Property, same as above |
|
|
The rendered icon |
The icon for the default identifier type |
|
Property, |
Overridden by |
|
|
The image address, or the portal’s brand icon when there is no image |
|
|
The first letters of the first two words of |
|
Class property, |
The polymorphic base type |
|
Properties, querysets |
The projects and datasets the contributor is credited on |
|
Properties, querysets |
The samples and measurements the contributor is credited on, whatever their registered type |
|
A values queryset of ids |
The ids of the contributor’s credits on |
|
Queryset |
The public projects the contributor is credited on. |
|
Queryset |
The public datasets the contributor is credited on, outside private projects. |
|
|
The number of credits for each credited model, keyed by the model’s plural verbose name |
|
|
The credits a profile may name, newest first. A project or dataset counts when it is one of the two public lists above. A sample or measurement counts when |
|
|
The number of credits each contribution role is held on, keyed by label. Counts every credit when |
|
Queryset slice |
The newest credits |
|
Queryset |
The credits on one model, such as |
|
|
Whether the contributor is credited on |
|
Queryset |
The other contributors credited on the same records, most frequent first and ties by name. Each carries |
|
|
Credits the contributor on |
|
|
One entry for each |
|
|
The names of the recorded languages in the active language. An unknown code is kept as written |
|
|
The contributor as a DataCite creator or contributor object |
|
|
The contributor as a Schema.org |
|
|
|
Language codes#
fairdm.contrib.contributors.validators holds the checks behind lang:
Name |
Type |
What it does |
|---|---|---|
|
|
The accepted codes |
|
Validator |
Refuses a value outside |
|
Validator |
Applies the single-code check to each element of a list. An empty value passes |
Attach validate_iso_639_1_language_codes to a field of your own that holds a list of language codes.
Person#
Person extends Django’s AbstractUser and Contributor. It is the portal’s user model. fairdm.setup() sets AUTH_USER_MODEL to contributors.Person and people sign in with their email address. The username field is removed.
Field |
Type |
Default |
What it holds |
What it accepts |
|---|---|---|---|---|
|
|
|
The sign-in address. Empty for a profile made for attribution alone |
A valid address. |
|
|
|
Whether the person has claimed the profile |
|
The AbstractUser fields (first_name, last_name, is_active, is_staff, is_superuser, password, last_login, date_joined, groups, user_permissions) are inherited. USERNAME_FIELD is email and REQUIRED_FIELDS is empty. A constraint, unique_person_email_ci, makes addresses unique regardless of case.
save() fills a blank name from first_name and last_name. clean() also refuses a claimed person with no email, a link that is not a web address, and an ORCID identifier that does not match the form 0000-0000-0000-0000.
Member |
Returns |
What it does |
|---|---|---|
|
|
One of |
|
|
Whether the account is active and the person has claimed it or has signed in ( |
|
|
|
|
|
The person’s ORCID identifier |
|
|
Whether the person has signed in with ORCID. Reads |
|
Social account or |
The person’s allauth social account for a provider id such as |
|
|
The primary affiliation, when it is verified (member or above) and has no end date. Reads |
|
|
The organization of |
|
Queryset |
Affiliations with no end date and a type of member or above, with the organization loaded |
|
|
|
|
|
The labels of the portal roles the person holds, in declaration order. Empty for an inactive person. Reads |
|
|
The initials of |
|
Properties |
|
|
|
The name as |
|
Property |
|
|
|
A flag for each of |
|
|
The city and country of the primary affiliation’s organization |
|
|
A GeoJSON feature placed at the primary affiliation’s organization. |
|
|
|
Person.objects is a UserManager. Person.objects.all().<method>() and Person.objects.<method>() give the same result.
Method |
Returns |
What it does |
|---|---|---|
|
|
Makes a person who is neither staff nor superuser. Does not set |
|
|
Makes a staff superuser. Raises |
|
|
Makes a ghost profile: active, unclaimed, no email, an unusable password. |
The queryset methods of PersonQuerySet:
Method |
Keeps |
|---|---|
|
Everyone except superusers and the anonymous placeholder (email |
|
|
|
|
|
|
|
|
|
Active, unclaimed, no email |
|
Active, unclaimed, with an email |
|
Everyone, with |
AccountState is in fairdm.contrib.contributors.choices with the values ghost, invited, claimed and inactive.
from fairdm.contrib.contributors.models import Person
people = Person.objects.real().claimed().for_cards()
Who may edit a profile#
Contributor.is_editable_by(user) is the one rule that decides who may open and save the profile editing page. It is not a Django permission, because the answer depends on the record.
Profile |
|
|---|---|
A person |
The person, while their account is active. An active holder of the Community Manager role, when |
An organization |
An active user who passes |
An anonymous visitor, a superuser and the holder of any other portal role get no right from that alone. The rule asks PortalRoles.is_held_by, never a Django permission. See Portal roles in code.
Organization#
Organization extends Contributor.
Field |
Type |
Default |
What it holds |
What it accepts |
|---|---|---|---|---|
|
|
|
The kind of institution |
A value of |
|
|
The affiliated people. Has no reverse accessor |
Add members through |
|
|
|
|
The organization this one is part of. Reached from the parent as |
An organization other than itself or one beneath it |
|
|
|
The city |
Text |
|
|
|
The country |
A two-letter country code |
|
|
|
A point in WGS 84 with no elevation or uncertainty |
A point |
OrganizationType is in fairdm.contrib.contributors.choices. Its values are education, funder, healthcare, company, archive, nonprofit, government, facility and other. ROR allows several types for one organization and FairDM keeps one.
clean() refuses a parent that is the organization itself or any organization beneath it, with an error on parent and the code parent_loop. It also checks each link and that a ROR identifier has the form 0xxxxxx00, with or without the https://ror.org/ prefix. An index, organization_location_geog_idx, supports location lookups.
Member |
Returns |
What it does |
|---|---|---|
|
Properties |
The latitude and longitude of |
|
|
Creates or updates an organization from a ROR record through |
|
Queryset |
The organization’s affiliations with |
|
|
Verified affiliations with no end date, the owner first, then administrators, then members, each group by name |
|
|
The person with a current owner affiliation |
|
|
Whether the user has a current affiliation of member or above. A visitor is never a member |
|
|
Whether the user has a current affiliation of administrator or owner. A portal role does not count |
|
|
|
|
|
Demotes every current owner to administrator and makes |
|
|
The primary keys of every organization beneath this one, at any depth. Empty before the first save |
|
|
|
|
Queryset |
The public projects the organization owns or is credited on, none twice |
|
Queryset |
The public datasets the organization is credited on or that sit in its projects, none twice. A dataset in a private project is left out |
|
|
A flag for each of |
|
|
The city and country |
|
|
The type and place joined by a middle dot, or |
|
|
The leading acronym when the name starts with one ( |
|
|
A GeoJSON feature for |
|
|
|
transfer_ownership raises ValidationError and changes nothing when the new owner:
holds no affiliation with the organization
has an affiliation that has ended
has an affiliation that is still pending
has not claimed their account
has a deactivated account
OrganizationQuerySet adds the location lookups described in Location fields.
Management of an organization#
manage_organization is derived, not stored. OrganizationPermissionBackend answers user.has_perm("contributors.manage_organization", organization) as follows:
For an inactive user or a visitor, it answers false.
For a superuser, it answers true.
For anyone else, it answers true only when they hold a current
OWNERaffiliation with the organization.
A stored django-guardian grant of this permission is never consulted. Setting end_date on an affiliation, changing its type away from OWNER, or deactivating the account removes the right on the next check.
Affiliation#
Affiliation links a Person to an Organization. An affiliation is current when it has no end_date. An end date in the future still makes it past.
Field |
Type |
Default |
What it holds |
What it accepts |
|---|---|---|---|---|
|
|
None, required |
The person. Reached from the person as |
A person |
|
|
None, required |
The organization. Reached from the organization as |
An organization |
|
|
|
The verification state and role |
A value of |
|
|
|
Whether this is the person’s primary affiliation |
|
|
|
|
When the affiliation began |
A year, a year and month, or a full date, as in Model fields |
|
|
|
When it ended. Empty means current |
As |
Affiliation.MembershipType has four values. Each includes the rights of the ones before it, so they compare as integers.
Name |
Value |
Meaning |
|---|---|---|
|
0 |
Declared and not yet verified |
|
1 |
A regular member |
|
2 |
Can manage memberships |
|
3 |
Full control of the organization |
Two database constraints apply:
unique_affiliation_person_organizationallows one affiliation for each person and organization.clean()refuses a second with the message<person> is already a member of <organization>.onorganization.unique_primary_affiliation_per_personallows one primary affiliation for each person.save()demotes the person’s other primary affiliation in the same transaction.
Member |
Returns |
What it does |
|---|---|---|
|
|
The date as precisely as it was recorded, or |
Affiliation.objects is an AffiliationManager with these methods, also available on person.affiliations:
Method |
Returns |
What it does |
|---|---|---|
|
Queryset |
Affiliations with no end date |
|
Queryset |
Affiliations with an end date |
|
|
The affiliation marked primary. Cannot be chained further |
|
Queryset |
Current affiliations of type |
Contribution#
Contribution is a contributor’s credit on one project, dataset, sample or measurement. It is an OrderedModel, so each row has an order.
Field |
Type |
Default |
What it holds |
What it accepts |
|---|---|---|---|---|
|
|
None, required |
The type of the credited record |
A content type |
|
|
None, required |
The credited record’s primary key as text |
A key |
|
|
The credited record |
||
|
|
|
The person or organization credited. Deleting the contributor sets this to |
A contributor |
|
|
Empty |
The roles on this credit |
Concepts of the contributor roles vocabulary |
|
|
|
The organization a person is credited from on this record. Kept on the credit and not read from the profile. Deleting the organization sets this to |
An organization, or |
|
|
|
What a person may do on the record. Empty for an organization |
A value of |
Contribution.Meta orders rows by object_id and order.
Two rules protect the data:
A named constraint,
unique_contribution_per_contributor_object, allows one row for each contributor and record.clean()refuses a second row with the messageThis contributor is already credited on this object.A role from any other vocabulary is refused before it is written:
roles.add()androles.set()raiseValidationError.
The roles a record’s type offers are the collection named by its CONTRIBUTOR_ROLES attribute, the address of one of the contributor roles vocabulary’s collections.
Member |
Returns |
What it does |
|---|---|---|
|
|
Credits |
|
|
|
|
|
Whether the contributor is a |
|
|
The contributor’s profile address |
add_to on the contribution, Contributor.add_to and the add_contributor(contributor, with_roles=None) method that every project, dataset, sample and measurement has all call the same code. Crediting the same contributor again adds roles and does not make a second row.
Contribution.objects is a ContributionManager. A record’s contributors relation uses the same manager, so dataset.contributors.people() works. The methods:
Method |
Returns |
What it does |
|---|---|---|
|
Queryset |
Credits whose roles include the role with this address |
|
Queryset |
Credits on one record |
|
Queryset |
Credits by one contributor across all records |
|
Queryset |
Credits of people, ordered by |
|
Queryset |
Credits of organizations, ordered by |
Move a credit through Crediting.move. Do not use contribution.up() or contribution.down() from OrderedModel. A contribution has no order_with_respect_to, so they swap places with a row on another record. See Crediting.
ContributorIdentifier#
ContributorIdentifier is an external identifier of a contributor.
Field |
Type |
Default |
What it holds |
What it accepts |
|---|---|---|---|---|
|
|
None, required |
The contributor. Reached as |
A contributor |
|
|
None, required |
The identifier scheme |
The address of one of the types below |
|
|
None, required |
The identifier |
Text. No two identifiers of any record type share a value |
The contributor collection offers, by identifier, orcid, researcher-id, ror, wikidata, isni and crossref-funder-id. A contributor holds at most one identifier of each type. A second is refused at the database and by clean(), with a message naming the type.
resolver_url is the address the identifier resolves to, or None for a type with no resolver. ORCID, ROR, Wikidata, ISNI and Crossref Funder ID identifiers have one. A ResearcherID has none.
Creating an identifier queues a sync with the external provider once the transaction commits. The sync is a Celery task, sync_contributor_identifier. See How to sync a profile from ORCID or ROR without Celery.
Profile helpers#
fairdm.contrib.contributors.profiles holds the pure functions behind a profile page. None reads the database or a request.
Function |
Returns |
What it does |
|---|---|---|
|
|
The host without a leading |
|
|
One name per ISO 639-1 code in the active language, in the order given. An unknown code is kept as written |
|
|
Ranks a mapping of counts, largest first, as |
|
|
Fits a list into |
|
|
Sums up items that each carry a |
|
|
Orders items with the active ones first, each group most recently updated first. |
from fairdm.contrib.contributors.profiles import fill_slots, link_host, ranked_shares
link_host("https://www.github.com/someone") # "github.com"
ranked_shares({"Creator": 4, "Editor": 2})
# [{"label": "Creator", "count": 4, "percent": 100},
# {"label": "Editor", "count": 2, "percent": 50}]
fill_slots(list(range(12)), 10, reserve=True)
# {"shown": [0, 1, 2, 3, 4, 5, 6, 7, 8], "more": 3, "total": 12}
Profile forms#
fairdm.contrib.contributors.forms.profile holds the forms behind the profile editing page and the field their lists are typed into. The page is the Update plugin in fairdm.contrib.contributors.plugins.update. It reads FAIRDM_PROFILE_FORMS (see Settings) for the kind of contributor, person or organization, and falls back to the shipped form when the setting names none. It declares its own check, contributor_is_editable, so a visitor is sent to sign in and a signed-in user who may not edit gets a 403. The check runs on every request, so a right lost while the page is open refuses the save.
ProfileForm#
ProfileForm is the base of the two shipped forms. A portal’s own form can extend it. It draws no <form> tag, because the editing page supplies the tag and the buttons.
Attribute |
Type |
Default |
What it does |
|---|---|---|---|
|
|
|
Groups the fields in page order. A row is one field name, or a tuple of names drawn side by side on a wide screen. A heading of |
A field the form does not carry is left out of its section. A section left with no field is not drawn. A field that no section names is drawn after the last section.
A stored record can fail the model’s validation on a field the form does not carry, such as an identifier stored in a malformed state. ProfileForm reports that failure as an error on the form as a whole, in form.non_field_errors(), and saves nothing.
PersonProfileForm#
Field |
Type |
Accepts |
|---|---|---|
|
|
|
|
Model fields |
The names that citations and exported metadata use |
|
Model field |
The display name. Required |
|
|
Other names, one per line |
|
Model field |
The biography |
|
|
Web addresses, one per line. Only |
|
|
ISO 639-1 codes, named in the active language. Each code is stored once |
The email, password, identifiers, affiliations, portal roles and account state are never on this form. Its sections are the unheaded group of first_name and last_name side by side, name and alternative_names, and the group “About you” with image, profile, lang and links.
OrganizationProfileForm#
Field |
Type |
Accepts |
|---|---|---|
|
|
The logo, as for the person’s photo |
|
Model field, |
As on the person form |
|
Model fields |
A value from their lists |
|
|
An organization other than itself or one beneath it. The picker searches every organization by name, alternative name or ROR identifier |
|
Model field |
A city |
|
|
A latitude and a longitude as signed decimal degrees in WGS 84. Both empty removes the point. See Location fields |
|
Model field |
The description |
|
|
An |
|
|
Other |
website and links are two fields over the one stored list Organization.links. The website is stored first and the other links follow it, and an address typed in both places is stored once. When the form opens, website shows the first stored link and links shows the rest. A form that keeps website must keep links as well. Without website, links shows every stored link.
The ROR identifier, the members and the owner are never on this form. Its sections are an unheaded group, “Location”, “About” and “Online”.
LinesField#
LinesField(entry_validator=None, max_entries=50, **kwargs) is a CharField on a text area whose cleaned value is a list. It trims each line, drops empty lines and keeps a repeated entry once, where it was first typed. A list given as the initial value is shown one entry per line.
Argument |
Type |
Default |
What it does |
|---|---|---|---|
|
callable or |
|
Called with each entry. The first entry it rejects is reported with the code |
|
|
|
The most entries accepted. More are refused with the code |
from django.core.validators import URLValidator
from fairdm.contrib.contributors.forms.profile import LinesField
websites = LinesField(
required=False,
entry_validator=URLValidator(schemes=["http", "https"]),
error_messages={"invalid_entry": "%(entry)s is not a web address."},
)
websites.clean("https://example.org\n\nhttps://example.org\nhttps://example.net")
# ['https://example.org', 'https://example.net']
language_choices() in the same module returns the (code, name) pairs that lang offers, sorted by name.
Transforms#
The classes in fairdm.contrib.contributors.utils.transforms convert between a Contributor and an external format. A transform is an instance, and the contract is export() and import_data().
Member of |
Returns |
What it does |
|---|---|---|
|
|
Converts a contributor to the external format. Raises |
|
|
Converts external data to a contributor, updating |
|
|
Whether |
|
Any |
Reads a nested value by a list of keys and indexes. Returns |
The built-in transforms:
Class |
Format |
Notes |
|---|---|---|
|
DataCite Metadata Schema 4.4 creator or contributor |
Export adds the default identifier, the given and family names, and the primary affiliation |
|
Schema.org |
Export includes the email address of a person. Use |
|
CSL-JSON author |
Import looks up an existing contributor by ORCID when no |
|
ORCID Public API v3.0 record |
Exports and imports |
|
ROR schema version 2 record |
Exports and imports |
ORCIDTransform and RORTransform also talk to the registry:
Member |
Returns |
What it does |
|---|---|---|
|
|
Fetches the public record with a 30 second timeout. Raises |
|
|
Finds a contributor by |
|
|
Finds a contributor by identifier value, or fetches the record and creates one |
RORTransform adds three more:
Member |
Returns |
What it does |
|---|---|---|
|
|
The bare ID without the |
|
|
The name ROR displays, or |
|
|
Sets |
The functions contributor_to_datacite, contributor_to_schema_org, contributor_to_csljson and csljson_to_contributor wrap the three format transforms. To write your own, see How to add a transform for another format.