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

feat: model for upstream-downstream links #269

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions openedx_learning/api/authoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from ..apps.authoring.collections.api import *
from ..apps.authoring.components.api import *
from ..apps.authoring.contents.api import *
from ..apps.authoring.linking.api import *
from ..apps.authoring.publishing.api import *

# This was renamed after the authoring API refactoring pushed this and other
Expand Down
1 change: 1 addition & 0 deletions openedx_learning/api/authoring_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
from ..apps.authoring.collections.models import *
from ..apps.authoring.components.models import *
from ..apps.authoring.contents.models import *
from ..apps.authoring.linking.models import *
from ..apps.authoring.publishing.model_mixins import *
from ..apps.authoring.publishing.models import *
Empty file.
65 changes: 65 additions & 0 deletions openedx_learning/apps/authoring/linking/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""
Django admin for linking models
"""

from django.contrib import admin

from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin

from .models import LearningContextLinksStatus, PublishableEntityLink


@admin.register(PublishableEntityLink)
class PublishableEntityLinkAdmin(ReadOnlyModelAdmin):
"""
PublishableEntityLink admin.
"""
fields = (
"uuid",
"upstream_block",
"upstream_usage_key",
"upstream_context_key",
"downstream_usage_key",
"downstream_context_key",
"downstream_context_title",
"version_synced",
"version_declined",
"created",
"updated",
)
readonly_fields = fields
list_display = [
"upstream_block",
"upstream_usage_key",
"downstream_usage_key",
"downstream_context_title",
"version_synced",
"updated",
]
search_fields = [
"upstream_usage_key",
"upstream_context_key",
"downstream_usage_key",
"downstream_context_key",
"downstream_context_title",
]


@admin.register(LearningContextLinksStatus)
class LearningContextLinksStatusAdmin(admin.ModelAdmin):
"""
LearningContextLinksStatus admin.
"""
fields = (
"context_key",
"status",
"created",
"updated",
)
readonly_fields = ("created", "updated")
list_display = (
"context_key",
"status",
"created",
"updated",
)
112 changes: 112 additions & 0 deletions openedx_learning/apps/authoring/linking/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""
Linking API (warning: UNSTABLE, in progress API)

Please look at the models.py file for more information about the kinds of data
are stored in this app.
"""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Any

from django.db.models import QuerySet

from ..components.models import Component
from .models import LearningContextLinksStatus, LearningContextLinksStatusChoices, PublishableEntityLink

__all__ = [
'delete_entity_link',
'get_entity_links',
'get_or_create_learning_context_link_status',
'update_or_create_entity_link',
]


def get_or_create_learning_context_link_status(
context_key: str, created: datetime | None = None
) -> LearningContextLinksStatus:
"""
Get or create course link status row from LearningContextLinksStatus table for given course key.

Args:
context_key: Learning context or Course key

Returns:
LearningContextLinksStatus object
"""
if not created:
created = datetime.now(tz=timezone.utc)
status, _ = LearningContextLinksStatus.objects.get_or_create(
context_key=context_key,
defaults={
'status': LearningContextLinksStatusChoices.PENDING,
'created': created,
'updated': created,
},
)
return status


def get_entity_links(filters: dict[str, Any]) -> QuerySet[PublishableEntityLink]:
"""
Get entity links based on passed filters.
"""
return PublishableEntityLink.objects.filter(**filters)


def update_or_create_entity_link(
upstream_block: Component | None,
/,
upstream_usage_key: str,
upstream_context_key: str,
downstream_usage_key: str,
downstream_context_key: str,
downstream_context_title: str,
version_synced: int,
version_declined: int | None = None,
created: datetime | None = None,
) -> PublishableEntityLink:
"""
Update or create entity link. This will only update `updated` field if something has changed.
"""
if not created:
created = datetime.now(tz=timezone.utc)
new_values = {
'upstream_usage_key': upstream_usage_key,
'upstream_context_key': upstream_context_key,
'downstream_usage_key': downstream_usage_key,
'downstream_context_key': downstream_context_key,
'downstream_context_title': downstream_context_title,
'version_synced': version_synced,
'version_declined': version_declined,
}
if upstream_block:
new_values.update(
{
'upstream_block': upstream_block.publishable_entity,
}
)
try:
link = PublishableEntityLink.objects.get(downstream_usage_key=downstream_usage_key)
has_changes = False
for key, value in new_values.items():
prev = getattr(link, key)
# None != None is True, so we need to check for it specially
if prev != value and ~(prev is None and value is None):
has_changes = True
setattr(link, key, value)
if has_changes:
link.updated = created
link.save()
except PublishableEntityLink.DoesNotExist:
link = PublishableEntityLink(**new_values)
link.created = created
link.updated = created
link.save()
return link


def delete_entity_link(downstream_usage_key: str):
"""Detele upstream->downstream entity link from database"""
PublishableEntityLink.objects.filter(downstream_usage_key=downstream_usage_key).delete()
16 changes: 16 additions & 0 deletions openedx_learning/apps/authoring/linking/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
linking Django application initialization.
"""

from django.apps import AppConfig


class LinkingConfig(AppConfig):
"""
Configuration for the linking Django application.
"""

name = "openedx_learning.apps.authoring.linking"
verbose_name = "Learning Core > Authoring > Linking"
default_auto_field = "django.db.models.BigAutoField"
label = "oel_linking"
Empty file.
Empty file.
133 changes: 133 additions & 0 deletions openedx_learning/apps/authoring/linking/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Generated by Django 4.2.15 on 2025-01-17 14:23

import uuid

import django.db.models.deletion
from django.db import migrations, models

import openedx_learning.lib.fields
import openedx_learning.lib.validators


class Migration(migrations.Migration):
initial = True

dependencies = [
('oel_publishing', '0002_alter_learningpackage_key_and_more'),
]

operations = [
migrations.CreateModel(
name='LearningContextLinksStatus',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
(
'context_key',
openedx_learning.lib.fields.MultiCollationCharField(
db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'},
help_text='Linking status for downstream/course context key',
max_length=500,
),
),
(
'status',
models.CharField(
choices=[
('pending', 'Pending'),
('processing', 'Processing'),
('failed', 'Failed'),
('completed', 'Completed'),
],
help_text='Status of links in given learning context/course.',
max_length=20,
),
),
('created', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('updated', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
],
options={
'verbose_name': 'Learning Context Links status',
'verbose_name_plural': 'Learning Context Links status',
},
),
migrations.CreateModel(
name='PublishableEntityLink',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='UUID')),
(
'upstream_usage_key',
openedx_learning.lib.fields.MultiCollationCharField(
db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'},
help_text=(
'Upstream block usage key, this value cannot be null and useful to track upstream'
' library blocks that do not exist yet'
),
max_length=500,
),
),
(
'upstream_context_key',
openedx_learning.lib.fields.MultiCollationCharField(
db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'},
help_text='Upstream context key i.e., learning_package/library key',
max_length=500,
),
),
(
'downstream_usage_key',
openedx_learning.lib.fields.MultiCollationCharField(
db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, max_length=500
),
),
(
'downstream_context_key',
openedx_learning.lib.fields.MultiCollationCharField(
db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, max_length=500
),
),
(
'downstream_context_title',
openedx_learning.lib.fields.MultiCollationCharField(
db_collations={'mysql': 'utf8mb4_unicode_ci', 'sqlite': 'NOCASE'},
help_text='The title of downstream context, for example, course display name.',
max_length=1000,
),
),
('version_synced', models.IntegerField()),
('version_declined', models.IntegerField(blank=True, null=True)),
('created', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
('updated', models.DateTimeField(validators=[openedx_learning.lib.validators.validate_utc_datetime])),
(
'upstream_block',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='links',
to='oel_publishing.publishableentity',
),
),
],
options={
'verbose_name': 'Publishable Entity Link',
'verbose_name_plural': 'Publishable Entity Links',
},
),
migrations.AddConstraint(
model_name='learningcontextlinksstatus',
constraint=models.UniqueConstraint(fields=('context_key',), name='oel_link_ent_status_ctx_key'),
),
migrations.AddIndex(
model_name='publishableentitylink',
index=models.Index(fields=['downstream_context_key'], name='oel_link_ent_idx_down_ctx_key'),
),
migrations.AddIndex(
model_name='publishableentitylink',
index=models.Index(fields=['upstream_context_key'], name='oel_link_ent_idx_up_ctx_key'),
),
migrations.AddConstraint(
model_name='publishableentitylink',
constraint=models.UniqueConstraint(fields=('downstream_usage_key',), name='oel_link_ent_downstream_key'),
),
]
Empty file.
Loading
Loading