Skip to content

Commit 063796e

Browse files
authored
Merge pull request #291 from hydraplatform/project_template_map
feat: add a project-template map table
2 parents 2502c09 + 6ec55bd commit 063796e

4 files changed

Lines changed: 174 additions & 2 deletions

File tree

hydra_base/db/model/project.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .base import *
2020

2121
from .ownership import NetworkOwner, ProjectOwner
22+
from .template import ProjectTemplate
2223
from .scenario import Scenario, ResourceScenario
2324
from .permissions import User
2425
from .network import Network, ResourceAttr
@@ -62,6 +63,9 @@ class Project(Base, Inspect, PermissionControlled):
6263
layout = Column(JSON)
6364
user = relationship('User', backref=backref("projects", order_by=id))
6465

66+
templates = relationship('Template', secondary=ProjectTemplate.__table__,
67+
backref=backref('projects', uselist=True))
68+
6569
parent_id = Column(Integer(), ForeignKey('tProject.id'), nullable=True)
6670
parent = relationship('Project', remote_side=[id],
6771
backref=backref("children", order_by=id))

hydra_base/db/model/template.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
#
1919
from .base import *
2020

21-
__all__ = ['Template', 'TemplateType', 'TypeAttr', 'ResourceType']
21+
__all__ = ['Template', 'TemplateType', 'TypeAttr', 'ResourceType', 'ProjectTemplate']
2222

2323
from .attributes import Attr
2424
from .units import Unit
@@ -323,6 +323,7 @@ def get_hierarchy(self, user_id):
323323
hierarchy = hierarchy + self.parent.get_hierarchy(user_id)
324324
return hierarchy
325325

326+
326327
class TemplateType(Base, Inspect):
327328
"""
328329
Template Type
@@ -646,3 +647,17 @@ def get_templatetype(self):
646647
type_i = template_i.get_type(self.type_id)
647648

648649
return JSONObject(type_i)
650+
651+
class ProjectTemplate(Base, Inspect, PermissionControlled):
652+
"""
653+
Links a template to a project, allowing template lookup at the project level.
654+
"""
655+
656+
__tablename__ = 'tProjectTemplate'
657+
658+
project_id = Column(Integer(), ForeignKey('tProject.id'), primary_key=True, nullable=False)
659+
template_id = Column(Integer(), ForeignKey('tTemplate.id'), primary_key=True, nullable=False)
660+
cr_date = Column(TIMESTAMP(), nullable=False, server_default=text(u'CURRENT_TIMESTAMP'))
661+
662+
_parents = ['tProject', 'tTemplate']
663+
_children = []

hydra_base/lib/template/__init__.py

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
from hydra_base import db
2929
from hydra_base.db.model import (Template, TemplateType, TypeAttr, Attr,
3030
Network, Node, Link, ResourceGroup,
31-
ResourceType, ResourceAttr, ResourceScenario, Scenario)
31+
ResourceType, ResourceAttr, ResourceScenario, Scenario,
32+
Project, ProjectTemplate)
3233

3334
from hydra_base.db.model import Dataset as ModelDataset
3435
from hydra_base.lib.objects import JSONObject, Dataset
@@ -1359,3 +1360,93 @@ def get_all_parent_types(ttype_id, **kwargs):
13591360
tt_qry = tt_qry.filter(TemplateType.id.in_(parent_ids))
13601361
tt_qry = tt_qry.options(noload(TemplateType.typeattrs))
13611362
return tt_qry.all()
1363+
1364+
1365+
@required_perms("get_project")
1366+
def get_project_templates(project_id, **kwargs):
1367+
"""
1368+
Return all templates linked to a project.
1369+
The requesting user must have read access on the project.
1370+
"""
1371+
user_id = kwargs.get('user_id')
1372+
1373+
try:
1374+
project_i = db.DBSession.query(Project).filter(Project.id == project_id).one()
1375+
except NoResultFound:
1376+
raise ResourceNotFoundError(f"Project {project_id} not found")
1377+
1378+
project_i.check_read_permission(user_id)
1379+
1380+
return project_i.templates
1381+
1382+
1383+
@required_perms("edit_project")
1384+
def add_project_template(project_id, template_id, **kwargs):
1385+
"""
1386+
Link a template to a project.
1387+
The requesting user must have edit rights on the project.
1388+
"""
1389+
user_id = kwargs.get('user_id')
1390+
1391+
try:
1392+
project_i = db.DBSession.query(Project).filter(Project.id == project_id).one()
1393+
except NoResultFound:
1394+
raise ResourceNotFoundError(f"Project {project_id} not found")
1395+
1396+
project_i.check_write_permission(user_id)
1397+
1398+
try:
1399+
db.DBSession.query(Template).filter(Template.id == template_id).one()
1400+
except NoResultFound:
1401+
raise ResourceNotFoundError(f"Template {template_id} not found")
1402+
1403+
existing = db.DBSession.query(ProjectTemplate).filter(
1404+
ProjectTemplate.project_id == project_id,
1405+
ProjectTemplate.template_id == template_id
1406+
).first()
1407+
1408+
if existing is not None:
1409+
return existing
1410+
1411+
pt = ProjectTemplate()
1412+
pt.project_id = project_id
1413+
pt.template_id = template_id
1414+
db.DBSession.add(pt)
1415+
db.DBSession.flush()
1416+
1417+
log.info("Template %s linked to project %s", template_id, project_id)
1418+
1419+
return pt
1420+
1421+
1422+
@required_perms("edit_project")
1423+
def remove_project_template(project_id, template_id, **kwargs):
1424+
"""
1425+
Remove the link between a template and a project.
1426+
The requesting user must have edit rights on the project.
1427+
"""
1428+
user_id = kwargs.get('user_id')
1429+
1430+
try:
1431+
project_i = db.DBSession.query(Project).filter(Project.id == project_id).one()
1432+
except NoResultFound:
1433+
raise ResourceNotFoundError(f"Project {project_id} not found")
1434+
1435+
project_i.check_write_permission(user_id)
1436+
1437+
try:
1438+
pt = db.DBSession.query(ProjectTemplate).filter(
1439+
ProjectTemplate.project_id == project_id,
1440+
ProjectTemplate.template_id == template_id
1441+
).one()
1442+
except NoResultFound:
1443+
raise ResourceNotFoundError(
1444+
f"Template {template_id} is not linked to project {project_id}"
1445+
)
1446+
1447+
db.DBSession.delete(pt)
1448+
db.DBSession.flush()
1449+
1450+
log.info("Template %s unlinked from project %s", template_id, project_id)
1451+
1452+
return 'OK'

tests/templates/test_templates.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,3 +1207,65 @@ def test_type_compatibility(self, client, mock_template, mock_template_copy):
12071207
assert len(errors_diff) == 1
12081208
errors_same = client.check_type_compatibility(same_type_1_id, same_type_2_id)
12091209
assert len(errors_same) == 0
1210+
1211+
def test_add_project_template(self, client, projectmaker, mock_template):
1212+
project = projectmaker.create()
1213+
pt = client.add_project_template(project.id, mock_template.id)
1214+
assert pt is not None
1215+
assert pt.project_id == project.id
1216+
assert pt.template_id == mock_template.id
1217+
1218+
def test_add_project_template_duplicate(self, client, projectmaker, mock_template):
1219+
"""Adding the same link twice returns the existing record without error."""
1220+
project = projectmaker.create()
1221+
pt1 = client.add_project_template(project.id, mock_template.id)
1222+
pt2 = client.add_project_template(project.id, mock_template.id)
1223+
assert pt1.project_id == pt2.project_id
1224+
assert pt1.template_id == pt2.template_id
1225+
1226+
def test_add_project_template_invalid_project(self, client, mock_template):
1227+
with pytest.raises(Exception):
1228+
client.add_project_template(-999, mock_template.id)
1229+
1230+
def test_add_project_template_invalid_template(self, client, projectmaker):
1231+
project = projectmaker.create()
1232+
with pytest.raises(Exception):
1233+
client.add_project_template(project.id, -999)
1234+
1235+
def test_get_project_templates(self, client, projectmaker, mock_template):
1236+
project = projectmaker.create()
1237+
client.add_project_template(project.id, mock_template.id)
1238+
templates = client.get_project_templates(project.id)
1239+
assert templates is not None
1240+
template_ids = [t.id for t in templates]
1241+
assert mock_template.id in template_ids
1242+
1243+
def test_get_project_templates_empty(self, client, projectmaker):
1244+
"""A project with no linked templates returns an empty list."""
1245+
project = projectmaker.create()
1246+
templates = client.get_project_templates(project.id)
1247+
assert len(templates) == 0
1248+
1249+
def test_get_project_templates_multiple(self, client, projectmaker, mock_template, mock_template_copy):
1250+
"""Multiple templates can be linked to the same project."""
1251+
project = projectmaker.create()
1252+
client.add_project_template(project.id, mock_template.id)
1253+
client.add_project_template(project.id, mock_template_copy.id)
1254+
templates = client.get_project_templates(project.id)
1255+
template_ids = [t.id for t in templates]
1256+
assert mock_template.id in template_ids
1257+
assert mock_template_copy.id in template_ids
1258+
1259+
def test_remove_project_template(self, client, projectmaker, mock_template):
1260+
project = projectmaker.create()
1261+
client.add_project_template(project.id, mock_template.id)
1262+
result = client.remove_project_template(project.id, mock_template.id)
1263+
assert result == 'OK'
1264+
templates = client.get_project_templates(project.id)
1265+
assert mock_template.id not in [t.id for t in templates]
1266+
1267+
def test_remove_project_template_not_linked(self, client, projectmaker, mock_template):
1268+
"""Removing a template that isn't linked to the project raises an error."""
1269+
project = projectmaker.create()
1270+
with pytest.raises(Exception):
1271+
client.remove_project_template(project.id, mock_template.id)

0 commit comments

Comments
 (0)