diff --git a/server/app/apis/__init__.py b/server/app/apis/__init__.py index 683b61c02b3d48caf29e38470d878b3a38f62ae7..181a6dcab59ab8db61eb4c01ed7eddecac75bd69 100644 --- a/server/app/apis/__init__.py +++ b/server/app/apis/__init__.py @@ -55,6 +55,8 @@ def item_response(item, code=http_codes.OK): from flask_restx import Api +from .alternatives import api as alternative_ns +from .answers import api as answer_ns from .auth import api as auth_ns from .codes import api as code_ns from .competitions import api as comp_ns @@ -73,6 +75,8 @@ flask_api.add_namespace(user_ns, path="/api/users") flask_api.add_namespace(auth_ns, path="/api/auth") flask_api.add_namespace(comp_ns, path="/api/competitions") flask_api.add_namespace(slide_ns, path="/api/competitions/<CID>/slides") +flask_api.add_namespace(alternative_ns, path="/api/competitions/<CID>/slides/<SOrder>/questions/<QID>/alternatives") +flask_api.add_namespace(answer_ns, path="/api/competitions/<CID>/teams/<TID>/answers") flask_api.add_namespace(team_ns, path="/api/competitions/<CID>/teams") flask_api.add_namespace(code_ns, path="/api/competitions/<CID>/codes") flask_api.add_namespace(question_ns, path="/api/competitions/<CID>") diff --git a/server/app/apis/alternatives.py b/server/app/apis/alternatives.py new file mode 100644 index 0000000000000000000000000000000000000000..8c247b40da2e33bda5cead7d28ce778f73881c6a --- /dev/null +++ b/server/app/apis/alternatives.py @@ -0,0 +1,47 @@ +import app.core.http_codes as codes +import app.database.controller as dbc +from app.apis import check_jwt, item_response, list_response +from app.core.dto import QuestionAlternativeDTO, QuestionDTO +from app.core.parsers import question_alternative_parser +from app.core.schemas import QuestionAlternativeSchema +from app.database.controller.add import question_alternative +from app.database.controller.get import question_alternatives +from app.database.models import Question, QuestionAlternative +from flask_jwt_extended import jwt_required +from flask_restx import Resource + +api = QuestionAlternativeDTO.api +schema = QuestionAlternativeDTO.schema +list_schema = QuestionAlternativeDTO.list_schema + + +@api.route("/") +@api.param("CID, SOrder, QID") +class QuestionAlternativeList(Resource): + @check_jwt(editor=True) + def get(self, CID, SOrder, QID): + items = dbc.get.question_alternatives(QID) + return list_response(list_schema.dump(items)) + + @check_jwt(editor=True) + def post(self, CID, SOrder, QID): + args = question_alternative_parser.parse_args(strict=True) + item = dbc.add.question_alternative(**args, question_id=QID) + return item_response(schema.dump(item)) + + +@api.route("/<AID>") +@api.param("CID, SOrder, QID, AID") +class QuestionAlternatives(Resource): + @check_jwt(editor=True) + def put(self, CID, SOrder, QID, AID): + args = question_alternative_parser.parse_args(strict=True) + item = dbc.get.one(QuestionAlternative, AID) + item = dbc.edit.question_alternative(item, **args) + return item_response(schema.dump(item)) + + @check_jwt(editor=True) + def delete(self, CID, SOrder, QID, AID): + item = dbc.get.one(QuestionAlternative, AID) + dbc.delete.default(item) + return {}, codes.NO_CONTENT diff --git a/server/app/apis/answers.py b/server/app/apis/answers.py new file mode 100644 index 0000000000000000000000000000000000000000..40e9407fc5d1df9cac874f7d16d28283278adf24 --- /dev/null +++ b/server/app/apis/answers.py @@ -0,0 +1,41 @@ +import app.core.http_codes as codes +import app.database.controller as dbc +from app.apis import check_jwt, item_response, list_response +from app.core.dto import QuestionAnswerDTO +from app.core.parsers import question_answer_edit_parser, question_answer_parser +from app.core.schemas import QuestionAlternativeSchema +from app.database.controller.add import question_alternative +from app.database.controller.get import question_alternatives +from app.database.models import Question, QuestionAlternative, QuestionAnswer +from flask_jwt_extended import jwt_required +from flask_restx import Resource + +api = QuestionAnswerDTO.api +schema = QuestionAnswerDTO.schema +list_schema = QuestionAnswerDTO.list_schema + + +@api.route("/") +@api.param("CID, TID") +class QuestionAnswerList(Resource): + @check_jwt(editor=True) + def get(self, CID, TID): + items = dbc.get.question_answers(TID) + return list_response(list_schema.dump(items)) + + @check_jwt(editor=True) + def post(self, CID, TID): + args = question_answer_parser.parse_args(strict=True) + item = dbc.add.question_answer(**args, team_id=TID) + return item_response(schema.dump(item)) + + +@api.route("/<AID>") +@api.param("CID, TID, AID") +class QuestionAnswers(Resource): + @check_jwt(editor=True) + def put(self, CID, TID, AID): + args = question_answer_edit_parser.parse_args(strict=True) + item = dbc.get.one(QuestionAnswer, AID) + item = dbc.edit.question_answer(item, **args) + return item_response(schema.dump(item)) diff --git a/server/app/apis/questions.py b/server/app/apis/questions.py index f486e3493b09070b5da145c48f497ea30792164e..7d16d3c89e660345c510d84dc645f111dbd7fbf4 100644 --- a/server/app/apis/questions.py +++ b/server/app/apis/questions.py @@ -14,7 +14,7 @@ list_schema = QuestionDTO.list_schema @api.route("/questions") @api.param("CID") -class QuestionsList(Resource): +class QuestionList(Resource): @check_jwt(editor=True) def get(self, CID): items = dbc.get.question_list(CID) @@ -23,7 +23,7 @@ class QuestionsList(Resource): @api.route("/slides/<SID>/questions") @api.param("CID, SID") -class QuestionsList(Resource): +class QuestionListForSlide(Resource): @check_jwt(editor=True) def post(self, SID, CID): args = question_parser.parse_args(strict=True) @@ -37,7 +37,7 @@ class QuestionsList(Resource): @api.route("/slides/<SID>/questions/<QID>") @api.param("CID, SID, QID") -class Questions(Resource): +class QuestionById(Resource): @check_jwt(editor=True) def get(self, CID, SID, QID): item_question = dbc.get.question(CID, SID, QID) diff --git a/server/app/core/dto.py b/server/app/core/dto.py index 034826f5fb784bca39bc85c355d767ccc7ab1498..90e49d00b8062ee3afe6eac7b44e7b397ccfe45c 100644 --- a/server/app/core/dto.py +++ b/server/app/core/dto.py @@ -68,3 +68,15 @@ class QuestionDTO: api = Namespace("questions") schema = schemas.QuestionSchema(many=False) list_schema = schemas.QuestionSchema(many=True) + + +class QuestionAlternativeDTO: + api = Namespace("alternatives") + schema = schemas.QuestionAlternativeSchema(many=False) + list_schema = schemas.QuestionAlternativeSchema(many=True) + + +class QuestionAnswerDTO: + api = Namespace("answers") + schema = schemas.QuestionAnswerSchema(many=False) + list_schema = schemas.QuestionAnswerSchema(many=True) diff --git a/server/app/core/parsers.py b/server/app/core/parsers.py index f5536cf2f1f0f5e0a751bb70257b9f6f35278d4e..45c0a6e34090951cffe29059cfd578b5d732de1a 100644 --- a/server/app/core/parsers.py +++ b/server/app/core/parsers.py @@ -60,7 +60,24 @@ question_parser.add_argument("total_score", type=int, default=None, location="js question_parser.add_argument("type_id", type=int, default=None, location="json") question_parser.add_argument("slide_id", type=int, location="json") -###QUESTION#### + +###QUESTION ALTERNATIVES#### +question_alternative_parser = reqparse.RequestParser() +question_alternative_parser.add_argument("text", type=str, default=None, location="json") +question_alternative_parser.add_argument("value", type=int, default=None, location="json") + +###QUESTION ANSWERS#### +question_answer_parser = reqparse.RequestParser() +question_answer_parser.add_argument("data", type=dict, required=True, location="json") +question_answer_parser.add_argument("score", type=int, required=True, location="json") +question_answer_parser.add_argument("question_id", type=int, required=True, location="json") + +###QUESTION ANSWERS EDIT#### +question_answer_edit_parser = reqparse.RequestParser() +question_answer_edit_parser.add_argument("data", type=dict, default=None, location="json") +question_answer_edit_parser.add_argument("score", type=int, default=None, location="json") + +###CODE#### code_parser = reqparse.RequestParser() code_parser.add_argument("pointer", type=str, default=None, location="json") code_parser.add_argument("view_type_id", type=int, default=None, location="json") diff --git a/server/app/core/rich_schemas.py b/server/app/core/rich_schemas.py index a890489e23a30aef19c41b1cfc105d9954c68d37..8852c392c4a060a065f618a66393e55b8a2627a8 100644 --- a/server/app/core/rich_schemas.py +++ b/server/app/core/rich_schemas.py @@ -20,7 +20,7 @@ class QuestionSchemaRich(RichSchema): total_score = ma.auto_field() slide_id = ma.auto_field() type_id = ma.auto_field() - alternatives = fields.Nested(schemas.QuestionAlternative, many=True) + alternatives = fields.Nested(schemas.QuestionAlternativeSchema, many=True) class TeamSchemaRich(RichSchema): diff --git a/server/app/core/schemas.py b/server/app/core/schemas.py index ff561491ac12644abda6e004efbec12a54becb77..79bdbc1c48fd7460e85fe18f13a744f45a0a57e1 100644 --- a/server/app/core/schemas.py +++ b/server/app/core/schemas.py @@ -62,13 +62,13 @@ class QuestionAnswerSchema(BaseSchema): model = models.QuestionAnswer id = ma.auto_field() - data = ma.auto_field() + data = ma.Function(lambda obj: obj.data) score = ma.auto_field() question_id = ma.auto_field() team_id = ma.auto_field() -class QuestionAlternative(BaseSchema): +class QuestionAlternativeSchema(BaseSchema): class Meta(BaseSchema.Meta): model = models.QuestionAlternative diff --git a/server/app/database/controller/add.py b/server/app/database/controller/add.py index be7a773e9e579669393dadd83527ad779726e8cc..929410fa4e22f01a8a7fa2b9e98c1cf8eb7eee0a 100644 --- a/server/app/database/controller/add.py +++ b/server/app/database/controller/add.py @@ -15,6 +15,8 @@ from app.database.models import ( Media, MediaType, Question, + QuestionAlternative, + QuestionAnswer, QuestionType, Role, Slide, @@ -114,6 +116,14 @@ def question(name, total_score, type_id, item_slide): return db_add(Question(name, total_score, type_id, item_slide.id)) +def question_alternative(text, value, question_id): + return db_add(QuestionAlternative(text, value, question_id)) + + +def question_answer(data, score, question_id, team_id): + return db_add(QuestionAnswer(data, score, question_id, team_id)) + + def code(pointer, view_type_id): """ Adds a code to the database using the provided arguments. """ diff --git a/server/app/database/controller/copy.py b/server/app/database/controller/copy.py index dabd0d7807025f9c908a6840e9e0a64c40b64a57..a6b408ec301e093d6e47bca359968c052ac62aa1 100644 --- a/server/app/database/controller/copy.py +++ b/server/app/database/controller/copy.py @@ -6,10 +6,15 @@ from app.database.controller import add, get, search, utils from app.database.models import Question +def _alternative(item_old, question_id): + """Internal function. Makes a copy of the provided question alternative""" + return add.question_alternative(item_old.text, item_old.value, question_id) + + def _question(item_question_old, slide_id): """ Internal function. Makes a copy of the provided question item to the - specified slide. Does not copy team, question answers or alternatives. + specified slide. Does not copy team, question answers. """ item_question_new = add.db_add( @@ -21,9 +26,8 @@ def _question(item_question_old, slide_id): ) ) - # TODO: Add question alternatives - # for item_alternatives in item_question_old.alternatives: - # dbc.add.alternatives() + for item_alternative in item_question_old.alternatives: + _alternative(item_alternative, item_question_new.id) return item_question_new @@ -48,7 +52,7 @@ def _component(item_component, item_slide_new): def slide(item_slide_old): """ Deep copies a slide to the same competition. - Does not copy team, question answers or alternatives. + Does not copy team, question answers. """ item_competition = get.competition(item_slide_old.competition_id) @@ -59,7 +63,7 @@ def slide(item_slide_old): def slide_to_competition(item_slide_old, item_competition): """ Deep copies a slide to the provided competition. - Does not copy team, question answers or alternatives. + Does not copy team, question answers. """ item_slide_new = add.slide(item_competition) @@ -85,7 +89,7 @@ def slide_to_competition(item_slide_old, item_competition): def competition(item_competition_old): """ Adds a deep-copy of the provided competition. - Will not copy teams, question answers or alternatives. + Will not copy teams, question answers. """ name = "Kopia av " + item_competition_old.name diff --git a/server/app/database/controller/edit.py b/server/app/database/controller/edit.py index 52471badd4edc43ff503436f4f9dbbb65a580c6c..49fcef87a4d28131501786782dbbf95fc58ac1ae 100644 --- a/server/app/database/controller/edit.py +++ b/server/app/database/controller/edit.py @@ -125,3 +125,31 @@ def question(item_question, name=None, total_score=None, type_id=None, slide_id= db.session.refresh(item_question) return item_question + + +def question_alternative(item, text=None, value=None): + + if text: + item.text = text + + if value: + item.value = value + + db.session.commit() + db.session.refresh(item) + + return item + + +def question_answer(item, data=None, score=None): + + if data: + item.data = data + + if score: + item.score = score + + db.session.commit() + db.session.refresh(item) + + return item diff --git a/server/app/database/controller/get.py b/server/app/database/controller/get.py index f7d14914957066b6ac90bad09b16745bc9a59262..45cbb6ec3a137667b74a2bba514a88685c3b3daa 100644 --- a/server/app/database/controller/get.py +++ b/server/app/database/controller/get.py @@ -12,6 +12,8 @@ from app.database.models import ( ComponentType, MediaType, Question, + QuestionAlternative, + QuestionAnswer, QuestionType, Role, Slide, @@ -78,6 +80,16 @@ def question(CID, SOrder, QID, required=True, error_msg=None): return Question.query.join(Slide, join_filters).filter(Question.id == QID).first_extended(required, error_msg) +def question_alternatives(QID): + # join_filters = (Slide.competition_id == CID) & (Slide.order == SOrder) + return QuestionAlternative.query.filter(QuestionAlternative.question_id == QID).all() + + +def question_answers(TID): + # join_filters = (Slide.competition_id == CID) & (Slide.order == SOrder) + return QuestionAnswer.query.filter(QuestionAnswer.team_id == TID).all() + + def competition(CID): """ Get Competition and all it's sub-entities """ """ HOT PATH """ diff --git a/server/app/database/models.py b/server/app/database/models.py index da9e0620b55bf20c6b2ab11bbc4b3ea4a80fe661..2063774f356fab21d53972aa31e542cb056e2bb8 100644 --- a/server/app/database/models.py +++ b/server/app/database/models.py @@ -160,7 +160,7 @@ class Question(db.Model): class QuestionAlternative(db.Model): id = db.Column(db.Integer, primary_key=True) text = db.Column(db.String(STRING_SIZE), nullable=False) - value = db.Column(db.Boolean, nullable=False) + value = db.Column(db.Integer, nullable=False) question_id = db.Column(db.Integer, db.ForeignKey("question.id"), nullable=False) def __init__(self, text, value, question_id): @@ -172,8 +172,9 @@ class QuestionAlternative(db.Model): class QuestionAnswer(db.Model): __table_args__ = (db.UniqueConstraint("question_id", "team_id"),) id = db.Column(db.Integer, primary_key=True) - data = db.Column(db.Text, nullable=False) + data = db.Column(Dictionary(), nullable=False) score = db.Column(db.Integer, nullable=False, default=0) + question_id = db.Column(db.Integer, db.ForeignKey("question.id"), nullable=False) team_id = db.Column(db.Integer, db.ForeignKey("team.id"), nullable=False) diff --git a/server/populate.py b/server/populate.py index 48ed2f955860c048ba62d1b5e4e57bf76358df4d..609193ba0dc5e5a19e3425fa25144d4aabe688d6 100644 --- a/server/populate.py +++ b/server/populate.py @@ -63,13 +63,16 @@ def _add_items(): dbc.utils.commit_and_refresh(item_slide) # Add question to competition - dbc.add.question( + item_question = dbc.add.question( name=f"Question {j}: {question_types_items[j].name}", total_score=j, type_id=question_types_items[j].id, item_slide=item_slide, ) + for i in range(3): + dbc.add.question_alternative(f"Alternative {i}", 0, item_question.id) + # Add text components # TODO: Add images as components for k in range(3): diff --git a/server/tests/test_db.py b/server/tests/test_db.py index 7d162065c8857a24a137b1f55c610f969045fd60..8f211473c14977634c5332874f455b954a2124cf 100644 --- a/server/tests/test_db.py +++ b/server/tests/test_db.py @@ -80,9 +80,11 @@ def check_slides_copy(item_slide_original, item_slide_copy, num_slides, order): assert item_slide_copy.settings == item_slide_original.settings # Checks that all components were correctly copied - assert len(item_slide_copy.components) == len(item_slide_original.components) - for i, c1 in enumerate(item_slide_original.components): - c2 = item_slide_copy.components[i] + components = item_slide_original.components + components_copy = item_slide_copy.components + assert len(components) == len(components_copy) + + for c1, c2 in zip(components, components_copy): assert c1 != c2 assert c1.x == c2.x assert c1.y == c2.y @@ -94,16 +96,28 @@ def check_slides_copy(item_slide_original, item_slide_copy, num_slides, order): assert c1.type_id == c2.type_id # Checks that all questions were correctly copied - assert len(item_slide_copy.questions) == len(item_slide_original.questions) - for i, q1 in enumerate(item_slide_original.questions): - q2 = item_slide_copy.questions[i] + questions = item_slide_original.questions + questions_copy = item_slide_copy.questions + assert len(questions) == len(questions_copy) + + for q1, q2 in zip(questions, questions_copy): assert q1 != q2 assert q1.name == q2.name assert q1.total_score == q2.total_score assert q1.type_id == q2.type_id assert q1.slide_id == item_slide_original.id assert q2.slide_id == item_slide_copy.id - # TODO: Assert alternatives + + # Assert alternatives + alternatives = q1.alternatives + alternatives_copy = q2.alternatives + assert len(alternatives) == len(alternatives_copy) + + for a1, a2 in zip(alternatives, alternatives_copy): + assert a1.text == a2.text + assert a1.value == a2.value + assert a1.quesiton_id == q1.id + assert a2.quesiton_id == q2.id # Checks that the copy put the slide in the database item_slides, total = dbc.search.slide(