Commit 8791f90c authored by ZeMKI's avatar ZeMKI
Browse files

Q-Sort: sort on the extremes

* now the researcher will be able to ask why the user has sorted in the extremes.
* corrected the export to show the answer to the question.
* improved translations.
parent c3a8c44b
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_DRIVER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
......@@ -7,6 +7,7 @@ use Illuminate\Http\Request;
/**
* App\Answer
*
* @property int $id
* @property int|null $question_id
* @property array $answer
......@@ -65,6 +66,21 @@ class Answer extends Model
$interview->answers()->attach($answer['id'], ['question_id' => $question->id]);
}
}
if($key === "extremes"){
/// save the answer as "extremes", save the token id
/// DB: there's only 1 question marked as extreme as well as 1 answer.
/// Here we save in interview_answer multiple entries since the same question has different answers for the same type of answer.
foreach ($value as $answer)
{
$result = ['text' => $answer['text'],'token_id' => $answer['token_id']];
$interview->answers()->attach($answer['id'], ['question_id' => $answer['question_id'],'result' => json_encode($result)]);
}
}
}
}
......
......@@ -2,6 +2,7 @@
namespace App\Exports;
use App\Interview;
use App\InterviewTokens;
use App\Study;
use App\Token;
......@@ -18,6 +19,7 @@ class AllInterviewTokenExport implements FromCollection, WithMapping, WithHeadin
private $id;
private $head;
private $sorting;
private $columnValues;
/**
* AllInterviewTokenExport constructor.
......@@ -29,6 +31,7 @@ class AllInterviewTokenExport implements FromCollection, WithMapping, WithHeadin
$this->id = $id;
$this->head = $headings;
$this->sorting = $sorting;
$this->columnValues = null;
}
......@@ -51,7 +54,22 @@ class AllInterviewTokenExport implements FromCollection, WithMapping, WithHeadin
array_push($columnNames, "position (pixels)");
array_push($columnNames, "% percentage position");
array_push($columnNames, "classifiers");
}else{
}elseif($this->sorting == 3){
array_push($columnNames, "position column/row");
array_push($columnNames, "position base");
$baseArray = [];
$study = Study::where('id',$this->id)->first();
$columns = explode('|separator|', substr($study->sortings[0]->pivot->details, strpos($study->sortings[0]->pivot->details, 'qsort|') + 6));
array_pop($columns);
foreach ($columns as $key => $value)
{
$c = explode('|',$value);
$columns[$key] = $c;
array_push($baseArray,$c[1]);
}
$this->columnValues = $baseArray;
}
else{
array_push($columnNames, "position column/row");
array_push($columnNames, "token description");
}
......@@ -130,10 +148,28 @@ class AllInterviewTokenExport implements FromCollection, WithMapping, WithHeadin
$toPrint['interview id'] = $token->interview_id;
$toPrint['interviewee'] = $interviewee;
if($token->interview->study->sortings[0]->id == 3)
if($token->interview->study->sortings[0]->id === 3)
{
$toPrint["position column/row"] = $position;
$toPrint["position base"] = $this->columnValues[($position[0] - 1)];
$interview = Interview::where('id',$token->interview_id)->first();
$tokenToCheck = Token::where('id', $token->id)->first();
$hasExtremeQuestion = $interview->study->questions->filter(function ($item)
{
return $item->detail == "extremeQuestion";
})->first();
if ($hasExtremeQuestion !== "" && $interview->answers)
{
foreach ($interview->answers as $answer)
{
$token_answer = json_decode($answer->getOriginal('pivot_result'));
if($tokenToCheck->id === $token_answer->token_id) $toPrint['Reason for Placement'] = $token_answer->text;
}
}
$toPrint["token description"] = json_decode($token->properties) ? json_decode($token->properties)->description : '' ;
}else{
......
......@@ -32,11 +32,10 @@ class InterviewQsortExport implements FromCollection, WithMapping, WithHeadings
* @param $id
* @param array $headings
*/
public function __construct($id, $headings = [], $sorting)
public function __construct($id, $headings = [])
{
$this->id = $id;
$this->head = $headings;
$this->sorting = $sorting;
$this->columnValues = null;
}
......@@ -48,6 +47,8 @@ class InterviewQsortExport implements FromCollection, WithMapping, WithHeadings
array_push($columnNames, "position base");
array_push($columnNames, "token description");
array_push($columnNames, "Interviewee");
$columns = explode('|separator|', substr($this->study()->sortings[0]->pivot->details, strpos($this->study()->sortings[0]->pivot->details, 'qsort|') + 6));
array_pop($columns);
$baseArray = [];
......@@ -71,9 +72,11 @@ class InterviewQsortExport implements FromCollection, WithMapping, WithHeadings
*/
public function map($token): array
{
ray($this->columnValues);
if ($this->invalidData($token)) return [];
$position = $token->valutation->position;
$tempValuesArray = [];
$tempValuesArray["token_id"] = $token->token_id;
$tempValuesArray["token_name"] = Token::where('id', $token->token_id)->first()->name;
......@@ -81,6 +84,22 @@ class InterviewQsortExport implements FromCollection, WithMapping, WithHeadings
$tempValuesArray["position base"] = $this->columnValues[($position[0] - 1)];
$tempValuesArray["token description"] = json_decode($token->properties) ? json_decode($token->properties)->description : '';
$tempValuesArray["interviewee name"] = $token->interview->interviewed;
$interview = Interview::where('id',$this->id)->first();
$tokenToCheck = Token::where('id', $token->token_id)->first();
$hasExtremeQuestion = $interview->study->questions->filter(function ($item)
{
return $item->detail == "extremeQuestion";
})->first();
if ($hasExtremeQuestion !== "" && $interview->answers)
{
foreach ($interview->answers as $answer)
{
$token_answer = json_decode($answer->getOriginal('pivot_result'));
if($tokenToCheck->id === $token_answer->token_id) $tempValuesArray['Reason for Placement'] = $token_answer->text;
}
}
return $tempValuesArray;
}
......
......@@ -35,19 +35,32 @@ class InterviewController extends Controller
$data['interview'] = $interview;
$data['screenshots'] = [];
$data['sortingtoken'] = $interview->tokens;
$hasExtremeQuestion = $interview->study->questions->filter(function ($item)
{
return $item->detail == "extremeQuestion";
})->first();
if ($interview->study->sortings[0]->id === 3 && $hasExtremeQuestion !== null)
{
$data['extremequestion'] = $hasExtremeQuestion;
foreach ($interview->answers as $answer)
{
$token_answer = json_decode($answer->getOriginal('pivot_result'));
$tokenToChange = $data['sortingtoken']->filter(function ($item) use ($token_answer)
{
return $item->id == $token_answer->token_id;
})->first();
$tokenToChange['answer'] = $token_answer->text;
}
}
$data['tokens'] = $interview->study->available_tokens;
$data['createdtokens'] = Token::formatForEdit($interview->tokens()->where('tokens.author', '=', 0)->get());
$data['study'] = $interview->study;
$data['questions'] = $interview->study->questions;
$data['sorting'] = $interview->study->sortings[0];
$data['author'] = User::where('id', $interview->author)->first()->email ?? $interview->author;
Answer::assignAnswersToQuestion($interview, $data);
Sorting::getSortingInfo($interview->study, $data);
Interview::getSortingImages($interview, $data);
return view('interview.view', $data);
}
......@@ -96,7 +109,7 @@ class InterviewController extends Controller
left join `answers` on qid = `answers`.`question_id`
group by qid,q,type';
$questions = DB::select(DB::raw($query));
$returnQuestions['presort'] = $returnQuestions['postsort'] = [];
$returnQuestions['presort'] = $returnQuestions['postsort'] = $returnQuestions['extremeQuestion'] = [];
foreach ($questions as $question)
{
$question->answer = json_decode('[' . $question->answer . ']');
......@@ -109,6 +122,10 @@ class InterviewController extends Controller
{
array_push($returnQuestions['postsort'], $question);
}
if ($question->type === 'extremeQuestion')
{
array_push($returnQuestions['extremeQuestion'], $question);
}
}
}
......@@ -210,7 +227,7 @@ class InterviewController extends Controller
abort(403, "You are not authorized to download these data.");
}
$headings = $this->getHeadings($interview->study);
if( $interview->study->sortings[0]->id === 3) return (new InterviewQsortExport($interview->id, $headings, $interview->study->sortings[0]->id))->download($interview->interviewed . ' tokens.xlsx');
if ($interview->study->sortings[0]->id === 3) return (new InterviewQsortExport($interview->id, $headings))->download($interview->interviewed . ' tokens.xlsx');
else return (new InterviewTokenExport($interview->id, $headings, $interview->study->sortings[0]->id))->download($interview->interviewed . ' tokens.xlsx');
}
......@@ -218,10 +235,12 @@ class InterviewController extends Controller
{
if ($study->sortings[0]->id == 2)
if ($study->sortings[0]->id === 2)
{
return ["section number", "section name"];
} else return [];
} elseif($study->sortings[0]->id === 3){
return ['Reason for Placement'];
}else return [];
}
/**
......
......@@ -23,9 +23,11 @@ class StudyInterviewController extends Controller
{
if($study->sortings[0]->id == 2)
if ($study->sortings[0]->id === 2)
{
return ["section number","section name"];
return ["section number", "section name"];
} elseif($study->sortings[0]->id === 3){
return ['Reason for Placement'];
}else return [];
}
......
......@@ -46,7 +46,7 @@ class Question extends Model
public static function formatForEdit($study)
{
$study->tokens = $study->available_tokens;
$questions['presort'] = $questions['postsort'] = $questionIds = [];
$questions['presort'] = $questions['postsort']= $questions['extremeQuestion'] = $questionIds = [];
foreach ($study->questions as $questionToEdit) {
if ($questionToEdit->detail === 'presort') {
$questionToEdit['answers'] = Answer::where('question_id', $questionToEdit->id)->get()->toArray();
......@@ -55,6 +55,9 @@ class Question extends Model
if(Str::contains($questionToEdit->detail, 'showsorting')) $questionToEdit['canShowSorting'] = true;
$questionToEdit['answers'] = Answer::where('question_id', $questionToEdit->id)->get()->toArray();
array_push($questions['postsort'], $questionToEdit);
} elseif ($questionToEdit->detail === 'extremeQuestion'){
$questions['extremeQuestion']['qsortextremequestion'] = $questionToEdit->question;
$questions['extremeQuestion']['qsortaskextremes'] = true;
}
array_push($questionIds, $questionToEdit->id);
}
......@@ -77,4 +80,17 @@ class Question extends Model
{
return Answer::where('question_id', '=', $this->id)->pluck('answer')->toArray();
}
public static function storeExtremeQuestion($extremeQuestion,$study){
$question = new Question();
$question->question = $extremeQuestion;
$question->detail = 'extremeQuestion';
$question->study_id = $study->id;
$question->save();
$answer = new Answer();
$answer->answer = json_decode('{"type":"extremes","answer":""}');
$answer->question_id = $question->id;
$answer->save();
}
}
......@@ -35,8 +35,8 @@ class Sorting extends Model
if (!$request->has('details'))
{
$circles="";
$classifiers="";
$circles = "";
$classifiers = "";
$sections = "";
$sectionNames = "";
$sectionCenter = "";
......@@ -48,9 +48,7 @@ class Sorting extends Model
$circles = 'circles|' . $request->input('sorting.numberofcircles');
$description = '||description|' . $request->input('sorting.description');
$classifiers = $request->input('sorting.classifier') && $request->input('sorting.classifier')['name'] != 'none' ? '||classifier|' . $request->input('sorting.classifier')['name'] : '';
}
else if ($request->input('sorting.id') == 2)
} else if ($request->input('sorting.id') == 2)
{
$circles = 'circles|' . $request->input('sorting.numberofcircles');
$description = '||description|' . $request->input('sorting.description');
......@@ -67,20 +65,20 @@ class Sorting extends Model
$description = '||description|' . $request->input('sorting.description');
$qsortsections = '||qsort|';
foreach($request->input('sorting.qsort') as $qsort)
foreach ($request->input('sorting.qsort') as $qsort)
{
foreach($qsort as &$val){
if($val === "" || $val === false || $val === null) $val = "--empty--";
foreach ($qsort as &$val)
{
if ($val === "" || $val === false || $val === null) $val = "--empty--";
}
$qsortsections .= implode('|',array_reverse($qsort)).'|separator|';
$qsortsections .= implode('|', array_reverse($qsort)) . '|separator|';
}
if($request->input('sorting.qsortshownumbers')) $qsortsections .= 'yesnumbersbelowqsort';
else $qsortsections .= 'nonumbersbelowqsort';
if ($request->input('sorting.qsortshownumbers')) $qsortsections .= 'yesnumbersbelowqsort';
else $qsortsections .= 'nonumbersbelowqsort';
}
$study->sortings()->attach($request->get('sorting')['id'], ['details' => $circles . $description . $classifiers . $sections . $sectionNames . $sectionCenter.$qsortsections]);
$study->sortings()->attach($request->get('sorting')['id'], ['details' => $circles . $description . $classifiers . $sections . $sectionNames . $sectionCenter . $qsortsections]);
} else
{
$study->sortings()->attach($request->get('sortingid'), ['details' => $request->get('details')]);
......
......@@ -67,6 +67,7 @@ class Token extends Model
else $token->author = Auth::user()->id;
if (!empty($tokenToSave['properties'])) $token->properties = json_encode($tokenToSave['properties']);
else $token->properties = '{"description":""}';
$token->save();
$token->studies()->sync($study->id);
}
......@@ -94,6 +95,7 @@ class Token extends Model
else $image = isset($tokenToSave['fileUpload']) && $tokenToSave['fileUpload'] != [] ? $tokenToSave['fileUpload']['base64'] : $tokenToSave['base64'];
} else
{
ray("no image to upload");
$image = config('utilities.sortingBasicIcon');
}
$name = $tokenToSave['name'];
......
......@@ -42,21 +42,21 @@
class="fill-current w-4 h-4 mr-2"
icon="eye"
>
</b-icon> View</a></span>
</b-icon> {{ trans('View') }}</a></span>
<span><a :href="productionUrl+'/export/'+props.row.id+'/interview'" target="_blank"
class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded inline-flex items-center">
<b-icon
class="fill-current w-4 h-4 mr-2"
icon="export"
>
</b-icon> Export</a></span>
</b-icon> {{ trans('Export') }}</a></span>
<span><a href="#" @click="confirmdelete(props.row.id)"
class="bg-red-300 hover:bg-red-400 text-black-800 font-bold py-2 px-4 rounded inline-flex items-center">
<b-icon
class="fill-current w-4 h-4 mr-2"
icon="delete"
>
</b-icon> Delete</a></span>
</b-icon> {{ trans('Delete') }}</a></span>
</b-table-column>
</template>
......@@ -84,7 +84,6 @@
},
data() {
return {
PorcodiDio: 'a',
sortIcon: 'arrow-up',
sortIconSize: 'is-small',
defaultSortDirection: 'asc',
......@@ -93,9 +92,6 @@
computed: {
},
created() {
this.PorcodiDio = this.productionUrl;
console.log(this.productionUrl)
console.log(this.PorcodiDio)
},
methods: {
formatdate: function (date) {
......
......@@ -122,6 +122,7 @@
:ref="'sortingContainer'+index"
:availabletokens="study.available_tokens"
:columns="qsortColumns"
:extremeQuestion="extremeQuestion"
:itemsnumber="qsortColumns.length"
:maxnumber="qsortMaxNumber"
:qsortshownumbers="qsortshownumbers"
......@@ -297,7 +298,7 @@ export default {
mousePosition: {x: 0, y: 0},
loading: false,
results: {
multi: [], onechoice: [],
multi: [], onechoice: [], extremes: []
}, interview: {
center_x: 0,
center_y: 0,
......@@ -411,6 +412,10 @@ export default {
return qsortArray;
},
extremeQuestion: function () {
// extract from question the question with extremeQuestion
return (this.questions.extremeQuestion[0] ? this.questions.extremeQuestion[0] : '')
},
centerLabel: function () {
function substrInBetween(whole_str, str1, str2) {
return whole_str.substring(whole_str.indexOf(str1) + str1.length, whole_str.lastIndexOf(str2));
......@@ -442,7 +447,6 @@ export default {
this.$store.commit('aretherepostsortquestions', false);
}
}, created() {
this.loading = true;
this.interview.timestart = this.calculateDateTime();
......@@ -1373,6 +1377,16 @@ export default {
let sortings = _.compact(this.interview.structure.sorting);
let interview = {};
if (this.sorting[0].id === 3 && this.extremeQuestion !== '') {
/// save the extreme questions, if any.
_.forEach(this.$refs['sortingContainer' + this.sortingtotal][0].extremes.answers, (a) => {
this.results.extremes.push({id: a.question.answerids, text: a.answer, question_id: a.question.qid, token_id: a.token_id})
})
}
_.merge(interview, {results_questions: this.results}, {time_end: this.interview.timeend}, {time_start: this.interview.timestart}, {study: this.study.id}, {interviewed: this.interviewed}, {sorting: sortings}, {publicInterviewToken: publicInterviewToken ?? ''});
window.axios.post('../interviews', interview).then(response => {
......
......@@ -2,40 +2,21 @@
<div class="flex">
<!--Modal-->
<div class="modal opacity-0 pointer-events-none fixed w-full h-full top-0 left-0 flex items-center justify-center">
<div class="absolute w-full h-full bg-gray-900 opacity-50" @click="toggleModal()"></div>
<div class="absolute w-full h-full bg-gray-900 opacity-50"></div>
<div class="modal-container bg-white w-1/2 md:max-w-md mx-auto rounded shadow-lg z-50 overflow-y-auto">
<div class="absolute top-0 right-0 cursor-pointer flex flex-col items-center mt-4 mr-4 text-white text-sm z-50" @click="toggleModal()">
<svg class="fill-current text-white" height="18" viewBox="0 0 18 18" width="18" xmlns="http://www.w3.org/2000/svg">
<path d="M14.53 4.53l-1.06-1.06L9 7.94 4.53 3.47 3.47 4.53 7.94 9l-4.47 4.47 1.06 1.06L9 10.06l4.47 4.47 1.06-1.06L10.06 9z"></path>
</svg>
<span class="text-sm">(Esc)</span>
</div>
<!-- Add margin if you want to see some of the overlay behind the modal-->
<div class="modal-content py-4 text-left px-6">
<!--Title-->
<div class="flex justify-between items-center pb-3">
<p class="text-2xl font-bold">{{ trans(extremes.question) }}</p>
<div class="cursor-pointer z-50" @click="toggleModal()">
<svg class="fill-current text-black" height="18" viewBox="0 0 18 18" width="18" xmlns="http://www.w3.org/2000/svg">
<path d="M14.53 4.53l-1.06-1.06L9 7.94 4.53 3.47 3.47 4.53 7.94 9l-4.47 4.47 1.06 1.06L9 10.06l4.47 4.47 1.06-1.06L10.06 9z"></path>
</svg>
</div>
</div>
<!--Body-->
<p>{{ trans(extremes.question) }}</p>
<p>{{ trans(extremeQuestion.q) }}</p>
<textarea id="extreme_question" v-model="extremes.temporaryAnswer" class="bg-white focus:outline-none focus:ring border border-gray-300 rounded-lg py-2 px-4 block w-full appearance-none leading-normal" type="text">
<textarea required id="extreme_question" v-model="extremes.temporaryAnswer" class="bg-white focus:outline-none focus:ring border border-gray-300 rounded-lg py-2 px-4 block w-full appearance-none leading-normal" type="text">
</textarea>
<!--Footer-->
<div class="flex justify-end pt-2">
<button class="px-4 bg-transparent p-3 rounded-lg text-blue-500 hover:bg-gray-100 hover:text-blue-400 mr-2" @click.prevent="answerExtremePlace">{{ trans('Save and Close') }}</button>
<button class="px-4 bg-blue-500 p-3 rounded-lg text-white hover:bg-blue-400" @click.prevent="toggleModal()">{{ trans('Close') }}</button>
</div>
</div>
......@@ -109,7 +90,7 @@ import {mapState} from "vuex"
export default {
name: "q-sort",
props: ['columns', 'itemsnumber', 'maxnumber', 'availabletokens', 'qsortshownumbers'],
props: ['columns', 'itemsnumber', 'maxnumber', 'availabletokens', 'qsortshownumbers', 'extremeQuestion'],
data() {
return {
sizes: [8, 12, 16, 24, 32],
......@@ -120,7 +101,6 @@ export default {
arrayOfQsort: [],
extremes: {
tokenId: -1,
question: "Why?",
temporaryAnswer: "",
answers: [],
replacingToken: -1
......@@ -199,27 +179,40 @@ export default {
},
methods:
{
answerExtremePlace() {
checkIfTokenIsMovedOnTopOfAnotherExtreme: function (existingAnswer) {
if (existingAnswer && (this.extremes.replacingToken !== existingAnswer.token_id)) {
_.remove(this.extremes.answers, (a) => {
return a.token_id === existingAnswer.token_id
});
}
}, answerExtremePlace() {
// same token sorted again
let existingAnswer = _.find(this.extremes.answers, ['id', this.extremes.tokenId]);
let existingAnswer = _.find(this.extremes.answers, ['token_id', this.extremes.tokenId]);
console.group(existingAnswer)
if (this.extremes.replacingToken !== -1) {
console.log("something was in here")
let replaceExistingToken = _.find(this.extremes.answers, ['id', this.extremes.replacingToken]);
replaceExistingToken.id = this.extremes.tokenId;
// check if the sorted token was already there in the graph.
this.checkIfTokenIsMovedOnTopOfAnotherExtreme(existingAnswer)
let replaceExistingToken = _.find(this.extremes.answers, ['token_id', this.extremes.replacingToken]);
replaceExistingToken.token_id = this.extremes.tokenId;
replaceExistingToken.answer = this.extremes.temporaryAnswer;
replaceExistingToken.question = this.extremeQuestion;
}
else if (!existingAnswer) {
console.log("brend new token")
this.extremes.answers.push({id: this.extremes.tokenId, answer: this.extremes.temporaryAnswer})
this.extremes.answers.push({token_id: this.extremes.tokenId, answer: this.extremes.temporaryAnswer, question: this.extremeQuestion})
}
else {
console.log("same token sorted again.")
existingAnswer.id = this.extremes.tokenId;
existingAnswer.token_id = this.extremes.tokenId;