Skip to content

Commit a02f932

Browse files
committed
Merge branch 'main' into EPICSYSTEM-61
2 parents c133113 + 0dd1301 commit a02f932

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+47077
-46583
lines changed

docs/MET_database_ERD.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ erDiagram
1818
string banner_filename
1919
timestamp scheduled_date
2020
integer tenant_id FK "The id from tenant"
21-
boolean is_internal
21+
integer visibility FK "The id from engagement visibility"
2222
}
2323
survey {
2424
integer id PK
@@ -45,6 +45,14 @@ erDiagram
4545
string updated_by
4646
}
4747
engagement only one to one engagement_status : has
48+
engagement_visibility {
49+
integer id PK
50+
string visibility_name
51+
string description
52+
timestamp created_date
53+
timestamp updated_date
54+
}
55+
engagement only one to one engagement_visibility : has
4856
tenant {
4957
integer id PK
5058
string short_name
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Add visibility column to engagment
2+
3+
Revision ID: a3e6dae331ab
4+
Revises: 9a93fda677eb
5+
Create Date: 2024-05-28 08:26:11.155679
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
from sqlalchemy.dialects import postgresql
11+
12+
# revision identifiers, used by Alembic.
13+
revision = 'a3e6dae331ab'
14+
down_revision = '9a93fda677eb'
15+
branch_labels = None
16+
depends_on = None
17+
18+
def upgrade():
19+
# Create the engagement_visibility table
20+
engagement_visiblity_table = op.create_table(
21+
'engagement_visibility',
22+
sa.Column('id', sa.Integer(), nullable=False, autoincrement=True),
23+
sa.Column('visibility_name', sa.String(length=50), nullable=False),
24+
sa.Column('description', sa.String(length=100), nullable=False),
25+
sa.Column('created_date', sa.TIMESTAMP(timezone=False), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
26+
sa.Column('updated_date', sa.TIMESTAMP(timezone=False), nullable=True),
27+
sa.PrimaryKeyConstraint('id')
28+
)
29+
# Insert the initial data into the engagement_visibility table
30+
op.bulk_insert(
31+
engagement_visiblity_table,
32+
[
33+
{'id': 1, 'visibility_name': 'Public', 'description': 'Visible to all users'},
34+
{'id': 2, 'visibility_name': 'Slug', 'description': 'Accessible to users with the direct link'},
35+
{'id': 3, 'visibility_name': 'AuthToken', 'description': 'Visible to authenticated users'}
36+
]
37+
)
38+
# Add the visibility column to the engagement table
39+
op.add_column('engagement', sa.Column('visibility', sa.Integer(), nullable=False, server_default='1'))
40+
op.create_foreign_key("engagement_visibility_fkey", 'engagement', 'engagement_visibility', ['visibility'], ['id'])
41+
# Update the visibility column based on the is_internal column
42+
op.execute("""
43+
UPDATE engagement e
44+
SET visibility = ev.id
45+
FROM engagement_visibility ev
46+
WHERE (e.is_internal AND ev.visibility_name = 'AuthToken')
47+
OR (NOT e.is_internal AND ev.visibility_name = 'Public')
48+
""")
49+
op.drop_column('engagement', 'is_internal')
50+
51+
52+
def downgrade():
53+
# Add the is_internal column to the engagement table
54+
op.add_column('engagement', sa.Column('is_internal', sa.BOOLEAN(), nullable=False, server_default='0'))
55+
# Populate the is_internal column based on the visibility column
56+
op.execute("""
57+
UPDATE engagement e
58+
SET is_internal = (e.visibility = (SELECT id FROM engagement_visibility WHERE visibility_name = 'AuthToken'))
59+
""")
60+
# Drop the foreign key constraint on the visibility column
61+
op.drop_constraint("engagement_visibility_fkey", 'engagement', type_='foreignkey')
62+
op.drop_column('engagement', 'visibility')
63+
op.drop_table('engagement_visibility')
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright © 2024 Province of British Columbia
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the 'License');
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an 'AS IS' BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Constants of engagement visibility."""
15+
from enum import IntEnum
16+
17+
18+
class Visibility(IntEnum):
19+
"""Enum of engagement visibility."""
20+
21+
Public = 1
22+
Slug = 2
23+
AuthToken = 3

met-api/src/met_api/models/comment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,6 @@ def get_public_viewable_comments_by_survey_id(cls, survey_id):
250250
ReportSetting.display == true(),
251251
Submission.reviewed_by != 'System'
252252
))
253-
query = query.order_by(Comment.text.asc())
253+
query = query.order_by(Comment.id.asc())
254254
items = query.all()
255255
return CommentSchema(many=True, only=['submission_id', 'label', 'text']).dump(items)

met-api/src/met_api/models/engagement.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from sqlalchemy.sql.schema import ForeignKey
1515

1616
from met_api.constants.engagement_status import EngagementDisplayStatus, Status
17+
from met_api.constants.engagement_visibility import Visibility
1718
from met_api.constants.user import SYSTEM_USER
1819
from met_api.models.engagement_metadata import EngagementMetadataModel
1920
from met_api.models.membership import Membership as MembershipModel
@@ -26,6 +27,7 @@
2627
from .base_model import BaseModel
2728
from .db import db
2829
from .engagement_status import EngagementStatus
30+
from .engagement_visibility import EngagementVisibility
2931

3032

3133
class Engagement(BaseModel):
@@ -47,7 +49,7 @@ class Engagement(BaseModel):
4749
surveys = db.relationship('Survey', backref='engagement', cascade='all, delete')
4850
status_block = db.relationship('EngagementStatusBlock', backref='engagement')
4951
tenant_id = db.Column(db.Integer, db.ForeignKey('tenant.id'), nullable=True)
50-
is_internal = db.Column(db.Boolean, nullable=False)
52+
visibility = db.Column(db.Integer, ForeignKey('engagement_visibility.id'), nullable=False)
5153

5254
@classmethod
5355
def get_engagements_paginated(
@@ -58,7 +60,7 @@ def get_engagements_paginated(
5860
search_options=None,
5961
):
6062
"""Get engagements paginated."""
61-
query = db.session.query(Engagement).join(EngagementStatus)
63+
query = db.session.query(Engagement).join(EngagementStatus).join(EngagementVisibility)
6264

6365
query = cls._add_tenant_filter(query)
6466

@@ -73,7 +75,7 @@ def get_engagements_paginated(
7375

7476
query = cls._filter_by_project_metadata(query, search_options)
7577

76-
query = cls._filter_by_internal(query, search_options)
78+
query = cls._filter_by_visibility(query, search_options)
7779

7880
if scope_options.restricted:
7981
if scope_options.include_assigned:
@@ -122,7 +124,7 @@ def update_engagement(cls, engagement: EngagementSchema) -> Engagement:
122124
banner_filename=engagement.get('banner_filename', None),
123125
content=engagement.get('content', None),
124126
rich_content=engagement.get('rich_content', None),
125-
is_internal=engagement.get('is_internal', record.is_internal),
127+
visibility=engagement.get('visibility', record.visibility)
126128
)
127129
query.update(update_fields)
128130
db.session.commit()
@@ -184,10 +186,9 @@ def publish_scheduled_engagements_due(cls) -> List[Engagement]:
184186
.filter(Engagement.status_id == Status.Scheduled.value) \
185187
.filter(Engagement.scheduled_date <= datetime_due)
186188
records = query.all()
187-
if not records:
188-
return None
189-
query.update(update_fields)
190-
db.session.commit()
189+
if records:
190+
query.update(update_fields)
191+
db.session.commit()
191192
return records
192193

193194
@staticmethod
@@ -253,10 +254,12 @@ def _filter_by_search_text(query, search_options):
253254
return query
254255

255256
@staticmethod
256-
def _filter_by_internal(query, search_options):
257+
def _filter_by_visibility(query, search_options):
257258
if exclude_internal := search_options.get('exclude_internal'):
258259
if exclude_internal:
259-
query = query.filter(Engagement.is_internal.is_(False))
260+
query = query.filter(Engagement.visibility == Visibility.Public)
261+
else:
262+
query = query.filter(Engagement.visibility == Visibility.AuthToken)
260263
return query
261264

262265
@staticmethod
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Engagement Visibility model class.
2+
3+
Manages the engagement visibility
4+
"""
5+
from .base_model import BaseModel
6+
from .db import db, ma
7+
8+
9+
class EngagementVisibility(BaseModel): # pylint: disable=too-few-public-methods
10+
"""Definition of the Engagement Visibility entity."""
11+
12+
__tablename__ = 'engagement_visibility'
13+
14+
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
15+
visibility_name = db.Column(db.String(50), nullable=False)
16+
description = db.Column(db.String(50))
17+
created_date = db.Column(db.DateTime, nullable=False, default=db.func.current_timestamp())
18+
updated_date = db.Column(db.DateTime, nullable=True)
19+
20+
21+
class EngagementVisibilitySchema(ma.Schema):
22+
"""Engagement visibility schema."""
23+
24+
class Meta: # pylint: disable=too-few-public-methods
25+
"""Meta class."""
26+
27+
fields = ('id', 'visibility_name', 'description', 'created_date', 'updated_date')

met-api/src/met_api/resources/engagement.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ def get(engagement_id):
5151
if engagement_record:
5252
return engagement_record, HTTPStatus.OK
5353

54-
return 'Engagement was not found', HTTPStatus.INTERNAL_SERVER_ERROR
54+
return 'Engagement was not found', HTTPStatus.NOT_FOUND
5555
except KeyError:
56-
return 'Engagement was not found', HTTPStatus.INTERNAL_SERVER_ERROR
56+
return 'Engagement was not found', HTTPStatus.NOT_FOUND
5757
except ValueError as err:
5858
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR
5959

met-api/src/met_api/schemas/engagement.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from met_api.utils.datetime import local_datetime
1616

1717
from .engagement_status import EngagementStatusSchema
18+
from .engagement_visibility import EngagementVisibilitySchema
1819

1920

2021
class EngagementSchema(Schema):
@@ -47,7 +48,8 @@ class Meta: # pylint: disable=too-few-public-methods
4748
submissions_meta_data = fields.Method('get_submissions_meta_data')
4849
status_block = fields.List(fields.Nested(EngagementStatusBlockSchema))
4950
tenant_id = fields.Str(data_key='tenant_id')
50-
is_internal = fields.Bool(data_key='is_internal')
51+
visibility = fields.Int(data_key='visibility')
52+
engagement_visibility = fields.Nested(EngagementVisibilitySchema)
5153

5254
def get_submissions_meta_data(self, obj):
5355
"""Get the meta data of the submissions made in the survey."""
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Engagement visibility schema class."""
2+
from marshmallow import EXCLUDE, Schema, fields
3+
4+
5+
class EngagementVisibilitySchema(Schema):
6+
"""Schema for engagement visibility."""
7+
8+
class Meta: # pylint: disable=too-few-public-methods
9+
"""Exclude unknown fields in the deserialized output."""
10+
11+
unknown = EXCLUDE
12+
13+
id = fields.Int(data_key='id')
14+
visibility_name = fields.Str(data_key='visibility_name')
15+
description = fields.Str(data_key='description')
16+
created_date = fields.Str(data_key='created_date')
17+
updated_date = fields.Str(data_key='updated_date')

met-api/src/met_api/services/email_verification_service.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from flask import current_app
77
from met_api.constants.email_verification import INTERNAL_EMAIL_DOMAIN, EmailVerificationType
8+
from met_api.constants.engagement_visibility import Visibility
89

910
from met_api.constants.subscription_type import SubscriptionTypes
1011
from met_api.exceptions.business_exception import BusinessException
@@ -52,7 +53,7 @@ def create(cls, email_verification: EmailVerificationSchema,
5253
survey = SurveyModel.find_by_id(email_verification.get('survey_id'))
5354
engagement: EngagementModel = EngagementModel.find_by_id(
5455
survey.engagement_id)
55-
if engagement.is_internal and not email_address.endswith(INTERNAL_EMAIL_DOMAIN):
56+
if engagement.visibility == Visibility.AuthToken and not email_address.endswith(INTERNAL_EMAIL_DOMAIN):
5657
raise BusinessException(
5758
error='Not an internal email address.',
5859
status_code=HTTPStatus.INTERNAL_SERVER_ERROR)
@@ -64,12 +65,13 @@ def create(cls, email_verification: EmailVerificationSchema,
6465

6566
email_verification['created_by'] = email_verification.get(
6667
'participant_id')
67-
email_verification['verification_token'] = uuid.uuid4()
68-
EmailVerification.create(email_verification, session)
68+
verification_token = uuid.uuid4()
69+
EmailVerification.create({**email_verification, 'verification_token': verification_token}, session)
6970

7071
# TODO: remove this once email logic is brought over from submission service to here
7172
if email_verification.get('type', None) != EmailVerificationType.RejectedComment:
72-
cls._send_verification_email(email_verification, subscription_type)
73+
cls._send_verification_email(
74+
{**email_verification, 'verification_token': verification_token}, subscription_type)
7375

7476
return email_verification
7577

@@ -239,14 +241,14 @@ def _get_tenant_name(tenant_id):
239241

240242
@staticmethod
241243
def _get_project_name(subscription_type, tenant_name, engagement):
242-
metadata_model: EngagementMetadataModel = EngagementMetadataModel.find_by_id(engagement.id)
243244
if subscription_type == SubscriptionTypes.TENANT.value:
244245
return tenant_name
245246

246247
if subscription_type == SubscriptionTypes.PROJECT.value:
247248
metadata_model: EngagementMetadataModel = EngagementMetadataModel.find_by_id(engagement.id)
248-
project_name = metadata_model.project_metadata.get('project_name', None)
249-
return project_name or engagement.name
249+
project_name = (
250+
metadata_model.project_metadata.get('project_name', None) if metadata_model else engagement.name)
251+
return project_name
250252

251253
if subscription_type == SubscriptionTypes.ENGAGEMENT.value:
252254
return engagement.name

met-api/src/met_api/services/engagement_service.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from flask import current_app
66

77
from met_api.constants.engagement_status import Status
8+
from met_api.constants.engagement_visibility import Visibility
89
from met_api.constants.membership_type import MembershipType
910
from met_api.exceptions.business_exception import BusinessException
1011
from met_api.models.engagement import Engagement as EngagementModel
@@ -139,9 +140,11 @@ def publish_scheduled_engagements():
139140
engagements = EngagementModel.publish_scheduled_engagements_due()
140141
print('Engagements published: ', engagements)
141142
for engagement in engagements:
142-
email_util.publish_to_email_queue(SourceType.ENGAGEMENT.value, engagement.id,
143-
SourceAction.PUBLISHED.value, True)
144-
print('Engagements published added to email queue: ', engagement.id)
143+
# Only add to email queue if engagement is public.
144+
if engagement.visibility == Visibility.Public.value:
145+
email_util.publish_to_email_queue(SourceType.ENGAGEMENT.value, engagement.id,
146+
SourceAction.PUBLISHED.value, True)
147+
print('Engagements published added to email queue: ', engagement.id)
145148
return engagements
146149

147150
@staticmethod
@@ -178,7 +181,7 @@ def _create_engagement_model(engagement_data: dict) -> EngagementModel:
178181
banner_filename=engagement_data.get('banner_filename', None),
179182
content=engagement_data.get('content', None),
180183
rich_content=engagement_data.get('rich_content', None),
181-
is_internal=engagement_data.get('is_internal', False)
184+
visibility=engagement_data.get('visibility', None)
182185
)
183186
new_engagement.save()
184187
return new_engagement
@@ -220,7 +223,7 @@ def _save_or_update_eng_block(engagement_id, status_block):
220223
@staticmethod
221224
def _validate_engagement_edit_data(engagement_id: int, data: dict):
222225
engagement = EngagementModel.find_by_id(engagement_id)
223-
draft_status_restricted_changes = (EngagementModel.is_internal.key,)
226+
draft_status_restricted_changes = (EngagementModel.visibility.key,)
224227
engagement_has_been_opened = engagement.status_id != Status.Draft.value
225228
if engagement_has_been_opened and any(field in data for field in draft_status_restricted_changes):
226229
raise ValueError('Some fields cannot be updated after the engagement has been published')

met-api/src/met_api/services/engagement_slug_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def update_engagement_slug(cls, slug: str, engagement_id: int) -> EngagementSlug
100100

101101
# publish changes to EPIC
102102
ProjectService.update_project_info(engagement_id)
103-
103+
104104
return {
105105
'slug': engagement_slug.slug,
106106
'engagement_id': engagement_slug.engagement_id,

0 commit comments

Comments
 (0)