Skip to content
Snippets Groups Projects
Commit 60a4155b authored by Remco Tukker's avatar Remco Tukker
Browse files

wrap up locking functionality

parent 14bc8982
No related branches found
No related tags found
1 merge request!57Feature/survey locking system
......@@ -6,6 +6,7 @@ from uuid import uuid4
from flask import Blueprint, request
from flask_login import login_required, current_user # type: ignore
from sqlalchemy import select
from sqlalchemy.orm import lazyload
from compendium_v2.db import db
from compendium_v2.db.model import NREN
......@@ -72,7 +73,8 @@ def try_survey(year) -> Any:
"model": survey.survey,
"data": {},
"page": 0,
"verification_status": {}
"verification_status": {},
"mode": "display"
}
......@@ -110,10 +112,37 @@ def inspect_survey(year) -> Any:
"model": survey.survey,
"data": {},
"page": 0,
"verification_status": {}
"verification_status": {},
"mode": "display"
}
def get_response_data(response, year, nren_id):
data = {}
page = 0
verification_status = {}
locked_by = None
if response and response.locked_by_user:
locked_by = response.locked_by_user.fullname
if response and response.answers:
data = response.answers["data"]
page = response.answers["page"]
verification_status = response.answers["verification_status"]
else:
previous_response = db.session.scalar(
select(SurveyResponse).where(SurveyResponse.survey_year == year-1).where(SurveyResponse.nren_id == nren_id)
)
if previous_response:
# TODO add a 'migration' hook here for updating data per year
previous_response_data: dict = previous_response.answers["data"]
data = previous_response_data
verification_status = {question_name: VerificationStatus.Unverified for question_name in data.keys()}
return data, page, verification_status, locked_by
@routes.route('/load/<int:year>/<string:nren_name>', methods=['GET'])
@common.require_accepts_json
@login_required
......@@ -141,35 +170,69 @@ def load_survey(year, nren_name) -> Any:
if not check_access_nren(current_user, nren):
return {'success': False, 'message': 'You do not have permissions to access this survey.'}, 403
data = {}
page = 0
verification_status = {}
locked_by = None
response = db.session.scalar(
select(SurveyResponse).where(SurveyResponse.survey_year == year).where(SurveyResponse.nren_id == nren.id)
)
previous_response = db.session.scalar(
select(SurveyResponse).where(SurveyResponse.survey_year == year-1).where(SurveyResponse.nren_id == nren.id)
)
if response:
data = response.answers["data"]
page = response.answers["page"]
verification_status = response.answers["verification_status"]
locked_by = response.locked_by_user.fullname if response.locked_by_user else None
elif previous_response:
# TODO add a 'migration' hook here for updating data per year
previous_response_data: dict = previous_response.answers["data"]
data = previous_response_data
verification_status = {question_name: VerificationStatus.Unverified for question_name in data.keys()}
data, page, verification_status, locked_by = get_response_data(response, year, nren.id)
return {
"model": survey.survey,
"locked_by": locked_by,
"data": data,
"page": page,
"verification_status": verification_status
"verification_status": verification_status,
"mode": "display"
}
@routes.route('/lock/<int:year>/<string:nren_name>', methods=['POST'])
@common.require_accepts_json
@login_required
def lock_survey(year, nren_name) -> Any:
nren = db.session.scalar(select(NREN).filter(NREN.name == nren_name))
if not nren:
return {'success': False, 'message': 'NREN not found'}, 404
survey = db.session.scalar(select(Survey).where(Survey.year == year))
if not survey:
return {'success': False, 'message': 'Survey not found'}, 404
if not check_access_nren(current_user, nren):
return {'success': False, 'message': 'You do not have permissions to access this survey.'}, 403
response = db.session.scalar(
select(SurveyResponse).where(SurveyResponse.survey_year == year)
.where(SurveyResponse.nren_id == nren.id)
.options(lazyload("*"))
.with_for_update()
)
data, page, verification_status, locked_by = get_response_data(response, year, nren.id)
# it's possible this is the first time somebody opens the survey for edit, in
# that case we have to create the response:
if response is None:
response = SurveyResponse(survey_year=year, nren_id=nren.id, status=ResponseStatus.started)
db.session.add(response)
if response.locked_by is not None:
return {'success': False, 'message': 'This survey is already locked.'}, 403
lock_uuid = uuid4()
response.lock_uuid = lock_uuid
response.locked_by = current_user.id
db.session.commit()
locked_by = response.locked_by_user.fullname
return {
'success': True,
'lock_uuid': lock_uuid,
'locked_by': locked_by,
'data': data,
'verification_status': verification_status,
"mode": "edit"
}
......@@ -201,8 +264,7 @@ def save_survey(year, nren_name) -> Any:
select(SurveyResponse).where(SurveyResponse.survey_year == year).where(SurveyResponse.nren_id == nren.id)
)
if response is None:
response = SurveyResponse(survey_year=year, nren_id=nren.id, status=ResponseStatus.started)
db.session.add(response)
return {'success': False, 'message': 'Survey response not found'}, 400
save_survey = request.json
if not save_survey:
......@@ -230,7 +292,7 @@ def save_survey(year, nren_name) -> Any:
}
db.session.commit()
mode = "display" if new_state == "readonly" else "edit"
mode = "edit" if new_state == "editing" else "display"
locked_by = response.locked_by_user.fullname if response.locked_by_user else None
return {'success': True, 'locked_by': locked_by, 'mode': mode}
......@@ -268,39 +330,3 @@ def unlock_survey(year, nren_name) -> Any:
db.session.commit()
return {'success': True, 'locked_by': None, 'mode': 'display'}
@routes.route('/lock/<int:year>/<string:nren_name>', methods=['POST'])
@common.require_accepts_json
@login_required
def lock_survey(year, nren_name) -> Any:
# TODO THIS SHOULD ALWAYS BE COMBINED WITH GET SURVEY DATA TO PREVENT RACE CONDITIONS!
nren = db.session.scalar(select(NREN).filter(NREN.name == nren_name))
if nren is None:
return {'success': False, 'message': 'NREN not found'}, 404
survey = db.session.scalar(select(Survey).where(Survey.year == year))
if survey is None:
return {'success': False, 'message': 'Survey not found'}, 404
if not check_access_nren(current_user, nren):
return {'success': False, 'message': 'You do not have permission to edit this survey.'}, 403
response = db.session.scalar(
select(SurveyResponse).where(SurveyResponse.survey_year == year).where(SurveyResponse.nren_id == nren.id)
)
if response is None:
response = SurveyResponse(survey_year=year, nren_id=nren.id, status=ResponseStatus.started)
# TODO this is always going to be the place where new responses are going to be saved for the first time
# so also fill the answer field, so that it always has data
db.session.add(response)
if response.locked_by is not None:
return {'success': False, 'message': 'This survey is already locked.'}, 403
lock_uuid = uuid4()
response.lock_uuid = lock_uuid
response.locked_by = current_user.id
db.session.commit()
return {'success': True, 'lock_uuid': lock_uuid}
......@@ -63,7 +63,7 @@ function SurveyContainerComponent({ loadFrom }) {
survey.currentPageNo = json['page'];
survey.showNavigationButtons = false;
survey.showTOC = false;
survey.mode = 'display';
survey.mode = json['mode'];
survey.lockedBy = json['locked_by']
setSurveyModel(survey);
......@@ -81,7 +81,7 @@ function SurveyContainerComponent({ loadFrom }) {
if (nren == '') {
return;
}
const saveData = {
lock_uuid: lockUUID.current,
new_state: newState,
......@@ -138,7 +138,7 @@ function SurveyContainerComponent({ loadFrom }) {
}
},
'complete': async () => {
const validSurvey = validateWithAnswerVerification(surveyModel.validate);
const validSurvey = validateWithAnswerVerification(surveyModel.validate.bind(surveyModel, true, true));
if (validSurvey) {
const errorMessage = await saveSurveyData(surveyModel, "completed");
if (errorMessage) {
......@@ -163,20 +163,30 @@ function SurveyContainerComponent({ loadFrom }) {
'startEdit': async () => {
const response = await fetch('/api/response/lock/' + year + '/' + nren, {method: "POST"});
const json = await response.json();
console.log(json);
lockUUID.current = json.lock_uuid;
// TODO handle failed lock!
if (!response.ok) {
toast("Failed starting edit: " + json['message']);
return;
}
addEventListener("pagehide", pageHideListener);
addEventListener("beforeunload", beforeUnloadListener, { capture: true });
surveyModel.mode = 'edit';
surveyModel.lockedBy = 'something we get back from the server?';
surveyModel.data = json['data'];
surveyModel.clearIncorrectValues(true);
surveyModel.mode = json['mode'];
surveyModel.lockedBy = json['locked_by']
lockUUID.current = json['lock_uuid'];
},
'releaseLock': async () => {
const response = await fetch('/api/response/unlock/' + year + '/' + nren, { method: 'POST' });
// TODO handle failed release
const json = await response.json();
if (!response.ok) {
toast("Failed releasing lock: " + json['message']);
return;
}
surveyModel.mode = json['mode'];
surveyModel.lockedBy = json['locked_by'];
},
'validatePage': () => {
const validSurvey = validateWithAnswerVerification(surveyModel.validatePage);
const validSurvey = validateWithAnswerVerification(surveyModel.validatePage.bind(surveyModel));
if (validSurvey) {
toast("Page validation successful!");
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment