diff --git a/Pipfile b/Pipfile index 0050c4dd..bc926475 100644 --- a/Pipfile +++ b/Pipfile @@ -13,6 +13,7 @@ requests = "~=2.31" psutil = "~=5.9" celery = "~=5.3.5" # Celery v5.4 may stop supporting Python 3.8 django-celery-results = "~=2.5" +django-markdownify = "~=0.9" # includes the markdown package psycopg = "~=3.1" virtualenv = "~=20.26" redis = "~=5.0" diff --git a/Pipfile.lock b/Pipfile.lock index 3f67df55..84feff88 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2ac517842d70e57912c87a0b643e0725eec2b29308c12faf6ca1d9f2324d36a3" + "sha256": "43f024e633e4e451a03b4907341ff79c0c15bad2fa03432de9fe85398c705ff6" }, "pipfile-spec": 6, "requires": { @@ -117,6 +117,17 @@ "markers": "python_version >= '3.7'", "version": "==4.2.0" }, + "bleach": { + "extras": [ + "css" + ], + "hashes": [ + "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe", + "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6" + ], + "markers": "python_version >= '3.8'", + "version": "==6.1.0" + }, "celery": { "hashes": [ "sha256:870cc71d737c0200c397290d730344cc991d13a057534353d124c9380267aab9", @@ -347,41 +358,41 @@ }, "cryptography": { "hashes": [ - "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee", - "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576", - "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d", - "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30", - "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413", - "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb", - "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da", - "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4", - "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd", - "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc", - "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8", - "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1", - "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc", - "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e", - "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8", - "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940", - "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400", - "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7", - "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16", - "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278", - "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74", - "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec", - "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1", - "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2", - "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c", - "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922", - "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a", - "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6", - "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1", - "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e", - "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac", - "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7" + "sha256:00c0faa5b021457848d031ecff041262211cc1e2bce5f6e6e6c8108018f6b44a", + "sha256:073104df012fc815eed976cd7d0a386c8725d0d0947cf9c37f6c36a6c20feb1b", + "sha256:076c92b08dd1ab88108bc84545187e10d3693a9299c593f98c4ea195a0b0ead7", + "sha256:089aeb297ff89615934b22c7631448598495ffd775b7d540a55cfee35a677bf4", + "sha256:3b750279f3e7715df6f68050707a0cee7cbe81ba2eeb2f21d081bd205885ffed", + "sha256:43e521f21c2458038d72e8cdfd4d4d9f1d00906a7b6636c4272e35f650d1699b", + "sha256:4bdb39ecbf05626e4bfa1efd773bb10346af297af14fb3f4c7cb91a1d2f34a46", + "sha256:5967e3632f42b0c0f9dc2c9da88c79eabdda317860b246d1fbbde4a8bbbc3b44", + "sha256:65d529c31bd65d54ce6b926a01e1b66eacf770b7e87c0622516a840e400ec732", + "sha256:6981acac509cc9415344cb5bfea8130096ea6ebcc917e75503143a1e9e829160", + "sha256:81dbe47e28b703bc4711ac74a64ef8b758a0cf056ce81d08e39116ab4bc126fa", + "sha256:8b90c57b3cd6128e0863b894ce77bd36fcb5f430bf2377bc3678c2f56e232316", + "sha256:9184aff0856261ecb566a3eb26a05dfe13a292c85ce5c59b04e4aa09e5814187", + "sha256:945a43ebf036dd4b43ebfbbd6b0f2db29ad3d39df824fb77476ca5777a9dde33", + "sha256:97eeacae9aa526ddafe68b9202a535f581e21d78f16688a84c8dcc063618e121", + "sha256:9f1a3bc2747166b0643b00e0b56cd9b661afc9d5ff963acaac7a9c7b2b1ef638", + "sha256:9ff75b88a4d273c06d968ad535e6cb6a039dd32db54fe36f05ed62ac3ef64a44", + "sha256:aeb6f56b004e898df5530fa873e598ec78eb338ba35f6fa1449970800b1d97c2", + "sha256:b16b90605c62bcb3aa7755d62cf5e746828cfc3f965a65211849e00c46f8348d", + "sha256:b99831397fdc6e6e0aa088b060c278c6e635d25c0d4d14bdf045bf81792fda0a", + "sha256:bc954251edcd8a952eeaec8ae989fec7fe48109ab343138d537b7ea5bb41071a", + "sha256:c05230d8aaaa6b8ab3ab41394dc06eb3d916131df1c9dcb4c94e8f041f704b74", + "sha256:d16a310c770cc49908c500c2ceb011f2840674101a587d39fa3ea828915b7e83", + "sha256:d93080d2b01b292e7ee4d247bf93ed802b0100f5baa3fa5fd6d374716fa480d4", + "sha256:e1f5f15c5ddadf6ee4d1d624a2ae940f14bd74536230b0056ccb28bb6248e42a", + "sha256:e3442601d276bd9e961d618b799761b4e5d892f938e8a4fe1efbe2752be90455", + "sha256:e85f433230add2aa26b66d018e21134000067d210c9c68ef7544ba65fc52e3eb", + "sha256:eecca86813c6a923cabff284b82ff4d73d9e91241dc176250192c3a9b9902a54", + "sha256:f1e933b238978ccfa77b1fee0a297b3c04983f4cb84ae1c33b0ea4ae08266cc9", + "sha256:f4cece02478d73dacd52be57a521d168af64ae03d2a567c0c4eb6f189c3b9d79", + "sha256:f567a82b7c2b99257cca2a1c902c1b129787278ff67148f188784245c7ed5495", + "sha256:f987a244dfb0333fbd74a691c36000a2569eaf7c7cc2ac838f85f59f0588ddc9" ], "markers": "python_version >= '3.7'", - "version": "==42.0.5" + "version": "==42.0.6" }, "daphne": { "hashes": [ @@ -450,6 +461,14 @@ "markers": "python_version >= '3.6'", "version": "==3.2.3" }, + "django-markdownify": { + "hashes": [ + "sha256:acf42614a418aef55535a66d4b3426b181cf8c8f990e265f453df900c3ad3d25", + "sha256:df00291bc338400b9ef999402751fc12c30623d266b7c04e43336d27d0eadee6" + ], + "index": "pypi", + "version": "==0.9.3" + }, "executing": { "hashes": [ "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147", @@ -490,6 +509,14 @@ "markers": "python_version >= '3.5'", "version": "==3.7" }, + "importlib-metadata": { + "hashes": [ + "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570", + "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2" + ], + "markers": "python_version < '3.10'", + "version": "==7.1.0" + }, "incremental": { "hashes": [ "sha256:912feeb5e0f7e0188e6f42241d2f450002e11bbc0937c65865045854c24c0bd0", @@ -682,6 +709,14 @@ "markers": "python_version >= '3.6'", "version": "==5.2.1" }, + "markdown": { + "hashes": [ + "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f", + "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224" + ], + "markers": "python_version >= '3.8'", + "version": "==3.6" + }, "matplotlib-inline": { "hashes": [ "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", @@ -886,11 +921,11 @@ }, "pygments": { "hashes": [ - "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", - "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" + "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", + "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" ], - "markers": "python_version >= '3.7'", - "version": "==2.17.2" + "markers": "python_version >= '3.8'", + "version": "==2.18.0" }, "pyjwt": { "hashes": [ @@ -1011,6 +1046,13 @@ ], "version": "==0.6.3" }, + "tinycss2": { + "hashes": [ + "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847", + "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627" + ], + "version": "==1.2.1" + }, "traitlets": { "hashes": [ "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", @@ -1043,7 +1085,7 @@ "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], - "markers": "python_version < '3.10'", + "markers": "python_version >= '3.8'", "version": "==4.11.0" }, "tzdata": { @@ -1086,6 +1128,21 @@ ], "version": "==0.2.13" }, + "webencodings": { + "hashes": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "version": "==0.5.1" + }, + "zipp": { + "hashes": [ + "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b", + "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715" + ], + "markers": "python_version >= '3.8'", + "version": "==3.18.1" + }, "zope-interface": { "hashes": [ "sha256:014bb94fe6bf1786da1aa044eadf65bc6437bcb81c451592987e5be91e70a91e", @@ -1501,7 +1558,7 @@ "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0", "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a" ], - "markers": "python_version < '3.10'", + "markers": "python_version >= '3.8'", "version": "==4.11.0" }, "virtualenv": { diff --git a/tin/apps/assignments/forms.py b/tin/apps/assignments/forms.py index 665ea418..41486ea0 100644 --- a/tin/apps/assignments/forms.py +++ b/tin/apps/assignments/forms.py @@ -28,6 +28,7 @@ class Meta: fields = [ "name", "description", + "markdown", "folder", "language", "filename", @@ -44,6 +45,7 @@ class Meta: "submission_limit_cooldown", ] labels = { + "markdown": "Use markdown?", "venv": "Virtual environment", "hidden": "Hide assignment from students?", "enable_grader_timeout": "Set a timeout for the grader?", @@ -59,6 +61,7 @@ class Meta: "filename": "Clarify which file students need to upload (including the file " "extension). For Java assignments, this also sets the name of the " "saved submission file.", + "markdown": "This allows adding images, code blocks, or hyperlinks to the assignment description.", "venv": "If set, Tin will run the student's code in this virtual environment.", "grader_has_network_access": 'If unset, this effectively disables "Give submissions ' 'internet access" below. If set, it increases the amount ' diff --git a/tin/apps/assignments/migrations/0029_assignment_markdown.py b/tin/apps/assignments/migrations/0029_assignment_markdown.py new file mode 100644 index 00000000..3ee78436 --- /dev/null +++ b/tin/apps/assignments/migrations/0029_assignment_markdown.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.11 on 2024-05-05 00:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("assignments", "0028_assignment_venv"), + ] + + operations = [ + migrations.AddField( + model_name="assignment", + name="markdown", + field=models.BooleanField(default=False), + ), + ] diff --git a/tin/apps/assignments/models.py b/tin/apps/assignments/models.py index 1b51f041..b3047da1 100644 --- a/tin/apps/assignments/models.py +++ b/tin/apps/assignments/models.py @@ -72,6 +72,7 @@ class Assignment(models.Model): related_name="assignments", ) description = models.CharField(max_length=4096) + markdown = models.BooleanField(default=False) LANGUAGES = ( ("P", "Python 3"), diff --git a/tin/settings/__init__.py b/tin/settings/__init__.py index 321e62f6..f3ffec88 100644 --- a/tin/settings/__init__.py +++ b/tin/settings/__init__.py @@ -56,6 +56,7 @@ "django_extensions", "django_celery_results", "debug_toolbar", + "markdownify.apps.MarkdownifyConfig", "tin.apps", "tin.apps.users", "tin.apps.auth", @@ -236,6 +237,41 @@ CELERY_BROKER_URL = "redis://localhost:6379/1" +# Markdown +MARKDOWNIFY = { + "default": { + "MARKDOWN_EXTENSIONS": [ + "fenced_code", + ], + "WHITELIST_ATTRS": ["class", "href", "src", "alt"], + "WHITELIST_TAGS": [ + "a", + "abbr", + "acronym", + "b", + "blockquote", + "code", + "em", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "i", + "img", + "li", + "ol", + "p", + "pre", + "span", + "strong", + "ul", + ], + } +} + + # Django debug toolbar settings _enabled_panels = { diff --git a/tin/static/css/base.css b/tin/static/css/base.css index 2e9df8f8..11a2be81 100644 --- a/tin/static/css/base.css +++ b/tin/static/css/base.css @@ -141,7 +141,7 @@ body { text-align: right; } -ul:not(.standard) { +ul.no-list { list-style-type: none; padding-left: 0px; } diff --git a/tin/templates/assignments/show.html b/tin/templates/assignments/show.html index 39496374..7b9d7b6e 100644 --- a/tin/templates/assignments/show.html +++ b/tin/templates/assignments/show.html @@ -1,5 +1,6 @@ {% extends "base.html" %} {% load static %} +{% load markdownify %} {% block title %} Turn-In: {{ course.name }}: {% if assignment.is_quiz %}[QUIZ] {% endif %}{{ assignment.name }} @@ -50,7 +51,11 @@