Store school email domains#787
Conversation
There was a problem hiding this comment.
Pull request overview
Introduces first-class storage for school email domains by adding a dedicated table/model and wiring it into the School domain model, with accompanying specs.
Changes:
- Added
school_email_domainstable with a composite unique index per school. - Added
SchoolEmailDomainmodel withbefore_validationnormalization (strip leading@, downcase). - Updated
Schooltohas_many :school_email_domainsand added/updated model specs.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/models/school_spec.rb | Adds association + dependent-destroy coverage for school_email_domains. |
| spec/models/school_email_domain_spec.rb | Adds model spec coverage for domain normalization behavior. |
| db/schema.rb | Reflects new school_email_domains table and foreign key. |
| db/migrate/20260420104937_create_school_email_domains.rb | Creates the new school_email_domains table and composite unique index. |
| app/models/school_email_domain.rb | Introduces the new model and domain normalization callback. |
| app/models/school.rb | Adds has_many :school_email_domains, dependent: :destroy. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
4740867 to
aff003a
Compare
Test coverage89.58% line coverage reported by SimpleCov. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
fspeirs
left a comment
There was a problem hiding this comment.
Overall this looks like the right approach. I have one consideration about validating domains - I feel like it would be helpful to be stricter in checking that the user hasn't typo'ed their domain or misunderstood what we are asking for.
fspeirs
left a comment
There was a problem hiding this comment.
Directionally, quite correct. Just some sharpening of the normalisation approach in school_email_domain and a request for a testing method in school.rb. The lack of a testing method is non-blocking if it's coming in a separate PR.
| def normalise_domain | ||
| return if domain.nil? | ||
|
|
||
| self.domain = build_normalised_domain_string(domain) | ||
| end |
There was a problem hiding this comment.
Issue: I think this code could be much simpler and doesn't have to expressly handle specific things like uri_host_if_http_url. Instead, I suggest that we just try to parse a URI and, if successful take the host portion:
def normalise_domain
return if domain.blank?
value = domain.strip.downcase
# Add a scheme unless it already has one, so URI can parse it
value = "http://#{value}" unless value =~ %r{\A[a-z][a-z0-9+\-.]*://}i
uri = URI.parse(value)
host = uri.host
return if host.blank?
if PublicSuffix.valid?(host)
self.domain = host
else
errors.add(:domain, "is not a valid domain")
end
rescue URI::InvalidURIError
errors.add(:domain, "is not a valid domain")
endThis is kind of the approach you're taking in uri_host_if_http_url and pulling it up to be the main way we normalise domains: (a) can we parse it with URI::parse and secondly is it a valid suffix.
There was a problem hiding this comment.
Thanks for this Fraser.
I've implemented your changes and taken account of Päivi's comment below
| has_many :projects, dependent: :nullify | ||
| has_many :roles, dependent: :nullify | ||
| has_many :school_projects, dependent: :nullify | ||
| has_many :school_email_domains, dependent: :destroy |
There was a problem hiding this comment.
Issue: Callers (including my work) would like a validator method on this called something like: valid_domain?(candidate_domain) which returns true if candidate_domain is registered for this school.
This would allow testing domains presented by registering users without exposing the internal implementation of school_email_domains.
There was a problem hiding this comment.
I've added a validator method to School using a straightforward school_email_domains.exists?
I don't think we need to optimise for batch domain checks, since we're validating one student at a time
If we ever did need to perform some sort of batch validation, we could revisit this and perhaps pluck then memoise the domains
|
The regex we use in profile side to validate is While it is similar, we would reject things that had a dash in the wrong place, like |
| context 'with an invalid domain' do | ||
| it { is_expected.not_to allow_value('').for(:domain) } | ||
| it { is_expected.not_to allow_value(' ').for(:domain) } | ||
| it { is_expected.not_to allow_value('http://').for(:domain) } | ||
| it { is_expected.not_to allow_value('edu').for(:domain) } | ||
| it { is_expected.not_to allow_value('com').for(:domain) } | ||
| it { is_expected.not_to allow_value('co.uk').for(:domain) } | ||
| it { is_expected.not_to allow_value('http://invalid uri').for(:domain) } | ||
| it { is_expected.not_to allow_value('-wrong.edu').for(:domain) } |
| accounts_host_format = | ||
| /\A\s*(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}\s*\z/i |
There was a problem hiding this comment.
@peconia we're validating against the regex that Profile uses
Add a migration to create the school_email_domains table, storing a domain string against a school_id
Create the model and add initial specs
Removing leading @ symbols and lower case domains
4fda047 to
ead00de
Compare
ead00de to
81f8ad5
Compare
00a63d5 to
e5a847a
Compare
e5a847a to
f5eb365
Compare
Status
Closes #1325
What's changed?
school_email_domainstableSchoolEmailDomainmodelSchool