From a1c163ba67417553ee99393305381f5c2f89bdba Mon Sep 17 00:00:00 2001 From: Ratheesh kumar R Date: Tue, 16 Jan 2024 14:43:07 -0800 Subject: [PATCH 01/14] DESENG-463: Created models for Poll widget --- .../08f69642b7ae_adding_widget_poll.py | 83 +++++++++++++++++++ met-api/src/met_api/models/__init__.py | 3 + met-api/src/met_api/models/poll_answers.py | 35 ++++++++ met-api/src/met_api/models/poll_responses.py | 38 +++++++++ met-api/src/met_api/models/widget_poll.py | 40 +++++++++ 5 files changed, 199 insertions(+) create mode 100644 met-api/migrations/versions/08f69642b7ae_adding_widget_poll.py create mode 100644 met-api/src/met_api/models/poll_answers.py create mode 100644 met-api/src/met_api/models/poll_responses.py create mode 100644 met-api/src/met_api/models/widget_poll.py diff --git a/met-api/migrations/versions/08f69642b7ae_adding_widget_poll.py b/met-api/migrations/versions/08f69642b7ae_adding_widget_poll.py new file mode 100644 index 000000000..af6df74f8 --- /dev/null +++ b/met-api/migrations/versions/08f69642b7ae_adding_widget_poll.py @@ -0,0 +1,83 @@ +"""adding_widget_poll + +Revision ID: 08f69642b7ae +Revises: bd0eb0d25caf +Create Date: 2024-01-16 14:25:07.611485 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '08f69642b7ae' +down_revision = 'bd0eb0d25caf' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('widget_polls', + 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('title', sa.String(length=255), nullable=False), + sa.Column('description', sa.String(length=2048), nullable=True), + sa.Column('status', sa.Enum('active', 'inactive', name='poll_status'), nullable=True), + sa.Column('widget_id', sa.Integer(), nullable=True), + sa.Column('engagement_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(['engagement_id'], ['engagement.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['widget_id'], ['widget.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('poll_answers', + 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('answer_text', sa.String(length=255), nullable=False), + sa.Column('poll_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(['poll_id'], ['widget_polls.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('poll_responses', + 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('participant_id', sa.String(length=255), nullable=False), + sa.Column('selected_answer_id', sa.Integer(), nullable=True), + sa.Column('poll_id', sa.Integer(), nullable=True), + sa.Column('widget_id', sa.Integer(), nullable=True), + sa.Column('is_deleted', sa.Boolean(), nullable=True), + sa.Column('created_by', sa.String(length=50), nullable=True), + sa.Column('updated_by', sa.String(length=50), nullable=True), + sa.ForeignKeyConstraint(['poll_id'], ['widget_polls.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['selected_answer_id'], ['poll_answers.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['widget_id'], ['widget.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + 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': 10, 'name': 'Poll', 'description': 'The Poll Widget enables real-time polling and feedback collection from public.'} + ]) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('poll_responses') + op.drop_table('poll_answers') + op.drop_table('widget_polls') + + conn = op.get_bind() + + conn.execute('DELETE FROM widget_type WHERE id=10') + # ### end Alembic commands ### diff --git a/met-api/src/met_api/models/__init__.py b/met-api/src/met_api/models/__init__.py index b61d5a5af..951c99073 100644 --- a/met-api/src/met_api/models/__init__.py +++ b/met-api/src/met_api/models/__init__.py @@ -51,3 +51,6 @@ from .cac_form import CACForm from .widget_timeline import WidgetTimeline from .timeline_event import TimelineEvent +from .widget_poll import Poll +from .poll_answers import PollAnswer +from .poll_responses import PollResponse diff --git a/met-api/src/met_api/models/poll_answers.py b/met-api/src/met_api/models/poll_answers.py new file mode 100644 index 000000000..e6d0394a8 --- /dev/null +++ b/met-api/src/met_api/models/poll_answers.py @@ -0,0 +1,35 @@ +""" +PollAnswers model class. + +Manages the Poll answers +""" +from __future__ import annotations + +from sqlalchemy.sql.schema import ForeignKey +from sqlalchemy import Enum + +from .base_model import BaseModel +from .db import db + +class PollAnswer(BaseModel): + """Definition of the PollAnswer entity.""" + + __tablename__ = 'poll_answers' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + answer_text = db.Column(db.String(255), nullable=False) + poll_id = db.Column(db.Integer, ForeignKey('widget_polls.id', ondelete='CASCADE')) + + @classmethod + def get_answers(cls, poll_id) -> list[PollAnswer]: + """Get answers for a poll.""" + return db.session.query(PollAnswer).filter(PollAnswer.poll_id == poll_id).all() + + @classmethod + def update_answer(cls, answer_id, answer_data: dict) -> PollAnswer: + """Update an answer.""" + answer = PollAnswer.query.get(answer_id) + if answer: + for key, value in answer_data.items(): + setattr(answer, key, value) + answer.save() + return answer diff --git a/met-api/src/met_api/models/poll_responses.py b/met-api/src/met_api/models/poll_responses.py new file mode 100644 index 000000000..4c6b38316 --- /dev/null +++ b/met-api/src/met_api/models/poll_responses.py @@ -0,0 +1,38 @@ +""" +PollResponse model class. + +Manages the Poll Responses +""" +from __future__ import annotations + +from sqlalchemy.sql.schema import ForeignKey + +from .base_model import BaseModel +from .db import db + + +class PollResponse(BaseModel): + """Definition of the PollResponse entity.""" + + __tablename__ = 'poll_responses' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + participant_id = db.Column(db.String(255), nullable=False) + selected_answer_id = db.Column(db.Integer, ForeignKey('poll_answers.id', ondelete='CASCADE')) + poll_id = db.Column(db.Integer, ForeignKey('widget_polls.id', ondelete='CASCADE')) + widget_id = db.Column(db.Integer, ForeignKey('widget.id', ondelete='CASCADE')) + is_deleted = db.Column(db.Boolean, default=False) + + @classmethod + def get_responses(cls, poll_id) -> list[PollResponse]: + """Get responses for a poll.""" + return db.session.query(PollResponse).filter(PollResponse.poll_id == poll_id).all() + + @classmethod + def update_response(cls, response_id, response_data: dict) -> PollResponse: + """Update a poll response.""" + response = PollResponse.query.get(response_id) + if response: + for key, value in response_data.items(): + setattr(response, key, value) + response.save() + return response diff --git a/met-api/src/met_api/models/widget_poll.py b/met-api/src/met_api/models/widget_poll.py new file mode 100644 index 000000000..0484f2398 --- /dev/null +++ b/met-api/src/met_api/models/widget_poll.py @@ -0,0 +1,40 @@ +""" +WidgetPoll model class. + +Manages the Poll widget +""" +from __future__ import annotations + +from sqlalchemy.sql.schema import ForeignKey +from sqlalchemy import Enum + +from .base_model import BaseModel +from .db import db + + + +class Poll(BaseModel): + """Definition of the Poll entity.""" + + __tablename__ = 'widget_polls' + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + title = db.Column(db.String(255), nullable=False) + description = db.Column(db.String(2048), nullable=True) + status = db.Column(Enum('active', 'inactive', name='poll_status'), default='inactive') + widget_id = db.Column(db.Integer, ForeignKey('widget.id', ondelete='CASCADE')) + engagement_id = db.Column(db.Integer, ForeignKey('engagement.id', ondelete='CASCADE'), nullable=True) + + @classmethod + def get_polls(cls, widget_id) -> list[Poll]: + """Get polls for a widget.""" + return db.session.query(Poll).filter(Poll.widget_id == widget_id).all() + + @classmethod + def update_poll(cls, poll_id, poll_data: dict) -> Poll: + """Update a poll.""" + poll = Poll.query.get(poll_id) + if poll: + for key, value in poll_data.items(): + setattr(poll, key, value) + poll.save() + return poll From e4b3842d1353ec897266cb1b4771fd67b9afb7a0 Mon Sep 17 00:00:00 2001 From: Ratheesh kumar R Date: Wed, 17 Jan 2024 16:12:22 -0800 Subject: [PATCH 02/14] DESENG-463: Resources, Services and Schema --- met-api/src/met_api/models/poll_answers.py | 27 +++++- met-api/src/met_api/models/widget_poll.py | 45 +++++++--- met-api/src/met_api/resources/__init__.py | 2 + met-api/src/met_api/resources/widget_poll.py | 66 ++++++++++++++ met-api/src/met_api/schemas/widget_poll.py | 17 ++++ .../met_api/services/poll_answers_service.py | 60 +++++++++++++ .../met_api/services/widget_poll_service.py | 88 +++++++++++++++++++ 7 files changed, 292 insertions(+), 13 deletions(-) create mode 100644 met-api/src/met_api/resources/widget_poll.py create mode 100644 met-api/src/met_api/schemas/widget_poll.py create mode 100644 met-api/src/met_api/services/poll_answers_service.py create mode 100644 met-api/src/met_api/services/widget_poll_service.py diff --git a/met-api/src/met_api/models/poll_answers.py b/met-api/src/met_api/models/poll_answers.py index e6d0394a8..f238906b4 100644 --- a/met-api/src/met_api/models/poll_answers.py +++ b/met-api/src/met_api/models/poll_answers.py @@ -6,23 +6,25 @@ from __future__ import annotations from sqlalchemy.sql.schema import ForeignKey -from sqlalchemy import Enum from .base_model import BaseModel from .db import db + class PollAnswer(BaseModel): """Definition of the PollAnswer entity.""" __tablename__ = 'poll_answers' id = db.Column(db.Integer, primary_key=True, autoincrement=True) answer_text = db.Column(db.String(255), nullable=False) - poll_id = db.Column(db.Integer, ForeignKey('widget_polls.id', ondelete='CASCADE')) + poll_id = db.Column(db.Integer, ForeignKey('widget_polls.id', + ondelete='CASCADE')) @classmethod def get_answers(cls, poll_id) -> list[PollAnswer]: """Get answers for a poll.""" - return db.session.query(PollAnswer).filter(PollAnswer.poll_id == poll_id).all() + session = db.session.query(PollAnswer) + return session.filter(PollAnswer.poll_id == poll_id).all() @classmethod def update_answer(cls, answer_id, answer_data: dict) -> PollAnswer: @@ -33,3 +35,22 @@ def update_answer(cls, answer_id, answer_data: dict) -> PollAnswer: setattr(answer, key, value) answer.save() return answer + + @classmethod + def delete_answers_by_poll_id(cls, poll_id): + """Delete answers.""" + poll_answers = db.session.query(PollAnswer).filter( + PollAnswer.poll_id == poll_id + ) + poll_answers.delete() + db.session.commit() + + @classmethod + def bulk_insert_answers(cls, poll_id, answers): + """Bulk insert answers for a poll.""" + answer_data = [ + {'poll_id': poll_id, 'answer_text': answer['answer_text']} + for answer in answers + ] + db.session.bulk_insert_mappings(PollAnswer, answer_data) + db.session.commit() diff --git a/met-api/src/met_api/models/widget_poll.py b/met-api/src/met_api/models/widget_poll.py index 0484f2398..fe31b72ba 100644 --- a/met-api/src/met_api/models/widget_poll.py +++ b/met-api/src/met_api/models/widget_poll.py @@ -5,14 +5,15 @@ """ from __future__ import annotations -from sqlalchemy.sql.schema import ForeignKey from sqlalchemy import Enum +from sqlalchemy.sql.schema import ForeignKey + +from met_api.models.poll_answers import PollAnswer from .base_model import BaseModel from .db import db - class Poll(BaseModel): """Definition of the Poll entity.""" @@ -20,9 +21,28 @@ class Poll(BaseModel): id = db.Column(db.Integer, primary_key=True, autoincrement=True) title = db.Column(db.String(255), nullable=False) description = db.Column(db.String(2048), nullable=True) - status = db.Column(Enum('active', 'inactive', name='poll_status'), default='inactive') - widget_id = db.Column(db.Integer, ForeignKey('widget.id', ondelete='CASCADE')) - engagement_id = db.Column(db.Integer, ForeignKey('engagement.id', ondelete='CASCADE'), nullable=True) + status = db.Column( + Enum('active', 'inactive', name='poll_status'), default='inactive') + widget_id = db.Column(db.Integer, ForeignKey( + 'widget.id', ondelete='CASCADE')) + engagement_id = db.Column(db.Integer, ForeignKey( + 'engagement.id', ondelete='CASCADE'), nullable=True) + + # Relationship to timeline_event + answers = db.relationship(PollAnswer, backref='widget_poll', lazy=True) + + @classmethod + def create_poll(cls, widget_id: int, poll_data: dict) -> Poll: + """Create a new poll.""" + poll = cls() + poll.widget_id = widget_id + poll.title = poll_data.get('title') + poll.description = poll_data.get('description') + poll.status = poll_data.get('status', 'inactive') + poll.engagement_id = poll_data.get('engagement_id') + db.session.add(poll) + db.session.commit() + return poll @classmethod def get_polls(cls, widget_id) -> list[Poll]: @@ -31,10 +51,15 @@ def get_polls(cls, widget_id) -> list[Poll]: @classmethod def update_poll(cls, poll_id, poll_data: dict) -> Poll: - """Update a poll.""" - poll = Poll.query.get(poll_id) + """Update a poll and its answers.""" + poll: Poll = Poll.query.get(poll_id) if poll: - for key, value in poll_data.items(): - setattr(poll, key, value) - poll.save() + # Update poll fields + for key in ['title', 'description', 'status', 'widget_id', + 'engagement_id']: + if key in poll_data: + setattr(poll, key, poll_data[key]) + + db.session.commit() + return poll diff --git a/met-api/src/met_api/resources/__init__.py b/met-api/src/met_api/resources/__init__.py index 57db720bc..c40463a03 100644 --- a/met-api/src/met_api/resources/__init__.py +++ b/met-api/src/met_api/resources/__init__.py @@ -50,6 +50,7 @@ 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 +from .widget_poll import API as WIDGET_POLL_API __all__ = ('API_BLUEPRINT',) @@ -91,3 +92,4 @@ API.add_namespace(ENGAGEMENT_SETTINGS_API) API.add_namespace(CAC_FORM_API, path='/engagements//cacform') API.add_namespace(WIDGET_TIMELINE_API, path='/widgets//timelines') +API.add_namespace(WIDGET_POLL_API, path='/widgets//polls') diff --git a/met-api/src/met_api/resources/widget_poll.py b/met-api/src/met_api/resources/widget_poll.py new file mode 100644 index 000000000..79a930170 --- /dev/null +++ b/met-api/src/met_api/resources/widget_poll.py @@ -0,0 +1,66 @@ +"""API endpoints for managing a poll 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_poll import WidgetPollSchema +from met_api.services.widget_poll_service import WidgetPollService +from met_api.utils.util import allowedorigins, cors_preflight + + +API = Namespace('widget_polls', description='Endpoints for Poll Widget Management') + + +@cors_preflight('GET, POST') +@API.route('') +class Polls(Resource): + """Resource for managing poll widgets.""" + + @staticmethod + @cross_origin(origins=allowedorigins()) + def get(widget_id): + """Get poll widget.""" + try: + widget_poll = WidgetPollService().get_poll(widget_id) + return jsonify(WidgetPollSchema().dump(widget_poll, 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 poll widget.""" + try: + request_json = request.get_json() + 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 + + +@cors_preflight('PATCH') +@API.route('/') +class Poll(Resource): + """Resource for managing poll widgets.""" + + @staticmethod + @cross_origin(origins=allowedorigins()) + @_jwt.requires_auth + def patch(widget_id, poll_widget_id): + """Update poll widget.""" + request_json = request.get_json() + valid_format, errors = schema_utils.validate(request_json, 'poll_widget_update') + if not valid_format: + return {'message': schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST + try: + 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 + diff --git a/met-api/src/met_api/schemas/widget_poll.py b/met-api/src/met_api/schemas/widget_poll.py new file mode 100644 index 000000000..78ea13103 --- /dev/null +++ b/met-api/src/met_api/schemas/widget_poll.py @@ -0,0 +1,17 @@ +"""Schema for WidgetPoll.""" +from met_api.models.widget_poll import Poll as PollModel +from met_api.models.poll_answers import PollAnswer as PollAnswerModel +from marshmallow import Schema +from marshmallow_sqlalchemy.fields import Nested + +class PollAnswerSchema(Schema): + class Meta: + model = PollAnswerModel + fields = ('id', 'answer_text', 'poll_id') + +class WidgetPollSchema(Schema): + class Meta: + model = PollModel + fields = ('id', 'title', 'description', 'status', 'widget_id', 'engagement_id', 'answers') + + answers = Nested(PollAnswerSchema, many=True) diff --git a/met-api/src/met_api/services/poll_answers_service.py b/met-api/src/met_api/services/poll_answers_service.py new file mode 100644 index 000000000..5c9ad01ad --- /dev/null +++ b/met-api/src/met_api/services/poll_answers_service.py @@ -0,0 +1,60 @@ +"""Service for PollAnswer management.""" +from met_api.models.poll_answers import PollAnswer as PollAnswerModel +from met_api.services import authorization +from met_api.utils.roles import Role + +class PollAnswerService: + """PollAnswer management service.""" + + @staticmethod + def get_poll_answer(poll_id): + """Get poll answer by poll id.""" + poll_answer = PollAnswerModel.get_answers(poll_id) + return poll_answer + + @staticmethod + def create_poll_answer(poll_id, answer_details: dict): + """Create poll answer for a poll.""" + answer_data = dict(answer_details) + # Assume engagement_id is associated with the poll or answer for authorization + eng_id = answer_data.get('engagement_id') + authorization.check_auth(one_of_roles=(Role.EDIT_ENGAGEMENT.value), engagement_id=eng_id) + + poll_answer = PollAnswerService.create_bulk_poll_answers(poll_id, answer_data) + poll_answer.commit() + return poll_answer + + @staticmethod + def create_bulk_poll_answers(poll_id, answers_data): + """Create multiple poll answers using bulk insert.""" + if answers_data: + PollAnswerModel.bulk_insert_answers(poll_id, answers_data) + + @staticmethod + def update_poll_answer(poll_id, answer_id, answer_data: dict): + """Update poll answer.""" + poll_answer: PollAnswerModel = PollAnswerModel.query.get(answer_id) + + if not poll_answer: + raise KeyError('Answer not found') + + if poll_answer.poll_id != poll_id: + raise ValueError('Invalid poll and answer') + + for key, value in answer_data.items(): + setattr(poll_answer, key, value) + poll_answer.save() + return poll_answer + + @staticmethod + def _create_poll_answer_model(poll_id, answer_data: dict): + poll_answer_model: PollAnswerModel = PollAnswerModel() + poll_answer_model.poll_id = poll_id + poll_answer_model.answer_text = answer_data.get('answer_text') + poll_answer_model.flush() + return poll_answer_model + + @staticmethod + def delete_poll_answers(poll_id: int): + """Delete poll answers for a given poll ID.""" + PollAnswerModel.delete_answers_by_poll_id(poll_id) diff --git a/met-api/src/met_api/services/widget_poll_service.py b/met-api/src/met_api/services/widget_poll_service.py new file mode 100644 index 000000000..8cc7d1043 --- /dev/null +++ b/met-api/src/met_api/services/widget_poll_service.py @@ -0,0 +1,88 @@ +"""Service for WidgetPoll management.""" +from met_api.constants.membership_type import MembershipType +from met_api.models.widget_poll import Poll as PollModel +from met_api.services import authorization +from met_api.services.poll_answers_service import PollAnswerService +from met_api.utils.roles import Role + + +class WidgetPollService: + """WidgetPoll management service.""" + + @staticmethod + def get_poll(widget_id: int): + """Get poll by widget id.""" + widget_poll = PollModel.get_polls(widget_id) + return widget_poll + + @staticmethod + def create_poll(widget_id: int, poll_details: dict): + """Create poll for the widget.""" + poll_data = dict(poll_details) + eng_id = poll_data.get('engagement_id') + authorization.check_auth(one_of_roles=( + MembershipType.TEAM_MEMBER.name, Role.EDIT_ENGAGEMENT.value), + engagement_id=eng_id) + + widget_poll = WidgetPollService._create_poll_model( + widget_id, poll_data) + widget_poll.commit() + return widget_poll + + @staticmethod + def update_poll(widget_id: int, poll_id: int, poll_data: dict): + """Update poll widget.""" + widget_poll: PollModel = PollModel.find_by_id(poll_id) + authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name, + Role.EDIT_ENGAGEMENT.value), + engagement_id=widget_poll.engagement_id) + if not widget_poll: + raise KeyError('Poll widget not found') + + if widget_poll.widget_id != widget_id: + raise ValueError('Invalid widget ID') + + return WidgetPollService._update_poll_model(poll_id, poll_data) + + @staticmethod + def _create_poll_model(widget_id: int, poll_data: dict): + """ + Sample data format. + + { + "title": "Favorite Programming Language", + "description": "A poll to determine the most popular programming + language among our users.", + "status": "active", + "widget_id": 123, + "engagement_id": 456, + "answers": [ + { + "answer_text": "Python" + }, + { + "answer_text": "Java" + }, + { + "answer_text": "JavaScript" + }, + { + "answer_text": "C#" + } + ] + } + """ + # Create poll model object + poll_model = PollModel.create_poll(widget_id, poll_data) + + answers_data = poll_data.get('answers', []) + PollAnswerService.create_bulk_poll_answers(poll_model.id, answers_data) + poll_model.flush() + return poll_model + + @staticmethod + def _update_poll_model(poll_id: int, poll_data: dict): + PollModel.update_poll(poll_id, poll_data) + PollAnswerService.delete_poll_answers(poll_id) + answers_data = poll_data.get('answers', []) + PollAnswerService.create_bulk_poll_answers(poll_id, answers_data) From 3bd9364899a20d97eaac61478b3afffe7a7f1649 Mon Sep 17 00:00:00 2001 From: Ratheesh kumar R Date: Thu, 18 Jan 2024 17:26:23 -0800 Subject: [PATCH 03/14] DESENG-463: Wrapping up API changes --- .../08f69642b7ae_adding_widget_poll.py | 12 +-- met-api/src/met_api/models/poll_answers.py | 2 +- met-api/src/met_api/models/poll_responses.py | 11 +- met-api/src/met_api/models/widget_poll.py | 4 +- met-api/src/met_api/resources/widget_poll.py | 49 ++++++++- .../schemas/schemas/poll_response.json | 22 ++++ .../met_api/schemas/schemas/poll_widget.json | 80 ++++++++++++++ .../met_api/services/poll_response_service.py | 31 ++++++ .../met_api/services/widget_poll_service.py | 102 +++++++++++++----- met-api/src/met_api/utils/ip_util.py | 19 ++++ 10 files changed, 290 insertions(+), 42 deletions(-) create mode 100644 met-api/src/met_api/schemas/schemas/poll_response.json create mode 100644 met-api/src/met_api/schemas/schemas/poll_widget.json create mode 100644 met-api/src/met_api/services/poll_response_service.py create mode 100644 met-api/src/met_api/utils/ip_util.py diff --git a/met-api/migrations/versions/08f69642b7ae_adding_widget_poll.py b/met-api/migrations/versions/08f69642b7ae_adding_widget_poll.py index af6df74f8..4f87cbe95 100644 --- a/met-api/migrations/versions/08f69642b7ae_adding_widget_poll.py +++ b/met-api/migrations/versions/08f69642b7ae_adding_widget_poll.py @@ -25,8 +25,8 @@ def upgrade(): sa.Column('title', sa.String(length=255), nullable=False), sa.Column('description', sa.String(length=2048), nullable=True), sa.Column('status', sa.Enum('active', 'inactive', name='poll_status'), nullable=True), - sa.Column('widget_id', sa.Integer(), nullable=True), - sa.Column('engagement_id', sa.Integer(), nullable=True), + sa.Column('widget_id', sa.Integer(), nullable=False), + sa.Column('engagement_id', sa.Integer(), nullable=False), 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'), @@ -38,7 +38,7 @@ def upgrade(): sa.Column('updated_date', sa.DateTime(), nullable=True), sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('answer_text', sa.String(length=255), nullable=False), - sa.Column('poll_id', sa.Integer(), nullable=True), + sa.Column('poll_id', sa.Integer(), nullable=False), sa.Column('created_by', sa.String(length=50), nullable=True), sa.Column('updated_by', sa.String(length=50), nullable=True), sa.ForeignKeyConstraint(['poll_id'], ['widget_polls.id'], ondelete='CASCADE'), @@ -49,9 +49,9 @@ def upgrade(): sa.Column('updated_date', sa.DateTime(), nullable=True), sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('participant_id', sa.String(length=255), nullable=False), - sa.Column('selected_answer_id', sa.Integer(), nullable=True), - sa.Column('poll_id', sa.Integer(), nullable=True), - sa.Column('widget_id', sa.Integer(), nullable=True), + sa.Column('selected_answer_id', sa.Integer(), nullable=False), + sa.Column('poll_id', sa.Integer(), nullable=False), + sa.Column('widget_id', sa.Integer(), nullable=False), sa.Column('is_deleted', sa.Boolean(), nullable=True), sa.Column('created_by', sa.String(length=50), nullable=True), sa.Column('updated_by', sa.String(length=50), nullable=True), diff --git a/met-api/src/met_api/models/poll_answers.py b/met-api/src/met_api/models/poll_answers.py index f238906b4..0dcf71c8b 100644 --- a/met-api/src/met_api/models/poll_answers.py +++ b/met-api/src/met_api/models/poll_answers.py @@ -18,7 +18,7 @@ class PollAnswer(BaseModel): id = db.Column(db.Integer, primary_key=True, autoincrement=True) answer_text = db.Column(db.String(255), nullable=False) poll_id = db.Column(db.Integer, ForeignKey('widget_polls.id', - ondelete='CASCADE')) + ondelete='CASCADE'), nullable=False) @classmethod def get_answers(cls, poll_id) -> list[PollAnswer]: diff --git a/met-api/src/met_api/models/poll_responses.py b/met-api/src/met_api/models/poll_responses.py index 4c6b38316..fb7d159c7 100644 --- a/met-api/src/met_api/models/poll_responses.py +++ b/met-api/src/met_api/models/poll_responses.py @@ -17,9 +17,9 @@ class PollResponse(BaseModel): __tablename__ = 'poll_responses' id = db.Column(db.Integer, primary_key=True, autoincrement=True) participant_id = db.Column(db.String(255), nullable=False) - selected_answer_id = db.Column(db.Integer, ForeignKey('poll_answers.id', ondelete='CASCADE')) - poll_id = db.Column(db.Integer, ForeignKey('widget_polls.id', ondelete='CASCADE')) - widget_id = db.Column(db.Integer, ForeignKey('widget.id', ondelete='CASCADE')) + selected_answer_id = db.Column(db.Integer, ForeignKey('poll_answers.id', ondelete='CASCADE'), nullable=False) + poll_id = db.Column(db.Integer, ForeignKey('widget_polls.id', ondelete='CASCADE'), nullable=False) + widget_id = db.Column(db.Integer, ForeignKey('widget.id', ondelete='CASCADE'), nullable=False) is_deleted = db.Column(db.Boolean, default=False) @classmethod @@ -27,6 +27,11 @@ def get_responses(cls, poll_id) -> list[PollResponse]: """Get responses for a poll.""" return db.session.query(PollResponse).filter(PollResponse.poll_id == poll_id).all() + @classmethod + def get_responses_by_participant_id(cls, poll_id, participant_id) -> list[PollResponse]: + """Get responses for a poll.""" + return db.session.query(PollResponse).filter(PollResponse.poll_id == poll_id, PollResponse.participant_id == participant_id,PollResponse.is_deleted == False).all() + @classmethod def update_response(cls, response_id, response_data: dict) -> PollResponse: """Update a poll response.""" diff --git a/met-api/src/met_api/models/widget_poll.py b/met-api/src/met_api/models/widget_poll.py index fe31b72ba..bbc5e0b57 100644 --- a/met-api/src/met_api/models/widget_poll.py +++ b/met-api/src/met_api/models/widget_poll.py @@ -24,9 +24,9 @@ class Poll(BaseModel): status = db.Column( Enum('active', 'inactive', name='poll_status'), default='inactive') widget_id = db.Column(db.Integer, ForeignKey( - 'widget.id', ondelete='CASCADE')) + 'widget.id', ondelete='CASCADE'), nullable=False) engagement_id = db.Column(db.Integer, ForeignKey( - 'engagement.id', ondelete='CASCADE'), nullable=True) + 'engagement.id', ondelete='CASCADE'), nullable=False) # Relationship to timeline_event answers = db.relationship(PollAnswer, backref='widget_poll', lazy=True) diff --git a/met-api/src/met_api/resources/widget_poll.py b/met-api/src/met_api/resources/widget_poll.py index 79a930170..31e2b824a 100644 --- a/met-api/src/met_api/resources/widget_poll.py +++ b/met-api/src/met_api/resources/widget_poll.py @@ -11,7 +11,7 @@ from met_api.schemas.widget_poll import WidgetPollSchema from met_api.services.widget_poll_service import WidgetPollService 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') @@ -38,6 +38,9 @@ def post(widget_id): """Create poll widget.""" try: request_json = request.get_json() + valid_format, errors = schema_utils.validate(request_json, 'poll_widget') + if not valid_format: + return {'message': schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST widget_poll = WidgetPollService().create_poll(widget_id, request_json) return WidgetPollSchema().dump(widget_poll), HTTPStatus.OK except BusinessException as err: @@ -55,7 +58,7 @@ class Poll(Resource): def patch(widget_id, poll_widget_id): """Update poll widget.""" request_json = request.get_json() - valid_format, errors = schema_utils.validate(request_json, 'poll_widget_update') + valid_format, errors = schema_utils.validate(request_json, 'poll_widget') if not valid_format: return {'message': schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST try: @@ -64,3 +67,45 @@ def patch(widget_id, poll_widget_id): except BusinessException as err: return str(err), err.status_code + +@cors_preflight('POST') +@API.route('//responses') +class PollResponseRecord(Resource): + """ Resource for recording responses for a poll widget. + Not require authentication + """ + + @staticmethod + @cross_origin(origins=allowedorigins()) + def post(widget_id, poll_widget_id): + """Record a response for a given poll widget.""" + try: + response_data = request.get_json() + valid_format, errors = schema_utils.validate(response_data, 'poll_response') + if not valid_format: + return {'message': schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST + response_dict = dict(response_data) + # Ensure the poll_id and widget_id is included in the response data + response_dict['poll_id'] = poll_widget_id + response_dict['widget_id'] = widget_id + response_dict['participant_id'] = hash_ip(request.remote_addr) + + # Checking poll active or not + is_poll_active = WidgetPollService.is_poll_active(poll_widget_id) + if not is_poll_active: + return {'message': 'Poll is not active'}, HTTPStatus.BAD_REQUEST + + # Checking the poll limit exceeded or not + already_polled: bool = WidgetPollService.check_already_polled(poll_widget_id, + response_dict['participant_id'], 10) + if already_polled: + return {'message': 'Already polled'}, HTTPStatus.FORBIDDEN + + # Call the record_response method of WidgetPollService + poll_response = WidgetPollService.record_response(response_dict) + if poll_response.id: + return {'message': 'Response recorded successfully'}, HTTPStatus.CREATED + else: + return {'message': 'Response failed to record'}, HTTPStatus.INTERNAL_SERVER_ERROR + except BusinessException as err: + return err.error, err.status_code diff --git a/met-api/src/met_api/schemas/schemas/poll_response.json b/met-api/src/met_api/schemas/schemas/poll_response.json new file mode 100644 index 000000000..b2633720a --- /dev/null +++ b/met-api/src/met_api/schemas/schemas/poll_response.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://met.gov.bc.ca/.well_known/schemas/poll_response", + "type": "object", + "title": "The Poll Response Schema", + "description": "Schema for a poll response.", + "default": {}, + "examples": [ + { + "selected_answer_id": "20" + } + ], + "required": ["selected_answer_id"], + "properties": { + "selected_answer_id": { + "$id": "#/properties/selected_answer_id", + "type": "integer", + "title": "Selected Answer ID", + "description": "The ID of the selected answer in the poll." + } + } +} diff --git a/met-api/src/met_api/schemas/schemas/poll_widget.json b/met-api/src/met_api/schemas/schemas/poll_widget.json new file mode 100644 index 000000000..de94b9987 --- /dev/null +++ b/met-api/src/met_api/schemas/schemas/poll_widget.json @@ -0,0 +1,80 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://met.gov.bc.ca/.well_known/schemas/poll_widget", + "type": "object", + "title": "The root schema", + "description": "The root schema comprises the entire JSON document for a WidgetPoll.", + "default": {}, + "examples": [ + { + "title": "Favorite Programming Language", + "description": "A poll to determine the most popular programming language among our users.", + "status": "active", + "widget_id": 6, + "engagement_id": 7, + "answers": [ + { + "answer_text": "Python" + }, + { + "answer_text": "Java" + }, + { + "answer_text": "JavaScript" + }, + { + "answer_text": "C#" + } + ] + } + ], + "required": ["title", "description", "status", "widget_id", "engagement_id", "answers"], + "properties": { + "title": { + "$id": "#/properties/title", + "type": "string", + "title": "Poll title", + "description": "The title of the poll." + }, + "description": { + "$id": "#/properties/description", + "type": "string", + "title": "Poll description", + "description": "The description of the poll." + }, + "status": { + "$id": "#/properties/status", + "type": "string", + "title": "Poll status", + "description": "The status of the poll (e.g., active, closed)." + }, + "widget_id": { + "$id": "#/properties/widget_id", + "type": "integer", + "title": "Widget ID", + "description": "The unique identifier for the widget." + }, + "engagement_id": { + "$id": "#/properties/engagement_id", + "type": "integer", + "title": "Engagement ID", + "description": "The unique identifier for the engagement." + }, + "answers": { + "$id": "#/properties/answers", + "type": "array", + "title": "Poll answers", + "description": "The list of answers for the poll.", + "items": { + "type": "object", + "required": ["answer_text"], + "properties": { + "answer_text": { + "type": "string", + "description": "The text of the poll answer." + } + } + } + } + } +} diff --git a/met-api/src/met_api/services/poll_response_service.py b/met-api/src/met_api/services/poll_response_service.py new file mode 100644 index 000000000..1a431fb25 --- /dev/null +++ b/met-api/src/met_api/services/poll_response_service.py @@ -0,0 +1,31 @@ +"""Service for PollResponse management.""" +from met_api.models.poll_responses import PollResponse as PollResponseModel +from met_api.services.poll_answers_service import PollAnswerService + + +class PollResponseService: + """PollResponse management service.""" + + @staticmethod + def create_response(response_data: dict): + """Create a poll response.""" + + poll_answers = PollAnswerService.get_poll_answer(response_data['poll_id']) + + # Check if selected_answer_id exists in the returned PollAnswer objects + if not any(poll_answer.id == response_data['selected_answer_id'] for poll_answer in poll_answers): + raise ValueError('Poll is not valid for the selected answer.') + + # Create poll response object + poll_response = PollResponseModel() + for key, value in response_data.items(): + setattr(poll_response, key, value) + + poll_response.save() + return poll_response + + @staticmethod + def get_poll_count(poll_id: int, ip: str = None) -> int: + """Get poll count""" + poll_response = PollResponseModel.get_responses_by_participant_id(poll_id, ip) + return len(poll_response) diff --git a/met-api/src/met_api/services/widget_poll_service.py b/met-api/src/met_api/services/widget_poll_service.py index 8cc7d1043..ef039e3e6 100644 --- a/met-api/src/met_api/services/widget_poll_service.py +++ b/met-api/src/met_api/services/widget_poll_service.py @@ -1,20 +1,33 @@ """Service for WidgetPoll management.""" +from flask import current_app +from http import HTTPStatus from met_api.constants.membership_type import MembershipType from met_api.models.widget_poll import Poll as PollModel from met_api.services import authorization 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 +from met_api.exceptions.business_exception import BusinessException +from sqlalchemy.exc import IntegrityError class WidgetPollService: """WidgetPoll management service.""" @staticmethod - def get_poll(widget_id: int): + def get_poll_by_widget_id(widget_id: int): """Get poll by widget id.""" widget_poll = PollModel.get_polls(widget_id) return widget_poll + @staticmethod + def get_poll_by_id(poll_id: int): + """Get poll by poll id.""" + poll = PollModel.query.get(poll_id) + if not poll: + raise KeyError('Poll widget not found') + return poll + @staticmethod def create_poll(widget_id: int, poll_details: dict): """Create poll for the widget.""" @@ -30,9 +43,9 @@ def create_poll(widget_id: int, poll_details: dict): return widget_poll @staticmethod - def update_poll(widget_id: int, poll_id: int, poll_data: dict): + def update_poll(widget_id: int, poll_widget_id: int, poll_data: dict): """Update poll widget.""" - widget_poll: PollModel = PollModel.find_by_id(poll_id) + widget_poll: PollModel = PollModel.query.get(poll_widget_id) authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name, Role.EDIT_ENGAGEMENT.value), engagement_id=widget_poll.engagement_id) @@ -42,35 +55,12 @@ def update_poll(widget_id: int, poll_id: int, poll_data: dict): if widget_poll.widget_id != widget_id: raise ValueError('Invalid widget ID') - return WidgetPollService._update_poll_model(poll_id, poll_data) + return WidgetPollService._update_poll_model(poll_widget_id, poll_data) @staticmethod def _create_poll_model(widget_id: int, poll_data: dict): """ - Sample data format. - - { - "title": "Favorite Programming Language", - "description": "A poll to determine the most popular programming - language among our users.", - "status": "active", - "widget_id": 123, - "engagement_id": 456, - "answers": [ - { - "answer_text": "Python" - }, - { - "answer_text": "Java" - }, - { - "answer_text": "JavaScript" - }, - { - "answer_text": "C#" - } - ] - } + Create poll model """ # Create poll model object poll_model = PollModel.create_poll(widget_id, poll_data) @@ -86,3 +76,59 @@ def _update_poll_model(poll_id: int, poll_data: dict): PollAnswerService.delete_poll_answers(poll_id) answers_data = poll_data.get('answers', []) PollAnswerService.create_bulk_poll_answers(poll_id, answers_data) + return WidgetPollService.get_poll_by_id(poll_id) + + @staticmethod + def record_response(response_data: dict): + """Record a response for a poll.""" + try: + return PollResponseService.create_response(response_data) + except IntegrityError as exc: + current_app.logger.error(str(exc), exc) + raise BusinessException( + error="IntegrityError: Could not record the response", + status_code=HTTPStatus.BAD_REQUEST) + except ValueError as exc: + current_app.logger.error(str(exc), exc) + raise BusinessException( + error="ValueError: Selected answer is not valid for the poll", + status_code=HTTPStatus.BAD_REQUEST) + except Exception as exc: + current_app.logger.error(str(exc), exc) + raise BusinessException( + error="Could not record the response", + status_code=HTTPStatus.BAD_REQUEST) + + @staticmethod + def check_already_polled(poll_id: int, ip: str, count: int): + """Check if an ip is polled for the given poll for more than the given count. + @rtype: bool + """ + try: + poll = WidgetPollService.get_poll_by_id(poll_id) + poll_count = PollResponseService.get_poll_count(poll.id, ip) + if poll_count >= count: + return True + else: + return False + except Exception as exc: + raise BusinessException( + error=str(exc), + status_code=HTTPStatus.BAD_REQUEST) + + @staticmethod + def is_poll_active(poll_id: int) -> bool: + """Check if the poll is active or not + """ + + try: + poll = WidgetPollService.get_poll_by_id(poll_id) + if poll.status == 'active': + return True + else: + return False + except Exception as exc: + raise BusinessException( + error=str(exc), + status_code=HTTPStatus.BAD_REQUEST) + diff --git a/met-api/src/met_api/utils/ip_util.py b/met-api/src/met_api/utils/ip_util.py new file mode 100644 index 000000000..524a1af69 --- /dev/null +++ b/met-api/src/met_api/utils/ip_util.py @@ -0,0 +1,19 @@ +from hashlib import sha256 +from flask import current_app + + +def hash_ip(ip_address): + """ + Hashes the given IP address concatenated with the Flask secret key. + + Args: + ip_address (str): The IP address to be hashed. + + Returns: + str: The resulting SHA256 hash as a hexadecimal string. + """ + # Retrieve the secret key from Flask configuration with a fallback empty string + secret_key = current_app.config.get('SECRET_KEY', '') + + # Concatenate the IP address and secret key, and hash the resulting string + return sha256(f"{ip_address}{secret_key}".encode()).hexdigest() From badb4570759530153a30ca644f350d526b2bb7f7 Mon Sep 17 00:00:00 2001 From: Ratheesh kumar R Date: Thu, 18 Jan 2024 17:52:55 -0800 Subject: [PATCH 04/14] DESENG-463: Refactoring --- met-api/src/met_api/resources/widget_poll.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/met-api/src/met_api/resources/widget_poll.py b/met-api/src/met_api/resources/widget_poll.py index 31e2b824a..51fd2baec 100644 --- a/met-api/src/met_api/resources/widget_poll.py +++ b/met-api/src/met_api/resources/widget_poll.py @@ -26,7 +26,7 @@ class Polls(Resource): def get(widget_id): """Get poll widget.""" try: - widget_poll = WidgetPollService().get_poll(widget_id) + widget_poll = WidgetPollService().get_poll_by_widget_id(widget_id) return jsonify(WidgetPollSchema().dump(widget_poll, many=True)), HTTPStatus.OK except BusinessException as err: return str(err), err.status_code From 6fba5d1ab432df7098b911704581759671790e70 Mon Sep 17 00:00:00 2001 From: Ratheesh kumar R Date: Thu, 18 Jan 2024 18:32:44 -0800 Subject: [PATCH 05/14] DESENG-463: Poll API updates --- met-api/src/met_api/resources/widget_poll.py | 96 ++++++++++++------- .../met_api/services/widget_poll_service.py | 2 +- 2 files changed, 62 insertions(+), 36 deletions(-) diff --git a/met-api/src/met_api/resources/widget_poll.py b/met-api/src/met_api/resources/widget_poll.py index 51fd2baec..1c5978b63 100644 --- a/met-api/src/met_api/resources/widget_poll.py +++ b/met-api/src/met_api/resources/widget_poll.py @@ -24,10 +24,10 @@ class Polls(Resource): @staticmethod @cross_origin(origins=allowedorigins()) def get(widget_id): - """Get poll widget.""" + """Get poll widgets.""" try: - widget_poll = WidgetPollService().get_poll_by_widget_id(widget_id) - return jsonify(WidgetPollSchema().dump(widget_poll, many=True)), HTTPStatus.OK + widget_poll = WidgetPollService().get_polls_by_widget_id(widget_id) + return WidgetPollSchema().dump(widget_poll, many=True), HTTPStatus.OK except BusinessException as err: return str(err), err.status_code @@ -38,14 +38,19 @@ def post(widget_id): """Create poll widget.""" try: request_json = request.get_json() - valid_format, errors = schema_utils.validate(request_json, 'poll_widget') + valid_format, errors = Polls.validate_response_format(request_json) if not valid_format: - return {'message': schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST + return {'message': 'Invalid response format', 'errors': errors}, 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_response_format(data): + valid_format, errors = schema_utils.validate(data, 'poll_widget') + return valid_format, errors + @cors_preflight('PATCH') @API.route('/') @@ -57,23 +62,27 @@ class Poll(Resource): @_jwt.requires_auth def patch(widget_id, poll_widget_id): """Update poll widget.""" - request_json = request.get_json() - valid_format, errors = schema_utils.validate(request_json, 'poll_widget') - if not valid_format: - return {'message': schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST + try: + request_json = request.get_json() + valid_format, errors = Poll.validate_response_format(request_json) + if not valid_format: + return {'message': 'Invalid response format', 'errors': errors}, 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_response_format(data): + valid_format, errors = schema_utils.validate(data, 'poll_widget') + return valid_format, errors @cors_preflight('POST') @API.route('//responses') class PollResponseRecord(Resource): - """ Resource for recording responses for a poll widget. - Not require authentication - """ + """Resource for recording responses for a poll widget. Not require authentication.""" @staticmethod @cross_origin(origins=allowedorigins()) @@ -81,31 +90,48 @@ def post(widget_id, poll_widget_id): """Record a response for a given poll widget.""" try: response_data = request.get_json() - valid_format, errors = schema_utils.validate(response_data, 'poll_response') + valid_format, errors = PollResponseRecord.validate_response_format(response_data) if not valid_format: - return {'message': schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST - response_dict = dict(response_data) - # Ensure the poll_id and widget_id is included in the response data - response_dict['poll_id'] = poll_widget_id - response_dict['widget_id'] = widget_id - response_dict['participant_id'] = hash_ip(request.remote_addr) - - # Checking poll active or not - is_poll_active = WidgetPollService.is_poll_active(poll_widget_id) - if not is_poll_active: + return {'message': 'Invalid response format', '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 - # Checking the poll limit exceeded or not - already_polled: bool = WidgetPollService.check_already_polled(poll_widget_id, - response_dict['participant_id'], 10) - if already_polled: - return {'message': 'Already polled'}, HTTPStatus.FORBIDDEN - - # Call the record_response method of WidgetPollService - poll_response = WidgetPollService.record_response(response_dict) - if poll_response.id: - return {'message': 'Response recorded successfully'}, HTTPStatus.CREATED - else: - return {'message': 'Response failed to record'}, HTTPStatus.INTERNAL_SERVER_ERROR + 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) + except BusinessException as err: return err.error, err.status_code + + @staticmethod + def validate_response_format(data): + valid_format, errors = schema_utils.validate(data, 'poll_response') + return valid_format, errors + + @staticmethod + def prepare_response_data(data, widget_id, poll_widget_id): + 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_widget_id): + return WidgetPollService.is_poll_active(poll_widget_id) + + @staticmethod + def is_poll_limit_exceeded(poll_widget_id, participant_id): + return WidgetPollService.check_already_polled(poll_widget_id, participant_id, 10) + + @staticmethod + def record_poll_response(response_dict): + poll_response = WidgetPollService.record_response(response_dict) + if poll_response.id: + return {'message': 'Response recorded successfully'}, HTTPStatus.CREATED + else: + return {'message': 'Response failed to record'}, HTTPStatus.INTERNAL_SERVER_ERROR diff --git a/met-api/src/met_api/services/widget_poll_service.py b/met-api/src/met_api/services/widget_poll_service.py index ef039e3e6..1f2dce113 100644 --- a/met-api/src/met_api/services/widget_poll_service.py +++ b/met-api/src/met_api/services/widget_poll_service.py @@ -15,7 +15,7 @@ class WidgetPollService: """WidgetPoll management service.""" @staticmethod - def get_poll_by_widget_id(widget_id: int): + def get_polls_by_widget_id(widget_id: int): """Get poll by widget id.""" widget_poll = PollModel.get_polls(widget_id) return widget_poll From 35aa0ef906fd1fd651804f8a62a30b0f5c6f7ae9 Mon Sep 17 00:00:00 2001 From: Ratheesh kumar R Date: Mon, 22 Jan 2024 11:21:58 -0800 Subject: [PATCH 06/14] DESENG-463 : Updated logics --- met-api/src/met_api/resources/widget_poll.py | 16 +- .../schemas/schemas/poll_widget_update.json | 80 ++++++++++ .../met_api/services/poll_answers_service.py | 47 ++---- .../met_api/services/poll_response_service.py | 53 ++++--- .../met_api/services/widget_poll_service.py | 147 +++++++----------- 5 files changed, 189 insertions(+), 154 deletions(-) create mode 100644 met-api/src/met_api/schemas/schemas/poll_widget_update.json diff --git a/met-api/src/met_api/resources/widget_poll.py b/met-api/src/met_api/resources/widget_poll.py index 1c5978b63..f7f2f10cc 100644 --- a/met-api/src/met_api/resources/widget_poll.py +++ b/met-api/src/met_api/resources/widget_poll.py @@ -49,6 +49,8 @@ def post(widget_id): @staticmethod def validate_response_format(data): valid_format, errors = schema_utils.validate(data, 'poll_widget') + if not valid_format: + errors = schema_utils.serialize(errors) return valid_format, errors @@ -76,7 +78,9 @@ def patch(widget_id, poll_widget_id): @staticmethod def validate_response_format(data): - valid_format, errors = schema_utils.validate(data, 'poll_widget') + 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') @@ -110,6 +114,8 @@ def post(widget_id, poll_widget_id): @staticmethod def validate_response_format(data): valid_format, errors = schema_utils.validate(data, 'poll_response') + if not valid_format: + errors = schema_utils.serialize(errors) return valid_format, errors @staticmethod @@ -121,12 +127,12 @@ def prepare_response_data(data, widget_id, poll_widget_id): return response_dict @staticmethod - def is_poll_active(poll_widget_id): - return WidgetPollService.is_poll_active(poll_widget_id) + def is_poll_active(poll_id): + return WidgetPollService.is_poll_active(poll_id) @staticmethod - def is_poll_limit_exceeded(poll_widget_id, participant_id): - return WidgetPollService.check_already_polled(poll_widget_id, participant_id, 10) + def is_poll_limit_exceeded(poll_id, participant_id): + return WidgetPollService.check_already_polled(poll_id, participant_id, 10) @staticmethod def record_poll_response(response_dict): diff --git a/met-api/src/met_api/schemas/schemas/poll_widget_update.json b/met-api/src/met_api/schemas/schemas/poll_widget_update.json new file mode 100644 index 000000000..780223bde --- /dev/null +++ b/met-api/src/met_api/schemas/schemas/poll_widget_update.json @@ -0,0 +1,80 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://met.gov.bc.ca/.well_known/schemas/poll_widget_update", + "type": "object", + "title": "The root schema", + "description": "The root schema comprises the entire JSON document for a WidgetPoll.", + "default": {}, + "examples": [ + { + "title": "Favorite Programming Language", + "description": "A poll to determine the most popular programming language among our users.", + "status": "active", + "widget_id": 6, + "engagement_id": 7, + "answers": [ + { + "answer_text": "Python" + }, + { + "answer_text": "Java" + }, + { + "answer_text": "JavaScript" + }, + { + "answer_text": "C#" + } + ] + } + ], + "required": ["widget_id", "engagement_id"], + "properties": { + "title": { + "$id": "#/properties/title", + "type": "string", + "title": "Poll title", + "description": "The title of the poll." + }, + "description": { + "$id": "#/properties/description", + "type": "string", + "title": "Poll description", + "description": "The description of the poll." + }, + "status": { + "$id": "#/properties/status", + "type": "string", + "title": "Poll status", + "description": "The status of the poll (e.g., active, closed)." + }, + "widget_id": { + "$id": "#/properties/widget_id", + "type": "integer", + "title": "Widget ID", + "description": "The unique identifier for the widget." + }, + "engagement_id": { + "$id": "#/properties/engagement_id", + "type": "integer", + "title": "Engagement ID", + "description": "The unique identifier for the engagement." + }, + "answers": { + "$id": "#/properties/answers", + "type": "array", + "title": "Poll answers", + "description": "The list of answers for the poll.", + "items": { + "type": "object", + "required": ["answer_text"], + "properties": { + "answer_text": { + "type": "string", + "description": "The text of the poll answer." + } + } + } + } + } +} diff --git a/met-api/src/met_api/services/poll_answers_service.py b/met-api/src/met_api/services/poll_answers_service.py index 5c9ad01ad..997f628e9 100644 --- a/met-api/src/met_api/services/poll_answers_service.py +++ b/met-api/src/met_api/services/poll_answers_service.py @@ -2,6 +2,8 @@ from met_api.models.poll_answers import PollAnswer as PollAnswerModel from met_api.services import authorization from met_api.utils.roles import Role +from met_api.exceptions.business_exception import BusinessException +from http import HTTPStatus class PollAnswerService: """PollAnswer management service.""" @@ -12,48 +14,17 @@ def get_poll_answer(poll_id): poll_answer = PollAnswerModel.get_answers(poll_id) return poll_answer - @staticmethod - def create_poll_answer(poll_id, answer_details: dict): - """Create poll answer for a poll.""" - answer_data = dict(answer_details) - # Assume engagement_id is associated with the poll or answer for authorization - eng_id = answer_data.get('engagement_id') - authorization.check_auth(one_of_roles=(Role.EDIT_ENGAGEMENT.value), engagement_id=eng_id) - - poll_answer = PollAnswerService.create_bulk_poll_answers(poll_id, answer_data) - poll_answer.commit() - return poll_answer @staticmethod - def create_bulk_poll_answers(poll_id, answers_data): - """Create multiple poll answers using bulk insert.""" - if answers_data: - PollAnswerModel.bulk_insert_answers(poll_id, answers_data) - - @staticmethod - def update_poll_answer(poll_id, answer_id, answer_data: dict): - """Update poll answer.""" - poll_answer: PollAnswerModel = PollAnswerModel.query.get(answer_id) + def create_bulk_poll_answers(poll_id: int, answers_data: list): + """Bulk insert of poll answers.""" + try: + if len(answers_data) > 0: + PollAnswerModel.bulk_insert_answers(poll_id, answers_data) + except Exception as e: + raise BusinessException(str(e), HTTPStatus.INTERNAL_SERVER_ERROR) - if not poll_answer: - raise KeyError('Answer not found') - if poll_answer.poll_id != poll_id: - raise ValueError('Invalid poll and answer') - - for key, value in answer_data.items(): - setattr(poll_answer, key, value) - poll_answer.save() - return poll_answer - - @staticmethod - def _create_poll_answer_model(poll_id, answer_data: dict): - poll_answer_model: PollAnswerModel = PollAnswerModel() - poll_answer_model.poll_id = poll_id - poll_answer_model.answer_text = answer_data.get('answer_text') - poll_answer_model.flush() - return poll_answer_model - @staticmethod def delete_poll_answers(poll_id: int): """Delete poll answers for a given poll ID.""" diff --git a/met-api/src/met_api/services/poll_response_service.py b/met-api/src/met_api/services/poll_response_service.py index 1a431fb25..9fad3af35 100644 --- a/met-api/src/met_api/services/poll_response_service.py +++ b/met-api/src/met_api/services/poll_response_service.py @@ -1,31 +1,42 @@ -"""Service for PollResponse management.""" from met_api.models.poll_responses import PollResponse as PollResponseModel from met_api.services.poll_answers_service import PollAnswerService class PollResponseService: - """PollResponse management service.""" + """Service for managing PollResponses.""" @staticmethod - def create_response(response_data: dict): - """Create a poll response.""" - - poll_answers = PollAnswerService.get_poll_answer(response_data['poll_id']) - - # Check if selected_answer_id exists in the returned PollAnswer objects - if not any(poll_answer.id == response_data['selected_answer_id'] for poll_answer in poll_answers): - raise ValueError('Poll is not valid for the selected answer.') - - # Create poll response object - poll_response = PollResponseModel() - for key, value in response_data.items(): - setattr(poll_response, key, value) - - poll_response.save() - return poll_response + def create_response(response_data: dict) -> PollResponseModel: + """ + Create a poll response. + Raises ValueError if the selected answer is not valid for the poll. + """ + try: + poll_id = response_data.get('poll_id') + selected_answer_id = response_data.get('selected_answer_id') + + # Validate if the poll and answer are valid + valid_answers = PollAnswerService.get_poll_answer(poll_id) + if not any(answer.id == selected_answer_id for answer in valid_answers): + raise ValueError('Invalid selected answer for the poll.') + + # Create and save the poll response + poll_response = PollResponseModel(**response_data) + poll_response.save() + return poll_response + except Exception as e: + # Log the exception or handle it as needed + raise ValueError(f'Error creating poll response: {e}') @staticmethod def get_poll_count(poll_id: int, ip: str = None) -> int: - """Get poll count""" - poll_response = PollResponseModel.get_responses_by_participant_id(poll_id, ip) - return len(poll_response) + """ + Get the count of responses for a given poll. + Optionally filters by participant IP. + """ + try: + responses = PollResponseModel.get_responses_by_participant_id(poll_id, ip) + return len(responses) + except Exception as e: + # Log the exception or handle it as needed + raise ValueError(f'Error retrieving poll count: {e}') diff --git a/met-api/src/met_api/services/widget_poll_service.py b/met-api/src/met_api/services/widget_poll_service.py index 1f2dce113..69e0b3ffd 100644 --- a/met-api/src/met_api/services/widget_poll_service.py +++ b/met-api/src/met_api/services/widget_poll_service.py @@ -1,134 +1,101 @@ -"""Service for WidgetPoll management.""" -from flask import current_app -from http import HTTPStatus -from met_api.constants.membership_type import MembershipType from met_api.models.widget_poll import Poll as PollModel from met_api.services import authorization from met_api.services.poll_answers_service import PollAnswerService -from met_api.services.poll_response_service import PollResponseService +from met_api.constants.membership_type import MembershipType from met_api.utils.roles import Role +from met_api.services.poll_response_service import PollResponseService from met_api.exceptions.business_exception import BusinessException -from sqlalchemy.exc import IntegrityError - +from http import HTTPStatus class WidgetPollService: - """WidgetPoll management service.""" + """Service for managing WidgetPolls.""" @staticmethod def get_polls_by_widget_id(widget_id: int): - """Get poll by widget id.""" - widget_poll = PollModel.get_polls(widget_id) - return widget_poll + """Get polls by widget ID.""" + return PollModel.get_polls(widget_id) @staticmethod def get_poll_by_id(poll_id: int): - """Get poll by poll id.""" + """Get poll by poll ID.""" poll = PollModel.query.get(poll_id) if not poll: - raise KeyError('Poll widget not found') + raise BusinessException('Poll widget not found', HTTPStatus.NOT_FOUND) return poll @staticmethod def create_poll(widget_id: int, poll_details: dict): """Create poll for the widget.""" - poll_data = dict(poll_details) - eng_id = poll_data.get('engagement_id') - authorization.check_auth(one_of_roles=( - MembershipType.TEAM_MEMBER.name, Role.EDIT_ENGAGEMENT.value), - engagement_id=eng_id) - - widget_poll = WidgetPollService._create_poll_model( - widget_id, poll_data) - widget_poll.commit() - return widget_poll + try: + eng_id = poll_details.get('engagement_id') + WidgetPollService._check_authorization(eng_id) + return WidgetPollService._create_poll_model(widget_id, poll_details) + except Exception as exc: + raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) @staticmethod def update_poll(widget_id: int, poll_widget_id: int, poll_data: dict): """Update poll widget.""" - widget_poll: PollModel = PollModel.query.get(poll_widget_id) - authorization.check_auth(one_of_roles=(MembershipType.TEAM_MEMBER.name, - Role.EDIT_ENGAGEMENT.value), - engagement_id=widget_poll.engagement_id) - if not widget_poll: - raise KeyError('Poll widget not found') - - if widget_poll.widget_id != widget_id: - raise ValueError('Invalid widget ID') - - return WidgetPollService._update_poll_model(poll_widget_id, poll_data) + try: + widget_poll = WidgetPollService.get_poll_by_id(poll_widget_id) + WidgetPollService._check_authorization(widget_poll.engagement_id) - @staticmethod - def _create_poll_model(widget_id: int, poll_data: dict): - """ - Create poll model - """ - # Create poll model object - poll_model = PollModel.create_poll(widget_id, poll_data) + if widget_poll.widget_id != widget_id: + raise BusinessException('Invalid widget ID', HTTPStatus.BAD_REQUEST) - answers_data = poll_data.get('answers', []) - PollAnswerService.create_bulk_poll_answers(poll_model.id, answers_data) - poll_model.flush() - return poll_model - - @staticmethod - def _update_poll_model(poll_id: int, poll_data: dict): - PollModel.update_poll(poll_id, poll_data) - PollAnswerService.delete_poll_answers(poll_id) - answers_data = poll_data.get('answers', []) - PollAnswerService.create_bulk_poll_answers(poll_id, answers_data) - return WidgetPollService.get_poll_by_id(poll_id) + return WidgetPollService._update_poll_model(poll_widget_id, poll_data) + except Exception as exc: + raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) @staticmethod def record_response(response_data: dict): """Record a response for a poll.""" try: return PollResponseService.create_response(response_data) - except IntegrityError as exc: - current_app.logger.error(str(exc), exc) - raise BusinessException( - error="IntegrityError: Could not record the response", - status_code=HTTPStatus.BAD_REQUEST) - except ValueError as exc: - current_app.logger.error(str(exc), exc) - raise BusinessException( - error="ValueError: Selected answer is not valid for the poll", - status_code=HTTPStatus.BAD_REQUEST) except Exception as exc: - current_app.logger.error(str(exc), exc) - raise BusinessException( - error="Could not record the response", - status_code=HTTPStatus.BAD_REQUEST) + raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) @staticmethod - def check_already_polled(poll_id: int, ip: str, count: int): - """Check if an ip is polled for the given poll for more than the given count. - @rtype: bool - """ + def check_already_polled(poll_id: int, ip: str, count: int) -> bool: + """Check if an IP has already polled more than the given count.""" try: - poll = WidgetPollService.get_poll_by_id(poll_id) - poll_count = PollResponseService.get_poll_count(poll.id, ip) - if poll_count >= count: - return True - else: - return False + poll_count = PollResponseService.get_poll_count(poll_id, ip) + return poll_count >= count except Exception as exc: - raise BusinessException( - error=str(exc), - status_code=HTTPStatus.BAD_REQUEST) + raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) @staticmethod def is_poll_active(poll_id: int) -> bool: - """Check if the poll is active or not - """ - + """Check if the poll is active.""" try: poll = WidgetPollService.get_poll_by_id(poll_id) - if poll.status == 'active': - return True - else: - return False + return poll.status == 'active' except Exception as exc: - raise BusinessException( - error=str(exc), - status_code=HTTPStatus.BAD_REQUEST) + raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) + + @staticmethod + def _create_poll_model(widget_id: int, poll_data: dict): + """Private method to create poll model.""" + poll_model = PollModel.create_poll(widget_id, poll_data) + WidgetPollService._handle_poll_answers(poll_model.id, poll_data) + return poll_model + @staticmethod + def _update_poll_model(poll_id: int, poll_data: dict): + """Private method to update poll model.""" + PollModel.update_poll(poll_id, poll_data) + WidgetPollService._handle_poll_answers(poll_id, poll_data) + return WidgetPollService.get_poll_by_id(poll_id) + + @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) + + @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) From 86a3f648f7e42198f0fc4805fe9380c2210c6f5d Mon Sep 17 00:00:00 2001 From: Ratheesh kumar R Date: Mon, 22 Jan 2024 11:22:25 -0800 Subject: [PATCH 07/14] DESENG-463 : Written unit test cases --- met-api/tests/unit/api/test_widget_poll.py | 192 ++++++++++++++++++ met-api/tests/unit/models/test_poll_answer.py | 76 +++++++ .../tests/unit/models/test_poll_response.py | 74 +++++++ met-api/tests/unit/models/test_widget_poll.py | 63 ++++++ .../services/test_poll_answers_service.py | 72 +++++++ .../unit/services/test_widget_poll_service.py | 170 ++++++++++++++++ met-api/tests/utilities/factory_scenarios.py | 62 ++++++ met-api/tests/utilities/factory_utils.py | 37 +++- 8 files changed, 745 insertions(+), 1 deletion(-) create mode 100644 met-api/tests/unit/api/test_widget_poll.py create mode 100644 met-api/tests/unit/models/test_poll_answer.py create mode 100644 met-api/tests/unit/models/test_poll_response.py create mode 100644 met-api/tests/unit/models/test_widget_poll.py create mode 100644 met-api/tests/unit/services/test_poll_answers_service.py create mode 100644 met-api/tests/unit/services/test_widget_poll_service.py diff --git a/met-api/tests/unit/api/test_widget_poll.py b/met-api/tests/unit/api/test_widget_poll.py new file mode 100644 index 000000000..46b88dd6d --- /dev/null +++ b/met-api/tests/unit/api/test_widget_poll.py @@ -0,0 +1,192 @@ +# 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. + +"""Tests to verify the Widget Subscribe API end-point. + +Test-Suite to ensure that the Widget Subscribe API endpoint +is working as expected. +""" +import json +from http import HTTPStatus + +from faker import Faker +from tests.utilities.factory_scenarios import TestJwtClaims, TestWidgetPollInfo, TestPollAnswerInfo +from tests.utilities.factory_utils import factory_auth_header, factory_engagement_model, factory_widget_model, \ + factory_poll_model, factory_poll_answer_model + +from met_api.utils.enums import ContentType + +fake = Faker() + + +def test_get_widget(client, jwt, session): + """Assert that a get API endpoint is working as expected""" + # Test setup: create a poll widget and a response model + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.no_role) + engagement = factory_engagement_model() + widget = factory_widget_model({'engagement_id': engagement.id}) + poll = factory_poll_model(widget, TestWidgetPollInfo.poll1) + answer = factory_poll_answer_model(poll, TestPollAnswerInfo.answer1) + + # Sending POST request + rv = client.get( + f'/api/widgets/{widget.id}/polls', + headers=headers, + content_type=ContentType.JSON.value, + ) + + # Checking response + assert rv.status_code == HTTPStatus.OK + json_data = rv.json + assert len(json_data) > 0 + assert json_data[0]['title'] == poll.title + assert json_data[0]['answers'][0]['answer_text'] == answer.answer_text + + + +def test_create_poll_widget(client, jwt, session, setup_admin_user_and_claims): + """Assert that a poll widget can be POSTed.""" + # Test setup: create a poll widget model + + user, claims = setup_admin_user_and_claims + headers = factory_auth_header(jwt=jwt, claims=claims) + + engagement = factory_engagement_model() + widget = factory_widget_model({'engagement_id': engagement.id}) + + poll_info = { + 'title': TestWidgetPollInfo.poll1.get('title'), + 'description': TestWidgetPollInfo.poll1.get('description'), + 'engagement_id': engagement.id, + 'status': TestWidgetPollInfo.poll1.get('status'), + 'widget_id': widget.id, + 'answers': [ + TestPollAnswerInfo.answer1.value, + TestPollAnswerInfo.answer2.value, + TestPollAnswerInfo.answer3.value + ] + } + + # Preparing data for POST request + data = { + **poll_info, + } + + # Sending POST request + rv = client.post( + f'/api/widgets/{widget.id}/polls', + data=json.dumps(data), + headers=headers, + content_type=ContentType.JSON.value, + ) + + # Checking response + assert rv.status_code == HTTPStatus.OK + assert rv.json.get('title') == poll_info.get('title') + + # testing Exceptions with wrong widget_id + + # Sending POST request + rv = client.post( + f'/api/widgets/100/polls', + data=json.dumps(data), + headers=headers, + content_type=ContentType.JSON.value, + ) + + assert rv.status_code == HTTPStatus.BAD_REQUEST + + +def test_update_poll_widget(client, jwt, session, setup_admin_user_and_claims): + """Assert that a poll widget can be PATCHed.""" + # Test setup: create and post a poll widget model + user, claims = setup_admin_user_and_claims + headers = factory_auth_header(jwt=jwt, claims=claims) + + engagement = factory_engagement_model() + widget = factory_widget_model({'engagement_id': engagement.id}) + poll = factory_poll_model(widget, TestWidgetPollInfo.poll1) + + # Preparing data for PATCH request + data = { + 'title': 'Updated Title', + 'engagement_id': engagement.id, + 'widget_id': widget.id, + 'answers': [TestPollAnswerInfo.answer3.value] + } + + # Sending PATCH request + rv = client.patch( + f'/api/widgets/{widget.id}/polls/{poll.id}', + data=json.dumps(data), + headers=headers, + content_type=ContentType.JSON.value, + ) + + # Checking response + assert rv.status_code == HTTPStatus.OK + assert rv.json.get('title') == data.get('title') + assert len(rv.json.get('answers')) == 1 + + # testing Exceptions with wrong data + # Sending patch request + rv = client.patch( + f'/api/widgets/{widget.id}/polls/{poll.id}', + data=json.dumps({'title_wrong_key': 'Updated'}), + headers=headers, + content_type=ContentType.JSON.value, + ) + + assert rv.status_code == HTTPStatus.BAD_REQUEST + + +def test_record_poll_response(client, jwt, session): + """Assert that a response for a poll widget can be POSTed.""" + # Test setup: create a poll widget and a response model + headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.no_role) + engagement = factory_engagement_model() + widget = factory_widget_model({'engagement_id': engagement.id}) + poll = factory_poll_model(widget, TestWidgetPollInfo.poll1) + answer = factory_poll_answer_model(poll, TestPollAnswerInfo.answer1) + + # Preparing data for poll response + data = { + 'selected_answer_id': answer.id, + } + + # Sending POST request + rv = client.post( + f'/api/widgets/{widget.id}/polls/{poll.id}/responses', + data=json.dumps(data), + headers=headers, + content_type=ContentType.JSON.value, + ) + + # Checking response + assert rv.status_code == HTTPStatus.CREATED + assert rv.json.get('message') == 'Response recorded successfully' + + data = { + 'selected_answer_wrong_key': answer.id, + } + + # Sending POST request + rv = client.post( + f'/api/widgets/{widget.id}/polls/{poll.id}/responses', + data=json.dumps(data), + headers=headers, + content_type=ContentType.JSON.value, + ) + + assert rv.status_code == HTTPStatus.BAD_REQUEST diff --git a/met-api/tests/unit/models/test_poll_answer.py b/met-api/tests/unit/models/test_poll_answer.py new file mode 100644 index 000000000..ad849fe70 --- /dev/null +++ b/met-api/tests/unit/models/test_poll_answer.py @@ -0,0 +1,76 @@ +# 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. +"""Tests for the Org model. + +Test suite to ensure that the Engagement model routines are working as expected. +""" + +from tests.utilities.factory_scenarios import TestPollAnswerInfo +from tests.utilities.factory_utils import factory_poll_model, factory_poll_answer_model, factory_engagement_model, \ + factory_widget_model + +from met_api.models.poll_answers import PollAnswer + + +def test_get_answers(session): + """Assert that answers for a poll can be fetched.""" + poll = _create_poll() + answer1 = factory_poll_answer_model(poll, TestPollAnswerInfo.answer1) + answer2 = factory_poll_answer_model(poll, TestPollAnswerInfo.answer2) + session.commit() + answers = PollAnswer.get_answers(poll.id) + assert len(answers) == 2 + + +def test_update_answer(session): + """Assert that an answer can be updated.""" + poll = _create_poll() + answer = factory_poll_answer_model(poll, TestPollAnswerInfo.answer1) + session.commit() + updated_text = 'Updated Answer' + updated_answer = PollAnswer.update_answer(answer.id, {'answer_text': updated_text}) + assert updated_answer.answer_text == updated_text + + +def test_delete_answers_by_poll_id(session): + """Assert that answers for a poll can be deleted.""" + poll = _create_poll() + factory_poll_answer_model(poll, TestPollAnswerInfo.answer1) + factory_poll_answer_model(poll, TestPollAnswerInfo.answer2) + session.commit() + PollAnswer.delete_answers_by_poll_id(poll.id) + answers = PollAnswer.get_answers(poll.id) + assert len(answers) == 0 + + +def test_bulk_insert_answers(session): + """Assert that answers can be bulk inserted for a poll.""" + poll = _create_poll() + answers_data = [{'answer_text': 'Answer 1'}, {'answer_text': 'Answer 2'}] + PollAnswer.bulk_insert_answers(poll.id, answers_data) + answers = PollAnswer.get_answers(poll.id) + assert len(answers) == 2 + + +def _create_poll(): + """Helper function to create a poll for testing.""" + widget = _create_widget() + return factory_poll_model(widget, {'title': 'Sample Poll', 'engagement_id': widget.engagement_id}) + + +def _create_widget(): + """Helper function to create a widget for testing.""" + engagement = factory_engagement_model() + widget = factory_widget_model({'engagement_id': engagement.id}) + return widget diff --git a/met-api/tests/unit/models/test_poll_response.py b/met-api/tests/unit/models/test_poll_response.py new file mode 100644 index 000000000..670cb7f31 --- /dev/null +++ b/met-api/tests/unit/models/test_poll_response.py @@ -0,0 +1,74 @@ +# 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. +"""Tests for the Org model. + +Test suite to ensure that the Engagement model routines are working as expected. +""" + +from tests.utilities.factory_scenarios import TestPollResponseInfo, TestPollAnswerInfo +from tests.utilities.factory_utils import factory_poll_response_model, factory_poll_model, factory_poll_answer_model, \ + factory_engagement_model, factory_widget_model + +from met_api.models.poll_responses import PollResponse + + +def test_get_responses(session): + """Assert that responses for a poll can be fetched.""" + poll, answer = _create_poll_answer() + poll_response1 = factory_poll_response_model(poll, answer, TestPollResponseInfo.response1) + poll_response2 = factory_poll_response_model(poll, answer, TestPollResponseInfo.response2) + session.commit() + responses = PollResponse.get_responses(poll.id) + assert len(responses) == 2 + + +def test_get_responses_by_participant_id(session): + """Assert that responses for a poll by a specific participant can be fetched.""" + poll, answer = _create_poll_answer() + poll_response1 = factory_poll_response_model(poll, answer, TestPollResponseInfo.response1) + + session.commit() + responses = PollResponse.get_responses_by_participant_id(poll.id, poll_response1.participant_id) + assert len(responses) > 0 + + +def test_update_or_delete_response(session): + """Assert that a poll response can be updated.""" + poll, answer = _create_poll_answer() + poll_response1 = factory_poll_response_model(poll, answer, TestPollResponseInfo.response1) + session.commit() + updated_data = {'is_deleted': True} + updated_response = PollResponse.update_response(poll_response1.id, updated_data) + assert updated_response.is_deleted == True + + +def _create_poll(): + """Helper function to create a poll for testing.""" + widget = _create_widget() + return factory_poll_model(widget, {'title': 'Sample Poll', 'engagement_id': widget.engagement_id}) + + +def _create_widget(): + """Helper function to create a widget for testing.""" + engagement = factory_engagement_model() + widget = factory_widget_model({'engagement_id': engagement.id}) + return widget + + +def _create_poll_answer(): + """Helper function to create a widget for testing.""" + poll = _create_poll() + answer = factory_poll_answer_model(poll, TestPollAnswerInfo.answer1) + return poll, answer +# Additional relevant tests can be added here diff --git a/met-api/tests/unit/models/test_widget_poll.py b/met-api/tests/unit/models/test_widget_poll.py new file mode 100644 index 000000000..478e64ab9 --- /dev/null +++ b/met-api/tests/unit/models/test_widget_poll.py @@ -0,0 +1,63 @@ +# 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. +"""Tests for the Org model. + +Test suite to ensure that the Engagement model routines are working as expected. +""" + +from faker import Faker +from tests.utilities.factory_scenarios import TestWidgetPollInfo +from tests.utilities.factory_utils import factory_widget_model, factory_engagement_model, factory_poll_model + +from met_api.models.widget_poll import Poll + +fake = Faker() + + +def test_create_poll(session): + """Assert that a poll can be created.""" + widget = _create_widget() + poll = factory_poll_model(widget, TestWidgetPollInfo.poll1) + session.commit() + assert poll.id is not None + assert poll.title == TestWidgetPollInfo.poll1['title'] + + +def test_get_polls_by_widget_id(session): + """Assert that polls for a widget can be fetched.""" + widget = _create_widget() + poll1 = factory_poll_model(widget, TestWidgetPollInfo.poll1) + poll2 = factory_poll_model(widget, TestWidgetPollInfo.poll2) + session.commit() + polls = Poll.get_polls(widget.id) + assert len(polls) == 2 + + +def test_update_poll(session): + """Assert that a poll can be updated.""" + widget = _create_widget() + poll = factory_poll_model(widget, TestWidgetPollInfo.poll1) + session.commit() + updated_title = 'Updated Title' + updated_poll = Poll.update_poll(poll.id, {'title': updated_title}) + assert updated_poll.title == updated_title + + +# Additional relevant tests can be added here + +def _create_widget(): + """Helper function to create a widget for testing.""" + engagement = factory_engagement_model() + widget = factory_widget_model({'engagement_id': engagement.id}) + return widget diff --git a/met-api/tests/unit/services/test_poll_answers_service.py b/met-api/tests/unit/services/test_poll_answers_service.py new file mode 100644 index 000000000..ff007606c --- /dev/null +++ b/met-api/tests/unit/services/test_poll_answers_service.py @@ -0,0 +1,72 @@ +# 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. +"""Tests for the document widget service. + +Test suite to ensure that the document widget service routines are working as expected. +""" + +from http import HTTPStatus + +import pytest +from tests.utilities.factory_scenarios import TestPollAnswerInfo, TestWidgetPollInfo +from tests.utilities.factory_utils import factory_poll_model, factory_poll_answer_model, factory_widget_model, \ + factory_engagement_model + +from met_api.exceptions.business_exception import BusinessException +from met_api.services.poll_answers_service import PollAnswerService + + +def test_get_poll_answer(session): + """Assert that poll answers can be fetched by poll ID.""" + widget = _create_widget() + poll = factory_poll_model(widget, TestWidgetPollInfo.poll1) + factory_poll_answer_model(poll, TestPollAnswerInfo.answer1) + session.commit() + poll_answers = PollAnswerService.get_poll_answer(poll.id) + assert len(poll_answers) > 0 + + +def test_delete_poll_answers(session): + """Assert that poll answers can be deleted for a given poll ID.""" + widget = _create_widget() + poll = factory_poll_model(widget, TestWidgetPollInfo.poll1) + factory_poll_answer_model(poll, TestPollAnswerInfo.answer1) + session.commit() + PollAnswerService.delete_poll_answers(poll.id) + poll_answers = PollAnswerService.get_poll_answer(poll.id) + assert len(poll_answers) == 0 + + +def test_create_bulk_poll_answers(session): + """Assert that poll answers can be deleted for a given poll ID.""" + widget = _create_widget() + poll = factory_poll_model(widget, TestWidgetPollInfo.poll1) + session.commit() + answers_data = [TestPollAnswerInfo.answer1, TestPollAnswerInfo.answer2, TestPollAnswerInfo.answer3] + PollAnswerService.create_bulk_poll_answers(poll.id, answers_data) + poll_answers = PollAnswerService.get_poll_answer(poll.id) + assert len(poll_answers) == 3 + + # Testing Exception + with pytest.raises(BusinessException) as exc_info: + _ = PollAnswerService.create_bulk_poll_answers(100, answers_data) + + assert exc_info.value.status_code == HTTPStatus.INTERNAL_SERVER_ERROR + + +def _create_widget(): + """Helper function to create a widget for testing.""" + engagement = factory_engagement_model() + widget = factory_widget_model({'engagement_id': engagement.id}) + return widget diff --git a/met-api/tests/unit/services/test_widget_poll_service.py b/met-api/tests/unit/services/test_widget_poll_service.py new file mode 100644 index 000000000..b2f7b851a --- /dev/null +++ b/met-api/tests/unit/services/test_widget_poll_service.py @@ -0,0 +1,170 @@ +# 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. +"""Tests for the document widget service. + +Test suite to ensure that the document widget service routines are working as expected. +""" +from http import HTTPStatus +from unittest.mock import patch + +import pytest +from tests.utilities.factory_scenarios import TestWidgetPollInfo, TestPollAnswerInfo, TestPollResponseInfo +from tests.utilities.factory_utils import factory_widget_model, factory_poll_model, factory_engagement_model, \ + factory_poll_answer_model + +from met_api.exceptions.business_exception import BusinessException +from met_api.services import authorization +from met_api.services.poll_response_service import PollResponseService +from met_api.services.widget_poll_service import WidgetPollService + + +def test_get_polls_by_widget_id(session): + """Assert that polls can be fetched by widget ID.""" + widget = _create_widget() + factory_poll_model(widget, TestWidgetPollInfo.poll1) + factory_poll_model(widget, TestWidgetPollInfo.poll2) + session.commit() + polls = WidgetPollService.get_polls_by_widget_id(widget.id) + assert len(polls) == 2 + + +def test_get_poll_by_id(session): + """Assert that polls can be fetched by poll ID.""" + widget = _create_widget() + poll = factory_poll_model(widget, TestWidgetPollInfo.poll1) + session.commit() + response = WidgetPollService.get_poll_by_id(poll.id) + assert response.id == poll.id + + # Test invalid poll ID + with pytest.raises(BusinessException) as exc_info: + _ = WidgetPollService.get_poll_by_id(100) + + assert exc_info.value.status_code == HTTPStatus.NOT_FOUND + + +def test_create_poll(session, monkeypatch): + """Assert that a poll can be created.""" + with patch.object(authorization, 'check_auth', return_value=True): + widget = _create_widget() + poll_data = TestWidgetPollInfo.poll1 + poll_data['engagement_id'] = widget.engagement_id + poll = WidgetPollService.create_poll(widget.id, poll_data) + assert poll.id is not None + assert poll.title == poll_data['title'] + + # Test invalid widget ID + with pytest.raises(BusinessException) as exc_info: + _ = WidgetPollService.create_poll(100, poll_data) + + assert exc_info.value.status_code == HTTPStatus.BAD_REQUEST + + +def test_update_poll(session): + """Assert that a poll can be updated.""" + + with patch.object(authorization, 'check_auth', return_value=True): + widget = _create_widget() + poll = factory_poll_model(widget, TestWidgetPollInfo.poll1) + session.commit() + updated_data = { + 'title': 'Updated Title', + 'answers': [ + { + "answer_text": "Python" + } + ] + } + updated_poll = WidgetPollService.update_poll(widget.id, poll.id, updated_data) + assert updated_poll.title == updated_data['title'] + assert updated_poll.answers[0].answer_text == updated_data['answers'][0]['answer_text'] + + # Test invalid poll ID + with pytest.raises(BusinessException) as exc_info: + _ = WidgetPollService.update_poll(widget.id, 150, updated_data) + + assert exc_info.value.status_code == HTTPStatus.BAD_REQUEST + + # Test invalid widget ID + with pytest.raises(BusinessException) as exc_info: + _ = WidgetPollService.update_poll(150, poll.id, updated_data) + + assert exc_info.value.status_code == HTTPStatus.BAD_REQUEST + + +def test_record_response(session): + """Assert that a poll can be created.""" + widget = _create_widget() + poll = factory_poll_model(widget, TestWidgetPollInfo.poll1) + answer = factory_poll_answer_model(poll, TestPollAnswerInfo.answer1) + response_data = TestPollResponseInfo.response1 + response_data['poll_id'] = poll.id + response_data['widget_id'] = widget.id + response_data['selected_answer_id'] = answer.id + + response = WidgetPollService.record_response(response_data) + assert response.id is not None + assert response.selected_answer_id == answer.id + + # Test creating response with invalid selected_answer_id + response_data['selected_answer_id'] = 100 + with pytest.raises(BusinessException) as exc_info: + _ = WidgetPollService.record_response(response_data) + + assert exc_info.value.status_code == HTTPStatus.BAD_REQUEST + + +def test_check_already_polled(session): + # Check already polled or not before poll response + response_data = TestPollResponseInfo.response1 + widget = _create_widget() + poll = factory_poll_model(widget, TestWidgetPollInfo.poll1) + already_polled = WidgetPollService.check_already_polled(poll.id, response_data['participant_id'], 1) + assert already_polled == False + + # Check already polled or not after poll response is created + answer = factory_poll_answer_model(poll, TestPollAnswerInfo.answer1) + response_data['poll_id'] = poll.id + response_data['widget_id'] = widget.id + response_data['selected_answer_id'] = answer.id + response = PollResponseService.create_response(response_data) + + already_polled = WidgetPollService.check_already_polled(poll.id, response_data['participant_id'], 1) + assert already_polled == True + + # Test wrong poll id + with pytest.raises(BusinessException) as exc_info: + _ = WidgetPollService.check_already_polled('wrong_string', response_data['participant_id'], 1) + + assert exc_info.value.status_code == HTTPStatus.BAD_REQUEST + + +def test_is_poll_active(session): + widget = _create_widget() + poll = factory_poll_model(widget, TestWidgetPollInfo.poll1) + is_active = WidgetPollService.is_poll_active(poll.id) + assert is_active == True + + # Test wrong poll id + with pytest.raises(BusinessException) as exc_info: + _ = WidgetPollService.is_poll_active(100) + + assert exc_info.value.status_code == HTTPStatus.BAD_REQUEST + + +def _create_widget(): + """Helper function to create a widget for testing.""" + engagement = factory_engagement_model() + widget = factory_widget_model({'engagement_id': engagement.id}) + return widget diff --git a/met-api/tests/utilities/factory_scenarios.py b/met-api/tests/utilities/factory_scenarios.py index 514f817ce..3b8c0dc72 100644 --- a/met-api/tests/utilities/factory_scenarios.py +++ b/met-api/tests/utilities/factory_scenarios.py @@ -667,3 +667,65 @@ class TestSubscribeInfo(Enum): } ] } + +class TestWidgetPollInfo(dict, Enum): + """Test scenarios of widget polls.""" + + poll1 = { + 'title': fake.sentence(), + 'description': fake.text(), + 'status': 'active', + 'engagement_id': 1 # Placeholder, should be replaced with actual engagement ID in tests + } + + poll2 = { + 'title': fake.sentence(), + 'description': fake.text(), + 'status': 'inactive', + 'engagement_id': 1 # Placeholder, should be replaced with actual engagement ID in tests + } + + poll3 = { + 'title': fake.sentence(), + 'description': fake.text(), + 'status': 'active', + 'engagement_id': 2 # Placeholder, should be replaced with another engagement ID in tests + } + +class TestPollAnswerInfo(dict, Enum): + """Test scenarios for poll answers.""" + + answer1 = { + 'answer_text': 'Answer 1' + } + + answer2 = { + 'answer_text': 'Answer 2' + } + + answer3 = { + 'answer_text': 'Answer 3' + } + + random_answer = lambda: { + 'answer_text': fake.sentence() + } + + + +class TestPollResponseInfo(dict, Enum): + """Test scenarios for poll responses.""" + + response1 = { + 'participant_id': fake.uuid4(), + 'selected_answer_id': 1, # should be replaced with an actual answer ID in tests + 'poll_id': 1, # should be replaced with an actual poll ID in tests + 'widget_id': 1, # Placeholder, should be replaced with an actual widget ID in tests + } + + response2 = { + 'participant_id': fake.uuid4(), + 'selected_answer_id': 2, # should be replaced with an actual answer ID in tests + 'poll_id': 1, # should be replaced with an actual poll ID in tests + 'widget_id': 1, # should be replaced with an actual widget ID in tests + } diff --git a/met-api/tests/utilities/factory_utils.py b/met-api/tests/utilities/factory_utils.py index 8d009ad1c..45bb9b317 100644 --- a/met-api/tests/utilities/factory_utils.py +++ b/met-api/tests/utilities/factory_utils.py @@ -37,13 +37,16 @@ from met_api.models.survey import Survey as SurveyModel from met_api.models.widget import Widget as WidgetModal from met_api.models.widget_documents import WidgetDocuments as WidgetDocumentModel +from met_api.models.widget_poll import Poll as WidgetPollModel +from met_api.models.poll_answers import PollAnswer as PollAnswerModel +from met_api.models.poll_responses import PollResponse as PollResponseModel from met_api.models.widget_item import WidgetItem as WidgetItemModal from met_api.utils.constants import TENANT_ID_HEADER from met_api.utils.enums import MembershipStatus from tests.utilities.factory_scenarios import ( TestCommentInfo, TestEngagementInfo, TestEngagementSlugInfo, TestFeedbackInfo, TestParticipantInfo, TestReportSettingInfo, TestSubmissionInfo, TestSurveyInfo, TestTenantInfo, TestUserInfo, TestWidgetDocumentInfo, - TestWidgetInfo, TestWidgetItemInfo) + TestWidgetInfo, TestWidgetItemInfo, TestWidgetPollInfo, TestPollAnswerInfo, TestPollResponseInfo) CONFIG = get_named_config('testing') fake = Faker() @@ -334,3 +337,35 @@ def factory_engagement_setting_model(engagement_id): ) setting.save() return setting + +def factory_poll_model(widget, poll_info: dict = TestWidgetPollInfo.poll1): + """Produce a Poll model.""" + poll = WidgetPollModel( + title = poll_info.get('title'), + description = poll_info.get('description'), + status = poll_info.get('status'), + engagement_id = widget.engagement_id, + widget_id = widget.id + ) + poll.save() + return poll + +def factory_poll_answer_model(poll, answer_info: dict = TestPollAnswerInfo.answer1): + """Produce a Poll model.""" + answer = PollAnswerModel( + answer_text = answer_info.get('answer_text'), + poll_id = poll.id + ) + answer.save() + return answer + +def factory_poll_response_model(poll, answer, response_info: dict = TestPollResponseInfo.response1): + """Produce a Poll model.""" + response = PollResponseModel( + participant_id = response_info.get('participant_id'), + selected_answer_id = answer.id, + poll_id = poll.id, + widget_id = poll.widget_id + ) + response.save() + return response \ No newline at end of file From f3f5554248542472d75c45536662148851cc15fa Mon Sep 17 00:00:00 2001 From: Ratheesh kumar R Date: Mon, 22 Jan 2024 11:46:04 -0800 Subject: [PATCH 08/14] Updated Change log --- CHANGELOG.MD | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 3e4c040a3..09291982a 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,3 +1,9 @@ +## January 22, 2024 +- **Task** Poll Widget: Back-end [🎟️DESENG-463](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-463) + - Created Database models for Widget Poll, Poll Answers, Poll Response. + - Created API to manage Widget Poll, Poll Answers, Poll Response. + - Created Unit tests to test the code. + ## January 15, 2024 - **Task** Audit for missing unit tests [🎟️DESENG-436](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-436) From ab7b322292ebca880ba7cef5155bad3636eec0ad Mon Sep 17 00:00:00 2001 From: Ratheesh kumar R Date: Mon, 22 Jan 2024 12:42:07 -0800 Subject: [PATCH 09/14] Solved pylint issues --- met-api/src/met_api/models/poll_responses.py | 5 +++- met-api/src/met_api/resources/widget_poll.py | 6 ++--- .../met_api/services/poll_answers_service.py | 12 ++++----- .../met_api/services/poll_response_service.py | 9 ++++--- .../met_api/services/widget_poll_service.py | 25 +++++++++++-------- 5 files changed, 31 insertions(+), 26 deletions(-) diff --git a/met-api/src/met_api/models/poll_responses.py b/met-api/src/met_api/models/poll_responses.py index fb7d159c7..08af03bb7 100644 --- a/met-api/src/met_api/models/poll_responses.py +++ b/met-api/src/met_api/models/poll_responses.py @@ -5,6 +5,7 @@ """ from __future__ import annotations +from sqlalchemy.sql.expression import false from sqlalchemy.sql.schema import ForeignKey from .base_model import BaseModel @@ -30,7 +31,9 @@ def get_responses(cls, poll_id) -> list[PollResponse]: @classmethod def get_responses_by_participant_id(cls, poll_id, participant_id) -> list[PollResponse]: """Get responses for a poll.""" - return db.session.query(PollResponse).filter(PollResponse.poll_id == poll_id, PollResponse.participant_id == participant_id,PollResponse.is_deleted == False).all() + return db.session.query(PollResponse).filter(PollResponse.poll_id == poll_id, + PollResponse.participant_id == participant_id, + PollResponse.is_deleted == false()).all() @classmethod def update_response(cls, response_id, response_data: dict) -> PollResponse: diff --git a/met-api/src/met_api/resources/widget_poll.py b/met-api/src/met_api/resources/widget_poll.py index f7f2f10cc..44f65b40b 100644 --- a/met-api/src/met_api/resources/widget_poll.py +++ b/met-api/src/met_api/resources/widget_poll.py @@ -1,7 +1,7 @@ """API endpoints for managing a poll widget resource.""" from http import HTTPStatus -from flask import jsonify, request +from flask import request from flask_cors import cross_origin from flask_restx import Namespace, Resource @@ -139,5 +139,5 @@ def record_poll_response(response_dict): poll_response = WidgetPollService.record_response(response_dict) if poll_response.id: return {'message': 'Response recorded successfully'}, HTTPStatus.CREATED - else: - return {'message': 'Response failed to record'}, HTTPStatus.INTERNAL_SERVER_ERROR + + return {'message': 'Response failed to record'}, HTTPStatus.INTERNAL_SERVER_ERROR diff --git a/met-api/src/met_api/services/poll_answers_service.py b/met-api/src/met_api/services/poll_answers_service.py index 997f628e9..76d128223 100644 --- a/met-api/src/met_api/services/poll_answers_service.py +++ b/met-api/src/met_api/services/poll_answers_service.py @@ -1,10 +1,10 @@ """Service for PollAnswer management.""" -from met_api.models.poll_answers import PollAnswer as PollAnswerModel -from met_api.services import authorization -from met_api.utils.roles import Role -from met_api.exceptions.business_exception import BusinessException from http import HTTPStatus +from met_api.exceptions.business_exception import BusinessException +from met_api.models.poll_answers import PollAnswer as PollAnswerModel + + class PollAnswerService: """PollAnswer management service.""" @@ -14,7 +14,6 @@ def get_poll_answer(poll_id): poll_answer = PollAnswerModel.get_answers(poll_id) return poll_answer - @staticmethod def create_bulk_poll_answers(poll_id: int, answers_data: list): """Bulk insert of poll answers.""" @@ -22,8 +21,7 @@ def create_bulk_poll_answers(poll_id: int, answers_data: list): if len(answers_data) > 0: PollAnswerModel.bulk_insert_answers(poll_id, answers_data) except Exception as e: - raise BusinessException(str(e), HTTPStatus.INTERNAL_SERVER_ERROR) - + raise BusinessException(str(e), HTTPStatus.INTERNAL_SERVER_ERROR) from e @staticmethod def delete_poll_answers(poll_id: int): diff --git a/met-api/src/met_api/services/poll_response_service.py b/met-api/src/met_api/services/poll_response_service.py index 9fad3af35..a7680e647 100644 --- a/met-api/src/met_api/services/poll_response_service.py +++ b/met-api/src/met_api/services/poll_response_service.py @@ -1,3 +1,4 @@ +"""Service for Poll Response management.""" from met_api.models.poll_responses import PollResponse as PollResponseModel from met_api.services.poll_answers_service import PollAnswerService @@ -26,17 +27,17 @@ def create_response(response_data: dict) -> PollResponseModel: return poll_response except Exception as e: # Log the exception or handle it as needed - raise ValueError(f'Error creating poll response: {e}') + raise ValueError(f'Error creating poll response: {e}') from e @staticmethod - def get_poll_count(poll_id: int, ip: str = None) -> int: + def get_poll_count(poll_id: int, ip_addr: str = None) -> int: """ Get the count of responses for a given poll. Optionally filters by participant IP. """ try: - responses = PollResponseModel.get_responses_by_participant_id(poll_id, ip) + responses = PollResponseModel.get_responses_by_participant_id(poll_id, ip_addr) return len(responses) except Exception as e: # Log the exception or handle it as needed - raise ValueError(f'Error retrieving poll count: {e}') + raise ValueError(f'Error creating poll response: {e}') from e diff --git a/met-api/src/met_api/services/widget_poll_service.py b/met-api/src/met_api/services/widget_poll_service.py index 69e0b3ffd..e06bc7b74 100644 --- a/met-api/src/met_api/services/widget_poll_service.py +++ b/met-api/src/met_api/services/widget_poll_service.py @@ -1,11 +1,14 @@ +"""Service for Widget Poll management.""" +from http import HTTPStatus + +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.poll_answers_service import PollAnswerService -from met_api.constants.membership_type import MembershipType -from met_api.utils.roles import Role from met_api.services.poll_response_service import PollResponseService -from met_api.exceptions.business_exception import BusinessException -from http import HTTPStatus +from met_api.utils.roles import Role + class WidgetPollService: """Service for managing WidgetPolls.""" @@ -31,7 +34,7 @@ def create_poll(widget_id: int, poll_details: dict): WidgetPollService._check_authorization(eng_id) return WidgetPollService._create_poll_model(widget_id, poll_details) except Exception as exc: - raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) + raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) from exc @staticmethod def update_poll(widget_id: int, poll_widget_id: int, poll_data: dict): @@ -45,7 +48,7 @@ def update_poll(widget_id: int, poll_widget_id: int, poll_data: dict): return WidgetPollService._update_poll_model(poll_widget_id, poll_data) except Exception as exc: - raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) + raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) from exc @staticmethod def record_response(response_data: dict): @@ -53,16 +56,16 @@ def record_response(response_data: dict): try: return PollResponseService.create_response(response_data) except Exception as exc: - raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) + raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) from exc @staticmethod - def check_already_polled(poll_id: int, ip: str, count: int) -> bool: + def check_already_polled(poll_id: int, ip_addr: str, count: int) -> bool: """Check if an IP has already polled more than the given count.""" try: - poll_count = PollResponseService.get_poll_count(poll_id, ip) + poll_count = PollResponseService.get_poll_count(poll_id, ip_addr) return poll_count >= count except Exception as exc: - raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) + raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) from exc @staticmethod def is_poll_active(poll_id: int) -> bool: @@ -71,7 +74,7 @@ def is_poll_active(poll_id: int) -> bool: poll = WidgetPollService.get_poll_by_id(poll_id) return poll.status == 'active' except Exception as exc: - raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) + raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) from exc @staticmethod def _create_poll_model(widget_id: int, poll_data: dict): From 4f64cd53f0911e7128a41951f992c535e7360a5f Mon Sep 17 00:00:00 2001 From: Ratheesh kumar R Date: Mon, 22 Jan 2024 14:49:46 -0800 Subject: [PATCH 10/14] fixing flake8 comments --- met-api/src/met_api/resources/widget_poll.py | 21 ++++++---- met-api/src/met_api/schemas/widget_poll.py | 33 ++++++++++++++-- .../met_api/services/poll_answers_service.py | 3 +- .../met_api/services/poll_response_service.py | 17 ++++++--- .../met_api/services/widget_poll_service.py | 11 +++--- met-api/src/met_api/utils/ip_util.py | 13 ++++++- met-api/tests/unit/api/test_widget_poll.py | 14 +++---- met-api/tests/unit/models/test_engagement.py | 3 +- met-api/tests/unit/models/test_poll_answer.py | 15 ++++---- .../tests/unit/models/test_poll_response.py | 21 +++++----- met-api/tests/unit/models/test_widget_poll.py | 13 +++---- .../services/test_poll_answers_service.py | 8 ++-- .../unit/services/test_widget_poll_service.py | 28 +++++++------- met-api/tests/utilities/factory_scenarios.py | 7 +--- met-api/tests/utilities/factory_utils.py | 38 ++++++++++--------- 15 files changed, 148 insertions(+), 97 deletions(-) diff --git a/met-api/src/met_api/resources/widget_poll.py b/met-api/src/met_api/resources/widget_poll.py index 44f65b40b..eaae6e784 100644 --- a/met-api/src/met_api/resources/widget_poll.py +++ b/met-api/src/met_api/resources/widget_poll.py @@ -38,7 +38,7 @@ def post(widget_id): """Create poll widget.""" try: request_json = request.get_json() - valid_format, errors = Polls.validate_response_format(request_json) + valid_format, errors = Polls.validate_request_format(request_json) if not valid_format: return {'message': 'Invalid response format', 'errors': errors}, HTTPStatus.BAD_REQUEST widget_poll = WidgetPollService().create_poll(widget_id, request_json) @@ -47,7 +47,8 @@ def post(widget_id): return str(err), err.status_code @staticmethod - def validate_response_format(data): + 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) @@ -64,10 +65,9 @@ class Poll(Resource): @_jwt.requires_auth def patch(widget_id, poll_widget_id): """Update poll widget.""" - try: request_json = request.get_json() - valid_format, errors = Poll.validate_response_format(request_json) + valid_format, errors = Poll.validate_request_format(request_json) if not valid_format: return {'message': 'Invalid response format', 'errors': errors}, HTTPStatus.BAD_REQUEST @@ -77,12 +77,14 @@ def patch(widget_id, poll_widget_id): return str(err), err.status_code @staticmethod - def validate_response_format(data): + 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('//responses') class PollResponseRecord(Resource): @@ -94,7 +96,7 @@ def post(widget_id, poll_widget_id): """Record a response for a given poll widget.""" try: response_data = request.get_json() - valid_format, errors = PollResponseRecord.validate_response_format(response_data) + valid_format, errors = PollResponseRecord.validate_request_format(response_data) if not valid_format: return {'message': 'Invalid response format', 'errors': errors}, HTTPStatus.BAD_REQUEST @@ -112,7 +114,8 @@ def post(widget_id, poll_widget_id): return err.error, err.status_code @staticmethod - def validate_response_format(data): + 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) @@ -120,6 +123,7 @@ def validate_response_format(data): @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 @@ -128,14 +132,17 @@ def prepare_response_data(data, widget_id, poll_widget_id): @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 limt 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 diff --git a/met-api/src/met_api/schemas/widget_poll.py b/met-api/src/met_api/schemas/widget_poll.py index 78ea13103..f3067519c 100644 --- a/met-api/src/met_api/schemas/widget_poll.py +++ b/met-api/src/met_api/schemas/widget_poll.py @@ -1,17 +1,42 @@ -"""Schema for WidgetPoll.""" +"""Schema for Widget Poll.""" from met_api.models.widget_poll import Poll as PollModel from met_api.models.poll_answers import PollAnswer as PollAnswerModel from marshmallow import Schema from marshmallow_sqlalchemy.fields import Nested + class PollAnswerSchema(Schema): + """ + Schema for serializing and deserializing Poll Answer data. + + This schema is used to represent poll answers in a structured format, + facilitating operations like loading from and dumping to JSON. + """ + class Meta: - model = PollAnswerModel - fields = ('id', 'answer_text', 'poll_id') + """Meta class for PollAnswerSchema options.""" + + model = PollAnswerModel # The model representing Poll Answer. + fields = ('id', 'answer_text', 'poll_id') # Fields to include in the schema. + class WidgetPollSchema(Schema): + """ + Schema for serializing and deserializing Widget Poll data. + + This schema is designed to handle Widget Poll data, enabling easy conversion + between Python objects and JSON representation, specifically for Widget Polls. + """ + class Meta: - model = PollModel + """Meta class for WidgetPollSchema options.""" + + model = PollModel # The model representing Widget Poll. fields = ('id', 'title', 'description', 'status', 'widget_id', 'engagement_id', 'answers') answers = Nested(PollAnswerSchema, many=True) + """Nested field for Poll Answers. + + This field represents a collection of Poll Answers associated with a Widget Poll, + allowing for the inclusion of related Poll Answer data within a Widget Poll's serialized form. + """ diff --git a/met-api/src/met_api/services/poll_answers_service.py b/met-api/src/met_api/services/poll_answers_service.py index 76d128223..0eb0e4a1e 100644 --- a/met-api/src/met_api/services/poll_answers_service.py +++ b/met-api/src/met_api/services/poll_answers_service.py @@ -1,6 +1,7 @@ """Service for PollAnswer management.""" from http import HTTPStatus +from sqlalchemy.exc import SQLAlchemyError from met_api.exceptions.business_exception import BusinessException from met_api.models.poll_answers import PollAnswer as PollAnswerModel @@ -20,7 +21,7 @@ def create_bulk_poll_answers(poll_id: int, answers_data: list): try: if len(answers_data) > 0: PollAnswerModel.bulk_insert_answers(poll_id, answers_data) - except Exception as e: + except SQLAlchemyError as e: raise BusinessException(str(e), HTTPStatus.INTERNAL_SERVER_ERROR) from e @staticmethod diff --git a/met-api/src/met_api/services/poll_response_service.py b/met-api/src/met_api/services/poll_response_service.py index a7680e647..4dcdaf554 100644 --- a/met-api/src/met_api/services/poll_response_service.py +++ b/met-api/src/met_api/services/poll_response_service.py @@ -1,4 +1,7 @@ """Service for Poll Response management.""" +from http import HTTPStatus +from sqlalchemy.exc import SQLAlchemyError +from met_api.exceptions.business_exception import BusinessException from met_api.models.poll_responses import PollResponse as PollResponseModel from met_api.services.poll_answers_service import PollAnswerService @@ -10,6 +13,7 @@ class PollResponseService: def create_response(response_data: dict) -> PollResponseModel: """ Create a poll response. + Raises ValueError if the selected answer is not valid for the poll. """ try: @@ -19,25 +23,28 @@ def create_response(response_data: dict) -> PollResponseModel: # Validate if the poll and answer are valid valid_answers = PollAnswerService.get_poll_answer(poll_id) if not any(answer.id == selected_answer_id for answer in valid_answers): - raise ValueError('Invalid selected answer for the poll.') + raise BusinessException('Invalid selected answer for the poll.', HTTPStatus.BAD_REQUEST) # Create and save the poll response poll_response = PollResponseModel(**response_data) poll_response.save() return poll_response - except Exception as e: + except SQLAlchemyError as e: # Log the exception or handle it as needed - raise ValueError(f'Error creating poll response: {e}') from e + raise BusinessException(f'Error creating poll response: {e}', + HTTPStatus.INTERNAL_SERVER_ERROR) from e @staticmethod def get_poll_count(poll_id: int, ip_addr: str = None) -> int: """ Get the count of responses for a given poll. + Optionally filters by participant IP. """ try: responses = PollResponseModel.get_responses_by_participant_id(poll_id, ip_addr) return len(responses) - except Exception as e: + except SQLAlchemyError as e: # Log the exception or handle it as needed - raise ValueError(f'Error creating poll response: {e}') from e + raise BusinessException(f'Error creating poll response: {e}', + HTTPStatus.INTERNAL_SERVER_ERROR) from e diff --git a/met-api/src/met_api/services/widget_poll_service.py b/met-api/src/met_api/services/widget_poll_service.py index e06bc7b74..36161daf5 100644 --- a/met-api/src/met_api/services/widget_poll_service.py +++ b/met-api/src/met_api/services/widget_poll_service.py @@ -1,6 +1,7 @@ """Service for Widget Poll management.""" from http import HTTPStatus +from sqlalchemy.exc import SQLAlchemyError 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 @@ -33,7 +34,7 @@ def create_poll(widget_id: int, poll_details: dict): eng_id = poll_details.get('engagement_id') WidgetPollService._check_authorization(eng_id) return WidgetPollService._create_poll_model(widget_id, poll_details) - except Exception as exc: + except SQLAlchemyError as exc: raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) from exc @staticmethod @@ -47,7 +48,7 @@ def update_poll(widget_id: int, poll_widget_id: int, poll_data: dict): raise BusinessException('Invalid widget ID', HTTPStatus.BAD_REQUEST) return WidgetPollService._update_poll_model(poll_widget_id, poll_data) - except Exception as exc: + except SQLAlchemyError as exc: raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) from exc @staticmethod @@ -55,7 +56,7 @@ def record_response(response_data: dict): """Record a response for a poll.""" try: return PollResponseService.create_response(response_data) - except Exception as exc: + except SQLAlchemyError as exc: raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) from exc @staticmethod @@ -64,7 +65,7 @@ def check_already_polled(poll_id: int, ip_addr: str, count: int) -> bool: try: poll_count = PollResponseService.get_poll_count(poll_id, ip_addr) return poll_count >= count - except Exception as exc: + except SQLAlchemyError as exc: raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) from exc @staticmethod @@ -73,7 +74,7 @@ def is_poll_active(poll_id: int) -> bool: try: poll = WidgetPollService.get_poll_by_id(poll_id) return poll.status == 'active' - except Exception as exc: + except SQLAlchemyError as exc: raise BusinessException(str(exc), HTTPStatus.BAD_REQUEST) from exc @staticmethod diff --git a/met-api/src/met_api/utils/ip_util.py b/met-api/src/met_api/utils/ip_util.py index 524a1af69..9698ea0f8 100644 --- a/met-api/src/met_api/utils/ip_util.py +++ b/met-api/src/met_api/utils/ip_util.py @@ -1,3 +1,14 @@ +""" +This module provides utility functions for handling IP addresses in a Flask application. + +It includes a function for hashing IP addresses using SHA256, ensuring secure and consistent +hashing by combining each IP address with the Flask application's secret key. This approach +is typically used for anonymizing IP addresses while maintaining the ability to identify +sessions or users without storing their actual IP addresses. + +Functions: + hash_ip(ip_address): Hashes an IP address with the Flask secret key. +""" from hashlib import sha256 from flask import current_app @@ -16,4 +27,4 @@ def hash_ip(ip_address): secret_key = current_app.config.get('SECRET_KEY', '') # Concatenate the IP address and secret key, and hash the resulting string - return sha256(f"{ip_address}{secret_key}".encode()).hexdigest() + return sha256(f'{ip_address}{secret_key}'.encode()).hexdigest() diff --git a/met-api/tests/unit/api/test_widget_poll.py b/met-api/tests/unit/api/test_widget_poll.py index 46b88dd6d..1d6874ec1 100644 --- a/met-api/tests/unit/api/test_widget_poll.py +++ b/met-api/tests/unit/api/test_widget_poll.py @@ -21,17 +21,18 @@ from http import HTTPStatus from faker import Faker -from tests.utilities.factory_scenarios import TestJwtClaims, TestWidgetPollInfo, TestPollAnswerInfo -from tests.utilities.factory_utils import factory_auth_header, factory_engagement_model, factory_widget_model, \ - factory_poll_model, factory_poll_answer_model from met_api.utils.enums import ContentType +from tests.utilities.factory_scenarios import TestJwtClaims, TestPollAnswerInfo, TestWidgetPollInfo +from tests.utilities.factory_utils import ( + factory_auth_header, factory_engagement_model, factory_poll_answer_model, factory_poll_model, factory_widget_model) + fake = Faker() def test_get_widget(client, jwt, session): - """Assert that a get API endpoint is working as expected""" + """Assert that a get API endpoint is working as expected.""" # Test setup: create a poll widget and a response model headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.no_role) engagement = factory_engagement_model() @@ -54,7 +55,6 @@ def test_get_widget(client, jwt, session): assert json_data[0]['answers'][0]['answer_text'] == answer.answer_text - def test_create_poll_widget(client, jwt, session, setup_admin_user_and_claims): """Assert that a poll widget can be POSTed.""" # Test setup: create a poll widget model @@ -99,7 +99,7 @@ def test_create_poll_widget(client, jwt, session, setup_admin_user_and_claims): # Sending POST request rv = client.post( - f'/api/widgets/100/polls', + '/api/widgets/100/polls', data=json.dumps(data), headers=headers, content_type=ContentType.JSON.value, @@ -151,7 +151,7 @@ def test_update_poll_widget(client, jwt, session, setup_admin_user_and_claims): assert rv.status_code == HTTPStatus.BAD_REQUEST -def test_record_poll_response(client, jwt, session): +def test_record_poll_response(client, session, jwt): """Assert that a response for a poll widget can be POSTed.""" # Test setup: create a poll widget and a response model headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.no_role) diff --git a/met-api/tests/unit/models/test_engagement.py b/met-api/tests/unit/models/test_engagement.py index c6a1f41c2..157f13695 100644 --- a/met-api/tests/unit/models/test_engagement.py +++ b/met-api/tests/unit/models/test_engagement.py @@ -20,10 +20,11 @@ from met_api.constants.engagement_status import Status from met_api.models.engagement import Engagement as EngagementModel -from met_api.models.pagination_options import PaginationOptions from met_api.models.engagement_scope_options import EngagementScopeOptions +from met_api.models.pagination_options import PaginationOptions from tests.utilities.factory_utils import factory_engagement_model + fake = Faker() diff --git a/met-api/tests/unit/models/test_poll_answer.py b/met-api/tests/unit/models/test_poll_answer.py index ad849fe70..92a0a11d9 100644 --- a/met-api/tests/unit/models/test_poll_answer.py +++ b/met-api/tests/unit/models/test_poll_answer.py @@ -16,18 +16,17 @@ Test suite to ensure that the Engagement model routines are working as expected. """ -from tests.utilities.factory_scenarios import TestPollAnswerInfo -from tests.utilities.factory_utils import factory_poll_model, factory_poll_answer_model, factory_engagement_model, \ - factory_widget_model - from met_api.models.poll_answers import PollAnswer +from tests.utilities.factory_scenarios import TestPollAnswerInfo +from tests.utilities.factory_utils import ( + factory_engagement_model, factory_poll_answer_model, factory_poll_model, factory_widget_model) def test_get_answers(session): """Assert that answers for a poll can be fetched.""" poll = _create_poll() - answer1 = factory_poll_answer_model(poll, TestPollAnswerInfo.answer1) - answer2 = factory_poll_answer_model(poll, TestPollAnswerInfo.answer2) + factory_poll_answer_model(poll, TestPollAnswerInfo.answer1) + factory_poll_answer_model(poll, TestPollAnswerInfo.answer2) session.commit() answers = PollAnswer.get_answers(poll.id) assert len(answers) == 2 @@ -64,13 +63,13 @@ def test_bulk_insert_answers(session): def _create_poll(): - """Helper function to create a poll for testing.""" + """Create and return a sample poll for testing.""" widget = _create_widget() return factory_poll_model(widget, {'title': 'Sample Poll', 'engagement_id': widget.engagement_id}) def _create_widget(): - """Helper function to create a widget for testing.""" + """Create and return a sample widget for testing.""" engagement = factory_engagement_model() widget = factory_widget_model({'engagement_id': engagement.id}) return widget diff --git a/met-api/tests/unit/models/test_poll_response.py b/met-api/tests/unit/models/test_poll_response.py index 670cb7f31..79e6319e4 100644 --- a/met-api/tests/unit/models/test_poll_response.py +++ b/met-api/tests/unit/models/test_poll_response.py @@ -16,18 +16,18 @@ Test suite to ensure that the Engagement model routines are working as expected. """ -from tests.utilities.factory_scenarios import TestPollResponseInfo, TestPollAnswerInfo -from tests.utilities.factory_utils import factory_poll_response_model, factory_poll_model, factory_poll_answer_model, \ - factory_engagement_model, factory_widget_model - from met_api.models.poll_responses import PollResponse +from tests.utilities.factory_scenarios import TestPollAnswerInfo, TestPollResponseInfo +from tests.utilities.factory_utils import ( + factory_engagement_model, factory_poll_answer_model, factory_poll_model, factory_poll_response_model, + factory_widget_model) def test_get_responses(session): """Assert that responses for a poll can be fetched.""" poll, answer = _create_poll_answer() - poll_response1 = factory_poll_response_model(poll, answer, TestPollResponseInfo.response1) - poll_response2 = factory_poll_response_model(poll, answer, TestPollResponseInfo.response2) + factory_poll_response_model(poll, answer, TestPollResponseInfo.response1) + factory_poll_response_model(poll, answer, TestPollResponseInfo.response2) session.commit() responses = PollResponse.get_responses(poll.id) assert len(responses) == 2 @@ -50,25 +50,24 @@ def test_update_or_delete_response(session): session.commit() updated_data = {'is_deleted': True} updated_response = PollResponse.update_response(poll_response1.id, updated_data) - assert updated_response.is_deleted == True + assert updated_response.is_deleted is True def _create_poll(): - """Helper function to create a poll for testing.""" + """Create sample poll for testing.""" widget = _create_widget() return factory_poll_model(widget, {'title': 'Sample Poll', 'engagement_id': widget.engagement_id}) def _create_widget(): - """Helper function to create a widget for testing.""" + """Create sample widget for testing.""" engagement = factory_engagement_model() widget = factory_widget_model({'engagement_id': engagement.id}) return widget def _create_poll_answer(): - """Helper function to create a widget for testing.""" + """Create sample poll answer for testing.""" poll = _create_poll() answer = factory_poll_answer_model(poll, TestPollAnswerInfo.answer1) return poll, answer -# Additional relevant tests can be added here diff --git a/met-api/tests/unit/models/test_widget_poll.py b/met-api/tests/unit/models/test_widget_poll.py index 478e64ab9..43d4bceff 100644 --- a/met-api/tests/unit/models/test_widget_poll.py +++ b/met-api/tests/unit/models/test_widget_poll.py @@ -17,10 +17,11 @@ """ from faker import Faker -from tests.utilities.factory_scenarios import TestWidgetPollInfo -from tests.utilities.factory_utils import factory_widget_model, factory_engagement_model, factory_poll_model from met_api.models.widget_poll import Poll +from tests.utilities.factory_scenarios import TestWidgetPollInfo +from tests.utilities.factory_utils import factory_engagement_model, factory_poll_model, factory_widget_model + fake = Faker() @@ -37,8 +38,8 @@ def test_create_poll(session): def test_get_polls_by_widget_id(session): """Assert that polls for a widget can be fetched.""" widget = _create_widget() - poll1 = factory_poll_model(widget, TestWidgetPollInfo.poll1) - poll2 = factory_poll_model(widget, TestWidgetPollInfo.poll2) + factory_poll_model(widget, TestWidgetPollInfo.poll1) + factory_poll_model(widget, TestWidgetPollInfo.poll2) session.commit() polls = Poll.get_polls(widget.id) assert len(polls) == 2 @@ -54,10 +55,8 @@ def test_update_poll(session): assert updated_poll.title == updated_title -# Additional relevant tests can be added here - def _create_widget(): - """Helper function to create a widget for testing.""" + """Create sample widget for testing.""" engagement = factory_engagement_model() widget = factory_widget_model({'engagement_id': engagement.id}) return widget diff --git a/met-api/tests/unit/services/test_poll_answers_service.py b/met-api/tests/unit/services/test_poll_answers_service.py index ff007606c..db43c82c8 100644 --- a/met-api/tests/unit/services/test_poll_answers_service.py +++ b/met-api/tests/unit/services/test_poll_answers_service.py @@ -19,12 +19,12 @@ from http import HTTPStatus import pytest -from tests.utilities.factory_scenarios import TestPollAnswerInfo, TestWidgetPollInfo -from tests.utilities.factory_utils import factory_poll_model, factory_poll_answer_model, factory_widget_model, \ - factory_engagement_model from met_api.exceptions.business_exception import BusinessException from met_api.services.poll_answers_service import PollAnswerService +from tests.utilities.factory_scenarios import TestPollAnswerInfo, TestWidgetPollInfo +from tests.utilities.factory_utils import ( + factory_engagement_model, factory_poll_answer_model, factory_poll_model, factory_widget_model) def test_get_poll_answer(session): @@ -66,7 +66,7 @@ def test_create_bulk_poll_answers(session): def _create_widget(): - """Helper function to create a widget for testing.""" + """Create sample widget for testing.""" engagement = factory_engagement_model() widget = factory_widget_model({'engagement_id': engagement.id}) return widget diff --git a/met-api/tests/unit/services/test_widget_poll_service.py b/met-api/tests/unit/services/test_widget_poll_service.py index b2f7b851a..8169bf6cc 100644 --- a/met-api/tests/unit/services/test_widget_poll_service.py +++ b/met-api/tests/unit/services/test_widget_poll_service.py @@ -19,14 +19,14 @@ from unittest.mock import patch import pytest -from tests.utilities.factory_scenarios import TestWidgetPollInfo, TestPollAnswerInfo, TestPollResponseInfo -from tests.utilities.factory_utils import factory_widget_model, factory_poll_model, factory_engagement_model, \ - factory_poll_answer_model from met_api.exceptions.business_exception import BusinessException from met_api.services import authorization from met_api.services.poll_response_service import PollResponseService from met_api.services.widget_poll_service import WidgetPollService +from tests.utilities.factory_scenarios import TestPollAnswerInfo, TestPollResponseInfo, TestWidgetPollInfo +from tests.utilities.factory_utils import ( + factory_engagement_model, factory_poll_answer_model, factory_poll_model, factory_widget_model) def test_get_polls_by_widget_id(session): @@ -73,7 +73,6 @@ def test_create_poll(session, monkeypatch): def test_update_poll(session): """Assert that a poll can be updated.""" - with patch.object(authorization, 'check_auth', return_value=True): widget = _create_widget() poll = factory_poll_model(widget, TestWidgetPollInfo.poll1) @@ -82,7 +81,7 @@ def test_update_poll(session): 'title': 'Updated Title', 'answers': [ { - "answer_text": "Python" + 'answer_text': 'Python' } ] } @@ -94,7 +93,7 @@ def test_update_poll(session): with pytest.raises(BusinessException) as exc_info: _ = WidgetPollService.update_poll(widget.id, 150, updated_data) - assert exc_info.value.status_code == HTTPStatus.BAD_REQUEST + assert exc_info.value.status_code == HTTPStatus.NOT_FOUND # Test invalid widget ID with pytest.raises(BusinessException) as exc_info: @@ -126,45 +125,46 @@ def test_record_response(session): def test_check_already_polled(session): - # Check already polled or not before poll response + """Checking whether the poll is already polled the specfied limit with same ip.""" response_data = TestPollResponseInfo.response1 widget = _create_widget() poll = factory_poll_model(widget, TestWidgetPollInfo.poll1) already_polled = WidgetPollService.check_already_polled(poll.id, response_data['participant_id'], 1) - assert already_polled == False + assert already_polled is False # Check already polled or not after poll response is created answer = factory_poll_answer_model(poll, TestPollAnswerInfo.answer1) response_data['poll_id'] = poll.id response_data['widget_id'] = widget.id response_data['selected_answer_id'] = answer.id - response = PollResponseService.create_response(response_data) + PollResponseService.create_response(response_data) already_polled = WidgetPollService.check_already_polled(poll.id, response_data['participant_id'], 1) - assert already_polled == True + assert already_polled is True # Test wrong poll id with pytest.raises(BusinessException) as exc_info: _ = WidgetPollService.check_already_polled('wrong_string', response_data['participant_id'], 1) - assert exc_info.value.status_code == HTTPStatus.BAD_REQUEST + assert exc_info.value.status_code == HTTPStatus.INTERNAL_SERVER_ERROR def test_is_poll_active(session): + """Check if poll is active or not.""" widget = _create_widget() poll = factory_poll_model(widget, TestWidgetPollInfo.poll1) is_active = WidgetPollService.is_poll_active(poll.id) - assert is_active == True + assert is_active is True # Test wrong poll id with pytest.raises(BusinessException) as exc_info: _ = WidgetPollService.is_poll_active(100) - assert exc_info.value.status_code == HTTPStatus.BAD_REQUEST + assert exc_info.value.status_code == HTTPStatus.NOT_FOUND def _create_widget(): - """Helper function to create a widget for testing.""" + """Create a widget for testing.""" engagement = factory_engagement_model() widget = factory_widget_model({'engagement_id': engagement.id}) return widget diff --git a/met-api/tests/utilities/factory_scenarios.py b/met-api/tests/utilities/factory_scenarios.py index 3b8c0dc72..820d8ad8a 100644 --- a/met-api/tests/utilities/factory_scenarios.py +++ b/met-api/tests/utilities/factory_scenarios.py @@ -668,6 +668,7 @@ class TestSubscribeInfo(Enum): ] } + class TestWidgetPollInfo(dict, Enum): """Test scenarios of widget polls.""" @@ -692,6 +693,7 @@ class TestWidgetPollInfo(dict, Enum): 'engagement_id': 2 # Placeholder, should be replaced with another engagement ID in tests } + class TestPollAnswerInfo(dict, Enum): """Test scenarios for poll answers.""" @@ -707,11 +709,6 @@ class TestPollAnswerInfo(dict, Enum): 'answer_text': 'Answer 3' } - random_answer = lambda: { - 'answer_text': fake.sentence() - } - - class TestPollResponseInfo(dict, Enum): """Test scenarios for poll responses.""" diff --git a/met-api/tests/utilities/factory_utils.py b/met-api/tests/utilities/factory_utils.py index 45bb9b317..76c1e92b7 100644 --- a/met-api/tests/utilities/factory_utils.py +++ b/met-api/tests/utilities/factory_utils.py @@ -30,6 +30,8 @@ from met_api.models.feedback import Feedback as FeedbackModel from met_api.models.membership import Membership as MembershipModel from met_api.models.participant import Participant as ParticipantModel +from met_api.models.poll_answers import PollAnswer as PollAnswerModel +from met_api.models.poll_responses import PollResponse as PollResponseModel from met_api.models.report_setting import ReportSetting as ReportSettingModel from met_api.models.staff_user import StaffUser as StaffUserModel from met_api.models.submission import Submission as SubmissionModel @@ -37,16 +39,15 @@ from met_api.models.survey import Survey as SurveyModel from met_api.models.widget import Widget as WidgetModal from met_api.models.widget_documents import WidgetDocuments as WidgetDocumentModel -from met_api.models.widget_poll import Poll as WidgetPollModel -from met_api.models.poll_answers import PollAnswer as PollAnswerModel -from met_api.models.poll_responses import PollResponse as PollResponseModel from met_api.models.widget_item import WidgetItem as WidgetItemModal +from met_api.models.widget_poll import Poll as WidgetPollModel from met_api.utils.constants import TENANT_ID_HEADER from met_api.utils.enums import MembershipStatus from tests.utilities.factory_scenarios import ( TestCommentInfo, TestEngagementInfo, TestEngagementSlugInfo, TestFeedbackInfo, TestParticipantInfo, - TestReportSettingInfo, TestSubmissionInfo, TestSurveyInfo, TestTenantInfo, TestUserInfo, TestWidgetDocumentInfo, - TestWidgetInfo, TestWidgetItemInfo, TestWidgetPollInfo, TestPollAnswerInfo, TestPollResponseInfo) + TestPollAnswerInfo, TestPollResponseInfo, TestReportSettingInfo, TestSubmissionInfo, TestSurveyInfo, TestTenantInfo, + TestUserInfo, TestWidgetDocumentInfo, TestWidgetInfo, TestWidgetItemInfo, TestWidgetPollInfo) + CONFIG = get_named_config('testing') fake = Faker() @@ -338,34 +339,37 @@ def factory_engagement_setting_model(engagement_id): setting.save() return setting + def factory_poll_model(widget, poll_info: dict = TestWidgetPollInfo.poll1): """Produce a Poll model.""" poll = WidgetPollModel( - title = poll_info.get('title'), - description = poll_info.get('description'), - status = poll_info.get('status'), - engagement_id = widget.engagement_id, - widget_id = widget.id + title=poll_info.get('title'), + description=poll_info.get('description'), + status=poll_info.get('status'), + engagement_id=widget.engagement_id, + widget_id=widget.id ) poll.save() return poll + def factory_poll_answer_model(poll, answer_info: dict = TestPollAnswerInfo.answer1): """Produce a Poll model.""" answer = PollAnswerModel( - answer_text = answer_info.get('answer_text'), - poll_id = poll.id + answer_text=answer_info.get('answer_text'), + poll_id=poll.id ) answer.save() return answer + def factory_poll_response_model(poll, answer, response_info: dict = TestPollResponseInfo.response1): """Produce a Poll model.""" response = PollResponseModel( - participant_id = response_info.get('participant_id'), - selected_answer_id = answer.id, - poll_id = poll.id, - widget_id = poll.widget_id + participant_id=response_info.get('participant_id'), + selected_answer_id=answer.id, + poll_id=poll.id, + widget_id=poll.widget_id ) response.save() - return response \ No newline at end of file + return response From 17d2dc8a4f4fc5b865980addddecf2b3ea554d3a Mon Sep 17 00:00:00 2001 From: Ratheesh kumar R Date: Tue, 23 Jan 2024 11:03:52 -0800 Subject: [PATCH 11/14] Fixing lint issues --- met-api/tests/utilities/factory_scenarios.py | 8 +++++--- met-api/tests/utilities/factory_utils.py | 10 +++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/met-api/tests/utilities/factory_scenarios.py b/met-api/tests/utilities/factory_scenarios.py index 59166f66a..ca0d45c7c 100644 --- a/met-api/tests/utilities/factory_scenarios.py +++ b/met-api/tests/utilities/factory_scenarios.py @@ -679,7 +679,7 @@ class TestSubscribeInfo(Enum): } ] } - + subscribe_info_2 = { 'widget_id': 1, 'type': 'SIGN_UP', @@ -696,6 +696,7 @@ class TestSubscribeInfo(Enum): ] } + class TestCACForm(dict, Enum): """Test scenarios of cac form.""" @@ -750,7 +751,8 @@ class TestTimelineInfo(dict, Enum): 'position': 1, 'status': TimelineEventStatus.Pending.value } - + + class TestWidgetPollInfo(dict, Enum): """Test scenarios of widget polls.""" @@ -807,4 +809,4 @@ class TestPollResponseInfo(dict, Enum): 'selected_answer_id': 2, # should be replaced with an actual answer ID in tests 'poll_id': 1, # should be replaced with an actual poll ID in tests 'widget_id': 1, # should be replaced with an actual widget ID in tests - } \ No newline at end of file + } diff --git a/met-api/tests/utilities/factory_utils.py b/met-api/tests/utilities/factory_utils.py index 82710e528..7fa29a843 100644 --- a/met-api/tests/utilities/factory_utils.py +++ b/met-api/tests/utilities/factory_utils.py @@ -40,19 +40,19 @@ from met_api.models.timeline_event import TimelineEvent as TimelineEventModel from met_api.models.widget import Widget as WidgetModal from met_api.models.widget_documents import WidgetDocuments as WidgetDocumentModel +from met_api.models.widget_item import WidgetItem as WidgetItemModal from met_api.models.widget_map import WidgetMap as WidgetMapModel +from met_api.models.widget_poll import Poll as WidgetPollModel from met_api.models.widget_timeline import WidgetTimeline as WidgetTimelineModel from met_api.models.widget_video import WidgetVideo as WidgetVideoModel -from met_api.models.widget_item import WidgetItem as WidgetItemModal -from met_api.models.widget_poll import Poll as WidgetPollModel from met_api.utils.constants import TENANT_ID_HEADER from met_api.utils.enums import MembershipStatus from tests.utilities.factory_scenarios import ( TestCommentInfo, TestEngagementInfo, TestEngagementSlugInfo, TestFeedbackInfo, TestParticipantInfo, TestPollAnswerInfo, TestPollResponseInfo, TestReportSettingInfo, TestSubmissionInfo, TestSurveyInfo, TestTenantInfo, - TestUserInfo, TestWidgetDocumentInfo, TestWidgetInfo, TestWidgetItemInfo, TestWidgetPollInfo, - TestReportSettingInfo, TestSubmissionInfo, TestSurveyInfo, TestTenantInfo, TestTimelineInfo, TestUserInfo, - TestWidgetDocumentInfo, TestWidgetInfo, TestWidgetItemInfo, TestWidgetMap, TestWidgetVideo) + TestTimelineInfo, TestUserInfo, TestWidgetDocumentInfo, TestWidgetInfo, TestWidgetItemInfo, TestWidgetMap, + TestWidgetPollInfo, TestWidgetVideo) + CONFIG = get_named_config('testing') fake = Faker() From 8d9c9d48b529ca4dfebc1594fc1fadd77579402a Mon Sep 17 00:00:00 2001 From: Ratheesh kumar R Date: Tue, 23 Jan 2024 11:15:20 -0800 Subject: [PATCH 12/14] Fixing sonarcloud suggestions --- met-api/src/met_api/resources/widget_poll.py | 7 ++++--- met-api/tests/unit/api/test_widget_poll.py | 4 ++-- met-api/tests/unit/services/test_widget_poll_service.py | 6 ------ 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/met-api/src/met_api/resources/widget_poll.py b/met-api/src/met_api/resources/widget_poll.py index eaae6e784..799932803 100644 --- a/met-api/src/met_api/resources/widget_poll.py +++ b/met-api/src/met_api/resources/widget_poll.py @@ -14,6 +14,7 @@ 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' @cors_preflight('GET, POST') @@ -40,7 +41,7 @@ def post(widget_id): request_json = request.get_json() valid_format, errors = Polls.validate_request_format(request_json) if not valid_format: - return {'message': 'Invalid response format', 'errors': errors}, HTTPStatus.BAD_REQUEST + return {'message': INVALID_REQUEST_MESSAGE, 'errors': errors}, HTTPStatus.BAD_REQUEST widget_poll = WidgetPollService().create_poll(widget_id, request_json) return WidgetPollSchema().dump(widget_poll), HTTPStatus.OK except BusinessException as err: @@ -69,7 +70,7 @@ def patch(widget_id, poll_widget_id): request_json = request.get_json() valid_format, errors = Poll.validate_request_format(request_json) if not valid_format: - return {'message': 'Invalid response format', 'errors': errors}, HTTPStatus.BAD_REQUEST + return {'message': INVALID_REQUEST_MESSAGE, 'errors': errors}, HTTPStatus.BAD_REQUEST widget_poll = WidgetPollService().update_poll(widget_id, poll_widget_id, request_json) return WidgetPollSchema().dump(widget_poll), HTTPStatus.OK @@ -98,7 +99,7 @@ def post(widget_id, poll_widget_id): response_data = request.get_json() valid_format, errors = PollResponseRecord.validate_request_format(response_data) if not valid_format: - return {'message': 'Invalid response format', 'errors': errors}, HTTPStatus.BAD_REQUEST + return {'message': INVALID_REQUEST_MESSAGE, 'errors': errors}, HTTPStatus.BAD_REQUEST response_dict = PollResponseRecord.prepare_response_data(response_data, widget_id, poll_widget_id) diff --git a/met-api/tests/unit/api/test_widget_poll.py b/met-api/tests/unit/api/test_widget_poll.py index 1d6874ec1..13ae08479 100644 --- a/met-api/tests/unit/api/test_widget_poll.py +++ b/met-api/tests/unit/api/test_widget_poll.py @@ -59,7 +59,7 @@ def test_create_poll_widget(client, jwt, session, setup_admin_user_and_claims): """Assert that a poll widget can be POSTed.""" # Test setup: create a poll widget model - user, claims = setup_admin_user_and_claims + _, claims = setup_admin_user_and_claims headers = factory_auth_header(jwt=jwt, claims=claims) engagement = factory_engagement_model() @@ -111,7 +111,7 @@ def test_create_poll_widget(client, jwt, session, setup_admin_user_and_claims): def test_update_poll_widget(client, jwt, session, setup_admin_user_and_claims): """Assert that a poll widget can be PATCHed.""" # Test setup: create and post a poll widget model - user, claims = setup_admin_user_and_claims + _, claims = setup_admin_user_and_claims headers = factory_auth_header(jwt=jwt, claims=claims) engagement = factory_engagement_model() diff --git a/met-api/tests/unit/services/test_widget_poll_service.py b/met-api/tests/unit/services/test_widget_poll_service.py index 8169bf6cc..053ecf3a8 100644 --- a/met-api/tests/unit/services/test_widget_poll_service.py +++ b/met-api/tests/unit/services/test_widget_poll_service.py @@ -142,12 +142,6 @@ def test_check_already_polled(session): already_polled = WidgetPollService.check_already_polled(poll.id, response_data['participant_id'], 1) assert already_polled is True - # Test wrong poll id - with pytest.raises(BusinessException) as exc_info: - _ = WidgetPollService.check_already_polled('wrong_string', response_data['participant_id'], 1) - - assert exc_info.value.status_code == HTTPStatus.INTERNAL_SERVER_ERROR - def test_is_poll_active(session): """Check if poll is active or not.""" From 58d1f33e2cd4394d092476f92397e678f7d8d13f Mon Sep 17 00:00:00 2001 From: Ratheesh kumar R Date: Thu, 25 Jan 2024 11:52:06 -0800 Subject: [PATCH 13/14] DESENG-463: Fixing Review comments --- met-api/src/met_api/resources/widget_poll.py | 4 ++-- met-api/src/met_api/utils/ip_util.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/met-api/src/met_api/resources/widget_poll.py b/met-api/src/met_api/resources/widget_poll.py index 799932803..02dab70ed 100644 --- a/met-api/src/met_api/resources/widget_poll.py +++ b/met-api/src/met_api/resources/widget_poll.py @@ -89,7 +89,7 @@ def validate_request_format(data): @cors_preflight('POST') @API.route('//responses') class PollResponseRecord(Resource): - """Resource for recording responses for a poll widget. Not require authentication.""" + """Resource for recording responses for a poll widget. Does not require authentication.""" @staticmethod @cross_origin(origins=allowedorigins()) @@ -138,7 +138,7 @@ def is_poll_active(poll_id): @staticmethod def is_poll_limit_exceeded(poll_id, participant_id): - """Check poll limt execeeded or not.""" + """Check poll limit execeeded or not.""" return WidgetPollService.check_already_polled(poll_id, participant_id, 10) @staticmethod diff --git a/met-api/src/met_api/utils/ip_util.py b/met-api/src/met_api/utils/ip_util.py index 9698ea0f8..81dd614fa 100644 --- a/met-api/src/met_api/utils/ip_util.py +++ b/met-api/src/met_api/utils/ip_util.py @@ -25,6 +25,9 @@ def hash_ip(ip_address): """ # Retrieve the secret key from Flask configuration with a fallback empty string secret_key = current_app.config.get('SECRET_KEY', '') + + # Extract the fragment (e.g., first three octets of an IPv4 address) + ip_fragment = '.'.join(ip_address.split('.')[:3]) # Concatenate the IP address and secret key, and hash the resulting string - return sha256(f'{ip_address}{secret_key}'.encode()).hexdigest() + return sha256(f'{ip_fragment}{secret_key}'.encode()).hexdigest() From 76cff5643b4486c9489769f43afd7a1d3e946a6e Mon Sep 17 00:00:00 2001 From: Ratheesh kumar R Date: Thu, 25 Jan 2024 12:05:56 -0800 Subject: [PATCH 14/14] Fixing lint --- met-api/src/met_api/utils/ip_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/met-api/src/met_api/utils/ip_util.py b/met-api/src/met_api/utils/ip_util.py index 81dd614fa..38d9481da 100644 --- a/met-api/src/met_api/utils/ip_util.py +++ b/met-api/src/met_api/utils/ip_util.py @@ -25,7 +25,7 @@ def hash_ip(ip_address): """ # Retrieve the secret key from Flask configuration with a fallback empty string secret_key = current_app.config.get('SECRET_KEY', '') - + # Extract the fragment (e.g., first three octets of an IPv4 address) ip_fragment = '.'.join(ip_address.split('.')[:3])