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 9, 2024
1 parent 1a41ecb commit 850b05d
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 2 deletions.
327 changes: 327 additions & 0 deletions frontend/src/AppInbusImport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
import { csrfToken } from './api.js';
class WeekActivity {
/**
Výskyt aktivity v konkrétním týdnu výuky.
*/
weekActivityId: number; // ID výskytu aktivity v konkrétním týdnu výuky
weekNumber: number; // číslo týdne v semestru
date: string; // datum výskytu aktivity (yyyy-MM-dd)
}
class ConcreteActivity {
/**
Concrete activity in schedule.
*/
concreteActivityId: number; // ID rozvrhové aktivity
template: string; // activityTemplate např. P, P1, C, C1 ...
order: number; // pořadí concreteActivity v rámci activity template, dohromady s activityTemplate pak dává např. P/01, P/02, P1/01, C/01 ...
subjectVersionId: number; // ID verze předmětu
subjectVersionCompleteCode: string; // kód verze předmětu (včetně čísla předmětu)
subjectId: number; // ID předmětu
subjectAbbrev: string; // zkratka předmětu
subjectTitle: string; // název předmětu
educationTypeId: number; // ID typu výuky (přednáška, cvičení, konzultace)
educationTypeAbbrev: string; // zkratka typu výuky
educationTypeTitle: string; // název typu výuky
semesterId: number; // ID semestru
semesterTypeId: number; // ID typu semestru
semesterTypeAbbrev: string; // zkratka typy semestru
semesterTypeTitle: string; // název typu semestru
academicYearId: number; // ID akademického roku
academicYearTitle: string; // název akademického roku
tutorialCentreId: number; // ID konzultačního střediska
tutorialCentreAbbrev: string; // zkratka konzultačního střediska
tutorialCentreTitle: string; // název konzultačního střediska
educationWeekId: number; // ID typu týdne výuky (každý, lichý, sudý, nepravidelně)
educationWeekTitle: string; // název typu týdne výuky
beginScheduleWindowId: number; // ID počáteční výukové jednotky (rozvrhového okna)
activityDuration: number; // délka výuky - počet výukových jednotek následujících po sobě
beginTime: string; // ($date-time) čas začátku výuky (HH:mm:ss)
endTime: string; // ($date-time) čas konce výuky (HH:mm:ss)
weekDayId: number; // ID dne v týdnu
weekDayAbbrev: string; // zkratka dne v týdnu
weekDayTitle: string; // název dne v týdnu
weekActivities: WeekActivity[]; // seznam výskytů aktivity v konkrétních týdnech výuky (využití při nepravidelné výuce), obsahuje: weekActivityId (Integer), weekNumber (Integer), date (Date)
roomIds: number[]; // seznam ID místností
teacherIds: number[]; // seznam ID vyučujících (osob)
studyGroupIds: number[]; // seznam ID studijních skupin (má význam při rozvrhování na studijní skupiny)
// Default values are defined here, since INBUS API sometimes does not provide there values
// and Python need default values at the end of the definition
roomFullcodes = ''; // optimalizační předpočítávaný atribut z vazby na místnosti
// Emptines may be caused by grouping of activities with other activities.
// Typical situation is a lecture shed by different versions of a subject.
studyGroupCodes = ''; // optimalizační předpočítávaný atribut z vazby na studijní skupiny
teacherFullNames = ''; // optimalizační předpočítávaný atribut z vazby na vyučující
teacherLogins = ''; // optimalizační předpočítávaný atribut z vazby na vyučující
teacherShortNames = ''; // optimalizační předpočítávaný atribut z vazby na vyučující
}
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(null);
let canImport = ref(classes_to_import.value.length && !busy.value);
function assembleRequest(url: string): RequestInfo {

Check failure on line 88 in frontend/src/AppInbusImport.vue

View workflow job for this annotation

GitHub Actions / test_frontend

'RequestInfo' is not defined
const headers: Headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.set('Accept', 'application/json');
const request: RequestInfo = new Request(url, {

Check failure on line 94 in frontend/src/AppInbusImport.vue

View workflow job for this annotation

GitHub Actions / test_frontend

'RequestInfo' is not defined
method: 'GET',
headers: headers
});
return request;
}
function svcc2num(svcc: string): number {
const [dept_code, version]: string[] = svcc.split('/');
const [dept, code]: string[] = dept_code.split('-');
const svcc_str = dept + code + version;
const svcc_num = Number(svcc_str);
return svcc_num;
}
class InbusGuarantee {
personId: number;
login: string;
fullName: string;
}
class InbusSubject {
subjectId: number;
code: string;
abbrev: string;
title: string;
guarantee: InbusGuarantee;
}
class InbusSubjectVersion {
subjectVersionId: number;
subject: InbusSubject;
subjectVersionCompleteCode: string;
guarantee: InbusGuarantee;
}
class KelvinSubject {
name: string;
abbr: string;
}
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;
}
class Semester {
pk: number;
year: number;
winter: boolean;
}
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(subject_inbus: InbusSubjectVersion) {
const request = assembleRequest(
'/api/inbus/schedule/subject/version/' + subject_inbus.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;
}
onMounted(() => {
watch(subject_inbus_selected, (newValue, oldValue) => {

Check failure on line 228 in frontend/src/AppInbusImport.vue

View workflow job for this annotation

GitHub Actions / test_frontend

'oldValue' is defined but never used
//console.log('subject_inbus_selected', 'newvalue: ', newValue, 'oldValue: ', oldValue)
inbus_schedule_loaded.value = false;
loadScheduleForSubjectVersionId(newValue);
});
watch(classes_to_import, (newValue, oldValue) => {

Check failure on line 234 in frontend/src/AppInbusImport.vue

View workflow job for this annotation

GitHub Actions / test_frontend

'newValue' is defined but never used

Check failure on line 234 in frontend/src/AppInbusImport.vue

View workflow job for this annotation

GitHub Actions / test_frontend

'oldValue' is defined but never used
canImport.value = classes_to_import.value.length && !busy.value;
});
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">
<option v-for="item in subjects_inbus_filtered" :key="item.subjectVersionId" :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">
<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>
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
8 changes: 8 additions & 0 deletions frontend/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,18 @@ window.addEventListener('DOMContentLoaded', focusTab);

import { defineCustomElement } from 'vue';
import Example from './ExampleComponent.vue';
import AppInbusImport from './AppInbusImport.vue';

customElements.define(
'kelvin-example',
defineCustomElement(Example, {
shadowRoot: false // https://github.com/vuejs/core/issues/4314#issuecomment-2266382877
})
);

customElements.define(
'kelvin-import-inbus',
defineCustomElement(AppInbusImport, {
shadowRoot: false // https://github.com/vuejs/core/issues/4314#issuecomment-2266382877
})
);
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-import-inbus></kelvin-import-inbus>
{% 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 850b05d

Please sign in to comment.