Skip to content

Commit

Permalink
Back end updates for feedback api (#2035)
Browse files Browse the repository at this point in the history
* back end updates for feedback api

* update tests

* update feedback test

* add new migration files & update tests for backend

* update enums && lint

* update migrations

* add missing enum

* update tests

* update resource

* fix migration error

* upload tests for front end

* update tests

* test updates

* add docstring and update feedback model

* linting
  • Loading branch information
djnunez-aot authored Aug 22, 2023
1 parent 08cd3a5 commit ab2cad8
Show file tree
Hide file tree
Showing 15 changed files with 246 additions and 24 deletions.
40 changes: 40 additions & 0 deletions met-api/migrations/versions/04e6c48187da_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""
Revision ID: 04e6c48187da
Revises: f40da1b8f3e0
Create Date: 2023-08-18 12:45:30.620941
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = '04e6c48187da'
down_revision = 'f40da1b8f3e0'
branch_labels = None
depends_on = None

# Define the Enum type for feedback status
feedback_status_enum = sa.Enum(
'Unreviewed', 'Archived', name='feedbackstatustype')


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

# Create the Enum type in the database
feedback_status_enum.create(op.get_bind())

op.add_column('feedback', sa.Column('status', sa.Enum(
'Unreviewed', 'Archived', name='feedbackstatustype'), nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('feedback', 'status')

# Drop the Enum type from the database
feedback_status_enum.drop(op.get_bind())
# ### end Alembic commands ###
7 changes: 7 additions & 0 deletions met-api/src/met_api/constants/feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ class CommentType(IntEnum):
Else = 3


class FeedbackStatusType(IntEnum):
"""Status types enum."""

Unreviewed = 0
Archived = 1


class FeedbackSourceType(IntEnum):
"""Source types enum."""

Expand Down
38 changes: 34 additions & 4 deletions met-api/src/met_api/models/feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
from sqlalchemy import TEXT, asc, cast, desc
from sqlalchemy.sql import text

from met_api.constants.feedback import CommentType, FeedbackSourceType, RatingType
from met_api.constants.feedback import CommentType, FeedbackSourceType, FeedbackStatusType, RatingType
from met_api.models.pagination_options import PaginationOptions

from .base_model import BaseModel
from .db import db

Expand All @@ -18,11 +19,13 @@ class Feedback(BaseModel):

__tablename__ = 'feedback'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
status = db.Column(db.Enum(FeedbackStatusType), nullable=True)
rating = db.Column(db.Enum(RatingType), nullable=True)
comment_type = db.Column(db.Enum(CommentType), nullable=True)
comment = db.Column(db.Text, nullable=True)
source = db.Column(db.Enum(FeedbackSourceType), nullable=True)
tenant_id = db.Column(db.Integer, db.ForeignKey('tenant.id'), nullable=True)
tenant_id = db.Column(
db.Integer, db.ForeignKey('tenant.id'), nullable=True)

@classmethod
def get_all_paginated(cls, pagination_options: PaginationOptions, search_text=''):
Expand All @@ -31,7 +34,8 @@ def get_all_paginated(cls, pagination_options: PaginationOptions, search_text=''

if search_text:
# Remove all non-digit characters from search text
query = query.filter(cast(Feedback.id, TEXT).like('%' + search_text + '%'))
query = query.filter(
cast(Feedback.id, TEXT).like('%' + search_text + '%'))

sort = asc(text(pagination_options.sort_key)) if pagination_options.sort_order == 'asc'\
else desc(text(pagination_options.sort_key))
Expand All @@ -43,14 +47,16 @@ def get_all_paginated(cls, pagination_options: PaginationOptions, search_text=''
items = query.all()
return items, len(items)

page = query.paginate(page=pagination_options.page, per_page=pagination_options.size)
page = query.paginate(page=pagination_options.page,
per_page=pagination_options.size)

return page.items, page.total

@staticmethod
def create_feedback(feedback):
"""Create new feedback entity."""
new_feedback = Feedback(
status=feedback.get('status', None),
comment=feedback.get('comment', None),
created_date=datetime.utcnow(),
rating=feedback.get('rating'),
Expand All @@ -60,3 +66,27 @@ def create_feedback(feedback):
db.session.add(new_feedback)
db.session.commit()
return new_feedback

@classmethod
def delete_by_id(cls, feedback_id):
"""Delete feedback by ID."""
feedback = cls.query.get(feedback_id)
if feedback:
db.session.delete(feedback)
db.session.commit()
return True # Successfully deleted
return False # Feedback not found

@classmethod
def update_feedback(cls, feedback_id, feedback_data):
"""Update feedback by ID."""
feedback = cls.query.get(feedback_id)
if not feedback:
return None # Feedback not found

for key, value in feedback_data.items():
if hasattr(feedback, key):
setattr(feedback, key, value)

db.session.commit()
return feedback
54 changes: 45 additions & 9 deletions met-api/src/met_api/resources/feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
# 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 feedback resource."""

"""API endpoints for managing a feedback resource."""

from http import HTTPStatus

Expand All @@ -21,22 +22,22 @@

from met_api.auth import auth
from met_api.auth import jwt as _jwt
from met_api.models.pagination_options import PaginationOptions
from met_api.schemas import utils as schema_utils
from met_api.models.pagination_options import PaginationOptions
from met_api.services.feedback_service import FeedbackService
from met_api.utils.token_info import TokenInfo
from met_api.utils.util import allowedorigins, cors_preflight


API = Namespace('feedbacks', description='Endpoints for Feedbacks Management')
"""Custom exception messages
"""

# For operations that don't require an ID


@cors_preflight('GET, POST, OPTIONS')
@API.route('/')
class Feedback(Resource):
"""Resource for managing feedbacks."""
@API.route('/', methods=['GET', 'POST'])
class FeedbackList(Resource):
"""Feedback List Resource."""

@staticmethod
@cross_origin(origins=allowedorigins())
Expand All @@ -52,7 +53,8 @@ def get():
sort_key=args.get('sort_key', 'name', str),
sort_order=args.get('sort_order', 'asc', str),
)
feedback_records = FeedbackService().get_feedback_paginated(pagination_options, search_text)
feedback_records = FeedbackService().get_feedback_paginated(
pagination_options, search_text)

return feedback_records, HTTPStatus.OK
except ValueError as err:
Expand All @@ -66,7 +68,8 @@ def post():
try:
user_id = TokenInfo.get_id()
request_json = request.get_json()
valid_format, errors = schema_utils.validate(request_json, 'feedback')
valid_format, errors = schema_utils.validate(
request_json, 'feedback')
if not valid_format:
return {'message': schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST
result = FeedbackService().create_feedback(request_json, user_id)
Expand All @@ -75,3 +78,36 @@ def post():
return 'feedback was not found', HTTPStatus.INTERNAL_SERVER_ERROR
except ValueError as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR


@cors_preflight('DELETE, PATCH, OPTIONS')
@API.route('/<int:feedback_id>', methods=['DELETE', 'PATCH'])
class FeedbackById(Resource):
"""Feedback Id Resource."""

@staticmethod
@cross_origin(origins=allowedorigins())
@_jwt.requires_auth
def delete(feedback_id):
"""Remove Feedback for an engagement."""
try:
result = FeedbackService().delete_feedback(feedback_id)
if result:
return 'Feedback successfully removed', HTTPStatus.OK
return 'Feedback not found', HTTPStatus.NOT_FOUND
except KeyError as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR
except ValueError as err:
return str(err), HTTPStatus.INTERNAL_SERVER_ERROR

@staticmethod
@cross_origin(origins=allowedorigins())
@_jwt.requires_auth
def patch(feedback_id):
"""Update feedback by ID."""
feedback_data = request.get_json()
updated_feedback = FeedbackService.update_feedback(
feedback_id, feedback_data)
if updated_feedback:
return updated_feedback, HTTPStatus.OK
return {'message': 'Feedback not found'}, HTTPStatus.NOT_FOUND
4 changes: 3 additions & 1 deletion met-api/src/met_api/schemas/feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from marshmallow import EXCLUDE, Schema, fields
from marshmallow_enum import EnumField
from met_api.constants.feedback import CommentType, FeedbackSourceType, RatingType

from met_api.constants.feedback import CommentType, FeedbackSourceType, FeedbackStatusType, RatingType


class FeedbackSchema(Schema):
Expand All @@ -16,6 +17,7 @@ class Meta: # pylint: disable=too-few-public-methods
id = fields.Int(data_key='id')
comment = fields.Str(data_key='comment')
created_date = fields.DateTime(data_key='created_date')
status = EnumField(FeedbackStatusType, by_value=True)
rating = EnumField(RatingType, by_value=True)
comment_type = EnumField(CommentType, by_value=True)
source = EnumField(FeedbackSourceType, by_value=True)
15 changes: 15 additions & 0 deletions met-api/src/met_api/services/feedback_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,18 @@ def create_feedback(cls, feedback: FeedbackSchema, user_id):
new_feedback = Feedback.create_feedback(feedback)
feedback_schema = FeedbackSchema()
return feedback_schema.dump(new_feedback)

@classmethod
def update_feedback(cls, feedback_id, feedback_data):
"""Update feedback by its ID."""
updated_feedback = Feedback.update_feedback(feedback_id, feedback_data)
if updated_feedback:
feedback_schema = FeedbackSchema()
return feedback_schema.dump(updated_feedback)
return None

@classmethod
def delete_feedback(cls, feedback_id):
"""Remove Feedback from engagement."""
is_deleted = Feedback.delete_by_id(feedback_id)
return is_deleted
52 changes: 51 additions & 1 deletion met-api/tests/unit/api/test_feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"""
import json

from met_api.constants.feedback import FeedbackSourceType
from met_api.constants.feedback import FeedbackSourceType, FeedbackStatusType
from met_api.utils.enums import ContentType

from tests.utilities.factory_scenarios import TestJwtClaims
Expand All @@ -31,6 +31,7 @@ def test_feedback(client, jwt, session): # pylint:disable=unused-argument

feedback = factory_feedback_model()
to_dict = {
'status': feedback.status,
'rating': feedback.rating,
'comment_type': feedback.comment_type,
'comment': feedback.comment
Expand All @@ -45,6 +46,7 @@ def test_feedback(client, jwt, session): # pylint:disable=unused-argument
assert result.get('id') is not None
assert result.get('rating') == feedback.rating
assert result.get('comment_type') == feedback.comment_type
assert result.get('status') == feedback.status
assert result.get('comment') == feedback.comment
assert result.get('source') == FeedbackSourceType.Internal

Expand Down Expand Up @@ -73,3 +75,51 @@ def test_invalid_feedback(client, jwt, session): # pylint:disable=unused-argume
headers=headers, content_type=ContentType.JSON.value)
print(rv.json.get('message'))
assert rating_error_msg in rv.json.get('message')


def test_patch_feedback(client, jwt, session): # pylint:disable=unused-argument
"""Assert that feedback can be updated via PATCH."""
# Setup: Create a new feedback first
claims = TestJwtClaims.public_user_role
feedback = factory_feedback_model()
feedback_creation = {
'status': feedback.status,
'rating': feedback.rating,
'comment_type': feedback.comment_type,
'comment': feedback.comment
}
headers = factory_auth_header(jwt=jwt, claims=claims)
rv = client.post('/api/feedbacks/', data=json.dumps(feedback_creation),
headers=headers, content_type=ContentType.JSON.value)
feedback_id = rv.json.get('id')

assert rv.status_code == 200

feedback_creation['status'] = FeedbackStatusType.Archived.value

rv = client.patch(f'/api/feedbacks/{feedback_id}', data=json.dumps(feedback_creation),
headers=headers, content_type=ContentType.JSON.value)
assert rv.status_code == 200
# Check if the status is update
assert rv.json.get('status') == FeedbackStatusType.Archived.value


def test_delete_feedback(client, jwt, session): # pylint:disable=unused-argument
"""Assert that feedback can be deleted."""
# Setup: Create a new feedback first
claims = TestJwtClaims.public_user_role
feedback = factory_feedback_model()
feedback_creation = {
'status': feedback.status,
'rating': feedback.rating,
'comment_type': feedback.comment_type,
'comment': feedback.comment
}
headers = factory_auth_header(jwt=jwt, claims=claims)
rv = client.post('/api/feedbacks/', data=json.dumps(feedback_creation),
headers=headers, content_type=ContentType.JSON.value)
feedback_id = rv.json.get('id')
assert rv.status_code == 200
# Now, delete this feedback
rv = client.delete(f'/api/feedbacks/{feedback_id}', headers=headers)
assert rv.status_code == 200
3 changes: 2 additions & 1 deletion met-api/tests/unit/models/test_feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"""

from faker import Faker
from met_api.constants.feedback import FeedbackSourceType
from met_api.constants.feedback import FeedbackSourceType, FeedbackStatusType

from met_api.models import Feedback as FeedbackModel
from met_api.models.pagination_options import PaginationOptions
Expand All @@ -31,6 +31,7 @@ def test_feedback(session):
feedback = factory_feedback_model()
assert feedback.id is not None
feedback_existing = FeedbackModel.find_by_id(feedback.id)
assert feedback.status == FeedbackStatusType.Unreviewed
assert feedback.comment == feedback_existing.comment
assert feedback.source == FeedbackSourceType.Public

Expand Down
6 changes: 3 additions & 3 deletions met-api/tests/utilities/factory_scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@

from met_api.config import get_named_config
from met_api.constants.comment_status import Status as CommentStatus
from met_api.constants.engagement_status import SubmissionStatus
from met_api.constants.engagement_status import Status as EngagementStatus

from met_api.constants.feedback import CommentType, FeedbackSourceType, RatingType
from met_api.constants.engagement_status import SubmissionStatus
from met_api.constants.feedback import CommentType, FeedbackSourceType, FeedbackStatusType, RatingType
from met_api.constants.widget import WidgetType
from met_api.utils.enums import LoginSource, UserStatus

Expand Down Expand Up @@ -235,6 +234,7 @@ class TestFeedbackInfo(dict, Enum):
"""Test scenarios of feedback."""

feedback1 = {
'status': FeedbackStatusType.Unreviewed,
'comment': 'A feedback comment',
'rating': RatingType.Satisfied,
'comment_type': CommentType.Idea,
Expand Down
Loading

0 comments on commit ab2cad8

Please sign in to comment.