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

Submission structure showcase #464

Merged
merged 7 commits into from
May 23, 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
6 changes: 4 additions & 2 deletions frontend/src/assets/lang/app/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
"leaveGroup": "Leave group",
"create": "Create new project",
"save": "Save project",
"edit": "Save project",
"edit": "Edit project",
"name": "Project name",
"description": "Description",
"startDate": "Start project",
Expand All @@ -97,7 +97,9 @@
"title": "Structure checks",
"placeholder": "Give a name to this folder",
"cancelSelection": "Deselect {0}",
"newFolder": "New folder"
"newFolder": "New folder",
"obligatedExtensions": "Obligated extensions",
"blockedExtensions": "Blocked extensions"
},
"extraChecks": {
"title": "Automatic checks on a submission",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/assets/lang/app/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@
"title": "Indieningsstructuur",
"placeholder": "Geef deze nieuwe map een naam",
"cancelSelection": "Deselecteer {0}",
"newFolder": "Nieuwe map"
"newFolder": "Nieuwe map",
"obligatedExtensions": "Verplichte extensies",
"blockedExtensions": "Niet toegelaten extensies"
},
"extraChecks": {
"title": "Automatische checks op een indiening",
Expand Down
64 changes: 64 additions & 0 deletions frontend/src/components/projects/ProjectStructure.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<script setup lang="ts">
import Tree from 'primevue/tree';
import Chip from 'primevue/chip';
import { useI18n } from 'vue-i18n';
import { type StructureCheck } from '@/types/StructureCheck.ts';
import { Project } from '@/types/Project.ts';

const { t } = useI18n();

const props = defineProps<{
structureChecks: StructureCheck[];
}>();

console.log(props.structureChecks);
</script>

<template>
<div>
<div class="p-2">
{{ t('views.projects.structureChecks.title') }}
</div>
<Tree class="w-100" selection-mode="single" :value="Project.getNodes(structureChecks)">
<template #default="{ node }">
<template v-if="node.obligated">
<div class="w-full" v-tooltip="t('views.projects.structureChecks.obligatedExtensions')">
bsilkyn marked this conversation as resolved.
Show resolved Hide resolved
<Chip
class="m-1"
:label="extension"
:key="extension"
v-for="extension in node.data.getObligatedExtensionList()"
/>
</div>
</template>
<template v-else-if="node.blocked">
<div class="w-full" v-tooltip="t('views.projects.structureChecks.blockedExtensions')">
bsilkyn marked this conversation as resolved.
Show resolved Hide resolved
<Chip
class="m-1"
:label="extension"
:key="extension"
v-for="extension in node.data.getBlockedExtensionList()"
/>
</div>
</template>
<template v-else>
<div class="flex align-items-center justify-content-between gap-3">
<span>
{{ node.label }}
</span>
</div>
</template>
</template>
</Tree>
</div>
</template>

<style scoped lang="scss">
.p-treenode-label {
width: 100%;

ul {
width: 100%;
}
}
</style>
72 changes: 6 additions & 66 deletions frontend/src/components/projects/ProjectStructureEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import Tree from 'primevue/tree';
import Button from 'primevue/button';
import Chips from 'primevue/chips';
import InputText from 'primevue/inputtext';
import { StructureCheck } from '@/types/StructureCheck.ts';
import { computed, ref } from 'vue';
import { ref } from 'vue';
import { type TreeNode } from 'primevue/treenode';
import { PrimeIcons } from 'primevue/api';
import { useI18n } from 'vue-i18n';
import { Project } from '@/types/Project.ts';
import { StructureCheck } from '@/types/StructureCheck.ts';

/* Models */
const structureChecks = defineModel<StructureCheck[]>();
Expand All @@ -21,31 +22,6 @@ const editingStructureCheck = ref<StructureCheck | null>(null);
const selectedKeys = ref<string[]>([]);
const expandedKeys = ref<string[]>([]);

/* Computed */
const nodes = computed<TreeNode[]>(() => {
const nodes: TreeNode[] = [];

if (structureChecks.value !== undefined) {
for (const [i, check] of structureChecks.value.entries()) {
const hierarchy = check.getDirectoryHierarchy();
let currentNodes = nodes;

for (const [j, part] of hierarchy.entries()) {
let node = currentNodes.find((node) => node.label === part);

if (node === undefined) {
node = newTreeNode(check, `${i}${j}`, part, j === hierarchy.length - 1);
currentNodes.push(node);
}

currentNodes = node.children ?? [];
}
}
}

return nodes;
});

/**
* Delete a structure check from the list.
*
Expand Down Expand Up @@ -119,44 +95,6 @@ function selectStructureCheck(node: TreeNode): void {
selectedStructureCheck.value = node.data;
}
}

/**
* Construct a tree node from a structure check folder path.
*
* @param check
* @param key
* @param label
* @param leaf
*/
function newTreeNode(check: StructureCheck, key: string, label: string, leaf: boolean = false): TreeNode {
const node: TreeNode = {
key,
label,
data: check,
icon: PrimeIcons.FOLDER,
check: leaf,
children: [],
};

if (leaf) {
node.children = [
{
key: key + '-obligated',
icon: PrimeIcons.CHECK_CIRCLE,
data: check,
obligated: true,
},
{
key: key + '-blocked',
icon: PrimeIcons.TIMES_CIRCLE,
data: check,
blocked: true,
},
];
}

return node;
}
</script>

<template>
Expand All @@ -166,13 +104,14 @@ function newTreeNode(check: StructureCheck, key: string, label: string, leaf: bo
selection-mode="single"
v-model:selection-keys="selectedKeys"
v-model:expanded-keys="expandedKeys"
:value="nodes"
:value="Project.getNodes(structureChecks)"
@node-select="selectStructureCheck"
>
<template #default="{ node }">
<template v-if="node.obligated">
<Chips
class="w-full"
separator=","
:model-value="node.data.getObligatedExtensionList()"
@update:model-value="node.data.setObligatedExtensionList($event)"
v-tooltip="t('views.projects.structureChecks.obligatedExtensions')"
Expand All @@ -185,6 +124,7 @@ function newTreeNode(check: StructureCheck, key: string, label: string, leaf: bo
<template v-else-if="node.blocked">
<Chips
class="w-full"
separator=","
:model-value="node.data.getBlockedExtensionList()"
@update:model-value="node.data.setBlockedExtensionList($event)"
v-tooltip="t('views.projects.structureChecks.blockedExtensions')"
Expand Down
70 changes: 70 additions & 0 deletions frontend/src/types/Project.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import moment from 'moment';
import { type TreeNode } from 'primevue/treenode';
import { PrimeIcons } from 'primevue/api';
import { Course, type CourseJSON } from './Course.ts';
import { type ExtraCheck } from './ExtraCheck.ts';
import { type Group } from './Group.ts';
Expand Down Expand Up @@ -117,6 +119,74 @@ export class Project {
);
}

/**
* Given a list of structureChecks (directory path), return a list of TreeNodes representing the tree hierarchy
* of the structure checks.
*
* @param structureChecks
*/
public static getNodes(structureChecks: StructureCheck[] | undefined): TreeNode[] {
const nodes: TreeNode[] = [];

if (structureChecks !== undefined) {
for (const [i, check] of structureChecks.entries()) {
const hierarchy = check.getDirectoryHierarchy();
let currentNodes = nodes;

for (const [j, part] of hierarchy.entries()) {
let node = currentNodes.find((node) => node.label === part);

if (node === undefined) {
node = Project.newTreeNode(check, `${i}${j}`, part, j === hierarchy.length - 1);
currentNodes.push(node);
}

currentNodes = node.children ?? [];
}
}
}

return nodes;
}

/**
* Construct a tree node from a structure check folder path.
*
* @param check
* @param key
* @param label
* @param leaf
*/
private static newTreeNode(check: StructureCheck, key: string, label: string, leaf: boolean = false): TreeNode {
const node: TreeNode = {
key,
label,
data: check,
icon: PrimeIcons.FOLDER,
check: leaf,
children: [],
};

if (leaf) {
node.children = [
{
key: key + '-obligated',
icon: PrimeIcons.CHECK_CIRCLE,
data: check,
obligated: true,
},
{
key: key + '-blocked',
icon: PrimeIcons.TIMES_CIRCLE,
data: check,
blocked: true,
},
];
}

return node;
}

/**
* Convert a project object to a project instance.
*
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/views/projects/roles/StudentProjectView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ChooseGroupCard from '@/components/projects/groups/GroupChooseCard.vue';
import JoinedGroupCard from '@/components/projects/groups/GroupJoinedCard.vue';
import SubmissionCard from '@/components/submissions/SubmissionCard.vue';
import ProjectInfo from '@/components/projects/ProjectInfo.vue';
import ProjectStructure from '@/components/projects/ProjectStructure.vue';
import Title from '@/views/layout/Title.vue';
import Loading from '@/components/Loading.vue';
import { ref } from 'vue';
Expand All @@ -15,6 +16,8 @@ import { useSubmission } from '@/composables/services/submission.service.ts';
import { useProject } from '@/composables/services/project.service.ts';
import { useRoute } from 'vue-router';
import { type Project } from '@/types/Project.ts';
import { useStructureCheck } from '@/composables/services/structure_check.service.ts';
import Divider from 'primevue/divider';

/* Props */
defineProps<{
Expand All @@ -25,6 +28,7 @@ defineProps<{
const { params } = useRoute();
const { group, groups, getGroupByStudentProject, getGroupsByProject } = useGroup();
const { students, getStudentsByGroup, studentJoinGroup, studentLeaveGroup } = useStudents();
const { structureChecks, getStructureCheckByProject } = useStructureCheck();
const { project, getProjectByID } = useProject();
const { submissions, getSubmissionByGroup } = useSubmission();

Expand Down Expand Up @@ -90,6 +94,7 @@ async function getStudentData(project: Project): Promise<void> {
*/
async function getProjectData(project: Project): Promise<void> {
await getGroupsByProject(project.id);
await getStructureCheckByProject(project.id);

if (groups.value !== null) {
project.groups = groups.value;
Expand Down Expand Up @@ -124,6 +129,8 @@ watchImmediate(
<div class="col-12 md:col-8">
<ProjectInfo class="mb-3" :project="project" />
<div v-if="project" v-html="project.description" />
<Divider />
<ProjectStructure v-if="structureChecks" :structure-checks="structureChecks" />
</div>
<div class="col-12 md:col-4">
<template v-if="!loadingGroup">
Expand Down
20 changes: 15 additions & 5 deletions frontend/src/views/submissions/SubmissionsView.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
<script setup lang="ts">
import BaseLayout from '@/views/layout/base/BaseLayout.vue';
import Title from '@/views/layout/Title.vue';
import Button from 'primevue/button';
import FileUpload from 'primevue/fileupload';
import { PrimeIcons } from 'primevue/api';
import Loading from '@/components/Loading.vue';
import { useI18n } from 'vue-i18n';
import { onMounted, ref } from 'vue';
import { useProject } from '@/composables/services/project.service.ts';
import { useRoute } from 'vue-router';
import FileUpload from 'primevue/fileupload';
import { PrimeIcons } from 'primevue/api';
import AllSubmission from '@/components/submissions/AllSubmission.vue';
import ProjectStructure from '@/components/projects/ProjectStructure.vue';
import { useGroup } from '@/composables/services/group.service.ts';
import { useSubmission } from '@/composables/services/submission.service.ts';
import { useStructureCheck } from '@/composables/services/structure_check.service.ts';
import { useMessagesStore } from '@/store/messages.store.ts';
import BaseLayout from '@/views/layout/base/BaseLayout.vue';
import Title from '@/views/layout/Title.vue';

const { t } = useI18n();
const route = useRoute();
const { project, getProjectByID } = useProject();
const { group, getGroupByID } = useGroup();
const { submission, submissions, createSubmission, getSubmissionByGroup } = useSubmission();
const { structureChecks, getStructureCheckByProject } = useStructureCheck();
const { addSuccessMessage, addErrorMessage } = useMessagesStore();

/* State */
Expand All @@ -32,7 +35,7 @@ const files = ref<File[]>([]);
const onUpload = async (callback: () => void): Promise<void> => {
if (group.value !== null) {
try {
await createSubmission(files.value as File[], group.value.id);
await createSubmission(files.value as File[], group.value.id, false);
addSuccessMessage(t('toasts.messages.success'), t('toasts.messages.submissions.create.success'));

if (submission.value != null) {
Expand Down Expand Up @@ -72,6 +75,9 @@ onMounted(async () => {
await getProjectByID(route.params.projectId as string);
await getGroupByID(route.params.groupId as string);
await getSubmissionByGroup(route.params.groupId as string);
if (project.value !== null) {
await getStructureCheckByProject(project.value.id);
}
});
</script>

Expand All @@ -83,6 +89,10 @@ onMounted(async () => {
<div class="col-12 md:col-6">
<div class="flex-column">
<!-- Project info column -->
<!-- Submission structure -->
<div v-if="structureChecks">
<ProjectStructure :structure-checks="structureChecks" />
</div>
<!-- Submission upload -->
<div class="py-2">
<h2>{{ t('views.submissions.submit') }}</h2>
Expand Down