Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
DJANGO_SECRET_KEY=your_secret_key
DJANGO_DEBUG=True
DJANGO_ALLOWED_HOSTS=127.0.0.1 localhost
DJANGO_EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
DJANGO_LOG_LEVEL=DEBUG
7 changes: 6 additions & 1 deletion .github/workflows/django-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ jobs:
- name: Run Django system checks
run: |
export DJANGO_SECRET_KEY="$(python -c "import secrets; print(secrets.token_urlsafe())")"
python manage.py check
python manage.py check --fail-level WARNING
# https://docs.djangoproject.com/en/5.1/ref/django-admin/#cmdoption-makemigrations-check
- name: Check for missing migrations
run: |
export DJANGO_SECRET_KEY="$(python -c "import secrets; print(secrets.token_urlsafe())")"
python manage.py makemigrations --check --dry-run
# https://docs.djangoproject.com/en/5.1/topics/testing/
- name: Run Django test suites
run: |
Expand Down
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ make lint # Python linting with flake8 and black

# Type checking (TypeScript/Svelte)
npm run lint:check

# Django system checks and migration verification
make check # Runs Django checks and verifies no missing migrations
# Or manually:
python manage.py check --fail-level WARNING
python manage.py makemigrations --check --dry-run
```

### Building for Production
Expand Down Expand Up @@ -265,6 +271,7 @@ See [docs/README.md](docs/README.md) for a complete documentation index.
- Survey structure is JSON-driven; modify configs in `data/readiness_descriptions/`
- File uploads go to `MEDIA_ROOT/survey/{survey_id}/` and `MEDIA_ROOT/survey_evidence/{section_id}/`
- Test user passwords match their role name in lowercase (from test data fixtures)
- **Migration checks**: Both CI/CD pipelines and deployment scripts automatically check for missing migrations using `makemigrations --check`. Always run `make check` before committing model changes to ensure migrations are created.

## Accessibility Compliance

Expand Down
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ help:
@echo " make runserver - Start Django development server"
@echo " make migrations - Create new database migrations"
@echo " make migrate - Apply database migrations"
@echo " make check - Run Django system checks (including migration check)"
@echo " make superuser - Create a superuser account"
@echo " make static - Collect static files"
@echo " make shell - Open Django shell"
Expand All @@ -31,6 +32,11 @@ migrations:
migrate:
$(MANAGE) migrate

# System checks
check:
$(MANAGE) check --fail-level WARNING
$(MANAGE) makemigrations --check --dry-run

# User management
superuser:
$(MANAGE) createsuperuser
Expand Down Expand Up @@ -63,4 +69,4 @@ lint:
.DEFAULT_GOAL := help

# Mark these targets as always needing to run (not files)
.PHONY: help runserver migrations migrate superuser static shell test clean requirements lint
.PHONY: help runserver migrations migrate check superuser static shell test clean requirements lint
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,7 @@ python manage.py createsuperuser
6. Create a `.env` file in the project root directory and add the following environment variables (which should be used in development only)

```bash
DJANGO_SECRET_KEY=your_secret_key
DJANGO_DEBUG=True
DJANGO_ALLOWED_HOSTS=127.0.0.1 localhost
DJANGO_EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
DJANGO_LOG_LEVEL=DEBUG
cp .env.example .env
```

---
Expand Down
46 changes: 46 additions & 0 deletions home/forms/organisation_invite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from django import forms
from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _
from invitations.forms import InviteForm
from invitations.adapters import get_invitations_adapter
from invitations.utils import get_invitation_model

Invitation = get_invitation_model()
User = get_user_model()


class OrganisationInviteForm(InviteForm):
"""
Custom invite form that allows inviting existing users to organisations.

Unlike the default InviteForm, this form permits inviting users who already
have accounts on SORT, since a user may belong to multiple organisations.
"""

def clean_email(self):
"""
Validates email but allows existing users (unlike default behavior).
Only prevents duplicate pending invitations or already-accepted invitations.
"""
email = self.cleaned_data["email"]
email = get_invitations_adapter().clean_email(email)

errors = {
"already_invited": _("This e-mail address has already been invited."),
"already_accepted": _(
"This e-mail address has already accepted an invite."
),
}

# Check for pending invitations
if Invitation.objects.all_valid().filter(email__iexact=email, accepted=False):
raise forms.ValidationError(errors["already_invited"])

# Check for already-accepted invitations
if Invitation.objects.filter(email__iexact=email, accepted=True):
raise forms.ValidationError(errors["already_accepted"])

# NOTE: We intentionally do NOT check if user exists
# because we want to allow inviting existing users to new organisations

return email
10 changes: 10 additions & 0 deletions home/templates/home/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,18 @@
{% block content %}
<h3 class="text-center mb-4">Login to SORT</h3>

{% if request.GET.invitation_key %}
<div class="alert alert-info" role="alert">
<i class="bx bxs-envelope"></i>
You've been invited to join an organisation. Please log in to accept the invitation.
</div>
{% endif %}

<form method="post">
{% csrf_token %}
{% if request.GET.invitation_key %}
<input type="hidden" name="invitation_key" value="{{ request.GET.invitation_key }}">
{% endif %}
<div class="mb-3">
<label for="username" class="form-label">Email</label>
<input type="email" class="form-control" id="username" name="username" required>
Expand Down
Loading
Loading