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

Vignettes #737

Open
wants to merge 108 commits into
base: feature/vignettes-longterm
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
80d9f7f
add action_text
f-buerckel Dec 4, 2024
63c9672
add questionaire and slide model for vignettes
f-buerckel Dec 4, 2024
0664a27
add basic views for create, list and edit questionnaire and slides
f-buerckel Dec 5, 2024
a1f4762
add modal create vignette panel
f-buerckel Dec 8, 2024
d59dbf0
Update action of slide
f-buerckel Dec 10, 2024
12ef9b5
add import statement of trix in application.js
f-buerckel Dec 15, 2024
0002f39
rebased dev into vignette
f-buerckel Dec 15, 2024
7ed2702
fix active storage direct upload routing error
f-buerckel Dec 16, 2024
b9176b0
fix activeStorage routing bug and add libsvips42
f-buerckel Dec 24, 2024
695ab13
add question model to vignettes and plain javascript approach for dyn…
f-buerckel Dec 26, 2024
c772b2e
add stimulus
f-buerckel Dec 26, 2024
c754ba1
add multiple choice option for questionnaire
f-buerckel Dec 29, 2024
d2497f7
Merge branch 'temporary-multipleChoiceQuestion' into feature/vignette
f-buerckel Dec 29, 2024
b220ab9
ensure correct post request for dynamic form
f-buerckel Jan 5, 2025
80ad4cf
add video support for action text
f-buerckel Jan 7, 2025
bb9c05e
add position attribute to slides
f-buerckel Jan 17, 2025
700bd0d
add answer model for vignettes
f-buerckel Jan 17, 2025
7c56f4f
improve video playback view
f-buerckel Jan 17, 2025
a782b5f
feat: take vignette questionnaire
f-buerckel Jan 17, 2025
b931d18
Track slide statistics for questionnaire answers
f-buerckel Jan 17, 2025
67cac52
Add route to export questionnaire answers as csv
f-buerckel Jan 17, 2025
02050b0
Add likert scale as vignette question type
f-buerckel Jan 17, 2025
b8e409f
add info slide version 1
f-buerckel Jan 18, 2025
fbbe137
-Add validation of position in questionnaire#take
f-buerckel Jan 22, 2025
9762e3e
-Add validations for title and question text;
f-buerckel Jan 24, 2025
2d37ed7
-Add icon support to info slides
f-buerckel Jan 25, 2025
6e383eb
correct date on migration
f-buerckel Jan 25, 2025
afa8891
removed unused views and routes
f-buerckel Jan 25, 2025
d820472
-minor bug fixes
f-buerckel Jan 25, 2025
e871872
remove leftovers from stimulus
f-buerckel Jan 25, 2025
9cf7638
fixed some rubocop errors
f-buerckel Jan 27, 2025
c37d9b1
fix more rubocop violations
f-buerckel Jan 27, 2025
674a781
fix all spec rubocop violations
f-buerckel Jan 27, 2025
e829f57
fix more spec rubocop violations
f-buerckel Jan 27, 2025
9bf7b6d
fix more spec rubocop violations
f-buerckel Jan 27, 2025
a9726f1
condense db migrations files
f-buerckel Jan 28, 2025
c5a385b
add ERD-diagram for vignettes
f-buerckel Feb 4, 2025
c8cb9f4
add version to active_storage_validations gem
f-buerckel Feb 9, 2025
11f0375
fix merge conflict of gemfile
f-buerckel Feb 9, 2025
b971ce4
bundle install for gemfile
f-buerckel Feb 9, 2025
86b27e1
Merge branch 'feature/vignettes-longterm' into feature/vignette
Splines Feb 20, 2025
9636aa2
Merge vignette answer migrations together
Splines Feb 22, 2025
d6cd90b
Init card design for slides
Splines Feb 23, 2025
b71cf8a
Style "Next slide" button & add slide shadow
Splines Feb 23, 2025
0cd77ad
Render info slides as modals
Splines Feb 23, 2025
5dd66a0
Only show info slide section if there is any
Splines Feb 23, 2025
3ca04e3
Redesign text area & add validation
Splines Feb 23, 2025
a3bb4e0
Check validations on form submit
Splines Feb 23, 2025
b23bfe7
Move answer section to card footer & restyle submit button
Splines Feb 23, 2025
debbb4a
Use object-oriented model for slide statistics
Splines Feb 23, 2025
553c035
Remove console errors
Splines Feb 23, 2025
288fc00
Early return if not text question
Splines Feb 23, 2025
47f857e
Add design for likert scale
Splines Feb 23, 2025
cf76766
Swap main content and additional information
Splines Feb 23, 2025
bbc6782
Fix alignment of additional information on small screens
Splines Feb 23, 2025
bc2003b
Improve accessibility for open info slide & submit button
Splines Feb 23, 2025
8889949
Make likert scale input required
Splines Feb 23, 2025
1c48ebe
Redesign multiple choice questions
Splines Feb 23, 2025
7375014
Fix indentation of ERB syntax
Splines Feb 23, 2025
f9436de
Init UI support for sortable slides
Splines Feb 23, 2025
4e5ba22
Add TODO where slide was not moved at all
Splines Feb 24, 2025
dc60ef2
Edit slide view: get basic dynamic loading of edit form to work
Splines Feb 25, 2025
f4f2aba
Render slide edit form dynamically into accordion
Splines Feb 25, 2025
0974a68
Remove unnecessary console logs
Splines Feb 25, 2025
6fa43c5
Redesign add new slide button
Splines Feb 25, 2025
1270ab6
Add option to add new slide (at least to display the new slide creati…
Splines Feb 25, 2025
61d49d5
Design text field (for edit slide)
Splines Feb 25, 2025
ff6c395
Start rewriting question types logic
Splines Feb 25, 2025
eb90a01
Redesign question type selection & use Bootstrap collapse
Splines Feb 25, 2025
3497567
Let backend populate select field & handle question field state
Splines Feb 25, 2025
2edf6ac
Make question field state update more explicit
Splines Feb 26, 2025
32d8c81
Start simplifying multiple choice options view
Splines Feb 26, 2025
ff50fc0
Reimplement adding new multiple choice option
Splines Feb 26, 2025
584b349
Implement removing a multiple choice option
Splines Feb 26, 2025
3903f64
Make sure remove button also works for dynamically added items
Splines Feb 26, 2025
363888e
Pin Tom Select & use bootstrap 5 CSS styles
Splines Feb 27, 2025
2638982
Use Tom Select for info slide selection & add form-floating
Splines Feb 27, 2025
f248fc2
Disable sortable dragging on accordion body
Splines Feb 27, 2025
ae20f2a
Remove unused ghost class on sortable
Splines Feb 27, 2025
6f15393
Use tom select for question type selection
Splines Feb 27, 2025
09f4daf
Don't prevent mouse clicks for filtered sortable items
Splines Feb 27, 2025
f29367a
Improve accordion slide opening (spinner & flickering)
Splines Feb 27, 2025
4169992
Ignore shown.bs.collapse for multiple choice options
Splines Feb 27, 2025
95fc4ef
Improve rendering of loading spinner
Splines Feb 27, 2025
f7651c8
Add hint for dragging slides
Splines Feb 27, 2025
255b5c4
Fix redundant line continuation
Splines Feb 28, 2025
d5b60ac
make questionnnaires part of course
f-buerckel Feb 28, 2025
cad182f
Merge branch 'feature/vignette' of github.com:MaMpf-HD/mampf into fea…
f-buerckel Feb 28, 2025
10f4706
Merge pull request #753 from MaMpf-HD/vignettes-ui
f-buerckel Feb 28, 2025
de97b7b
fix docker compose command in just docker-reseed
f-buerckel Feb 28, 2025
15369ef
add locking mechanism to vignettes
f-buerckel Feb 28, 2025
c3b02f0
Make questionnaire part of lecture
f-buerckel Feb 28, 2025
d6a8b23
make vignettes accessible from lecture
f-buerckel Mar 1, 2025
aa8d4e4
reworked routes to not use vignettes prefix
f-buerckel Mar 1, 2025
24ebaae
Add authentication for vignette controller actions
f-buerckel Mar 1, 2025
c17e83f
fix bug where vignette answer not stored correctly
f-buerckel Mar 2, 2025
56bfe4d
Remove unreachable code for questionnaire function
f-buerckel Mar 3, 2025
ecd1d09
fix rubocop warnings
f-buerckel Mar 3, 2025
65238e7
Add update slide function for questionnaire
f-buerckel Mar 3, 2025
3f94297
Remove unused routes and views
f-buerckel Mar 3, 2025
3b206d3
Add delete button for questionnaire and slides
f-buerckel Mar 3, 2025
59d7886
Use a floating form for create new vignette title
Splines Mar 4, 2025
2af894c
Use modal-footer & respect MaMpf "Cancel vs. Submit" button ordering
Splines Mar 4, 2025
caa37b9
Only show "drag slides" tip if there are at least 2 slides
Splines Mar 4, 2025
9691e6a
Remove "New Slide" title
Splines Mar 4, 2025
7b32faa
Don't show spinner when creating a new slide
Splines Mar 4, 2025
4ca39d9
Add backend validations for actions after publish
f-buerckel Mar 4, 2025
1b8bb4b
Merge branch 'feature/vignette' of github.com:MaMpf-HD/mampf into fea…
f-buerckel Mar 4, 2025
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
2 changes: 1 addition & 1 deletion .config/commands/docker.justfile
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ db-tear-down:
echo -e "\033[33mIgnore the error 'Resource is still in use' for the development_default network\033[0m"
cd {{justfile_directory()}}/docker/development/
docker compose down db --volumes
docker-compose up -d --force-recreate db
docker compose up -d --force-recreate db
just docker wait-for-postgres

# Removes the development docker containers
Expand Down
7 changes: 7 additions & 0 deletions .config/commands/utils.justfile
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,12 @@ erd:
exclude="${exclude_default},Claimable,Editable,Teachable" \
only="Lecture,Lesson,Chapter,Section,Item,LessonSectionJoin,Term"

# 🌟 Vignettes
docker compose exec -it mampf rake erd warn=false \
title="Vignettes" filename=/usr/src/app/tmp/erd/mampf-erd-vignettes \
inheritance=true polymorphism=true indirect=true attributes=content \
exclude="${exclude_default},Claimable,Editable,Teachable,Notifiable,Record" \
only="Vignettes::Questionnaire, Vignettes::Slide, Vignettes::InfoSlide, Vignettes::Answer, Vignettes::UserAnswer, Vignettes::Question, Vignettes::SlideStatistic, Vignettes::LikertScaleAnswer, Vignettes::LikertScaleQuestion, Vignettes::MultipleChoiceAnswer, Vignettes::MultipleChoiceQuestion, Vignettes::TextQuestion, Vignettes::TextAnswe, Vignettes::LikertScaleAnswer, Vignettes::LikertScaleQuestion, Vignettes::MultipleChoiceAnswer, Vignettes::MultipleChoiceQuestion, Vignettes::TextQuestion, Vignettes::TextAnswer, Vignettes::Option"

echo "📂 Diagrams are ready for you in the folder {{justfile_directory()}}/tmp/erd/"
echo "🔀 For the meanings of the arrows, refer to https://voormedia.github.io/rails-erd/gallery.html#notations"
2 changes: 2 additions & 0 deletions .config/eslint.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ const customGlobals = {
renderMathInElement: "readable",

openAnnotationIfSpecifiedInUrl: "readable",

Sortable: "readable",
};

export default [
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ coverage
# Ignore shrine store
/public/uploads

# Ignore activeStorage uploads
/storage/*

# Ignore environment variables
completed_initial_run
/public/uploads.zip
Expand Down
1 change: 1 addition & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--require spec_helper
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? I do see the advantage that this means the spec_helper is automatically included. However, this is already done in the rails_helper by requiring it explicitly. All you have to do is therefore include the rails helper which is done in every of our test files.

1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
"justfile",
"katex",
"lightgray",
"likert",
"localroot",
"mailcatcher",
"mampf",
Expand Down
1 change: 1 addition & 0 deletions app/assets/config/manifest.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//= link_tree ../images
//= link_directory ../javascripts/vignettes .js
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use application.js instead (or rather I'm not sure what the difference between this manifest.js and the other file is and will check it out)

//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css
//= link commontator/manifest.js
Expand Down
107 changes: 107 additions & 0 deletions app/assets/javascripts/vignettes/edit.js.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// When using turbolinks, the event is not fired on redirect
$(document).ready(function () {
const vignetteSlideList = $("#vignettes-slides-accordion");
createSortableVignetteSlides(vignetteSlideList);
registerVignetteSlideListeners(vignetteSlideList);
registerNewSlideButtonListener(vignetteSlideList);
});

function createSortableVignetteSlides(vignetteSlideList) {
Sortable.create(vignetteSlideList.get(0), {
animation: 150,
filter: ".accordion-collapse",
preventOnFilter: false,
onEnd: function (evt) {
if (evt.oldIndex == evt.newIndex) return;

let questionnaire_id = evt.target.dataset.questionnaireId;
$.ajax({
url: `/questionnaires/${questionnaire_id}/update_slide_position`,
method: "PATCH",
data: {
old_position: evt.oldIndex,
new_position: evt.newIndex,
},
success: function (_response) {
console.log(`Slide was moved from position ${evt.oldIndex} to ${evt.newIndex}`);
},
error: function (xhr, status, error) {
console.error(`Failed to update position: ${error}`);
},
});
},
});
}

function registerVignetteSlideListeners(vignetteSlideList) {
$(".vignette-accordion-collapse").on("shown.bs.collapse", function (evt) {
const slideId = evt.target.dataset.slideId;
if (slideId === undefined) {
// multiple choice options are also collapsible
// and trigger shown.bs.collapse
return;
}

const questionnaireId = vignetteSlideList.attr("data-questionnaire-id");
if (questionnaireId === undefined) {
console.error("Questionnaire id is missing");
return;
}

const loadingSpinner = $(`#vignette-slide-loading-${slideId}`);
let spinnerTimeout = setTimeout(function () {
loadingSpinner.show();
}, 100); // avoid flickering when loading is fast

$.ajax({
url: Routes.edit_questionnaire_slide_path(questionnaireId, slideId),
method: "GET",
dataType: "html",
success: function (response) {
$(vignetteSlideList).find(".slides-edit-form-container").html("");
const editFormContainer = $(`#slide-edit-form-container-${slideId}`);
editFormContainer.hide().html(response).ready(function () {
clearTimeout(spinnerTimeout);
loadingSpinner.hide();
editFormContainer.show();
});
},
error: function (xhr, status, error) {
clearTimeout(spinnerTimeout);
console.error(`Failed to load slide edit form: ${error}`);
},
});
});
}

function registerNewSlideButtonListener(vignetteSlideList) {
$("#vignettes-new-slide-btn").click(function () {
$(this).prop("disabled", true);

const questionnaireId = vignetteSlideList.attr("data-questionnaire-id");
if (questionnaireId === undefined) {
console.error("Questionnaire id is missing");
return;
}

$.ajax({
url: Routes.new_questionnaire_slide_path(questionnaireId),
method: "GET",
dataType: "html",
success: function (response) {
$(vignetteSlideList).append(response);
const newSlide = $(vignetteSlideList).children().last();
openAccordionItem(newSlide);
},
error: function (xhr, status, error) {
console.error(`Failed to load new slide form: ${error}`);
},
});
});
}

function openAccordionItem($item) {
new bootstrap.Collapse($item.find(".collapse"), {
toggle: true,
});
}
5 changes: 5 additions & 0 deletions app/assets/javascripts/vignettes/edit_slide.js.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
$(document).ready(function () {
new TomSelect("#vignettes-linked-info-slides", {
plugins: ["remove_button"],
});
});
55 changes: 55 additions & 0 deletions app/assets/javascripts/vignettes/question_type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
var QUESTION_TYPE_SELECT_ID = "#vignettes-question-type-select";

$(document).ready(function () {
handleQuestionTypes();
updateQuestionFieldState($(QUESTION_TYPE_SELECT_ID).val());
new TomSelect(QUESTION_TYPE_SELECT_ID, { allowEmptyOption: true });
handleMultipleChoiceEditor();
});

function handleQuestionTypes() {
const questionTypeDropdown = $(QUESTION_TYPE_SELECT_ID);

questionTypeDropdown.on("change", function (event) {
updateQuestionFieldState(event.target.value);
});
}

function updateQuestionFieldState(selectedName) {
const multipleChoiceField = $("#vignette-edit-multiple-choice");
const questionTextField = $("#vignette-question-text");

// Type "No question"
if (selectedName === "") {
questionTextField.find("textarea").val("");
questionTextField.collapse("hide");
}
else {
questionTextField.collapse("show");
}

if (selectedName === "Vignettes::MultipleChoiceQuestion") {
multipleChoiceField.collapse("show");
}
else {
multipleChoiceField.collapse("hide");
}
}

function handleMultipleChoiceEditor() {
// Adding an option
$("#vignette-multiple-choice-add").click(function (_evt) {
const template = $("#vignette-multiple-choice-options-template");
const newOptionHtml = template.html();
const uniqueId = new Date().getTime();
const newBlockHtml = newOptionHtml.replace(/NEW_RECORD/g, uniqueId);
$("#vignette-multiple-choice-options").append(newBlockHtml);
});

// Removing an option
$(document).on("click", ".remove-vignette-mc-option", function (evt) {
const parentDiv = $(evt.target).closest("div");
parentDiv.find(".vignette-mc-hidden-destroy").val("1");
parentDiv.removeClass("d-flex").hide();
});
}
170 changes: 170 additions & 0 deletions app/assets/javascripts/vignettes/take_questionnaire.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
var VIGNETTE_FORM_ID = "#vignettes-answer-form";
function shouldRegisterVignette() {
return $(VIGNETTE_FORM_ID).length > 0;
}

$(document).on("turbolinks:load", () => {
if (!shouldRegisterVignette()) {
return;
}

registerSubmitHandler();
registerTextAnswerValidator();

const stats = new VignetteSlideStatistics();
registerStatisticsHandler(stats);
});

function registerSubmitHandler() {
$(VIGNETTE_FORM_ID).submit((event) => {
let isValid = false;
isValid = validateTextAnswer();

if (!isValid) {
event.preventDefault();
return false;
}
});
}

////////////////////////////////////////////////////////////////////////////////
// Text Answer Fields
////////////////////////////////////////////////////////////////////////////////
var TEXT_ANSWER_ID = "vignettes_answer_text";

function registerTextAnswerValidator() {
const textBody = document.getElementById(TEXT_ANSWER_ID);
if (!textBody) {
return;
}
$(textBody).on("input", () => {
validateTextAnswer();
});
}

function validateTextAnswer() {
const textBody = document.getElementById(TEXT_ANSWER_ID);
if (!textBody) {
return true;
}

let isValid = false;

const validityState = textBody.validity;
if (validityState.tooShort) {
const tooShortMessage = textBody.dataset.tooShortMessage;
textBody.setCustomValidity(tooShortMessage);
}
else if (validityState.valueMissing) {
const valueMissingMessage = textBody.dataset.valueMissingMessage;
textBody.setCustomValidity(valueMissingMessage);
}
else {
// render input valid, so that form will submit
textBody.setCustomValidity("");
isValid = true;
}

textBody.reportValidity();
return isValid;
}

////////////////////////////////////////////////////////////////////////////////
// Statistics
////////////////////////////////////////////////////////////////////////////////

class VignetteSlideStatistics {
slideStartTime = new Date();
totalSlideTime = 0;

infoSlideAccessCounts = {};
infoSlideStartTime = null;
infoSlideTimes = {};

increaseInfoSlideAccessCount(index) {
this.infoSlideAccessCounts[index] ??= 0;
this.infoSlideAccessCounts[index]++;
}

freezeSlideTime() {
if (this.slideStartTime === null) {
console.error("Attempted to freeze slide time when it was already frozen");
return;
}
this.totalSlideTime += (Date.now() - this.slideStartTime);
this.slideStartTime = null;
}

unfreezeSlideTime() {
this.slideStartTime = Date.now();
}

startInfoSlideTimer() {
this.infoSlideStartTime = Date.now();
}

stopInfoSlideTimer(index) {
if (this.infoSlideStartTime === null) {
console.error("Attempted to stop info slide timer when it was already stopped");
return;
}
const timeOnInfoSlide = (Date.now() - this.infoSlideStartTime);
this.infoSlideTimes[index] = (this.infoSlideTimes[index] || 0.0) + timeOnInfoSlide;
this.infoSlideStartTime = null;
}

postProcessTimes() {
for (let key in this.infoSlideTimes) {
this.infoSlideTimes[key] = Math.floor(this.infoSlideTimes[key] / 1000);
}
this.totalSlideTime = Math.floor(this.totalSlideTime / 1000);
}
}

function registerStatisticsHandler(stats) {
// Info Slide - Opening
const openInfoSlideButtons = $(".open-info-slide-btn");
if (!openInfoSlideButtons) {
return;
}
openInfoSlideButtons.each(function () {
const index = $(this).attr("data-info-slide-index");
$(this).click(() => {
stats.freezeSlideTime();
stats.increaseInfoSlideAccessCount(index);
stats.startInfoSlideTimer();
});
});

// Info Slide - Closing
const infoSlideModals = $(".vignette-info-slide-modal");
if (!infoSlideModals) {
console.error("No info slide modals found");
return;
}
infoSlideModals.each(function () {
$(this).on("hidden.bs.modal", function () {
const index = $(this).attr("data-info-slide-index");
stats.stopInfoSlideTimer(index);
stats.unfreezeSlideTime();
});
});

// Form Submission
$(VIGNETTE_FORM_ID).submit(() => {
// Take rest of time into account
if (stats.slideStartTime) {
stats.freezeSlideTime();
}

stats.postProcessTimes();

// Transfer results to hidden form-fields
const timeOnSlideField = $("#time-on-slide-field");
const timeOnInfoSlidesField = $("#time-on-info-slides-field");
const infoSlidesAccessCountField = $("#info-slides-access-count-field");
timeOnSlideField.val(stats.totalSlideTime);
timeOnInfoSlidesField.val(JSON.stringify(stats.infoSlideTimes));
infoSlidesAccessCountField.val(JSON.stringify(stats.infoSlideAccessCounts));
});
}
Loading
Loading