Description
When you copy/duplicate a LearningClass, the new class gets its own LearningClassActivity rows (due dates, points, order, etc.), but each new LearningClassActivity still points at the same LearningActivity record as the original class. Since LearningActivity holds the actual activity content (name, description, activity type, and ActivityComponentSettingsJson — the real quiz/assignment configuration), editing an activity's content in either class edits it in both.
This does not match the behavior of the sibling "copy a single activity" feature, which correctly clones the LearningActivity as well.
Also based on a comment from Jon in the LMS channel on rocketchat, describing the intended use of "Copy Class":
A class is an instance of a course. It's easier to think about it in the context of a college setting. ENG101 (Course) has curriculum. That course is offered M,W,F at 9am or T,Th at 2pm as classes. A common pattern for many churches will be that a Baptism Course only has one class that is used on-going. But... when the curriculum needs to change you can clone the original class, change the activities in the new class then make it active. It's a nice way to transition the fire hose from the original curriculum to the new one. The one class concept only works if you don't have a 'cohort' of students that need to be grouped together with a specific facilitator.
That workflow only works if the cloned class's activities can be edited independently of the original's. As implemented, editing an activity in the new class also silently rewrites it in the class you're trying to phase out, which defeats the purpose of cloning in the first place.
Actual Behavior
When copying/duplicating a LearningClass - edits to an activity show up in both classes, because both LearningClassActivity records reference the same LearningActivityId.
Expected Behavior
Each class should have an independent copy of the activity. Editing content in one class's activity should not affect the other class.
Steps to Reproduce
- Go to a Learning Program > Course > Class that has at least one activity in its learning plan.
- Use "Copy Class" to duplicate the class.
- Open the same activity in each of the two classes and edit its name, description, or activity-type-specific content (e.g. quiz questions) in one of them.
- Reload the other class and view the same activity.
NOTES
I used Claude cowork to diagnosis the behavior we were seeing - this submission was almost entirely created by it. Here are some additional details it included that don't exactly fit anywhere else in this form:
Root Cause
LearningClassService.Copy(LearningClass, bool includeActivities) in Rock/Model/LMS/LearningClass/LearningClassService.Partial.cs clones each LearningClassActivity but never clones its related LearningActivity:
foreach ( var activity in activities )
{
var newActivity = activity.CloneWithoutIdentity();
newActivity.LearningClassId = newLearningClass.Id;
activityService.Add( newActivity );
activity.LoadAttributes();
newActivity.LoadAttributes();
newActivity.CopyAttributesFrom( activity );
}
newActivity.LearningActivityId (copied by CloneWithoutIdentity()) still points at the original LearningActivity row.
Compare this to LearningClassActivityService.Copy(string key) in Rock/Model/LMS/LearningClassActivity/LearningClassActivityService.Partial.cs, which is used when copying a single activity within a class, and does this correctly:
var newActivity = activity.CloneWithoutIdentity();
newActivity.LearningActivity = activity.LearningActivity.CloneWithoutIdentity();
newActivity.LearningActivity.Name += " - Copy";
The class-level copy path should follow the same pattern: clone the LearningActivity for each activity being copied (unless LearningActivity.IsShared is true for that activity — see note below), rather than reusing the same LearningActivityId.
Secondary bug in the same method
In LearningClassService.Copy(), a newActivities list is declared to later persist attribute values for the copied activities, but it is never populated:
var newActivities = new List<LearningClassActivity>();
...
foreach ( var activity in activities )
{
var newActivity = activity.CloneWithoutIdentity();
// newActivity is never added to newActivities
...
}
rockContext.WrapTransaction( () =>
{
rockContext.SaveChanges();
newLearningClass.SaveAttributeValues( rockContext );
foreach ( var newActivity in newActivities ) // always empty — never runs
{
rockContext.SaveChanges();
newActivity.SaveAttributeValues( rockContext );
}
} );
As a result, any custom Attribute values on copied LearningClassActivity records are computed in memory (CopyAttributesFrom) but never persisted to the database.
Additional context: the IsShared flag
LearningActivity.IsShared (bit NOT NULL, present in the schema since Rock v17) is documented as: "Indicates whether or not this activity is intended to be shared as a template by multiple LearningClassActivity." This suggests the data model already anticipated the sharing-vs-independent distinction that this bug runs into.
However, this flag is not currently exposed anywhere:
LearningClassActivityBag (the DTO used by the Class Detail activity editor) has no IsShared property.
- There is no standalone
LearningActivityDetail block for managing LearningActivity templates directly.
- No block, REST endpoint, or Obsidian component appears to read or write this column.
Since nothing sets it, every activity effectively behaves as IsShared = false today, which makes the current copy behavior doubly unexpected: even in the case the schema explicitly designed for non-sharing, Copy() shares content anyway with no way to opt out.
Suggested Fix
In LearningClassService.Copy(), when copying each LearningClassActivity, also clone the associated LearningActivity (matching the pattern already used in LearningClassActivityService.Copy()) — unless LearningActivity.IsShared is true, in which case reusing the same LearningActivityId is correct. Also fix the newActivities list population so attribute values are saved for copied activities.
Environment
- Reviewed against the
develop branch on GitHub as of 2026-07-02.
Issue Confirmation
Rock Version
v18.3.5
Client Culture Setting
en-US
Description
When you copy/duplicate a
LearningClass, the new class gets its ownLearningClassActivityrows (due dates, points, order, etc.), but each newLearningClassActivitystill points at the sameLearningActivityrecord as the original class. SinceLearningActivityholds the actual activity content (name, description, activity type, andActivityComponentSettingsJson— the real quiz/assignment configuration), editing an activity's content in either class edits it in both.This does not match the behavior of the sibling "copy a single activity" feature, which correctly clones the
LearningActivityas well.Also based on a comment from Jon in the LMS channel on rocketchat, describing the intended use of "Copy Class":
That workflow only works if the cloned class's activities can be edited independently of the original's. As implemented, editing an activity in the new class also silently rewrites it in the class you're trying to phase out, which defeats the purpose of cloning in the first place.
Actual Behavior
When copying/duplicating a LearningClass - edits to an activity show up in both classes, because both
LearningClassActivityrecords reference the sameLearningActivityId.Expected Behavior
Each class should have an independent copy of the activity. Editing content in one class's activity should not affect the other class.
Steps to Reproduce
NOTES
I used Claude cowork to diagnosis the behavior we were seeing - this submission was almost entirely created by it. Here are some additional details it included that don't exactly fit anywhere else in this form:
Root Cause
LearningClassService.Copy(LearningClass, bool includeActivities)inRock/Model/LMS/LearningClass/LearningClassService.Partial.csclones eachLearningClassActivitybut never clones its relatedLearningActivity:newActivity.LearningActivityId(copied byCloneWithoutIdentity()) still points at the originalLearningActivityrow.Compare this to
LearningClassActivityService.Copy(string key)inRock/Model/LMS/LearningClassActivity/LearningClassActivityService.Partial.cs, which is used when copying a single activity within a class, and does this correctly:The class-level copy path should follow the same pattern: clone the
LearningActivityfor each activity being copied (unlessLearningActivity.IsSharedistruefor that activity — see note below), rather than reusing the sameLearningActivityId.Secondary bug in the same method
In
LearningClassService.Copy(), anewActivitieslist is declared to later persist attribute values for the copied activities, but it is never populated:As a result, any custom Attribute values on copied
LearningClassActivityrecords are computed in memory (CopyAttributesFrom) but never persisted to the database.Additional context: the IsShared flag
LearningActivity.IsShared(bit NOT NULL, present in the schema since Rock v17) is documented as: "Indicates whether or not this activity is intended to be shared as a template by multiple LearningClassActivity." This suggests the data model already anticipated the sharing-vs-independent distinction that this bug runs into.However, this flag is not currently exposed anywhere:
LearningClassActivityBag(the DTO used by the Class Detail activity editor) has noIsSharedproperty.LearningActivityDetailblock for managingLearningActivitytemplates directly.Since nothing sets it, every activity effectively behaves as
IsShared = falsetoday, which makes the current copy behavior doubly unexpected: even in the case the schema explicitly designed for non-sharing,Copy()shares content anyway with no way to opt out.Suggested Fix
In
LearningClassService.Copy(), when copying eachLearningClassActivity, also clone the associatedLearningActivity(matching the pattern already used inLearningClassActivityService.Copy()) — unlessLearningActivity.IsSharedistrue, in which case reusing the sameLearningActivityIdis correct. Also fix thenewActivitieslist population so attribute values are saved for copied activities.Environment
developbranch on GitHub as of 2026-07-02.Issue Confirmation
Rock Version
v18.3.5
Client Culture Setting
en-US