diff --git a/dev.db b/dev.db index 7b5ad803e..d9c472835 100644 Binary files a/dev.db and b/dev.db differ diff --git a/openedx_learning/apps/core/learning_partitioning/models.py b/openedx_learning/apps/core/learning_partitioning/models.py index 7c42fbb05..7ff45a9ba 100644 --- a/openedx_learning/apps/core/learning_partitioning/models.py +++ b/openedx_learning/apps/core/learning_partitioning/models.py @@ -33,7 +33,7 @@ """ from django.db import models -from ..learning_publishing.models import LearningObjectVersion +# from ..learning_publishing.models import LearningObjectVersion from openedx_learning.lib.fields import hash_field, identifier_field, immutable_uuid_field @@ -62,7 +62,7 @@ class Block(models.Model): How do we decompose this? Separate tables for different aspects, like grades? """ - learning_object_version = models.ForeignKey(LearningObjectVersion, on_delete=models.CASCADE) +# learning_object_version = models.ForeignKey(LearningObjectVersion, on_delete=models.CASCADE) class Unit(models.Model): diff --git a/openedx_learning/apps/core/learning_publishing/admin.py b/openedx_learning/apps/core/learning_publishing/admin.py index 9c604889a..1ae74dc20 100644 --- a/openedx_learning/apps/core/learning_publishing/admin.py +++ b/openedx_learning/apps/core/learning_publishing/admin.py @@ -1,19 +1,44 @@ from django.contrib import admin - from .models import ( LearningContext, LearningContextVersion, - LearningContextBranch, - LearningAppVersionReport, - LearningAppContentError, + BlockType, + LearningContextBlock, + BlockContent, + BlockVersion, ) -# @admin.register(LearningContext) -# class LearningContextAdmin(admin.ModelAdmin): -# pass +@admin.register(LearningContext) +class LearningContextAdmin(admin.ModelAdmin): + pass + +@admin.register(LearningContextVersion) +class LearningContextVersionAdmin(admin.ModelAdmin): + pass + +@admin.register(BlockType) +class BlockTypeAdmin(admin.ModelAdmin): + pass + +@admin.register(LearningContextBlock) +class LearningContextBlockAdmin(admin.ModelAdmin): + pass + +@admin.register(BlockVersion) +class BlockVersionAdmin(admin.ModelAdmin): + readonly_fields = ( + 'title', + 'start_version_num', + 'end_version_num', + 'content', + 'block', + ) + -admin.site.register(LearningContext) -admin.site.register(LearningContextVersion) -admin.site.register(LearningContextBranch) -admin.site.register(LearningAppVersionReport) -admin.site.register(LearningAppContentError) +@admin.register(BlockContent) +class BlockContentAdmin(admin.ModelAdmin): + readonly_fields = ( + 'learning_context', + 'hash_digest', + 'data', + ) diff --git a/openedx_learning/apps/core/learning_publishing/migrations/0001_initial.py b/openedx_learning/apps/core/learning_publishing/migrations/0001_initial.py index e986682fe..7ac33cb30 100644 --- a/openedx_learning/apps/core/learning_publishing/migrations/0001_initial.py +++ b/openedx_learning/apps/core/learning_publishing/migrations/0001_initial.py @@ -1,11 +1,8 @@ -# Generated by Django 3.2.10 on 2022-01-19 17:04 +# Generated by Django 3.2.10 on 2022-03-19 12:33 -from django.conf import settings from django.db import migrations, models import django.db.models.deletion -import django.utils.timezone -import model_utils.fields -import simple_history.models +import uuid class Migration(migrations.Migration): @@ -13,332 +10,58 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name="LearningContext", + name='BlockType', fields=[ - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="created", - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="modified", - ), - ), - ("id", models.BigAutoField(primary_key=True, serialize=False)), - ("identifier", models.CharField(max_length=255, unique=True)), - ( - "created_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('major', models.CharField(help_text="Major types include 'block', 'unit', and 'sequence'.", max_length=50)), + ('minor', models.CharField(blank=True, max_length=50)), ], - options={ - "abstract": False, - }, ), migrations.CreateModel( - name="LearningContextType", + name='BlockVersion', fields=[ - ("id", models.BigAutoField(primary_key=True, serialize=False)), - ("identifier", models.CharField(max_length=100)), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('identifier', models.CharField(max_length=255)), + ('start_version_num', models.PositiveIntegerField()), + ('end_version_num', models.PositiveIntegerField(default=2147483647)), + ('title', models.CharField(blank=True, max_length=1000, null=True)), + ('data', models.TextField()), ], ), migrations.CreateModel( - name="LearningContextVersion", + name='LearningContext', fields=[ - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="created", - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="modified", - ), - ), - ("id", models.BigAutoField(primary_key=True, serialize=False)), - ("title", models.CharField(db_index=True, max_length=255)), - ( - "learning_context", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="learning_publishing.learningcontext", - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('identifier', models.CharField(max_length=255)), ], - options={ - "abstract": False, - }, ), migrations.CreateModel( - name="LearningContextBranch", + name='LearningContextVersion', fields=[ - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="created", - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="modified", - ), - ), - ("id", models.BigAutoField(primary_key=True, serialize=False)), - ("branch_name", models.CharField(max_length=100)), - ( - "learning_context", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="learning_publishing.learningcontext", - ), - ), - ( - "version", - models.ForeignKey( - on_delete=django.db.models.deletion.RESTRICT, - to="learning_publishing.learningcontextversion", - ), - ), - ], - ), - migrations.AddField( - model_name="learningcontext", - name="type", - field=models.ForeignKey( - on_delete=django.db.models.deletion.RESTRICT, - to="learning_publishing.learningcontexttype", - ), - ), - migrations.CreateModel( - name="LearningAppVersionReport", - fields=[ - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="created", - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="modified", - ), - ), - ("id", models.BigAutoField(primary_key=True, serialize=False)), - ("app_name", models.CharField(max_length=100)), - ("num_critical", models.PositiveIntegerField()), - ("num_errors", models.PositiveIntegerField()), - ("num_warnings", models.PositiveIntegerField()), - ( - "version", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="learning_publishing.learningcontextversion", - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('identifier', models.CharField(max_length=255)), + ('version_num', models.PositiveIntegerField()), ], ), migrations.CreateModel( - name="LearningAppContentError", + name='LearningContextBlock', fields=[ - ("id", models.BigAutoField(primary_key=True, serialize=False)), - ("error_code", models.CharField(max_length=100)), - ( - "level", - models.CharField( - choices=[ - ("critical", "critical"), - ("error", "error"), - ("warning", "warning"), - ], - max_length=10, - ), - ), - ("usage_key", models.CharField(max_length=255, null=True)), - ("data", models.JSONField()), - ( - "app_version_report", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="learning_publishing.learningappversionreport", - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('identifier', models.CharField(max_length=255)), + ('block_type', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='learning_publishing.blocktype')), + ('learning_context', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='learning_publishing.learningcontext')), ], ), - migrations.CreateModel( - name="HistoricalLearningContextBranch", - fields=[ - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="created", - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="modified", - ), - ), - ("id", models.BigIntegerField(blank=True, db_index=True)), - ("branch_name", models.CharField(max_length=100)), - ("history_id", models.AutoField(primary_key=True, serialize=False)), - ("history_date", models.DateTimeField()), - ("history_change_reason", models.CharField(max_length=100, null=True)), - ( - "history_type", - models.CharField( - choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], - max_length=1, - ), - ), - ( - "history_user", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "learning_context", - models.ForeignKey( - blank=True, - db_constraint=False, - null=True, - on_delete=django.db.models.deletion.DO_NOTHING, - related_name="+", - to="learning_publishing.learningcontext", - ), - ), - ( - "version", - models.ForeignKey( - blank=True, - db_constraint=False, - null=True, - on_delete=django.db.models.deletion.DO_NOTHING, - related_name="+", - to="learning_publishing.learningcontextversion", - ), - ), - ], - options={ - "verbose_name": "historical learning context branch", - "ordering": ("-history_date", "-history_id"), - "get_latest_by": "history_date", - }, - bases=(simple_history.models.HistoricalChanges, models.Model), - ), - migrations.CreateModel( - name="HistoricalLearningAppVersionReport", - fields=[ - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="created", - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, - editable=False, - verbose_name="modified", - ), - ), - ("id", models.BigIntegerField(blank=True, db_index=True)), - ("app_name", models.CharField(max_length=100)), - ("num_critical", models.PositiveIntegerField()), - ("num_errors", models.PositiveIntegerField()), - ("num_warnings", models.PositiveIntegerField()), - ("history_id", models.AutoField(primary_key=True, serialize=False)), - ("history_date", models.DateTimeField()), - ("history_change_reason", models.CharField(max_length=100, null=True)), - ( - "history_type", - models.CharField( - choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], - max_length=1, - ), - ), - ( - "history_user", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="+", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "version", - models.ForeignKey( - blank=True, - db_constraint=False, - null=True, - on_delete=django.db.models.deletion.DO_NOTHING, - related_name="+", - to="learning_publishing.learningcontextversion", - ), - ), - ], - options={ - "verbose_name": "historical learning app version report", - "ordering": ("-history_date", "-history_id"), - "get_latest_by": "history_date", - }, - bases=(simple_history.models.HistoricalChanges, models.Model), - ), - migrations.AddConstraint( - model_name="learningcontextbranch", - constraint=models.UniqueConstraint( - fields=("learning_context_id", "branch_name"), - name="one_branch_per_learning_context", - ), - ), migrations.AddConstraint( - model_name="learningappversionreport", - constraint=models.UniqueConstraint( - fields=("app_name", "version"), name="one_report_per_app_and_version" - ), + model_name='learningcontextblock', + constraint=models.UniqueConstraint(fields=('learning_context_id', 'identifier'), name='learning_publishing_lcb_one_identifier_per_lc'), ), ] diff --git a/openedx_learning/apps/core/learning_publishing/models.py b/openedx_learning/apps/core/learning_publishing/models.py index b36dc56b8..5e0d05209 100644 --- a/openedx_learning/apps/core/learning_publishing/models.py +++ b/openedx_learning/apps/core/learning_publishing/models.py @@ -1,136 +1,40 @@ """ -Database models for publishing. - -Publishing new versions of content requires different bits of data to be pushed -into different apps, and this may not happen all at once. These publishes are -triggered asynchronously and apps may fail without being able to immediately -signal the caller. When this happens today in edx-platform, we can end up in a -mixed-published state where certain features (e.g. the course outline) are stale -because the process that updated it has failed. - -In order to try to address these issues, we take the following approach: - -1. Explicitly track each app's data creation for a version of content. -2. Only update the branch information to point to a new version when all the - required apps have successfully completed. -3. Make the apps as resilient as possible, by giving them relatively simple data - models and having them sift out strange/inconsistent data using - ContentErrors. -4. Centralize the creation and reporting of these errors, to ease reporting and - debugging. - -Priority Considerations - -Not all apps necessarily need to complete in order for a publish to be -successful. For instance, pushing course content data into CourseGraph for later -analysis is a task that the overall publish process doesn't need to wait for, -and a failure there shouldn't be fatal for the publish process. - -Ordering Considerations - -Some post-publish tasks will depend on data having already been written to -certain APIs that they can in turn query. For instance, learning_composition -requires that learning_partitioning has already run and populated its data -models. - -# Cleanup - -Associating things with versions and allowing for duplication/deletion, vs. -heavily normalizing and deduping, with the caveat that it will be harder to -determine what can and can't be deleted safely. """ -import uuid - from django.db import models -from django.conf import settings from model_utils.models import TimeStampedModel -from simple_history.models import HistoricalRecords - -from openedx_learning.lib.fields import identifier_field, immutable_uuid_field -# Note: We probably want to make our own IdentifierField so that we can control -# how collation happens in a MySQL/Postgres compatible way. +from openedx_learning.lib.fields import hash_field, identifier_field, immutable_uuid_field -class LearningContextType(models.Model): - # Identifier - identifier = models.CharField(max_length=100, blank=False, null=False) - - -class LearningContext(TimeStampedModel): - """ - A LearningContext represents a set of content that is versioned together. - - Courses and Content Libraries are examples. - - .. no_pii: - """ +class LearningContext(models.Model): uuid = immutable_uuid_field() identifier = identifier_field(unique=True) - # Don't allow deletion of LearningContextTypes. - type = models.ForeignKey(LearningContextType, on_delete=models.RESTRICT) - created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL - ) - - class Meta: - verbose_name = "Learning Context" - - def __str__(self): - """ - Get a string representation of this model instance. - """ - # TODO: return a string appropriate for the data fields - return "".format(self.id) + return f"{self.uuid}: {self.identifier}" - -class LearningContextVersion(TimeStampedModel): +class LearningContextVersion(models.Model): """ - What's a Context Version? - - .. no_pii: + We actually rely on """ uuid = immutable_uuid_field() identifier = identifier_field(unique=False) + version_num = models.PositiveIntegerField() - # TODO: Replace this with something that doesn't require opaque-keys - learning_context = models.ForeignKey(LearningContext, on_delete=models.CASCADE) - title = models.CharField(max_length=255, db_index=True) -class LearningContextBranch(TimeStampedModel): - """ - What's a Branch? - .. no_pii: +class BlockType(models.Model): """ - learning_context = models.ForeignKey(LearningContext, on_delete=models.CASCADE) - branch_name = models.CharField(max_length=100) - - # The intent of having on_delete=RESTRICT for version and on_delete=CASCADE - # for learning_context is to say, "You can't delete the version that is - # being pointed to by a branch (because it might be the live version that - # students are using), but if you're deleting _the entire Learning Context_, - # then it's fine to delete the branch pointer info as well." - version = models.ForeignKey(LearningContextVersion, on_delete=models.RESTRICT) + Data we want to track: - history = HistoricalRecords() + * Abstract type, from the LMS's point of view (e.g. an atomic leaf thing vs. + a whole sequence). + * What is the subsystem that understands more about this (e.g. XBlock)? + * Classification of this thing within the subsystem (e.g 'problem'). - class Meta: - constraints = [ - models.UniqueConstraint( - fields=["learning_context_id", "branch_name"], - name="one_branch_per_learning_context", - ) - ] - - -class LearningObjectType(models.Model): - """ - Encodes both the primary type (e.g. Unit) and sub-type if applicable. + Need to add another field? """ major = models.CharField( max_length=50, @@ -140,171 +44,86 @@ class LearningObjectType(models.Model): ) minor = models.CharField( max_length=50, - blank=False, + blank=True, null=False, - help_text="Minor types in" ) - -class LearningObject(models.Model): - """ - We have a few different overlapping concepts: - - * A generic bit of leaf content. - - * Can potentially be from different sources. - - * A Unit - - * Different kinds of Units potentially - - * A Sequence - - * Can potentially be diffent sequences - - * A LearningObject - - * A sharable thing. All Units and Sequences are this. Are all leaf content? - - This model is immutable. - - Open Question: What does it mean for a LearningObject to not be associated - with any LearningContext. With multiple LearningContexts? - """ - uuid = immutable_uuid_field() - type = models.ForeignKey(LearningObjectType, on_delete=models.RESTRICT) - - # created_by + def __str__(self): + return f"{self.major}:{self.minor}" -class LearningObjectVersion(models.Model): +class LearningContextBlock(models.Model): """ - This represents a semantically different version of a Learning Object. - - Some important notes: - + This represents any Block that has ever existed in a LearningContext. + Notes: + * I think it's feasible """ - learning_object = models.ForeignKey(LearningObject, on_delete=models.CASCADE) - - # identifier and title would seem to make more sense to attach to the - # LearningObject than the LearningObjectVersion, but while these things - # change infrequently, they *can* change. Things are periodically renamed, - # and even identifiers are sometimes changed (e.g. for case-sensitivity - # issues). + learning_context = models.ForeignKey(LearningContext, on_delete=models.CASCADE) + uuid = immutable_uuid_field() identifier = identifier_field(unique=False) - title = models.CharField(max_length=255, db_index=True) - + block_type = models.ForeignKey(BlockType, on_delete=models.RESTRICT) -class LearningContextVersionContents(models.Model): - """ - What LearningObjectVersions are in a given LearningContextVersion? - - This is effectively gives us a snapshot of all the versioned content - associated with a version of the Learning Context. This could be used to - diff what changed from a previously published version to the next one. - - Note that content does not have to be directly accessible via any sort of - parent-child hierarchy to be in this this list. It's pefectly possible for - detached blocks of content to be published as well. + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["learning_context_id", "identifier"], + name="learning_publishing_lcb_one_identifier_per_lc", + ) + ] - This table can potentially grow to be very large if versions are not cleaned - up. Once a :model:`learning_publishing.LearningContextVersion` is deleted, - all entries pointing to it from this model will cascade delete. + def __str__(self): + return f"{self.identifier}" - Open Questions: - * Does a container LO change if its list of children remains the same, - but the contents of one of those children changes? - - Actually, yeah, it does, because a container points to LOVs, not - LOs. So its hash would change, as we'd expect. - * Can a LearningContextVersion simply point to other LearningContextVersions - by reference, so that we don't have to copy the contents of every library - version to our own each time? - - Probably? - - I think that implies that it's not a LearningContextVersion. Or rather - that there are three things at work: - 1. LearningObjectVersions - 2. Something that maps a bunch of LearningObjectVersions together in a - coherent version (LearningContextVersion?) - 3. Something that ties together multiple #2's into the same published - branch of a course. - - Ooohhh... maybe a branch points to multiple versions of different - libraries? This would also help us with the issue we have around - knowing what versions of libraries are safe to clean up, since - branches would have an fkey on them preventing them from deleting. - - Or do we need those references in the LearningObjectVersions - themselves? +class BlockContent(models.Model): """ - # If the LearningContextVersion itself gets deleted, we should remove the - # mappings for all versioned content that is associated with it. - learning_context_version = models.ForeignKey(LearningContextVersion, on_delete=models.CASCADE) - - # A LearningObjectVersion should not be deleted if it's in a version that - # is still potentially live. - learning_object_version = models.ForeignKey(LearningObjectVersion, on_delete=models.RESTRICT) - - + Holds the basic content data. + A few notes: + + This shouldn't be connected to LearningContexts directly, but to a + ContentPackage. A LearningContext can use multiple ContentPackages, and + multiple LearningContexts can use the same ContentPackage. + + We don't want this data in BlockVersion for two reasons: - -### The stuff below this is error reporting related - - -class LearningAppVersionReport(TimeStampedModel): - """ - Results of creating a version. - - .. no_pii: + 1. If something is deleted and recreated across versions (e.g. accidental + deletion followed by re-upload), we don't want to have to re-upload + everything. + 2. There are minor savings to be had by redundant content (e.g. some HTML + templates that are repeated). + 3. We want to allow BlockVersion queries without fetching a bunch of extra + content data by accident. """ - # Put custom collation setting here? utf8mb4_0900_ai_ci - app_name = models.CharField(max_length=100, blank=False, null=False) - version = models.ForeignKey(LearningContextVersion, on_delete=models.CASCADE) - - # Summary of the ContentErrors associated with a Learning App processing a - # LearningContextVersion's content data. - num_critical = models.PositiveIntegerField() - num_errors = models.PositiveIntegerField() - num_warnings = models.PositiveIntegerField() - - # The only time when it makes sense to have a history for this model is when - # there are code-related errors, and we're retring the same version with new - # code. Unfortunately, this kind of thing does happen, and historical data - # is useful for debugging these cases. - history = HistoricalRecords() + learning_context = models.ForeignKey(LearningContext, on_delete=models.CASCADE) + hash_digest = hash_field(unique=False) + data = models.TextField() class Meta: constraints = [ models.UniqueConstraint( - fields=["app_name", "version"], name="one_report_per_app_and_version" + fields=["learning_context_id", "hash_digest"], + name="learning_publishing_bc_unique_content_hash", ) ] -class LearningAppContentError(models.Model): +class BlockVersion(models.Model): """ - Generic Error Container - - .. no_pii: + Maybe this is also associated with the Content """ - app_version_report = models.ForeignKey( - LearningAppVersionReport, on_delete=models.CASCADE - ) + LAST_VERSION_NUM = 2147483647 + block = models.ForeignKey(LearningContextBlock, on_delete=models.RESTRICT) + content = models.ForeignKey(BlockContent, on_delete=models.RESTRICT) - # Convention for error_code should be {app}.{plugin}.{short_name}? - error_code = models.CharField(max_length=100, blank=False, null=False) - level = models.CharField( - max_length=10, - choices=[ - ("critical", "critical"), - ("error", "error"), - ("warning", "warning"), - ], - ) + uuid = immutable_uuid_field() + start_version_num = models.PositiveIntegerField() + end_version_num = models.PositiveIntegerField(default=LAST_VERSION_NUM) + + title = models.CharField(max_length=1000, blank=True, null=True) + + def __str__(self): + return f"{self.uuid}: {self.title}" - # identifier is intentionally optional, since some errors cannot be tied to - # a speicific item (e.g. "too many blocks"). - usage_key = models.CharField(max_length=255, null=True) - # Generic JSON field - data = models.JSONField() diff --git a/openedx_learning/apps/example/save_example.py b/openedx_learning/apps/example/save_example.py index bf0967f34..b8452e266 100644 --- a/openedx_learning/apps/example/save_example.py +++ b/openedx_learning/apps/example/save_example.py @@ -1,3 +1,5 @@ +# type: ignore +# variable: ignore """ We're dealing with a few different problems here: @@ -18,11 +20,7 @@ 3. If it isn't, don't make anything new, just make a new assocation between LearningContextVersion and your existing content root. - - """ -# type: ignore -# variable: ignore test = foo() @@ -31,15 +29,43 @@ def write_to_lms(course): - """ - This what writing content to the LMS might look like. - - How do we deal with LearningApps that never report back? - """ - partitioning_data = get_partition_data_for_course(course.partitions) - version = course_version - lc_version = publishing_api.create_version(context_key, course.version) - partitioning_api.update_partitions(lc_version, course) + """ + This what writing content to the LMS might look like. + + How do we deal with LearningApps that never report back? + """ + partitioning_data = get_partition_data_for_course(course.partitions) + version = course_version + lc_version = publishing_api.create_version(context_key, course.version) + partitioning_api.update_partitions(lc_version, course) + + + +def write_lib_to_lms(library_key): + + # Find the existing state in the LMS + existing_content = get_content_summary(library_key, branch="draft") + + # Figure out what the state is in Studio's Modulestore + desired_content = generate_content_summary(library_key) + + # Diff the two to see what things we need to write + content_keys_to_write = diff_content_summaries(existing_content, desired_content) + + if not content_keys_to_write: + # Some early return logic here + + # This is some generator so we don't have to pull everything into memory at once. + content_to_write = get_contents(library_key, keys=content_keys_to_write) + + + with transaction.atomic(): + # Step 1: Write the bare content objects, with no policy information + publishing_api.write_contents(content_obj) + + # Step 2: Write compositional data to stitch together + + diff --git a/openedx_learning/lib/fields.py b/openedx_learning/lib/fields.py index e4f531644..bf0b217cd 100644 --- a/openedx_learning/lib/fields.py +++ b/openedx_learning/lib/fields.py @@ -21,6 +21,7 @@ def immutable_uuid_field(): blank=False, null=False, editable=False, + unique=True, ) def hash_field(*, unique): diff --git a/projects/dev.py b/projects/dev.py index 12fd111e4..14a27ec14 100644 --- a/projects/dev.py +++ b/projects/dev.py @@ -79,4 +79,4 @@ STATICFILES_DIRS = [ BASE_DIR / 'projects' / 'static' ] -MEDIA_URL = '/media/' \ No newline at end of file +MEDIA_URL = '/media/'