Merged in feature/MS851-MigrateSnapshots (pull request #143)

Feature/MS851 MigrateSnapshots

Approved-by: Ramon Wenger
This commit is contained in:
Lorenz Padberg 2024-02-13 10:40:42 +00:00
commit a07f2aab4f
14 changed files with 1017 additions and 147 deletions

1
.gitignore vendored
View File

@ -51,3 +51,4 @@ server/test-reports/
*.prod.sql
latest.dump*
dump*.sql
*.dump

View File

@ -5,19 +5,20 @@
v-if="module.id"
>
<div class="module__header">
<h2
class="module__meta-title"
id="meta-title"
>
{{ module.metaTitle }}
</h2>
<h2
class="module__meta-title"
id="meta-title"
>
{{ module.metaTitle }}
</h2>
<div class="module__categoryindicators" v-if="$flavor.showModuleFilter">
<div
class="module__categoryindicators"
v-if="$flavor.showModuleFilter"
>
<pill :text="module.level?.name"></pill>
<pill :text="module.category?.name"></pill>
</div>
</div>
<h1
@ -66,7 +67,7 @@
<div
class="module__objective-groups"
v-if="module.objectiveGroups.length"
v-if="module.objectiveGroups.length && showObjectives"
>
<objective-groups
:groups="languageCommunicationObjectiveGroups"
@ -95,127 +96,129 @@
</template>
<script>
import ObjectiveGroups from '@/components/objective-groups/ObjectiveGroups.vue';
import Chapter from '@/components/Chapter.vue';
import BookmarkActions from '@/components/notes/BookmarkActions.vue';
import Pill from "@/components/ui/Pill.vue";
import ObjectiveGroups from '@/components/objective-groups/ObjectiveGroups.vue';
import Chapter from '@/components/Chapter.vue';
import BookmarkActions from '@/components/notes/BookmarkActions.vue';
import Pill from '@/components/ui/Pill.vue';
export default {
props: {
module: {
type: Object,
default: () => ({}),
},
export default {
props: {
module: {
type: Object,
default: () => ({}),
},
},
components: {
Pill,
BookmarkActions,
ObjectiveGroups,
Chapter,
},
components: {
Pill,
BookmarkActions,
ObjectiveGroups,
Chapter,
},
computed: {
languageCommunicationObjectiveGroups() {
return this.module.objectiveGroups
? this.module.objectiveGroups.filter((group) => group.title.toLowerCase() === 'language_communication')
: [];
},
societyObjectiveGroups() {
return this.module.objectiveGroups
? this.module.objectiveGroups.filter((group) => group.title.toLowerCase() === 'society')
: [];
},
interdisciplinaryObjectiveGroups() {
return this.module.objectiveGroups
? this.module.objectiveGroups.filter((group) => group.title.toLowerCase() === 'interdisciplinary')
: [];
},
note() {
if (!(this.module && this.module.bookmark)) {
return;
}
return this.module.bookmark.note;
},
computed: {
languageCommunicationObjectiveGroups() {
return this.module.objectiveGroups
? this.module.objectiveGroups.filter((group) => group.title.toLowerCase() === 'language_communication')
: [];
},
};
societyObjectiveGroups() {
return this.module.objectiveGroups
? this.module.objectiveGroups.filter((group) => group.title.toLowerCase() === 'society')
: [];
},
interdisciplinaryObjectiveGroups() {
return this.module.objectiveGroups
? this.module.objectiveGroups.filter((group) => group.title.toLowerCase() === 'interdisciplinary')
: [];
},
note() {
if (!(this.module && this.module.bookmark)) {
return;
}
return this.module.bookmark.note;
},
showObjectives() {
return this.$route && this.$route.query['show-objectives'] !== undefined;
},
},
};
</script>
<style scoped lang="scss">
@import 'styles/helpers';
@import 'styles/helpers';
.module {
.module {
display: flex;
justify-self: center;
max-width: 100vw;
padding: $large-spacing 0;
@include desktop {
width: 800px;
padding: $large-spacing 15px;
}
flex-direction: column;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
&__hero {
margin-bottom: 35px;
}
&__hero-image {
max-width: 100%;
border-radius: 12px;
}
&__hero-source {
@include tiny-text;
line-height: 25px;
}
&__header {
display: flex;
justify-self: center;
max-width: 100vw;
justify-content: flex-start;
align-items: stretch;
margin-bottom: $small-spacing;
}
padding: $large-spacing 0;
@include desktop {
width: 800px;
padding: $large-spacing 15px;
}
flex-direction: column;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
&__meta-title {
@include meta-title;
margin-right: $medium-spacing;
}
&__hero {
margin-bottom: 35px;
&__intro-wrapper {
position: relative;
}
&__intro {
> :deep(p) {
margin-bottom: $large-spacing;
@include lead-paragraph;
&:last-child {
margin-bottom: 0;
}
}
&__hero-image {
max-width: 100%;
border-radius: 12px;
}
> :deep(ul) {
@include list-parent;
&__hero-source {
@include tiny-text;
line-height: 25px;
}
&__header {
display: flex;
justify-content: flex-start;
align-items: stretch;
margin-bottom: $small-spacing;
}
&__meta-title {
@include meta-title;
margin-right: $medium-spacing;
}
&__intro-wrapper {
position: relative;
}
&__intro {
> :deep(p) {
margin-bottom: $large-spacing;
> li {
@include list-child;
@include lead-paragraph;
&:last-child {
margin-bottom: 0;
}
}
> :deep(ul) {
@include list-parent;
> li {
@include list-child;
@include lead-paragraph;
}
}
}
&__bookmark-actions {
margin-top: 3px;
}
&__objective-groups {
margin-bottom: 2 * $large-spacing;
}
}
&__bookmark-actions {
margin-top: 3px;
}
&__objective-groups {
margin-bottom: 2 * $large-spacing;
}
}
</style>

View File

@ -9,8 +9,6 @@
<section class="snapshot-header__section">
<h2 class="snapshot-header__subtitle">In diesem Snapshot sind {{ changesCount }} Anpassungen gespeichert:</h2>
<ul class="snapshot-header__list">
<li class="snapshot-header__list-item">{{ hiddenObjectives }} Lernziele wurden ausgeblendet</li>
<li class="snapshot-header__list-item">{{ newObjectives }} Lernziele wurde erfasst</li>
<li class="snapshot-header__list-item">{{ hiddenContentBlocks }} Inhaltsblöcke wurden ausgeblendet</li>
<li class="snapshot-header__list-item">{{ newContentBlocks }} Inhaltsblock wurde erfasst</li>
</ul>

View File

@ -0,0 +1,36 @@
from django.core.management import BaseCommand
from django.db.models import Count
from django.db.models.functions import ExtractYear
from books.models import Snapshot, ObjectiveGroupSnapshot
from objectives.models import ObjectiveSnapshot
# Query to group by creator's email, count the snapshots, and order by the count
class Command(BaseCommand):
def handle(self, *args, **options):
snapshots_grouped = (Snapshot.objects
.annotate(year=ExtractYear('created'))
.values('year', 'creator__email')
.annotate(count=Count('id'))
.order_by('year', '-count')) # Order by year and then by count (descending)
# To access the results
for snapshot in snapshots_grouped:
modified_email = snapshot['creator__email'].split('@')[0] + '@skillbox.ch'
print(f"Year: {snapshot['year']}, Creator Email: {modified_email}, Count: {snapshot['count']}")
hidden_objectives_counter = 0
custom_objectives_counter = 0
for snapshot in Snapshot.objects.all():
if snapshot.hidden_objectives.count() > 0:
hidden_objectives_counter += 1
if snapshot.custom_objectives.count() > 0:
custom_objectives_counter += 1
print(f"Hidden objectives: {hidden_objectives_counter}")
print(f"Custom objectives: {custom_objectives_counter}")
print(f"ObjectiveGroupSnapshot objectives: {ObjectiveGroupSnapshot.objects.count()}")
print(f"ObjectiveSnapshot objectives: {ObjectiveSnapshot.objects.count()}")

View File

@ -0,0 +1,225 @@
import json
from logging import getLogger
from django.core.management import BaseCommand
from books.management.commands.migrate_objectives_to_content import create_text_in_content_block, \
create_content_block_snapshot_from_objective, \
create_content_block_contents
from books.models import Chapter, ObjectiveGroupSnapshot, ContentBlockSnapshot, Snapshot, ChapterSnapshot
from books.models import ContentBlock
from books.models import Module
from objectives.models import Objective, ObjectiveSnapshot, ObjectiveGroup
logger = getLogger(__name__)
class Command(BaseCommand):
def handle(self, *args, **options):
"""
This command must be run after the migration of objectives to content blocks.
Migrate objective snapshots to content blocks
- Verlagsinhalte - deafult content blocks are referced in the snapshot by foreign key
Man muss unterscheiden zwischen, snapshots die nur Verlagslernziele sichtbar und unsichtbar machen.
Und solchen die auch benutzerdefinierte Lernziele sichtbar und unsichtbar machen.
Es gibt keine custom objective groups!
Es gibt keine hidden custom objective_groups
Case1:
- 100% Verlagslernziele, 100% Verlagsgruppe, nicht neue content blocks erstellen, nichts hidden... migration muss nichts machen.
Case2:
- 100% Verlagslernziele, 100% Verlagsgruppe, hidden gruppe, nur verlagsinhalt gruppe verstecken
Case3:
- Bestende gruppe mit benutzerdefinierten lernzielen, - custom content_block snapshot erstellen.
- Einzelne verslagslernziele sind versteckt. - custom content_block snapshot erstellen.
--- user created setzen bei custom content snapshot.
"""
prefix = "SNAP "
ContentBlock.objects.filter(title__startswith=prefix).delete()
Chapter.objects.filter(title__startswith=prefix).delete()
ContentBlockSnapshot.objects.filter(title__startswith=prefix).delete()
ChapterSnapshot.objects.filter(chapter__title__startswith=prefix).delete()
analyze()
migrate_snapshots()
def migrate_snapshots():
count = 0
case1_count = 0
case2_count = 0
case3_count = 0
createed_content_blocks = 0
visible_objectives_by_ids = {}
snapshot_counter = 0
for module in Module.objects.all():
for snapshot in Snapshot.objects.filter(module=module):
group_counter = snapshot.objective_groups.through.objects.filter(objective_group__module=module,
snapshot=snapshot).count()
print(
f"{snapshot_counter} Snapshot id: {snapshot.id} Module: {module.title} {group_counter} groups {snapshot.creator} {snapshot.title}")
snapshot_counter += 1
for objective_group_snapshot in snapshot.objective_groups.through.objects.filter(
objective_group__module=module, snapshot=snapshot):
header = f"{count} {module.title:50} {objective_group_snapshot.objective_group.get_title_display():25} {str(snapshot.creator):40} {objective_group_snapshot.hidden} "
count += 1
objective_group = objective_group_snapshot.objective_group
hidden_default_objectives = snapshot.hidden_objectives.filter(
group=objective_group)
visible_custom_objectives = snapshot.custom_objectives.filter(snapshot=snapshot, hidden=False,
group=objective_group_snapshot.objective_group)
group_is_hidden = objective_group_snapshot.hidden
info = f"{hidden_default_objectives.count()} {visible_custom_objectives.count()}"
if (not hidden_default_objectives and not visible_custom_objectives and not group_is_hidden):
# print(f"{info} Case 1 - skip")
case1_count += 1
break
if not hidden_default_objectives and not visible_custom_objectives and group_is_hidden:
print(header + f"Case 2 - {info} hide default content group")
case2_count += 1
content_block = get_content_block_by_objective_group(objective_group_snapshot,
module, False, None)
snapshot.hidden_content_blocks.add(content_block.id)
snapshot.save()
if hidden_default_objectives or visible_custom_objectives:
print(header + f"Case 3 - {info} create custom content blocks")
case3_count += 1
# Verlags Lernziele
visible_default_objectives = get_visible_default_objectives(objective_group, module, snapshot)
# Benutzerdefinierte Lernziele
visible_custom_objectives = list(snapshot.custom_objectives.filter(hidden=False))
visible_objectives = visible_default_objectives + visible_custom_objectives
# filter for unique texts in objectives
# TODO: I don't know why there are duplicated objectives
objectives_by_texts = {}
for objective in visible_objectives:
if objective.text not in objectives_by_texts:
objectives_by_texts[objective.text] = objective
visible_objectives = list(objectives_by_texts.values())
if visible_objectives:
# make combinations of objectives unique by text, this prevents generation of many duplicated content blocks
visible_objectives_hash = hash(
[objective.text for objective in visible_objectives].__str__())
visible_objectives_by_ids[visible_objectives_hash] = visible_objectives
for objectives in visible_objectives_by_ids.values():
print("")
for objective in objectives:
print(f" Objective: {objective.group} {objective} owner:{objective.owner}")
print("-")
print(f" visible_objectives_by_ids: {len(visible_objectives_by_ids.items())}")
# create custom content blocks with the objectives
created_content_blocks = 0
chapter = module.get_first_child()
if "Lernziele" not in chapter.title:
raise Exception(f"Chapter does not contain 'Lernziele' first title is {chapter.title}")
# Owner des custom blocks festlegen
custom_content_block_snapshot = create_content_block_snapshot_from_objective(
objective_group, chapter, snapshot,
owner=snapshot.creator)
# Hide default content block for this objective group, since custom content block is created
default_content_block = get_default_content_block(objective_group_snapshot, module)
if default_content_block:
print(f"Default content block: {default_content_block.title}")
snapshot.hidden_content_blocks.add(default_content_block.id)
if list(visible_objectives_by_ids.values()):
objectives = list(visible_objectives_by_ids.values())[0]
create_text_in_content_block(objectives, custom_content_block_snapshot, get_or_create=True)
created_content_blocks += 1
snapshot.save()
print()
print(f"Skipped {case1_count} Case 1")
print(f"Hidden default content groups {case2_count} Case 2")
print(f"Created new content {case3_count} Case 3")
def get_content_block_by_objective_group(objective_group_snapshot, module, user_created: bool, user):
# TODO: review qustion, is it necessary to filter for module
content_block = ContentBlock.objects.filter(
title__contains=objective_group_snapshot.objective_group.get_title_display(),
user_created=user_created, owner=user).descendant_of(module)
if content_block.count() > 1:
return content_block.first()
return content_block.first()
def get_default_content_block(objective_group_snapshot, module):
default_objectives = Objective.objects.filter(group=objective_group_snapshot.objective_group,
group__module=module,
owner__isnull=True,
objectivesnapshot__isnull=True)
default_content_block_contents = create_content_block_contents(default_objectives)
contents = json.loads(default_content_block_contents)
contents[0].get("value").get("text")
chapter = Chapter.objects.filter(title__contains="Lernziele").descendant_of(module)
text_contents = contents[0].get("value").get("text")
group_title = objective_group_snapshot.objective_group.get_title_display()
content_blocks = ContentBlock.objects.filter(user_created=False).descendant_of(chapter.first())
for content_block in content_blocks:
print(content_block.title, group_title)
if group_title in content_block.title:
return content_block
if not content_block.exists():
raise Exception("Content block does not exist ")
def get_visible_default_objectives(objective_group, module, snapshot):
default_objectives = Objective.objects.filter(group=objective_group,
group__module=module,
owner__isnull=True,
objectivesnapshot__isnull=True)
hidden_default_objectives = snapshot.hidden_objectives.filter(group=objective_group)
visible_default_objectives = [objective for objective in default_objectives if
objective.id not in hidden_default_objectives.values_list("id",
flat=True)]
return visible_default_objectives
def analyze():
print(f"""
OjectiveGroups: {ObjectiveGroup.objects.count()}
Objectives: {Objective.objects.count()}
ObjectiveGroupSnapshots: {ObjectiveGroupSnapshot.objects.count()}
ObjectivesSnapshots: {ObjectiveSnapshot.objects.count()}
ObjectiveGroups: {ObjectiveGroup.objects.filter(objectivegroupsnapshot__isnull=True).count()}
Objectives: {Objective.objects.filter(objectivesnapshot__isnull=True).count()}
Snapshot: {Snapshot.objects.count()}
""")

View File

@ -0,0 +1,280 @@
import json
from logging import getLogger
from django.core.management import BaseCommand
from django.core.exceptions import ValidationError
from books.models import Chapter, ObjectiveGroupSnapshot, Snapshot, ContentBlockSnapshot, ChapterSnapshot
from books.models import ContentBlock
from books.models import Module
from objectives.models import ObjectiveSnapshot, Objective, ObjectiveGroup
logger = getLogger(__name__)
class Command(BaseCommand):
def handle(self, *args, **options):
ContentBlock.objects.filter(title__startswith="TESTOBJECTIVE").delete()
ContentBlock.objects.filter(title__startswith="CUSTOM").delete()
Chapter.objects.filter(title__startswith="TESTOBJECTIVE").delete()
ContentBlock.objects.filter(title__startswith="XXX").delete()
Chapter.objects.filter(title__startswith="XXX").delete()
migrate_objectives_to_content()
def migrate_objectives_to_content():
created_content_blocks = 0
failed_modules = []
# This dict stores all content blocks that have been created for a set of objectives
# In this way we can reuse content blocks for the same set of objectives
created_default_content_blocks = {}
for module in Module.objects.all():
try:
chapter = create_chapter_from_objective_group(module)
for objective_group in module.objective_groups.all().order_by('title'):
default_objectives = list(objective_group.objectives.filter(owner__isnull=True, )
.exclude(objectivesnapshot__isnull=False).order_by('order'))
default_objectives_ids = tuple(objective.id for objective in default_objectives)
# Create "Verlagsinhalte" content block if it does not exist yet
if default_objectives_ids not in created_default_content_blocks:
default_content_block = create_default_content(objective_group, chapter)
created_default_content_blocks[default_objectives_ids] = default_content_block
else:
default_content_block = created_default_content_blocks[default_objectives_ids]
custom_objectives_by_owner = get_objectives_by_owner(objective_group)
if default_objectives or custom_objectives_by_owner:
contentblocks_by_merged_objectives_ids = {}
# cor custom objectives iterate over owners,
# - one ownsers custom objectives must not be changed by another owner
# - visibility is set per class
for owner, owner_objectives in custom_objectives_by_owner.items():
print(f"Owner: {owner}")
print(f" Objectives: ")
visible_default_objectives_by_class = filter_visible_objectives_by_class(default_objectives, owner)
for school_class, default_objectives_for_class in visible_default_objectives_by_class.items():
custom_content_block = None
print(f" School class: {school_class}")
# merge "Verlagsinhalte" and "benutzerdefinierte Inhalte"
visible_owner_objectives = [objective for objective in owner_objectives if not objective.is_hidden_for_class(school_class)]
merged_objectives = default_objectives_for_class + visible_owner_objectives
merged_objectives_ids = tuple(objective.id for objective in merged_objectives)
is_default_content = merged_objectives_ids == default_objectives_ids
if is_default_content:
print(f" Objective: Reuse default content block")
# custom_content_block = default_content_block
# Create content block if that set of objectives has not been created yet
if merged_objectives_ids not in contentblocks_by_merged_objectives_ids and not is_default_content:
for objective in merged_objectives:
print(f" Objective: {objective} {objective.owner}")
if merged_objectives_ids:
custom_content_block = create_content_block_from_objective(objective_group,
chapter,
owner=owner,
)
contentblocks_by_merged_objectives_ids[
merged_objectives_ids] = custom_content_block
create_text_in_content_block(merged_objectives, custom_content_block)
created_content_blocks += 1
else:
if not is_default_content:
print(f" Objective: Reuse content block")
custom_content_block = contentblocks_by_merged_objectives_ids[
merged_objectives_ids]
else:
print(f" Objective: Reuse default content block")
# set visibility
# hide default objectives if custom objectives exist
if custom_content_block:
default_content_block.hidden_for.add(school_class)
default_content_block.save_revision().publish()
default_content_block.save()
custom_content_block.visible_for.add(school_class)
custom_content_block.save_revision().publish()
custom_content_block.save()
if objective_group.hidden_for.filter(id=school_class.id).exists():
default_content_block.hidden_for.add(school_class)
default_content_block.save_revision().publish()
default_content_block.save()
except ValidationError as e:
print(f"Error with module {module}")
logger.error(e)
failed_modules.append(module)
print(f"Created {created_content_blocks} content blocks")
print(f"Failed modules: {len(failed_modules)}")
for module in failed_modules:
print(f"Faile module: {module}")
def create_default_content(objective_group, chapter):
"""Create Verlagsinhalt Lernziele"""
print(f" Objective group: {objective_group}")
print(" Default objectives:")
default_objectives = list(
objective_group.objectives.filter(owner__isnull=True, objectivesnapshot__isnull=True).order_by('id'))
default_content_block = create_content_block_from_objective(objective_group, chapter)
create_text_in_content_block(default_objectives, default_content_block)
for objective in default_objectives:
print(f" Objective: {objective} {objective.owner}")
return default_content_block
def filter_visible_objectives_by_class(objectives, user):
school_classes = user.school_classes.all()
visible_objectives = {}
for school_class in school_classes:
if school_class not in visible_objectives:
visible_objectives[school_class] = []
objectives_vis = [objective for objective in objectives if not objective.is_hidden_for_class(school_class)]
visible_objectives[school_class].extend(objectives_vis)
return visible_objectives
def get_objectives_by_owner(objective_group, exclude_snapshots=True):
custom_objectives = objective_group.objectives.filter(owner__isnull=False, objectivesnapshot__isnull=True).order_by('order')
custom_objectives_by_owner = {}
for objective in custom_objectives:
owner = objective.owner
if owner not in custom_objectives_by_owner:
custom_objectives_by_owner[owner] = []
custom_objectives_by_owner[owner].append(objective)
# add owners with hidden default objectives to custom objectives, needed for further processing
hidden_default_objectives = list(
objective_group.objectives.filter(owner__isnull=True, objectivesnapshot__isnull=True, hidden_for__isnull=False))
for hidden_default_objective in hidden_default_objectives:
for school_class in hidden_default_objective.hidden_for.all():
for teacher in school_class.get_teachers():
if teacher not in custom_objectives_by_owner:
custom_objectives_by_owner[teacher] = []
return custom_objectives_by_owner
def create_chapter_from_objective_group(module, prefix="XXX "):
chapter = Chapter(title=f"Lernziele")
first_sibling = module.get_first_child()
if first_sibling is not None:
first_sibling.add_sibling(instance=chapter, pos='left')
chapter.save_revision().publish()
chapter.save()
return chapter
def create_chapter_snapshot_from_objective_group(module, snapshot, prefix="XXX "):
chapter = Chapter.objects.filter(parent=module, title=f"Lernziele").first()
chapter_snapshot = ChapterSnapshot(title=f"Lernziele", snapshot=snapshot)
first_sibling = module.get_first_child()
if first_sibling is not None:
first_sibling.add_sibling(instance=chapter_snapshot, pos='left')
return chapter
def create_content_block_from_objective(objective_group, chapter, owner=None):
content_block = ContentBlock(
title=f"{objective_group.get_title_display()}",
type="normal",
owner=owner,
user_created=owner is not None,
original_creator=owner
)
chapter.add_child(instance=content_block)
return content_block
def create_content_block_snapshot_from_objective(objective_group, chapter, snapshot, owner=None):
content_block_snapshot = ContentBlockSnapshot(
title=f"{objective_group.get_title_display()}",
type="normal",
owner=owner,
user_created=owner is not None,
snapshot=snapshot
)
chapter.add_child(instance=content_block_snapshot)
return content_block_snapshot
def create_text_in_content_block(objectives, content_block, get_or_create=False):
objectives = list(objectives)
content_block.contents = create_content_block_contents(objectives)
if get_or_create:
content_block_qs = ContentBlock.objects.filter(title=content_block.title,
owner=content_block.owner,
user_created=content_block.user_created,
contents=content_block.contents)
if content_block_qs.exists():
content_block = content_block_qs.first()
content_block.save_revision().publish()
return content_block
def create_content_block_contents(objectives):
objectives = list(objectives)
objective_li = [f"<li>{objective.text}</li>" for objective in objectives if objective.text]
texts = [{'type': 'text_block',
'value': {
'text': f"<ul>{''.join(str(i) for i in objective_li)}</ul>"
}}]
contents = json.dumps(texts)
return contents
def analyze():
print(f"""
OjectiveGroups: {ObjectiveGroup.objects.count()}
Objectives: {Objective.objects.count()}
ObjectiveGroupSnapshots: {ObjectiveGroupSnapshot.objects.count()}
ObjectivesSnapshots: {ObjectiveSnapshot.objects.filter(hidden=True).count()}
ObjectiveGroups: {ObjectiveGroup.objects.filter(objectivegroupsnapshot__isnull=True).count()}
Objectives: {Objective.objects.filter(objectivesnapshot__isnull=True).count()}
Snapshot: {Snapshot.objects.count()}
""")
def clean_snapshots():
""" utility function to clean up snapshots """
emails = ["mia.teacher", "simone.gerber", "pascal.sigg", "dario.aebersold", "steph-teacher"]
for snapshot in Snapshot.objects.all():
for email in emails:
if email in snapshot.creator.email:
print(f"Deleting snapshot of user {email}")
snapshot.delete()

View File

@ -54,9 +54,11 @@ class SnapshotManager(models.Manager):
description_hidden=chapter.description_hidden_for.filter(id=school_class.id).exists()
)
base_qs = ContentBlock.get_by_parent(chapter).filter(contentblocksnapshot__isnull=True)
# Verlagsinhalte
for content_block in base_qs.filter(user_created=False):
if content_block.hidden_for.filter(id=school_class.id).exists():
snapshot.hidden_content_blocks.add(content_block)
# Benutzerdefinierte Inhalte
for content_block in base_qs.filter(Q(user_created=True) & Q(owner=user)):
new_content_block = ContentBlockSnapshot(
hidden=content_block.is_hidden_for_class(school_class),
@ -78,9 +80,11 @@ class SnapshotManager(models.Manager):
hidden=objective_group.hidden_for.filter(id=school_class.id).exists(),
)
base_qs = objective_group.objectives.filter(objectivesnapshot__isnull=True)
# Verlagslernziele
for objective in base_qs.filter(owner__isnull=True):
if objective.hidden_for.filter(id=school_class.id).exists():
snapshot.hidden_objectives.add(objective)
# Benutzerdefinierte Lernziele
for objective in base_qs.filter(owner=user):
ObjectiveSnapshot.objects.create(
hidden=objective.is_hidden_for_class(school_class=school_class),
@ -152,10 +156,3 @@ class Snapshot(models.Model):
chapter.title_hidden_for.add(selected_class)
if chapter_snapshot.description_hidden:
chapter.description_hidden_for.add(selected_class)
for objective_group_snapshot in self.objective_groups.through.objects.all():
if objective_group_snapshot.hidden:
objective_group_snapshot.objective_group.hidden_for.add(selected_class)
for objective in self.hidden_objectives.all():
objective.hidden_for.add(selected_class)
for custom_objective in self.custom_objectives.all():
custom_objective.to_regular_objective(owner=user, school_class=selected_class)

View File

@ -22,6 +22,7 @@ query ModulesQuery($slug: String, $id: ID) {
contentBlocks {
id
title
userCreated
originalCreator {
id
fullName
@ -33,6 +34,7 @@ query ModulesQuery($slug: String, $id: ID) {
hiddenFor {
name
}
contents
}
}
}

View File

@ -0,0 +1,111 @@
from django.test import RequestFactory
from graphene.test import Client
from graphql_relay import to_global_id, from_global_id
from api.schema import schema
from api.utils import get_object
from books.factories import ModuleFactory, ChapterFactory, ContentBlockFactory
from books.management.commands.migrate_objective_snapshots import migrate_snapshots
from books.management.commands.migrate_objectives_to_content import migrate_objectives_to_content
from books.models import Snapshot, ChapterSnapshot
from books.tests.queries import MODULE_QUERY, SNAPSHOT_MODULE_QUERY, CREATE_SNAPSHOT_MUTATION, APPLY_SNAPSHOT_MUTATION, \
MODULE_SNAPSHOTS_QUERY, SHARE_SNAPSHOT_MUTATION, UPDATE_SNAPSHOT_MUTATION, DELETE_SNAPSHOT_MUTATION
from core.tests.base_test import SkillboxTestCase
from objectives.factories import ObjectiveGroupFactory, ObjectiveFactory
from users.factories import SchoolClassFactory
from users.models import User, SchoolClass
class TestObjectivesMigration(SkillboxTestCase):
@property
def graphene_client(self):
return self.get_client()
def setUp(self):
self.createDefault()
self.client = self.get_client()
# teacher will create snapshot
self.slug = 'some-module'
self.module = ModuleFactory(slug=self.slug)
self.skillbox_class = SchoolClass.objects.get(name='skillbox')
self.teacher2 = User.objects.get(username='teacher2')
self.second_class_name = 'second_class'
self.second_class = SchoolClass.objects.get(name=self.second_class_name)
self.admin = User.objects.get(username='admin')
# we make a snapshot S of the module M
# snapshot S looks like module M for school class X
# module M has a chapter
self.chapter = ChapterFactory(parent=self.module, slug='some-chapter', owner=self.admin)
ChapterFactory(parent=self.module, slug='some-other-chapter', owner=self.admin)
objective_group = ObjectiveGroupFactory(module=self.module, title='Gesellschaft')
second_objective_group = ObjectiveGroupFactory(module=self.module, title='Sprache & Kommunikation')
self.visible_objective = ObjectiveFactory(text='visible-objective', group=objective_group)
self.hidden_objective = ObjectiveFactory(text='hidden-objective', group=objective_group)
self.custom_objective = ObjectiveFactory(text='custom-objective', group=objective_group, owner=self.teacher)
self.custom_hidden_objective = ObjectiveFactory(text='custom-hidden-objective', group=objective_group,
owner=self.teacher)
self.visible_objective = ObjectiveFactory(text='objective1', group=second_objective_group)
self.hidden_objective.hidden_for.add(self.skillbox_class)
self.hidden_objective.save()
self.custom_objective.visible_for.add(self.skillbox_class)
self.custom_objective.save()
second_objective_group.hidden_for.add(self.skillbox_class)
second_objective_group.save()
self.custom_hidden_objective.visible_for.remove(self.skillbox_class)
migrate_objectives_to_content()
def test_objectives_migration_hidden_default_content(self):
result = self.client.execute(MODULE_QUERY, variables={
'slug': self.module.slug
})
module = result.data['module']
chapter1 = module['chapters'][0]
default_content, _, _ = chapter1['contentBlocks']
# default content block (Verlagsinhalte) exists but is hidden (since one objective is hidden for this class)
self.assertEqual(default_content['title'], 'Gesellschaft')
self.assertEqual(default_content['originalCreator'], None)
self.assertEqual(default_content['contents'][0]['value']['text'],
'<ul><li>visible-objective</li><li>hidden-objective</li></ul>')
self.assertEqual(default_content['hiddenFor'], [{'name': 'skillbox'}])
def test_objectives_migration_hidden_default_content_creates_custom_content(self):
result = self.client.execute(MODULE_QUERY, variables={
'slug': self.module.slug
})
module = result.data['module']
chapter1 = module['chapters'][0]
_, custom, _ = chapter1['contentBlocks']
# default content block (Verlagsinhalte) exists but is hidden (since one objective is hidden for this class)
self.assertEqual(custom['title'], 'Gesellschaft')
self.assertTrue(custom['originalCreator'] is not None)
self.assertEqual(custom['hiddenFor'], [])
self.assertEqual(custom['visibleFor'], [{'name': 'skillbox'}])
self.assertEqual(custom['contents'][0]['value']['text'],
'<ul><li>visible-objective</li><li>custom-objective</li></ul>')
def test_objectives_migration_hidden_default_content_creates_hidden_content_block(self):
result = self.client.execute(MODULE_QUERY, variables={
'slug': self.module.slug
})
module = result.data['module']
chapter1 = module['chapters'][0]
_, _, hidden_custom = chapter1['contentBlocks']
# default content block (Verlagsinhalte) exists but is hidden (since one objective is hidden for this class)
self.assertEqual(hidden_custom['title'], 'Sprache & Kommunikation')
self.assertTrue(hidden_custom['originalCreator'] is None)
self.assertEqual(hidden_custom['hiddenFor'], [])
self.assertEqual(hidden_custom['visibleFor'], [])
self.assertEqual(hidden_custom['contents'][0]['value']['text'], '<ul><li>objective1</li></ul>')

View File

@ -110,25 +110,6 @@ class CreateSnapshotTestCase(SkillboxTestCase):
self.assertTrue(
school_class_name in [school_class['name'] for school_class in
custom_content_block.get('visibleFor')])
objectives = module['objectiveGroups'][0]['objectives']
self.assertEqual(len(objectives), 4)
hidden_objective = [objective for objective in objectives if
objective['text'] == self.hidden_objective.text][0]
custom_objective = [objective for objective in objectives if
objective['text'] == self.custom_objective.text][0]
# check if hidden objective is hidden for this school class
self.assertTrue(
school_class_name in [school_class['name'] for school_class in
hidden_objective.get('hiddenFor')])
# check if the custom objective is visible for this school class
self.assertTrue(
school_class_name in [school_class['name'] for school_class in
custom_objective.get('visibleFor')])
return module
def _compare_content_blocks(self, content_blocks):

View File

@ -0,0 +1,195 @@
from django.test import RequestFactory
from graphene.test import Client
from graphql_relay import to_global_id, from_global_id
from api.schema import schema
from api.utils import get_object
from books.factories import ModuleFactory, ChapterFactory, ContentBlockFactory
from books.management.commands.migrate_objective_snapshots import migrate_snapshots
from books.management.commands.migrate_objectives_to_content import migrate_objectives_to_content
from books.models import Snapshot, ChapterSnapshot, ContentBlock
from books.tests.queries import MODULE_QUERY, SNAPSHOT_MODULE_QUERY, CREATE_SNAPSHOT_MUTATION, APPLY_SNAPSHOT_MUTATION, \
MODULE_SNAPSHOTS_QUERY, SHARE_SNAPSHOT_MUTATION, UPDATE_SNAPSHOT_MUTATION, DELETE_SNAPSHOT_MUTATION
from core.tests.base_test import SkillboxTestCase
from objectives.factories import ObjectiveGroupFactory, ObjectiveFactory
from users.factories import SchoolClassFactory
from users.models import User, SchoolClass
class TestSnapshotMigration(SkillboxTestCase):
@property
def graphene_client(self):
return self.get_client()
def setUp(self):
self.createDefault()
self.client = self.get_client()
# teacher will create snapshot
self.slug = 'some-module'
self.module = ModuleFactory(slug=self.slug)
self.skillbox_class = SchoolClass.objects.get(name='skillbox')
self.teacher2 = User.objects.get(username='teacher2')
self.second_class_name = 'second_class'
self.second_class = SchoolClass.objects.get(name=self.second_class_name)
self.admin = User.objects.get(username='admin')
# we make a snapshot S of the module M
# snapshot S looks like module M for school class X
# module M has a chapter
self.chapter = ChapterFactory(parent=self.module, slug='some-chapter', owner=self.admin)
ChapterFactory(parent=self.module, slug='some-other-chapter', owner=self.admin)
objective_group = ObjectiveGroupFactory(module=self.module, title='Gesellschaft')
second_objective_group = ObjectiveGroupFactory(module=self.module, title='Sprache & Kommunikation')
self.visible_objective = ObjectiveFactory(text='visible-objective', group=objective_group)
self.visible_objective_2 = ObjectiveFactory(text='hidden-objective', group=objective_group)
self.custom_objective = ObjectiveFactory(text='custom-objective', group=objective_group, owner=self.teacher)
self.custom_hidden_objective = ObjectiveFactory(text='custom-hidden-objective', group=objective_group,
owner=self.teacher)
self.visible_objective = ObjectiveFactory(text='objective1', group=second_objective_group)
self.custom_objective.visible_for.add(self.skillbox_class)
self.custom_objective.save()
second_objective_group.hidden_for.add(self.skillbox_class)
second_objective_group.save()
self.custom_hidden_objective.visible_for.remove(self.skillbox_class)
self.snapshot1 = Snapshot.objects.create_snapshot(self.module, self.skillbox_class, self.teacher)
migrate_objectives_to_content()
migrate_snapshots()
# Change visibility of objectives resp. content blocks, hide all
for content_block in ContentBlock.objects.all().descendant_of(self.chapter):
if content_block.owner is None:
content_block.hidden_for.add(self.skillbox_class)
else:
content_block.visible_for.remove(self.skillbox_class)
content_block.save()
def test_snapshot_migration_dfault_content_pre_apply(self):
result = self.client.execute(MODULE_QUERY, variables={
'slug': self.module.slug
})
module = result.data['module']
chapter1 = module['chapters'][0]
default_content, _, _ = chapter1['contentBlocks']
# default content block (Verlagsinhalte) exists but is hidden (since one objective is hidden for this class)
self.assertEqual(default_content['title'], 'Gesellschaft')
self.assertEqual(default_content['originalCreator'], None)
self.assertEqual(default_content['contents'][0]['value']['text'],
'<ul><li>visible-objective</li><li>hidden-objective</li></ul>')
self.assertEqual(default_content['hiddenFor'], [{'name': 'skillbox'}])
def test_snapshot_migration_hidden_default_content_creates_custom_content_pre_apply(self):
result = self.client.execute(MODULE_QUERY, variables={
'slug': self.module.slug
})
module = result.data['module']
chapter1 = module['chapters'][0]
_, custom, _ = chapter1['contentBlocks']
self.assertEqual(custom['title'], 'Gesellschaft')
self.assertTrue(custom['originalCreator'] is not None)
self.assertEqual(custom['hiddenFor'], [])
self.assertEqual(custom['visibleFor'], [])
self.assertEqual(custom['contents'][0]['value']['text'],
'<ul><li>visible-objective</li><li>hidden-objective</li><li>custom-objective</li></ul>')
def test_snapshot_migration_hidden_content_block_pre_apply(self):
result = self.client.execute(MODULE_QUERY, variables={
'slug': self.module.slug
})
module = result.data['module']
chapter1 = module['chapters'][0]
_, _, hidden_custom = chapter1['contentBlocks']
# default content block (Verlagsinhalte) exists but is hidden (since one objective is hidden for this class)
self.assertEqual(hidden_custom['title'], 'Sprache & Kommunikation')
self.assertTrue(hidden_custom['originalCreator'] is None)
self.assertEqual(hidden_custom['hiddenFor'], [{'name': 'skillbox'}])
self.assertEqual(hidden_custom['visibleFor'], [])
self.assertEqual(hidden_custom['contents'][0]['value']['text'], '<ul><li>objective1</li></ul>')
def test_snapshot_migration_default_apply_snapshot(self):
# apply snapshot
self.snapshot1.apply(self.teacher, self.school_class)
result = self.client.execute(MODULE_QUERY, variables={
'slug': self.module.slug
})
module = result.data['module']
chapter1 = module['chapters'][0]
default_content, _, _, _ = chapter1['contentBlocks']
# default content block (Verlagsinhalte) exists but is hidden (since one objective is hidden for this class)
self.assertEqual(default_content['title'], 'Gesellschaft')
self.assertFalse(default_content['userCreated'])
self.assertEqual(default_content['originalCreator'], None)
self.assertEqual(default_content['contents'][0]['value']['text'],
'<ul><li>visible-objective</li><li>hidden-objective</li></ul>')
self.assertEqual(default_content['hiddenFor'], [{'name': 'skillbox'}])
def test_snapshot_migration_hidden_custom_content_apply_snapshot(self):
# custom content from bevore the snapshot must be hidden (visible for nobody)
self.snapshot1.apply(self.teacher, self.school_class)
result = self.client.execute(MODULE_QUERY, variables={
'slug': self.module.slug
})
module = result.data['module']
chapter1 = module['chapters'][0]
_, custom, _, _ = chapter1['contentBlocks']
self.assertEqual(custom['title'], 'Gesellschaft')
self.assertTrue(custom['userCreated'])
self.assertTrue(custom['originalCreator'] is not None)
self.assertEqual(custom['hiddenFor'], [])
self.assertEqual(custom['visibleFor'], [])
self.assertEqual(custom['contents'][0]['value']['text'],
'<ul><li>visible-objective</li><li>hidden-objective</li><li>custom-objective</li></ul>')
def test_snapshot_migration_hidden_content_block_apply_snapshot_2(self):
# custom content from bevore the snapshot must be hidden (visible for nobody)
self.snapshot1.apply(self.teacher, self.school_class)
result = self.client.execute(MODULE_QUERY, variables={
'slug': self.module.slug
})
module = result.data['module']
chapter1 = module['chapters'][0]
_, _, hidden_custom, _ = chapter1['contentBlocks']
# default content block (Verlagsinhalte) exists but is hidden (since one objective is hidden for this class)
self.assertEqual(hidden_custom['title'], 'Sprache & Kommunikation')
self.assertFalse(hidden_custom['userCreated'])
self.assertTrue(hidden_custom['originalCreator'] is None)
self.assertEqual(hidden_custom['hiddenFor'], [{'name': 'skillbox'}])
self.assertEqual(hidden_custom['visibleFor'], [])
self.assertEqual(hidden_custom['contents'][0]['value']['text'], '<ul><li>objective1</li></ul>')
def test_snapshot_migration_visible_content_snapshot_new_content(self):
# the applicaiton of a snapshot hides old custom content, and creates new custom content for the visible stuff
self.snapshot1.apply(self.teacher, self.school_class)
result = self.client.execute(MODULE_QUERY, variables={
'slug': self.module.slug
})
module = result.data['module']
chapter1 = module['chapters'][0]
_, _, _, new_content_block = chapter1['contentBlocks']
self.assertEqual(new_content_block['title'], 'Gesellschaft')
self.assertTrue(new_content_block['userCreated'])
self.assertTrue(new_content_block['originalCreator'] is None)
self.assertEqual(new_content_block['hiddenFor'], [])
self.assertEqual(new_content_block['visibleFor'], [{'name': 'skillbox'}])
self.assertEqual(new_content_block['contents'][0]['value']['text'],
'<ul><li>visible-objective</li><li>hidden-objective</li><li>custom-objective</li></ul>')

View File

@ -1,3 +1,6 @@
from concurrent.futures import ThreadPoolExecutor
from datetime import timedelta, datetime
from django.contrib.auth import get_user_model
from django.core.management import BaseCommand
@ -9,9 +12,32 @@ class Command(BaseCommand):
self.stdout.write("If so, type \"YES\"")
result = input()
user_model = get_user_model()
if result == 'YES':
users = get_user_model().objects.all()
for user in users:
user.set_password('test')
user.save()
users = user_model.objects.all().order_by('email')
with ThreadPoolExecutor(max_workers=5) as executor:
executor.map(process_user, users)
def process_user(usr):
# replace domain with id.skillbox.ch,to ensure unique emails
usr.email = usr.email.split('@')[0] + f'@skillbox.ch'
user_model = get_user_model()
if user_model.objects.filter(email=usr.email).exists():
usr.email = usr.email.split('@')[0] + f'@{usr.id}.skillbox.ch'
usr.username = usr.email
print(usr.email)
# make license valid for 1 year
now = datetime.now()
if usr.license_expiry_date:
usr.license_expiry_date = now + timedelta(days=365)
# set password to test
usr.set_password('test')
usr.save()

View File

@ -158,6 +158,9 @@ AUTH_USER_MODEL = "users.User"
WEAK_PASSWORDS = DEBUG
if WEAK_PASSWORDS:
AUTH_PASSWORD_VALIDATORS = []
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.MD5PasswordHasher",
]
else:
AUTH_PASSWORD_VALIDATORS = [
{

View File

@ -0,0 +1,12 @@
pg_restore --verbose --clean --no-acl --no-owner -h localhost -U skillbox -d skillbox latest.dump
python manage.py migrate
python manage.py reset_all_passwords
pg_dump -Fc --no-acl -h localhost -U skillbox skillbox > latest-anonymized.dump
python manage.py migrate_objectives_to_content
pg_dump -Fc --no-acl -h localhost -U skillbox skillbox > latest-migrated-objectives.dump
python manage.py migrate_objective_snapshots
pg_dump -Fc --no-acl -h localhost -U skillbox skillbox > latest-migrated-objectives-and-snapshots.dump
#Use this command to restore the database from the dump:
#pg_restore --verbose --clean --no-acl --no-owner -h localhost -U skillbox -d skillbox latest-migrated-objectives.dump