Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/development' into course-join-link
Browse files Browse the repository at this point in the history
  • Loading branch information
BramMeir committed May 1, 2024
2 parents 31e3ec0 + b66df40 commit de93c1d
Show file tree
Hide file tree
Showing 12 changed files with 479 additions and 82 deletions.
49 changes: 46 additions & 3 deletions backend/api/views/docker_view.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,66 @@
from api.models.docker import DockerImage
from api.permissions.docker_permissions import DockerPermission
from api.permissions.role_permissions import IsAssistant, IsTeacher
from api.serializers.docker_serializer import DockerImageSerializer
from rest_framework.permissions import IsAdminUser
from django.db.models import Q
from django.db.models.manager import BaseManager
from rest_framework.decorators import action
from rest_framework.mixins import (CreateModelMixin, DestroyModelMixin,
RetrieveModelMixin, UpdateModelMixin)
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet

from api.views.pagination.basic_pagination import BasicPagination


class DockerImageViewSet(RetrieveModelMixin, CreateModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet):

queryset = DockerImage.objects.all()
serializer_class = DockerImageSerializer
permission_classes = [DockerPermission]
permission_classes = [DockerPermission, IsAdminUser]

@action(detail=False)
def search(self, request: Request) -> Response:
self.pagination_class = BasicPagination

search = request.query_params.get("search", "")
identifier = request.query_params.get("id", "")
name = request.query_params.get("name", "")
owner = request.query_params.get("owner", "")

queryset1 = self.get_queryset().filter(
id__icontains=search
)
queryset2 = self.get_queryset().filter(
name__icontains=search
)
queryset3 = self.get_queryset().filter(
owner__id__icontains=search
)
queryset1 = queryset1.union(queryset2, queryset3)
queryset = self.get_queryset().filter(
id__icontains=identifier,
name__icontains=name,
owner__id__contains=owner
)
queryset = queryset.intersection(queryset1)

serializer = self.serializer_class(self.paginate_queryset(queryset), many=True, context={
"request": request
})

return self.get_paginated_response(serializer.data)

@action(detail=True, methods=['PATCH'], url_path='public', permission_classes=[IsAdminUser])
def patch_public(self, request: Request, **_) -> Response:
docker_image = self.get_object()
serializer = DockerImageSerializer(docker_image, data=request.data, partial=True, context={"request": request})

if serializer.is_valid():
serializer.save()

return Response(serializer.data)

# TODO: Maybe not necessary
# https://www.django-rest-framework.org/api-guide/permissions/#overview-of-access-restriction-methods
Expand Down
17 changes: 15 additions & 2 deletions frontend/src/assets/lang/app/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@
"admin": {
"title": "Admin",
"keyword": "Keyword",
"id": "ID",
"list": "List",
"add": "Add",
"search": {
"search": "Search",
"general": "Search by general keyword"
Expand All @@ -269,7 +272,6 @@
"save": "Save",
"users": {
"title": "Users",
"id": "ID",
"username": "Username",
"email": "Email",
"roles": "Roles"
Expand All @@ -286,7 +288,18 @@
"teachers": {
"title": "Teachers"
},
"teacher": "Teacher"
"teacher": "Teacher",
"catalog": "Catalog",
"docker_images": {
"title": "Docker Images",
"name_input": "Name of docker image",
"name": "Name",
"owner": "Owner ID",
"public": "Public",
"private": "Private"
},
"none_found": "No matching data.",
"loading": "Loading data. Please wait."
},
"primevue": {
"startsWith": "Starts with",
Expand Down
17 changes: 15 additions & 2 deletions frontend/src/assets/lang/app/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@
"admin": {
"title": "Beheerder",
"keyword": "Trefwoord",
"id": "ID",
"list": "Lijst",
"add": "Voeg toe",
"search": {
"search": "Zoeken",
"general": "Zoek op algemeen trefwoord"
Expand All @@ -267,7 +270,6 @@
"save": "Sla op",
"users": {
"title": "Gebruikers",
"id": "ID",
"username": "Gebruikersnaam",
"email": "E-mail",
"roles": "Functies"
Expand All @@ -284,7 +286,18 @@
"teachers": {
"title": "Proffen"
},
"teacher": "Prof"
"teacher": "Prof",
"catalog": "Catalogus",
"docker_images": {
"title": "Docker Images",
"name_input": "Naam van docker image",
"name": "Naam",
"owner": "Eigenaar ID",
"public": "Publiek",
"private": "Privaat"
},
"none_found": "Geen overeenkomende data gevonden.",
"loading": "Aan het laden. Wacht een momentje aub."
},
"primevue": {
"accept": "Ja",
Expand Down
110 changes: 110 additions & 0 deletions frontend/src/components/admin/LazyDataTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<script setup lang="ts">
import DataTable, { type DataTableSelectAllChangeEvent } from 'primevue/datatable';
import { onMounted, watch, ref, defineExpose, toRef } from 'vue';
import { useI18n } from 'vue-i18n';
import { type PaginatorResponse } from '@/types/filter/Paginator.ts';
import { usePaginator } from '@/composables/filters/paginator.ts';
import { type Filter } from '@/types/filter/Filter.ts';
import Column from 'primevue/column';
/* Properties */
const props = defineProps<{
pagination: PaginatorResponse<any> | null;
entities: any[] | null; // list containing all the entities displayed by data table after executing get method
get: () => Promise<void>; // get method for backend
search: (filters: Filter, page: number, pageSize: number) => Promise<void>;
filter: Filter;
onFilter: (callback: () => Promise<void>, debounce?: number | undefined, immediate?: boolean | undefined) => void;
}>();
/* Injections */
const { t } = useI18n();
const { page, first, pageSize, onPaginate, resetPagination } = usePaginator();
const loading = ref(false);
const selected = ref<any[] | null>(null);
const selectAll = ref(false);
onMounted(async () => {
onPaginate(fetch);
watch(
props.filter,
() => {
loading.value = true;
},
{ deep: true },
);
props.onFilter(fetch);
props.onFilter(
async () => {
await resetPagination([toRef(props.pagination)]);
},
0,
false,
);
});
const fetch = async (): Promise<void> => {
loading.value = true;
props.search(props.filter, page.value, pageSize.value).then(() => {
loading.value = false;
});
};
const onSelectAllChange = (event: DataTableSelectAllChangeEvent): void => {
selectAll.value = event.checked;
if (selectAll.value) {
props.get().then(() => {
selectAll.value = true;
selected.value = props.entities;
});
} else {
selectAll.value = false;
selected.value = [];
}
};
const onRowSelect = (): void => {
selectAll.value = selected.value?.length === (props.pagination?.count ?? 0);
};
const onRowUnselect = (): void => {
selectAll.value = false;
};
defineExpose({ fetch });
</script>

<template>
<div class="card p-fluid">
<DataTable
:value="pagination?.results"
lazy
paginator
v-model:first="first"
:rows="pageSize"
dataKey="id"
auto-layout
:totalRecords="pagination?.count"
:loading="loading"
@page="loading = true"
filterDisplay="row"
v-model:selection="selected"
:selectAll="selectAll"
@select-all-change="onSelectAllChange"
@row-select="onRowSelect"
@row-unselect="onRowUnselect"
tableStyle="min-width: 75rem"
>
<template #header>
<slot name="header" />
</template>
<template #empty>{{ t('admin.none_found') }}</template>
<template #loading>{{ t('admin.loading') }}</template>
<Column selectionMode="multiple" headerStyle="width: 3rem" class="justify-content-center"></Column>
<slot />
</DataTable>
</div>
</template>

<style scoped lang="scss"></style>
9 changes: 9 additions & 0 deletions frontend/src/components/layout/admin/AdminSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ const items = ref([
},
],
},
{
label: 'admin.catalog',
items: [
{
label: 'admin.docker_images.title',
route: 'admin-dockerImages',
},
],
},
]);
</script>

Expand Down
64 changes: 64 additions & 0 deletions frontend/src/composables/services/docker.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { DockerImage } from '@/types/DockerImage.ts';
import { Response } from '@/types/Response.ts';
import { endpoints } from '@/config/endpoints.ts';
import { type Ref, ref } from 'vue';
import { type Filter } from '@/types/filter/Filter.ts';
import { create, getList, getPaginatedList, patch } from '@/composables/services/helpers.ts';
import { type PaginatorResponse } from '@/types/filter/Paginator.ts';

interface DockerImagesState {
pagination: Ref<PaginatorResponse<DockerImage> | null>;
dockerImages: Ref<DockerImage[] | null>;
response: Ref<Response | null>;
getDockerImages: () => Promise<void>;
searchDockerImages: (filters: Filter, page: number, pageSize: number) => Promise<void>;
patchDockerImage: (dockerData: DockerImage) => Promise<void>;
createDockerImage: (dockerData: DockerImage, file: File) => Promise<void>;
}

export function useDockerImages(): DockerImagesState {
const pagination = ref<PaginatorResponse<DockerImage> | null>(null);
const dockerImages = ref<DockerImage[] | null>(null);
const response = ref<Response | null>(null);

async function getDockerImages(): Promise<void> {
const endpoint = endpoints.dockerImages.index;
await getList<DockerImage>(endpoint, dockerImages, DockerImage.fromJSON);
}

async function searchDockerImages(filters: Filter, page: number, pageSize: number): Promise<void> {
const endpoint = endpoints.dockerImages.search;
await getPaginatedList<DockerImage>(endpoint, filters, page, pageSize, pagination, DockerImage.fromJSON);
}

async function patchDockerImage(dockerData: DockerImage): Promise<void> {
const endpoint = endpoints.dockerImages.patch.replace('{id}', dockerData.id);
await patch(endpoint, { public: dockerData.public }, response);
}

async function createDockerImage(dockerData: DockerImage, file: File): Promise<void> {
const endpoint = endpoints.dockerImages.index;
await create<Response>(
endpoint,
{
file,
name: dockerData.name,
public: dockerData.public,
},
response,
Response.fromJSON,
'multipart/form-data',
);
}

return {
pagination,
dockerImages,
response,

getDockerImages,
searchDockerImages,
patchDockerImage,
createDockerImage,
};
}
5 changes: 5 additions & 0 deletions frontend/src/config/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export const endpoints = {
byTeacher: '/api/teachers/{teacherId}/courses/',
byAssistant: '/api/assistants/{assistantId}/courses/',
},
dockerImages: {
index: '/api/docker-images/',
search: '/api/docker-images/search/',
patch: '/api/docker-images/{id}/public/',
},
students: {
index: '/api/students/',
retrieve: '/api/students/{id}/',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import SubmissionView from '@/views/submissions/SubmissionView.vue';
import AdminView from '@/views/admin/AdminView.vue';
import UsersView from '@/views/admin/UsersView.vue';
import ProjectsView from '@/views/projects/ProjectsView.vue';
import DockerImagesView from '@/views/admin/DockerImagesView.vue';

const routes: RouteRecordRaw[] = [
// Authentication
Expand Down Expand Up @@ -151,6 +152,7 @@ const routes: RouteRecordRaw[] = [
children: [
{ path: '', component: AdminView, name: 'admin' },
{ path: 'users', component: UsersView, name: 'admin-users' },
{ path: 'docker-images', component: DockerImagesView, name: 'admin-dockerImages' },
],
},

Expand Down
20 changes: 20 additions & 0 deletions frontend/src/types/DockerImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export class DockerImage {
public public: boolean;
constructor(
public id: string,
public name: string,
public file: string, // in the form of a uri
public publicStatus: boolean,
public owner: string,
) {
this.public = publicStatus;
}

static fromJSON(dockerData: DockerImage): DockerImage {
return new DockerImage(dockerData.id, dockerData.name, dockerData.file, dockerData.public, dockerData.owner);
}

static blankDockerImage(): DockerImage {
return new DockerImage('', '', '', false, '');
}
}
Loading

0 comments on commit de93c1d

Please sign in to comment.