Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSCEXAM-1237 Permission for creating BYOD exams #1030

Merged
merged 1 commit into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/controllers/ExamController.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import models.ExamType;
import models.GradeScale;
import models.Language;
import models.Permission;
import models.Role;
import models.Software;
import models.User;
Expand Down Expand Up @@ -494,6 +495,12 @@ public Result createExamDraft(Http.Request request) {
return badRequest("Unsupported execution type");
}
User user = request.attrs().get(Attrs.AUTHENTICATED_USER);
if (
Exam.Implementation.valueOf(implementation) != Exam.Implementation.AQUARIUM &&
!user.hasPermission(Permission.Type.CAN_CREATE_BYOD_EXAM)
) {
return forbidden("No permission to create home examinations");
}
Exam exam = new Exam();
exam.generateHash();
exam.setState(Exam.State.DRAFT);
Expand Down
10 changes: 10 additions & 0 deletions app/controllers/SessionController.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import models.ExamEnrolment;
import models.Language;
import models.Organisation;
import models.Permission;
import models.Reservation;
import models.Role;
import models.User;
Expand Down Expand Up @@ -329,6 +330,15 @@ private void updateUser(User user, Http.Request request) throws AddressException
user.setUserIdentifier(
parse(request.header("schacPersonalUniqueCode").orElse("")).map(this::parseUserIdentifier).orElse(null)
);
// Grant BYOD permission automatically for teachers if configuration so mandates
if (user.hasRole(Role.Name.TEACHER) && configReader.isByodExamCreationPermissionGrantedForNewUsers()) {
Permission permission = DB
.find(Permission.class)
.where()
.eq("type", Permission.Type.CAN_CREATE_BYOD_EXAM)
.findOne();
user.getPermissions().add(permission);
}
user.setEmail(
parse(request.header("mail").orElse(""))
.flatMap(this::validateEmail)
Expand Down
2 changes: 2 additions & 0 deletions app/models/Permission.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public class Permission extends GeneratedIdentityModel implements be.objectify.d
public enum Type {
@EnumValue("1")
CAN_INSPECT_LANGUAGE,
@EnumValue("2")
CAN_CREATE_BYOD_EXAM,
}

private Type type;
Expand Down
1 change: 1 addition & 0 deletions app/util/config/ConfigReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public interface ConfigReader {
String getSettingsPasswordEncryptionKey();
String getHomeOrganisationRef();
Integer getMaxByodExaminationParticipantCount();
boolean isByodExamCreationPermissionGrantedForNewUsers();
String getCourseCodePrefix();
String getIopHost();
boolean isApiKeyUsed();
Expand Down
5 changes: 5 additions & 0 deletions app/util/config/ConfigReaderImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@ public Integer getMaxByodExaminationParticipantCount() {
return config.getInt("exam.byod.maxConcurrentParticipants");
}

@Override
public boolean isByodExamCreationPermissionGrantedForNewUsers() {
return config.getBoolean("exam.byod.permission.allowed");
}

@Override
public String getCourseCodePrefix() {
return config.getString("exam.course.code.prefix");
Expand Down
2 changes: 2 additions & 0 deletions conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ exam.user.studentIds.multiple.organisations = "org1.org,org2.org,org3.org"
exam.byod.seb.active = false
# Enable / disable support for unsupervised home examination
exam.byod.home.active = false
# Automatically grant permission to create BYOD examinations for new users with teacher role
exam.byod.permission.allowed = true
# Maximum number of concurrent BYOD examination participants
exam.byod.maxConcurrentParticipants = 100000
# SEB configuration
Expand Down
5 changes: 5 additions & 0 deletions conf/evolutions/default/133.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# --- !Ups
INSERT INTO permission (id, object_version, type, description) VALUES (2, 1, 2, 'can create BYOD exams')

# --- !Downs
DELETE FROM permission where id = 2;
21 changes: 11 additions & 10 deletions ui/src/app/administrative/users/users.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
}
</div>
</span>
<!-- ng-show="permissions.length > 0" -->

<span class="dropdown" ngbDropdown>
<button
ngbDropdownToggle
Expand Down Expand Up @@ -126,13 +126,13 @@
<td>{{ user.lastLogin | date: 'dd.MM.yyyy HH:mm:ss' }}</td>
<td>
@if (hasRole(user, 'ADMIN')) {
<i class="bi-gear-fill"></i>
<i class="bi-gear-fill pe-1"></i>
}
@if (hasRole(user, 'TEACHER')) {
<i class="bi-person-fill"></i>
<i class="bi-person pe-1"></i>
}
@if (hasRole(user, 'STUDENT')) {
<i class="bi-person"></i>
<i class="bi-mortarboard pe-1"></i>
}
<div class="float-end">
<span class="no-caret pointer" ngbDropdown>
Expand All @@ -146,8 +146,7 @@
>
@if (user.availableRoles.length > 0) {
<i class="text-success bi-plus"></i>
}
@if (user.availableRoles.length === 0) {
} @else {
<i class="text-muted bi-plus"></i>
}
</button>
Expand Down Expand Up @@ -178,8 +177,7 @@
>
@if (user.removableRoles.length > 1) {
<i class="text-danger bi-dash"></i>
}
@if (user.removableRoles.length <= 1) {
} @else {
<i
class="text-muted bi-dash"
triggers="mouseenter:mouseleave"
Expand Down Expand Up @@ -213,7 +211,10 @@
</td>
<td>
@if (hasPermission(user, 'CAN_INSPECT_LANGUAGE')) {
<i class="bi-pencil"></i>
<i class="bi-alphabet pe-1"></i>
}
@if (hasPermission(user, 'CAN_CREATE_BYOD_EXAM')) {
<i class="bi-house-gear pe-1"></i>
}
<div class="float-end">
<span class="no-caret" ngbDropdown>
Expand Down Expand Up @@ -296,7 +297,7 @@
}
</tbody>
</table>
@if (filteredUsers && filteredUsers.length > pageSize) {
@if (filteredUsers.length > pageSize) {
<div>
<xm-paginator
[items]="filteredUsers"
Expand Down
13 changes: 10 additions & 3 deletions ui/src/app/administrative/users/users.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ export class UsersComponent implements OnInit, OnDestroy {
ngUnsubscribe = new Subject();
roles: RoleOption[] = [
{ type: 'ADMIN', name: 'i18n_admin', icon: 'bi-gear' },
{ type: 'TEACHER', name: 'i18n_teacher', icon: 'bi-person-fill' },
{ type: 'STUDENT', name: 'i18n_student', icon: 'bi-person' },
{ type: 'TEACHER', name: 'i18n_teacher', icon: 'bi-person' },
{ type: 'STUDENT', name: 'i18n_student', icon: 'bi-mortarboard' },
];
permissions: PermissionOption[] = [];
loader = { loading: false };
Expand Down Expand Up @@ -99,7 +99,14 @@ export class UsersComponent implements OnInit, OnDestroy {
return {
...p,
name: 'i18n_can_inspect_language',
icon: 'bi-pencil',
icon: 'bi-alphabet',
};
}
if (p.type === PermissionType.CAN_CREATE_BYOD_EXAM) {
return {
...p,
name: 'i18n_can_create_byod_exam',
icon: 'bi-house-gear',
};
}

Expand Down
1 change: 1 addition & 0 deletions ui/src/app/administrative/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Role, User } from '../../session/session.service';

export enum PermissionType {
CAN_INSPECT_LANGUAGE = 'CAN_INSPECT_LANGUAGE',
CAN_CREATE_BYOD_EXAM = 'CAN_CREATE_BYOD_EXAM',
}

export interface Permission {
Expand Down
16 changes: 12 additions & 4 deletions ui/src/app/exam/editor/creation/new-exam.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
popoverTitle="{{ 'i18n_instructions' | translate }}"
ngbPopover="{{ 'i18n_new_exam_type_description' | translate }}"
>
<img src="/assets/images/icon_tooltip.svg" alt="" />
<img
src="/assets/images/icon_tooltip.svg"
alt=""
onerror="this.onerror=null;this.src='/assets/images/icon_tooltip.png'"
/>
</sup>
</label>
<select
Expand Down Expand Up @@ -57,7 +61,11 @@
popoverTitle="{{ 'i18n_instructions' | translate }}"
ngbPopover="{{ 'i18n_examination_type_description' | translate }}"
>
<img src="/assets/images/icon_tooltip.svg" alt="" />
<img
src="/assets/images/icon_tooltip.svg"
alt=""
onerror="this.onerror=null;this.src='/assets/images/icon_tooltip.png'"
/>
</sup>
</label>
<select
Expand All @@ -69,12 +77,12 @@
>
<option value="AQUARIUM">{{ 'i18n_examination_type_aquarium' | translate }}</option>
@if (sebExaminationSupported) {
<option value="CLIENT_AUTH">
<option [disabled]="!canCreateByodExams" value="CLIENT_AUTH">
{{ 'i18n_examination_type_seb' | translate }}
</option>
}
@if (homeExaminationSupported) {
<option value="WHATEVER">
<option [disabled]="!canCreateByodExams" value="WHATEVER">
{{ 'i18n_examination_type_home_exam' | translate }}
</option>
}
Expand Down
4 changes: 4 additions & 0 deletions ui/src/app/exam/editor/creation/new-exam.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { SessionService } from 'src/app/session/session.service';
import { HistoryBackComponent } from '../../../shared/history/history-back.component';
import type { ExamExecutionType, Implementation } from '../../exam.model';
import { ExamService } from '../../exam.service';
Expand All @@ -35,13 +36,16 @@ export class NewExamComponent implements OnInit {
examinationType: Implementation = 'AQUARIUM';
homeExaminationSupported = false;
sebExaminationSupported = false;
canCreateByodExams = false;

constructor(
private http: HttpClient,
private Exam: ExamService,
private Session: SessionService,
) {}

ngOnInit() {
this.canCreateByodExams = this.Session.getUser().canCreateByodExam;
this.Exam.listExecutionTypes$().subscribe((types) => {
this.executionTypes = types;
this.http
Expand Down
2 changes: 1 addition & 1 deletion ui/src/app/session/role/role-picker-dialog.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import type { User } from '../session.service';
title="{{ role.displayName || '' | translate }}"
(click)="activeModal.close(role)"
>
{{ role.displayName || '' | translate }} <i [ngClass]="role.icon || ''"></i>
{{ role.displayName || '' | translate }} <i class="ps-1" [ngClass]="role.icon"></i>
</button>
}
</div>
Expand Down
7 changes: 5 additions & 2 deletions ui/src/app/session/session.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface User {
isLanguageInspector: boolean;
employeeNumber: string | null;
lastLogin: string | null;
canCreateByodExam: boolean;
}

interface Env {
Expand Down Expand Up @@ -245,6 +246,7 @@ export class SessionService implements OnDestroy {
user.isTeacher = role.name === 'TEACHER';
user.isStudent = role.name === 'STUDENT';
user.isLanguageInspector = user.isTeacher && this.hasPermission(user, 'CAN_INSPECT_LANGUAGE');
user.canCreateByodExam = !user.isStudent && this.hasPermission(user, 'CAN_CREATE_BYOD_EXAM');
return user;
}),
);
Expand All @@ -259,11 +261,11 @@ export class SessionService implements OnDestroy {
break;
case 'TEACHER':
role.displayName = 'i18n_teacher';
role.icon = 'bi-person-fill';
role.icon = 'bi-person';
break;
case 'STUDENT':
role.displayName = 'i18n_student';
role.icon = 'bi-person';
role.icon = 'bi-mortarboard';
break;
}
});
Expand All @@ -278,6 +280,7 @@ export class SessionService implements OnDestroy {
isAdmin: loginRole != null && loginRole === 'ADMIN',
isStudent: loginRole != null && loginRole === 'STUDENT',
isLanguageInspector: isTeacher && this.hasPermission(user, 'CAN_INSPECT_LANGUAGE'),
canCreateByodExam: loginRole !== 'STUDENT' && this.hasPermission(user, 'CAN_CREATE_BYOD_EXAM'),
})),
);
}
Expand Down
3 changes: 2 additions & 1 deletion ui/src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1159,5 +1159,6 @@
"i18n_button_preview": "Preview question",
"i18n_no_preview_available": "No preview available",
"i18n_used_in_exams": "Used in exams",
"i18n_quit_password": "SEB-poistumissalasana EN"
"i18n_quit_password": "SEB-poistumissalasana EN",
"i18n_can_create_byod_exam": "Voi luoda omakonetentin EN"
}
3 changes: 2 additions & 1 deletion ui/src/assets/i18n/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1159,5 +1159,6 @@
"i18n_button_preview": "Esikatsele kysymys",
"i18n_no_preview_available": "Kysymyksen esikatselu ei ole saatavilla",
"i18n_used_in_exams": "Käytössä tenteissä",
"i18n_quit_password": "SEB-poistumissalasana"
"i18n_quit_password": "SEB-poistumissalasana",
"i18n_can_create_byod_exam": "Voi luoda omakonetentin"
}
3 changes: 2 additions & 1 deletion ui/src/assets/i18n/sv.json
Original file line number Diff line number Diff line change
Expand Up @@ -1159,5 +1159,6 @@
"i18n_button_preview": "Preview question SV",
"i18n_no_preview_available": "No preview available SV",
"i18n_used_in_exams": "Käytössä tenteissä SV",
"i18n_quit_password": "SEB-poistumissalasana SV"
"i18n_quit_password": "SEB-poistumissalasana SV",
"i18n_can_create_byod_exam": "Voi luoda omakonetentin SV"
}
Loading