diff --git a/wcsudat/accounts/tests.py b/wcsudat/accounts/tests.py index 9524ee9b9acdfca87a8efd99517f87f1fae901a9..93de1659fad57d75f388b11b5b21779114bcfca3 100644 --- a/wcsudat/accounts/tests.py +++ b/wcsudat/accounts/tests.py @@ -7,8 +7,9 @@ class LoginViewTests(TestCase): response = self.client.post('/accounts/login/', {'username': 'test', 'password': 'Dj@ng0U$3r'}) self.assertEqual(response.status_code, HTTPStatus.FOUND) response = self.client.get('/surveys/') - self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, "You are logged in as test") + self.assertEqual(response.status_code, HTTPStatus.FOUND) + # Should redirect to access page since the user doesn't have access to a group yet + self.assertEqual(str(response.url), "/surveys/access/") def test_empty_login(self): response = self.client.post('/accounts/login/', {'username': '', 'password': ''}) diff --git a/wcsudat/surveys/admin.py b/wcsudat/surveys/admin.py index 75941ed032721ad86af6e32081850174ea7a9c19..2156063a2c213c613e07bbcac8558281a5c214a0 100644 --- a/wcsudat/surveys/admin.py +++ b/wcsudat/surveys/admin.py @@ -2,7 +2,9 @@ from django.contrib import admin from django.urls import reverse from django.utils.html import format_html -# Register your models here. +# This file controls how the database tables will be displayed on the admin page +# Fields are the editable fields in a model page +# List display is a list of fields to display on the model table page from .models import * from .views import calculate_risk @@ -28,6 +30,9 @@ class QuestionInline(admin.TabularInline): extra = 0 def edit(self, instance): + ''' + This creates an edit button so that the Question can be accessed from the inline + ''' url = reverse('admin:%s_%s_change' % \ (instance._meta.app_label, instance._meta.model_name), args=(instance.id,)) @@ -37,8 +42,11 @@ class ChoiceQuestionInline(admin.TabularInline): model = ChoiceQuestion readonly_fields = ('edit',) extra = 0 - + def edit(self, instance): + ''' + This creates an edit button so that the Choice Question can be accessed from the inline + ''' url = reverse('admin:%s_%s_change' % \ (instance._meta.app_label, instance._meta.model_name), args=(instance.id,)) @@ -50,6 +58,9 @@ class SubsectionInline(admin.TabularInline): extra = 0 def edit(self, instance): + ''' + This creates an edit button so that the Subsection can be accessed from the inline + ''' url = reverse('admin:%s_%s_change' % \ (instance._meta.app_label, instance._meta.model_name), args=(instance.id,)) @@ -62,6 +73,9 @@ class SectionInline(admin.TabularInline): extra = 0 def edit(self, instance): + ''' + This creates an edit button so that the Section can be accessed from the inline + ''' url = reverse('admin:%s_%s_change' % \ (instance._meta.app_label, instance._meta.model_name), args=(instance.id,)) diff --git a/wcsudat/surveys/apps.py b/wcsudat/surveys/apps.py index 840a0c6ddeeb0d6427e4275f457f7d431407479a..22d48acce01c404317407bb8a3e9f4437094961f 100644 --- a/wcsudat/surveys/apps.py +++ b/wcsudat/surveys/apps.py @@ -1,5 +1,5 @@ from django.apps import AppConfig - +# Default django app config class SurveysConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' diff --git a/wcsudat/surveys/models.py b/wcsudat/surveys/models.py index 7104391410032f6426baa151620b6a0c2065ff72..139dc8a39c3ac49a5f98fe84ad38e890a546c9af 100644 --- a/wcsudat/surveys/models.py +++ b/wcsudat/surveys/models.py @@ -9,6 +9,11 @@ from datetime import datetime import time from colorfield.fields import ColorField +# The classes in this file dictate the database configuration. +# Each class is related to a table in the database +# This helps to enforce third normal form. +# For instance a foreignkey stores the id of the associated object whereas a manytomany +# field creates a new table to store relationships. class Feedback(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL, blank=True) @@ -17,7 +22,6 @@ class Feedback(models.Model): return f"{str(self.user)}: {self.feedback_text}" class Client(models.Model): - # TODO Maybe change this to distinct choices for normalized data clinician = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL, blank=True) name = models.CharField(max_length=200) surname = models.CharField(max_length=200) @@ -63,11 +67,17 @@ class Risk(models.Model): return self.risk_category def set_min_max(self): + ''' + This function calculates the miniumum and maximum possible + values for the risk based on the questions in the assessment + ''' mn = 0 mx = 0 questions = ChoiceQuestion.objects.filter(risks__in=[self]) for question in questions: numbers = [choice.risk_amount for choice in question.choices.all()] + if(len(numbers) == 0): + continue mn += min(numbers) mx += max(numbers) self.minimum = mn @@ -75,8 +85,7 @@ class Risk(models.Model): self.save() def get_amount(self): - # TODO Make this work. We want to quickly query a sum of choice answer risk amounts for all answers in a survey answer - # grouped by the risk group + # TODO Remove survey_answer = SurveyAnswer.objects.all().order_by('-date_time')[0] # choicequestion__answer__subsection_answer__survey_answer=survey_answer sum = Choice.objects.filter(answer__subsection_answer__survey_answer=survey_answer, answer__choice_question__risks__in=[self]).annotate(sum_total=Sum('risk_amount')) @@ -99,7 +108,7 @@ class Survey(models.Model): def __str__(self): return self.survey_text - + # TODO Remove these def add_section(self, section_text): pass @@ -110,6 +119,9 @@ class Survey(models.Model): pass def section_count(self): + ''' + This function counts the number of sections in a survey + ''' sections = Survey.objects.annotate(num_sections=Count('section')).filter(id=self.id,) return sections[0].num_sections if len(sections) > 0 else 0 @@ -127,6 +139,9 @@ class Section(models.Model): return str(self.order) def subsection_count(self): + ''' + This function counts the number of subsections in a section + ''' subsections = Section.objects.annotate(num_subsections=Count('subsection')).filter(id=self.id,) return subsections[0].num_subsections if len(subsections) > 0 else 0 @@ -145,19 +160,26 @@ class Subsection(models.Model): def ordering(self): return '{}.{}'.format(self.section.ordering() if self.section else 'x', self.order) - ''' - Return a comma separated string of risk categories in this section - ''' + # TODO Remove def risk_factors(self): + ''' + Return a comma separated string of risk categories in this section + ''' # TODO Remove duplicates from this list # This could be changed to return the queryset for further processing return ', '.join([str(r.risks) for r in ChoiceQuestion.objects.filter(subsection_id=self.id,)]) def question_count(self): + ''' + This function counts the number of questions in a subsection + ''' questions = Subsection.objects.annotate(num_questions=Count('question')).filter(id=self.id,) return questions[0].num_questions if len(questions) > 0 else 0 def choice_question_count(self): + ''' + This function counts the number of questions in a subsection + ''' choice_questions = Subsection.objects.annotate(num_choice_questions=Count('choicequestion')).filter(id=self.id,) return choice_questions[0].num_choice_questions if len(choice_questions) > 0 else 0 @@ -205,17 +227,19 @@ class ChoiceQuestion(models.Model): def __str__(self): return self.question_text # self.ordering() + ' ' + - ''' - Get all of the choice options as comma seperated strings - ''' + # TODO Maybe remove these def choice_options(self): + ''' + Get all of the choice options as comma seperated strings + ''' # This could be changed to return the queryset for further processing return ', '.join([str(ch.choice_text) for ch in Choice.objects.filter(choicequestion=self,)]) - ''' - Get all of the risks options as comma seperated strings - ''' + def risk_options(self): + ''' + Get all of the risks options as comma seperated strings + ''' # This could be changed to return the queryset for further processing return ', '.join([str(r.risk_category) for r in Risk.objects.filter(choicequestion=self,)]) @@ -237,15 +261,27 @@ class SurveyAnswer(models.Model): return self.date_time.strftime("%H:%M:%S") def close(self): + ''' + This function closes a survey and sets the necessary fields + ''' + # We assume that if we were able to close the survey + # then all the visible surveys are complete self.complete_subsections = len(SubsectionAnswer.special_objects.visible_in_survey(self)) self.date_time = Now() self.in_progress = False self.save() def since(self): + ''' + This function finds the largest non-zero time period which has passed since the survey was closed + It then returns the number of these that have passed + ''' if self.in_progress: return "In Progress" + # Get the time delta as total seconds t = int((timezone.now() - self.date_time).total_seconds()) + # Parse as an epoch time. We then just need to calculate the + # time units which have passed since 1970/1/1 00:00:00 t = time.localtime(t) years = t[0] - 1970 if years > 1: @@ -286,9 +322,14 @@ class SurveyAnswer(models.Model): return "In Progress" def risks(self): + ''' + This function creates a dictionary of the format + Risk Groups: {Risks: {Risk Levels}} + and all of their associated data + Most importantly it sums the risk amount from each answer in the survey + and adds this to the dictionary + ''' res_dict = {} - - # TODO Optimise for riskgroup in RiskGroup.objects.all().order_by('order'): res_dict[riskgroup.order] = {} res_dict[riskgroup.order][riskgroup.id] = {} @@ -325,6 +366,10 @@ class SurveyAnswer(models.Model): class SubsectionAnswerManager(models.Manager): def visible_in_survey(self, survey_answer): + ''' + Return a queryset of Subsection Answers in a given + survey_answer if they return true for show_subsection + ''' subsection_answers = SubsectionAnswer.objects.filter(survey_answer=survey_answer).order_by('subsection__section__order','subsection__order') ids = [answer.id for answer in subsection_answers if answer.show_subsection()] subsection_answers = subsection_answers.filter(id__in=ids) @@ -340,6 +385,15 @@ class SubsectionAnswer(models.Model): special_objects = SubsectionAnswerManager() def show_subsection(self): + ''' + This function checks to see if the subsection should be shown. + It does this by summing the risk amount for every answer with the same + risk type as the risk level requirement. + It then checks that the sum of the risk amount is greater than or equal to the + threshold amount. + ''' + if not self.subsection: + return False requirement = self.subsection.risk_level_requirement if not requirement: return True @@ -357,18 +411,19 @@ class SubsectionAnswer(models.Model): return str(self.subsection) + " answer" class AnswerManager(models.Manager): + # TODO remove def visible_in_subsection(self, subsection_answer): answers = Answer.objects.filter(subsection_answer=subsection_answer).order_by('order') ids = [answer.id for answer in answers if answer.show_question()] answers = answers.filter(id__in=ids) return answers + # TODO remove def incomplete_visible_in_subsection(self, subsection_answer): answers = self.visible_in_subsection(subsection_answer) ids = [answer.id for answer in answers if str(answer.answer()) == "" or answer.answer() == None] answers = answers.filter(id__in=ids) return answers - def has_answer(self): answers = Answer.objects.all().filter(choice_answer__isnull=False) return answers @@ -383,6 +438,7 @@ class Answer(models.Model): objects = models.Manager() special_objects = AnswerManager() + # TODO Remove def show_question(self): requirement = self.question().risk_level_requirement if not requirement: @@ -427,6 +483,9 @@ class Answer(models.Model): ) ] def question(self): + ''' + Get the question object depending on whether it is a text or choice question + ''' if self.text_question != None: return self.text_question elif self.choice_question != None: @@ -435,6 +494,9 @@ class Answer(models.Model): return '' def answer(self): + ''' + Get the answer object depending on whether it is a text or choice question + ''' if self.text_question != None: return self.text_answer elif self.choice_question != None: diff --git a/wcsudat/surveys/serializers.py b/wcsudat/surveys/serializers.py index e763190bc468c86adc7090832fa7da2d07e05d37..8d750d11e2b80f458979cd827b72e32ebd028972 100644 --- a/wcsudat/surveys/serializers.py +++ b/wcsudat/surveys/serializers.py @@ -2,6 +2,11 @@ from random import choices from rest_framework import serializers from .models import * +# This file contains serializers which convert data from the database into JSON dictionaries +# By default, foreignkey fields will be displayed as the id of the associated object +# Using a serializer for the fields type with the source set to that field will change the id to a serialized dictionary +# e.g. risk_level_requirement: 1 -> risk_level_requirement: {id: 1, risk: 1, level_name: "High", level_threshold: 5, color: "#FF0000"} + class FeedbackSerializer(serializers.ModelSerializer): class Meta: model = Feedback diff --git a/wcsudat/surveys/static/surveys/assessment.js b/wcsudat/surveys/static/surveys/assessment.js index 96215ba5d1fabf17dbcb7de5647c09381d38dcb8..312a49d7f972c12eff638cf59bb1db0ae782235d 100644 --- a/wcsudat/surveys/static/surveys/assessment.js +++ b/wcsudat/surveys/static/surveys/assessment.js @@ -1,3 +1,6 @@ +/** + * Creates a csrf cookie + */ function getCookie(name) { var cookieValue = null; if (document.cookie && document.cookie !== '') { @@ -14,6 +17,11 @@ function getCookie(name) { return cookieValue; } var csrftoken = getCookie('csrftoken'); + +// These variables are used to store the state of the assessment locally to prevent extra api calls +// The snapshot lists are used to allow smooth re-rendering of the html as items in the list are replaced +// instead of having to rebuild the entire list from scratch. +// If the current list is longer than the old list then the rest of the elements in the old list are removed var activeItem = null var list_snapshot = {} var all_answers = {} @@ -25,13 +33,12 @@ var risk_dict = {} var total_questions = 0 var complete_questions = 0 +// Here we instantiate some events and components document.getElementById(`submit-button`).onclick = submitSurvey const scrollSpy = new bootstrap.ScrollSpy(document.getElementById('survey-wrapper'), { target: '#nav-wrapper', // rootMargin: "0px 0px -80% 0px" }) - - var popup = document.getElementById("submit-popup") var popupClose = document.getElementById("popup-close") window.onclick = function(event) { @@ -42,16 +49,20 @@ window.onclick = function(event) { popupClose.onclick = function() { popup.style.display = "none" } + +// Calling this function starts the chain of functions used to build the initial assessment page getSurvey() +/** + * Fetch a list of subsection answers in the active assessment from the database + */ function getSurvey(){ - // Fetch all subsection answers var url = `http://127.0.0.1:8000/surveys/api/sub-answer-list/${client_id}/${survey}/` fetch(url) .then((resp) => resp.json()) .then(function(data){ all_subsections = data - // Could initialise all_answers here + // Create entries in the dictionary for each subsection to store a list of answers later on for(var i in all_subsections) list_snapshot[all_subsections[i].subsection_detail.id] = [] getAnswers() @@ -60,8 +71,10 @@ function getSurvey(){ } +/** + * Fetch a list of answers in the active assessment from the database + */ function getAnswers(){ - // Fetch all answers var url = `http://127.0.0.1:8000/surveys/api/full-answer-list/${client_id}/${survey}/` fetch(url) .then((resp) => resp.json()) @@ -70,7 +83,9 @@ function getAnswers(){ for(var i in data){ var id = data[i].subsection_answer_detail.subsection_detail.id - + // Store each answer in the dictionary grouped by subsection + // Each answer is also given a list index + // This data structure allows quick access to answer objects if(all_answers[id]){ all_answers[id].push(data[i]) data[i]['list_index'] = all_answers[id].length-1 @@ -84,9 +99,13 @@ function getAnswers(){ calculateRisks() }) } - +/** + * Create a dictionary of risks and calculate the total risk amount for the active assessment + */ function calculateRisks(){ risk_dict = {} + // Here we loop through each subsection and all of the choice answers in each one and + // calculate the total amount of risk for each risk type for(var k in all_subsections){ var id = all_subsections[k].subsection_detail.id if(all_answers[id] === null) @@ -109,6 +128,9 @@ function calculateRisks(){ buildWrappers() } +/** + * Return a list of subsections that aren't currently locked by a risk level requirement + */ function getActiveSubsectionList(){ var list = [] for(var i in all_subsections){ @@ -123,7 +145,9 @@ function getActiveSubsectionList(){ return list } -// Build all wrappers for active subsections and then call functions to build headers and lists in each +/** + * Build all wrappers for active subsections and then call functions to build headers and lists in each + */ function buildWrappers(){ var wrapper = document.getElementById('survey-wrapper') @@ -163,11 +187,15 @@ function buildWrappers(){ wrapper_list_snapshot = list buildLists(list) buildNav() + // Refresh the scrollspy to resync all of the attached elements scrollSpy.refresh() } +/** + * Call functions to build lists of answers and headers in each wrapper + */ function buildLists(active_subsections) { total_questions = 0 @@ -180,6 +208,9 @@ function buildLists(active_subsections) } } +/** + * Build a navigation list to navigate between subsections + */ function buildNav(){ var wrapper = document.getElementById('nav-wrapper') @@ -189,6 +220,8 @@ function buildNav(){ // continue var active = "" var element = document.getElementById(`nav-button-${i}`) + // This allows us to keep the active element from the scrollspy so that there + // is no visual flashing when it is reset if(element && element.classList.contains('active')) active = "active" try{ @@ -222,7 +255,6 @@ function buildNav(){ } } try{document.getElementById(`complete-progress`).remove()}catch(err){} - console.log('Questions', complete_questions, total_questions) if(total_questions > 0){ var complete_pcnt = Math.round(100*complete_questions/total_questions) wrapper.innerHTML += `<div class="progress m-1" id="complete-progress"> @@ -235,6 +267,9 @@ function buildNav(){ } +/** + * Build the header for a given subsection with the section and subsection details + */ function buildHeader(item, active_subsections){ var wrapper = document.getElementById(`header-wrapper-${item}`) @@ -265,6 +300,10 @@ function buildHeader(item, active_subsections){ } + +/** + * Build the clinician notes input for a given subsection + */ function buildNotes(subsection_index){ @@ -289,10 +328,15 @@ function buildNotes(subsection_index){ } +/** + * Build a list of questions and answer inputs for a given subsection + */ function buildList(subsection_index, active_subsections){ var wrapper = document.getElementById(`list-wrapper-${subsection_index}`) var list = [] var id = active_subsections[subsection_index].subsection_detail.id + // We first create a list of answers in a subsection that should not be hidden by + // risk level requirements by checking that their requirement has been met for(var i in all_answers[id]){ var answer = all_answers[id][i] var required = null @@ -311,6 +355,7 @@ function buildList(subsection_index, active_subsections){ for (var i in list){ total_questions++ if(!list[i].choice_answer && (!list[i].text_answer || list[i].text_answer.trim()==="")){ + // Subsection not complete complete = false }else{ complete_questions++ @@ -321,14 +366,17 @@ function buildList(subsection_index, active_subsections){ }catch(err){ } + // Create the question text var title = `<span class="title" id="data-row-${list[i].id}-title">${parseInt(i)+1}. ${list[i].text}</span>` if (!list[i].choice_set_question) { + // Create the answer input for a text question var answer = `<input id="data-row-${list[i].id}-answer" value="${list[i].text_answer}" class="answer-${id}" type="text submit">` } else { var answer = `<div id="data-row-${list[i].id}-choice-answer" class="answer-${id} choice-wrapper d-flex justify-content-between">` + // Create the choice buttons for a choice question for (var k in list[i].choice_set_question.choice_set) { var choice = list[i].choice_set_question.choice_set[k] @@ -348,6 +396,7 @@ function buildList(subsection_index, active_subsections){ answer+="</div>" } + // Create the full question item var item = ` <div id="data-row-${id}-${i}" class="question-wrapper container"> <div class="container"> @@ -371,7 +420,7 @@ function buildList(subsection_index, active_subsections){ } list_snapshot[id] = list - + // Add necessary event listeners for the answer fields for (var i in list){ var answerField = document.getElementsByClassName(`answer-${id}`)[i] if(answerField.id.includes('choice')) @@ -400,6 +449,7 @@ function buildList(subsection_index, active_subsections){ } + // If the completion status of a subsection has changed then update it on the server if(complete != active_subsections[subsection_index].complete){ active_subsections[subsection_index].complete = complete updateComplete(active_subsections[subsection_index]) @@ -407,18 +457,23 @@ function buildList(subsection_index, active_subsections){ } + +/** + * Check and submit the active assessment + */ function submitSurvey(){ var list = getActiveSubsectionList() var complete = true - var alert_text = "<p>Please complete the following subsections</p>" - console.log("Submit Data: ", list) + var alert_text = "<h2>Please complete the following subsections</h2>" + // Check to see if the assessment is complete for (var i in list){ if(!list[i].complete){ complete = false alert_text += `<p>${list[i].subsection_detail.subsection_name}</p>` } } + // Show a popup if the assessment is not complete, otherwise submit the assessment if(!complete){ var popup_wrapper = document.getElementById('submit-popup-content-wrapper') popup_wrapper.innerHTML = alert_text @@ -431,6 +486,10 @@ function submitSurvey(){ } + +/** + * This function updates the clinician notes for the given subsection on the database + */ function updateNotes(item, subsection_index){ var active_subsections = getActiveSubsectionList() var id = active_subsections[subsection_index].id @@ -451,6 +510,9 @@ function updateNotes(item, subsection_index){ }) } +/** + * This function updates the complete field for the given subsection on the database + */ function updateComplete(item){ var url = `http://127.0.0.1:8000/surveys/api/sub-answer-update/${item.id}/` fetch(url, { @@ -469,7 +531,9 @@ function updateComplete(item){ }) } - +/** + * This function updates the answer for a text question on the database + */ function updateAnswer(item){ var url = `http://127.0.0.1:8000/surveys/api/answer-update/${item.id}/` var new_answer = document.getElementById(`data-row-${item.id}-answer`).value @@ -482,7 +546,7 @@ function updateAnswer(item){ body:JSON.stringify({'text_answer':new_answer}) } ).then(function(response){ - + // Sync with database fetchAnswer(item) @@ -490,6 +554,9 @@ function updateAnswer(item){ //document.getElementById(`data-row-${item.id+1}-answer`).scrollIntoView() } +/** + * This function updates the answer for a choice question on the database + */ function updateChoiceAnswer(item){ var url = `http://127.0.0.1:8000/surveys/api/answer-update/${item.id}/` var new_answer = document.querySelector(`input[name="choice-answer-${item.id}"]:checked`).value @@ -510,12 +577,16 @@ function updateChoiceAnswer(item){ body:JSON.stringify({'choice_answer':new_answer}) } ).then(function(response){ + // Sync with database fetchAnswer(item) }) //document.getElementById(`data-row-${item.id+1}-answer`).scrollIntoView() } +/** + * This function retrieves a single answer from the database to sync the client with the server + */ function fetchAnswer(item){ var url = `http://127.0.0.1:8000/surveys/api/answer-detail/${item.id}/` fetch(url) diff --git a/wcsudat/surveys/static/surveys/client.js b/wcsudat/surveys/static/surveys/client.js index acb3ed80e454799ae9883cce652a7e243893f62b..462d8d3f635cff499ea33e2ee86d95095c1f8da3 100644 --- a/wcsudat/surveys/static/surveys/client.js +++ b/wcsudat/surveys/static/surveys/client.js @@ -1,3 +1,6 @@ +/** + * Creates a csrf cookie + */ function getCookie(name) { var cookieValue = null; if (document.cookie && document.cookie !== '') { @@ -14,26 +17,29 @@ function getCookie(name) { return cookieValue; } var csrftoken = getCookie('csrftoken'); -//var clinician = "{{ user.id }}" -var list_snapshot = [] +// These variables are used to manage state +var list_snapshot = [] var survey_answer_list = [] -//var selectedClient = "{{ client.id }}" - var selectedSurvey = null +var summary = {} +// Instantiate some components and event listeners var surveyFormWrapper = document.getElementById('survey-view-wrapper') - var newSurveyButton = document.getElementById('new-survey') - newSurveyButton.addEventListener('click', (function(item){ return function(e){ newSurvey() } })()) -var summary = {} + +// Start building the client page getSurveyAnswers() + +/** + * Fetch a list of assessments for the active client from the database + */ function getSurveyAnswers(){ var url = `http://127.0.0.1:8000/surveys/api/survey-answer-list/${selectedClient}/` @@ -45,11 +51,14 @@ function getSurveyAnswers(){ }) } - +/** + * Build a list of assessments for the active client + */ function buildList(){ // TODO Make sure the list doesn't get moved by other components being created var wrapper = document.getElementById('list-wrapper') //wrapper.innerHTML = '' + // Remove the loading spinner once we start building the list try{document.getElementById(`list-spinner`).remove()}catch(err){} var list = survey_answer_list @@ -58,6 +67,7 @@ function buildList(){ wrapper.innerHTML = `<p><strong>This client has no assessments. Please click the button above to start a new one.</strong></p>` return }else{ + // Set the selected survey to the most recent one by default selectedSurvey = list[0].id } @@ -92,6 +102,7 @@ function buildList(){ } wrapper.innerHTML += item + // Calculate the average risk amounts over all complete assessments if(!list[i].in_progress /*&& list[i].id != selectedSurvey*/ && list[i].complete_subsections === 12) for(var order in list[i].risks) for (var section in list[i].risks[order]) @@ -126,7 +137,7 @@ function buildList(){ var answerField = document.getElementsByClassName('survey-row-item')[i] answerField.addEventListener('click', (function(item){ return function(e){ - getSurvey(item) + setSurvey(item) } })(list[i])) } @@ -136,18 +147,26 @@ function buildList(){ } - +/** + * Start a new assessment + */ function newSurvey(){ window.location.href = `/surveys/new-survey/${selectedClient}/` } -function getSurvey(item){ - selectedSurvey = item.id +/** + * Set the selected assessment to the given assessment + */ +function setSurvey(survey){ + selectedSurvey = survey.id buildList() } +/** + * Show the details of the selected survey. This includes information about the assessment as well as risk radar charts for analysis + */ function showDetails(){ - // TODO Add animated loading icon + // TODO Refactor this into multiple functions surveyFormWrapper.className = "visible container" var wrapper = document.getElementById('survey-view-content') var url = `http://127.0.0.1:8000/surveys/api/survey-answer-detail/${selectedSurvey}/` @@ -159,17 +178,20 @@ function showDetails(){ if(data.complete_subsections <= 2) { + // Screening only wrapper.innerHTML = `<p>Screening completed on ${data.date} at ${data.time}</p>` wrapper.innerHTML += `<a href="/surveys/transcript/${selectedClient}/${selectedSurvey}/">View Transcript</a><br>` wrapper.innerHTML += `<br><p><i class="bi-check-circle-fill text-success"></i> Low risk of substance abuse disorder.</p>` } else{ + // Full assessment wrapper.innerHTML = `<p>Assessment completed on ${data.date} at ${data.time}</p>` wrapper.innerHTML += `<a href="/surveys/transcript/${selectedClient}/${selectedSurvey}/">View Transcript</a><br>` wrapper.innerHTML += `<br><p><i class="bi-exclamation-circle-fill text-danger"></i> High risk of substance abuse disorder</p>` for(var order in data.risks) + for (var section in data.risks[order]){ - // console.log(data.risks[order][section].risks) + // Create a canvas and list for each risk group if(JSON.stringify(data.risks[order][section].risks) === "{}") continue wrapper.innerHTML += ` <div class="container d-flex flex-row border-dark @@ -184,6 +206,8 @@ function showDetails(){ } for(var order in data.risks) for (var section in data.risks[order]){ + // Here section refers to the risk section or risk group + // Add all the risk amounts to the risk list var list_wrapper = document.getElementById(`survey-view-risks-${section}`) var list = data.risks[order][section].risks if(JSON.stringify(data.risks[order][section].risks) === "{}") @@ -201,6 +225,7 @@ function showDetails(){ var amnt = list[i].amount var summary_amnt = summary[i] var color = "" + // Get the risk level and associated color for (var k in list[i].levels){ var threshold = list[i].levels[k].level if(amnt >= threshold) @@ -210,7 +235,8 @@ function showDetails(){ } } - + + // Create accordions with risk explanations var item = ` <div class="accordion-item"> <h2 class="accordion-header" id="info-panel-${section}-${j}"> @@ -226,6 +252,7 @@ function showDetails(){ </div> </div> ` + // Calculate the percent risk using the minimum and maximum risk values container += item var mn = list[i].minimum var mx = list[i].maximum @@ -236,7 +263,7 @@ function showDetails(){ values.push(pcnt) summary_values.push(summary_pcnt) - + // Create a progress bar to show the risk amount container += `<div class="progress"> <div class="progress-bar" role="progressbar" style="width: ${pcnt}%; background-color: ${color} !important;" aria-valuenow="${amnt}" aria-valuemin="${mn}" aria-valuemax="${mx}"></div> @@ -250,19 +277,8 @@ function showDetails(){ } container += '</div>' list_wrapper.innerHTML += container - // j = 0 - // for (var i in list){ - // document.getElementById(`info-${section}-${j}`).addEventListener('click', (function(label, text){ - // return function(e){ - // console.log('Clicked') - // e.preventDefault() - // $('#riskModalTitle').text(label) - // $('#risk-modal-body').text(text) - // $('#risk-modal').modal() - // } - // })(i, list[i].text)) - // j+=1 - // } + + // Calculate the colour for the chart total_pcnt /= (100*j) var high_is_good = data.risks[order][section].high_is_good if(high_is_good) @@ -295,6 +311,7 @@ function showDetails(){ color = red var rgb = 'rgb(' + [color.r, color.g, color.b].join(',') + ')' var rgba = 'rgb(' + [color.r, color.g, color.b, 0.2].join(',') + ')' + // Configure chart options and datasets const dataset = { labels: labels, datasets: [{ @@ -337,6 +354,7 @@ function showDetails(){ } }, }; + // Render the chart for the risk group new Chart(`chart-${section}`, config) } } diff --git a/wcsudat/surveys/static/surveys/home.js b/wcsudat/surveys/static/surveys/home.js index ddc54382faee032b50f2d1b40e7d93e364846330..667ef01a029f6a44a160bca4f1e0d53a06682ef6 100644 --- a/wcsudat/surveys/static/surveys/home.js +++ b/wcsudat/surveys/static/surveys/home.js @@ -1,3 +1,6 @@ +/** + * Creates a csrf cookie + */ function getCookie(name) { var cookieValue = null; if (document.cookie && document.cookie !== '') { @@ -15,10 +18,11 @@ function getCookie(name) { } var csrftoken = getCookie('csrftoken'); -// console.log(clinician) +// This stores the snapshot for the client list and the selected client var list_snapshot = [] - var selectedClient = null + +// Input elements for the client edit/create var clientFormWrapper = document.getElementById('client-form-wrapper') var clientFormHeader = document.getElementById('client-form-header') var firstname = document.getElementById('name-input') @@ -35,12 +39,12 @@ var education = document.getElementById('education-input') var police = document.getElementById('police-input') var clientForm = document.getElementById('client-form') - +// Activate all tooltips $(document).ready(function(){ $('[data-toggle="tooltip"]').tooltip(); }); - +// Control elements for the client panel as well as feedback element var saveButton = document.getElementById('save-client') var editButton = document.getElementById('edit-client') var newSurveyButton = document.getElementById('new-survey') @@ -52,6 +56,8 @@ var feedBackSubmitButton = document.getElementById('feedback-submit') var searchInput = document.getElementById('client-search-input') var searchText = "" + +// Add relevent event listeners searchInput.addEventListener('keyup', (function(item){ return function(e){ searchText = searchInput.value @@ -116,29 +122,38 @@ feedBackSubmitButton.addEventListener('click', (function(item){ } })()) - +// Initialise the page by building a client list buildList() +// Start the client panel in the new client state newClient() - +// Create a toast element for the feedback element and get the form const feedbackToast = document.getElementById('feedback-toast') const toast = new bootstrap.Toast(feedbackToast) const feedBackForm = document.getElementById('feedback-form') - +/** + * Show the feedback input toast + */ function showFeedbackToast(){ toast.show() feedBackButton.style.display = "none" } +/** + * Hide the feedback input toast + */ function hideFeedbackToast(){ setTimeout(function() { feedBackButton.style.display = "block" }, 150); } +/** + * Post the feedback to the database with the clinician as the user + */ function submitFeedback(){ var feedbackInput = document.getElementById('feedback-input') var url = `http://127.0.0.1:8000/surveys/api/feedback-create/` @@ -157,7 +172,9 @@ function submitFeedback(){ }) } - +/** + * Build a list of client cards + */ function buildList(){ var wrapper = document.getElementById('list-wrapper') //wrapper.innerHTML = '' @@ -172,6 +189,7 @@ function buildList(){ var list = data var searchList = [] + // Create a list of clients that match the search query for (var i in list){ var found = searchText.trim()==="" for(var k in searchValues){ @@ -196,6 +214,7 @@ function buildList(){ searchList.push(list[i]) } list = searchList + // Build the list of clients for (var i in list){ try{ document.getElementById(`client-row-${i}`).remove() @@ -207,6 +226,7 @@ function buildList(){ var risk_amnt = list[i].overall_risk if(!list[i].overall_risk) risk_amnt = 0 + // This gives a color gradient depending on the client risk amount var colours_list = [ // TODO Improve this color gradient "#9c0909", @@ -217,7 +237,7 @@ function buildList(){ var index = (colours_list.length-1)-Math.round(risk_amnt*(colours_list.length-1)) var colour = colours_list[index] - // console.log(risk_amnt, index) + // This gives different icons depending on the client risk amount var icon_list = [ `<i class="bi bi-chevron-double-up" style="color: ${colour}"></i>`, `<i class="bi bi-chevron-up" style="color: ${colour}"></i>`, @@ -226,7 +246,9 @@ function buildList(){ ] var icon_index = (icon_list.length-1)-Math.round(risk_amnt*(icon_list.length-1)) var icon = icon_list[icon_index] + // Create a list entry with a button to view the surveys, create a new survey and edit the client if(selectedClient == list[i].id){ + // These buttons are active item = `<div class="btn-group m-1 client-row-item" id="client-row-${i}" role="group" aria-label="client-row-${i}"> <button type="button" class="btn btn-outline-primary text-start client-view-item" data-toggle="tooltip" data-placement="top" title="View Client Assessments" @@ -234,7 +256,7 @@ function buildList(){ <button type="button" class="btn btn-outline-primary new-survey-item" data-toggle="tooltip" data-placement="top" title="Start New Assessment" - ><i class="bi bi-book" aria-hidden="true"></i></button> + ><i class="bi bi-journal-plus" aria-hidden="true"></i></button> <button type="button" class="btn btn-outline-primary client-edit-item" data-toggle="tooltip" data-placement="top" title="Toggle Edit" @@ -250,7 +272,7 @@ function buildList(){ <button type="button" class="btn btn-outline-dark new-survey-item" data-toggle="tooltip" data-placement="top" title="Start New Assessment" - ><i class="bi bi-book" aria-hidden="true"></i></button> + ><i class="bi bi-journal-plus" aria-hidden="true"></i></button> <button type="button" class="btn btn-outline-dark client-edit-item" data-toggle="tooltip" data-placement="top" title="Toggle Edit" @@ -269,7 +291,7 @@ function buildList(){ } list_snapshot = list - + // Add event listeners to each button for view surveys, new survey and edit client for (var i in list){ var answerField = document.getElementsByClassName('new-survey-item')[i] answerField.addEventListener('click', (function(item){ @@ -294,6 +316,7 @@ function buildList(){ var answerField = document.getElementsByClassName('client-edit-item')[i] answerField.addEventListener('click', (function(item){ return function(e){ + // Toggle between edit and new client if(selectedClient != item.id){ selectedClient = item.id getClient(item) @@ -308,7 +331,7 @@ function buildList(){ } })(list[i])) } - + // Re-activate tooltips var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) @@ -316,7 +339,9 @@ function buildList(){ }) } -//--------Function to populate the client details pane with selected clients details--------------------------------- +/** + * Populate the client details pane with selected clients details + */ function getClient(item){ // console.log(item) selectedClient = item.id @@ -342,6 +367,9 @@ function getClient(item){ buildList() } +/** + * Set the panel mode to new client + */ function newClient(){ selectedClient = null clientFormHeader.innerHTML = `<h4><div class="container" id="new-user-button" data-toggle="tooltip" data-placement="left" title="Add a New Client"> @@ -356,6 +384,9 @@ function newClient(){ showDetails() } +/** + * Set the panel mode to edit client + */ function editClient(){ console.log(selectedClient) var form_controls = document.querySelectorAll('.client-form-control') @@ -377,6 +408,9 @@ function editClient(){ } +/** + * Delete the client on the database + */ function deleteClient(){ if(selectedClient){ var url = `http://127.0.0.1:8000/surveys/api/client-delete/${selectedClient}/` @@ -395,6 +429,9 @@ function deleteClient(){ } } +/** + * Update or save a client on the database + */ function saveClient(){ if(selectedClient){ @@ -439,25 +476,30 @@ function saveClient(){ }) } +/** + * Start a new assessment for the selected client + */ function newSurvey(){ window.location.href = `/surveys/new-survey/${selectedClient}/` } +/** + * View assessments for the selected client + */ function viewSurveys(){ window.location.href = `/surveys/view-surveys/${selectedClient}/` } +/** + * Show the client panel + */ function showDetails(){ - if(clientFormWrapper.className == "visible container"){ - clientFormWrapper.className = "invisible container" - } - else{ - clientFormWrapper.className = "visible container" - } + // Show the panel clientFormWrapper.className = "visible container" var form_controls = document.querySelectorAll('.client-form-control') var form_controls_select = document.querySelectorAll('.client-form-control-select') + // Disable editting by default when a client is selected if(selectedClient){ form_controls.forEach((form_control) => { form_control.setAttribute('readonly', true) @@ -479,6 +521,4 @@ function showDetails(){ var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) }) -} -// TODO Client Create/Edit Form -// TODO Change API for survey to use a client id \ No newline at end of file +} \ No newline at end of file diff --git a/wcsudat/surveys/static/surveys/researcher.js b/wcsudat/surveys/static/surveys/researcher.js index e31cba745796f9392d60ba1ea293d62719ff91d5..35529f8900afd79db39ade4715bb5638ce0c79ef 100644 --- a/wcsudat/surveys/static/surveys/researcher.js +++ b/wcsudat/surveys/static/surveys/researcher.js @@ -1,3 +1,6 @@ +/** + * Creates a csrf cookie + */ function getCookie(name) { var cookieValue = null; if (document.cookie && document.cookie !== '') { @@ -16,7 +19,9 @@ function getCookie(name) { var csrftoken = getCookie('csrftoken'); var activeTable = "full-summary" +// These are the possible database tables which can be accessed var tables = { + "full-summary": "Full Summary", "answer": "Answers", "choice": "Choices", "risk": "Risks", @@ -26,23 +31,27 @@ var tables = { "section": "Sections", "choicequestion-risk": "Choice Question Risks", "choicequestion-choice": "Choice Question Choices", - "full-summary": "Full Summary", } + +// Initialise the page by building the selected table and navigation buildTable() buildNav() +/** + * Build a navigation bar with different tables as well as the full summary tab + */ function buildNav(){ var wrapper = document.getElementById('nav-wrapper') wrapper.innerHTML = "" - if(activeTable != 'full-summary') - wrapper.innerHTML += `<a href="http://127.0.0.1:8000/surveys/api/csv/${activeTable}/"">Download CSV</a>` for(var table in tables){ - var button = `<button id="nav-button-${table}" class="nav-button btn btn-secondary">${tables[table]}</button>` + var button = `<button id="nav-button-${table}" class="nav-button btn btn-secondary m-1">${tables[table]}</button>` if(table == activeTable){ - button = `<button id="nav-button-${table}" class="nav-button btn btn-primary">${tables[table]}</button>` + button = `<button id="nav-button-${table}" class="nav-button btn btn-primary m-1">${tables[table]}</button>` } wrapper.innerHTML += button } + if(activeTable != 'full-summary') + wrapper.innerHTML += `<a href="http://127.0.0.1:8000/surveys/api/csv/${activeTable}/"">Download CSV</a>` for(var table in tables){ var tableButton = document.getElementById(`nav-button-${table}`) tableButton.addEventListener('click', (function(item){ @@ -52,19 +61,33 @@ function buildNav(){ })(table)) } } -function updateActiveTable(item){ - activeTable = item + +/** + * Set the active table to the given table + */ +function updateActiveTable(table_name){ + activeTable = table_name buildTable() buildNav() } // TODO Fixed navigation position // TODO List replacement for table creation to remove glitches caused by scrollbar appearing // or maybe have scrollbar always shown + +/** + * Build a table for the active table + */ function buildTable(){ var header_row = document.getElementById('fields') var wrapper = document.getElementById('summary-wrapper') var table_rows = document.getElementById('table-rows') - header_row.innerHTML = "" + // Show a loading spinner while we wait for the api request to complete + header_row.innerHTML = ` + <div class="d-flex justify-content-center" id="list-spinner"> + <div class="spinner-border" role="status"> + <span class="visually-hidden">Loading...</span> + </div> + </div>` table_rows.innerHTML = "" wrapper.innerHTML = "" if(activeTable == 'full-summary'){ @@ -80,11 +103,23 @@ function buildTable(){ var list = data console.log(data) var text = "" + header_row.innerHTML = "" + if(list.length === 0){ + header_row.innerHTML = "<h2>No data to display.</h2>" + return + } + + // Build the table header row for (var i in list[0]){ + // Replace underscore followed by a lowercase letter with a space followed by the uppercase letter + // This converts from snake_case to spaced camelCase + // The first letter is then capitalised + // e.g. risk_group -> Risk Group var title = i.replace(/_([a-z])/g, function (g) { return ' ' + g[1].toUpperCase(); }); title = title[0].toUpperCase() + title.substring(1) header_row.innerHTML += `<th scope="col">${title}</th>` } + // Build the body with the data for (var i in list){ text += "<tr>" for (var k in list[i]){ @@ -108,6 +143,9 @@ function buildTable(){ }) } +/** + * Build the summary + */ function buildSummary(){ var wrapper = document.getElementById('summary-wrapper') var url = `http://127.0.0.1:8000/surveys/api/summary/` @@ -115,7 +153,10 @@ function buildSummary(){ .then((resp) => resp.json()) .then(function(data){ var sections = data + wrapper.innerHTML = "" for(var section in sections){ + // Display the section header and text as well as the subsection header + // If they are equal only display the section header wrapper.innerHTML += `<h2>${sections[section].name}</h2>` wrapper.innerHTML += `<h3>${sections[section].text}</h3>` var subsections = sections[section].subsections @@ -130,14 +171,14 @@ function buildSummary(){ wrapper.innerHTML += `<h4>${questions[question].name}</h4>` var bar = `` var choices = questions[question].choices - var mn = 0 - var mx = 0 + // Calculate the total answers for the question + var total = 0 for(var choice in choices){ - if(choices[choice].votes > mx) - mx = choices[choice].votes + total += choices[choice].votes } for(var choice in choices){ - var pcnt = 100*choices[choice].votes/mx + // Calculate the percentage of the total answers for each choice and display them + var pcnt = 100*choices[choice].votes/total bar += `<p>${choices[choice].name}</p><div class="progress"> <div class="progress-bar" role="progressbar" style="width: ${pcnt}%" aria-valuenow="${pcnt}" aria-valuemin="0" aria-valuemax="100">${choices[choice].votes}</div> diff --git a/wcsudat/surveys/static/surveys/transcription.js b/wcsudat/surveys/static/surveys/transcription.js index a1a020198f6cbf8c0db59152ca0dd2a79a30d3d8..5a4524cc2246ac746c4350ec4b04109a44fbc459 100644 --- a/wcsudat/surveys/static/surveys/transcription.js +++ b/wcsudat/surveys/static/surveys/transcription.js @@ -1,4 +1,7 @@ -function getCookie(name) { +/** + * Creates a csrf cookie + */ + function getCookie(name) { var cookieValue = null; if (document.cookie && document.cookie !== '') { var cookies = document.cookie.split(';'); @@ -14,6 +17,11 @@ function getCookie(name) { return cookieValue; } var csrftoken = getCookie('csrftoken'); + +// These variables are used to store the state of the assessment locally to prevent extra api calls +// The snapshot lists are used to allow smooth re-rendering of the html as items in the list are replaced +// instead of having to rebuild the entire list from scratch. +// If the current list is longer than the old list then the rest of the elements in the old list are removed var activeItem = null var list_snapshot = {} var all_answers = {} @@ -23,13 +31,11 @@ var all_subsections = [] var active_subsection = null var risk_dict = {} - +// Here we instantiate some events and components const scrollSpy = new bootstrap.ScrollSpy(document.getElementById('survey-wrapper'), { target: '#nav-wrapper', // rootMargin: "0px 0px -80% 0px" }) - - var popup = document.getElementById("submit-popup") var popupClose = document.getElementById("popup-close") window.onclick = function(event) { @@ -40,16 +46,20 @@ window.onclick = function(event) { popupClose.onclick = function() { popup.style.display = "none" } + +// Calling this function starts the chain of functions used to build the initial assessment page getSurvey() +/** + * Fetch a list of subsection answers in the active assessment from the database + */ function getSurvey(){ - // Fetch all subsection answers var url = `http://127.0.0.1:8000/surveys/api/sub-answer-list/${client_id}/${survey}/` fetch(url) .then((resp) => resp.json()) .then(function(data){ all_subsections = data - // Could initialise all_answers here + // Create entries in the dictionary for each subsection to store a list of answers later on for(var i in all_subsections) list_snapshot[all_subsections[i].subsection_detail.id] = [] getAnswers() @@ -58,8 +68,10 @@ function getSurvey(){ } +/** + * Fetch a list of answers in the active assessment from the database + */ function getAnswers(){ - // Fetch all answers var url = `http://127.0.0.1:8000/surveys/api/full-answer-list/${client_id}/${survey}/` fetch(url) .then((resp) => resp.json()) @@ -68,7 +80,9 @@ function getAnswers(){ for(var i in data){ var id = data[i].subsection_answer_detail.subsection_detail.id - + // Store each answer in the dictionary grouped by subsection + // Each answer is also given a list index + // This data structure allows quick access to answer objects if(all_answers[id]){ all_answers[id].push(data[i]) data[i]['list_index'] = all_answers[id].length-1 @@ -82,9 +96,13 @@ function getAnswers(){ calculateRisks() }) } - +/** + * Create a dictionary of risks and calculate the total risk amount for the active assessment + */ function calculateRisks(){ risk_dict = {} + // Here we loop through each subsection and all of the choice answers in each one and + // calculate the total amount of risk for each risk type for(var k in all_subsections){ var id = all_subsections[k].subsection_detail.id if(all_answers[id] === null) @@ -107,6 +125,9 @@ function calculateRisks(){ buildWrappers() } +/** + * Return a list of subsections that aren't currently locked by a risk level requirement + */ function getActiveSubsectionList(){ var list = [] for(var i in all_subsections){ @@ -121,7 +142,9 @@ function getActiveSubsectionList(){ return list } -// Build all wrappers for active subsections and then call functions to build headers and lists in each +/** + * Build all wrappers for active subsections and then call functions to build headers and lists in each + */ function buildWrappers(){ var wrapper = document.getElementById('survey-wrapper') @@ -161,11 +184,15 @@ function buildWrappers(){ wrapper_list_snapshot = list buildLists(list) buildNav() + // Refresh the scrollspy to resync all of the attached elements scrollSpy.refresh() } +/** + * Call functions to build lists of answers and headers in each wrapper + */ function buildLists(active_subsections) { for(var i in active_subsections){ @@ -176,6 +203,9 @@ function buildLists(active_subsections) } } +/** + * Build a navigation list to navigate between subsections + */ function buildNav(){ var wrapper = document.getElementById('nav-wrapper') @@ -194,7 +224,6 @@ function buildNav(){ var icon = '' if(subsection.icon) icon = `<i class="bi bi-${subsection.icon}" style="font-size: x-large;"></i>` - console.log(subsection) if(list[i].complete){ complete_icon = `<i class="bi bi-check" style="font-size: x-large;"></i>` button = `<a id="nav-button-${i}" class="list-group-item list-group-item-action" @@ -214,11 +243,14 @@ function buildNav(){ document.getElementById(`nav-button-${i}`).remove() } } - + nav_list_snapshot = list } +/** + * Build the header for a given subsection with the section and subsection details + */ function buildHeader(item, active_subsections){ var wrapper = document.getElementById(`header-wrapper-${item}`) @@ -249,6 +281,10 @@ function buildHeader(item, active_subsections){ } + +/** + * Build the clinician notes input for a given subsection + */ function buildNotes(subsection_index){ @@ -273,10 +309,15 @@ function buildNotes(subsection_index){ } +/** + * Build a list of questions and answer inputs for a given subsection + */ function buildList(subsection_index, active_subsections){ var wrapper = document.getElementById(`list-wrapper-${subsection_index}`) var list = [] var id = active_subsections[subsection_index].subsection_detail.id + // We first create a list of answers in a subsection that should not be hidden by + // risk level requirements by checking that their requirement has been met for(var i in all_answers[id]){ var answer = all_answers[id][i] var required = null @@ -298,14 +339,17 @@ function buildList(subsection_index, active_subsections){ }catch(err){ } - var title = `<span class="title" id="data-row-${list[i].id}-title">${list[i].text}</span>` + // Create the question text + var title = `<span class="title" id="data-row-${list[i].id}-title">${parseInt(i)+1}. ${list[i].text}</span>` if (!list[i].choice_set_question) { + // Create the answer input for a text question var answer = `<input id="data-row-${list[i].id}-answer" value="${list[i].text_answer}" class="answer-${id}" type="text submit" readonly >` } else { var answer = `<div id="data-row-${list[i].id}-choice-answer" class="answer-${id} choice-wrapper d-flex justify-content-between">` + // Create the choice buttons for a choice question for (var k in list[i].choice_set_question.choice_set) { var choice = list[i].choice_set_question.choice_set[k] @@ -315,7 +359,6 @@ function buildList(subsection_index, active_subsections){ answer += `<label id="data-row-${id}-${i}-choice-${choice.id}-label" for="data-row-${id}-${i}-choice-${choice.id}-answer">${choice.choice_text}<br><br>${icon}` answer += `<input class="d-none" id="data-row-${id}-${i}-choice-${choice.id}-answer" value="${choice.id}" name="choice-answer-${list[i].id}" type="radio"` - // TODO change button fill as soon as it is clicked and then post data then re-render to synchronise with datastore if(choice.id == list[i].choice_answer) { answer += 'checked=""' @@ -325,6 +368,7 @@ function buildList(subsection_index, active_subsections){ answer+="</div>" } + // Create the full question item var item = ` <div id="data-row-${id}-${i}" class="question-wrapper container"> <div class="container"> @@ -348,67 +392,12 @@ function buildList(subsection_index, active_subsections){ } list_snapshot[id] = list - - for (var i in list){ - var answerField = document.getElementsByClassName(`answer-${id}`)[i] - if(answerField.id.includes('choice')) - { - answerField.addEventListener('click', (function(item){ - return function(e){ - if(e.target.value) - updateChoiceAnswer(item) - } - })(list[i])) - answerField.addEventListener('keypress', (function(item){ - return function(e){ - if(e.target.value && e.key === 'Enter') - updateChoiceAnswer(item) - } - })(list[i])) - } - else{ - answerField.addEventListener('change', (function(item){ - return function(e){ - updateAnswer(item) - } - })(list[i])) - } - - - - } - - } -function submitSurvey(){ - var url = `http://127.0.0.1:8000/surveys/api/sub-answer-list/${client_id}/${survey}/` - - - fetch(url) - .then((resp) => resp.json()) - .then(function(data){ - var list = data - var complete = true - var alert_text = "<p>Please complete the following subsections</p>" - console.log("Submit Data: ", list) - for (var i in list){ - if(!list[i].complete){ - complete = false - alert_text += `<p>${list[i].subsection_detail.subsection_name}</p>` - } - } - if(!complete){ - var popup_wrapper = document.getElementById('submit-popup-content-wrapper') - popup_wrapper.innerHTML = alert_text - popup.style.display = "block" - console.log("Popup") - } - else{ - window.location.href = `/surveys/submit-survey/${client_id}/${survey}/` - } - }) -} + +/** + * This function updates the clinician notes for the given subsection on the database + */ function updateNotes(item, subsection_index){ var active_subsections = getActiveSubsectionList() var id = active_subsections[subsection_index].id @@ -428,57 +417,3 @@ function updateNotes(item, subsection_index){ }) } - - -function updateAnswer(item){ - var url = `http://127.0.0.1:8000/surveys/api/answer-update/${item.id}/` - var new_answer = document.getElementById(`data-row-${item.id}-answer`).value - fetch(url, { - method:'POST', - headers:{ - 'Content-type':'application/json', - 'X-CSRFToken':csrftoken, - }, - body:JSON.stringify({'text_answer':new_answer}) - } - ).then(function(response){ - - fetchAnswer(item) - - }) -} - -function updateChoiceAnswer(item){ - var url = `http://127.0.0.1:8000/surveys/api/answer-update/${item.id}/` - var new_answer = document.querySelector(`input[name="choice-answer-${item.id}"]:checked`).value - // This code was to be used to set the button to filled as soon as it's clicked but it seems like it happens instantly already - // var id = item.subsection_answer_detail.subsection_detail.id - // var i = item.list_index - // var choice_id = item.choice_answer - // console.log(item) - // console.log(`data-row-${id}-${i}-choice-${choice_id}-label`) - // - // console.log(document.getElementById(`data-row-${id}-${i}-choice-${choice_id}-label`).innerHTML) - fetch(url, { - method:'POST', - headers:{ - 'Content-type':'application/json', - 'X-CSRFToken':csrftoken, - }, - body:JSON.stringify({'choice_answer':new_answer}) - } - ).then(function(response){ - fetchAnswer(item) - }) -} - -function fetchAnswer(item){ - var url = `http://127.0.0.1:8000/surveys/api/answer-detail/${item.id}/` - fetch(url) - .then((resp) => resp.json()) - .then(function(data){ - all_answers[item.subsection_answer_detail.subsection_detail.id][item.list_index] = data - all_answers[item.subsection_answer_detail.subsection_detail.id][item.list_index]['list_index'] = item.list_index - calculateRisks() - }) -} diff --git a/wcsudat/surveys/templates/surveys/assessment.html b/wcsudat/surveys/templates/surveys/assessment.html index cc242abd6aad848552c31dd27bda18025cb22198..7fdb4b2616e2c97c63299de37157b6b014f32bcb 100644 --- a/wcsudat/surveys/templates/surveys/assessment.html +++ b/wcsudat/surveys/templates/surveys/assessment.html @@ -6,8 +6,8 @@ <nav id="page-nav" aria-label="breadcrumb"> <ol class="breadcrumb welcome"> <li class="breadcrumb-item"><a href = "{% url 'surveys:home' %}" ><i class="bi bi-house-fill homebutton" aria-hidden="true" type="submit"></i></a></li> - <li class="breadcrumb-item" aria-current="page"><a href = "{% url 'surveys:client' client.id %}" ><i class="bi bi-person" aria-hidden="true"></i></a></li> - <li class="breadcrumb-item bcactive" aria-current="page"><a href = "{% url 'surveys:client' client.id %}" ></a>Assessment for {{ client.name }} {{ client.surname }}</li> + <li class="breadcrumb-item" aria-current="page"><a href = "{% url 'surveys:client' client.id %}" ><i class="bi bi-journals" aria-hidden="true"></i></a></li> + <li class="breadcrumb-item bcactive" aria-current="page"><a href = "{% url 'surveys:client' client.id %}" ></a><i class="bi bi-journal-plus" aria-hidden="true"></i> Assessment for {{ client.name }} {{ client.surname }}</li> </ol> </nav> <form method="post" action = "{% url 'logout' %}"> diff --git a/wcsudat/surveys/templates/surveys/client.html b/wcsudat/surveys/templates/surveys/client.html index d73d8a0fe6cc9c077a94a92101fddb817f6a388e..4feaa715d9a86eec2e138acc58dc09f3e2a69bfa 100644 --- a/wcsudat/surveys/templates/surveys/client.html +++ b/wcsudat/surveys/templates/surveys/client.html @@ -17,8 +17,8 @@ <body> <nav id="page-nav" aria-label="breadcrumb"> <ol class="breadcrumb welcome"> - <li class="breadcrumb-item"><a href = "{% url 'surveys:home' %}" ><i class="bi bi-house-fill" aria-hidden="true" type="submit"></i></a></li> - <li class="breadcrumb-item bcactive" aria-current="page">{{ client.name }} {{ client.surname }}</li> + <li class="breadcrumb-item"><a href = "{% url 'surveys:home' %}" ><i class="bi bi-house-fill homebutton" aria-hidden="true" type="submit"></i></a></li> + <li class="breadcrumb-item bcactive" aria-current="page"><i class="bi bi-journals" aria-hidden="true"></i> Assessments for {{ client.name }} {{ client.surname }}</li> </ol> </nav> <form method="post" action = "{% url 'logout' %}"> @@ -45,8 +45,8 @@ <div class="container-fluid all-content"> <div class="container survey-container"> - <button class="btn btn-primary" id="new-survey" data-toggle="tooltip" data-placement="top" title="New Survey"> - <i class="bi bi-book" aria-hidden="true"></i> + <button class="btn btn-primary" id="new-survey" data-toggle="tooltip" data-placement="top" title="New Assessment"> + <i class="bi bi-journal-plus" aria-hidden="true"></i> </button> <div class="list-group" id="list-wrapper"> <div class="d-flex justify-content-center" id="list-spinner"> diff --git a/wcsudat/surveys/templates/surveys/home.html b/wcsudat/surveys/templates/surveys/home.html index 19b5ed4c1b53fd4b13663c9bf61568657808b23a..39a9a3eece49be00dc2d068aaf6fcbd3285ae888 100644 --- a/wcsudat/surveys/templates/surveys/home.html +++ b/wcsudat/surveys/templates/surveys/home.html @@ -498,7 +498,7 @@ <i class="bi bi-save" aria-hidden="true"></i> </button> <button type="button" class="btn btn-primary" id="edit-client">Edit</button> - <button type="button" class="btn btn-primary" id="new-survey">New Survey</button> + <button type="button" class="btn btn-primary" id="new-survey">New Assessment</button> <button type="button" class="btn btn-primary" id="view-surveys">View Surveys</button> <button type="button" class="btn btn-danger" id="delete-client">Delete</button> </div> diff --git a/wcsudat/surveys/templates/surveys/researcher.html b/wcsudat/surveys/templates/surveys/researcher.html index f9ddb8e4ce2be0af58fa9874ebcdae93669099d5..667738614ed80c7b392a12c161c72530b09a8623 100644 --- a/wcsudat/surveys/templates/surveys/researcher.html +++ b/wcsudat/surveys/templates/surveys/researcher.html @@ -16,7 +16,7 @@ </form> <div class = "container-fluid all-content d-flex flex-row w-100 mw-100"> - <div class="container-fluid d-flex flex-column position-fixed" id="nav-wrapper"> + <div class="container-fluid d-flex flex-column position-fixed p-3" id="nav-wrapper"> </div> <div class="container m-200 padding-left:300px;" id="data-wrapper"> diff --git a/wcsudat/surveys/templates/surveys/subsection.html b/wcsudat/surveys/templates/surveys/subsection.html index 40c907da484438d8c5b807011eef8ca67c5eb6e5..4ce07c153160e2fdf5c974c95e28c7354836d0d0 100644 --- a/wcsudat/surveys/templates/surveys/subsection.html +++ b/wcsudat/surveys/templates/surveys/subsection.html @@ -2,7 +2,7 @@ {% block title %}Survey{% endblock %} {% block content %} {% if user.is_authenticated %} -<p><a href="{% url 'surveys:survey' %}">New Survey</a></p> +<p><a href="{% url 'surveys:survey' %}">New Assessment</a></p> <h1>{{ section.section_name }}</h1> <h2>{{ section.section_text }}</h2> {% if subsection.subsection_name != section.section_name %} diff --git a/wcsudat/surveys/templates/surveys/transcription.html b/wcsudat/surveys/templates/surveys/transcription.html index ad537d562c90f6d72f7d38b8e2f6689e35615a62..63b7d75c3ddbd6600bdf8e118ba857fa4676d048 100644 --- a/wcsudat/surveys/templates/surveys/transcription.html +++ b/wcsudat/surveys/templates/surveys/transcription.html @@ -7,8 +7,8 @@ <nav id="page-nav" aria-label="breadcrumb"> <ol class="breadcrumb welcome"> <li class="breadcrumb-item"><a href = "{% url 'surveys:home' %}" ><i class="bi bi-house-fill homebutton" aria-hidden="true" type="submit"></i></a></li> - <li class="breadcrumb-item" aria-current="page"><a href = "{% url 'surveys:client' client.id %}" ><i class="bi bi-person" aria-hidden="true"></i></a></li> - <li class="breadcrumb-item bcactive" aria-current="page">Previous assessment for {{ client.name }} {{ client.surname }}</li> + <li class="breadcrumb-item" aria-current="page"><a href = "{% url 'surveys:client' client.id %}" ><i class="bi bi-journals" aria-hidden="true"></i></a></li> + <li class="breadcrumb-item bcactive" aria-current="page"><i class="bi bi-journal" aria-hidden="true"></i> Previous assessment for {{ client.name }} {{ client.surname }}</li> </ol> </nav> diff --git a/wcsudat/surveys/tests.py b/wcsudat/surveys/tests.py index 8ecbe1fd2c4dd6c09f9b092882be5b0b72236b3b..28c3d1bcebdbf786de92e733c8df7be60168e095 100644 --- a/wcsudat/surveys/tests.py +++ b/wcsudat/surveys/tests.py @@ -13,7 +13,9 @@ class RiskModelTests(TestCase): and that the correct error messages are displayed ''' def test_long_fields(self): - risk = Risk(risk_category = "a"*201, risk_explanation = "a"*2001) + risk_group = RiskGroup(group_name="Test") + risk_group.save() + risk = Risk(risk_category = "a"*201, risk_explanation = "a"*2001, risk_group = risk_group) with self.assertRaises(ValidationError) as e: risk.full_clean() self.assertEqual(str(e.exception), @@ -28,13 +30,15 @@ class RiskModelTests(TestCase): and that the correct error messages are displayed ''' def test_empty_fields(self): - risk = Risk(risk_category = '', risk_explanation = '') + risk = Risk(risk_category = '', risk_explanation = '', risk_group = None) with self.assertRaises(ValidationError) as e: risk.full_clean() self.assertEqual(str(e.exception), str({'risk_category': ['This field cannot be blank.'], 'risk_explanation': + ['This field cannot be blank.'], + 'risk_group': ['This field cannot be blank.']})) ''' @@ -42,14 +46,71 @@ class RiskModelTests(TestCase): and that the correct error messages are displayed ''' def test_null_fields(self): - risk = Risk(risk_category = None, risk_explanation = None) + risk = Risk(risk_category = None, risk_explanation = None, risk_group = None) with self.assertRaises(ValidationError) as e: risk.full_clean() self.assertEqual(str(e.exception), str({'risk_category': ['This field cannot be null.'], 'risk_explanation': - ['This field cannot be null.']})) + ['This field cannot be null.'], + 'risk_group': + ['This field cannot be blank.']})) + + + def test_set_min_max(self): + risk0 = Risk(risk_category = "Test", risk_explanation="Test") + risk1 = Risk(risk_category = "Test", risk_explanation="Test") + risk0.save() + risk1.save() + + cq0 = ChoiceQuestion(question_text="Test") + cq1 = ChoiceQuestion(question_text="Test") + cq2 = ChoiceQuestion(question_text="Test") + cq3 = ChoiceQuestion(question_text="Test") + cq4 = ChoiceQuestion(question_text="Test") + cq0.save() + cq1.save() + cq2.save() + cq3.save() + cq4.save() + + + c0 = Choice(risk_amount=0) + c1 = Choice(risk_amount=1) + c2 = Choice(risk_amount=2) + c3 = Choice(risk_amount=3) + c4 = Choice(risk_amount=5) + c0.save() + c1.save() + c2.save() + c3.save() + c4.save() + + cq0.choices.add(c0, c1, c2, c3, c4) + cq1.choices.add(c1, c2, c3) + cq2.choices.add(c3, c4) + cq3.choices.add(c1, c2) + + cq0.risks.add(risk0) + cq1.risks.add(risk1) + cq2.risks.add(risk0, risk1) + cq4.risks.add(risk0) + + cq0.save() + cq1.save() + cq2.save() + cq3.save() + cq4.save() + + risk0.set_min_max() + risk1.set_min_max() + + self.assertEqual(risk0.minimum, 3) + self.assertEqual(risk0.maximum, 10) + self.assertEqual(risk1.minimum, 4) + self.assertEqual(risk1.maximum, 8) + class RiskLevelModelTests(TestCase): pass @@ -125,25 +186,6 @@ class SubsectionModelTests(TestCase): # The ordering should be "section order"."subsection order" self.assertEqual(subsection0.ordering(), "1.5") - ''' - This tests that the risk category for the risk associated with a Choice Question - is correctly returned by the function - ''' - def test_risk_factors(self): - # Create two risk objects with categories "Risk0" and "Risk1" - risk0 = Risk(risk_category = "Risk0") - risk0.save() - risk1 = Risk(risk_category = "Risk1") - risk1.save() - # Create a subsection for the ChoiceQuestion to reside on - subsection0 = Subsection(subsection_text = "0") - subsection0.save() - # Create a Choice Question with the risk set to the above risk - cq0 = ChoiceQuestion(subsection = subsection0, question_text = "Test0", risk = risk0) - cq0.save() - cq1 = ChoiceQuestion(subsection = subsection0, question_text = "Test1", risk = risk1) - cq1.save() - self.assertEqual(subsection0.risk_factors(), "Risk0, Risk1") ''' This tests that the question counts for a subsection return the correct values @@ -180,11 +222,12 @@ class ChoiceModelTests(TestCase): pass class ChoiceQuestionModelTests(TestCase): - ''' - This tests that the choice options for a Choice Question - are correctly returned by the function - ''' + def test_choice_options(self): + ''' + This tests that the choice options for a Choice Question + are correctly returned by the function + ''' choice0 = Choice(choice_text = "Choice 0") choice1 = Choice(choice_text = "Choice 1") choice2 = Choice(choice_text = "Choice 2") @@ -199,8 +242,58 @@ class ChoiceQuestionModelTests(TestCase): cq.choices.add(choice2) self.assertEqual(cq.choice_options(), "Choice 0, Choice 1, Choice 2") + def test_risk_options(self): + ''' + This tests that the risks for a Choice Question + are correctly returned by the function + ''' + risk0 = Risk(risk_category = "Risk 0", risk_explanation = "Risk 0") + risk1 = Risk(risk_category = "Risk 1", risk_explanation = "Risk 1") + risk2 = Risk(risk_category = "Risk 2", risk_explanation = "Risk 2") + risk0.save() + risk1.save() + risk2.save() + # Create a Choice Question with the risks set to the above risks + cq = ChoiceQuestion(question_text = "Test") + cq.save() + cq.risks.add(risk0) + cq.risks.add(risk1) + cq.risks.add(risk2) + self.assertEqual(cq.risk_options(), "Risk 0, Risk 1, Risk 2") + +class SurveyAnswerModelTests(TestCase): + + def test_close(self): + survey_answer = SurveyAnswer() + survey_answer.save() + subsection = Subsection(subsection_name="Test") + subsection.save() + subsection_answer0 = SubsectionAnswer(survey_answer=survey_answer) + subsection_answer0.save() + subsection_answer1 = SubsectionAnswer(survey_answer=survey_answer, subsection=subsection) + subsection_answer1.save() + self.assertEqual(survey_answer.in_progress, True) + self.assertEqual(survey_answer.complete_subsections, 0) + survey_answer.close() + self.assertEqual(survey_answer.in_progress, False) + self.assertEqual(survey_answer.complete_subsections, 1) + + def test_since(self): + pass + + def test_risks(self): + pass + + class SubsectionAnswerModelTests(TestCase): - pass + + def test_show_subsection(self): + pass + + def test_visible_in_survey(self): + pass + + class AnswerModelTests(TestCase): ''' @@ -220,25 +313,26 @@ class AnswerModelTests(TestCase): # Answers with different combinations of various members # They are arranged in a truth table so 1010 means that there is a text question # and a choice question but no answers to either. - # This is an invalid state as an answer should have one and only one type of question and an answer to this question - # Therefore only 1100 and 0011 should be valid states. + # 1010 is an invalid state as an answer should have one and only one type of question + # An answer does not can have no text or choice answer + # Therefore only 1000, 0010, 1100 and 0011 should be valid states. answers = [ - Answer(subsection = sub_ans, text_question=None, text_answer=None, choice_question=None, choice_answer=None), - Answer(subsection = sub_ans, text_question=None, text_answer=None, choice_question=None, choice_answer=cq_ans), - Answer(subsection = sub_ans, text_question=None, text_answer=None, choice_question=cq, choice_answer=None), - Answer(subsection = sub_ans, text_question=None, text_answer=None, choice_question=cq, choice_answer=cq_ans), - Answer(subsection = sub_ans, text_question=None, text_answer=ans, choice_question=None, choice_answer=None), - Answer(subsection = sub_ans, text_question=None, text_answer=ans, choice_question=None, choice_answer=cq_ans), - Answer(subsection = sub_ans, text_question=None, text_answer=ans, choice_question=cq, choice_answer=None), - Answer(subsection = sub_ans, text_question=None, text_answer=ans, choice_question=cq, choice_answer=cq_ans), - Answer(subsection = sub_ans, text_question=q, text_answer=None, choice_question=None, choice_answer=None), - Answer(subsection = sub_ans, text_question=q, text_answer=None, choice_question=None, choice_answer=cq_ans), - Answer(subsection = sub_ans, text_question=q, text_answer=None, choice_question=cq, choice_answer=None), - Answer(subsection = sub_ans, text_question=q, text_answer=None, choice_question=cq, choice_answer=cq_ans), - Answer(subsection = sub_ans, text_question=q, text_answer=ans, choice_question=None, choice_answer=None), - Answer(subsection = sub_ans, text_question=q, text_answer=ans, choice_question=None, choice_answer=cq_ans), - Answer(subsection = sub_ans, text_question=q, text_answer=ans, choice_question=cq, choice_answer=None), - Answer(subsection = sub_ans, text_question=q, text_answer=ans, choice_question=cq, choice_answer=cq_ans), + Answer(subsection_answer = sub_ans, text_question=None, text_answer=None, choice_question=None, choice_answer=None), + Answer(subsection_answer = sub_ans, text_question=None, text_answer=None, choice_question=None, choice_answer=cq_ans), + Answer(subsection_answer = sub_ans, text_question=None, text_answer=None, choice_question=cq, choice_answer=None), + Answer(subsection_answer = sub_ans, text_question=None, text_answer=None, choice_question=cq, choice_answer=cq_ans), + Answer(subsection_answer = sub_ans, text_question=None, text_answer=ans, choice_question=None, choice_answer=None), + Answer(subsection_answer = sub_ans, text_question=None, text_answer=ans, choice_question=None, choice_answer=cq_ans), + Answer(subsection_answer = sub_ans, text_question=None, text_answer=ans, choice_question=cq, choice_answer=None), + Answer(subsection_answer = sub_ans, text_question=None, text_answer=ans, choice_question=cq, choice_answer=cq_ans), + Answer(subsection_answer = sub_ans, text_question=q, text_answer=None, choice_question=None, choice_answer=None), + Answer(subsection_answer = sub_ans, text_question=q, text_answer=None, choice_question=None, choice_answer=cq_ans), + Answer(subsection_answer = sub_ans, text_question=q, text_answer=None, choice_question=cq, choice_answer=None), + Answer(subsection_answer = sub_ans, text_question=q, text_answer=None, choice_question=cq, choice_answer=cq_ans), + Answer(subsection_answer = sub_ans, text_question=q, text_answer=ans, choice_question=None, choice_answer=None), + Answer(subsection_answer = sub_ans, text_question=q, text_answer=ans, choice_question=None, choice_answer=cq_ans), + Answer(subsection_answer = sub_ans, text_question=q, text_answer=ans, choice_question=cq, choice_answer=None), + Answer(subsection_answer = sub_ans, text_question=q, text_answer=ans, choice_question=cq, choice_answer=cq_ans), ] valid = [] for i, answer in enumerate(answers): @@ -247,7 +341,7 @@ class AnswerModelTests(TestCase): valid.append(i) except: continue - self.assertEqual(valid, [3, 12]) + self.assertEqual(valid, [2, 3, 8, 12]) ''' This tests that the methods for getting the question and answer correctly return them @@ -264,12 +358,12 @@ class AnswerModelTests(TestCase): sub_ans = SubsectionAnswer() sub_ans.save() # Create answers with the different types of questions/answers - q_ans = Answer(subsection = sub_ans, text_question=q, text_answer=ans, choice_question=None, choice_answer=None) - cq_ans = Answer(subsection = sub_ans, text_question=None, text_answer=None, choice_question=cq, choice_answer=cq_ans) + q_ans = Answer(subsection_answer = sub_ans, text_question=q, text_answer=ans, choice_question=None, choice_answer=None) + cq_ans = Answer(subsection_answer = sub_ans, text_question=None, text_answer=None, choice_question=cq, choice_answer=cq_ans) q_ans.save() cq_ans.save() - self.assertEqual(str(q_ans.question()), "x.0 Question") - self.assertEqual(str(cq_ans.question()), "x.0 Choice Question") + self.assertEqual(str(q_ans.question()), "Question") + self.assertEqual(str(cq_ans.question()), "Choice Question") self.assertEqual(str(q_ans.answer()), "Answer") self.assertEqual(str(cq_ans.answer()), "Choice Answer - 0") diff --git a/wcsudat/surveys/views.py b/wcsudat/surveys/views.py index c18f40f43183d35360f6511b91db471b6970d9bb..89ab46e65f743644083172d77f84d0e79a6359f3 100644 --- a/wcsudat/surveys/views.py +++ b/wcsudat/surveys/views.py @@ -395,110 +395,14 @@ def viewSurvey(request, client_id, survey_answer_id): @login_required def survey(request, client_id, survey_answer_id): - return render(request, 'surveys/assessment.html', context={'client': Client.objects.get(id=client_id), 'survey': survey_answer_id}) ''' - Return a subsection page for a given survey and subsection number + Return the assessment page for a given client and survey answer ''' - # TODO Prevent survey completion until all visible questions are answered - - # Get the survey, subsection and subsection objects for the current page - survey_answer = SurveyAnswer.objects.filter(in_progress=True)[0] - survey_id = survey_answer.id - # TODO: Change these to "get" once order is unique - section = Section.objects.filter(order=section_order)[0] - subsection = Subsection.objects.filter(order=subsection_order, section = section)[0] - subsection_answer = SubsectionAnswer.objects.get(survey_answer=survey_answer, subsection=subsection) - - # Create a list of subsections with links - all_sections = Section.objects.all().order_by('order') - all_subsections = [] - for sec in all_sections: - all_subsections += sec.subsection_set.all() - links = [sub.ordering().replace('.', '/') + '/' for sub in all_subsections] - all_subsections = zip(all_subsections, links) - - - # Construct forms for each answer in the subsection - AnswerFormSet = modelformset_factory( - Answer, - form=AnswerForm, - fields=('text_answer', 'choice_answer',), - extra=0, - edit_only=True,) - - # Construct a list of the question details to be used to display the questions - questions = [{'question': q.question(), 'text': q.text_question != None} \ - for q in Answer.special_objects.visible_in_subsection(subsection_answer)] - - # When the page is submitted it will have a POST method - if request.method == "POST": - - - # Get the data for the subsection components - parent_form = SubsectionAnswerForm(request.POST, instance=subsection_answer) - # Get the data from the answer objects - formset = AnswerFormSet(request.POST, queryset=Answer.special_objects.visible_in_subsection(subsection_answer),) - - # If they are both valid then save and redirect to next page or home page - # TODO Change to redirect to specific page depending on what is clicked - if formset.is_valid() and parent_form.is_valid(): - formset.save() - parent_form.save() - for link in links: - if f'navigation-{link}' in request.POST: - return HttpResponseRedirect(f'/surveys/survey/{link}') - link = f'{section_order}/{subsection_order}/' - print(link) - - if 'navigation-next' in request.POST: - print('navigation-next') - print(links.index(link)) - if link in links and links.index(link)+1 < len(links): - link = 'survey/'+links[links.index(link)+1] - return HttpResponseRedirect(f'/surveys/{link}') - else: - return HttpResponseRedirect('/surveys/') - elif 'navigation-prev' in request.POST: - print('navigation-prev') - print(links.index(link)) - if link in links and links.index(link)-1 >= 0: - link = 'survey/'+links[links.index(link)-1] - return HttpResponseRedirect(f'/surveys/{link}') - else: - return HttpResponseRedirect('/surveys/') - else: - return HttpResponseRedirect(f'/surveys/survey/{link}') - - else: - # If it is not a submitted page, just fetch data from database - parent_form = SubsectionAnswerForm(instance=subsection_answer) - formset = AnswerFormSet(queryset=Answer.special_objects.visible_in_subsection(subsection_answer)) - # Combine the questions with the answer forms so that they become associated - combined = zip(questions, formset) - - # Add the necessary fields to the context - context = { - 'combined': combined, - 'formset': formset, - 'parent_form': parent_form, - 'section': section, - 'subsection': subsection, - 'subsections': all_subsections,} - # Pass the context to the subsection page - return render(request, 'surveys/subsection.html', context) + return render(request, 'surveys/assessment.html', context={'client': Client.objects.get(id=client_id), 'survey': survey_answer_id}) @login_required def newSurvey(request, client_id): - # TODO Possibly improve this behaviour to take the user to the open survey or prevent new survey when one is in progress - # or maybe prompt - # or use a URL with format survey_id/section_order/subsection_order/ - - # Close all open surveys when a new survey is created - # TODO pass a real user to this function client=Client.objects.get(id=client_id) - # for survey in SurveyAnswer.objects.filter(client=client): - # survey.in_progress = False - # survey.save() survey = SurveyAnswer(client=client) survey.save() client.selected_survey_id = survey.id