Skip to content

Commit

Permalink
Deseng 464 : Poll widget UI (#2367)
Browse files Browse the repository at this point in the history
* DESENG-464: Poll UI basic

* DESENG-464: Updated service

* DESENG-464: Updating MODEL

* DESENG-464: Poll Editing restricted

* DESENG-464: Poll UI wrapping up

* DESENF-464:  Poll UI wrapped up

* Changelog updated

* Fixing issues

* DESENG-464: Refactoring Poll Widget

* Fixed Build issues with github

* Fixing review comments
  • Loading branch information
ratheesh-aot authored Jan 31, 2024
1 parent ed80ca2 commit 40fe3b9
Show file tree
Hide file tree
Showing 29 changed files with 1,265 additions and 108 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
- **Task** Updated Babel Traverse library. [🎟️DESENG-474](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-474)
- Run `npm audit fix` to update the vulnerable Babel traverse library.

## January 26, 2024
- **Task** Poll Widget: Front-end. [🎟️DESENG-464](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-464)
- Created UI for Poll Widget.
- Updated Poll widget API and unit tests.

## January 25, 2024
- **Task** Resolve issue preventing met-web from deploying on the Dev OpenShift environment. [🎟️DESENG-469](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-469)
- Remove Epic Engage-related links and update Keycloak link.
Expand Down
169 changes: 94 additions & 75 deletions met-api/src/met_api/resources/widget_poll.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
from met_api.utils.util import allowedorigins, cors_preflight
from met_api.utils.ip_util import hash_ip

API = Namespace('widget_polls', description='Endpoints for Poll Widget Management')
INVALID_REQUEST_MESSAGE = 'Invalid request format'
API = Namespace(
'widget_polls', description='Endpoints for Poll Widget Management'
)


@cors_preflight('GET, POST')
Expand All @@ -28,7 +29,10 @@ def get(widget_id):
"""Get poll widgets."""
try:
widget_poll = WidgetPollService().get_polls_by_widget_id(widget_id)
return WidgetPollSchema().dump(widget_poll, many=True), HTTPStatus.OK
return (
WidgetPollSchema().dump(widget_poll, many=True),
HTTPStatus.OK,
)
except BusinessException as err:
return str(err), err.status_code

Expand All @@ -39,22 +43,22 @@ def post(widget_id):
"""Create poll widget."""
try:
request_json = request.get_json()
valid_format, errors = Polls.validate_request_format(request_json)
valid_format, errors = schema_utils.validate(
request_json, 'poll_widget'
)
if not valid_format:
return {'message': INVALID_REQUEST_MESSAGE, 'errors': errors}, HTTPStatus.BAD_REQUEST
widget_poll = WidgetPollService().create_poll(widget_id, request_json)
raise BusinessException(
error=schema_utils.serialize(errors),
status_code=HTTPStatus.BAD_REQUEST,
)

widget_poll = WidgetPollService().create_poll(
widget_id, request_json
)
return WidgetPollSchema().dump(widget_poll), HTTPStatus.OK
except BusinessException as err:
return str(err), err.status_code

@staticmethod
def validate_request_format(data):
"""Validate response format."""
valid_format, errors = schema_utils.validate(data, 'poll_widget')
if not valid_format:
errors = schema_utils.serialize(errors)
return valid_format, errors


@cors_preflight('PATCH')
@API.route('/<int:poll_widget_id>')
Expand All @@ -68,23 +72,31 @@ def patch(widget_id, poll_widget_id):
"""Update poll widget."""
try:
request_json = request.get_json()
valid_format, errors = Poll.validate_request_format(request_json)
valid_format, errors = schema_utils.validate(
request_json, 'poll_widget_update'
)
if not valid_format:
return {'message': INVALID_REQUEST_MESSAGE, 'errors': errors}, HTTPStatus.BAD_REQUEST

widget_poll = WidgetPollService().update_poll(widget_id, poll_widget_id, request_json)
raise BusinessException(
error=schema_utils.serialize(errors),
status_code=HTTPStatus.BAD_REQUEST,
)
# Check if the poll engagement is published
if WidgetPollService.is_poll_engagement_published(poll_widget_id):
# Define the keys to check in the request_json
keys_to_check = ['title', 'description', 'answers']
if any(key in request_json for key in keys_to_check):
raise BusinessException(
error='Cannot update poll widget as the engagement is published',
status_code=HTTPStatus.BAD_REQUEST,
)

widget_poll = WidgetPollService().update_poll(
widget_id, poll_widget_id, request_json
)
return WidgetPollSchema().dump(widget_poll), HTTPStatus.OK
except BusinessException as err:
return str(err), err.status_code

@staticmethod
def validate_request_format(data):
"""Validate request format."""
valid_format, errors = schema_utils.validate(data, 'poll_widget_update')
if not valid_format:
errors = schema_utils.serialize(errors)
return valid_format, errors


@cors_preflight('POST')
@API.route('/<int:poll_widget_id>/responses')
Expand All @@ -94,58 +106,65 @@ class PollResponseRecord(Resource):
@staticmethod
@cross_origin(origins=allowedorigins())
def post(widget_id, poll_widget_id):
# pylint: disable=too-many-return-statements
"""Record a response for a given poll widget."""
try:
response_data = request.get_json()
valid_format, errors = PollResponseRecord.validate_request_format(response_data)
poll_response_data = request.get_json()
valid_format, errors = schema_utils.validate(
poll_response_data, 'poll_response'
)
if not valid_format:
return {'message': INVALID_REQUEST_MESSAGE, 'errors': errors}, HTTPStatus.BAD_REQUEST

response_dict = PollResponseRecord.prepare_response_data(response_data, widget_id, poll_widget_id)

if not PollResponseRecord.is_poll_active(poll_widget_id):
return {'message': 'Poll is not active'}, HTTPStatus.BAD_REQUEST

if PollResponseRecord.is_poll_limit_exceeded(poll_widget_id, response_dict['participant_id']):
return {'message': 'Limit exceeded for this poll'}, HTTPStatus.FORBIDDEN

return PollResponseRecord.record_poll_response(response_dict)
raise BusinessException(
error=schema_utils.serialize(errors),
status_code=HTTPStatus.BAD_REQUEST,
)

# Prepare poll request object
poll_response_dict = {
**poll_response_data,
'poll_id': poll_widget_id,
'widget_id': widget_id,
'participant_id': hash_ip(request.remote_addr),
}

# Check if poll active or not
if not WidgetPollService.is_poll_active(poll_widget_id):
raise BusinessException(
error='Poll is not active',
status_code=HTTPStatus.BAD_REQUEST,
)

# Check if engagement of this poll is published or not
if not WidgetPollService.is_poll_engagement_published(
poll_widget_id
):
raise BusinessException(
error='Poll engagement is not published',
status_code=HTTPStatus.BAD_REQUEST,
)

# Check poll limit execeeded or not
if WidgetPollService.check_already_polled(
poll_widget_id, poll_response_dict['participant_id'], 10
):
raise BusinessException(
error='Limit exceeded for this poll',
status_code=HTTPStatus.BAD_REQUEST,
)

# Record poll response in database
poll_response = WidgetPollService.record_response(
poll_response_dict
)
if poll_response.id:
return {
'message': 'Response recorded successfully'
}, HTTPStatus.CREATED

raise BusinessException(
error='Response failed to record',
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
)

except BusinessException as err:
return err.error, err.status_code

@staticmethod
def validate_request_format(data):
"""Validate Request format."""
valid_format, errors = schema_utils.validate(data, 'poll_response')
if not valid_format:
errors = schema_utils.serialize(errors)
return valid_format, errors

@staticmethod
def prepare_response_data(data, widget_id, poll_widget_id):
"""Prepare poll response object."""
response_dict = dict(data)
response_dict['poll_id'] = poll_widget_id
response_dict['widget_id'] = widget_id
response_dict['participant_id'] = hash_ip(request.remote_addr)
return response_dict

@staticmethod
def is_poll_active(poll_id):
"""Check if poll active or not."""
return WidgetPollService.is_poll_active(poll_id)

@staticmethod
def is_poll_limit_exceeded(poll_id, participant_id):
"""Check poll limit execeeded or not."""
return WidgetPollService.check_already_polled(poll_id, participant_id, 10)

@staticmethod
def record_poll_response(response_dict):
"""Record poll respinse in database."""
poll_response = WidgetPollService.record_response(response_dict)
if poll_response.id:
return {'message': 'Response recorded successfully'}, HTTPStatus.CREATED

return {'message': 'Response failed to record'}, HTTPStatus.INTERNAL_SERVER_ERROR
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
]
}
],
"required": ["widget_id", "engagement_id"],
"required": [],
"properties": {
"title": {
"$id": "#/properties/title",
Expand Down
55 changes: 46 additions & 9 deletions met-api/src/met_api/services/widget_poll_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
from http import HTTPStatus

from sqlalchemy.exc import SQLAlchemyError

from met_api.constants.engagement_status import Status as EngagementStatus
from met_api.constants.membership_type import MembershipType
from met_api.exceptions.business_exception import BusinessException
from met_api.models.widget_poll import Poll as PollModel
from met_api.services import authorization
from met_api.services.engagement_service import EngagementService
from met_api.services.poll_answers_service import PollAnswerService
from met_api.services.poll_response_service import PollResponseService
from met_api.utils.roles import Role
Expand All @@ -24,7 +27,9 @@ def get_poll_by_id(poll_id: int):
"""Get poll by poll ID."""
poll = PollModel.query.get(poll_id)
if not poll:
raise BusinessException('Poll widget not found', HTTPStatus.NOT_FOUND)
raise BusinessException(
'Poll widget not found', HTTPStatus.NOT_FOUND
)
return poll

@staticmethod
Expand All @@ -33,7 +38,9 @@ def create_poll(widget_id: int, poll_details: dict):
try:
eng_id = poll_details.get('engagement_id')
WidgetPollService._check_authorization(eng_id)
return WidgetPollService._create_poll_model(widget_id, poll_details)
return WidgetPollService._create_poll_model(
widget_id, poll_details
)
except SQLAlchemyError as exc:
raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) from exc

Expand All @@ -45,9 +52,13 @@ def update_poll(widget_id: int, poll_widget_id: int, poll_data: dict):
WidgetPollService._check_authorization(widget_poll.engagement_id)

if widget_poll.widget_id != widget_id:
raise BusinessException('Invalid widget ID', HTTPStatus.BAD_REQUEST)
raise BusinessException(
'Invalid widget ID', HTTPStatus.BAD_REQUEST
)

return WidgetPollService._update_poll_model(poll_widget_id, poll_data)
return WidgetPollService._update_poll_model(
poll_widget_id, poll_data
)
except SQLAlchemyError as exc:
raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) from exc

Expand Down Expand Up @@ -77,6 +88,21 @@ def is_poll_active(poll_id: int) -> bool:
except SQLAlchemyError as exc:
raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) from exc

@staticmethod
def is_poll_engagement_published(poll_id: int) -> bool:
"""Check if the poll is published."""
try:
poll = WidgetPollService.get_poll_by_id(poll_id)
engagement = EngagementService().get_engagement(poll.engagement_id)
pub_val = EngagementStatus.Published.value
# Return False immediately if engagement is None
if engagement is None:
return False
# Check if the engagement's status matches the published value
return engagement.get('status_id') == pub_val
except SQLAlchemyError as exc:
raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) from exc

@staticmethod
def _create_poll_model(widget_id: int, poll_data: dict):
"""Private method to create poll model."""
Expand All @@ -94,12 +120,23 @@ def _update_poll_model(poll_id: int, poll_data: dict):
@staticmethod
def _check_authorization(engagement_id):
"""Check user authorization."""
authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name, Role.EDIT_ENGAGEMENT.value),
engagement_id=engagement_id)
authorization.check_auth(
one_of_roles=(
MembershipType.TEAM_MEMBER.name,
Role.EDIT_ENGAGEMENT.value,
),
engagement_id=engagement_id,
)

@staticmethod
def _handle_poll_answers(poll_id: int, poll_data: dict):
"""Handle poll answers creation and deletion."""
PollAnswerService.delete_poll_answers(poll_id)
answers_data = poll_data.get('answers', [])
PollAnswerService.create_bulk_poll_answers(poll_id, answers_data)
try:
if 'answers' in poll_data and len(poll_data['answers']) > 0:
PollAnswerService.delete_poll_answers(poll_id)
answers_data = poll_data.get('answers', [])
PollAnswerService.create_bulk_poll_answers(
poll_id, answers_data
)
except SQLAlchemyError as exc:
raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) from exc
Loading

0 comments on commit 40fe3b9

Please sign in to comment.