-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Propose ADR to formalise the Accredited/Training provider relationships #4610
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,380 @@ | ||||||||||
# 12. Formalize Accredited Provider relationships | ||||||||||
|
||||||||||
Date: 23/10/24 | ||||||||||
|
||||||||||
## Status | ||||||||||
|
||||||||||
Proposal | ||||||||||
|
||||||||||
## Context | ||||||||||
|
||||||||||
|
||||||||||
### Evolving provider relationships | ||||||||||
The structural and conceptual relationships among and between providers and users has changed in many ways since this codebase was started.[[1]] | ||||||||||
|
||||||||||
Providers have always had a relationship between one another in the form of course accreditation. More recently, this relationship has been extended to delegating this accreditation. | ||||||||||
|
||||||||||
We have Accredited provider relationships that exist solely for organisational reasons. This happens when: | ||||||||||
|
||||||||||
|
||||||||||
| Provider | Is accredited? | Is accredited by | | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||
| --- | --- | --- | | ||||||||||
| A | Yes | - | | ||||||||||
| B | Yes | A | | ||||||||||
| C | No | B | | ||||||||||
|
||||||||||
|
||||||||||
| Provider | Is accredited? | "Is accredited by" | asdf | Who ratifies their courses? | | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||
| --- | --- | --- | --- | --- | | ||||||||||
| C76 | Yes | - | | No courses | | ||||||||||
| 2DB | Yes | C76 | | C76 (all) | | ||||||||||
| 1AV | No | 2DB | | 2DB (all) | | ||||||||||
|
||||||||||
|
||||||||||
* In Publish, a provider "is accredited by" is a relationship that exists solely to filter the accredited providers a training provider can choose from when selecting an accredited provider for their course. | ||||||||||
|
||||||||||
This relationship does not exist outside of Publish in concrete terms. In Apply: | ||||||||||
|
||||||||||
Provider A is the accredited provider of Provider B only if Provider B is running courses that are ratified by Provider A. | ||||||||||
|
||||||||||
Example: | ||||||||||
https://github.com/DFE-Digital/publish-teacher-training/pull/4589/files | ||||||||||
https://github.com/DFE-Digital/publish-teacher-training/blob/b7fcf16c39e087706d2b076c3515c7a946126a45/db/data/20241011081455_update_accredited_providers.rb | ||||||||||
Question: | ||||||||||
- Which is the ratifying provider of the courses run by Provider B and Provider C? | ||||||||||
|
||||||||||
|
||||||||||
This feels like an abuse of the accredited provider association, or a mislabelling of the relationship. | ||||||||||
|
||||||||||
### Unconventional entity associations | ||||||||||
|
||||||||||
We have a database table `provider` that has an implicit self-join relationship based on another column `accrediting_provider`. | ||||||||||
The association exists through a column called `accredited_provider_enrichments`. | ||||||||||
|
||||||||||
The relationship that currently exists is unconventional with respect to a normalised relational database. | ||||||||||
|
||||||||||
This is explained more in [Option 1](#1.-leave-the-relationships-as-they-are). | ||||||||||
|
||||||||||
|
||||||||||
### Organisations | ||||||||||
|
||||||||||
Publish used to employ a system of 'Organisations'. A number of Providers would be grouped by all being a member of an organisation. | ||||||||||
Users would also be members of the organisation. This architecture has been abandoned but there are still remnants of this system in the codebase. | ||||||||||
|
||||||||||
|
||||||||||
### Definitions | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could use this design history for definitions of different providers: |
||||||||||
|
||||||||||
- A Training Provider | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
* A provider that runs training courses directly. | ||||||||||
* |or| | ||||||||||
* A provider that has an subordinate assocation to another provider (accredited). | ||||||||||
- An Accredited Provider | ||||||||||
* A provider that has an authoritative association to another provider (training). | ||||||||||
- Accrediting Provider | ||||||||||
* A provider that accredits one or more courses on behalf of one or more training provider. | ||||||||||
Comment on lines
+73
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
- An Accredited Provider Relationship | ||||||||||
* A directional association between an accredited provider and another provider. | ||||||||||
- Lead partner | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lead partner is a term used when accredited providers lost their accreditation. We previously had 'Lead schools,' which were logical groupings of schools with one designated as the administrator, hence 'lead school'. Instead of creating a new category for the accredited providers that lost accreditation, they were merged with the school grouping and called 'lead partners'. In the UI, we use 'training partner' not least because they partner with accredited providers to deliver training. i.e., it's a more meaningful term. |
||||||||||
* This concept does not exist in the codebase | ||||||||||
|
||||||||||
### In the codebase | ||||||||||
An Accredited provider can run training courses, meaning they are a training provider and an accredited provider. | ||||||||||
|
||||||||||
An Accredited provider can be accredited by another accredite provider. | ||||||||||
|
||||||||||
#### Three levels of providers scenario | ||||||||||
|
||||||||||
Accredited provider A1 | ||||||||||
|
||||||||||
Accredited provider A2 | ||||||||||
|
||||||||||
Training provider T1 | ||||||||||
|
||||||||||
```mermaid | ||||||||||
flowchart LR; | ||||||||||
T1 | ||||||||||
A2 | ||||||||||
A1 | ||||||||||
|
||||||||||
T1 --> A2 --> A1; | ||||||||||
``` | ||||||||||
|
||||||||||
- A2 has an accredited provider relationship with A1. A1 is the accredited provider in this relationship. | ||||||||||
- A2 is the accredited provider for T1. T1 courses are accredited by A2. | ||||||||||
- A2 runs training courses and self accredits those courses. | ||||||||||
|
||||||||||
|
||||||||||
### Assumptions | ||||||||||
|
||||||||||
1. A provider is an Accredited provider if the value in the `accrediting_provider` column is `'Y'` | ||||||||||
2. A provider is not an Accredited provider if the value in the `accrediting_provider` column is `'N'` | ||||||||||
3. A Course must either be run by an Accredited provider |or| reference an Accredited provider (via `courses.accredited_provider_code`) | ||||||||||
4. There must exist an accredited provider relationship between the training provider and the accredited provider for the Course accredited provider to be valid. | ||||||||||
|
||||||||||
|
||||||||||
### Training Provider -> Accredited Provider association | ||||||||||
|
||||||||||
```mermaid | ||||||||||
flowchart LR; | ||||||||||
TrainingProvider --> Enrichments --> AccreditedProvider; | ||||||||||
``` | ||||||||||
|
||||||||||
### Course -> Accredited Provider association | ||||||||||
```mermaid | ||||||||||
flowchart LR; | ||||||||||
TrainingProvider --> Course --> AccreditedProvider; | ||||||||||
``` | ||||||||||
|
||||||||||
## Options | ||||||||||
|
||||||||||
### 1. Leave the relationships as they are | ||||||||||
|
||||||||||
A training provider stores their accredited provider relationships in a serialized JSON column called `accredited_provider_enrichments`. The primary key is stored as a property in this serialized string. This columns is of type `jsonb` but the json type of the value is `string`, not `object` using a key that is outdated and misleading `'UcasProviderCode'`. | ||||||||||
|
||||||||||
``` | ||||||||||
manage_courses_backend_development=# select jsonb_typeof(accrediting_provider_enrichments) from provider limit 1; | ||||||||||
jsonb_typeof | ||||||||||
-------------- | ||||||||||
string | ||||||||||
(1 row) | ||||||||||
``` | ||||||||||
|
||||||||||
This requires us to pattern match the string to find a training providers accredited provider associations. We cannot coerce the value to a jsonb object (array) because of the quote escaping in the stored string. | ||||||||||
|
||||||||||
|
||||||||||
```ruby | ||||||||||
class Provider < ApplicationRecord | ||||||||||
enum :accrediting_provider, { | ||||||||||
accredited_provider: 'Y', | ||||||||||
not_an_accredited_provider: 'N' | ||||||||||
} | ||||||||||
|
||||||||||
has_and_belongs_to_many :organisations, join_table: :organisation_provider | ||||||||||
|
||||||||||
has_many :users_via_organisation, -> { kept }, through: :organisations, source: :users | ||||||||||
|
||||||||||
has_many :user_permissions | ||||||||||
has_many :users, -> { kept }, through: :user_permissions | ||||||||||
|
||||||||||
def accredited_providers | ||||||||||
recruitment_cycle.providers.where(provider_code: accredited_provider_codes) | ||||||||||
end | ||||||||||
|
||||||||||
serialize :accrediting_provider_enrichments, coder: AccreditingProviderEnrichment::ArraySerializer | ||||||||||
|
||||||||||
alias accrediting_providers accredited_providers | ||||||||||
alias accredited? accredited_provider? | ||||||||||
|
||||||||||
# the providers that this provider is an accredited_provider for | ||||||||||
def training_providers | ||||||||||
Provider.where(id: current_accredited_courses.pluck(:provider_id)) | ||||||||||
end | ||||||||||
|
||||||||||
def current_accredited_courses | ||||||||||
accredited_courses.includes(:provider).where(provider: { recruitment_cycle: }) | ||||||||||
end | ||||||||||
|
||||||||||
def accredited_body(provider_code) | ||||||||||
accrediting_provider_enrichment = accrediting_provider_enrichments&.find { |enrichment| enrichment.UcasProviderCode == provider_code } | ||||||||||
|
||||||||||
return unless accrediting_provider_enrichment | ||||||||||
|
||||||||||
accredited_provider = recruitment_cycle.providers.find_by(provider_code:) | ||||||||||
|
||||||||||
return if accredited_provider.blank? | ||||||||||
|
||||||||||
{ | ||||||||||
accredited_provider_id: accredited_provider.id, | ||||||||||
description: accrediting_provider_enrichment.Description || '' | ||||||||||
} | ||||||||||
end | ||||||||||
|
||||||||||
def accredited_bodies | ||||||||||
accrediting_provider_enrichments&.filter_map do |accrediting_provider_enrichment| | ||||||||||
provider_code = accrediting_provider_enrichment.UcasProviderCode | ||||||||||
|
||||||||||
accredited_provider = recruitment_cycle.providers.find_by(provider_code:) | ||||||||||
|
||||||||||
if accredited_provider.present? | ||||||||||
{ | ||||||||||
provider_name: accredited_provider.provider_name, | ||||||||||
provider_code: accredited_provider.provider_code, | ||||||||||
description: accrediting_provider_enrichment.Description || '' | ||||||||||
} | ||||||||||
end | ||||||||||
end || [] | ||||||||||
end | ||||||||||
|
||||||||||
def add_enrichment_errors | ||||||||||
accrediting_provider_enrichments&.each do |item| | ||||||||||
provider_code = item.UcasProviderCode | ||||||||||
|
||||||||||
accredited_provider = recruitment_cycle.providers.find_by(provider_code:) | ||||||||||
|
||||||||||
if accredited_provider.present? && item.invalid? | ||||||||||
message = "^Reduce the word count for #{accredited_provider.provider_name}" | ||||||||||
errors.add :accredited_bodies, message | ||||||||||
end | ||||||||||
end | ||||||||||
end | ||||||||||
|
||||||||||
def accredited_provider_codes | ||||||||||
accrediting_provider_enrichments&.map(&:UcasProviderCode) || [] | ||||||||||
end | ||||||||||
|
||||||||||
... | ||||||||||
|
||||||||||
class Organisation < ApplicationRecord | ||||||||||
has_many :organisation_users | ||||||||||
|
||||||||||
has_many :users, through: :organisation_users | ||||||||||
|
||||||||||
has_and_belongs_to_many :providers | ||||||||||
|
||||||||||
... | ||||||||||
|
||||||||||
class Course < ApplicationRecord | ||||||||||
|
||||||||||
def accrediting_provider_description | ||||||||||
return if accrediting_provider.blank? | ||||||||||
return if provider.accrediting_provider_enrichments.blank? | ||||||||||
|
||||||||||
accrediting_provider_enrichment = provider.accrediting_provider_enrichments | ||||||||||
.find do |provider| | ||||||||||
provider.UcasProviderCode == accrediting_provider.provider_code | ||||||||||
end | ||||||||||
|
||||||||||
accrediting_provider_enrichment.Description if accrediting_provider_enrichment.present? | ||||||||||
end | ||||||||||
``` | ||||||||||
#### Pros | ||||||||||
|
||||||||||
- No effort expended and other work can be prioritised | ||||||||||
|
||||||||||
#### Cons | ||||||||||
|
||||||||||
- Eases the burden of onboarding developers | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The con is
|
||||||||||
- Leaves the data in an under constrained state | ||||||||||
- Inefficient methods for querying entity relationships | ||||||||||
|
||||||||||
### 2. Create typical self-join has_many through pivot table relationship | ||||||||||
|
||||||||||
```ruby | ||||||||||
class Provider < ApplicationRecord | ||||||||||
has_many :accredited_accreditations, | ||||||||||
class_name: 'ProviderAccreditation', | ||||||||||
foreign_key: :training_provider_id | ||||||||||
|
||||||||||
has_many :training_accreditations, | ||||||||||
class_name: 'ProviderAccreditation', | ||||||||||
foreign_key: :accredited_provider_id | ||||||||||
|
||||||||||
has_many :accredited_providers, | ||||||||||
through: :accredited_accreditations, | ||||||||||
source: :accredited_provider, | ||||||||||
class_name: 'Provider' | ||||||||||
|
||||||||||
has_many :training_providers, | ||||||||||
through: :training_accreditations, | ||||||||||
source: :training_provider, | ||||||||||
class_name: 'Provider' | ||||||||||
|
||||||||||
class ProviderAccreditation < ApplicationRecord | ||||||||||
|
||||||||||
``` | ||||||||||
|
||||||||||
#### Pros | ||||||||||
|
||||||||||
- Query the training provider / accredited provider relationships through typical relational associations | ||||||||||
- Index the columns used to make these queries. | ||||||||||
- More formally distinguish accredited and accrediting providers. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to review the correct usages of wording here as |
||||||||||
- Database validations (if desired) on which accredited providers can accredit which courses. | ||||||||||
- Opportunity to clarify definitions and understand how we are using them. | ||||||||||
|
||||||||||
#### Cons | ||||||||||
|
||||||||||
- None! | ||||||||||
|
||||||||||
|
||||||||||
### 3. Replicate the Apply User / Provider Relationship model | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to warrant deliberation and steer cc @carlosmartinez There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The relationship model in Apply/Manage is complex partly because of safeguarding but also to define who can make application decisions. Beyond the fundamental relationship (for example, X works with Y), I caution against a complex relationship model that may introduce a permissions model like Manage. Here is some early work we did on permissions in Publish, which we ended up not using:
And a related post Managing accredited bodies if you’re a lead school which includes changes that were implemented. (NB. 'accredited body' and 'lead school' are outdated terms replaced by 'accredited provider' and 'lead partner' respectively.) |
||||||||||
|
||||||||||
```ruby | ||||||||||
class ProviderRelationshipPermissions < ApplicationRecord | ||||||||||
belongs_to :ratifying_provider, class_name: 'Provider' | ||||||||||
belongs_to :training_provider, class_name: 'Provider' | ||||||||||
|
||||||||||
... | ||||||||||
|
||||||||||
class Provider < ApplicationRecord | ||||||||||
has_many :provider_permissions, dependent: :destroy | ||||||||||
has_many :provider_users, through: :provider_permissions | ||||||||||
has_many :training_provider_permissions, class_name: 'ProviderRelationshipPermissions', foreign_key: :training_provider_id | ||||||||||
has_many :ratifying_provider_permissions, class_name: 'ProviderRelationshipPermissions', foreign_key: :ratifying_provider_id | ||||||||||
|
||||||||||
... | ||||||||||
|
||||||||||
class Course < ApplicationRecord | ||||||||||
belongs_to :provider | ||||||||||
belongs_to :accredited_provider, class_name: 'Provider', optional: true | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
from publications negates |
||||||||||
|
||||||||||
... | ||||||||||
``` | ||||||||||
|
||||||||||
#### Pros | ||||||||||
|
||||||||||
- Consistency between the two services | ||||||||||
- Could we sync users and their permissions? | ||||||||||
|
||||||||||
#### Cons | ||||||||||
|
||||||||||
- None! | ||||||||||
|
||||||||||
### 4. Introduce the Lead partner delegation concept | ||||||||||
|
||||||||||
```ruby | ||||||||||
class Provider < ApplicationRecord | ||||||||||
has_many :accredited_accreditations, | ||||||||||
class_name: 'ProviderAccreditation', | ||||||||||
foreign_key: :training_provider_id | ||||||||||
|
||||||||||
has_many :training_accreditations, | ||||||||||
class_name: 'ProviderAccreditation', | ||||||||||
foreign_key: :accredited_provider_id | ||||||||||
|
||||||||||
has_many :accredited_providers, | ||||||||||
through: :accredited_accreditations, | ||||||||||
source: :accredited_provider, | ||||||||||
class_name: 'Provider' | ||||||||||
|
||||||||||
has_many :training_providers, | ||||||||||
through: :training_accreditations, | ||||||||||
source: :training_provider, | ||||||||||
class_name: 'Provider' | ||||||||||
``` | ||||||||||
|
||||||||||
#### Pros | ||||||||||
|
||||||||||
- Query the training provider / accredited provider relationships through typical relational associations | ||||||||||
- Index the columns used to make these queries. | ||||||||||
- More formally distinguish accredited and accrediting providers. | ||||||||||
- Database validations (if desired) on which accredited providers can accredit which courses. | ||||||||||
- Opportunity to clarify definitions and understand how we are using them. | ||||||||||
|
||||||||||
#### Cons | ||||||||||
|
||||||||||
- None! | ||||||||||
|
||||||||||
|
||||||||||
## Decision | ||||||||||
|
||||||||||
The change option that we're proposing or have agreed to implement. | ||||||||||
|
||||||||||
## Consequences | ||||||||||
|
||||||||||
What becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated. | ||||||||||
|
||||||||||
## Resources: | ||||||||||
|
||||||||||
[1]: https://becoming-a-teacher.design-history.education.gov.uk/becoming-a-teacher/understanding-the-relationships-between-organisations-delivering-initial-teacher-training/ | ||||||||||
|
||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For clarity, DfE accredits providers, and these accredited providers ratify courses published by their training (a.k.a. lead) partners.
Accredited providers cannot delegate their accreditation to training partners.