From af407c389d552d727ba08ba41bc46781b417586d Mon Sep 17 00:00:00 2001 From: Mike Allaway Date: Wed, 25 Sep 2024 07:39:13 +0100 Subject: [PATCH 1/4] latest series of updates --- django/account/admin.py | 24 +++++++++++++++++-- django/core/static/css/custom.css | 8 +++++-- .../0005_exercise_instructions_video_url.py | 19 +++++++++++++++ django/exercises/models.py | 2 ++ .../templates/exercises/exercise-detail.html | 15 ++++++++---- .../sentence-builder.html | 5 ++-- django/exercises/views.py | 4 ++-- requirements.txt | 10 ++++---- 8 files changed, 69 insertions(+), 18 deletions(-) create mode 100644 django/exercises/migrations/0005_exercise_instructions_video_url.py diff --git a/django/account/admin.py b/django/account/admin.py index d0ecbc7..7d4ccb0 100644 --- a/django/account/admin.py +++ b/django/account/admin.py @@ -7,7 +7,7 @@ def delete_users(modeladmin, request, queryset): """ - Sets all selected items in queryset to published + Deletes all selected users """ queryset.delete() @@ -15,6 +15,26 @@ def delete_users(modeladmin, request, queryset): delete_users.short_description = "PERMANENTLY DELETE selected users from database" +def users_active(modeladmin, request, queryset): + """ + Sets all selected users in queryset as 'active' + """ + queryset.update(is_active=True) + + +users_active.short_description = "Make selected users 'active' (they can login)" + + +def users_inactive(modeladmin, request, queryset): + """ + Sets all selected users in queryset as 'inactive' + """ + queryset.update(is_active=False) + + +users_inactive.short_description = "Make selected users 'inactive' (they can not login)" + + class UsersImportSpreadsheetAdmin(admin.ModelAdmin): """ Customise the admin interface: UsersImportSpreadsheet @@ -59,7 +79,7 @@ class UserAdmin(DjangoUserAdmin): 'date_joined', 'last_login') fieldsets = None - actions = (delete_users,) + actions = (users_active, users_inactive, delete_users) ordering = ['first_name', 'last_name'] def has_add_permission(self, request, obj=None): diff --git a/django/core/static/css/custom.css b/django/core/static/css/custom.css index 1465e07..f91ba86 100755 --- a/django/core/static/css/custom.css +++ b/django/core/static/css/custom.css @@ -121,7 +121,7 @@ select { .popup { display: none; - background: rgba(0, 0, 0, 0.8); + background: rgba(0, 0, 0, 0.2); position: fixed; top: 0; left: 0; @@ -563,6 +563,10 @@ main { margin: 2em auto; } +#exercise-detail-instructions-video { + margin: 2em auto; +} + #exercise-detail-ownership { font-size: 0.8em; margin-top: -3em; @@ -916,7 +920,7 @@ main { .exerciseformat-sentencebuilder-undo, .exerciseformat-sentencebuilder-checkanswer, .exerciseformat-sentencebuilder-restart { - float: left; + float: right; clear: both; font-size: 0.95em; color: #505050; diff --git a/django/exercises/migrations/0005_exercise_instructions_video_url.py b/django/exercises/migrations/0005_exercise_instructions_video_url.py new file mode 100644 index 0000000..fd3f1ef --- /dev/null +++ b/django/exercises/migrations/0005_exercise_instructions_video_url.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.16 on 2024-09-18 09:46 + +from django.db import migrations +import embed_video.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('exercises', '0004_fontsize_exercise_collaborators_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='exercise', + name='instructions_video_url', + field=embed_video.fields.EmbedVideoField(blank=True, help_text='(Optional) Provide the URL of a YouTube or Vimeo video to include the embedded video in the exercise instructions.', null=True), + ), + ] diff --git a/django/exercises/models.py b/django/exercises/models.py index 9c6dfdd..54e8fc5 100644 --- a/django/exercises/models.py +++ b/django/exercises/models.py @@ -2,6 +2,7 @@ from django.urls import reverse from account.models import User, UserRole from datetime import date +from embed_video.fields import EmbedVideoField import random import re @@ -244,6 +245,7 @@ class Exercise(models.Model): instructions_image = models.ImageField(upload_to='exercises-exercise-instructions', blank=True, null=True, help_text=OPTIONAL_HELP_TEXT + " Include an image here to illustrate the instructions for the entire exercise. E.g. if all questions relate to this image.") instructions_image_url = models.URLField(blank=True, null=True, help_text=f'{OPTIONAL_HELP_TEXT} {IMAGE_URL_HELP_TEXT}') instructions_image_width_percent = models.IntegerField(blank=True, null=True, help_text="Optional. Set the percentage width of the instructions box. Images will fill width of instructions box by default.", verbose_name="Instructions image width (%)") + instructions_video_url = EmbedVideoField(blank=True, null=True, help_text=f'{OPTIONAL_HELP_TEXT} Provide the URL of a YouTube or Vimeo video to include the embedded video in the exercise instructions.') is_a_formal_assessment = models.BooleanField(default=False, help_text="Marking this as a formal assessment (i.e. a test that counts to the student's grade) will put restrictions on this exercise, like preventing students from being able to check answers and only allowing a single attempt") is_published = models.BooleanField(default=True, verbose_name="Published") owned_by = models.ForeignKey(User, related_name="exercise_owned_by", on_delete=models.SET_NULL, blank=True, null=True, help_text="The person who is mainly responsible for managing this exercise") diff --git a/django/exercises/templates/exercises/exercise-detail.html b/django/exercises/templates/exercises/exercise-detail.html index eb1cc87..446d4f8 100644 --- a/django/exercises/templates/exercises/exercise-detail.html +++ b/django/exercises/templates/exercises/exercise-detail.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% load static %} +{% load static embed_video_tags %} {% block main %} @@ -96,6 +96,14 @@ {% if exercise.instructions_image_path %} exercise instructions image {% endif %} + + {% if exercise.instructions_video_url %} +
+ {% video exercise.instructions_video_url as instructions_video %} + {% video instructions_video %} + {% endvideo %} +
+ {% endif %} {% if user.is_staff %} @@ -312,11 +320,12 @@ // Click to prompt confirm submission of exercise attempt $('#exercise-detail-footer-completed').on('click', function(){ + $('.exerciseformat-showanswer, .answer').show(); {% if exercise.exercise_format.is_marked_automatically_by_system %} // Add score to popup $('#exercise-attempt-popup-score').text(totalScorePercent); // Set score val in exercise attempt form - if(totalScorePercent == '0') totalScorePercent=0.1 // 0 is ignored by backend, so change to 0.1 (will be rounded to 0) + if(totalScorePercent == '0') totalScorePercent=0.1; // 0 is ignored by backend, so change to 0.1 (will be rounded to 0) $('#exercise-attempt-form-score').val(totalScorePercent); {% endif %} @@ -380,8 +389,6 @@ }); // Store the hidden value as a JSON string, ready to pass to back-end if (attempt_detail.length) $('#exercise-attempt-form-attemptdetail').val(JSON.stringify(attempt_detail)) - - console.log(attempt_detail) }); // Calculate how long it takes the user to attempt the exercise diff --git a/django/exercises/templates/exercises/snippets/exercise-detail-formats/sentence-builder.html b/django/exercises/templates/exercises/snippets/exercise-detail-formats/sentence-builder.html index d9e79d0..b7fc98d 100644 --- a/django/exercises/templates/exercises/snippets/exercise-detail-formats/sentence-builder.html +++ b/django/exercises/templates/exercises/snippets/exercise-detail-formats/sentence-builder.html @@ -90,9 +90,8 @@ let newSentence = sentenceWords.join(" "); // Set the new sentence text (remove the last word) thisSentence.text(newSentence); - // Create the new word button - let newButtonHtml = `` - $(this).parent().find('.exerciseformat-sentencebuilder-words').append(newButtonHtml); + // Show the last word button + $(this).parent().find(`.exerciseformat-sentencebuilder-words button[value="${ sentenceLastWord }"]`).prop('disabled', false); // Clear results for this answer $(this).parent().parent().find('.exerciseformat-sentencebuilder-result').html(''); } diff --git a/django/exercises/views.py b/django/exercises/views.py index 2e1be4b..923e196 100644 --- a/django/exercises/views.py +++ b/django/exercises/views.py @@ -125,7 +125,7 @@ class ExerciseCreateView(PermissionRequiredMixin, CreateView): template_name = 'exercises/exercise-add.html' model = models.Exercise - fields = ['name', 'language', 'exercise_format', 'exercise_format_reverse_image_match', 'theme', 'difficulty', 'font_size', 'instructions', 'instructions_image', 'instructions_image_url', 'instructions_image_width_percent', 'is_a_formal_assessment', 'is_published'] + fields = ['name', 'language', 'exercise_format', 'exercise_format_reverse_image_match', 'theme', 'difficulty', 'font_size', 'instructions', 'instructions_image', 'instructions_image_url', 'instructions_image_width_percent', 'instructions_video_url', 'is_a_formal_assessment', 'is_published'] permission_required = ('exercises.add_exercise') success_url = reverse_lazy('exercises:list') @@ -151,7 +151,7 @@ class ExerciseUpdateView(PermissionRequiredMixin, UpdateView): template_name = 'exercises/exercise-edit.html' model = models.Exercise - fields = ['name', 'language', 'exercise_format_reverse_image_match', 'theme', 'difficulty', 'font_size', 'instructions', 'instructions_image', 'instructions_image_url', 'instructions_image_width_percent', 'is_a_formal_assessment', 'owned_by', 'collaborators', 'is_published'] + fields = ['name', 'language', 'exercise_format_reverse_image_match', 'theme', 'difficulty', 'font_size', 'instructions', 'instructions_image', 'instructions_image_url', 'instructions_image_width_percent', 'instructions_video_url', 'is_a_formal_assessment', 'owned_by', 'collaborators', 'is_published'] permission_required = ('exercises.change_exercise') def get_object(self): diff --git a/requirements.txt b/requirements.txt index 720aa75..fff0ef7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,13 +2,13 @@ # For Django versioning we use ~= (e.g. Django~=3.2.0) which will keep the minor version up to date (e.g. the latest version prior to 3.3.0) Django~=4.2.0 -psycopg~=3.1.8 -flake8~=4.0.1 +psycopg~=3.2.2 +flake8~=7.1.1 Pillow~=10.4.0 -gunicorn~=20.1.0 +gunicorn~=23.0.0 django-recaptcha~=3.0.0 -django-embed-video~=1.4.0 +django-embed-video~=1.4.10 pandas~=2.2.2 openpyxl~=3.1.5 XlsxWriter~=3.2.0 -django-cleanup~=6.0.0 \ No newline at end of file +django-cleanup~=8.1.0 \ No newline at end of file From 663bd72009809c24b6dfbe2a5182fb7c7ddae686 Mon Sep 17 00:00:00 2001 From: Mike Allaway Date: Wed, 25 Sep 2024 07:41:49 +0100 Subject: [PATCH 2/4] block users from being deleted --- django/account/admin.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/django/account/admin.py b/django/account/admin.py index 7d4ccb0..43b39fd 100644 --- a/django/account/admin.py +++ b/django/account/admin.py @@ -5,16 +5,6 @@ from .models import User, UsersImportSpreadsheet -def delete_users(modeladmin, request, queryset): - """ - Deletes all selected users - """ - queryset.delete() - - -delete_users.short_description = "PERMANENTLY DELETE selected users from database" - - def users_active(modeladmin, request, queryset): """ Sets all selected users in queryset as 'active' @@ -79,11 +69,14 @@ class UserAdmin(DjangoUserAdmin): 'date_joined', 'last_login') fieldsets = None - actions = (users_active, users_inactive, delete_users) + actions = (users_active, users_inactive) ordering = ['first_name', 'last_name'] def has_add_permission(self, request, obj=None): return False + + def has_delete_permission(self, request, obj=None): + return False def has_change_permission(self, request, obj=None): # Only allow changes to the password From 1563c8a55282e0b0f9aca048e30acd4dc86c1dfc Mon Sep 17 00:00:00 2001 From: Mike Allaway Date: Wed, 25 Sep 2024 07:46:33 +0100 Subject: [PATCH 3/4] f8 fixes --- django/account/admin.py | 2 +- django/exercises/views.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/django/account/admin.py b/django/account/admin.py index 43b39fd..a134352 100644 --- a/django/account/admin.py +++ b/django/account/admin.py @@ -74,7 +74,7 @@ class UserAdmin(DjangoUserAdmin): def has_add_permission(self, request, obj=None): return False - + def has_delete_permission(self, request, obj=None): return False diff --git a/django/exercises/views.py b/django/exercises/views.py index 923e196..171d31e 100644 --- a/django/exercises/views.py +++ b/django/exercises/views.py @@ -151,7 +151,12 @@ class ExerciseUpdateView(PermissionRequiredMixin, UpdateView): template_name = 'exercises/exercise-edit.html' model = models.Exercise - fields = ['name', 'language', 'exercise_format_reverse_image_match', 'theme', 'difficulty', 'font_size', 'instructions', 'instructions_image', 'instructions_image_url', 'instructions_image_width_percent', 'instructions_video_url', 'is_a_formal_assessment', 'owned_by', 'collaborators', 'is_published'] + fields = [ + 'name', 'language', 'exercise_format_reverse_image_match', 'theme', 'difficulty', + 'font_size', 'instructions', 'instructions_image', 'instructions_image_url', + 'instructions_image_width_percent', 'instructions_video_url', 'is_a_formal_assessment', + 'owned_by', 'collaborators', 'is_published' + ] permission_required = ('exercises.change_exercise') def get_object(self): From 583ee16ed2b4249309aa519a59a01b198378e5dc Mon Sep 17 00:00:00 2001 From: Mike Allaway Date: Wed, 25 Sep 2024 07:49:11 +0100 Subject: [PATCH 4/4] f8 fix --- django/exercises/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django/exercises/models.py b/django/exercises/models.py index 54e8fc5..745ca65 100644 --- a/django/exercises/models.py +++ b/django/exercises/models.py @@ -52,7 +52,7 @@ def make_urls_clickable(text): Find all urls in text and add suitable html tag to make them 'clickable' on the website """ # If a valid string with content - if type(text) == str and text != '': + if type(text) is str and text != '': # Regex to find all urls in the provided text urls = re.findall(r'''(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))''', text) # NOQA # Loop through all urls found in text