Contacts¶
Part of the user manual. Related: VISIBILITY · PERMISSIONS · TENANCY · PROJECTS · ROLES · NOTIFICATIONS · WORKFLOWS · EDMS · ACTIONS.
A contact in REBIM is a directory entry — a person who matters to one or more projects. They're tenancy-wide, not project-scoped: your tenant's directory holds every person you might assign to a project, route a transmittal to, or reference in a workflow.
Contacts are separate from user accounts (logins) and separate from project membership. The relationships matter — get them wrong and notifications go to the wrong place, or a person you added to a project can't actually log in.
TL;DR¶
- Contact = directory entry. Has a name, email, mobile, company,
optional link to a login account (
site_login). - User = login account. A user can have a linked contact, or not. A contact can have a linked user, or not.
- ProjectPeople = the link between a contact and a project (with disciplines, role_leader, project_admin flags).
- Adding a contact does NOT put them on any project. Adding a user
does NOT create a contact. You usually need all three: User +
Contact (linked via
site_login) + ProjectPeople (per project). - The contact's
emailfield, when set, overrides the linked user's email for notification delivery (per-project routing). See The email caveat for the resolution rules and edge cases.
Contact, User, ProjectPeople — three things, one person¶
REBIM splits "a person" into up to three records:
| Record | What it represents | Lives where |
|---|---|---|
User (auth.User) |
A login account. Has username, password, perms, is_active, etc. |
One row per person who can log in. Created via Django admin or signup. |
Contact (DirectoryContact) |
A directory entry. Has name, email, mobile, company. Visible across the tenancy. | One row per person in your directory — including people without logins (external contractors, historical records, etc.). |
| ProjectPeople | Membership of a specific project. Links a Contact to a Project, with disciplines + flags. | One row per (contact × project) pair. A contact in five projects has five ProjectPeople rows. |
A typical "internal staff member" is all three:
- User (login)
- Contact (directory entry with site_login = user)
- ProjectPeople rows for each project they work on
A "contractor we send transmittals to but who doesn't log in" is just:
- Contact (with site_login = None)
- ProjectPeople for each project they're involved in
A "user with no directory presence" is just: - User - (no contact, no project membership)
The combinations are all valid; which you use depends on the person's role.
Why three records, not one¶
The split is intentional. Each layer has a different lifecycle:
- Login accounts (User) change rarely. Created once, kept for the account's lifetime.
- Directory entries (Contact) change occasionally. People move companies, change phone numbers, retire.
- Project membership (ProjectPeople) changes per project — fresh disciplines, fresh flags, possibly removed when the project closes.
Keeping them separate means you don't have to delete the contact when a project ends, or break the directory when someone changes companies, or lose project membership history when a user account is deactivated.
Contact fields¶
| Field | Purpose |
|---|---|
first_name, last_name |
Display name |
email |
Per-contact email address. Used as the recipient for notifications when set (overrides user.email) — see The email caveat below. |
mobile, telephone |
Phone numbers |
company (FK) |
Primary company affiliation. Used for "[ACME] Jane Smith" style display. |
affiliations (M2M) |
Additional companies the contact is associated with. Different from company — used when someone works across multiple firms. |
associates (M2M to self) |
Related contacts. "Jane often works with Bob and Carol." Informational. |
contact_type (M2M) |
Tagging — Architect, Engineer, Subcontractor, Approver, etc. Different from disciplines (which are project-scoped roles). |
site_login (OneToOne to User) |
Optional link to a login account. Critical for notifications — see below. |
address1, address2, locality, post_code |
Postal address |
owner (FK to User) |
The user who created this contact (DataModel field) |
tenants (M2M) |
Tenancy (TENANCY) |
The email caveat¶
The contact's email field IS now consulted by the notification
machinery — but only when the signal that fires the email has a
ProjectPeople.contact in scope and passes it through to
email_user(contact=…). All the existing signal-handler call sites
do this (workflow events, document changes, transmittals, mentions),
so for the normal notification surfaces the override works.
Resolution rules:
- Contact has
emailset + valid: notifications go tocontact.email. Wins over the linked User's email. Per-project routing works. - Contact has
emailblank/invalid: falls back touser.email(whereuser = contact.site_login). - Contact has
site_login = None: no emails fire at all — there's no User to anchor the call. The contact-email field on its own isn't enough; a login link is the trigger that creates the email in the first place. user.is_active = False: no emails fire regardless of either field. Deactivated users are never emailed.
The override hinges on the caller passing contact=…. Every
signal-handler call site in the codebase does; ad-hoc / future
callers that wrap email_user() should follow the same pattern when
they have a ProjectPeople.contact available. Callers without a
contact in scope (e.g. admin CC fan-out via the
notifications__disable_admin_cc path) deliberately don't override —
they go to the User's email as before, which is correct for those
flows.
site_login — the link to a login account¶
A contact's site_login field is a OneToOneField(User). When set,
this contact "is" a particular user account:
- They appear in the People tab with a "site login" column showing their username.
- Mention resolution (
@username) goes through this link — the username comes fromUser.username, the project membership lookup goes viaProjectPeople.contact.site_login. - Notifications go to the linked User's email.
Setting site_login = None is valid — it's how you represent
"contact exists in the directory but doesn't have a login here." They
still get directory presence + project membership; they just don't
log in or receive REBIM emails.
A user can have a site_login-pointing contact in only one
contact record at a time (it's OneToOne). Trying to point two
contacts at the same user violates the constraint.
CREATE / EDIT auto-populate¶
The Contacts form has an auto-populate convenience on the site_login
field — when you pick a User, the form's first name / last name /
email pre-fill from the user's record. The exact behaviour:
| Form mode | Action | Behaviour |
|---|---|---|
| CREATE (new contact) | Pick a site_login |
Empty fields pre-fill from the user. User-typed values are NOT clobbered — type-then-pick keeps your typed values; pick-then-type lets the user values stand. |
| EDIT (existing contact) | Don't touch site_login |
Nothing happens. Form fields stay as they are. |
| EDIT | Swap site_login to a different user |
All three fields (first_name / last_name / email) overwrite from the new user. Treated as a re-creation. |
The asymmetry is deliberate: CREATE saves typing for the common case (contact mirrors a user), EDIT preserves intentional admin overrides unless the user-link itself was changed.
Why EDIT doesn't auto-populate by default¶
An admin might deliberately override a contact's display name or email after first creation (e.g. "Robert Smith" → "Bob Smith" as a preferred display name). Re-firing the auto-populate every time the form opens would clobber that deliberate edit silently. EDIT mode takes the conservative position: hands off, unless you tell us otherwise via a site_login swap.
The protection is defended on two grounds — both real now:
- Don't clobber admin-typed values. Display-name overrides ("Robert Smith" → "Bob Smith") are deliberate edits that shouldn't be silently undone on every form open.
- Preserve per-project email routing. Now that
contact.emailis honoured by the notification machinery (see the email caveat), the per-contact email field really IS per-project — clobbering it on every form open would silently break the routing the admin configured.
Permissions and visibility¶
For who can create / edit / delete contacts, see PERMISSIONS § Contacts. Summary: WRITE PERMISSIONS group + (project admin / contact owner) instance gate.
For visibility — which contacts a user sees — only tenancy (VISIBILITY § layer 1) applies. Contacts are not project-scoped, so layer 2 doesn't filter them. A contact with tenants overlapping the user's tenants is visible to that user; otherwise it isn't.
Practical operations¶
Create a new contact¶
From the frontend:
- Contacts tab → "Add Contact" (visible if you have WRITE PERMISSIONS).
- Fill the form: first name, last name, optional company, optional email + mobile.
- Optionally pick a
site_loginuser. Triggers auto-populate of empty fields. - Set tenancy — usually the same tenancy mix as the contact's primary use case.
- Save.
The contact is now in the directory. It is NOT on any project yet — see next.
Add a contact to a project¶
The contact must already exist. Then:
- Project → People tab → "Add Person".
- Pick the contact from the dropdown.
- Set disciplines (their roles on this project — see ROLES).
- Optionally tick
project_adminand/orrole_leader. - Save.
A new ProjectPeople row links the contact to the project. The
contact is now a project member — visibility on workflow events
follows from ROLES.
Link an existing contact to a login¶
- Edit the contact.
- Pick the user in the
site_loginfield. - The form will auto-populate empty fields. Decide whether to overwrite the existing first/last/email — if the contact was manually customised, the swap will overwrite them, so adjust afterwards if needed.
- Save.
Effect: the contact now "is" that user — mentions resolve, emails route, etc.
Create a contact for someone who doesn't log in¶
Same as "create a new contact," just leave site_login empty.
The contact exists in the directory, can be added to projects, can be referenced in transmittals, can appear in the People table — but cannot log in and does not receive emails. Useful for external parties (clients, subcontractors who only need to be mentioned in records but never use the system directly).
Remove a contact from a project¶
Project → People tab → kebab next to the person → "Delete the
Relationship". This removes the ProjectPeople row only — the
contact stays in the directory and remains on any other projects
they're a member of.
Deactivate a contact¶
REBIM doesn't have a built-in "deactivate contact" toggle. Options:
- Remove from projects but keep in directory — they no longer participate but historical records remain intact.
- Delete the contact — only if they're not referenced anywhere
(no
ProjectPeople, no transmittal acknowledgements, no contact owner of EDMS data sources). REBIM's referential integrity will block delete if references exist. - Set
site_login = None— if they had a login that you've separately deactivated, breaking the link prevents email attempts.
Companies and affiliations¶
A DirectoryCompany represents an organisation — a firm, supplier,
subcontractor. Contacts link to companies in two ways:
company(FK) — primary affiliation. One company per contact. Used in display ("[ACME] Jane Smith").affiliations(M2M) — secondary affiliations. A contact who consults for multiple firms can list them all.
Companies have:
name,code(short identifier),telephonetenants(M2M)
Editing companies requires WRITE PERMISSIONS group. Use cases:
- Contractor visibility: documents track
active_revision.contractoras a FK toDirectoryCompany. The contractor company shown on a document column comes from this — used in compliance reports + filtering. - Transmittal distribution lists: when assembling a transmittal, picking by company filters the available contacts.
For the full company story — fields, the [CODE] Name display
convention, where companies surface (contact primary FK + affiliations
+ document revision contractor), the modify-contractor bulk action,
the manual merge flow — see COMPANIES.
Troubleshooting¶
| Symptom | Likely cause | First check |
|---|---|---|
| Added a new contact but they don't appear on any project | Adding to the directory ≠ adding to a project | Add a ProjectPeople row via the project's People tab |
| Contact has an email but isn't getting notifications | Email routing uses user.email not contact.email; contact has no site_login linked, or the linked user has no email |
The contact's site_login + the linked user's email |
| Changed the contact's email, notifications still go to the old address | Same as above — notifications use the linked User's email | Change the User's email instead, OR change the site_login link |
| Contact disappeared from the Contacts table | Tenancy mismatch (layer 1) — contact's tenants no longer overlap user's | Compare contact's tenants vs your UserProfile.tenancy |
Auto-populate not firing when picking a site_login |
Form likely opened before the select2 init finished, OR you're in EDIT mode and didn't swap site_login | Reload the form; for EDIT, you need to actually change the site_login value (just opening the form doesn't re-trigger) |
| Picked a site_login on EDIT, fields overwrote | Working as designed — EDIT-with-site_login-swap is treated as re-creation | If unintentional, undo + save. Otherwise the new values stand. |
| Can't delete a contact | Referential integrity blocks delete if the contact is referenced by ProjectPeople, transmittals, or data source owner | Remove the references first (ProjectPeople, etc.), then retry delete |
| "Cannot link two contacts to the same user" error | site_login is OneToOne — only one contact can point at any User |
Find + remove the duplicate link; one of them must drop site_login |
Mention @username doesn't resolve |
The mentioned username doesn't exist OR the corresponding contact has no ProjectPeople row in this project | Check the User exists + has a Contact with site_login linked + a ProjectPeople row on this project |
For developers¶
This page is for users and admins. Implementation pointers:
- Models:
apps.directory.models.DirectoryContact— the contact.apps.directory.models.DirectoryCompany— companies.apps.directory.models.DirectoryContactType— contact type tagging (M2M).- Serializers:
apps.directory.serializers.DirectoryContactSerializer,DirectoryContactDetailSerializer(used by the form). - Form template:
templates/default/directory/contact_detail.html new_contact_detail.html. Auto-populate JS wired inapps/directory/static/directory/js/app_people.js:load_person_form→_wire_contact_site_login_autopopulate/_populate_contact_form_from_user.- Auto-populate backend:
apps/core/views.py:user_mini(minimal endpoint returning pk + first_name + last_name + email for the selected User). - Email routing:
core.utils.email_user(user, …, contact=None)— whencontact=…is passed ANDcontact.emailis set + valid, that overridesuser.emailfor delivery. Signal-handler call sites inapps/workflow/signals.pyandapps/edms/signals.pypasscontact=person.contactfor the notification fan-out paths. - Mention resolution:
apps.workflow.signals.py— walksProjectPeople.contact.site_login.usernameto resolve@usernamementions.
See devdocs/ for codebase-level material.