Skip to content

Commit

Permalink
CSCEXAM-1214 Question preview feature
Browse files Browse the repository at this point in the history
  • Loading branch information
VirmasaloA authored and Matti Lupari committed Dec 13, 2023
1 parent 2935d3a commit 00e6add
Show file tree
Hide file tree
Showing 25 changed files with 279 additions and 69 deletions.
78 changes: 78 additions & 0 deletions app/controllers/QuestionController.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@
import models.Role;
import models.Tag;
import models.User;
import models.questions.ClozeTestAnswer;
import models.questions.MultipleChoiceOption;
import models.questions.Question;
import models.sections.ExamSectionQuestion;
import models.sections.ExamSectionQuestionOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import play.data.DynamicForm;
Expand Down Expand Up @@ -465,4 +468,79 @@ public Result importQuestions(Http.Request request) throws IOException {
xmlImporter.convert(content, request.attrs().get(Attrs.AUTHENTICATED_USER));
return ok();
}

private Result processPreview(ExamSectionQuestion esq) {
if (esq.getQuestion().getType() == Question.Type.ClozeTestQuestion) {
ClozeTestAnswer answer = new ClozeTestAnswer();
answer.setQuestion(esq);
esq.setClozeTestAnswer(answer);
}

esq.setDerivedMaxScore();
if (esq.getQuestion().getType() == Question.Type.ClaimChoiceQuestion) {
esq.setDerivedMinScore();
}
if (esq.getQuestion().getType() == Question.Type.ClozeTestQuestion) {
esq.getQuestion().setQuestion(null);
}
return ok(esq);
}

@Authenticated
@Restrict({ @Group("TEACHER"), @Group("ADMIN") })
public Result getQuestionPreview(Long qid, Http.Request request) {
User user = request.attrs().get(Attrs.AUTHENTICATED_USER);
ExpressionList<Question> el = DB
.find(Question.class)
.fetch("attachment", "fileName")
.fetch("options")
.where()
.idEq(qid);
if (user.hasRole(Role.Name.TEACHER)) {
el = el.eq("questionOwners", user);
}
return el
.findOneOrEmpty()
.map(question -> {
// Produce fake exam section question based on base question
List<ExamSectionQuestionOption> esqos = question
.getOptions()
.stream()
.map(o -> {
ExamSectionQuestionOption esqo = new ExamSectionQuestionOption();
esqo.setId(1L);
esqo.setOption(o);
esqo.setScore(o.getDefaultScore());
return esqo;
})
.toList();
ExamSectionQuestion esq = new ExamSectionQuestion();
esq.setOptions(Set.copyOf(esqos));
esq.setQuestion(question);
esq.setAnswerInstructions(question.getDefaultAnswerInstructions());
esq.setEvaluationCriteria(question.getDefaultEvaluationCriteria());
esq.setExpectedWordCount(question.getDefaultExpectedWordCount());
esq.setEvaluationType(question.getDefaultEvaluationType());
return processPreview(esq);
})
.orElse(notFound());
}

@Authenticated
@Restrict({ @Group("TEACHER"), @Group("ADMIN") })
public Result getExamSectionQuestionPreview(Long esqId, Http.Request request) {
User user = request.attrs().get(Attrs.AUTHENTICATED_USER);
ExpressionList<ExamSectionQuestion> el = DB
.find(ExamSectionQuestion.class)
.fetch("question", "id, type, question")
.fetch("question.attachment", "fileName")
.fetch("options")
.fetch("options.option", "id, option")
.where()
.idEq(esqId);
if (user.hasRole(Role.Name.TEACHER)) {
el = el.or().in("question.questionOwners", user).in("examSection.exam.examOwners", user).endOr();
}
return el.findOneOrEmpty().map(this::processPreview).orElse(notFound());
}
}
2 changes: 1 addition & 1 deletion app/models/questions/ClozeTestAnswer.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public void setQuestion(ExamSectionQuestion esq) {
b.text("");
b.attr("aria-label", "cloze test answer");
b.attr("type", isNumeric ? "number" : "text");
b.attr("class", "cloze-input");
b.attr("class", "cloze-input mt-2");
if (isNumeric) {
b.attr("step", "any");
// Should allow for using both comma and period as decimal separator
Expand Down
2 changes: 2 additions & 0 deletions conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ DELETE /app/questions/:id controlle
POST /app/questions/owner/:uid controllers.QuestionController.addOwner(uid: Long, request: Request)
POST /app/questions/export controllers.QuestionController.exportQuestions(request: Request)
POST /app/questions/import controllers.QuestionController.importQuestions(request: Request)
GET /app/questions/:id/preview/exam controllers.QuestionController.getExamSectionQuestionPreview(id: Long, request: Request)
GET /app/questions/:id/preview/library controllers.QuestionController.getQuestionPreview(id: Long, request: Request)

############### Reservation interface ###############

Expand Down
2 changes: 0 additions & 2 deletions ui/src/app/enrolment/enrolment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import { CollaborativeParticipation } from '../exam/collaborative/collaborative-
import type { CollaborativeExam, Exam, ExaminationEventConfiguration, ExamParticipation } from '../exam/exam.model';
import type { ExamRoom } from '../reservation/reservation.model';
import type { User } from '../session/session.service';
import { SessionService } from '../session/session.service';
import { ConfirmationDialogService } from '../shared/dialogs/confirmation-dialog.service';
import { isObject } from '../shared/miscellaneous/helpers';
import { AddEnrolmentInformationDialogComponent } from './active/dialogs/add-enrolment-information-dialog.component';
Expand All @@ -46,7 +45,6 @@ export class EnrolmentService {
private ngbModal: NgbModal,
private toast: ToastrService,
private Confirmation: ConfirmationDialogService,
private Session: SessionService,
) {}

removeExaminationEvent = (enrolment: ExamEnrolment) => {
Expand Down
1 change: 1 addition & 0 deletions ui/src/app/examination/examination.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ import { ExaminationSectionComponent } from './section/examination-section.compo
DynamicClozeTestComponent,
],
providers: [ExaminationService, ExaminationStatusService],
exports: [ExaminationQuestionComponent],
})
export class ExaminationModule {}
2 changes: 1 addition & 1 deletion ui/src/app/examination/examination.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class ExaminationService {
getResource = (url: string) => (this.isExternal ? url.replace('/app/', '/app/iop/') : url);

startExam$(hash: string, isPreview: boolean, isCollaboration: boolean, id: number): Observable<Examination> {
const getUrl = (h: string) => (isPreview && id ? '/app/exams/' + id + '/preview' : '/app/student/exam/' + h);
const getUrl = (h: string) => (isPreview && id ? `/app/exams/${id}/preview` : `/app/student/exam/${h}`);
return this.http.get<void>('/app/session').pipe(
switchMap(() =>
this.http.get<Examination>(isCollaboration ? getUrl(hash).replace('/app/', '/app/iop/') : getUrl(hash)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,18 @@ import { ExaminationService } from '../examination.service';

@Component({
selector: 'xm-examination-cloze-test',
template: `<div class="row">
template: `<div class="row" *ngIf="!isPreview">
<div class="col-md-12">
<small class="sitnet-info-text" *ngIf="sq.autosaved">
{{ 'sitnet_autosaved' | translate }}:&nbsp;{{ sq.autosaved | date : 'HH:mm' }}
</small>
<small class="sitnet-info-text" *ngIf="!sq.autosaved"> &nbsp; </small>
</div>
</div>
<div class="padl0 question-type-text">
<span *ngIf="sq.evaluationType === 'Selection'">
{{ 'sitnet_evaluation_select' | translate }}
</span>
<span *ngIf="sq.evaluationType !== 'Selection'">
{{ sq.derivedMaxScore }} {{ 'sitnet_unit_points' | translate }}
</span>
<div class="row">
<div class="col-12">{{ sq.derivedMaxScore }} {{ 'sitnet_unit_points' | translate }}</div>
</div>
<div class="row top-margin-1">
<div class="row mt-2">
<div class="col-md-12">
<button (click)="saveAnswer()" [disabled]="isPreview" class="pointer btn btn-success">
{{ 'sitnet_save' | translate }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
{{ sq.derivedMaxScore }} {{ 'sitnet_unit_points' | translate }}
</span>
</div>
<div *ngIf="exam.implementation !== 'CLIENT_AUTH' && sq.essayAnswer?.attachment?.fileName" class="col-md-9 mt-2">
<div *ngIf="exam?.implementation !== 'CLIENT_AUTH' && sq.essayAnswer?.attachment?.fileName" class="col-md-9 mt-2">
<span class="float-end">
<span class="pe-2 filename-text">{{ sq.essayAnswer?.attachment?.fileName | uppercase }}</span>
<button class="pointer green_button" (click)="removeQuestionAnswerAttachment()">
Expand All @@ -56,7 +56,7 @@
{{ 'sitnet_save' | translate }}
</button>
</div>
<div class="col-md-6" *ngIf="exam.implementation !== 'CLIENT_AUTH' && sq.essayAnswer">
<div class="col-md-6" *ngIf="exam?.implementation !== 'CLIENT_AUTH' && sq.essayAnswer">
<div class="float-end mart10">
<div class="filename-text" *ngIf="!sq.essayAnswer.attachment?.fileName">
{{ 'sitnet_no_attachment' | translate }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { ExaminationService } from '../examination.service';
})
export class ExaminationEssayQuestionComponent implements OnInit {
@Input() sq!: Omit<ExaminationQuestion, 'essayAnswer'> & { essayAnswer: EssayAnswer };
@Input() exam!: Examination;
@Input() exam?: Examination;
@Input() isPreview = false;

questionTitle!: string;
Expand All @@ -48,22 +48,23 @@ export class ExaminationEssayQuestionComponent implements OnInit {
const decodedString = doc.documentElement.innerText;
this.questionTitle = decodedString;
}
saveAnswer = () => this.Examination.saveTextualAnswer$(this.sq, this.exam.hash, false, false).subscribe();
saveAnswer = () => this.Examination.saveTextualAnswer$(this.sq, this.exam?.hash || '', false, false).subscribe();

removeQuestionAnswerAttachment = () => {
const answeredQuestion = this.sq as AnsweredQuestion; // TODO: no casting
if (this.exam.external) {
if (this.exam?.external) {
this.Attachment.removeExternalQuestionAnswerAttachment(answeredQuestion, this.exam.hash);
return;
}
this.Attachment.removeQuestionAnswerAttachment(answeredQuestion);
};

selectFile = () => {
if (this.isPreview) {
if (this.isPreview || !this.exam) {
return;
}
this.Attachment.selectFile(false).then((data) => {
if (this.exam.external) {
if (this.exam?.external) {
this.Files.uploadAnswerAttachment(
'/app/iop/attachment/question/answer',
data.$value.attachmentFile,
Expand Down
21 changes: 11 additions & 10 deletions ui/src/app/examination/question/examination-question.component.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div class="row">
<div class="col-md-12">
<div class="guide-wrapper" [ngClass]="isAnswered() ? 'active' : 'notactive'">
<div class="row justify-content-center">
<div class="row align-items-center">
<div class="col-auto">
<div *ngIf="isAnswered()" [attr.aria-label]="'sitnet_all_questions_answered' | translate">
<img class="mart05" src="/assets/images/icon_question_type_ready_grey.png" alt="" />
Expand All @@ -11,7 +11,7 @@
</div>
</div>
<!-- question text -->
<div class="col">
<div class="col mt-3">
<div *ngIf="sq.question.type !== 'ClozeTestQuestion'">
<div class="section-box-title" *ngIf="expanded" [xmMathJax]="sq.question.question"></div>
<div
Expand All @@ -30,6 +30,7 @@
</div>
</div>
</div>
<!-- Expand button -->
<div class="col-auto">
<div
tabindex="0"
Expand All @@ -44,9 +45,9 @@
</div>
<!-- instruction -->
<div class="row" *ngIf="sq.answerInstructions && expanded">
<div class="col-md-12">
<div class="col-md-12 ps-4">
<p class="question-text question-type-text info" role="note">
<img class="marr20" src="/assets/images/icon_info.svg" alt="" />
<img class="me-4" src="/assets/images/icon_info.svg" alt="" />
{{ 'sitnet_instructions' | translate }}:&nbsp;{{ sq.answerInstructions }}
</p>
</div>
Expand Down Expand Up @@ -85,24 +86,24 @@
<xm-examination-cloze-test
*ngSwitchCase="'ClozeTestQuestion'"
[sq]="sq"
[examHash]="exam.hash"
[examHash]="exam?.hash || ''"
[isPreview]="isPreview"
>
</xm-examination-cloze-test>

<xm-examination-multi-choice-question
*ngSwitchCase="'MultipleChoiceQuestion'"
[sq]="sq"
[examHash]="exam.hash"
[examHash]="exam?.hash || ''"
[isPreview]="isPreview"
[orderOptions]="!exam.external"
[orderOptions]="!exam?.external"
>
</xm-examination-multi-choice-question>

<xm-examination-multi-choice-question
*ngSwitchCase="'ClaimChoiceQuestion'"
[sq]="sq"
[examHash]="exam.hash"
[examHash]="exam?.hash || ''"
[isPreview]="isPreview"
[orderOptions]="true"
>
Expand All @@ -111,9 +112,9 @@
<xm-examination-weighted-multi-choice-question
*ngSwitchCase="'WeightedMultipleChoiceQuestion'"
[sq]="sq"
[examHash]="exam.hash"
[examHash]="exam?.hash || ''"
[isPreview]="isPreview"
[orderOptions]="!exam.external"
[orderOptions]="!exam?.external"
>
</xm-examination-weighted-multi-choice-question>
</div>
Expand Down
17 changes: 10 additions & 7 deletions ui/src/app/examination/question/examination-question.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type ClozeTestAnswer = { [key: string]: string };
templateUrl: './examination-question.component.html',
})
export class ExaminationQuestionComponent implements OnInit, AfterViewInit {
@Input() exam!: Examination;
@Input() exam?: Examination;
@Input() question!: ExaminationQuestion;
@Input() isPreview = false;
@Input() isCollaborative = false;
Expand Down Expand Up @@ -67,12 +67,15 @@ export class ExaminationQuestionComponent implements OnInit, AfterViewInit {
};

downloadQuestionAttachment = () => {
if (this.exam.external) {
this.Attachment.downloadExternalQuestionAttachment(this.exam, this.sq);
} else if (this.isCollaborative) {
this.Attachment.downloadCollaborativeQuestionAttachment(this.exam.id, this.sq);
} else {
this.Attachment.downloadQuestionAttachment(this.sq.question);
if (this.exam) {
if (this.exam.external) {
this.Attachment.downloadExternalQuestionAttachment(this.exam, this.sq);
} else if (this.isCollaborative) {
this.Attachment.downloadCollaborativeQuestionAttachment(this.exam.id, this.sq);
} else {
this.Attachment.downloadQuestionAttachment(this.sq.question);
}
console.error('Cannot retrieve attachment without exam.');
}
};

Expand Down
4 changes: 2 additions & 2 deletions ui/src/app/question/basequestion/question-body.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class QuestionBodyComponent implements OnInit {
@Input() currentOwners: User[] = [];
@Input() lotteryOn = false;
@Input() examId = 0;
@Input() sectionQuestion!: ExamSectionQuestion;
@Input() sectionQuestion?: ExamSectionQuestion;
@Input() collaborative = false;

isInPublishedExam = false;
Expand Down Expand Up @@ -128,7 +128,7 @@ export class QuestionBodyComponent implements OnInit {
});

downloadQuestionAttachment = () => {
if (this.question.attachment && this.question.attachment.externalId) {
if (this.question.attachment && this.question.attachment.externalId && this.sectionQuestion) {
this.Attachment.downloadCollaborativeQuestionAttachment(this.examId, this.sectionQuestion);
return;
}
Expand Down
16 changes: 16 additions & 0 deletions ui/src/app/question/basequestion/question.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@
</form>
<!-- buttons -->
<div class="mart20">
<div class="question-cancel marl20">
<button
(click)="openPreview()"
type="submit"
class="btn btn-success float-end bigbutton"
[disabled]="
!question ||
!question.type ||
hasNoCorrectOption() ||
hasInvalidClaimChoiceOptions() ||
questionForm.invalid
"
>
{{ 'sitnet_button_preview' | translate }}
</button>
</div>
<div class="question-cancel">
<button
[disabled]="
Expand Down
Loading

0 comments on commit 00e6add

Please sign in to comment.