Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
VineetBala-AOT authored Jan 10, 2024
2 parents f934749 + f2a1f8e commit 80fbb37
Show file tree
Hide file tree
Showing 30 changed files with 1,418 additions and 2 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Change Log

All notable changes to this project will be documented in this file.
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/).

## December 28, 2023

> **Feature**: Added the timeline widget. [🎟️DESENG-439](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-439)
## January 9, 2024

Expand Down
27 changes: 27 additions & 0 deletions docs/MET_database_ERD.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,33 @@ erDiagram
string updated_by
}
widget only one to zero or more widget_video : has
widget_timeline {
integer id PK
integer widget_id FK "The id from widget"
integer engagement_id FK "The id from engagement"
string title
string description
timestamp created_date
timestamp updated_date
string created_by
string updated_by
}
widget only one to zero or more widget_timeline : has
timeline_event {
integer id PK
integer widget_id FK "The id from widget"
integer engagement_id FK "The id from engagement"
integer timeline_id FK "The id from timeline"
string description
string time
enum status
integer position
timestamp created_date
timestamp updated_date
string created_by
string updated_by
}
widget only one to zero or more timeline_event : has
widget_documents {
integer id PK
string title
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Added the Timeline Widget.
Revision ID: 3e4dc76a96ab
Revises: 02ff8ecc6b91
Create Date: 2023-12-05 17:04:46.304368
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from met_api.constants.timeline_event_status import TimelineEventStatus

# revision identifiers, used by Alembic.
revision = '3e4dc76a96ab'
down_revision = '02ff8ecc6b91'
branch_labels = None
depends_on = None


def upgrade():
op.create_table('widget_timeline',
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True, nullable=False),
sa.Column('engagement_id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=255), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['engagement_id'], ['engagement.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
)
op.create_table('timeline_event',
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True, nullable=False),
sa.Column('engagement_id', sa.Integer(), nullable=False),
sa.Column('timeline_id', sa.Integer(), nullable=False),
sa.Column('status', sa.Enum(TimelineEventStatus), nullable=True),
sa.Column('position', sa.Integer(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('time', sa.String(length=255), nullable=True),
sa.ForeignKeyConstraint(['engagement_id'], ['engagement.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['timeline_id'], ['widget_timeline.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
)

def downgrade():
op.drop_table('widget_timeline')
op.drop_table('timeline_event')
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Add widget_id to widget_timeline and timeline_event tables
Revision ID: 4114001e1a4c
Revises: c09e77fde608
Create Date: 2023-12-11 15:46:30.773046
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = '4114001e1a4c'
down_revision = 'c09e77fde608'
branch_labels = None
depends_on = None


def upgrade():
op.add_column('widget_timeline', sa.Column('widget_id', sa.Integer()))
op.create_foreign_key('timeline_widget_fk', 'widget_timeline', 'widget', ['widget_id'], ['id'], ondelete='CASCADE')
op.add_column('timeline_event', sa.Column('widget_id', sa.Integer()))
op.create_foreign_key('event_widget_fk', 'timeline_event', 'widget', ['widget_id'], ['id'], ondelete='CASCADE')

def downgrade():
op.drop_column('widget_timeline', 'widget_id')
op.drop_column('timeline_event', 'widget_id')
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Added enum value for Timeline Widget.
Revision ID: c09e77fde608
Revises: 3e4dc76a96ab
Create Date: 2023-12-06 11:46:20.934373
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = 'c09e77fde608'
down_revision = '3e4dc76a96ab'
branch_labels = None
depends_on = None


def upgrade():
op.add_column('timeline_event', sa.Column('created_date', sa.DateTime(), nullable=False))
op.add_column('timeline_event', sa.Column('updated_date', sa.DateTime(), nullable=True))
op.add_column('timeline_event', sa.Column('created_by', sa.String(length=50), nullable=True))
op.add_column('timeline_event', sa.Column('updated_by', sa.String(length=50), nullable=True))

op.add_column('widget_timeline', sa.Column('created_date', sa.DateTime(), nullable=False))
op.add_column('widget_timeline', sa.Column('updated_date', sa.DateTime(), nullable=True))
op.add_column('widget_timeline', sa.Column('created_by', sa.String(length=50), nullable=True))
op.add_column('widget_timeline', sa.Column('updated_by', sa.String(length=50), nullable=True))

widget_type_table = sa.table('widget_type',
sa.Column('id', sa.Integer),
sa.Column('name', sa.String),
sa.Column('description', sa.String))

op.bulk_insert(
widget_type_table,
[
{
'id': 8,
'name': 'CAC Form',
'description': 'Add a CAC Form to your project',
},
{
'id': 9,
'name': 'Timeline',
'description': 'Create a timeline for a series of events',
},
]
)

def downgrade():
op.drop_column('widget_timeline', 'updated_by')
op.drop_column('widget_timeline', 'created_by')
op.drop_column('widget_timeline', 'updated_date')
op.drop_column('widget_timeline', 'created_date')
op.drop_column('timeline_event', 'updated_by')
op.drop_column('timeline_event', 'created_by')
op.drop_column('timeline_event', 'updated_date')
op.drop_column('timeline_event', 'created_date')
op.delete(widget_type_table).filter_by(id=8)
op.delete(widget_type_table).filter_by(id=9)
10 changes: 10 additions & 0 deletions met-api/src/met_api/constants/timeline_event_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Constants of timeline events."""
from enum import IntEnum


class TimelineEventStatus(IntEnum):
"""Enum of timeline event status status."""

Pending = 1
InProgress = 2
Completed = 3
2 changes: 2 additions & 0 deletions met-api/src/met_api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@
from .report_setting import ReportSetting
from .widget_video import WidgetVideo
from .cac_form import CACForm
from .widget_timeline import WidgetTimeline
from .timeline_event import TimelineEvent
51 changes: 51 additions & 0 deletions met-api/src/met_api/models/timeline_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Timeline Event model class.
Manages the timeline events
"""
from __future__ import annotations

from sqlalchemy.sql.schema import ForeignKey
from met_api.constants.timeline_event_status import TimelineEventStatus

from .base_model import BaseModel
from .db import db


class TimelineEvent(BaseModel):
"""Definition of the TimelineEvent entity."""

__tablename__ = 'timeline_event'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
engagement_id = db.Column(db.Integer, ForeignKey('engagement.id', ondelete='CASCADE'), nullable=True)
widget_id = db.Column(db.Integer, ForeignKey('widget.id', ondelete='CASCADE'), nullable=True)
timeline_id = db.Column(db.Integer, ForeignKey('widget_timeline.id', ondelete='CASCADE'), nullable=True)
status = db.Column(db.Enum(TimelineEventStatus), nullable=False)
position = db.Column(db.Integer, nullable=False)
description = db.Column(db.Text(), nullable=True)
time = db.Column(db.String(255), nullable=True)

@classmethod
def delete_event(cls, timeline_id):
"""Delete timeline."""
timeline_event = db.session.query(TimelineEvent) \
.filter(TimelineEvent.timeline_id == timeline_id)
timeline_event.delete()
db.session.commit()

@classmethod
def get_timeline_events(cls, timeline_id) -> list[TimelineEvent]:
"""Get timeline event."""
timeline_event = db.session.query(TimelineEvent) \
.filter(TimelineEvent.timeline_id == timeline_id) \
.all()
return timeline_event

@classmethod
def update_timeline_event(cls, timeline_id, event_data: dict) -> TimelineEvent:
"""Update timeline event."""
timeline_event: TimelineEvent = TimelineEvent.query.get(timeline_id)
if timeline_event:
for key, value in event_data.items():
setattr(timeline_event, key, value)
timeline_event.save()
return timeline_event
45 changes: 45 additions & 0 deletions met-api/src/met_api/models/widget_timeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""WidgetTimeline model class.
Manages the timeline widget
"""
from __future__ import annotations
from typing import Optional
from sqlalchemy.sql.schema import ForeignKey
from met_api.models.timeline_event import TimelineEvent
from met_api.services.timeline_event_service import TimelineEventService
from .base_model import BaseModel
from .db import db

class WidgetTimeline(BaseModel): # pylint: disable=too-few-public-methods, too-many-instance-attributes
"""Definition of the Timeline entity."""

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

# Relationship to timeline_event
events = db.relationship(TimelineEvent, backref='widget_timeline', lazy=True)

@classmethod
def get_timeline(cls, timeline_id) -> list[WidgetTimeline]:
"""Get timeline."""
widget_timeline = db.session.query(WidgetTimeline) \
.filter(WidgetTimeline.widget_id == timeline_id) \
.all()
return widget_timeline

@classmethod
def update_timeline(cls, timeline_id, timeline_data: dict) -> Optional[WidgetTimeline or None]:
"""Update timeline."""
TimelineEvent.delete_event(timeline_id)
widget_timeline: WidgetTimeline = WidgetTimeline.query.get(timeline_id)
if widget_timeline:
widget_timeline.title = timeline_data.get('title')
widget_timeline.description = timeline_data.get('description')
for event in timeline_data.get('events', []):
TimelineEventService.create_timeline_event(timeline_id, event)
widget_timeline.save()
return widget_timeline
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 @@ -49,6 +49,7 @@
from .widget_video import API as WIDGET_VIDEO_API
from .engagement_settings import API as ENGAGEMENT_SETTINGS_API
from .cac_form import API as CAC_FORM_API
from .widget_timeline import API as WIDGET_TIMELINE_API

__all__ = ('API_BLUEPRINT',)

Expand Down Expand Up @@ -89,3 +90,4 @@
API.add_namespace(WIDGET_VIDEO_API, path='/widgets/<int:widget_id>/videos')
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')
79 changes: 79 additions & 0 deletions met-api/src/met_api/resources/widget_timeline.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 timeline 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_timeline import WidgetTimelineSchema
from met_api.services.widget_timeline_service import WidgetTimelineService
from met_api.utils.util import allowedorigins, cors_preflight


API = Namespace('widget_timelines', description='Endpoints for Timeline Widget Management')
"""Widget Timelines"""


@cors_preflight('GET, POST')
@API.route('')
class Timelines(Resource):
"""Resource for managing timeline widgets."""

@staticmethod
@cross_origin(origins=allowedorigins())
def get(widget_id):
"""Get timeline widget."""
try:
widget_timeline = WidgetTimelineService().get_timeline(widget_id)
return jsonify(WidgetTimelineSchema().dump(widget_timeline, 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 timeline widget."""
try:
request_json = request.get_json()
widget_timeline = WidgetTimelineService().create_timeline(widget_id, request_json)
return WidgetTimelineSchema().dump(widget_timeline), HTTPStatus.OK
except BusinessException as err:
return str(err), err.status_code


@cors_preflight('PATCH')
@API.route('/<int:timeline_widget_id>')
class Timeline(Resource):
"""Resource for managing timeline widgets."""

@staticmethod
@cross_origin(origins=allowedorigins())
@_jwt.requires_auth
def patch(widget_id, timeline_widget_id):
"""Update timeline widget."""
request_json = request.get_json()
valid_format, errors = schema_utils.validate(request_json, 'timeline_widget_update')
if not valid_format:
return {'message': schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST
try:
widget_timeline = WidgetTimelineService().update_timeline(widget_id, timeline_widget_id, request_json)
return WidgetTimelineSchema().dump(widget_timeline), HTTPStatus.OK
except BusinessException as err:
return str(err), err.status_code
Loading

0 comments on commit 80fbb37

Please sign in to comment.