-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2586 from bcgov/feature/DESENG-689-add-image-widget
DESENG-689: Add image widget
- Loading branch information
Showing
30 changed files
with
1,121 additions
and
149 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
met-api/migrations/versions/e706db763790_add_new_image_type_to_widget_type_table.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
"""Add new Image type to widget type table | ||
Revision ID: e706db763790 | ||
Revises: 42641011576a | ||
Create Date: 2024-09-04 14:03:57.967946 | ||
""" | ||
|
||
from datetime import datetime, UTC | ||
from alembic import op | ||
import sqlalchemy as sa | ||
from sqlalchemy.sql import table, column | ||
from sqlalchemy import String, Integer, DateTime | ||
|
||
|
||
# revision identifiers, used by Alembic. | ||
revision = "e706db763790" | ||
down_revision = "42641011576a" | ||
branch_labels = None | ||
depends_on = None | ||
|
||
|
||
def upgrade(): | ||
# Temporary table model for existing widget_type table | ||
widget_type_table = table( | ||
"widget_type", | ||
column("id", Integer), | ||
column("name", String), | ||
column("description", String), | ||
column("created_date", DateTime), | ||
column("updated_date", DateTime), | ||
column("created_by", String), | ||
column("updated_by", String), | ||
) | ||
# Insert new widget type | ||
op.bulk_insert( | ||
widget_type_table, | ||
[ | ||
{ | ||
"id": 11, | ||
"name": "Image", | ||
"description": "Displays a static image, with optional caption", | ||
"created_by": "migration", | ||
"updated_by": "migration", | ||
"created_date": datetime.now(UTC), | ||
"updated_date": datetime.now(UTC), | ||
} | ||
], | ||
) | ||
op.create_table( | ||
"widget_image", | ||
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("image_url", sa.String(length=255), nullable=False), | ||
sa.Column("alt_text", sa.String(length=255), 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"), | ||
) | ||
|
||
|
||
def downgrade(): | ||
# ### commands auto generated by Alembic - please adjust! ### | ||
op.drop_table("widget_image") | ||
op.execute("DELETE FROM widget_type WHERE id = 11") | ||
# ### end Alembic commands ### |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,3 +26,4 @@ class WidgetType(IntEnum): | |
Video = 7 | ||
Timeline = 9 | ||
Poll = 10 | ||
Image = 11 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
"""WidgetImage model class. | ||
Manages the image widget | ||
""" | ||
|
||
from __future__ import annotations | ||
|
||
from sqlalchemy.sql.schema import ForeignKey | ||
|
||
from .base_model import BaseModel | ||
from .db import db | ||
|
||
|
||
class WidgetImage( | ||
BaseModel | ||
): # pylint: disable=too-few-public-methods, too-many-instance-attributes | ||
"""Definition of the Image entity.""" | ||
|
||
__tablename__ = 'widget_image' | ||
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 | ||
) | ||
image_url = db.Column(db.String(255), nullable=False) | ||
alt_text = db.Column(db.String(255)) | ||
description = db.Column(db.Text()) | ||
|
||
@classmethod | ||
def get_image(cls, widget_id) -> list[WidgetImage]: | ||
"""Get an image by widget_id.""" | ||
return WidgetImage.query.filter(WidgetImage.widget_id == widget_id).all() | ||
|
||
@classmethod | ||
def update_image(cls, widget_id, widget_data) -> WidgetImage: | ||
"""Update an image by widget_id.""" | ||
image = WidgetImage.get_image(widget_id)[0] | ||
for key, value in widget_data.items(): | ||
setattr(image, key, value) | ||
image.save() | ||
return image |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
# 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 image widget resource.""" | ||
from http import HTTPStatus | ||
|
||
from flask import request | ||
from flask_cors import cross_origin | ||
from flask_restx import Namespace, Resource, fields | ||
|
||
from met_api.auth import jwt as _jwt | ||
from met_api.exceptions.business_exception import BusinessException | ||
from met_api.schemas.widget_image import WidgetImageSchema | ||
from met_api.services.widget_image_service import WidgetImageService | ||
from met_api.utils.util import allowedorigins, cors_preflight | ||
|
||
|
||
API = Namespace('widget_images', description='Endpoints for Image Widget Management') | ||
|
||
# Do not allow updating the widget_id or engagement_id via API calls | ||
|
||
image_creation_model = API.model( | ||
'ImageCreation', | ||
{ | ||
'image_url': fields.String(description='The URL of the image', required=True), | ||
'alt_text': fields.String(description='The alt text for the image'), | ||
'description': fields.String(description='The description of the image'), | ||
}, | ||
) | ||
|
||
image_update_model = API.model( | ||
'ImageUpdate', | ||
{ | ||
'image_url': fields.String(description='The URL of the image'), | ||
'alt_text': fields.String(description='The alt text for the image'), | ||
'description': fields.String(description='The description of the image'), | ||
}, | ||
) | ||
|
||
|
||
@cors_preflight('GET, POST, PATCH, OPTIONS') | ||
@API.route('') | ||
class Images(Resource): | ||
"""Resource for managing image widgets.""" | ||
|
||
@staticmethod | ||
@cross_origin(origins=allowedorigins()) | ||
def get(widget_id): | ||
"""Get image widget.""" | ||
try: | ||
widget_image = WidgetImageService().get_image(widget_id) | ||
return ( | ||
WidgetImageSchema().dump(widget_image, many=True), | ||
HTTPStatus.OK, | ||
) | ||
except BusinessException as err: | ||
return str(err), err.status_code | ||
|
||
@staticmethod | ||
@cross_origin(origins=allowedorigins()) | ||
@_jwt.requires_auth | ||
@API.expect(image_creation_model, validate=True) | ||
def post(widget_id): | ||
"""Create image widget.""" | ||
try: | ||
request_json = request.get_json() | ||
widget_image = WidgetImageService().create_image(widget_id, request_json) | ||
return WidgetImageSchema().dump(widget_image), HTTPStatus.OK | ||
except BusinessException as err: | ||
return str(err), err.status_code | ||
|
||
|
||
@cors_preflight('PATCH') | ||
@API.route('/<int:image_widget_id>') | ||
class Image(Resource): | ||
"""Resource for managing specific image widget instances by ID.""" | ||
|
||
@staticmethod | ||
@cross_origin(origins=allowedorigins()) | ||
@_jwt.requires_auth | ||
@API.expect(image_update_model, validate=True) | ||
def patch(widget_id, image_widget_id): | ||
"""Update image widget.""" | ||
request_json = request.get_json() | ||
try: | ||
WidgetImageSchema().load(request_json) | ||
widget_image = WidgetImageService().update_image( | ||
widget_id, image_widget_id, request_json | ||
) | ||
return WidgetImageSchema().dump(widget_image), HTTPStatus.OK | ||
except BusinessException as err: | ||
return str(err), err.status_code |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# 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. | ||
"""Widget image schema definition.""" | ||
|
||
from met_api.models.widget_image import WidgetImage as WidgetImageModel | ||
|
||
from marshmallow import Schema | ||
|
||
|
||
class WidgetImageSchema(Schema): | ||
"""This is the schema for the image model.""" | ||
|
||
class Meta: # pylint: disable=too-few-public-methods | ||
"""Images all of the Widget Image fields to a default schema.""" | ||
|
||
model = WidgetImageModel | ||
fields = ( | ||
'id', | ||
'widget_id', | ||
'engagement_id', | ||
'image_url', | ||
'alt_text', | ||
'description', | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
"""Service for Widget Image management.""" | ||
|
||
from met_api.constants.membership_type import MembershipType | ||
from met_api.models.widget_image import WidgetImage as WidgetImageModel | ||
from met_api.services import authorization | ||
from met_api.utils.roles import Role | ||
|
||
|
||
class WidgetImageService: | ||
"""Widget image management service.""" | ||
|
||
@staticmethod | ||
def get_image(widget_id): | ||
"""Get image by widget id.""" | ||
widget_image = WidgetImageModel.get_image(widget_id) | ||
return widget_image | ||
|
||
@staticmethod | ||
def create_image(widget_id, image_details: dict): | ||
"""Create image for the widget.""" | ||
image_data = dict(image_details) | ||
eng_id = image_data.get('engagement_id') | ||
authorization.check_auth( | ||
one_of_roles=(MembershipType.TEAM_MEMBER.name, Role.EDIT_ENGAGEMENT.value), | ||
engagement_id=eng_id, | ||
) | ||
|
||
widget_image = WidgetImageService._create_image_model(widget_id, image_data) | ||
widget_image.commit() | ||
return widget_image | ||
|
||
@staticmethod | ||
def update_image(widget_id, image_widget_id, image_data): | ||
"""Update image widget.""" | ||
widget_image: WidgetImageModel = WidgetImageModel.find_by_id(image_widget_id) | ||
authorization.check_auth( | ||
one_of_roles=(MembershipType.TEAM_MEMBER.name, Role.EDIT_ENGAGEMENT.value), | ||
engagement_id=widget_image.engagement_id, | ||
) | ||
|
||
if not widget_image: | ||
raise KeyError('image widget not found') | ||
|
||
if widget_image.widget_id != widget_id: | ||
raise ValueError('Invalid widgets and image') | ||
|
||
return WidgetImageModel.update_image(widget_id, image_data) | ||
|
||
@staticmethod | ||
def _create_image_model(widget_id, image_data: dict): | ||
image_model: WidgetImageModel = WidgetImageModel() | ||
image_model.widget_id = widget_id | ||
image_model.engagement_id = image_data.get('engagement_id') | ||
image_model.image_url = image_data.get('image_url') | ||
image_model.description = image_data.get('description') | ||
image_model.alt_text = image_data.get('alt_text') | ||
image_model.flush() | ||
return image_model |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.