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

Subscribe Widget - Email list form tab #1879

Merged
merged 29 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
64b375f
email list & form sign up tabs
djnunez-aot Jul 21, 2023
62e179b
add rich text editor to both drawers
djnunez-aot Jul 24, 2023
4c7c419
push changes
djnunez-aot Jul 25, 2023
1f29a94
add subscribe widget path
djnunez-aot Jul 25, 2023
c85d7e1
add print statements for debugging
djnunez-aot Jul 26, 2023
e29bb92
update routes
djnunez-aot Jul 27, 2023
a9e539a
update column type cleanup pr
djnunez-aot Jul 27, 2023
f0133a6
cleanup pr
djnunez-aot Jul 27, 2023
b46017b
remove unused imports
djnunez-aot Jul 27, 2023
44c43b7
py lint
djnunez-aot Jul 27, 2023
b007dd1
lint and add unit test
djnunez-aot Jul 27, 2023
a836861
update revisions
djnunez-aot Jul 27, 2023
0b1ee8f
update test file
djnunez-aot Jul 27, 2023
e2e0c52
pylint
djnunez-aot Jul 28, 2023
841684b
Merge branch 'main' into email-list-tab
djnunez-aot Jul 28, 2023
4b8f296
Update factory_scenarios.py
djnunez-aot Jul 28, 2023
5263668
add subscription import
djnunez-aot Jul 28, 2023
3d64578
pylint
djnunez-aot Jul 28, 2023
dd861c0
fix head colisions
djnunez-aot Jul 28, 2023
5212350
update tests
djnunez-aot Jul 28, 2023
89b1615
update test
djnunez-aot Jul 28, 2023
6305998
add .strip function for json
djnunez-aot Jul 28, 2023
8065f9b
update
djnunez-aot Jul 31, 2023
f361f37
update widget test
djnunez-aot Jul 31, 2023
0060b5a
add more logs
djnunez-aot Jul 31, 2023
591a4a6
lint
djnunez-aot Jul 31, 2023
e2f711c
isort fix
djnunez-aot Jul 31, 2023
be6580a
application out of context error
djnunez-aot Jul 31, 2023
2a8adf5
add widget_id to filtering
djnunez-aot Jul 31, 2023
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
55 changes: 55 additions & 0 deletions met-api/migrations/versions/df73727dc6d9b7_add_sub_tabl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
""" Add subscribe_item table to database

Revision ID: df73727dc6d9b7_add_sub_tbl
Revises: 5a1258a76598
Create Date: 2023-07-26 13:03:24.113767

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = 'df73727dc6d9b7_add_sub_tbl'
down_revision = '5a1258a76598'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('widget_subscribe',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('type', sa.Enum('EMAIL_LIST', 'SIGN_UP', name='subscribetypes'), nullable=False),
sa.Column('sort_index', sa.Integer(), nullable=True),
sa.Column('widget_id', sa.Integer(), nullable=True),
sa.Column('created_by', sa.String(length=50), nullable=True),
sa.Column('updated_by', sa.String(length=50), nullable=True),
sa.Column('created_date', sa.DateTime(), nullable=False),
sa.Column('updated_date', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['widget_id'], ['widget.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('subscribe_item',
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('description', sa.String(length=500), nullable=True),
sa.Column('call_to_action_text', sa.String(length=25), nullable=True),
sa.Column('call_to_action_type', sa.String(length=25), nullable=True),
sa.Column('sort_index', sa.Integer(), nullable=True),
sa.Column('widget_subscribe_id', sa.Integer(), nullable=True),
sa.Column('created_by', sa.String(length=50), nullable=True),
sa.Column('updated_by', sa.String(length=50), nullable=True),
sa.ForeignKeyConstraint(['widget_subscribe_id'], ['widget_subscribe.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###

op.drop_table('subscribe_item')
op.drop_table('widget_subscribe')
op.execute('DROP TYPE subscribetypes;')
# ### end Alembic commands ###
22 changes: 22 additions & 0 deletions met-api/src/met_api/constants/subscribe_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 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.
"""Constants of Subscribe types."""
from enum import IntEnum


class SubscribeTypes(IntEnum):
"""Enum of subscribe types."""

EMAIL_LIST = 0
SIGN_UP = 1
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 @@ -23,6 +23,7 @@
from .engagement_status import EngagementStatus
from .engagement_status_block import EngagementStatusBlock
from .event_item import EventItem
from .subscribe_item import SubscribeItem
from .feedback import Feedback
from .generated_document_template import GeneratedDocumentTemplate
from .generated_document_type import GeneratedDocumentType
Expand All @@ -39,6 +40,7 @@
from .widget import Widget
from .widget_documents import WidgetDocuments
from .widget_events import WidgetEvents
from .widgets_subscribe import WidgetSubscribe
from .widget_item import WidgetItem
from .widget_type import WidgetType
from .email_queue import EmailQueue
Expand Down
28 changes: 28 additions & 0 deletions met-api/src/met_api/models/subscribe_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Widget Subscribe model class.

Manages the Widget Subscribe
"""
from __future__ import annotations

from sqlalchemy.sql.schema import ForeignKey

from .base_model import BaseModel
from .db import db


class SubscribeItem(BaseModel): # pylint: disable=too-few-public-methods, too-many-instance-attributes
"""Subscribe Item table."""

__tablename__ = 'subscribe_item'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
description = db.Column(db.Text)
call_to_action_text = db.Column(db.String(25))
call_to_action_type = db.Column((db.String(25)), nullable=False)
sort_index = db.Column(db.Integer, nullable=True, default=1)
widget_subscribe_id = db.Column(db.Integer, ForeignKey(
'widget_subscribe.id', ondelete='CASCADE'), nullable=True)

@classmethod
def save_subscribe_items(cls, subscribe_items: list) -> None:
"""Update widgets.."""
db.session.bulk_save_objects(subscribe_items)
2 changes: 1 addition & 1 deletion met-api/src/met_api/models/widget_events.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Widget Documents model class.
"""Widget Events model class.

Manages the Widget Events
"""
Expand Down
47 changes: 47 additions & 0 deletions met-api/src/met_api/models/widgets_subscribe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Widget Subscribe model class.

Manages the Widget Subscribe
"""
from __future__ import annotations

from typing import List

from sqlalchemy.sql.schema import ForeignKey

from .base_model import BaseModel
from .db import db
from ..constants.subscribe_types import SubscribeTypes


class WidgetSubscribe(BaseModel): # pylint: disable=too-few-public-methods
"""Widget Subscribe table."""

__tablename__ = 'widget_subscribe'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
type = db.Column(db.Enum(SubscribeTypes), nullable=False)
sort_index = db.Column(db.Integer, nullable=True, default=1)
widget_id = db.Column(db.Integer, ForeignKey(
'widget.id', ondelete='CASCADE'), nullable=True)
subscribe_items = db.relationship(
'SubscribeItem', backref='widget_subscribe', cascade='all,delete,delete-orphan')

@classmethod
def get_all_by_widget_id(cls, widget_id) -> List[WidgetSubscribe]:
"""Get widget subscribe by widget id."""
widget_subscribe_forms = db.session.query(WidgetSubscribe) \
.filter(WidgetSubscribe.widget_id == widget_id) \
.order_by(WidgetSubscribe.sort_index.asc()) \
.all()
return widget_subscribe_forms

@classmethod
def get_all_by_type(cls, type_, widget_id):
"""Get widget subscribe by type."""
return db.session.query(cls).filter_by(type=type_, widget_id=widget_id).all()

@classmethod
def update_widget_events_bulk(cls, update_mappings: list) -> list[WidgetSubscribe]:
"""Save widget subscribe sorting."""
db.session.bulk_update_mappings(WidgetSubscribe, update_mappings)
db.session.commit()
return update_mappings
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 @@ -40,6 +40,7 @@
from .widget import API as WIDGET_API
from .widget_documents import API as WIDGET_DOCUMENTS_API
from .widget_events import API as WIDGET_EVENTS_API
from .widget_subscribe import API as WIDGET_SUBSCRIBE_API
from .widget_map import API as WIDGET_MAPS_API
from .shape_file import API as SHAPEFILE_API
from .tenant import API as TENANT_API
Expand Down Expand Up @@ -80,6 +81,7 @@
API.add_namespace(WIDGET_DOCUMENTS_API, path='/widgets/<string:widget_id>/documents')
API.add_namespace(ENGAGEMENT_MEMBERS_API, path='/engagements/<string:engagement_id>/members')
API.add_namespace(WIDGET_EVENTS_API, path='/widgets/<int:widget_id>/events')
API.add_namespace(WIDGET_SUBSCRIBE_API, path='/widgets/<int:widget_id>/subscribe')
API.add_namespace(WIDGET_MAPS_API, path='/widgets/<int:widget_id>/maps')
API.add_namespace(ENGAGEMENT_SLUG_API, path='/slugs')
API.add_namespace(REPORT_SETTING_API, path='/surveys/<int:survey_id>/reportsettings')
Expand Down
126 changes: 126 additions & 0 deletions met-api/src/met_api/resources/widget_subscribe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# 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 an user 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.exceptions.business_exception import BusinessException
from met_api.schemas.subscribe_item import SubscribeItemSchema
from met_api.schemas.widget_subscribe import WidgetSubscribeSchema
from met_api.services.widget_subscribe_service import WidgetSubscribeService
from met_api.utils.token_info import TokenInfo
from met_api.utils.util import allowedorigins, cors_preflight


API = Namespace('widgets_subscribe',
description='Endpoints for Widget Subscribe')
"""Widget Subscribe
"""


@cors_preflight('GET, POST, OPTIONS, DELETE')
@API.route('')
class WidgetSubscribe(Resource):
"""Resource for managing a Widget Subscribe."""

@staticmethod
@cross_origin(origins=allowedorigins())
def get(widget_id):
"""Fetch a list of widgets by engagement_id."""
try:
subscribe = WidgetSubscribeService().get_subscribe_by_widget_id(widget_id)
return jsonify(WidgetSubscribeSchema().dump(subscribe, many=True)), HTTPStatus.OK
except (KeyError, ValueError) as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR

@staticmethod
@cross_origin(origins=allowedorigins())
def post(widget_id):
"""Add new subscribe to the widgets."""
request_json = request.get_json()
try:
subscribe = WidgetSubscribeService().create_subscribe(widget_id, request_json)
return WidgetSubscribeSchema().dump(subscribe, many=False), HTTPStatus.OK
except BusinessException as err:
return str(err), err.status_code

@staticmethod
@cross_origin(origins=allowedorigins())
def delete(widget_id, subscribe_id):
"""Delete an subscribe ."""
try:
WidgetSubscribeService().delete_subscribe(subscribe_id, widget_id)
response, status = {}, HTTPStatus.OK
except BusinessException as err:
response, status = str(err), err.status_code
return response, status


@cors_preflight('GET,POST,OPTIONS')
@API.route('/<int:subscribe_id>/items', methods=['GET', 'DELETE', 'OPTIONS'])
class WidgetSubscribeItems(Resource):
"""Resource for managing a Widget Subscribe."""

@staticmethod
@cross_origin(origins=allowedorigins())
def post(widget_id, subscribe_id):
"""Add new subscribe to the widgets."""
request_json = request.get_json()
print(request_json)
try:
subscribe = WidgetSubscribeService().create_subscribe_items(
widget_id, subscribe_id, request_json)
return WidgetSubscribeSchema().dump(subscribe), HTTPStatus.OK
except BusinessException as err:
return str(err), err.status_code


@cors_preflight('PATCH')
@API.route('/<int:subscribe_id>/item/<int:item_id>', methods=['PATCH'])
class SubscribeItems(Resource):
"""Resource for managing a Widget Subscribe Item."""

@staticmethod
@cross_origin(origins=allowedorigins())
def patch(widget_id, subscribe_id, item_id):
"""Update subscribe item."""
request_json = request.get_json()
try:
subscribe = WidgetSubscribeService().update_subscribe_item(
widget_id, subscribe_id, item_id, request_json)
return SubscribeItemSchema().dump(subscribe), HTTPStatus.OK
except BusinessException as err:
return str(err), err.status_code


@cors_preflight('PATCH')
@API.route('/sort_index')
class WidgetSubscribeSort(Resource):
"""Resource for managing subscribe sort order within subscribe widget."""

@staticmethod
@cross_origin(origins=allowedorigins())
def patch(widget_id):
"""Sort subscribe for an subscribe widget."""
try:
request_json = request.get_json()
sort_widget_subscribe = WidgetSubscribeService().save_widget_subscribes_bulk(widget_id, request_json,
user_id=TokenInfo.get_id())
return WidgetSubscribeSchema().dump(sort_widget_subscribe), HTTPStatus.OK
except BusinessException as err:
return str(err), err.status_code
27 changes: 27 additions & 0 deletions met-api/src/met_api/schemas/subscribe_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 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 link schema and export."""

from met_api.models import SubscribeItem as SubscribeItemModel

from .base_schema import BaseSchema


class SubscribeItemSchema(BaseSchema): # pylint: disable=too-many-ancestors, too-few-public-methods
"""This is the schema for the Contact link model."""

class Meta(BaseSchema.Meta): # pylint: disable=too-few-public-methods
"""Maps all of the Widget subscribe fields to a default schema."""

model = SubscribeItemModel
35 changes: 35 additions & 0 deletions met-api/src/met_api/schemas/widget_subscribe.py
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.
"""Manager for widget link schema and export."""

from marshmallow import fields
from marshmallow_enum import EnumField

from met_api.models import WidgetSubscribe as WidgetSubscribeModel

from .base_schema import BaseSchema
from .subscribe_item import SubscribeItemSchema
from ..constants.subscribe_types import SubscribeTypes


class WidgetSubscribeSchema(BaseSchema): # pylint: disable=too-many-ancestors, too-few-public-methods
"""This is the schema for the Contact link model."""

class Meta(BaseSchema.Meta): # pylint: disable=too-few-public-methods
"""Maps all of the Widget Subscribe fields to a default schema."""

model = WidgetSubscribeModel

subscribe_items = fields.List(fields.Nested(SubscribeItemSchema))
type = EnumField(SubscribeTypes)
Loading
Loading