Skip to content

Commit bd729c7

Browse files
authored
Merge pull request #3 from osg-htc/pr/auto-generate-id
Automatically generate OSG IID on the backend, validate ROR ID, enable reactivating institutions
2 parents 047da57 + 4ee8b91 commit bd729c7

File tree

11 files changed

+56
-42
lines changed

11 files changed

+56
-42
lines changed

institutions-api/app.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,4 @@ def invalidate_institution(institution_id: str, request: Request):
5757
db.invalidate_institution(institution_id, OIDCUserInfo(request))
5858
return "ok"
5959

60-
@prefix_router.get('/next_institution_id')
61-
@with_error_logging
62-
def get_next_institution_id():
63-
return { "next_id": db.get_unused_osg_id() }
64-
6560
app.include_router(prefix_router)

institutions-api/db/db.py

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from sqlalchemy import create_engine, select, delete
22
from sqlalchemy.orm import sessionmaker, Session
33
from os import environ
4+
from typing import Optional
45
import urllib.parse
56
from .db_models import *
67
from .error_wrapper import sqlalchemy_http_exceptions
78
from ..util.oidc_utils import OIDCUserInfo
9+
from ..util.ror_utils import validate_ror_id
810
from ..models.api_models import InstitutionModel, OSG_ID_PREFIX
911
from secrets import choice
1012
from string import ascii_lowercase, digits
@@ -26,10 +28,34 @@ def _ror_id_type(session: Session) -> IdentifierType:
2628
""" Get the IdentifierType entity that corresponds to ROR ID """
2729
return session.scalars(select(IdentifierType).where(IdentifierType.name == ROR_ID_TYPE)).first()
2830

29-
def _full_osg_id(short_id):
31+
def _full_osg_id(short_id: str):
3032
""" Get the full osg-htc url of an institution based on its ID suffix """
3133
return f"{OSG_ID_PREFIX}{short_id}"
3234

35+
def _short_osg_id(full_id: str):
36+
""" Get the full osg-htc url of an institution based on its ID suffix """
37+
return full_id.replace(OSG_ID_PREFIX, '')
38+
39+
def _get_unused_osg_id(session: Session):
40+
""" Generate an unused OSG ID """
41+
MAX_TRIES = 1000 # Give up after hitting x collisions in a row
42+
ID_LENGTH = 12
43+
44+
# TODO actually guaranteeing uniqueness here might be a bit overkill based on ID length
45+
all_ids = set(session.scalars(select(Institution.topology_identifier)).all())
46+
for _ in range(MAX_TRIES):
47+
short_id = ''.join([choice(ascii_lowercase + digits) for _ in range(ID_LENGTH)])
48+
next_id = f"{OSG_ID_PREFIX}{short_id}"
49+
if not next_id in all_ids:
50+
return next_id
51+
raise HTTPException(500, "Unable to generate new unique ID")
52+
53+
def _check_for_deactivated_institution(session: Session, name: str) -> Optional[str]:
54+
""" Check if a deactivated institution with the given name exists. Return its short ID if so.
55+
Used for reactivation workflows
56+
"""
57+
deactivated_inst = session.scalar(select(Institution).where(Institution.valid == False).where(Institution.name == name))
58+
return _short_osg_id(deactivated_inst.topology_identifier) if deactivated_inst else None
3359

3460
@sqlalchemy_http_exceptions
3561
def get_valid_institutions() -> List[InstitutionModel]:
@@ -55,8 +81,14 @@ def get_institution_details(short_id: str) -> InstitutionModel:
5581
@sqlalchemy_http_exceptions
5682
def add_institution(institution: InstitutionModel, author: OIDCUserInfo):
5783
""" Create a new institution """
84+
validate_ror_id(institution.ror_id)
5885
with DbSession() as session:
59-
inst = Institution(institution.name, institution.id, author.id)
86+
if deactivated_id := _check_for_deactivated_institution(session, institution.name):
87+
session.rollback()
88+
return update_institution(deactivated_id, institution, author)
89+
90+
topology_id = _get_unused_osg_id(session)
91+
inst = Institution(institution.name, topology_id, author.id)
6092
session.add(inst)
6193
if institution.ror_id:
6294
ror_id = InstitutionIdentifier(_ror_id_type(session), institution.ror_id, inst.id)
@@ -86,6 +118,7 @@ def _update_institution_ror_id(session: Session, institution: Institution, ror_i
86118
@sqlalchemy_http_exceptions
87119
def update_institution(short_id: str, institution: InstitutionModel, author: OIDCUserInfo):
88120
""" Update an existing institution """
121+
validate_ror_id(institution.ror_id)
89122
with DbSession() as session:
90123
to_update = session.scalar(select(Institution)
91124
.where(Institution.topology_identifier == _full_osg_id(short_id)))
@@ -96,6 +129,7 @@ def update_institution(short_id: str, institution: InstitutionModel, author: OID
96129
to_update.name = institution.name
97130
_update_institution_ror_id(session, to_update, institution.ror_id)
98131
to_update.updated_by = author.id
132+
to_update.valid = True
99133

100134
session.commit()
101135

@@ -108,19 +142,3 @@ def invalidate_institution(short_id: str, author: OIDCUserInfo):
108142
to_invalidate.valid = False
109143
to_invalidate.updated_by = author.id
110144
session.commit()
111-
112-
@sqlalchemy_http_exceptions
113-
def get_unused_osg_id():
114-
""" Generate an unused OSG ID """
115-
MAX_TRIES = 1000 # Give up after hitting x collisions in a row
116-
ID_LENGTH = 9
117-
118-
with DbSession() as session:
119-
all_ids = set(session.scalars(select(Institution.topology_identifier)).all())
120-
for _ in range(MAX_TRIES):
121-
short_id = ''.join([choice(ascii_lowercase + digits) for _ in range(ID_LENGTH)])
122-
next_id = f"{OSG_ID_PREFIX}{short_id}"
123-
if not next_id in all_ids:
124-
return next_id
125-
126-
raise HTTPException(500, "Unable to generate new unique ID")

institutions-api/db/db_models.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ class InstitutionIdentifier(Base):
5959

6060
identifier_type: Mapped["IdentifierType"] = relationship()
6161

62-
6362
def __init__(self, identifier_type: IdentifierType, identifier: str, institution_id: UUID = None):
6463
self.identifier_type_id = identifier_type.id
6564
self.identifier = identifier

institutions-api/models/api_models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
class InstitutionModel(BaseModel):
1010
""" API model for topology institutions """
1111
name: str = Field(..., description="The name of the institution")
12-
id: str = Field(..., description="The institution's OSG ID")
12+
id: Optional[str] = Field(None, description="The institution's OSG ID")
1313
ror_id: Optional[str] = Field(None, description="The institution's research organization registry id (https://ror.org/)")
1414

1515
@classmethod
@@ -23,6 +23,6 @@ def from_institution(cls, inst: Institution) -> "InstitutionModel":
2323
@model_validator(mode='after')
2424
def check_id_format(self):
2525
assert self.name, "Name must be non-empty"
26-
assert self.id.startswith(OSG_ID_PREFIX), f"OSG ID must start with '{OSG_ID_PREFIX}'"
26+
assert (not self.id) or self.id.startswith(OSG_ID_PREFIX), f"OSG ID must start with '{OSG_ID_PREFIX}'"
2727
assert (not self.ror_id) or self.ror_id.startswith(ROR_ID_PREFIX), f"ROR ID must be empty or start with '{ROR_ID_PREFIX}'"
2828
return self

institutions-api/util/ror_utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from typing import Optional
2+
from fastapi import HTTPException
3+
import requests
4+
from ..models.api_models import ROR_ID_PREFIX
5+
6+
7+
def validate_ror_id(ror_id: Optional[str]):
8+
""" Check whether an ROR ID is valid by performing a HEAD request to ror.org """
9+
# TODO we might want to rate-limit this in some way
10+
if ror_id and requests.head(ror_id, allow_redirects=True).status_code != 200:
11+
raise HTTPException(400, f"Invalid ROR ID: institution does not exist. See {ROR_ID_PREFIX}.")

institutions-ui/src/app/institutions-editor/institutions-editor.component.html

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ <h3 *ngIf="!institutionId">Create Institution</h3>
44
<div>
55
<span>Institution Name</span> <input type="text" [(ngModel)]="institution.name"/>
66
</div>
7-
<div *ngIf="!institutionId">
8-
<span>Institution ID</span> <input type="text" [(ngModel)]="institution.id"/>
9-
</div>
107
<div>
118
<span>Institution ROR ID</span><input type="text" [(ngModel)]="institution.ror_id"/>
129
</div>

institutions-ui/src/app/institutions-editor/institutions-editor.component.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class InstitutionsEditorComponent implements OnInit {
2020

2121
institutionId?: string;
2222

23-
institution: Institution = {name:'', id: this.OSG_ID_PREFIX , ror_id: this.ROR_ID_PREFIX };
23+
institution: Institution = {name:'', id: '' , ror_id: this.ROR_ID_PREFIX };
2424

2525
errorMessage?: string;
2626

@@ -35,9 +35,7 @@ export class InstitutionsEditorComponent implements OnInit {
3535
ngOnInit(): void {
3636
if(this.institutionId) {
3737
this.instService.getInstitutionDetails(this.institutionId).subscribe(inst=>this.institution=inst)
38-
} else {
39-
this.instService.getNextInstitutionId().subscribe(({next_id})=>this.institution.id = next_id)
40-
}
38+
}
4139
}
4240

4341
extractErrorMessage(err: any) {
@@ -47,8 +45,8 @@ export class InstitutionsEditorComponent implements OnInit {
4745

4846
sanitizeFormData() {
4947
// clean up form data prior to submission
50-
let {name, id, ror_id } = this.institution
51-
this.institution = { name: name.trim(), id: id.trim(), ror_id: ror_id?.trim() }
48+
let {name, ror_id } = this.institution
49+
this.institution = { name: name.trim(), ror_id: ror_id?.trim() }
5250
}
5351

5452
submitInstitution(): void {

institutions-ui/src/app/institutions-list/institutions-list.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,6 @@ export class InstitutionsListComponent implements OnInit{
2424
}
2525

2626
editRouteFor(inst: Institution) {
27-
return `edit/${this.instService.shortId(inst.id)}`
27+
return `edit/${this.instService.shortId(inst.id!)}`
2828
}
2929
}

institutions-ui/src/app/institutions/institutions.models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
export interface Institution {
33
name: String
4-
id: String
4+
id?: String
55
ror_id?: String
66
}
77

institutions-ui/src/app/institutions/institutions.service.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,4 @@ export class InstitutionsService {
3838
return this.http.delete(`${BASE_URL}/institutions/${this.shortId(institutionId)}`);
3939
}
4040

41-
getNextInstitutionId() {
42-
return this.http.get<NextId>(`${BASE_URL}/next_institution_id`)
43-
}
44-
45-
4641
}

0 commit comments

Comments
 (0)