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 email field, 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 email set + valid: notifications go to contact.email. Wins over the linked User's email. Per-project routing works.
  • Contact has email blank/invalid: falls back to user.email (where user = 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.

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 from User.username, the project membership lookup goes via ProjectPeople.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.email is 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:

  1. Contacts tab → "Add Contact" (visible if you have WRITE PERMISSIONS).
  2. Fill the form: first name, last name, optional company, optional email + mobile.
  3. Optionally pick a site_login user. Triggers auto-populate of empty fields.
  4. Set tenancy — usually the same tenancy mix as the contact's primary use case.
  5. 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:

  1. Project → People tab → "Add Person".
  2. Pick the contact from the dropdown.
  3. Set disciplines (their roles on this project — see ROLES).
  4. Optionally tick project_admin and/or role_leader.
  5. 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.

  1. Edit the contact.
  2. Pick the user in the site_login field.
  3. 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.
  4. 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), telephone
  • tenants (M2M)

Editing companies requires WRITE PERMISSIONS group. Use cases:

  • Contractor visibility: documents track active_revision.contractor as a FK to DirectoryCompany. 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 in apps/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) — when contact=… is passed AND contact.email is set + valid, that overrides user.email for delivery. Signal-handler call sites in apps/workflow/signals.py and apps/edms/signals.py pass contact=person.contact for the notification fan-out paths.
  • Mention resolution: apps.workflow.signals.py — walks ProjectPeople.contact.site_login.username to resolve @username mentions.

See devdocs/ for codebase-level material.