Skip to content
Merged
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
23 changes: 22 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,30 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest] # Add macos-latest later?
python-version: ['3.8']
toxenv: ["py38-django32", "py38-django42"]
# We're only testing against MySQL 8 right now because 5.7 is
# incompatible with Djagno 4.2. We'd have to make the tox.ini file more
# complicated than it's worth given the short expected shelf-life of
# MySQL 5.7 in our stack.
mysql-version: ["8"]
services:
mysql:
image: mysql:${{ matrix.mysql-version }}
ports:
- 3306:3306
env:
MYSQL_DATABASE: "test_oel_db"
MYSQL_USER: "test_oel_user"
MYSQL_PASSWORD: "test_oel_pass"
MYSQL_RANDOM_ROOT_PASSWORD: true
# these options are blatantly copied from edx-platform's values
options: >-
--health-cmd "mysqladmin ping"
--health-interval 10s
--health-timeout 5s
--health-retries 3
steps:
- uses: actions/checkout@v3
- name: setup python
Expand Down
26 changes: 26 additions & 0 deletions mysql_test_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
This is an extension of the default test_settings.py file that uses MySQL for
the backend. While the openedx-learning apps should run fine using SQLite, they
also do some MySQL-specific things around charset/collation settings and row
compression.

The tox targets for py38-django32 and py38-django42 will use this settings file.
For the most part, you can use test_settings.py instead (that's the default if
you just run "pytest" with no arguments).
"""

from test_settings import *

DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": "oel_db",
"USER": "test_oel_user",
"PASSWORD": "test_oel_pass",
"HOST": "127.0.0.1",
"PORT": "3306",
"OPTIONS": {
"charset": "utf8mb4"
}
}
}
149 changes: 38 additions & 111 deletions openedx_learning/core/components/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,153 +1,80 @@
# Generated by Django 3.2.18 on 2023-05-11 02:07
# Generated by Django 3.2.19 on 2023-06-15 14:43

from django.db import migrations, models
import django.db.models.deletion
import openedx_learning.lib.fields
import uuid


class Migration(migrations.Migration):

initial = True

dependencies = [
("oel_publishing", "0001_initial"),
("oel_contents", "0001_initial"),
('oel_publishing', '0001_initial'),
('oel_contents', '0001_initial'),
]

operations = [
migrations.CreateModel(
name="Component",
name='Component',
fields=[
(
"publishable_entity",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
serialize=False,
to="oel_publishing.publishableentity",
),
),
("namespace", models.CharField(max_length=100)),
("type", models.CharField(blank=True, max_length=100)),
("local_key", models.CharField(max_length=255)),
(
"learning_package",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="oel_publishing.learningpackage",
),
),
('publishable_entity', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='oel_publishing.publishableentity')),
('namespace', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, max_length=100)),
('type', openedx_learning.lib.fields.MultiCollationCharField(blank=True, db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, max_length=100)),
('local_key', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, max_length=500)),
('learning_package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oel_publishing.learningpackage')),
],
options={
"verbose_name": "Component",
"verbose_name_plural": "Components",
'verbose_name': 'Component',
'verbose_name_plural': 'Components',
},
),
migrations.CreateModel(
name="ComponentVersion",
name='ComponentVersion',
fields=[
(
"publishable_entity_version",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
serialize=False,
to="oel_publishing.publishableentityversion",
),
),
(
"component",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="versions",
to="oel_components.component",
),
),
('publishable_entity_version', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='oel_publishing.publishableentityversion')),
('component', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='oel_components.component')),
],
options={
"verbose_name": "Component Version",
"verbose_name_plural": "Component Versions",
'verbose_name': 'Component Version',
'verbose_name_plural': 'Component Versions',
},
),
migrations.CreateModel(
name="ComponentVersionRawContent",
name='ComponentVersionRawContent',
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",
),
),
("key", models.CharField(max_length=255)),
("learner_downloadable", models.BooleanField(default=False)),
(
"component_version",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="oel_components.componentversion",
),
),
(
"raw_content",
models.ForeignKey(
on_delete=django.db.models.deletion.RESTRICT,
to="oel_contents.rawcontent",
),
),
('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')),
('key', openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_bin', 'sqlite': 'BINARY'}, max_length=500)),
('learner_downloadable', models.BooleanField(default=False)),
('component_version', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='oel_components.componentversion')),
('raw_content', models.ForeignKey(on_delete=django.db.models.deletion.RESTRICT, to='oel_contents.rawcontent')),
],
),
migrations.AddField(
model_name="componentversion",
name="raw_contents",
field=models.ManyToManyField(
related_name="component_versions",
through="oel_components.ComponentVersionRawContent",
to="oel_contents.RawContent",
),
model_name='componentversion',
name='raw_contents',
field=models.ManyToManyField(related_name='component_versions', through='oel_components.ComponentVersionRawContent', to='oel_contents.RawContent'),
),
migrations.AddIndex(
model_name="componentversionrawcontent",
index=models.Index(
fields=["raw_content", "component_version"],
name="oel_cvrawcontent_c_cv",
),
model_name='componentversionrawcontent',
index=models.Index(fields=['raw_content', 'component_version'], name='oel_cvrawcontent_c_cv'),
),
migrations.AddIndex(
model_name="componentversionrawcontent",
index=models.Index(
fields=["component_version", "raw_content"],
name="oel_cvrawcontent_cv_d",
),
model_name='componentversionrawcontent',
index=models.Index(fields=['component_version', 'raw_content'], name='oel_cvrawcontent_cv_d'),
),
migrations.AddConstraint(
model_name="componentversionrawcontent",
constraint=models.UniqueConstraint(
fields=("component_version", "key"), name="oel_cvrawcontent_uniq_cv_key"
),
model_name='componentversionrawcontent',
constraint=models.UniqueConstraint(fields=('component_version', 'key'), name='oel_cvrawcontent_uniq_cv_key'),
),
migrations.AddIndex(
model_name="component",
index=models.Index(
fields=["learning_package", "namespace", "type", "local_key"],
name="oel_component_idx_lc_ns_t_lk",
),
model_name='component',
index=models.Index(fields=['learning_package', 'namespace', 'type', 'local_key'], name='oel_component_idx_lc_ns_t_lk'),
),
migrations.AddConstraint(
model_name="component",
constraint=models.UniqueConstraint(
fields=("learning_package", "namespace", "type", "local_key"),
name="oel_component_uniq_lc_ns_t_lk",
),
model_name='component',
constraint=models.UniqueConstraint(fields=('learning_package', 'namespace', 'type', 'local_key'), name='oel_component_uniq_lc_ns_t_lk'),
),
]
10 changes: 7 additions & 3 deletions openedx_learning/core/components/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
"""
from django.db import models

from openedx_learning.lib.fields import key_field, immutable_uuid_field
from openedx_learning.lib.fields import (
case_sensitive_char_field,
immutable_uuid_field,
key_field,
)
from ..publishing.models import LearningPackage
from ..publishing.model_mixins import (
PublishableEntityMixin,
Expand Down Expand Up @@ -77,13 +81,13 @@ class Component(PublishableEntityMixin):
# namespace and type work together to help figure out what Component needs
# to handle this data. A namespace is *required*. The namespace for XBlocks
# is "xblock.v1" (to match the setup.py entrypoint naming scheme).
namespace = models.CharField(max_length=100, null=False, blank=False)
namespace = case_sensitive_char_field(max_length=100, blank=False)

# type is a way to help sub-divide namespace if that's convenient. This
# field cannot be null, but it can be blank if it's not necessary. For an
# XBlock, type corresponds to tag, e.g. "video". It's also the block_type in
# the UsageKey.
type = models.CharField(max_length=100, null=False, blank=True)
type = case_sensitive_char_field(max_length=100, blank=True)

# local_key is an identifier that is local to the (namespace, type). The
# publishable.key should be calculated as a combination of (namespace, type,
Expand Down
Loading