Skip to content

Commit

Permalink
Feature/deseng695: Updated Who is Listening Widget, changed colour of…
Browse files Browse the repository at this point in the history
… Video Widget overlay logo, fixed media query for Map Widget expand link. (#2598)

* feature/deseng695: Implemented Who Is Listening widget design, changed icon color for video widget, adjusted viewport for expand map link in map widget.

* feature/deseng695: Updated contact create/edit form, minor styling update to image picker.

* feature/deseng695: Made changes as per Alex, updated Who is Listening form for better legibility.

* feature/deseng695: Removed blank labels from contact form.
  • Loading branch information
jareth-whitney authored Oct 3, 2024
1 parent 0b61f80 commit bb89f8d
Show file tree
Hide file tree
Showing 30 changed files with 913 additions and 125 deletions.
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))


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):
"""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

0 comments on commit bb89f8d

Please sign in to comment.