Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/deseng695: Updated Who is Listening Widget, changed colour of Video Widget overlay logo, fixed media query for Map Widget expand link. #2598

Merged
merged 5 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
## October 2, 2024

- **Feature** New Who is Listening widget front end [🎟️ DESENG-695](https://citz-gdx.atlassian.net/browse/DESENG-695)
- Implemented Figma design
- Added widget_listening table to db for widget instance data
- Added option to enter widget description text (below title, above contacts)
- Adjusted CSS to accomodate multiple viewports
- Added ARIA labels for accessibility
- Updated contact create/edit form
- Updated Who is Listening widget form

- **Feature** New Video Widget front end [🎟️ DESENG-692](https://citz-gdx.atlassian.net/browse/DESENG-692)
- Changed icon colour from yellow to white to accomodate company branding requests

- **Feature** New Map Widget front end [🎟️ DESENG-693](https://citz-gdx.atlassian.net/browse/DESENG-693)
- Adjusted viewport settings of expand map link to accomodate breakpoints of engagement view page

## September 26, 2024

- **Feature** New Timeline Widget designs [🎟️ DESENG-694](https://citz-gdx.atlassian.net/browse/DESENG-694)
Expand All @@ -7,7 +24,6 @@
## September 25, 2024

- **Feature** New Video Widget front end [🎟️ DESENG-692](https://citz-gdx.atlassian.net/browse/DESENG-692)

- Removed unneeded tables from db
- Updated all other met_api and met_web logic to accomodate this

Expand Down
11 changes: 11 additions & 0 deletions docs/MET_database_ERD.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,17 @@ erDiagram
string updated_by
boolean is_uploaded
}
widget only one to zero or more widget_documents : has
widget_listening {
integer id PK
integer widget_id FK "The id from widget"
integer engagement_id FK "The id from engagement"
string description
timestamp created_date
timestamp updated_date
string created_by
string updated_by
}
widget only one to zero or more widget_documents : has
user_status {
integer id PK
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Added widget_listening table for storing values particular to the listening widget instance, but not related to contacts.

Revision ID: 917a911b6ca9
Revises: 70a410c85b24
Create Date: 2024-10-01 15:34:13.253066

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = '917a911b6ca9'
down_revision = '70a410c85b24'
branch_labels = None
depends_on = None


def upgrade():
op.create_table('widget_listening',
sa.Column('created_date', sa.DateTime(), nullable=False),
sa.Column('updated_date', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('widget_id', sa.Integer(), nullable=True),
sa.Column('engagement_id', sa.Integer(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('created_by', sa.String(length=50), nullable=True),
sa.Column('updated_by', sa.String(length=50), nullable=True),
sa.ForeignKeyConstraint(['engagement_id'], ['engagement.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['widget_id'], ['widget.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.add_column('widget_translation', sa.Column('listening_description', sa.Text(), nullable=True))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for remembering the translation for it!



def downgrade():
op.drop_column('widget_translation', 'listening_description')
op.drop_table('widget_listening')
1 change: 1 addition & 0 deletions met-api/src/met_api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
from .engagement_slug import EngagementSlug
from .report_setting import ReportSetting
from .widget_video import WidgetVideo
from .widget_listening import WidgetListening
from .widget_image import WidgetImage
from .cac_form import CACForm
from .engagement_metadata import EngagementMetadata, MetadataTaxon
Expand Down
38 changes: 38 additions & 0 deletions met-api/src/met_api/models/widget_listening.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""WidgetListening model class.

Manages the who is listening widget
"""
from __future__ import annotations

from sqlalchemy.sql.schema import ForeignKey

from .base_model import BaseModel
from .db import db


class WidgetListening(BaseModel): # pylint: disable=too-few-public-methods, too-many-instance-attributes
"""Definition of the who is listening widget."""

__tablename__ = 'widget_listening'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
widget_id = db.Column(db.Integer, ForeignKey('widget.id', ondelete='CASCADE'), nullable=True)
engagement_id = db.Column(db.Integer, ForeignKey('engagement.id', ondelete='CASCADE'), nullable=True)
description = db.Column(db.Text())

@classmethod
def get_listening(cls, widget_id) -> list[WidgetListening]:
"""Get who is listening widget."""
widget_listening = db.session.query(WidgetListening) \
.filter(WidgetListening.widget_id == widget_id) \
.all()
return widget_listening

@classmethod
def update_listening(cls, listening_widget_id, listening_data: dict) -> WidgetListening:
"""Update who is listening widget."""
widget_listening: WidgetListening = WidgetListening.query.get(listening_widget_id)
if widget_listening:
for key, value in listening_data.items():
setattr(widget_listening, key, value)
widget_listening.save()
return widget_listening
2 changes: 2 additions & 0 deletions met-api/src/met_api/models/widget_translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class WidgetTranslation(BaseModel): # pylint: disable=too-few-public-methods
poll_description = db.Column(db.String(2048))
video_url = db.Column(db.String(255))
video_description = db.Column(db.Text())
listening_description = db.Column(db.Text())

@classmethod
def get_translation_by_widget_id_and_language_id(cls, widget_id=None, language_id=None):
Expand Down Expand Up @@ -63,6 +64,7 @@ def __create_new_widget_translation_entity(translation):
poll_description=translation.get('poll_description', None),
video_url=translation.get('video_url', None),
video_description=translation.get('video_description', None),
listening_description=translation.get('listening_description', None),
)

@classmethod
Expand Down
2 changes: 2 additions & 0 deletions met-api/src/met_api/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
from .widget_timeline import API as WIDGET_TIMELINE_API
from .widget_poll import API as WIDGET_POLL_API
from .widget_image import API as WIDGET_IMAGE_API
from .widget_listening import API as WIDGET_LISTENING_API
from .language import API as LANGUAGE_API
from .widget_translation import API as WIDGET_TRANSLATION_API
from .survey_translation import API as SURVEY_TRANSLATION_API
Expand Down Expand Up @@ -101,6 +102,7 @@
API.add_namespace(ENGAGEMENT_SLUG_API, path='/slugs')
API.add_namespace(REPORT_SETTING_API, path='/surveys/<int:survey_id>/reportsettings')
API.add_namespace(WIDGET_VIDEO_API, path='/widgets/<int:widget_id>/videos')
API.add_namespace(WIDGET_LISTENING_API, path='/widgets/<int:widget_id>/listening_widgets')
API.add_namespace(ENGAGEMENT_SETTINGS_API)
API.add_namespace(CAC_FORM_API, path='/engagements/<int:engagement_id>/cacform')
API.add_namespace(WIDGET_TIMELINE_API, path='/widgets/<int:widget_id>/timelines')
Expand Down
79 changes: 79 additions & 0 deletions met-api/src/met_api/resources/widget_listening.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Copyright © 2021 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.
"""API endpoints for managing a listening widget resource."""
from http import HTTPStatus

from flask import jsonify, request
from flask_cors import cross_origin
from flask_restx import Namespace, Resource

from met_api.auth import jwt as _jwt
from met_api.exceptions.business_exception import BusinessException
from met_api.schemas import utils as schema_utils
from met_api.schemas.widget_listening import WidgetListeningSchema
from met_api.services.widget_listening_service import WidgetListeningService
from met_api.utils.util import allowedorigins, cors_preflight


API = Namespace('widget_listenings', description='Endpoints for Who Is Listening Widget Management')
"""Widget Listenings"""


@cors_preflight('GET, POST')
@API.route('')
class Listenings(Resource):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😂 the name

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, it reminds me of a horror movie. "The Listening".

"""Resource for managing Who is Listening widgets."""

@staticmethod
@cross_origin(origins=allowedorigins())
def get(widget_id):
"""Get Who is Listening widget."""
try:
widget_listening = WidgetListeningService().get_listening(widget_id)
return jsonify(WidgetListeningSchema().dump(widget_listening, many=True)), HTTPStatus.OK
except BusinessException as err:
return str(err), err.status_code

@staticmethod
@cross_origin(origins=allowedorigins())
@_jwt.requires_auth
def post(widget_id):
"""Create Who is Listening widget."""
try:
request_json = request.get_json()
widget_listening = WidgetListeningService().create_listening(widget_id, request_json)
return WidgetListeningSchema().dump(widget_listening), HTTPStatus.OK
except BusinessException as err:
return str(err), err.status_code


@cors_preflight('PATCH')
@API.route('/<int:listening_widget_id>')
class Listening(Resource):
"""Resource for managing Who is Listening widgets."""

@staticmethod
@cross_origin(origins=allowedorigins())
@_jwt.requires_auth
def patch(widget_id, listening_widget_id):
"""Update Who is Listening widget."""
request_json = request.get_json()
valid_format, errors = schema_utils.validate(request_json, 'listening_widget_update')
if not valid_format:
return {'message': schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST
try:
widget_listening = WidgetListeningService().update_listening(widget_id, listening_widget_id, request_json)
return WidgetListeningSchema().dump(widget_listening), HTTPStatus.OK
except BusinessException as err:
return str(err), err.status_code
23 changes: 23 additions & 0 deletions met-api/src/met_api/schemas/schemas/listening_widget_update.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://met.gov.bc.ca/.well_known/schemas/listening_widget_update",
"type": "object",
"title": "The root schema",
"description": "The root schema comprises the entire JSON document.",
"default": {},
"examples": [
{
"description": "A Who is Listening widget description"
}
],
"required": [],
"properties": {
"description": {
"$id": "#/properties/description",
"type": "string",
"title": "Who is Listening description",
"description": "The description of this Who is Listening widget.",
"examples": ["A Who is Listening widget description"]
}
}
}
28 changes: 28 additions & 0 deletions met-api/src/met_api/schemas/widget_listening.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright © 2019 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.
"""Manager for widget listening schema."""

from met_api.models.widget_listening import WidgetListening as WidgetListeningModel

from marshmallow import Schema
from marshmallow_sqlalchemy.fields import Nested

class WidgetListeningSchema(Schema): # pylint: disable=too-many-ancestors, too-few-public-methods
"""This is the schema for the widget listening model."""

class Meta: # pylint: disable=too-few-public-methods
"""All of the fields in the Widget Listening schema."""

model = WidgetListeningModel
fields = ('id', 'engagement_id', 'widget_id', 'description')
1 change: 1 addition & 0 deletions met-api/src/met_api/schemas/widget_translation.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ class Meta: # pylint: disable=too-few-public-methods
poll_description = fields.Str(data_key='poll_description')
video_title = fields.Str(data_key='video_title')
video_description = fields.Str(data_key='video_description')
listening_description = fields.Str(data_key='listening_description')
84 changes: 84 additions & 0 deletions met-api/src/met_api/services/widget_listening_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Service for Widget Listening management."""
from http import HTTPStatus
from typing import Optional

from met_api.constants.membership_type import MembershipType
from met_api.exceptions.business_exception import BusinessException
from met_api.models.widget_listening import WidgetListening as WidgetListeningModel
from met_api.services import authorization
from met_api.utils.roles import Role


class WidgetListeningService:
"""Widget Listening management service."""

@staticmethod
def get_listening_by_id(listening_id: int):
"""Get listening by id."""
widget_listening = WidgetListeningModel.find_by_id(listening_id)
return widget_listening

@staticmethod
def get_listening(widget_id: int):
"""Get listening by widget id."""
widget_listening = WidgetListeningModel.get_listening(widget_id)
return widget_listening

@staticmethod
def create_listening(widget_id: int, listening_details: dict):
"""Create listening for the widget."""
listening_data = dict(listening_details)
eng_id = listening_data.get('engagement_id')
authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name,
Role.EDIT_ENGAGEMENT.value), engagement_id=eng_id)

widget_listening = WidgetListeningService._create_listening_model(widget_id, listening_data)
widget_listening.commit()
return widget_listening

@staticmethod
def update_listening(widget_id: int, listening_id: int, listening_data: dict) -> Optional[WidgetListeningModel]:
"""Update listening widget."""
engagement_id = listening_data.get('engagement_id')

WidgetListeningService._check_update_listening_auth(engagement_id)

widget_listening: WidgetListeningModel = WidgetListeningModel.find_by_id(listening_id)

if not widget_listening:
raise BusinessException(
error='Who is Listening widget not found',
status_code=HTTPStatus.BAD_REQUEST)

if widget_listening.widget_id != widget_id:
raise BusinessException(
error='Invalid widget ID',
status_code=HTTPStatus.BAD_REQUEST)

if widget_listening.id != listening_id:
raise BusinessException(
error='Invalid Who is Listening widget ID',
status_code=HTTPStatus.BAD_REQUEST)

WidgetListeningService._update_widget_listening(widget_listening, listening_data)

return widget_listening

@staticmethod
def _check_update_listening_auth(engagement_id):
authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name,
Role.EDIT_ENGAGEMENT.value), engagement_id=engagement_id)

@staticmethod
def _update_widget_listening(widget_listening: WidgetListeningModel, listening_data: dict):
widget_listening.description = listening_data.get('description')
widget_listening.save()

@staticmethod
def _create_listening_model(widget_id: int, listening_data: dict):
listening_model: WidgetListeningModel = WidgetListeningModel()
listening_model.widget_id = widget_id
listening_model.engagement_id = listening_data.get('engagement_id')
listening_model.description = listening_data.get('description')
listening_model.flush()
return listening_model
Loading
Loading