diff --git a/backend/api/migrations/0016_course_excerpt_alter_checkresult_submission_and_more.py b/backend/api/migrations/0016_course_excerpt_alter_checkresult_submission_and_more.py
new file mode 100644
index 00000000..e9f327a1
--- /dev/null
+++ b/backend/api/migrations/0016_course_excerpt_alter_checkresult_submission_and_more.py
@@ -0,0 +1,35 @@
+# Generated by Django 5.0.4 on 2024-04-15 19:09
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0015_checkresult_remove_extrachecksresult_error_message_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='course',
+ name='excerpt',
+ field=models.CharField(default='no excerpt provided', max_length=200),
+ preserve_default=False,
+ ),
+ migrations.AlterField(
+ model_name='checkresult',
+ name='submission',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='api.submission'),
+ ),
+ migrations.AlterField(
+ model_name='extracheckresult',
+ name='extra_check',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extra_check', to='api.extracheck'),
+ ),
+ migrations.AlterField(
+ model_name='structurecheckresult',
+ name='structure_check',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='structure_check', to='api.structurecheck'),
+ ),
+ ]
diff --git a/backend/api/migrations/0017_merge_20240416_1054.py b/backend/api/migrations/0017_merge_20240416_1054.py
index 2a7a22d8..12753b02 100644
--- a/backend/api/migrations/0017_merge_20240416_1054.py
+++ b/backend/api/migrations/0017_merge_20240416_1054.py
@@ -7,6 +7,7 @@ class Migration(migrations.Migration):
dependencies = [
('api', '0016_alter_checkresult_submission_and_more'),
+ ('api', '0016_course_excerpt_alter_checkresult_submission_and_more'),
('api', '0016_remove_checkresult_is_valid_submission_is_valid_and_more'),
]
diff --git a/backend/api/models/course.py b/backend/api/models/course.py
index 36f42a30..10a41d9c 100644
--- a/backend/api/models/course.py
+++ b/backend/api/models/course.py
@@ -16,6 +16,9 @@ class Course(models.Model):
# Begin year of the academic year
academic_startyear = models.IntegerField(blank=False, null=False)
+ # The excerpt of the course
+ excerpt = models.CharField(max_length=200, blank=False, null=False)
+
# The description of the course
description = models.TextField(blank=True, null=True)
diff --git a/backend/api/serializers/course_serializer.py b/backend/api/serializers/course_serializer.py
index 5ed47e72..0345239b 100644
--- a/backend/api/serializers/course_serializer.py
+++ b/backend/api/serializers/course_serializer.py
@@ -1,3 +1,4 @@
+from nh3 import clean
from django.utils.translation import gettext
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
@@ -37,6 +38,11 @@ class CourseSerializer(serializers.ModelSerializer):
read_only=True
)
+ def validate(self, attrs: dict) -> dict:
+ """Extra custom validation for course serializer"""
+ attrs['description'] = clean(attrs['description'])
+ return attrs
+
class Meta:
model = Course
fields = "__all__"
diff --git a/backend/api/serializers/project_serializer.py b/backend/api/serializers/project_serializer.py
index 1d299a5b..e8c80cf2 100644
--- a/backend/api/serializers/project_serializer.py
+++ b/backend/api/serializers/project_serializer.py
@@ -1,4 +1,5 @@
from api.logic.check_folder_structure import parse_zip_file
+from nh3 import clean
from api.models.checks import FileExtension
from api.models.course import Course
from api.models.group import Group
@@ -64,6 +65,8 @@ def validate(self, data):
if "deadline" in data and data["deadline"] < start_date:
raise ValidationError(gettext("project.errors.deadline_before_start_date"))
+ data['description'] = clean(data['description'])
+
return data
class Meta:
diff --git a/backend/api/tests/test_course.py b/backend/api/tests/test_course.py
index e845de44..7133af5a 100644
--- a/backend/api/tests/test_course.py
+++ b/backend/api/tests/test_course.py
@@ -782,6 +782,7 @@ def test_create_course(self):
data={
"name": "Introduction to Computer Science",
"academic_startyear": 2022,
+ "excerpt": "Excerpt",
"description": "An introductory course on computer science.",
"faculty": faculty.id,
},
diff --git a/backend/poetry.lock b/backend/poetry.lock
index 62ade4a7..81c2ddc8 100644
--- a/backend/poetry.lock
+++ b/backend/poetry.lock
@@ -815,6 +815,31 @@ files = [
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
+[[package]]
+name = "nh3"
+version = "0.2.17"
+description = "Python bindings to the ammonia HTML sanitization library."
+optional = false
+python-versions = "*"
+files = [
+ {file = "nh3-0.2.17-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:551672fd71d06cd828e282abdb810d1be24e1abb7ae2543a8fa36a71c1006fe9"},
+ {file = "nh3-0.2.17-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c551eb2a3876e8ff2ac63dff1585236ed5dfec5ffd82216a7a174f7c5082a78a"},
+ {file = "nh3-0.2.17-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:66f17d78826096291bd264f260213d2b3905e3c7fae6dfc5337d49429f1dc9f3"},
+ {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0316c25b76289cf23be6b66c77d3608a4fdf537b35426280032f432f14291b9a"},
+ {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:22c26e20acbb253a5bdd33d432a326d18508a910e4dcf9a3316179860d53345a"},
+ {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:85cdbcca8ef10733bd31f931956f7fbb85145a4d11ab9e6742bbf44d88b7e351"},
+ {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40015514022af31975c0b3bca4014634fa13cb5dc4dbcbc00570acc781316dcc"},
+ {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba73a2f8d3a1b966e9cdba7b211779ad8a2561d2dba9674b8a19ed817923f65f"},
+ {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c21bac1a7245cbd88c0b0e4a420221b7bfa838a2814ee5bb924e9c2f10a1120b"},
+ {file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d7a25fd8c86657f5d9d576268e3b3767c5cd4f42867c9383618be8517f0f022a"},
+ {file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c790769152308421283679a142dbdb3d1c46c79c823008ecea8e8141db1a2062"},
+ {file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:b4427ef0d2dfdec10b641ed0bdaf17957eb625b2ec0ea9329b3d28806c153d71"},
+ {file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a3f55fabe29164ba6026b5ad5c3151c314d136fd67415a17660b4aaddacf1b10"},
+ {file = "nh3-0.2.17-cp37-abi3-win32.whl", hash = "sha256:1a814dd7bba1cb0aba5bcb9bebcc88fd801b63e21e2450ae6c52d3b3336bc911"},
+ {file = "nh3-0.2.17-cp37-abi3-win_amd64.whl", hash = "sha256:1aa52a7def528297f256de0844e8dd680ee279e79583c76d6fa73a978186ddfb"},
+ {file = "nh3-0.2.17.tar.gz", hash = "sha256:40d0741a19c3d645e54efba71cb0d8c475b59135c1e3c580f879ad5514cbf028"},
+]
+
[[package]]
name = "openapi-codec"
version = "1.3.2"
@@ -1439,13 +1464,13 @@ files = [
[[package]]
name = "virtualenv"
-version = "20.25.1"
+version = "20.25.2"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.7"
files = [
- {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"},
- {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"},
+ {file = "virtualenv-20.25.2-py3-none-any.whl", hash = "sha256:6e1281a57849c8a54da89ba82e5eb7c8937b9d057ff01aaf5bc9afaa3552e90f"},
+ {file = "virtualenv-20.25.2.tar.gz", hash = "sha256:fa7edb8428620518010928242ec17aa7132ae435319c29c1651d1cf4c4173aad"},
]
[package.dependencies]
@@ -1454,7 +1479,7 @@ filelock = ">=3.12.2,<4"
platformdirs = ">=3.9.1,<5"
[package.extras]
-docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
[[package]]
@@ -1485,4 +1510,4 @@ brotli = ["Brotli"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11.4"
-content-hash = "c64b4d21616b08629ec50d1a033dda85bd46b060c0bf592c0f0521d0c83de9eb"
+content-hash = "30432bf7421f8d6b3cb6149e45d72e52ee7387927d818db83d52bbd71647fc90"
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 0ad43927..77200ab1 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -29,6 +29,7 @@ django-seed = "^0.3.1"
django-celery-results = "^2.5.1"
django-polymorphic = "^3.1.0"
django-rest-polymorphic = "^0.1.10"
+nh3 = "^0.2.17"
[build-system]
diff --git a/backend/setup.sh b/backend/setup.sh
index 9271d2e8..b6fdc270 100755
--- a/backend/setup.sh
+++ b/backend/setup.sh
@@ -4,7 +4,6 @@ poetry install > /dev/null
echo "Migrating database..."
python manage.py migrate > /dev/null
-python manage.py migrate django_celery_results > /dev/null
echo "Compiling translations..."
django-admin compilemessages > /dev/null
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 332f22c7..7661561c 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -19,6 +19,7 @@
"primeflex": "^3.3.1",
"primeicons": "^7.0.0",
"primevue": "^3.50.0",
+ "quill": "^1.3.7",
"vue": "^3.4.18",
"vue-i18n": "^9.10.2",
"vue-router": "^4.3.0",
@@ -2354,7 +2355,6 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
- "dev": true,
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
@@ -2584,6 +2584,14 @@
"node": ">=12"
}
},
+ "node_modules/clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2890,6 +2898,25 @@
"node": ">=6"
}
},
+ "node_modules/deep-equal": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz",
+ "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==",
+ "dependencies": {
+ "is-arguments": "^1.1.1",
+ "is-date-object": "^1.0.5",
+ "is-regex": "^1.1.4",
+ "object-is": "^1.1.5",
+ "object-keys": "^1.1.1",
+ "regexp.prototype.flags": "^1.5.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -2900,7 +2927,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
- "dev": true,
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
@@ -2917,7 +2943,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
- "dev": true,
"dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
@@ -3083,7 +3108,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
- "dev": true,
"dependencies": {
"get-intrinsic": "^1.2.4"
},
@@ -3095,7 +3119,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "dev": true,
"engines": {
"node": ">= 0.4"
}
@@ -3727,6 +3750,11 @@
"integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==",
"dev": true
},
+ "node_modules/eventemitter3": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
+ "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg=="
+ },
"node_modules/execa": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
@@ -3765,8 +3793,7 @@
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
- "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
- "dev": true
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"node_modules/extract-zip": {
"version": "2.0.1",
@@ -4038,7 +4065,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -4065,7 +4091,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
- "dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -4091,7 +4116,6 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
- "dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
@@ -4291,7 +4315,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
- "dev": true,
"dependencies": {
"get-intrinsic": "^1.1.3"
},
@@ -4340,7 +4363,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
- "dev": true,
"dependencies": {
"es-define-property": "^1.0.0"
},
@@ -4352,7 +4374,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
- "dev": true,
"engines": {
"node": ">= 0.4"
},
@@ -4364,7 +4385,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
- "dev": true,
"engines": {
"node": ">= 0.4"
},
@@ -4376,7 +4396,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "dev": true,
"dependencies": {
"has-symbols": "^1.0.3"
},
@@ -4391,7 +4410,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -4594,6 +4612,21 @@
"node": ">= 0.4"
}
},
+ "node_modules/is-arguments": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
+ "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
@@ -4720,7 +4753,6 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
"integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
- "dev": true,
"dependencies": {
"has-tostringtag": "^1.0.0"
},
@@ -4836,7 +4868,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
"integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
- "dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
@@ -5572,11 +5603,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/object-is": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
+ "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
- "dev": true,
"engines": {
"node": ">= 0.4"
}
@@ -5745,6 +5790,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/parchment": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
+ "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg=="
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -6158,6 +6208,37 @@
}
]
},
+ "node_modules/quill": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
+ "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
+ "dependencies": {
+ "clone": "^2.1.1",
+ "deep-equal": "^1.0.1",
+ "eventemitter3": "^2.0.3",
+ "extend": "^3.0.2",
+ "parchment": "^1.1.4",
+ "quill-delta": "^3.6.2"
+ }
+ },
+ "node_modules/quill-delta": {
+ "version": "3.6.3",
+ "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
+ "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
+ "dependencies": {
+ "deep-equal": "^1.0.1",
+ "extend": "^3.0.2",
+ "fast-diff": "1.1.2"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/quill-delta/node_modules/fast-diff": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
+ "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig=="
+ },
"node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
@@ -6180,7 +6261,6 @@
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
"integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
- "dev": true,
"dependencies": {
"call-bind": "^1.0.6",
"define-properties": "^1.2.1",
@@ -6477,7 +6557,6 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
- "dev": true,
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
@@ -6494,7 +6573,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
"integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
- "dev": true,
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
diff --git a/frontend/package.json b/frontend/package.json
index 7e95e033..9acb039f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -6,7 +6,7 @@
"scripts": {
"dev": "vite",
"host": "vite --host",
- "build": "vue-tsc && vite build",
+ "build": "vite build",
"preview": "vite preview",
"test": "vitest run",
"cypress:open": "cypress open",
@@ -26,6 +26,7 @@
"primeflex": "^3.3.1",
"primeicons": "^7.0.0",
"primevue": "^3.50.0",
+ "quill": "^1.3.7",
"vue": "^3.4.18",
"vue-i18n": "^9.10.2",
"vue-router": "^4.3.0",
diff --git a/frontend/src/assets/lang/en.json b/frontend/src/assets/lang/en.json
index 14be5435..fc809884 100644
--- a/frontend/src/assets/lang/en.json
+++ b/frontend/src/assets/lang/en.json
@@ -23,8 +23,8 @@
"dashboard": {
"courses": "My courses",
"projects": "Current projects",
- "no_projects": "No projects available for this academic year.",
- "no_courses": "No courses available for this academic year.",
+ "noProjects": "No projects available for this academic year.",
+ "noCourses": "No courses available for this academic year.",
"select_course": "Select the course for which you want to create a project:",
"showPastProjects": "Projects with passed deadline"
},
@@ -43,8 +43,12 @@
"noProjects": "No projects on selected date."
},
"projects": {
+ "all": "All projects",
+ "coming": "Near deadlines",
"deadline": "Deadline",
- "submissionStatus": "Indienstatus",
+ "days": "Today at {hour} | Tomorrow at {hour} | In {count} days",
+ "start": "Start date",
+ "submissionStatus": "Submission status",
"group": "Group",
"groupMembers": "Group members",
"chooseGroup": "Choose a group",
diff --git a/frontend/src/assets/lang/nl.json b/frontend/src/assets/lang/nl.json
index 029522ae..96a40f0a 100644
--- a/frontend/src/assets/lang/nl.json
+++ b/frontend/src/assets/lang/nl.json
@@ -23,8 +23,8 @@
"dashboard": {
"courses": "Mijn vakken",
"projects": "Lopende projecten",
- "no_projects": "Geen projecten beschikbaar voor dit academiejaar.",
- "no_courses": "Geen vakken beschikbaar voor dit academiejaar.",
+ "noProjects": "Geen projecten beschikbaar voor dit academiejaar.",
+ "noCourses": "Geen vakken beschikbaar voor dit academiejaar.",
"select_course": "Selecteer het vak waarvoor je een project wil maken:",
"showPastProjects": "Projecten met verstreken deadline"
},
@@ -43,7 +43,11 @@
"noProjects": "Geen projecten op geselecteerde datum."
},
"projects": {
+ "all": "Alle projecten",
+ "coming": "Aankomende deadlines",
"deadline": "Deadline",
+ "days": "Vandaag om {hour} | Morgen om {hour} | Over {count} dagen",
+ "start": "Startdatum",
"submissionStatus": "Indienstatus",
"group": "Groep",
"groupMembers": "Groepsleden",
@@ -75,6 +79,7 @@
"clone_teachers": "Kloon lesgevers:",
"name": "Vaknaam",
"description": "Beschrijving",
+ "excerpt": "Korte beschrijving",
"faculty": "Faculteit",
"year": "Academiejaar",
"enroll": "Inschrijven",
diff --git a/frontend/src/assets/scss/theme/base/components/misc/_progressbar.scss b/frontend/src/assets/scss/theme/base/components/misc/_progressbar.scss
index 22971661..dc8bad0b 100644
--- a/frontend/src/assets/scss/theme/base/components/misc/_progressbar.scss
+++ b/frontend/src/assets/scss/theme/base/components/misc/_progressbar.scss
@@ -2,13 +2,13 @@
.p-progressbar {
position: relative;
overflow: hidden;
+ display: flex;
}
.p-progressbar-determinate .p-progressbar-value {
height: 100%;
- width: 0%;
+ width: 0;
position: absolute;
- display: none;
border: 0 none;
display: flex;
align-items: center;
diff --git a/frontend/src/components/courses/CourseGeneralCard.vue b/frontend/src/components/courses/CourseGeneralCard.vue
index 77b09fc4..77b89233 100644
--- a/frontend/src/components/courses/CourseGeneralCard.vue
+++ b/frontend/src/components/courses/CourseGeneralCard.vue
@@ -9,8 +9,12 @@ import { useGlob } from '@/composables/glob.ts';
/* Props */
defineProps<{
course: Course;
+ courses: Course[];
}>();
+/* Emits */
+const emit = defineEmits(['update:courses']);
+
/* Composable injections */
const { user } = storeToRefs(useAuthStore());
@@ -36,7 +40,12 @@ const { getImport } = useGlob(import.meta.glob('@/assets/img/faculties/*.png', {
-
+
diff --git a/frontend/src/components/courses/CourseList.vue b/frontend/src/components/courses/CourseList.vue
index b233528d..6efce56e 100644
--- a/frontend/src/components/courses/CourseList.vue
+++ b/frontend/src/components/courses/CourseList.vue
@@ -6,6 +6,10 @@ import { type Course } from '@/types/Course.ts';
import { useI18n } from 'vue-i18n';
import { PrimeIcons } from 'primevue/api';
import Button from 'primevue/button';
+import { useCourses } from '@/composables/services/course.service.ts';
+import { storeToRefs } from 'pinia';
+import { useAuthStore } from '@/store/authentication.store.ts';
+import { watch } from 'vue';
/* Props */
interface Props {
@@ -20,7 +24,29 @@ withDefaults(defineProps(), {
});
/* Composable injections */
+const courseService = useCourses();
const { t } = useI18n();
+const { user } = storeToRefs(useAuthStore());
+
+/* State */
+const userCourses = courseService.courses;
+
+/**
+ * Load the courses based on the user role.
+ */
+async function loadCourses(): Promise {
+ if (user.value !== null) {
+ if (user.value.isStudent()) {
+ await courseService.getCoursesByStudent(user.value.id);
+ } else if (user.value.isAssistant()) {
+ await courseService.getCourseByAssistant(user.value.id);
+ } else if (user.value.isTeacher()) {
+ await courseService.getCoursesByTeacher(user.value.id);
+ }
+ }
+}
+
+watch(user, loadCourses, { immediate: true });
@@ -42,12 +68,18 @@ const { t } = useI18n();
-
+
-
{{ t('views.dashboard.no_courses') }}
+
{{ t('views.dashboard.noCourses') }}
diff --git a/frontend/src/components/courses/students/StudentCourseJoinButton.vue b/frontend/src/components/courses/students/StudentCourseJoinButton.vue
index 34c6ed45..34da3478 100644
--- a/frontend/src/components/courses/students/StudentCourseJoinButton.vue
+++ b/frontend/src/components/courses/students/StudentCourseJoinButton.vue
@@ -2,20 +2,31 @@
import Button from 'primevue/button';
import { type Course } from '@/types/Course.ts';
import { type Student } from '@/types/users/Student.ts';
-import { useAuthStore } from '@/store/authentication.store.ts';
import { useMessagesStore } from '@/store/messages.store.ts';
import { useStudents } from '@/composables/services/student.service.ts';
import { useI18n } from 'vue-i18n';
/* Props */
-const props = defineProps<{ student: Student; course: Course }>();
+const props = defineProps<{ student: Student; courses: Course[]; course: Course }>();
/* Composable injections */
-const { refreshUser } = useAuthStore();
const { addSuccessMessage, addErrorMessage } = useMessagesStore();
const { studentJoinCourse, studentLeaveCourse } = useStudents();
const { t } = useI18n();
+/* Emits */
+const emit = defineEmits(['update:courses']);
+
+/**
+ * Check if the student has already enrolled in the course.
+ *
+ * @param course The course to check
+ * @returns True if the student has the course, false otherwise
+ */
+function hasCourse(course: Course): boolean {
+ return props.courses.some((c) => c.id === course.id);
+}
+
/**
* Enroll the student in the course.
*/
@@ -28,7 +39,7 @@ async function joinCourse(): Promise {
t('toasts.messages.courses.enrollment.success', [props.course.name]),
);
- await refreshUser();
+ emit('update:courses');
} catch (error) {
addErrorMessage(t('toasts.messages.error'), t('toasts.messages.courses.enrollment.error', [props.course.name]));
}
@@ -46,7 +57,7 @@ async function leaveCourse(): Promise {
t('toasts.messages.courses.leave.success', [props.course.name]),
);
- await refreshUser();
+ emit('update:courses');
} catch (error) {
addErrorMessage(t('toasts.messages.error'), t('toasts.messages.courses.leave.error', [props.course.name]));
}
@@ -60,7 +71,7 @@ async function leaveCourse(): Promise {
icon-pos="right"
icon="pi pi-arrow-right"
@click="joinCourse"
- v-if="!student.hasCourse(course)"
+ v-if="!hasCourse(course)"
link
/>