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

split survey endpoints over two files and send lock uid up and down

parent ff27bb68
No related branches found
No related tags found
1 merge request!57Feature/survey locking system
......@@ -14,6 +14,7 @@ from sqlalchemy.types import JSON
from compendium_v2.db import db
from compendium_v2.db.auth_model import User
from compendium_v2.db.model import NREN
......@@ -37,6 +38,8 @@ class SurveyStatus(Enum):
published = "published"
# TODO The checked status is currently not used
# see if we want to keep the checked survey status or control what is published in some other way..
class ResponseStatus(Enum):
started = "started"
completed = "completed"
......@@ -60,4 +63,5 @@ class SurveyResponse(db.Model):
answers: Mapped[json]
status: Mapped[ResponseStatus]
locked_by: Mapped[uuid_nullable_fkUser]
locked_by_user: Mapped[User] = relationship(lazy='joined')
lock_uuid: Mapped[Optional[UUID]]
import logging
from enum import Enum
from typing import Any
from uuid import uuid4
from flask import Blueprint, request
from flask_login import login_required, current_user # type: ignore
from sqlalchemy import select
from compendium_v2.db import db
from compendium_v2.db.model import NREN
from compendium_v2.db.survey_model import Survey, SurveyResponse, SurveyStatus, ResponseStatus
from compendium_v2.routes import common
from compendium_v2.auth.session_management import admin_required, User
routes = Blueprint('response', __name__)
logger = logging.getLogger(__name__)
SURVEY_RESPONSE_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'type': 'object',
'properties': {
'model': {'type': 'object'},
'data': {'type': 'object'},
'page': {'type': 'number'},
'verification_status': {'type': 'object'}
},
'required': ['model', 'data', 'page', 'verification_status'],
'additionalProperties': False
}
class VerificationStatus(str, Enum):
New = "new" # a question that was not answered last year
Answered = "answered" # a question that was not answered last year but has an answer now
Unverified = "unverified" # a question that has its answered copied from last year
Verified = "verified" # a question for which last years answer was verified
Edited = "edited" # a question for which last years answer was edited
def check_access_nren(user: User, nren: str) -> bool:
if user.is_anonymous:
return False
if user.is_admin:
return True
if nren == user.nren:
return True
return False
@routes.route('/try/<int:year>', methods=['GET'])
@common.require_accepts_json
@admin_required
def try_survey(year) -> Any:
"""
Get a survey without any associated nren for trying out the survey.
The survey will behave exactly as when an NREN opens it.
response will be formatted as:
.. asjson::
compendium_v2.routes.survey.SURVEY_RESPONSE_SCHEMA
"""
survey = db.session.scalar(select(Survey).where(Survey.year == year))
if not survey:
return {'success': False, 'message': 'Survey not found'}, 404
return {
"model": survey.survey,
"data": {},
"page": 0,
"verification_status": {}
}
@routes.route('/inspect/<int:year>', methods=['GET'])
@common.require_accepts_json
@admin_required
def inspect_survey(year) -> Any:
"""
Get a survey without any associated nren for inspecting all questions.
All questions are made visible and any VisibleIf condition added to the title.
response will be formatted as:
.. asjson::
compendium_v2.routes.survey.SURVEY_RESPONSE_SCHEMA
"""
survey = db.session.scalar(select(Survey).where(Survey.year == year))
if not survey:
return {'success': False, 'message': 'Survey not found'}, 404
def visible_visitor(object, items):
for key, value in items:
if isinstance(value, dict):
visible_visitor(value, value.items())
elif isinstance(value, list):
visible_visitor(value, enumerate(value))
elif key == 'visibleIf':
object['title'] = object['title'] + ' (visibleif: [' + value.replace('{', '#').replace('}', '#') + '])'
object[key] = 'true'
visible_visitor(survey.survey, survey.survey.items())
return {
"model": survey.survey,
"data": {},
"page": 0,
"verification_status": {}
}
@routes.route('/load/<int:year>/<string:nren_name>', methods=['GET'])
@common.require_accepts_json
@login_required
def load_survey(year, nren_name) -> Any:
"""
Get a survey for an nren.
If the survey was saved before, that data will be loaded.
If the survey was not saved before and the survey was completed last year,
the data from last year will be prefilled.
response will be formatted as:
.. asjson::
compendium_v2.routes.survey.SURVEY_RESPONSE_SCHEMA
"""
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
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()}
return {
"model": survey.survey,
"locked_by": locked_by,
"data": data,
"page": page,
"verification_status": verification_status
}
@routes.route('/save/<int:year>/<string:nren_name>', methods=['POST'])
@common.require_accepts_json
@login_required
def save_survey(year, nren_name) -> Any:
"""
endpoint to save a survey response
:returns: ``{'success': True}`` or a 400 or 404 status with a descriptive message
"""
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
if survey.status != SurveyStatus.open and not current_user.is_admin:
return {'success': False, 'message': 'Survey is closed'}, 400
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)
db.session.add(response)
save_survey = request.json
if not save_survey:
return {'success': False, 'message': 'Invalid Survey Format'}, 400
lock_uuid = save_survey["lock_uuid"]
if (lock_uuid != str(response.lock_uuid)):
return {'success': False, 'message': 'Somebody else is currently editing this survey.'}, 403
new_state = save_survey["new_state"]
if (new_state == "editing"):
pass
elif (new_state == "completed"):
response.locked_by = None
response.status = ResponseStatus.completed
elif (new_state == "readonly"):
response.locked_by = None
else:
return {'success': False, 'message': 'Invalid new_state paramater'}, 400
response.answers = {
"data": save_survey["data"],
"page": save_survey["page"],
"verification_status": save_survey["verification_status"]
}
db.session.commit()
return {'success': True}
@routes.route('/unlock/<int:year>/<string:nren_name>', methods=['POST'])
@common.require_accepts_json
@login_required
def unlock_survey(year, nren_name) -> Any:
"""
endpoint to release a lock on a survey response without saving any data
:returns: ``{'success': True}`` or a 400 or 404 status with a descriptive message
"""
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:
return {'success': False, 'message': 'Survey response not found'}, 404
if current_user.id != response.locked_by and not current_user.is_admin:
return {'success': False, 'message': 'This survey was not locked by you.'}, 403
response.locked_by = None # we leave lock_uuid, it will be overwritten when a new lock is acquired
db.session.commit()
return {'success': True}
@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}
import logging
from enum import Enum
from typing import Any, TypedDict, List, Dict
from uuid import uuid4
from flask import Blueprint, jsonify, request
from flask_login import login_required, current_user # type: ignore
from flask import Blueprint
from sqlalchemy import select
from sqlalchemy.orm import joinedload, load_only
......@@ -12,7 +9,7 @@ from compendium_v2.db import db
from compendium_v2.db.model import NREN
from compendium_v2.db.survey_model import Survey, SurveyResponse, SurveyStatus, ResponseStatus
from compendium_v2.routes import common
from compendium_v2.auth.session_management import admin_required, User
from compendium_v2.auth.session_management import admin_required
routes = Blueprint('survey', __name__)
......@@ -47,38 +44,6 @@ LIST_SURVEYS_RESPONSE_SCHEMA = {
}
SURVEY_RESPONSE_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'type': 'object',
'properties': {
'model': {'type': 'object'},
'data': {'type': 'object'},
'page': {'type': 'number'},
'verification_status': {'type': 'object'}
},
'required': ['model', 'data', 'page', 'verification_status'],
'additionalProperties': False
}
class VerificationStatus(str, Enum):
New = "new" # a question that was not answered last year
Answered = "answered" # a question that was not answered last year but has an answer now
Unverified = "unverified" # a question that has its answered copied from last year
Verified = "verified" # a question for which last years answer was verified
Edited = "edited" # a question for which last years answer was edited
def check_access_nren(user: User, nren: str) -> bool:
if user.is_anonymous:
return False
if user.is_admin:
return True
if nren == user.nren:
return True
return False
@routes.route('/list', methods=['GET'])
@common.require_accepts_json
@admin_required
......@@ -129,7 +94,7 @@ def list_surveys() -> Any:
for nren_name in sorted(nren_names.difference(nrens_with_responses), key=str.lower):
entry["responses"].append({"nren": nren_name, "status": "not started"})
return jsonify(entries)
return entries
@routes.route('/new', methods=['POST'])
......@@ -143,17 +108,17 @@ def start_new_survey() -> Any:
"""
all_surveys = db.session.scalars(select(Survey).options(load_only(Survey.status)))
if any([survey.status != SurveyStatus.published for survey in all_surveys]):
return jsonify({
return {
'success': False,
'message': 'All earlier surveys should be published before starting a new one'
}), 400
}, 400
last_survey = db.session.scalar(
select(Survey).order_by(Survey.year.desc()).limit(1)
)
if not last_survey:
return jsonify({'success': False, 'message': 'No surveys found'}), 404
return {'success': False, 'message': 'No surveys found'}, 404
new_year = last_survey.year + 1
new_survey = last_survey.survey
......@@ -175,14 +140,14 @@ def open_survey(year) -> Any:
"""
survey = db.session.scalar(select(Survey).where(Survey.year == year))
if not survey:
return jsonify({'success': False, 'message': 'Survey not found'}), 404
return {'success': False, 'message': 'Survey not found'}, 404
if survey.status != SurveyStatus.closed:
return jsonify({'success': False, 'message': 'Survey is not closed and can therefore not be opened'}), 400
return {'success': False, 'message': 'Survey is not closed and can therefore not be opened'}, 400
all_surveys = db.session.scalars(select(Survey))
if any([s.status == SurveyStatus.open for s in all_surveys]):
return jsonify({'success': False, 'message': 'There already is an open survey'}), 400
return {'success': False, 'message': 'There already is an open survey'}, 400
survey.status = SurveyStatus.open
db.session.commit()
......@@ -201,10 +166,10 @@ def close_survey(year) -> Any:
"""
survey = db.session.scalar(select(Survey).where(Survey.year == year))
if not survey:
return jsonify({'success': False, 'message': 'Survey not found'}), 404
return {'success': False, 'message': 'Survey not found'}, 404
if survey.status != SurveyStatus.open:
return jsonify({'success': False, 'message': 'Survey is not open and can therefore not be closed'}), 400
return {'success': False, 'message': 'Survey is not open and can therefore not be closed'}, 400
survey.status = SurveyStatus.closed
db.session.commit()
......@@ -223,16 +188,17 @@ def publish_survey(year) -> Any:
"""
survey = db.session.scalar(select(Survey).where(Survey.year == year))
if not survey:
return jsonify({'success': False, 'message': 'Survey not found'}), 404
return {'success': False, 'message': 'Survey not found'}, 404
if survey.status not in [SurveyStatus.closed, SurveyStatus.published]:
return jsonify({
return {
'success': False,
'message': 'Survey is not closed or published and can therefore not be published'
}), 400
}, 400
# TODO probably replace checked state with something else
if any([response.status != ResponseStatus.checked for response in survey.responses]):
return jsonify({'success': False, 'message': 'There are responses that arent checked yet'}), 400
return {'success': False, 'message': 'There are responses that arent checked yet'}, 400
# TODO call new survey_publisher with all responses and the year
......@@ -240,235 +206,3 @@ def publish_survey(year) -> Any:
db.session.commit()
return {'success': True}
@routes.route('/try/<int:year>', methods=['GET'])
@common.require_accepts_json
@admin_required
def try_survey(year) -> Any:
"""
Get a survey without any associated nren for trying out the survey.
The survey will behave exactly as when an NREN opens it.
response will be formatted as:
.. asjson::
compendium_v2.routes.survey.SURVEY_RESPONSE_SCHEMA
"""
survey = db.session.scalar(select(Survey).where(Survey.year == year))
if not survey:
return jsonify({'success': False, 'message': 'Survey not found'}), 404
return jsonify({
"model": survey.survey,
"data": {},
"page": 0,
"verification_status": {}
})
@routes.route('/inspect/<int:year>', methods=['GET'])
@common.require_accepts_json
@admin_required
def inspect_survey(year) -> Any:
"""
Get a survey without any associated nren for inspecting all questions.
All questions are made visible and any VisibleIf condition added to the title.
response will be formatted as:
.. asjson::
compendium_v2.routes.survey.SURVEY_RESPONSE_SCHEMA
"""
survey = db.session.scalar(select(Survey).where(Survey.year == year))
if not survey:
return jsonify({'success': False, 'message': 'Survey not found'}), 404
def visible_visitor(object, items):
for key, value in items:
if isinstance(value, dict):
visible_visitor(value, value.items())
elif isinstance(value, list):
visible_visitor(value, enumerate(value))
elif key == 'visibleIf':
object['title'] = object['title'] + ' (visibleif: [' + value.replace('{', '#').replace('}', '#') + '])'
object[key] = 'true'
visible_visitor(survey.survey, survey.survey.items())
return jsonify({
"model": survey.survey,
"data": {},
"page": 0,
"verification_status": {}
})
@routes.route('/load/<int:year>/<string:nren_name>', methods=['GET'])
@common.require_accepts_json
@login_required
def load_survey(year, nren_name) -> Any:
"""
Get a survey for an nren.
If the survey was saved before, that data will be loaded.
If the survey was not saved before and the survey was completed last year,
the data from last year will be prefilled.
response will be formatted as:
.. asjson::
compendium_v2.routes.survey.SURVEY_RESPONSE_SCHEMA
"""
nren = db.session.scalar(select(NREN).filter(NREN.name == nren_name))
if not nren:
return jsonify({'success': False, 'message': 'NREN not found'}), 404
survey = db.session.scalar(select(Survey).where(Survey.year == year))
if not survey:
return jsonify({'success': False, 'message': 'Survey not found'}), 404
if not check_access_nren(current_user, nren):
return jsonify({'success': False, 'message': 'You do not have permissions to access this survey.'}), 403
data = {}
page = 0
verification_status = {}
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"]
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()}
return jsonify({
"model": survey.survey,
"data": data,
"page": page,
"verification_status": verification_status
})
@routes.route('/save/<int:year>/<string:nren_name>', methods=['POST'])
@common.require_accepts_json
@login_required
def save_survey(year, nren_name) -> Any:
"""
endpoint to save a survey response
:returns: ``{'success': True}`` or a 400 or 404 status with a descriptive message
"""
nren = db.session.scalar(select(NREN).filter(NREN.name == nren_name))
if nren is None:
return jsonify({'success': False, 'message': 'NREN not found'}), 404
survey = db.session.scalar(select(Survey).where(Survey.year == year))
if survey is None:
return jsonify({'success': False, 'message': 'Survey not found'}), 404
if not check_access_nren(current_user, nren):
return jsonify({'success': False, 'message': 'You do not have permission to edit this survey.'}), 403
if survey.status != SurveyStatus.open and not current_user.is_admin:
return jsonify({'success': False, 'message': 'Survey is closed'}), 400
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)
db.session.add(response)
save_survey = request.json
if not save_survey:
return jsonify({'success': False, 'message': 'Invalid Survey Format'}), 400
# TODO check if the lock_uuid matches
# TODO add a 'save and stop editing' and a 'complete' field and remove the lock in those two cases
# TODO the checked ResponseStatus should probably be removed
response.answers = {
"data": save_survey["data"],
"page": save_survey["page"],
"verification_status": save_survey["verification_status"]
}
db.session.commit()
return {'success': True}
@routes.route('/unlock/<int:year>/<string:nren_name>', methods=['POST'])
@common.require_accepts_json
@login_required
def unlock_survey(year, nren_name) -> Any:
nren = db.session.scalar(select(NREN).filter(NREN.name == nren_name))
if nren is None:
return jsonify({'success': False, 'message': 'NREN not found'}), 404
survey = db.session.scalar(select(Survey).where(Survey.year == year))
if survey is None:
return jsonify({'success': False, 'message': 'Survey not found'}), 404
if not check_access_nren(current_user, nren):
return jsonify({'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:
return jsonify({'success': False, 'message': 'Survey response not found'}), 404
if not current_user.is_admin and current_user.id != response.locked_by:
return jsonify({'success': False, 'message': 'This survey was not locked by you.'}), 403
response.locked_by = None
# we leave lock_uuid, it will be overwritten when a new lock is acquired
db.session.commit()
return {'success': True}
@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 nren is None:
return jsonify({'success': False, 'message': 'NREN not found'}), 404
survey = db.session.scalar(select(Survey).where(Survey.year == year))
if survey is None:
return jsonify({'success': False, 'message': 'Survey not found'}), 404
if not check_access_nren(current_user, nren):
return jsonify({'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)
db.session.add(response)
if response.locked_by is not None:
return jsonify({'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}
......@@ -17,9 +17,9 @@ function App(): ReactElement {
<Routes>
<Route path="survey/admin/surveys" element={<SurveyManagementComponent />} />
<Route path="survey/admin/users" element={<UserManagementComponent />} />
<Route path="survey/admin/inspect/:year" element={<SurveyContainerComponent loadFrom={'/api/survey/inspect/'} />} />
<Route path="survey/admin/try/:year" element={<SurveyContainerComponent loadFrom={'/api/survey/try/'} />} />
<Route path="survey/respond/:year/:nren" element={<SurveyContainerComponent loadFrom={'/api/survey/load/'} saveTo={'/api/survey/save/'} />} />
<Route path="survey/admin/inspect/:year" element={<SurveyContainerComponent loadFrom={'/api/response/inspect/'} />} />
<Route path="survey/admin/try/:year" element={<SurveyContainerComponent loadFrom={'/api/response/try/'} />} />
<Route path="survey/respond/:year/:nren" element={<SurveyContainerComponent loadFrom={'/api/response/load/'} saveTo={'/api/response/save/'} />} />
<Route path="*" element={<Landing />} />
</Routes>
</UserProvider>
......
......@@ -18,6 +18,7 @@ function SurveyContainerComponent({ loadFrom, saveTo = '' }) {
const verificationStatus = useRef<Map<string, VerificationStatus>>(new Map());
const { year, nren } = useParams();
const [error, setError] = useState<string>('loading survey...');
const lockUUID = useRef<string>();
useEffect(() => {
async function getModel() {
......@@ -48,6 +49,7 @@ function SurveyContainerComponent({ loadFrom, saveTo = '' }) {
survey.showNavigationButtons = false;
survey.showTOC = false;
survey.mode = 'display';
survey.lockedBy = json['locked_by']
setSurveyModel(survey);
}
......@@ -60,7 +62,7 @@ function SurveyContainerComponent({ loadFrom, saveTo = '' }) {
return error
}
function saveSurveyData(survey, success?, failure?) {
function saveSurveyData(survey, newState, success?, failure?) {
if (saveTo == '') {
return;
}
......@@ -74,7 +76,10 @@ function SurveyContainerComponent({ loadFrom, saveTo = '' }) {
failure();
}
}
console.log(lockUUID.current);
const saveData = {
lock_uuid: lockUUID.current,
new_state: newState,
data: survey.data,
page: survey.currentPageNo,
verification_status: Object.fromEntries(verificationStatus.current)
......@@ -97,7 +102,7 @@ function SurveyContainerComponent({ loadFrom, saveTo = '' }) {
const validSurvey = surveyModel.validate();
surveyModel.onValidateQuestion.remove(verificationValidator);
if (validSurvey) {
saveSurveyData(surveyModel, () => toast("Survey completed!"), () => toast("Failed completing survey!"));
saveSurveyData(surveyModel, "completed", () => toast("Survey completed!"), () => toast("Failed completing survey!"));
} else {
surveyModel.focusQuestion(firstValidationError);
}
......@@ -131,29 +136,37 @@ function SurveyContainerComponent({ loadFrom, saveTo = '' }) {
};
const pageHideListener = (event) => {
window.navigator.sendBeacon('/api/survey/unlock/' + year + '/' + nren, JSON.stringify({some: "data"}))
window.navigator.sendBeacon('/api/response/unlock/' + year + '/' + nren, JSON.stringify({some: "data"}))
}
const doSurveyAction = (action) => {
const doSurveyAction = async (action) => {
switch(action) {
case 'save':
saveSurveyData(surveyModel, () => toast("Survey saved!"), () => toast("Failed saving survey!"));
saveSurveyData(surveyModel, "editing", () => toast("Survey saved!"), () => toast("Failed saving survey!"));
break;
case 'complete':
endSurvey();
break;
case 'saveAndStopEdit':
saveSurveyData(surveyModel, () => toast("Survey saved!"), () => toast("Failed saving survey!")); // TODO include stop editing
saveSurveyData(surveyModel, "readonly", () => toast("Survey saved!"), () => toast("Failed saving survey!"));
surveyModel.mode = 'display';
surveyModel.lockedBy = '';
removeEventListener("beforeunload", beforeUnloadListener, { capture: true });
removeEventListener("pagehide", pageHideListener);
break;
case 'startEdit':
// TODO lock http request
case 'startEdit': {
const response = await fetch('/api/survey/lock/' + year + '/' + nren, {method: "POST"});
const json = await response.json();
console.log(json);
lockUUID.current = json.lock_uuid;
// TODO handle failed lock
addEventListener("pagehide", pageHideListener);
addEventListener("beforeunload", beforeUnloadListener, { capture: true });
surveyModel.mode = 'edit';
surveyModel.lockedBy = 'something we get back from the server?';
break;
}
}
}
......
import React, { useEffect, useState } from "react";
import React, { useContext, useEffect, useState } from "react";
import ProgressBar from './ProgressBar';
import { Container, Row } from "react-bootstrap";
import { userContext } from "./providers/UserProvider";
function SurveyNavigationComponent({ surveyModel, doSurveyAction, validatePage, children }) {
const [pageNo, setPageNo] = useState(0);
const [editing, setEditing] = useState(false);
const [lockedBy, setLockedBy] = useState("");
const { user: loggedInUser } = useContext(userContext);
useEffect(() => {
setPageNo(surveyModel.currentPageNo);
setEditing(surveyModel.mode == 'edit');
setLockedBy(surveyModel.lockedBy);
setPageNo(surveyModel.currentPageNo);
}, [surveyModel]);
const pageNoSetter = (page) => {
......@@ -28,25 +32,34 @@ function SurveyNavigationComponent({ surveyModel, doSurveyAction, validatePage,
);
};
const startEdit = () => {
doSurveyAction('startEdit');
const startEdit = async () => {
await doSurveyAction('startEdit');
setEditing(surveyModel.mode == 'edit');
setLockedBy(surveyModel.lockedBy);
}
const saveAndStopEdit = () => {
doSurveyAction('saveAndStopEdit');
const saveAndStopEdit = async () => {
await doSurveyAction('saveAndStopEdit');
setEditing(surveyModel.mode == 'edit');
setLockedBy(surveyModel.lockedBy);
}
const save = () => doSurveyAction('save');
const complete = () => doSurveyAction('complete');
const save = async () => await doSurveyAction('save');
const complete = async () => {
await doSurveyAction('complete');
setEditing(surveyModel.mode == 'edit');
setLockedBy(surveyModel.lockedBy);
setPageNo(surveyModel.currentPageNo);
};
const renderExternalNavigation = () => {
return (
<div className="navigation-block">
<div className="navigation-progress-container">
<div className="navigation-buttons-container">
{!editing && renderButton('Start editing', startEdit)}
{!editing && !lockedBy && renderButton('Start editing', startEdit)}
{!editing && lockedBy && lockedBy != loggedInUser.name && renderButton('Cannot edit, locked by: ' + lockedBy, () => {})}
{!editing && lockedBy && lockedBy == loggedInUser.name && renderButton('Release your lock [TODO add explanation/warning]', () => {})}
{editing && (pageNo === surveyModel.visiblePages.length - 1) && renderButton('Complete Survey', complete)}
{editing && renderButton('Save and stop editing', saveAndStopEdit)}
{editing && renderButton('Save progress', save)}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment