Skip to content
Snippets Groups Projects
Commit 9dc12955 authored by Madeleine Stenberg Jonassen's avatar Madeleine Stenberg Jonassen
Browse files

Merge branch 'main' into 'ui-playquiz'

# Conflicts:
#   FullstackProsjekt/src/frontend/src/components/shared/EditQuestionModel.vue
parents c474242d 94bebcd9
No related branches found
No related tags found
1 merge request!51Ui playquiz
Pipeline #270153 passed
Showing
with 359 additions and 64 deletions
......@@ -4,7 +4,9 @@ import edu.ntnu.idatt2105.dto.QuestionDTO;
import edu.ntnu.idatt2105.model.Question;
import edu.ntnu.idatt2105.service.QuestionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.stream.Collectors;
......@@ -71,6 +73,7 @@ public class QuestionController {
);
}
/**
* Endpoint for deleting a question by ID.
*
......
......@@ -47,6 +47,7 @@ public class QuestionDTO {
this.quizId = quizId;
}
/**
* Get the ID of the question.
*
......
......@@ -5,6 +5,7 @@ import edu.ntnu.idatt2105.model.Question;
import edu.ntnu.idatt2105.model.QuestionType;
import edu.ntnu.idatt2105.repository.QuestionRepository;
import edu.ntnu.idatt2105.repository.QuizRepository;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
......@@ -41,6 +42,7 @@ public class QuestionService {
*/
@Transactional
public Question createOrUpdateQuestion(QuestionDTO questionDTO) {
Question question;
if (questionDTO.getId() != null) {
Optional<Question> optionalQuestion = questionRepository.findById(questionDTO.getId());
......@@ -66,6 +68,7 @@ public class QuestionService {
question.setScore(questionDTO.getScore());
question.setQuiz(quizRepository.findById(questionDTO.getQuizId()).orElse(null));
return questionRepository.save(question);
}
......
......@@ -177,3 +177,4 @@ class QuestionServiceTest {
}
import axios from 'axios';
import {getToken} from "@/tokenController.js";
export const apiClient = axios.create({
baseURL: 'http://localhost:8080/api',
//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";
axios.defaults.baseURL= 'http://localhost:5173/';
//axios.defaults.headers.common['Authorization'] = 'Bearer' + getToken();
<script>
/*
import {apiClient} from "@/api.js";
export default {
props: {
......@@ -21,6 +22,7 @@ export default {
}
},
beforeMount() {
//console.log("before mount:" + this.questionId);
this.getQuestion(this.questionId);
this.findCorrectAnswerIndex();
},
......@@ -28,11 +30,11 @@ export default {
getQuestion(questionId) {
console.log('Fetching question: ', questionId);
try {
apiClient.get('/questions/get/' + this.questionId).then(response => {
apiClient.get('/questions/get/' + questionId).then(response => {
this.question = response.data;
this.quizId = response.data.quizId;
this.questionText = response.data.id;
this.answers = JSON.parse(response.data.options);
this.questionText = response.data.questionText;
this.answers = response.data.options;
this.correctAnswer = response.data.answer;
this.type = response.data.type;
this.score = response.data.score;
......@@ -78,8 +80,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>
<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>
<div class="modal-overlay" @click="closeModal">
<div @click.stop class="modal-mask">
......@@ -93,11 +208,12 @@ export default {
<label>Score:</label>
<input type="number" id="scoreInput" v-model="score">
</div>
<div class="modal-body">
<div class="modal-body">-->
<!--
<AnswerCard answer-id="answerCard" v-for="answer in answers"
:key="answer.id" :answerId="answer.id" :answer="answer.answer" :correct="answer.correct"/>
-->
<!--
<table class="table">
<thead>
<tr>
......@@ -124,7 +240,76 @@ export default {
</div>
</div>
</div>
</template>
<div class="modal-overlay" @click="closeModal">
<div @click.stop class="modal-mask">
<div class="modal-container">
<div class="question-title">
<h3>Question: </h3>
<input v-model="questionText" placeholder="Type your question here">
</div>
<div class="modal-body">
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Answer</th>
<th scope="col">Correct ?</th>
</tr>
</thead>
<tbody>
<tr v-for="answer in answers">
<th scope="row">{{answer.answer}}</th>
<td>
<input type="text" v-model="answer.answer" id="questionInput">
</td>
<td>
<input :checked="answer.correct_answer === 1" class="form-check-input" :value="answer.id" @change="handleRadioToggle(answer.id)" type="radio">
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
default footer
<button
class="modal-default-button"
@click="$emit('close')">
OK
</button>
</div>
</div>
</div>
</div>-->
<!--
<Transition name="modal">
<div v-if="show" class="modal-mask">
<div class="modal-container">
<div class="modal-header">
<slot name="header">default header</slot>
</div>
<div class="modal-body">
<slot name="body">default body</slot>
</div>
<div class="modal-footer">
<slot name="footer">
default footer
<button
class="modal-default-button"
@click="$emit('close')"
>OK</button>
</slot>
</div>
</div>
</div>
</Transition>
</template>-->
<style>
.modal-mask {
......
......@@ -10,9 +10,9 @@ export default {
data() {
return {
questionText: '',
answers: [{ text: '', correct: false }],
answers: [],
correctAnswerIndex: 0,
type: 'MC',
type: 'MULTIPLE_CHOICE',
score: 0,
correctAnswer: null,
errorMsg: ''
......@@ -22,37 +22,35 @@ export default {
//TODO: error prevention/handling
async handleSubmit() {
try {
this.findCorrectAnswer();
await apiClient.post('/questions', {
quizId: this.quizId,
//this.findCorrectAnswer();
//console.log(this.correctAnswer);
await apiClient.post('/questions/save', {
questionText: this.questionText,
type: this.type,
answer: this.correctAnswer,
options: this.answers.map(answer => answer.text), //just the strings!
score: this.score
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';
}
},
closeModal() {
this.$emit('close');
},
newAnswer() {
this.answers.push({ text: '', correct: false });
this.answers.push({ text: ''});
},
findCorrectAnswer(){
if (this.correctAnswerIndex !== null && this.answers[this.correctAnswerIndex]) {
this.correctAnswer = this.answers[this.correctAnswerIndex].text;
}
selectOption(option) {
this.correctAnswer = option;
console.log(this.correctAnswer);
}
}
};
</script>
<template>
<div class="modal-overlay" @click="closeModal">
<div class="modal-overlay" >
<div @click.stop class="modal-mask">
<div class="modal-container">
<form @submit.prevent="handleSubmit">
......@@ -68,15 +66,15 @@ export default {
<thead>
<tr>
<th scope="col">Answer</th>
<th scope="col">Correct ?</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="'correctAnswer_' + index" :value="index" v-model="correctAnswerIndex">
<label :for="'correctAnswer_' + index">Correct</label>
<input type="radio" :id="index" :value="answer" v-model="correctAnswerIndex"
:checked="answer === correctAnswer" @change="selectOption(answer)">
</td>
</tr>
</tbody>
......@@ -84,9 +82,10 @@ export default {
</div>
<div class="modal-footer">
<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>
</form>
</div>
</div>
</div>
......
......@@ -8,16 +8,21 @@ export default {
questionId: {
type: Number,
required: true,
},
questionText: {
type: String,
required: true,
}
},
mounted() {
//API req, get question from id
beforeMount() {
this.quizId = Number(this.$route.params.quizId);
},
data() {
return {
showEditQuestion: false,
questionNum: 0,
question: 'question text'
question: 'question text',
quizId: null,
}
},
methods: {
......@@ -34,7 +39,6 @@ export default {
},
hideEditQuestion() {
this.showEditQuestion = false;
//TODO: update answer count
}
}
}
......@@ -43,12 +47,12 @@ export default {
<template>
<div class="question-wrapper">
<h4>{{questionId}}</h4>
<h3>{{question}}</h3>
<h3>{{questionText}}</h3>
<div class="quiz-footer">
<button @click="editQuestion" class="edit-btn">Edit</button>
<button @click="deleteQuestion" class="delete-btn">Delete</button>
</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>
</template>
......
......@@ -30,12 +30,13 @@ export default {
questions: [], //question list is just a list of q-ids!!
category: '',
difficulty: '',
errorMsg: ''
errorMsg: '',
//TODO: make quiz object
};
},
beforeMount() {
this.quizId = this.$route.params.quizId;
console.log(this.quizId);
this.getQuiz(this.quizId);
},
mounted() {
......@@ -43,20 +44,29 @@ export default {
//this.getQuiz(this.quizId);
},
methods: {
getQuiz(quizId) {
async getQuiz(quizId) {
console.log('Fetching data for quiz: ', quizId);
try {
apiClient.get('/quiz/quiz/' + this.quizId).then(response => {
await apiClient.get('/quiz/quiz/' + quizId).then(response => {
console.log(response);
this.quizTitle = response.data.title;
this.questions = response.data.questions;
this.creatorId = JSON.parse(response.data.creatorId);
//this.questions = response.data.questions;
this.creatorId = response.data.creatorId;
this.category = response.data.category;
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) {
//TODO: proper error handling
console.log(error);
console.log("Edit quiz error: " + error);
this.errorMsg = 'Error retrieving quizzes';
}
},
......@@ -66,14 +76,18 @@ export default {
},
hideNewQuestion() {
this.showNewQuestion = false;
//TODO: questions answers, +question count
},
deleteQuiz() {
//API req, quizId
try {
apiClient.post('/quiz/delete/' + this.questionId, )
} catch (error) {
this.errorMsg = 'Error deleting quiz';
}
}
},
};
/**
function createQuestion() {
showNewQuestionModal.value = true;
......@@ -164,14 +178,16 @@ async function submitQuestion() {
<router-link to="/overviewQuiz"> <- </router-link>
<h1>Edit quiz: {{quizTitle}}</h1>
<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>
<NewQuestionModel v-if="showNewQuestion" @close="hideNewQuestion" quiz-id="this.quizId"/>
<NewQuestionModel v-if="showNewQuestion" @close="hideNewQuestion" :quiz-id="Number(quizId)"/>
<div class="footer">
<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>
</div>
</div>
......
......@@ -41,10 +41,17 @@ export default {
router.push({name: 'playQuiz', params: {quizId: this.quizId}});
},
editQuiz() {
console.log("edit quiz " + this.quizTitle);
console.log("edit quiz " + this.quizTitle + ", quizId: " + this.quizId);
//create new router-method to editQuiz, using 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>
......@@ -62,7 +69,7 @@ export default {
<p>{{ quizCategory }}</p>
</div>
<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>
</div>
</div>
......
......@@ -4,7 +4,7 @@
<div class="headerDiv">
<router-link to="/dashboard" ><Svg name="go-back-icon" class="go-back-icon"/></router-link>
<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>
<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>
<body>
<div class="profile">
......@@ -71,7 +130,7 @@
</body>
</template>
<!--
<script>
import Svg from "@/assets/Svg.vue";
import Modal from "@/components/shared/modal/Modal.vue"
......@@ -124,7 +183,7 @@ export default {
}
}
};
</script>
</script>-->
<style scoped>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment