Skip to content

Commit

Permalink
[EPICSYSTEM-20] Add new Hidden visibility to engagements (#59)
Browse files Browse the repository at this point in the history
* [EPICSYSTEM-20] Add new Hidden visibility to engagements
  • Loading branch information
tolkamps1 authored Jun 12, 2024
1 parent 179cae7 commit af87b24
Show file tree
Hide file tree
Showing 27 changed files with 483 additions and 105 deletions.
10 changes: 9 additions & 1 deletion docs/MET_database_ERD.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ erDiagram
string banner_filename
timestamp scheduled_date
integer tenant_id FK "The id from tenant"
boolean is_internal
integer visibility FK "The id from engagement visibility"
}
survey {
integer id PK
Expand All @@ -45,6 +45,14 @@ erDiagram
string updated_by
}
engagement only one to one engagement_status : has
engagement_visibility {
integer id PK
string visibility_name
string description
timestamp created_date
timestamp updated_date
}
engagement only one to one engagement_visibility : has
tenant {
integer id PK
string short_name
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Add visibility column to engagment
Revision ID: a3e6dae331ab
Revises: 9a93fda677eb
Create Date: 2024-05-28 08:26:11.155679
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = 'a3e6dae331ab'
down_revision = '9a93fda677eb'
branch_labels = None
depends_on = None

def upgrade():
# Create the engagement_visibility table
engagement_visiblity_table = op.create_table(
'engagement_visibility',
sa.Column('id', sa.Integer(), nullable=False, autoincrement=True),
sa.Column('visibility_name', sa.String(length=50), nullable=False),
sa.Column('description', sa.String(length=100), nullable=False),
sa.Column('created_date', sa.TIMESTAMP(timezone=False), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.Column('updated_date', sa.TIMESTAMP(timezone=False), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# Insert the initial data into the engagement_visibility table
op.bulk_insert(
engagement_visiblity_table,
[
{'id': 1, 'visibility_name': 'Public', 'description': 'Visible to all users'},
{'id': 2, 'visibility_name': 'Slug', 'description': 'Accessible to users with the direct link'},
{'id': 3, 'visibility_name': 'AuthToken', 'description': 'Visible to authenticated users'}
]
)
# Add the visibility column to the engagement table
op.add_column('engagement', sa.Column('visibility', sa.Integer(), nullable=False, server_default='1'))
op.create_foreign_key("engagement_visibility_fkey", 'engagement', 'engagement_visibility', ['visibility'], ['id'])
# Update the visibility column based on the is_internal column
op.execute("""
UPDATE engagement e
SET visibility = ev.id
FROM engagement_visibility ev
WHERE (e.is_internal AND ev.visibility_name = 'AuthToken')
OR (NOT e.is_internal AND ev.visibility_name = 'Public')
""")
op.drop_column('engagement', 'is_internal')


def downgrade():
# Add the is_internal column to the engagement table
op.add_column('engagement', sa.Column('is_internal', sa.BOOLEAN(), nullable=False, server_default='0'))
# Populate the is_internal column based on the visibility column
op.execute("""
UPDATE engagement e
SET is_internal = (e.visibility = (SELECT id FROM engagement_visibility WHERE visibility_name = 'AuthToken'))
""")
# Drop the foreign key constraint on the visibility column
op.drop_constraint("engagement_visibility_fkey", 'engagement', type_='foreignkey')
op.drop_column('engagement', 'visibility')
op.drop_table('engagement_visibility')
23 changes: 23 additions & 0 deletions met-api/src/met_api/constants/engagement_visibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright © 2024 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Constants of engagement visibility."""
from enum import IntEnum


class Visibility(IntEnum):
"""Enum of engagement visibility."""

Public = 1
Slug = 2
AuthToken = 3
23 changes: 13 additions & 10 deletions met-api/src/met_api/models/engagement.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from sqlalchemy.sql.schema import ForeignKey

from met_api.constants.engagement_status import EngagementDisplayStatus, Status
from met_api.constants.engagement_visibility import Visibility
from met_api.constants.user import SYSTEM_USER
from met_api.models.engagement_metadata import EngagementMetadataModel
from met_api.models.membership import Membership as MembershipModel
Expand All @@ -26,6 +27,7 @@
from .base_model import BaseModel
from .db import db
from .engagement_status import EngagementStatus
from .engagement_visibility import EngagementVisibility


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

@classmethod
def get_engagements_paginated(
Expand All @@ -58,7 +60,7 @@ def get_engagements_paginated(
search_options=None,
):
"""Get engagements paginated."""
query = db.session.query(Engagement).join(EngagementStatus)
query = db.session.query(Engagement).join(EngagementStatus).join(EngagementVisibility)

query = cls._add_tenant_filter(query)

Expand All @@ -73,7 +75,7 @@ def get_engagements_paginated(

query = cls._filter_by_project_metadata(query, search_options)

query = cls._filter_by_internal(query, search_options)
query = cls._filter_by_visibility(query, search_options)

if scope_options.restricted:
if scope_options.include_assigned:
Expand Down Expand Up @@ -122,7 +124,7 @@ def update_engagement(cls, engagement: EngagementSchema) -> Engagement:
banner_filename=engagement.get('banner_filename', None),
content=engagement.get('content', None),
rich_content=engagement.get('rich_content', None),
is_internal=engagement.get('is_internal', record.is_internal),
visibility=engagement.get('visibility', record.visibility)
)
query.update(update_fields)
db.session.commit()
Expand Down Expand Up @@ -184,10 +186,9 @@ def publish_scheduled_engagements_due(cls) -> List[Engagement]:
.filter(Engagement.status_id == Status.Scheduled.value) \
.filter(Engagement.scheduled_date <= datetime_due)
records = query.all()
if not records:
return None
query.update(update_fields)
db.session.commit()
if records:
query.update(update_fields)
db.session.commit()
return records

@staticmethod
Expand Down Expand Up @@ -253,10 +254,12 @@ def _filter_by_search_text(query, search_options):
return query

@staticmethod
def _filter_by_internal(query, search_options):
def _filter_by_visibility(query, search_options):
if exclude_internal := search_options.get('exclude_internal'):
if exclude_internal:
query = query.filter(Engagement.is_internal.is_(False))
query = query.filter(Engagement.visibility == Visibility.Public)
else:
query = query.filter(Engagement.visibility == Visibility.AuthToken)
return query

@staticmethod
Expand Down
27 changes: 27 additions & 0 deletions met-api/src/met_api/models/engagement_visibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Engagement Visibility model class.
Manages the engagement visibility
"""
from .base_model import BaseModel
from .db import db, ma


class EngagementVisibility(BaseModel): # pylint: disable=too-few-public-methods
"""Definition of the Engagement Visibility entity."""

__tablename__ = 'engagement_visibility'

id = db.Column(db.Integer, primary_key=True, autoincrement=True)
visibility_name = db.Column(db.String(50), nullable=False)
description = db.Column(db.String(50))
created_date = db.Column(db.DateTime, nullable=False, default=db.func.current_timestamp())
updated_date = db.Column(db.DateTime, nullable=True)


class EngagementVisibilitySchema(ma.Schema):
"""Engagement visibility schema."""

class Meta: # pylint: disable=too-few-public-methods
"""Meta class."""

fields = ('id', 'visibility_name', 'description', 'created_date', 'updated_date')
4 changes: 3 additions & 1 deletion met-api/src/met_api/schemas/engagement.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from met_api.utils.datetime import local_datetime

from .engagement_status import EngagementStatusSchema
from .engagement_visibility import EngagementVisibilitySchema


class EngagementSchema(Schema):
Expand Down Expand Up @@ -47,7 +48,8 @@ class Meta: # pylint: disable=too-few-public-methods
submissions_meta_data = fields.Method('get_submissions_meta_data')
status_block = fields.List(fields.Nested(EngagementStatusBlockSchema))
tenant_id = fields.Str(data_key='tenant_id')
is_internal = fields.Bool(data_key='is_internal')
visibility = fields.Int(data_key='visibility')
engagement_visibility = fields.Nested(EngagementVisibilitySchema)

def get_submissions_meta_data(self, obj):
"""Get the meta data of the submissions made in the survey."""
Expand Down
17 changes: 17 additions & 0 deletions met-api/src/met_api/schemas/engagement_visibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Engagement visibility schema class."""
from marshmallow import EXCLUDE, Schema, fields


class EngagementVisibilitySchema(Schema):
"""Schema for engagement visibility."""

class Meta: # pylint: disable=too-few-public-methods
"""Exclude unknown fields in the deserialized output."""

unknown = EXCLUDE

id = fields.Int(data_key='id')
visibility_name = fields.Str(data_key='visibility_name')
description = fields.Str(data_key='description')
created_date = fields.Str(data_key='created_date')
updated_date = fields.Str(data_key='updated_date')
3 changes: 2 additions & 1 deletion met-api/src/met_api/services/email_verification_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from flask import current_app
from met_api.constants.email_verification import INTERNAL_EMAIL_DOMAIN, EmailVerificationType
from met_api.constants.engagement_visibility import Visibility

from met_api.constants.subscription_type import SubscriptionTypes
from met_api.exceptions.business_exception import BusinessException
Expand Down Expand Up @@ -52,7 +53,7 @@ def create(cls, email_verification: EmailVerificationSchema,
survey = SurveyModel.find_by_id(email_verification.get('survey_id'))
engagement: EngagementModel = EngagementModel.find_by_id(
survey.engagement_id)
if engagement.is_internal and not email_address.endswith(INTERNAL_EMAIL_DOMAIN):
if engagement.visibility == Visibility.AuthToken and not email_address.endswith(INTERNAL_EMAIL_DOMAIN):
raise BusinessException(
error='Not an internal email address.',
status_code=HTTPStatus.INTERNAL_SERVER_ERROR)
Expand Down
13 changes: 8 additions & 5 deletions met-api/src/met_api/services/engagement_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from flask import current_app

from met_api.constants.engagement_status import Status
from met_api.constants.engagement_visibility import Visibility
from met_api.constants.membership_type import MembershipType
from met_api.exceptions.business_exception import BusinessException
from met_api.models.engagement import Engagement as EngagementModel
Expand Down Expand Up @@ -139,9 +140,11 @@ def publish_scheduled_engagements():
engagements = EngagementModel.publish_scheduled_engagements_due()
print('Engagements published: ', engagements)
for engagement in engagements:
email_util.publish_to_email_queue(SourceType.ENGAGEMENT.value, engagement.id,
SourceAction.PUBLISHED.value, True)
print('Engagements published added to email queue: ', engagement.id)
# Only add to email queue if engagement is public.
if engagement.visibility == Visibility.Public.value:
email_util.publish_to_email_queue(SourceType.ENGAGEMENT.value, engagement.id,
SourceAction.PUBLISHED.value, True)
print('Engagements published added to email queue: ', engagement.id)
return engagements

@staticmethod
Expand Down Expand Up @@ -178,7 +181,7 @@ def _create_engagement_model(engagement_data: dict) -> EngagementModel:
banner_filename=engagement_data.get('banner_filename', None),
content=engagement_data.get('content', None),
rich_content=engagement_data.get('rich_content', None),
is_internal=engagement_data.get('is_internal', False)
visibility=engagement_data.get('visibility', None)
)
new_engagement.save()
return new_engagement
Expand Down Expand Up @@ -220,7 +223,7 @@ def _save_or_update_eng_block(engagement_id, status_block):
@staticmethod
def _validate_engagement_edit_data(engagement_id: int, data: dict):
engagement = EngagementModel.find_by_id(engagement_id)
draft_status_restricted_changes = (EngagementModel.is_internal.key,)
draft_status_restricted_changes = (EngagementModel.visibility.key,)
engagement_has_been_opened = engagement.status_id != Status.Draft.value
if engagement_has_been_opened and any(field in data for field in draft_status_restricted_changes):
raise ValueError('Some fields cannot be updated after the engagement has been published')
Expand Down
2 changes: 1 addition & 1 deletion met-api/src/met_api/services/engagement_slug_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def update_engagement_slug(cls, slug: str, engagement_id: int) -> EngagementSlug

# publish changes to EPIC
ProjectService.update_project_info(engagement_id)

return {
'slug': engagement_slug.slug,
'engagement_id': engagement_slug.engagement_id,
Expand Down
15 changes: 15 additions & 0 deletions met-api/tests/unit/api/test_engagement.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from flask import current_app

from met_api.constants.engagement_status import EngagementDisplayStatus, SubmissionStatus
from met_api.constants.engagement_visibility import Visibility
from met_api.models.tenant import Tenant as TenantModel
from met_api.utils.constants import TENANT_ID_HEADER
from met_api.utils.enums import ContentType
Expand Down Expand Up @@ -277,6 +278,20 @@ def test_search_engagements_not_logged_in(client, session): # pylint:disable=un
assert rv.status_code == 200


def test_search_hidden_engagements_not_logged_in(client, session): # pylint:disable=unused-argument
"""Assert that an engagement cannot be searched if it is hidden."""
factory_engagement_model(visibility=Visibility.Slug)

rv = client.get('/api/engagements/', content_type=ContentType.JSON.value)
assert rv.json.get('total') == 0, 'it is not visible for public user'
assert rv.status_code == 200

factory_engagement_model(TestEngagementInfo.engagement3)
rv = client.get('/api/engagements/', content_type=ContentType.JSON.value)
assert rv.json.get('total') == 1, 'Only the public engagaments should be visible for public user'
assert rv.status_code == 200


@pytest.mark.parametrize('engagement_info', [TestEngagementInfo.engagement1])
def test_patch_engagement(client, jwt, session, engagement_info): # pylint:disable=unused-argument
"""Assert that an engagement can be updated."""
Expand Down
9 changes: 5 additions & 4 deletions met-api/tests/utilities/factory_scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from met_api.constants.comment_status import Status as CommentStatus
from met_api.constants.engagement_status import Status as EngagementStatus
from met_api.constants.engagement_status import SubmissionStatus
from met_api.constants.engagement_visibility import Visibility
from met_api.constants.feedback import CommentType, FeedbackSourceType, FeedbackStatusType, RatingType
from met_api.constants.widget import WidgetType
from met_api.utils.enums import LoginSource, UserStatus
Expand Down Expand Up @@ -163,7 +164,7 @@ class TestEngagementInfo(dict, Enum):
'created_by': '123',
'updated_by': '123',
'status': EngagementStatus.Published.value,
'is_internal': False,
'visibility': Visibility.Public.value,
'description': 'My Test Engagement Description',
'rich_description': '"{\"blocks\":[{\"key\":\"2ku94\",\"text\":\"Rich Description Sample\",\"type\":\"unstyled\",\
\"depth\":0,\"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}}],\"entityMap\":{}}"',
Expand All @@ -180,7 +181,7 @@ class TestEngagementInfo(dict, Enum):
'created_by': '123',
'updated_by': '123',
'status': EngagementStatus.Draft.value,
'is_internal': False,
'visibility': Visibility.Public.value,
'description': 'My Test Engagement Description',
'rich_description': '"{\"blocks\":[{\"key\":\"2ku94\",\"text\":\"Rich Description Sample\",\"type\":\"unstyled\",\
\"depth\":0,\"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}}],\"entityMap\":{}}"',
Expand All @@ -197,7 +198,7 @@ class TestEngagementInfo(dict, Enum):
'created_by': '123',
'updated_by': '123',
'status': SubmissionStatus.Open.value,
'is_internal': False,
'visibility': Visibility.Public.value,
'description': 'My Test Engagement Description',
'rich_description': '"{\"blocks\":[{\"key\":\"2ku94\",\"text\":\"Rich Description Sample\",\"type\":\"unstyled\",\
\"depth\":0,\"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}}],\"entityMap\":{}}"',
Expand All @@ -221,7 +222,7 @@ class TestEngagementInfo(dict, Enum):
'created_by': '123',
'updated_by': '123',
'status': SubmissionStatus.Open.value,
'is_internal': False,
'visibility': Visibility.Public.value,
'description': 'My Test Engagement Description',
'rich_description': '"{\"blocks\":[{\"key\":\"2ku94\",\"text\":\"Rich Description Sample\",\"type\":\"unstyled\",\
\"depth\":0,\"inlineStyleRanges\":[],\"entityRanges\":[],\"data\":{}}],\"entityMap\":{}}"',
Expand Down
Loading

0 comments on commit af87b24

Please sign in to comment.