Skip to content

Commit

Permalink
Merge branch 'dev' into submissions_teacher_testen
Browse files Browse the repository at this point in the history
# Conflicts:
#	frontend/src/i18n/locales/en.ts
#	frontend/src/i18n/locales/nl.ts
#	frontend/src/views/CreateProjectView.vue
#	frontend/tests/components/submission/SubmissionCard.spec.ts
  • Loading branch information
masinnae committed May 21, 2024
2 parents aa418bf + 49bfb36 commit b9422cc
Show file tree
Hide file tree
Showing 26 changed files with 852 additions and 215 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ Instructions for the backend are located [here](backend/README.md).

Automated clients can interact with the web application via the [API](https://sel2-5.ugent.be/api/docs).

## Used tools and frameworks

### Database
- Database system: [PostgreSQL](https://www.postgresql.org/)
- Database migrations: [alembic](https://github.com/sqlalchemy/alembic).

### Backend
- Backend framework: [FastAPI](https://fastapi.tiangolo.com/)
- Database interface: [SQLAlchemy](https://www.sqlalchemy.org/)
- JSON-validation: [Pydantic](https://github.com/pydantic/pydantic)
- Test framework: [pytest](https://github.com/pytest-dev/pytest)

### Frontend
- Frontend framework: [Vue.js](https://vuejs.org/) (Composition API) + [TypeScript](https://www.typescriptlang.org/)
- Component library: [Vuetify](https://dev.vuetifyjs.com/en/)
- Test framework: [Vitest](https://vitest.dev/)

## The team

| | |
Expand Down
2 changes: 1 addition & 1 deletion backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

```sh
# Create a python virtual environment
python -m venv venv
python3.12 -m venv venv
# Activate the environment
source venv/bin/activate
# Install dependencies
Expand Down
2 changes: 0 additions & 2 deletions backend/src/project/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ async def retrieve_project(project_id: int,


async def retrieve_test_files_uuid(project: Project = Depends(retrieve_project)):
if project.test_files_uuid is None:
raise TestsNotFound
return project.test_files_uuid


Expand Down
9 changes: 6 additions & 3 deletions backend/src/project/router.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Sequence, List
from typing import Sequence, List, Optional

from docker import DockerClient
from fastapi import APIRouter, Depends, UploadFile, BackgroundTasks
Expand Down Expand Up @@ -96,7 +96,9 @@ async def get_submissions_dump(project_id: int, db: AsyncSession = Depends(get_a


@router.get("/{project_id}/test_files")
async def get_test_files(test_files_uuid: str = Depends(retrieve_test_files_uuid)):
async def get_test_files(test_files_uuid: Optional[str] = Depends(retrieve_test_files_uuid)):
if not test_files_uuid:
return []
return get_files_from_dir(tests_path(test_files_uuid))


Expand All @@ -116,7 +118,8 @@ async def put_test_files(

if not using_default_docker_image(uuid):
# build custom docker image if dockerfile is present
background_tasks.add_task(build_docker_image, tests_path(uuid), uuid, client)
background_tasks.add_task(
build_docker_image, tests_path(uuid), uuid, client)

return await update_test_files(db, project.id, uuid)

Expand Down
5 changes: 4 additions & 1 deletion backend/src/project/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class ProjectBase(BaseModel):
is_visible: bool = Field(default=True)
capacity: int = Field(gt=0)
requirements: List[Requirement] = []
enroll_deadline: Optional[datetime]
publish_date: datetime


class ProjectCreate(ProjectBase):
Expand Down Expand Up @@ -55,7 +57,8 @@ class ProjectUpdate(BaseModel):
deadline: Optional[datetime] = None
description: Optional[str] = None
requirements: Optional[List[Requirement]] = None
is_visible: Optional[bool] = None
enroll_deadline: Optional[datetime] = None
publish_date: Optional[datetime] = None

@field_validator("deadline")
def validate_deadline(cls, value: datetime) -> datetime:
Expand Down
9 changes: 6 additions & 3 deletions backend/src/project/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ async def create_project(db: AsyncSession, project_in: ProjectCreate) -> Project
description=project_in.description,
is_visible=project_in.is_visible,
capacity=project_in.capacity,
requirements=[Requirement(**r.model_dump()) for r in project_in.requirements],
requirements=[Requirement(**r.model_dump())
for r in project_in.requirements],
)
db.add(new_project)
await db.commit()
Expand Down Expand Up @@ -77,10 +78,12 @@ async def update_project(
project.name = project_update.name
if project_update.deadline is not None:
project.deadline = project_update.deadline
if project_update.publish_date is not None:
project.publish_date = project_update.publish_date
if project_update.enroll_deadline is not None:
project.enroll_deadline = project.enroll_deadline
if project_update.description is not None:
project.description = project_update.description
if project_update.is_visible is not None:
project.is_visible = project_update.is_visible
if project_update.requirements is not None:
await delete_requirements_for_project(db, project_id)
project.requirements = [Requirement(**r.model_dump())
Expand Down
3 changes: 2 additions & 1 deletion backend/src/subject/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ async def is_instructor(db: AsyncSession, subject_id: int, uid: str) -> bool:


async def create_subject(db: AsyncSession, subject: SubjectCreate) -> Subject:
db_subject = Subject(name=subject.name, academic_year=subject.academic_year)
db_subject = Subject(
name=subject.name, academic_year=subject.academic_year)
db.add(db_subject)
await db.commit()
await db.refresh(db_subject)
Expand Down
3 changes: 2 additions & 1 deletion backend/src/user/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
retrieve_subjects,
retrieve_user,
admin_user_validation,
teacher_or_admin_user_validation,
)
from .schemas import User, UserSimple, UserSubjectList
from .service import set_admin, set_teacher, get_all_users
Expand All @@ -25,7 +26,7 @@
)


@router.get("/", dependencies=[Depends(admin_user_validation)])
@router.get("/", dependencies=[Depends(teacher_or_admin_user_validation)])
async def get_users(
db: AsyncSession = Depends(get_async_db),
) -> list[User]:
Expand Down
1 change: 1 addition & 0 deletions backend/tests/test_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"capacity": 1,
"requirements": [{"mandatory": "true", "value": "*.py"}],
"test_files": [],
"publish_date": future_date.strftime("%Y-%m-%dT%H:%M:%SZ"),
}

group_data = {"team_name": "test group", "project_id": 0}
Expand Down
27 changes: 1 addition & 26 deletions backend/tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
"deadline": future_date.strftime("%Y-%m-%dT%H:%M:%SZ"),
"description": "test",
"enroll_deadline": future_date.strftime("%Y-%m-%dT%H:%M:%SZ"),
"is_visible": True,
"capacity": 1,
"requirements": [{"mandatory": "false", "value": "*.pdf"}],
"publish_date": future_date.strftime("%Y-%m-%dT%H:%M:%SZ"),
}


Expand Down Expand Up @@ -111,28 +111,3 @@ async def test_patch_project(client: AsyncClient, db: AsyncSession, project_id:
assert response.status_code == 200
response = await client.get(f"/api/projects/{project_id}")
assert response.json()["description"] == "new description"


@pytest.mark.asyncio
async def test_is_visible_project(client: AsyncClient, db: AsyncSession, project_id: int):

response = await client.get(f"/api/projects/{project_id}")
subject_id = response.json()["subject_id"]

await set_admin(db, "test", True)
await client.patch(f"/api/projects/{project_id}", json={"is_visible": False})
await set_admin(db, "test", False)

response = await client.get(f"/api/projects/{project_id}")
assert response.status_code == 404 # Not found as project is not visible

response = await client.get(f"/api/subjects/{subject_id}/projects")
assert len(response.json()["projects"]) == 0

# Now privileged get request
await make_instructor(subject_id, "test", db, client)
response = await client.get(f"/api/projects/{project_id}")
assert response.status_code == 200

response = await client.get(f"/api/subjects/{subject_id}/projects")
assert len(response.json()["projects"]) == 1
1 change: 1 addition & 0 deletions backend/tests/test_submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"is_visible": True,
"capacity": 1,
"requirements": [{"mandatory": "true", "value": "*.py"}, {"mandatory": "false", "value": "*.pdf"}],
"publish_date": future_date.strftime("%Y-%m-%dT%H:%M:%SZ"),
}

group_data = {"team_name": "test group", "project_id": 0}
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/components/form_elements/FilesInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,18 @@ function onDeleteClick(index: number) {
.files {
margin-top: 15px;
}
.custom-alert .alert-text {
white-space: nowrap; /* Prevents the text from wrapping */
overflow: hidden; /* Prevents overflow of text outside the alert box */
text-overflow: ellipsis; /* Adds an ellipsis if the text overflows */
}
.custom-alert a {
display: inline; /* Ensures the link is in line with other text */
white-space: normal; /* Allows normal wrapping inside the link if needed */
}
.custom-alert {
margin-bottom: 15px; /* Added spacing between the alert and the button */
}
</style>
56 changes: 46 additions & 10 deletions frontend/src/components/project/DatePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,66 @@
></v-text-field>
</template>
<v-date-picker v-model="date" no-title></v-date-picker>
<v-time-picker v-model="time" format="24hr"></v-time-picker>
</v-menu>
</template>

<script setup lang="ts">
import { ref, computed } from "vue";
import { ref, watch, computed, defineEmits } from "vue";
import { VTimePicker } from "vuetify/labs/VTimePicker";
const date = defineModel<Date>({});
defineProps<{
// Define props and emits
const props = defineProps<{
modelValue: Date; // This expects a JavaScript Date object
label: string;
}>();
const emit = defineEmits(["update:modelValue"]);
const menuVisible = ref<boolean>(false);
const date = ref<Date>(new Date(props.modelValue || Date.now())); // Initialize with current date or modelValue
const time = ref<string>(formatTime(props.modelValue || new Date())); // Initialize with current time or modelValue
// Watcher to sync changes in modelValue to date and time pickers
watch(
() => props.modelValue,
(newValue, oldValue) => {
if (newValue && newValue !== oldValue) {
date.value = new Date(newValue); // Update the date picker
time.value = formatTime(new Date(newValue)); // Update the time picker
}
},
{ immediate: true, deep: true }
);
// Format time into a string
function formatTime(date: Date): string {
return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`;
}
const menuVisible = ref(false);
// Watcher to emit updates when date or time changes
watch(
[date, time],
() => {
const [hours, minutes] = time.value.split(":").map(Number);
const updatedDate = new Date(date.value);
updatedDate.setHours(hours, minutes, 0, 0); // Apply time changes to the date
emit("update:modelValue", new Date(updatedDate)); // Emit the updated Date object
},
{ deep: true }
);
// Computed to format the date as ISO string (just the date part)
// Computed property to display date and time in text field
const displayDate = computed(() => {
if (date.value) {
const selectedDate = new Date(date.value.getTime());
selectedDate.setMinutes(selectedDate.getMinutes() - selectedDate.getTimezoneOffset());
return selectedDate.toISOString().substring(0, 10);
if (date.value && time.value) {
const selectedDate = new Date(date.value);
const [hours, minutes] = time.value.split(":").map(Number);
selectedDate.setHours(hours, minutes);
return `${selectedDate.getFullYear()}-${(selectedDate.getMonth() + 1).toString().padStart(2, "0")}-${selectedDate.getDate().toString().padStart(2, "0")} ${selectedDate.getHours().toString().padStart(2, "0")}:${selectedDate.getMinutes().toString().padStart(2, "0")}`;
}
return "";
});
// Toggle visibility of date/time picker
function toggleDatePicker() {
menuVisible.value = !menuVisible.value;
}
Expand Down
91 changes: 91 additions & 0 deletions frontend/src/components/project/DisplayTestFiles.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<template>
<v-card class="file-display-container" outlined>
<v-card-title>{{ $t("project.testfiles") }}</v-card-title>
<v-card-text>
<v-treeview v-model="tree" :items="treeItems" activatable hoverable open-on-click>
<template v-slot:prepend="{ item }">
<v-icon>{{ getFileIcon(item.title) }}</v-icon>
</template>
</v-treeview>
</v-card-text>
</v-card>
</template>

<script setup>
import { computed, ref } from "vue";
import { VTreeview } from "vuetify/labs/VTreeview";
const props = defineProps({
files: Array,
});
const tree = ref([]);
const treeItems = computed(() => {
const rootNode = {};
props.files.forEach((file) => {
const parts = file.filename.split("/").filter(Boolean);
let current = rootNode;
parts.forEach((part, index) => {
if (!current[part]) {
current[part] = {
_isFile: index === parts.length - 1,
children: {},
title: part,
};
}
if (index === parts.length - 1) {
current[part]._isFile = true;
current[part].title = parts.slice(-1)[0];
current[part].id = file.path;
current[part].children = undefined;
} else {
current = current[part].children;
}
});
});
return buildTree(rootNode);
});
function buildTree(node, path = "") {
const result = [];
Object.keys(node).forEach((key) => {
if (!node[key]._isFile && node[key].children) {
const fullPath = path ? `${path}/${key}` : key;
result.push({
title: node[key].title,
id: fullPath,
children: buildTree(node[key].children, fullPath),
});
} else if (node[key]._isFile) {
result.push({
title: node[key].title,
id: node[key].id,
});
}
});
return result;
}
const icons = ref({
html: "mdi-language-html5",
js: "mdi-nodejs",
json: "mdi-code-json",
md: "mdi-language-markdown",
pdf: "mdi-file-pdf-box",
png: "mdi-file-image",
txt: "mdi-file-document-outline",
xls: "mdi-file-excel",
folder: "mdi-folder",
folderOpen: "mdi-folder-open",
});
function getFileIcon(filename) {
const extension = filename.split(".").pop();
if (extension === filename) {
return "mdi-folder";
} else {
return icons.value[extension] || "mdi-file";
}
}
</script>
2 changes: 1 addition & 1 deletion frontend/src/components/project/ProjectSideBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
class="group-button"
:email="subject!.email"
></NeedHelpButton>
<router-link v-if="isTeacher" :to="`/projects/${project!.id}/edit`">
<router-link v-if="isTeacher" :to="`/project/${project!.id}/edit`">
<v-btn class="group-button" prepend-icon="mdi-pencil">
{{ $t("project.edit") }}
</v-btn>
Expand Down
Loading

0 comments on commit b9422cc

Please sign in to comment.