Skip to content

Commit a4bebe0

Browse files
Merge pull request #150 from VineetBala-AOT/main
Project and document visibility after EPIC Public sync
2 parents 4d13bf6 + 10bd050 commit a4bebe0

File tree

15 files changed

+319
-9
lines changed

15 files changed

+319
-9
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""add_is_active_to_projects_and_documents
2+
3+
Revision ID: a3f2b8c91d47
4+
Revises: 1eb403b982a4
5+
Create Date: 2026-02-19
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = 'a3f2b8c91d47'
14+
down_revision = '1eb403b982a4'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# Add is_active column to projects table with default True
21+
with op.batch_alter_table('projects', schema='condition') as batch_op:
22+
batch_op.add_column(
23+
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('false'))
24+
)
25+
26+
# Add is_active column to documents table with default True
27+
with op.batch_alter_table('documents', schema='condition') as batch_op:
28+
batch_op.add_column(
29+
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('false'))
30+
)
31+
32+
# Set is_active = False for any records created by the cronjob
33+
op.execute("""
34+
UPDATE condition.projects
35+
SET is_active = false
36+
WHERE created_by = 'cronjob'
37+
""")
38+
39+
op.execute("""
40+
UPDATE condition.documents
41+
SET is_active = false
42+
WHERE created_by = 'cronjob'
43+
""")
44+
45+
46+
def downgrade():
47+
with op.batch_alter_table('documents', schema='condition') as batch_op:
48+
batch_op.drop_column('is_active')
49+
50+
with op.batch_alter_table('projects', schema='condition') as batch_op:
51+
batch_op.drop_column('is_active')

condition-api/src/condition_api/models/document.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class Document(BaseModel):
2929
first_nations = Column(ARRAY(Text))
3030
consultation_records_required = Column(Boolean)
3131
is_latest_amendment_added = Column(Boolean)
32+
is_active = Column(Boolean, default=False, nullable=False, server_default='false')
3233

3334
# Foreign key to link to the project
3435
project_id = Column(String(255), ForeignKey('condition.projects.project_id', ondelete='RESTRICT'))

condition-api/src/condition_api/models/project.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
Manages the project
44
"""
5-
from sqlalchemy import Column, Integer, String, Text
5+
from sqlalchemy import Boolean, Column, Integer, String, Text
66
from sqlalchemy.ext.declarative import declarative_base
77
from sqlalchemy.orm import relationship
88
from sqlalchemy.schema import UniqueConstraint
@@ -21,6 +21,7 @@ class Project(BaseModel):
2121
project_id = Column(String(255), nullable=False, unique=True)
2222
project_name = Column(Text)
2323
project_type = Column(Text)
24+
is_active = Column(Boolean, default=False, nullable=False, server_default='false')
2425

2526
# Establish a one-to-many relationship with the Document table
2627
documents = relationship('Document', back_populates='project')

condition-api/src/condition_api/resources/document.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,52 @@ def get(project_id):
8686
return {"message": str(err)}, HTTPStatus.BAD_REQUEST
8787

8888

89+
@cors_preflight("GET, OPTIONS")
90+
@API.route("/project/<string:project_id>/available", methods=["GET", "OPTIONS"])
91+
class AvailableDocumentsResource(Resource):
92+
"""Resource for fetching inactive (available to add) documents for a project."""
93+
94+
@staticmethod
95+
@ApiHelper.swagger_decorators(API, endpoint_description="Get available documents for a project")
96+
@API.response(code=HTTPStatus.OK, model=document_model, description="Get available documents")
97+
@API.response(HTTPStatus.BAD_REQUEST, "Bad Request")
98+
@auth.has_one_of_roles([EpicConditionRole.VIEW_CONDITIONS.value])
99+
@cross_origin(origins=allowedorigins())
100+
def get(project_id):
101+
"""Fetch inactive documents for a project that can be added."""
102+
try:
103+
documents = DocumentService.get_available_documents(project_id)
104+
return DocumentSchema(many=True).dump(documents), HTTPStatus.OK
105+
except ValidationError as err:
106+
return {"message": str(err)}, HTTPStatus.BAD_REQUEST
107+
108+
109+
@cors_preflight("PATCH, OPTIONS")
110+
@API.route("/<string:document_id>/activate", methods=["PATCH", "OPTIONS"])
111+
class ActivateDocumentResource(Resource):
112+
"""Resource for activating a document."""
113+
114+
@staticmethod
115+
@ApiHelper.swagger_decorators(API, endpoint_description="Activate a document")
116+
@API.response(code=HTTPStatus.OK, model=document_model, description="Activate document")
117+
@API.response(HTTPStatus.BAD_REQUEST, "Bad Request")
118+
@auth.has_one_of_roles([EpicConditionRole.VIEW_CONDITIONS.value])
119+
@cross_origin(origins=allowedorigins())
120+
def patch(document_id):
121+
"""Activate a document to make it visible."""
122+
try:
123+
document = DocumentService.activate_document(document_id)
124+
if not document:
125+
return {"message": "Document not found"}, HTTPStatus.NOT_FOUND
126+
return DocumentSchema().dump({
127+
"document_id": document.document_id,
128+
"document_label": document.document_label,
129+
"is_active": document.is_active,
130+
}), HTTPStatus.OK
131+
except ValidationError as err:
132+
return {"message": str(err)}, HTTPStatus.BAD_REQUEST
133+
134+
89135
@cors_preflight("GET, OPTIONS")
90136
@API.route("/type", methods=["GET", "OPTIONS"])
91137
class DocumentTypeResource(Resource):

condition-api/src/condition_api/resources/project.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,53 @@ def get():
6363
return {"message": str(err)}, HTTPStatus.BAD_REQUEST
6464

6565

66+
@cors_preflight("GET, OPTIONS")
67+
@API.route("/available", methods=["GET", "OPTIONS"])
68+
class AvailableProjectsResource(Resource):
69+
"""Resource for fetching inactive (available to add) projects."""
70+
71+
@staticmethod
72+
@ApiHelper.swagger_decorators(API, endpoint_description="Get available projects")
73+
@API.response(code=HTTPStatus.OK, model=projects_model, description="Get available projects")
74+
@API.response(HTTPStatus.BAD_REQUEST, "Bad Request")
75+
@auth.has_one_of_roles([EpicConditionRole.VIEW_CONDITIONS.value])
76+
@cross_origin(origins=allowedorigins())
77+
def get():
78+
"""Fetch inactive projects that can be added to the condition repo."""
79+
try:
80+
project_data = ProjectService.get_available_projects()
81+
projects_schema = ProjectSchema(many=True)
82+
return projects_schema.dump(project_data), HTTPStatus.OK
83+
except ValidationError as err:
84+
return {"message": str(err)}, HTTPStatus.BAD_REQUEST
85+
86+
87+
@cors_preflight("PATCH, OPTIONS")
88+
@API.route("/<string:project_id>/activate", methods=["PATCH", "OPTIONS"])
89+
class ActivateProjectResource(Resource):
90+
"""Resource for activating a project."""
91+
92+
@staticmethod
93+
@ApiHelper.swagger_decorators(API, endpoint_description="Activate a project")
94+
@API.response(code=HTTPStatus.OK, model=projects_model, description="Activate project")
95+
@API.response(HTTPStatus.BAD_REQUEST, "Bad Request")
96+
@auth.has_one_of_roles([EpicConditionRole.VIEW_CONDITIONS.value])
97+
@cross_origin(origins=allowedorigins())
98+
def patch(project_id):
99+
"""Activate a project to make it visible."""
100+
try:
101+
project = ProjectService.activate_project(project_id)
102+
if not project:
103+
return {"message": "Project not found"}, HTTPStatus.NOT_FOUND
104+
return ProjectSchema().dump({
105+
"project_id": project.project_id,
106+
"project_name": project.project_name,
107+
"is_active": project.is_active,
108+
}), HTTPStatus.OK
109+
except ValidationError as err:
110+
return {"message": str(err)}, HTTPStatus.BAD_REQUEST
111+
112+
66113
@cors_preflight("GET, OPTIONS")
67114
@API.route("/with-approved-conditions", methods=["GET", "OPTIONS"])
68115
class ApprovedProjectsResource(Resource):

condition-api/src/condition_api/schemas/document.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class DocumentSchema(Schema):
3737
status = fields.Bool(data_key="status")
3838
amendment_count = fields.Int(data_key="amendment_count")
3939
is_latest_amendment_added = fields.Bool(data_key="is_latest_amendment_added")
40+
is_active = fields.Bool(data_key="is_active")
4041
project_name = fields.Str(data_key="project_name")
4142
type = fields.Str(data_key="type")
4243

condition-api/src/condition_api/schemas/project.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class ProjectSchema(BaseSchema):
2222

2323
project_id = fields.Str(data_key="project_id")
2424
project_name = fields.Str(data_key="project_name")
25+
is_active = fields.Bool(data_key="is_active")
2526

2627
# A project can have multiple documents
2728
documents = fields.List(fields.Nested(DocumentSchema), data_key="documents")

condition-api/src/condition_api/services/document_service.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def get_all_documents_by_category(project_id, category_id):
105105
).filter(
106106
(Project.project_id == project_id)
107107
& (DocumentCategory.id == category_id)
108+
& (Document.is_active.is_(True))
108109
).group_by(
109110
Project.project_name,
110111
DocumentCategory.category_name,
@@ -238,7 +239,8 @@ def get_all_documents_by_project_id(project_id, document_id=None, document_type=
238239
Project,
239240
Project.project_id == Document.project_id
240241
).filter(
241-
Project.project_id == project_id
242+
Project.project_id == project_id,
243+
Document.is_active.is_(True)
242244
)
243245

244246
documents = documents_query.all()
@@ -404,3 +406,45 @@ def update_document(document_id: str, document_label: str):
404406

405407
# Return updated document details
406408
return DocumentService.get_document_details(document_id)
409+
410+
@staticmethod
411+
def get_available_documents(project_id):
412+
"""Fetch all inactive documents for a project (synced but not yet added)."""
413+
documents = (
414+
db.session.query(
415+
Document.document_id,
416+
Document.document_label,
417+
Document.date_issued,
418+
DocumentTypeModel.document_type.label('document_type'),
419+
)
420+
.outerjoin(DocumentTypeModel, DocumentTypeModel.id == Document.document_type_id)
421+
.filter(
422+
Document.project_id == project_id,
423+
Document.is_active.is_(False)
424+
)
425+
.order_by(Document.date_issued.desc())
426+
.all()
427+
)
428+
429+
if not documents:
430+
return []
431+
432+
return [
433+
{
434+
"document_id": row.document_id,
435+
"document_label": row.document_label,
436+
"date_issued": str(row.date_issued) if row.date_issued else None,
437+
"document_type": row.document_type,
438+
}
439+
for row in documents
440+
]
441+
442+
@staticmethod
443+
def activate_document(document_id):
444+
"""Activate a document to make it visible in the condition repo."""
445+
document = db.session.query(Document).filter_by(document_id=document_id).first()
446+
if not document:
447+
return None
448+
document.is_active = True
449+
db.session.commit()
450+
return document

condition-api/src/condition_api/services/project_service.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@ def get_all_projects():
4444
func.count(Amendment.document_id).label("amendment_count"), # pylint: disable=not-callable
4545
func.bool_or(Document.is_latest_amendment_added).label("is_latest_amendment_added")
4646
)
47-
.outerjoin(Document, Document.project_id == Project.project_id)
47+
.outerjoin(Document, and_(Document.project_id == Project.project_id, Document.is_active.is_(True)))
4848
.outerjoin(DocumentType, DocumentType.id == Document.document_type_id)
4949
.outerjoin(DocumentCategory, DocumentCategory.id == DocumentType.document_category_id)
5050
.outerjoin(Amendment, Amendment.document_id == Document.id)
51+
.filter(Project.is_active.is_(True))
5152
.group_by(
5253
Project.project_id,
5354
Project.project_name,
@@ -98,7 +99,11 @@ def check_project_conditions(project_id, document_category_id):
9899
db.session.query(Document.id, Document.document_id)
99100
.join(DocumentType, DocumentType.id == Document.document_type_id)
100101
.join(DocumentCategory, DocumentCategory.id == DocumentType.document_category_id)
101-
.filter(and_(Document.project_id == project_id, DocumentCategory.id == document_category_id))
102+
.filter(and_(
103+
Document.project_id == project_id,
104+
DocumentCategory.id == document_category_id,
105+
Document.is_active.is_(True)
106+
))
102107
.all()
103108
)
104109

@@ -199,6 +204,7 @@ def get_projects_with_approved_conditions():
199204
db.session.query(Project.project_id)
200205
.join(Condition, Project.project_id == Condition.project_id)
201206
.filter(
207+
Project.is_active.is_(True),
202208
Condition.is_approved.is_(True),
203209
Condition.is_topic_tags_approved.is_(True),
204210
Condition.is_condition_attributes_approved.is_(True)
@@ -208,3 +214,37 @@ def get_projects_with_approved_conditions():
208214
)
209215

210216
return projects
217+
218+
@staticmethod
219+
def get_available_projects():
220+
"""Fetch all inactive projects (synced but not yet added to condition repo)."""
221+
projects = (
222+
db.session.query(
223+
Project.project_id,
224+
Project.project_name,
225+
)
226+
.filter(Project.is_active.is_(False))
227+
.order_by(Project.project_name)
228+
.all()
229+
)
230+
231+
if not projects:
232+
return []
233+
234+
return [
235+
{
236+
"project_id": row.project_id,
237+
"project_name": row.project_name,
238+
}
239+
for row in projects
240+
]
241+
242+
@staticmethod
243+
def activate_project(project_id):
244+
"""Activate a project to make it visible in the condition repo."""
245+
project = Project.get_by_id(project_id)
246+
if not project:
247+
return None
248+
project.is_active = True
249+
db.session.commit()
250+
return project

condition-api/tests/utilities/factory_utils.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,15 @@ def factory_auth_header(jwt, claims):
5656
def factory_project_model(
5757
project_id="58851056aaecd9001b80ebf8",
5858
project_name="Tulsequah Chief Mine",
59-
project_type="Mines"
59+
project_type="Mines",
60+
is_active=True
6061
):
6162
"""Factory for Project model."""
6263
project = Project(
6364
project_id=project_id,
6465
project_name=project_name,
65-
project_type=project_type
66+
project_type=project_type,
67+
is_active=is_active
6668
)
6769
db.session.add(project)
6870
db.session.commit()
@@ -89,7 +91,7 @@ def factory_document_type_model(category, name="Certificate"):
8991
return doc_type
9092

9193

92-
def factory_document_model(project_id, document_type_id, is_latest=True):
94+
def factory_document_model(project_id, document_type_id, is_latest=True, is_active=True):
9395
"""Document"""
9496
document = Document(
9597
document_id=str(uuid.uuid4()),
@@ -99,7 +101,8 @@ def factory_document_model(project_id, document_type_id, is_latest=True):
99101
document_file_name="test.pdf",
100102
is_latest_amendment_added=is_latest,
101103
date_issued=datetime.utcnow(),
102-
consultation_records_required=True
104+
consultation_records_required=True,
105+
is_active=is_active
103106
)
104107
db.session.add(document)
105108
db.session.commit()

0 commit comments

Comments
 (0)