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

feat: Admin docker images #345

Merged
merged 19 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a776eb3
chore: added ability to upload docker images as an admin #334
bsilkyn Apr 18, 2024
ee700ef
chore: actually add the actual view of admin panel to add docker imag…
bsilkyn Apr 18, 2024
04d1d7d
chore: lint fixes #334
bsilkyn Apr 18, 2024
14b7ff9
chore: rename url path and name to remove underscores #334
bsilkyn Apr 18, 2024
c74171d
chore: update translations and add public option docker image add adm…
bsilkyn Apr 20, 2024
01b4c8a
chore: lint fix #334
bsilkyn Apr 20, 2024
4c9cca6
chore: start of a listing of docker images
bsilkyn Apr 20, 2024
2a36fe3
chore: add docker type #369 #334
bsilkyn Apr 25, 2024
f1e2552
chore: add search endpoint in frontend + abstract the lazy data table…
bsilkyn Apr 25, 2024
0917094
chore: fully fix abstract Lazy Paginator Data Table and adjust UsersV…
bsilkyn Apr 25, 2024
a28f92c
chore: frontend listing of docker images #369
bsilkyn Apr 25, 2024
1bf2aee
chore: backend search implementation (filter not fully functional) + …
bsilkyn Apr 25, 2024
78c50d1
chore: try adding edit of public status (right now non-functional) #369
bsilkyn Apr 28, 2024
e5a91b9
chore: docker images edit button
bsilkyn Apr 29, 2024
8194973
chore: filters fixed + public status editing fixed #369
bsilkyn Apr 29, 2024
dcbee3d
chore: lint fixes + filter background fix #369
bsilkyn Apr 29, 2024
55cae76
chore: lint fixes fix #369
bsilkyn Apr 29, 2024
8e36587
chore: add translations
bsilkyn Apr 30, 2024
db3a38d
chore: fix wrong endpoint (sorryyyyy; well spotted though)
bsilkyn Apr 30, 2024
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
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 @@ -231,6 +231,9 @@
"admin": {
"title": "Admin",
"keyword": "Keyword",
"id": "ID",
"list": "List",
"add": "Add",
"search": {
"search": "Search",
"general": "Search by general keyword"
Expand All @@ -240,7 +243,6 @@
"save": "Save",
"users": {
"title": "Users",
"id": "ID",
"username": "Username",
"email": "Email",
"roles": "Roles"
Expand All @@ -257,7 +259,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 @@ -231,6 +231,9 @@
"admin": {
"title": "Beheerder",
"keyword": "Trefwoord",
"id": "ID",
"list": "Lijst",
"add": "Voeg toe",
"search": {
"search": "Zoeken",
"general": "Zoek op algemeen trefwoord"
Expand All @@ -240,7 +243,6 @@
"save": "Sla op",
"users": {
"title": "Gebruikers",
"id": "ID",
"username": "Gebruikersnaam",
"email": "E-mail",
"roles": "Functies"
Expand All @@ -257,7 +259,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 @@ -18,6 +18,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