Skip to content

Commit

Permalink
Import from INBUS rewrite in Vue
Browse files Browse the repository at this point in the history
  • Loading branch information
geordi committed Oct 10, 2024
1 parent fb7d83e commit e86f36a
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 2 deletions.
2 changes: 1 addition & 1 deletion frontend/src/ClassList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ $: {
teacher={filter.teacher}
clazz={filter.class} />

<a class="btn btn-sm p-1" href="/#/import" title="Bulk import students from EDISON">
<a class="btn btn-sm p-1" href="/import/inbus" title="Bulk import students from EDISON">
<span class="iconify" data-icon="mdi:calendar-import"></span>
</a>

Expand Down
279 changes: 279 additions & 0 deletions frontend/src/Teacher/InbusImport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
<script setup lang="ts">
/**
* This component provides import of classes of students from Edison
* into Kelvin using the INBUS API.
*
* It is only available to teachers, and it is accessible from the main page
* next to class selection drop down menu.
*/
import { computed, onMounted, ref } from 'vue';
import { csrfToken } from '../api.js';
import { ConcreteActivity, InbusSubjectVersion } from './inbusdto';
interface KelvinSubject {
name: string;
abbr: string;
}
interface Semester {
pk: number;
year: number;
winter: boolean;
}
interface ImportResult {
login: string;
firstname: string;
lastname: string;
created: boolean;
}
interface Result {
users: ImportResult[];
count: number;
error?: string;
}
let subject_inbus_selected = ref<InbusSubjectVersion | null>(null);
let subject_kelvin_selected = ref(null);
let semester = ref(null);
let busy = ref<boolean>(false);
let semesters = ref(null);
let semesters_loading = ref<boolean>(true);
let inbus_and_kelvin_subjects_loading = ref<boolean>(true);
let inbus_schedule_loaded = ref<boolean>(false);
let subjects_inbus: InbusSubjectVersion[] | null = null;
let subjects_inbus_filtered: InbusSubjectVersion[] | null = null;
let subjects_kelvin: KelvinSubject[] | null = null;
let subject_inbus_schedule = ref<ConcreteActivity[] | null>(null);
let classes_to_import = ref([]);
let result = ref<Result | null>(null);
let canImport = computed(() => {
return (
classes_to_import.value.length && !busy.value && subject_kelvin_selected.value && semester.value
);
});
function assembleRequest(url: string): Request {
const headers: Headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.set('Accept', 'application/json');
const request: Request = new Request(url, {
method: 'GET',
headers: headers
});
return request;
}
function svcc2num(svcc: string): number {
const [dept_code, version] = svcc.split('/');
const [dept, code] = dept_code.split('-');
const svcc_str = dept + code + version;
const svcc_num = Number(svcc_str);
return svcc_num;
}
async function loadInbusAndKelvinSubjects() {
const res1 = await fetch('/api/inbus/subject_versions', {});
const res2 = await fetch('/api/subjects/all', {});
subjects_inbus = await res1.json();
const subjects_kelvin_resp = await res2.json();
subjects_kelvin = subjects_kelvin_resp.subjects;
subjects_kelvin.sort((a, b) => {
const name_a = a.name.toUpperCase();
const name_b = b.name.toUpperCase();
if (name_a < name_b) {
return -1;
}
if (name_a > name_b) {
return 1;
}
return 0;
});
const subject_kelvin_abbrs = subjects_kelvin.map((s) => s.abbr);
subjects_inbus_filtered = subjects_inbus.filter((subject_inbus) =>
subject_kelvin_abbrs.includes(subject_inbus.subject.abbrev)
);
subjects_inbus_filtered = subjects_inbus_filtered.sort(
(a, b) => svcc2num(a.subjectVersionCompleteCode) - svcc2num(b.subjectVersionCompleteCode)
);
inbus_and_kelvin_subjects_loading.value = false;
}
function parseSemesters(semesters_data: Semester[]) {
semesters.value = semesters_data.map((sm) => ({
pk: sm.pk,
year: sm.year,
winter: sm.winter,
display: String(sm.year) + (sm.winter ? 'W' : 'S')
}));
semesters_loading.value = false;
}
async function loadSemesters() {
const request = assembleRequest('/api/semesters');
const res = await fetch(request, {});
const semesters_data = await res.json();
parseSemesters(semesters_data['semesters']);
}
async function loadScheduleForSubjectVersionId() {
inbus_schedule_loaded.value = false;
const request = assembleRequest(
'/api/inbus/schedule/subject/version/' + subject_inbus_selected.value.subjectVersionId
);
let res = await fetch(request, {});
subject_inbus_schedule.value = await res.json();
inbus_schedule_loaded.value = true;
}
async function importActivities() {
busy.value = true;
const req = {
semester_id: semester.value,
subject: subject_kelvin_selected.value,
activities: classes_to_import.value
};
//console.log(req);
const res = await fetch('/api/import/activities', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken()
},
body: JSON.stringify(req)
});
result.value = await res.json();
busy.value = false;
}
function onInbusSubjectSelected(event: Event) {

Check failure on line 173 in frontend/src/Teacher/InbusImport.vue

View workflow job for this annotation

GitHub Actions / test_frontend

'event' is defined but never used
loadScheduleForSubjectVersionId();
}
onMounted(() => {
loadSemesters();
loadInbusAndKelvinSubjects();
});
</script>

<template>
<select v-if="!semesters_loading" v-model="semester">
<option v-for="item in semesters" :key="item.pk" :value="item.pk">{{ item.display }}</option>
</select>

<select v-if="!inbus_and_kelvin_subjects_loading" v-model="subject_kelvin_selected">
<option v-for="item in subjects_kelvin" :key="item.abbr" :value="item">
{{ item.abbr }} - {{ item.name }}
</option>
</select>

<select
v-if="!inbus_and_kelvin_subjects_loading"
v-model="subject_inbus_selected"
@change="onInbusSubjectSelected"
>
<option v-for="(item, index) in subjects_inbus_filtered" :key="index" :value="item">
{{ item.subjectVersionCompleteCode }} - {{ item.subject.abbrev }} - {{ item.subject.title }}
</option>
</select>

<table v-if="inbus_schedule_loaded" class="table table-hover table-stripped table-sm">
<tbody>
<tr v-for="ca in subject_inbus_schedule" :key="ca.concreteActivityId">
<td>
<label>
<input
v-model="classes_to_import"
type="checkbox"
:value="ca.concreteActivityId"
name="classesToImport"
/>
{{ ca.educationTypeAbbrev }}
</label>
</td>

<td>{{ ca.educationTypeAbbrev }}/{{ ca.order }}, {{ ca.subjectVersionCompleteCode }}</td>

<td>
{{ ca.teacherFullNames }}
</td>

<td>
{{ ca.weekDayAbbrev }}
</td>

<td>
{{ ca.beginTime }}
</td>

<td>
{{ ca.endTime }}
</td>
</tr>
</tbody>
</table>

<button
class="btn"
:class="{ 'btn-success': canImport, 'btn-danger': !canImport }"
:disabled="!canImport"
@click="importActivities"
>
<span v-if="busy">Importing...</span>
<span v-else>Import</span>
</button>

<div>
<div v-if="result">
<div v-if="result.error" class="alert alert-danger" role="alert">
{{ result.error }}
</div>
<div v-else>
<table class="table table-sm table-hover table-striped">
<thead>
<tr>
<th>Login</th>
<th>First name</th>
<th>Last name</th>
<th>User created</th>
</tr>
</thead>
<tbody>
<tr v-for="item in result.users" :key="item.login">
<td>{{ item.login }}</td>
<td>{{ item.firstname }}</td>
<td>{{ item.lastname }}</td>
<td>{{ item.created }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>

<style scoped></style>
27 changes: 27 additions & 0 deletions frontend/src/Teacher/inbusdto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export interface ConcreteActivity {
/**
Concrete activity in schedule.
*/
concreteActivityId: number;
order: number;
subjectVersionId: number;
subjectVersionCompleteCode: string;
educationTypeAbbrev: string;
beginTime: string;
endTime: string;
weekDayAbbrev: string;
teacherFullNames?: string;
}

interface InbusSubject {
subjectId: number;
code: string;
abbrev: string;
title: string;
}

export interface InbusSubjectVersion {
subjectVersionId: number;
subject: InbusSubject;
subjectVersionCompleteCode: string;
}
2 changes: 2 additions & 0 deletions frontend/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ window.addEventListener('DOMContentLoaded', focusTab);
import { defineCustomElement, h } from 'vue';
import SuspensionWrapper from './components/SuspensionWrapper.vue';
import AllTasks from './Teacher/AllTasks.vue';
import InbusImport from './Teacher/InbusImport.vue';

/**
* Register new Vue component as a custom element.
Expand Down Expand Up @@ -182,3 +183,4 @@ const registerSuspendedVueComponent = (name, component, configureApp = undefined
};

registerSuspendedVueComponent('tasks-all', AllTasks);
registerSuspendedVueComponent('inbus-import', InbusImport);
5 changes: 5 additions & 0 deletions templates/web/inbusimport.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% extends 'web/layout.html' %}

{% block fullcontent %}
<kelvin-inbus-import></kelvin-inbus-import>
{% endblock %}
1 change: 1 addition & 0 deletions web/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

urlpatterns = [
path("", common_view.index, name="index"),
path("import/inbus", common_view.import_inbus, name="import_inbus"),
path("student-view", student_view.student_index, name="student_index"),
path(
"find-task/<int:task_id>/<str:login>/",
Expand Down
7 changes: 6 additions & 1 deletion web/views/common.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import login_required, user_passes_test
from django.shortcuts import render
from django.utils.crypto import get_random_string
from django.conf import settings
Expand All @@ -17,6 +17,11 @@ def index(request):
return student_index(request)


@user_passes_test(is_teacher)
def import_inbus(request):
return render(request, "web/inbusimport.html", {})


@login_required()
def api_token(request):
data = {
Expand Down

0 comments on commit e86f36a

Please sign in to comment.