Skip to content
Snippets Groups Projects
Commit 94bebcd9 authored by Kristiane Skogvang Kolshus's avatar Kristiane Skogvang Kolshus
Browse files

Merge branch 'questions_endpoint' into 'main'

Questions endpoint

See merge request !50
parents bc390ee1 5f17325f
No related branches found
No related tags found
1 merge request!50Questions endpoint
Pipeline #270151 passed
Showing
with 291 additions and 66 deletions
...@@ -4,7 +4,9 @@ import edu.ntnu.idatt2105.dto.QuestionDTO; ...@@ -4,7 +4,9 @@ import edu.ntnu.idatt2105.dto.QuestionDTO;
import edu.ntnu.idatt2105.model.Question; import edu.ntnu.idatt2105.model.Question;
import edu.ntnu.idatt2105.service.QuestionService; import edu.ntnu.idatt2105.service.QuestionService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
...@@ -71,6 +73,7 @@ public class QuestionController { ...@@ -71,6 +73,7 @@ public class QuestionController {
); );
} }
/** /**
* Endpoint for deleting a question by ID. * Endpoint for deleting a question by ID.
* *
......
...@@ -47,6 +47,7 @@ public class QuestionDTO { ...@@ -47,6 +47,7 @@ public class QuestionDTO {
this.quizId = quizId; this.quizId = quizId;
} }
/** /**
* Get the ID of the question. * Get the ID of the question.
* *
......
...@@ -5,6 +5,7 @@ import edu.ntnu.idatt2105.model.Question; ...@@ -5,6 +5,7 @@ import edu.ntnu.idatt2105.model.Question;
import edu.ntnu.idatt2105.model.QuestionType; import edu.ntnu.idatt2105.model.QuestionType;
import edu.ntnu.idatt2105.repository.QuestionRepository; import edu.ntnu.idatt2105.repository.QuestionRepository;
import edu.ntnu.idatt2105.repository.QuizRepository; import edu.ntnu.idatt2105.repository.QuizRepository;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
...@@ -41,6 +42,7 @@ public class QuestionService { ...@@ -41,6 +42,7 @@ public class QuestionService {
*/ */
@Transactional @Transactional
public Question createOrUpdateQuestion(QuestionDTO questionDTO) { public Question createOrUpdateQuestion(QuestionDTO questionDTO) {
Question question; Question question;
if (questionDTO.getId() != null) { if (questionDTO.getId() != null) {
Optional<Question> optionalQuestion = questionRepository.findById(questionDTO.getId()); Optional<Question> optionalQuestion = questionRepository.findById(questionDTO.getId());
...@@ -66,6 +68,7 @@ public class QuestionService { ...@@ -66,6 +68,7 @@ public class QuestionService {
question.setScore(questionDTO.getScore()); question.setScore(questionDTO.getScore());
question.setQuiz(quizRepository.findById(questionDTO.getQuizId()).orElse(null)); question.setQuiz(quizRepository.findById(questionDTO.getQuizId()).orElse(null));
return questionRepository.save(question); return questionRepository.save(question);
} }
......
...@@ -177,3 +177,4 @@ class QuestionServiceTest { ...@@ -177,3 +177,4 @@ class QuestionServiceTest {
} }
import axios from 'axios'; import axios from 'axios';
import {getToken} from "@/tokenController.js";
export const apiClient = axios.create({ export const apiClient = axios.create({
baseURL: 'http://localhost:8080/api', baseURL: 'http://localhost:8080/api',
//TODO: set api URL //TODO: set api URL
}); });
apiClient.interceptors.request.use(
(config) => {
const token = getToken();
if(token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
});
\ No newline at end of file
import axios from "axios"; import axios from "axios";
axios.defaults.baseURL= 'http://localhost:5173/'; axios.defaults.baseURL= 'http://localhost:5173/';
//axios.defaults.headers.common['Authorization'] = 'Bearer' + getToken();
<script> <script>
/*
import {apiClient} from "@/api.js"; import {apiClient} from "@/api.js";
export default { export default {
props: { props: {
...@@ -21,6 +22,7 @@ export default { ...@@ -21,6 +22,7 @@ export default {
} }
}, },
beforeMount() { beforeMount() {
//console.log("before mount:" + this.questionId);
this.getQuestion(this.questionId); this.getQuestion(this.questionId);
this.findCorrectAnswerIndex(); this.findCorrectAnswerIndex();
}, },
...@@ -28,11 +30,11 @@ export default { ...@@ -28,11 +30,11 @@ export default {
getQuestion(questionId) { getQuestion(questionId) {
console.log('Fetching question: ', questionId); console.log('Fetching question: ', questionId);
try { try {
apiClient.get('/questions/get/' + this.questionId).then(response => { apiClient.get('/questions/get/' + questionId).then(response => {
this.question = response.data; this.question = response.data;
this.quizId = response.data.quizId; this.quizId = response.data.quizId;
this.questionText = response.data.id; this.questionText = response.data.questionText;
this.answers = JSON.parse(response.data.options); this.answers = response.data.options;
this.correctAnswer = response.data.answer; this.correctAnswer = response.data.answer;
this.type = response.data.type; this.type = response.data.type;
this.score = response.data.score; this.score = response.data.score;
...@@ -79,9 +81,121 @@ export default { ...@@ -79,9 +81,121 @@ export default {
} }
} }
*/
import {apiClient} from "@/api.js";
export default {
props: {
quizId: {
type: Number,
required: true
},
questionId: {
type: Number,
required: true
}
},
data() {
return {
questionText: '',
answers: [],
correctAnswerIndex: 0,
type: 'MULTIPLE_CHOICE',
score: 0,
correctAnswer: null,
errorMsg: ''
}
},
beforeMount() {
this.getQuestionInfo(this.questionId);
},
methods: {
async getQuestionInfo(questionId) {
try {
await apiClient.get('/questions/get/' + questionId).then(response => {
this.questionText = response.data.questionText;
this.answers = response.data.options;
this.correctAnswer = response.data.answer;
this.score = response.data.score;
this.type = response.data.type;
})
}catch (error) {
console.log("Edit question error: " + error);
this.errorMsg = 'Error retrieving question';
}
},
async handleSubmit() {
try {
await apiClient.post('/questions/save', {
//TODO: add questionID
questionText: this.questionText,
type: this.type,
answer: this.correctAnswer.text,
options: this.answers.map(answer => answer.text),
score: this.score,
quizId: this.quizId,
})
this.$emit('close');
} catch (error) {
console.log("error: " + error);
this.errorMsg = 'Error submitting question';
}
},
newAnswer() {
this.answers.push({ text: ''});
},
selectOption(option) {
this.correctAnswer = option;
console.log(this.correctAnswer);
},
}
}
</script> </script>
<template>
<div class="modal-overlay" >
<div @click.stop class="modal-mask">
<div class="modal-container">
<form @submit.prevent="handleSubmit">
<div class="question-title">
<h3>Question:</h3>
<input v-model="questionText" placeholder="Question">
<label>Score:</label>
<input type="number" id="scoreInput" v-model="score">
</div>
<div class="modal-body">
<table class="table">
<thead>
<tr>
<th scope="col">Answer</th>
<th scope="col">Correct</th>
</tr>
</thead>
<tbody>
<tr v-for="(answer, index) in answers">
<td><input type="text" v-model="answer.text"></td>
<td>
<input type="radio" :id="index" :value="answer" v-model="correctAnswerIndex"
:checked="answer === correctAnswer" @change="selectOption(answer)">
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button class="edit-btn" @click="newAnswer">Add answer</button>
<button class="modal-default-button" type="submit">SUBMIT</button>
</div>
</form>
</div>
</div>
</div>
</template>
<!--
<template> <template>
<div class="modal-overlay" @click="closeModal"> <div class="modal-overlay" @click="closeModal">
<div @click.stop class="modal-mask"> <div @click.stop class="modal-mask">
...@@ -95,11 +209,12 @@ export default { ...@@ -95,11 +209,12 @@ export default {
<label>Score:</label> <label>Score:</label>
<input type="number" id="scoreInput" v-model="score"> <input type="number" id="scoreInput" v-model="score">
</div> </div>
<div class="modal-body"> <div class="modal-body">-->
<!-- <!--
<AnswerCard answer-id="answerCard" v-for="answer in answers" <AnswerCard answer-id="answerCard" v-for="answer in answers"
:key="answer.id" :answerId="answer.id" :answer="answer.answer" :correct="answer.correct"/> :key="answer.id" :answerId="answer.id" :answer="answer.answer" :correct="answer.correct"/>
--> -->
<!--
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
...@@ -126,7 +241,7 @@ export default { ...@@ -126,7 +241,7 @@ export default {
</div> </div>
</div> </div>
</div> </div>
<!--
<div class="modal-overlay" @click="closeModal"> <div class="modal-overlay" @click="closeModal">
<div @click.stop class="modal-mask"> <div @click.stop class="modal-mask">
...@@ -194,8 +309,8 @@ export default { ...@@ -194,8 +309,8 @@ export default {
</div> </div>
</div> </div>
</Transition> </Transition>
-->
</template> </template>-->
<style> <style>
.modal-mask { .modal-mask {
......
...@@ -10,9 +10,9 @@ export default { ...@@ -10,9 +10,9 @@ export default {
data() { data() {
return { return {
questionText: '', questionText: '',
answers: [{ text: '', correct: false }], answers: [],
correctAnswerIndex: 0, correctAnswerIndex: 0,
type: 'MC', type: 'MULTIPLE_CHOICE',
score: 0, score: 0,
correctAnswer: null, correctAnswer: null,
errorMsg: '' errorMsg: ''
...@@ -22,37 +22,35 @@ export default { ...@@ -22,37 +22,35 @@ export default {
//TODO: error prevention/handling //TODO: error prevention/handling
async handleSubmit() { async handleSubmit() {
try { try {
this.findCorrectAnswer(); //this.findCorrectAnswer();
await apiClient.post('/questions', { //console.log(this.correctAnswer);
quizId: this.quizId, await apiClient.post('/questions/save', {
questionText: this.questionText, questionText: this.questionText,
type: this.type, type: this.type,
answer: this.correctAnswer, answer: this.correctAnswer.text,
options: this.answers.map(answer => answer.text), //just the strings! options: this.answers.map(answer => answer.text),
score: this.score score: this.score,
quizId: this.quizId,
}) })
this.$emit('close');
} catch (error) { } catch (error) {
console.log("error: " + error);
this.errorMsg = 'Error submitting question'; this.errorMsg = 'Error submitting question';
} }
}, },
closeModal() {
this.$emit('close');
},
newAnswer() { newAnswer() {
this.answers.push({ text: '', correct: false }); this.answers.push({ text: ''});
}, },
findCorrectAnswer(){ selectOption(option) {
if (this.correctAnswerIndex !== null && this.answers[this.correctAnswerIndex]) { this.correctAnswer = option;
this.correctAnswer = this.answers[this.correctAnswerIndex].text; console.log(this.correctAnswer);
}
} }
} }
}; };
</script> </script>
<template> <template>
<div class="modal-overlay" @click="closeModal"> <div class="modal-overlay" >
<div @click.stop class="modal-mask"> <div @click.stop class="modal-mask">
<div class="modal-container"> <div class="modal-container">
<form @submit.prevent="handleSubmit"> <form @submit.prevent="handleSubmit">
...@@ -68,15 +66,15 @@ export default { ...@@ -68,15 +66,15 @@ export default {
<thead> <thead>
<tr> <tr>
<th scope="col">Answer</th> <th scope="col">Answer</th>
<th scope="col">Correct ?</th> <th scope="col">Correct</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(answer, index) in answers"> <tr v-for="(answer, index) in answers">
<td><input type="text" v-model="answer.text"></td> <td><input type="text" v-model="answer.text"></td>
<td> <td>
<input type="radio" :id="'correctAnswer_' + index" :value="index" v-model="correctAnswerIndex"> <input type="radio" :id="index" :value="answer" v-model="correctAnswerIndex"
<label :for="'correctAnswer_' + index">Correct</label> :checked="answer === correctAnswer" @change="selectOption(answer)">
</td> </td>
</tr> </tr>
</tbody> </tbody>
...@@ -84,9 +82,10 @@ export default { ...@@ -84,9 +82,10 @@ export default {
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="edit-btn" @click="newAnswer">Add answer</button> <button class="edit-btn" @click="newAnswer">Add answer</button>
<button class="modal-default-button" @click="$emit('close')">SUBMIT</button> <button class="modal-default-button" type="submit">SUBMIT</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -10,16 +10,21 @@ export default { ...@@ -10,16 +10,21 @@ export default {
questionId: { questionId: {
type: Number, type: Number,
required: true, required: true,
},
questionText: {
type: String,
required: true,
} }
}, },
mounted() { beforeMount() {
//API req, get question from id this.quizId = Number(this.$route.params.quizId);
}, },
data() { data() {
return { return {
showEditQuestion: false, showEditQuestion: false,
questionNum: 0, questionNum: 0,
question: 'question text' question: 'question text',
quizId: null,
} }
}, },
methods: { methods: {
...@@ -36,7 +41,6 @@ export default { ...@@ -36,7 +41,6 @@ export default {
}, },
hideEditQuestion() { hideEditQuestion() {
this.showEditQuestion = false; this.showEditQuestion = false;
//TODO: update answer count
} }
} }
} }
...@@ -45,12 +49,12 @@ export default { ...@@ -45,12 +49,12 @@ export default {
<template> <template>
<div class="question-wrapper"> <div class="question-wrapper">
<h4>{{questionId}}</h4> <h4>{{questionId}}</h4>
<h3>{{question}}</h3> <h3>{{questionText}}</h3>
<div class="quiz-footer"> <div class="quiz-footer">
<button @click="editQuestion" class="edit-btn">Edit</button> <button @click="editQuestion" class="edit-btn">Edit</button>
<button @click="deleteQuestion" class="delete-btn">Delete</button> <button @click="deleteQuestion" class="delete-btn">Delete</button>
</div> </div>
<EditQuestionModel :question-id=questionId v-if="this.showEditQuestion" @close="hideEditQuestion"/> <EditQuestionModel :question-id=questionId :quiz-id=quizId v-if="this.showEditQuestion" @close="hideEditQuestion"/>
</div> </div>
</template> </template>
......
...@@ -30,12 +30,13 @@ export default { ...@@ -30,12 +30,13 @@ export default {
questions: [], //question list is just a list of q-ids!! questions: [], //question list is just a list of q-ids!!
category: '', category: '',
difficulty: '', difficulty: '',
errorMsg: '' errorMsg: '',
//TODO: make quiz object //TODO: make quiz object
}; };
}, },
beforeMount() { beforeMount() {
this.quizId = this.$route.params.quizId; this.quizId = this.$route.params.quizId;
console.log(this.quizId);
this.getQuiz(this.quizId); this.getQuiz(this.quizId);
}, },
mounted() { mounted() {
...@@ -43,20 +44,29 @@ export default { ...@@ -43,20 +44,29 @@ export default {
//this.getQuiz(this.quizId); //this.getQuiz(this.quizId);
}, },
methods: { methods: {
getQuiz(quizId) { async getQuiz(quizId) {
console.log('Fetching data for quiz: ', quizId); console.log('Fetching data for quiz: ', quizId);
try { try {
apiClient.get('/quiz/quiz/' + this.quizId).then(response => { await apiClient.get('/quiz/quiz/' + quizId).then(response => {
console.log(response); console.log(response);
this.quizTitle = response.data.title; this.quizTitle = response.data.title;
this.questions = response.data.questions; //this.questions = response.data.questions;
this.creatorId = JSON.parse(response.data.creatorId); this.creatorId = response.data.creatorId;
this.category = response.data.category; this.category = response.data.category;
this.difficulty = response.data.difficulty; this.difficulty = response.data.difficulty;
}); });
//console.log("fetching questions");
//get questions separately
await apiClient.get('/questions/allQuestionsToAQuiz/' + quizId).then(response => {
//console.log("fetched questions");
//console.log(response.data);
this.questions = response.data;
console.log(this.questions[1]);
});
} catch (error) { } catch (error) {
//TODO: proper error handling //TODO: proper error handling
console.log(error); console.log("Edit quiz error: " + error);
this.errorMsg = 'Error retrieving quizzes'; this.errorMsg = 'Error retrieving quizzes';
} }
}, },
...@@ -66,14 +76,18 @@ export default { ...@@ -66,14 +76,18 @@ export default {
}, },
hideNewQuestion() { hideNewQuestion() {
this.showNewQuestion = false; this.showNewQuestion = false;
//TODO: questions answers, +question count
}, },
deleteQuiz() { deleteQuiz() {
//API req, quizId try {
apiClient.post('/quiz/delete/' + this.questionId, )
} catch (error) {
this.errorMsg = 'Error deleting quiz';
}
} }
}, },
}; };
/** /**
function createQuestion() { function createQuestion() {
showNewQuestionModal.value = true; showNewQuestionModal.value = true;
...@@ -164,14 +178,16 @@ async function submitQuestion() { ...@@ -164,14 +178,16 @@ async function submitQuestion() {
<router-link to="/overviewQuiz"> <- </router-link> <router-link to="/overviewQuiz"> <- </router-link>
<h1>Edit quiz: {{quizTitle}}</h1> <h1>Edit quiz: {{quizTitle}}</h1>
<div class="question-div"> <div class="question-div">
<QuestionCard id="questionCard" v-for="question in questions" :key="question.id" :question-id=question.id
/> <QuestionCard id="questionCard" v-for="question in questions" :key="question.id"
:question-id=question.id :question-text=question.questionText />
</div> </div>
<NewQuestionModel v-if="showNewQuestion" @close="hideNewQuestion" quiz-id="this.quizId"/> <NewQuestionModel v-if="showNewQuestion" @close="hideNewQuestion" :quiz-id="Number(quizId)"/>
<div class="footer"> <div class="footer">
<button @click="newQuestion" class="add-Btn"> Add Question </button> <button @click="newQuestion" class="add-Btn"> Add Question </button>
<button class="delete-btn"> DELETE QUIZ </button> <button class="delete-btn" @click="deleteQuiz"> DELETE QUIZ </button>
<button class="save-Btn"> SAVE QUIZ </button> <button class="save-Btn"> SAVE QUIZ </button>
</div> </div>
</div> </div>
......
...@@ -41,10 +41,17 @@ export default { ...@@ -41,10 +41,17 @@ export default {
router.push({name: 'playQuiz', params: {quizId: this.quizId}}); router.push({name: 'playQuiz', params: {quizId: this.quizId}});
}, },
editQuiz() { editQuiz() {
console.log("edit quiz " + this.quizTitle); console.log("edit quiz " + this.quizTitle + ", quizId: " + this.quizId);
//create new router-method to editQuiz, using quizId //create new router-method to editQuiz, using quizId
router.push({name: 'editQuiz', params: {quizId: this.quizId}}); router.push({name: 'editQuiz', params: {quizId: this.quizId}});
}, },
deleteQuiz() {
try {
apiClient.post('/quiz/delete/' + this.quizId, )
} catch (error) {
this.errorMsg = 'Error deleting quiz';
}
}
} }
} }
</script> </script>
...@@ -62,7 +69,7 @@ export default { ...@@ -62,7 +69,7 @@ export default {
<p>{{ quizCategory }}</p> <p>{{ quizCategory }}</p>
</div> </div>
<div class="quiz-footer"> <div class="quiz-footer">
<!--<button @click="playQuiz" class="play-btn">Play</button>--> <button @click="playQuiz" class="play-btn">Play</button>
<button @click="editQuiz" class="edit-btn">Edit</button> <button @click="editQuiz" class="edit-btn">Edit</button>
</div> </div>
</div> </div>
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<div class="headerDiv"> <div class="headerDiv">
<router-link to="/dashboard" ><Svg name="go-back-icon" class="go-back-icon"/></router-link> <router-link to="/dashboard" ><Svg name="go-back-icon" class="go-back-icon"/></router-link>
<h1>Your quizzes</h1> <h1>Your quizzes</h1>
<router-link to="/create-quiz" class="add-Btn">Create new quiz</router-link> <router-link to="/createQuiz" class="add-Btn">Create new quiz</router-link>
</div> </div>
<p>Play, edit or delete a quiz saved from your profile</p> <p>Play, edit or delete a quiz saved from your profile</p>
......
<script>
import Svg from "@/assets/Svg.vue";
import Modal from "@/components/shared/modal/Modal.vue"
import {ref} from 'vue'
import {getIdByToken} from "@/tokenController.js";
import {apiClient} from "@/api.js";
export default {
components: {Modal, Svg},
data() {
return {
userId: null,
quizList:[],
showModal: ref(false),
isLoggedIn: true,
user: {
username: '',
}
};
},
mounted() {
this.user.username = localStorage.getItem('username');
this.populateQuizzes();
},
computed: {
quizAttempts() {
// Mock data for quiz attempts (replace with actual data)
return [
{ id: 1, quizTitle: 'Math Quiz', score: '80%', date: '2024-04-05' },
{ id: 2, quizTitle: 'Science Quiz', score: '90%', date: '2024-04-04' }
];
}
},
methods:{
logout(){
this.isLoggedIn = false;
this.$router.push('/login');
},
closeModal(){
this.showModal=false;
},
async populateQuizzes() {
try {
await this.setUserId();
const response = await apiClient.get('/quiz/creator/' + this.userId);
this.quizList = response.data;
} catch (error) {
console.error('Error retrieving quizzes:', error);
}
},
async setUserId() {
this.userId = await getIdByToken();
}
}
};
</script>
<template> <template>
<body> <body>
<div class="profile"> <div class="profile">
...@@ -71,7 +130,7 @@ ...@@ -71,7 +130,7 @@
</body> </body>
</template> </template>
<!--
<script> <script>
import Svg from "@/assets/Svg.vue"; import Svg from "@/assets/Svg.vue";
import Modal from "@/components/shared/modal/Modal.vue" import Modal from "@/components/shared/modal/Modal.vue"
...@@ -124,7 +183,7 @@ export default { ...@@ -124,7 +183,7 @@ export default {
} }
} }
}; };
</script> </script>-->
<style scoped> <style scoped>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment