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

Course Lesson order is changed whenever a single lesson is added or updated - Restore Lesson Order #3921

Closed
hansschuijff opened this issue Jan 23, 2021 · 7 comments · Fixed by #3951 · May be fixed by #3957
Closed

Course Lesson order is changed whenever a single lesson is added or updated - Restore Lesson Order #3921

hansschuijff opened this issue Jan 23, 2021 · 7 comments · Fixed by #3951 · May be fixed by #3957
Labels
Milestone

Comments

@hansschuijff
Copy link
Contributor

As I mentioned in #3900 we had problems with the lesson order. Every time we saved a lesson the order of lessons in the course is being changed so in the end I had to add custom code to override sensei-lms and force the lesson order in the database,.

I now have looked a bit deeper, logging what update_post_meta calls are activated, and I noticed that in saving the single lesson, sensei calls update_post_meta() updating the "_order_" .$course_id meta of all lessons.

The function that calls that update_post_meta() is Sensei_Course_Structure::save() and I'm wondering why that function should even be called when saving a single lesson in the gutenberg editor. I'm thinking: it shouldn't.

Since $raw_structure in that call, contains a different lesson order than I had set in the course edit page, or in the lesson order page, it corrupts the intentioned lesson order.

The order that is set, is probably coming from courses the _lesson_order meta, since I found that contains the same order that is passed to Sensei_Course_Structure::save() in $raw_structure. And at the end it saves the courses _lesson_order meta too, so after every save both values are the same, it's just not the intended order, since that was saved in the lessons meta and seems not to have been remembered the same order properly in the courses _lesson_order meta.

But then the extra problem (next to unnecessary updating all lessons when only one lesson has changed and the order cannot be changed there as far as I know) is that the values in the courses "_lesson_order" are not the same as the value in the individual lessons after ordering the lessons in either the course edit page or the lesson order page.

So it is also a problem of storing the lesson order in two separate places in the db that introduces the possibility they get out of sync.

That is as far as I've looked. I hope it get solved soon, so I can remove the custom fix. I now know that I can probably better change the custom code so it forces the _lesson_order meta in the course than the "_order_" .$course_id meta in all lessons, since at every update it seems to force everything to that order, but this functionality isn't perfect yet.

By the way, although my custom code hasn't updated the _lesson_order meta too, the problem was noted before any code like that was added.

After about ten time having to reorder 44 lessons, I just gave up and decided to instead find the meta in the lesson post that controls the pagination and the order in the course page and update that outside of sensei lms.

Also we didn't use course modules on the course that showed us the problem. When we first noticed the problem, we almost had no quizzes yet, just lessons.

I tried to reproduce the problem with a simple course on my local system, but that seemed to function like it should. So there might be some specific condition that causes it.

On our production and staging install, we had this problem after either saving or adding a lesson. Although our production system was running version 3.6.1, I have seen that version 3.7.0 still calls Sensei_Course_Structure::save() on a single lesson update. So on the receiving end of the error the functionality is still there.

Remaining Question for me is how the lessons and the course meta got out of sync. That answer I'm still short.

Steps to Reproduce

  1. Add a course
  2. Add lessons
  3. Order the lessons
  4. Save one lesson
  5. Go to the course page on the frontend en check the lesson order.

What I Expected

No change in course lesson order when updating an individual lesson, or adding a new lesson. Only the course structure block and the order lessons admin page should affect the lesson order, since they are the only pages where this functionality is offered to the user.

What Happened Instead

The order of lessons we set in the course page was changed in most lessons and we had to reorder them again and again.

PHP / WordPress / Sensei LMS version

php 7.4.1 and 8.0
wp 5.6
sensei lms 3.6.1 and 3.7.0

Browser / OS version

Windows 10
Firefox developer edition

Screenshot / Video

Context / Source

@yscik
Copy link
Contributor

yscik commented Jan 25, 2021

Thanks for digging into this! The order meta does need to be updated/calculated at least when adding a new lesson, and that was moved to the new structure class, but obviously something is not working correctly.

@hansschuijff
Copy link
Contributor Author

@yscik yes, obviously. :) I understand that in case of a new lesson a meta needs to be generated, but that only needs to be done in the meta of the new post. When updating a post, even that isn't necessary, since there is no way that action will influence the lesson order.

I'm sure there are ways to determine the highest lesson order number for a new post, and even when not, the repercussions of having two lessons with the same lesson order-meta isn't that severe. Often the content creator will reorder the lessons after adding a lesson anyway.

Hope there are enough clues now, so it can be fixed soon.

@hansschuijff
Copy link
Contributor Author

@yscik
I don't know if this is the exact cause, but in digging further in this issue I looked at where the lessons are ordered in the structure and came across Sensei_Course_Structure::sort_structure.

If I understand this function correctly, it is meant to sort the modules/lessons structure of the course to match the $order that has been passed to it. This $order as far as I have looked is generated by the lesson order admin page (or also by the structure block?). But it is empty ($order_string parameter) when the current page is the lesson editor.

In case a lesson is saved, that $order is an array containing only one entry with the value of 0. That one entry is generated since $order_string passed to Sensei()->admin->save_lesson_order() is empty, but since no id matches it, all calls to the anonymous callback of usort() return the value 0

I even tested that by hardcoding the return 0 as first statement, but otherwise it is returned after:


$a_position = array_search( $a['id'], $order, true );
$b_position = array_search( $b['id'], $order, true );

// If both weren't sorted, keep the current positions.
if ( false === $a_position && false === $b_position ) {
    return 0;
}

Perhaps that is expected to leave the order as is (also given the comment in the above code), but I find that it doesn't.

When the structure is passed into it, it has already been sorted to the lesson_order in the lessons meta ( '_order_' . $course_id ) giving any new lessons a order meta of 100000, but after it has been passed through Sensei_Course_Structure::sort_structure that order has been changed, even though it shouldn't.

In the php documentation see a note:

Note:

If two members compare as equal, their relative order in the sorted array is undefined.

And I'm wondering what undefined means in this context. It might mean the result is that the order may or may not change in that cases. Here all members compare as equal (since the result of the callable is 0 in any case) so php might not find it important to keep the order it started with. Since equal values may be switched without affecting the order.

Another note says that usort() may also change existing keys.

Note: 

This function assigns new keys to the elements in array. It will remove any existing keys that may have been assigned, rather than just reordering the keys.

When I log the values on our system ( I extracted only the id's from $structure here, to make it easy to compare visually ) I see the difference between what the function accepts and what it returns:

The current method is: Sensei_Course_Structure::sort_structure
The current filter is: save_post
Accepted values at the start of the function
The value of $structure is: 
'90597, 89981, 90645, 90319, 90876, 90318, 89961, 90307, 90309, 90192, 90197, 90598, 90184, 90584, 91101, 90651, 90190, 90217, 90306, 90294, 90295, 90296, 90283, 90285, 90284, 90277, 90278, 90276, 90475, 90478, 90482, 90486, 90267, 90257, 91244, 90255, 90256, 90258, 90604, 90242, 90243, 90241, 90240, 90234, 90230, 90308, 90661'
The value of $order is: 
array (
  0 => 0,
)
The value of $type is: 
'lesson'

2021-02-01 17:38:02 :: 
The current method is: Sensei_Course_Structure::sort_structure
The current filter is: save_post
Values just before returning $structure after sorting it.
The value of $structure is: 
'90597, 90255, 90278, 90276, 90475, 90478, 90482, 90486, 90267, 90257, 91244, 90256, 90284, 90258, 90604, 90242, 90243, 90241, 90240, 90234, 90230, 90308, 90277, 90285, 89981, 90598, 90645, 90319, 90876, 90318, 89961, 90307, 90309, 90192, 90197, 90184, 90283, 90584, 91101, 90651, 90190, 90217, 90306, 90294, 90295, 90296, 90661'
The value of $order is: 
array (
  0 => 0,
)
The value of $type is: 
'lesson'

You see that at those two markers, the $structure is different. So I'm thinking this is the problem, although it doesn't change that a lot of code has been executed in a situation that no change should even have been considered. When $order_string is empty ordering seems redundant and probably $order should be an empty array.

An easy fix might than be to make the call to Sensei_Course_Structure::sort_structure conditional on the $order value and skip it when $order is empty (in this case that means array ( 0 => 0, ).

But it might be that usort() isn't the best solution for this functionality anyway, given that a return 0 from the callback affects the initial order.

@hansschuijff
Copy link
Contributor Author

I've tried making the Sensei_Course_Structure::sort_structure call conditional like this to skip the sort_structure call for lesson pages.

if ( ! empty( $order ) 
&& $order !== array( 0 ) ) {
    $course_structure = Sensei_Course_Structure::sort_structure( $course_structure, $order, 'lesson' );
}

Not tested enough to be absolutely certain it solves the complete problem, but it might. Calling sort_structure in this cases is redundant anyway and knowing there is a problem there, makes it important to skip the call.

@hansschuijff
Copy link
Contributor Author

hansschuijff commented Feb 1, 2021

@yscik
I've issued a PR for this issue. Perhaps it is enough. For us it seems to do the trick. The lesson order is now retained at both course save and lesson save.

Instead of making the call to sort_structure conditional I decided to wrap the usort call in a condition. I have seen sort_structure is called in several places and this way any other calls that pass an empty $order parameter will be corrected too.

Hope it helps.

@yscik
Copy link
Contributor

yscik commented Feb 2, 2021

Thank you @hansschuijff, that's some great research! I'll check the PR and we can get this out there.

yscik added a commit that referenced this issue Feb 2, 2021
…r-on-single-lesson-save

Skip sort_structure method for single lesson save.
@donnapep donnapep added this to the 3.8.0 milestone Feb 2, 2021
@hansschuijff
Copy link
Contributor Author

@yscik I was thinking some more at why usort() failed in the first place. The PR I issued was just skipping of that problem. The problem as I said earlier is that returning 0 implies the order doesn't matter. So today I tested a bit more and issued a new PR to also address that problem. The basic problem is trying to sort information that isn't available in the callback.

You find my thoughts and solution in PR3957.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
3 participants