11from sqlalchemy import create_engine , select , delete
22from sqlalchemy .orm import sessionmaker , Session
33from os import environ
4+ from typing import Optional
45import urllib .parse
56from .db_models import *
67from .error_wrapper import sqlalchemy_http_exceptions
78from ..util .oidc_utils import OIDCUserInfo
9+ from ..util .ror_utils import validate_ror_id
810from ..models .api_models import InstitutionModel , OSG_ID_PREFIX
911from secrets import choice
1012from 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
3561def get_valid_institutions () -> List [InstitutionModel ]:
@@ -55,8 +81,14 @@ def get_institution_details(short_id: str) -> InstitutionModel:
5581@sqlalchemy_http_exceptions
5682def 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
87119def 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" )
0 commit comments