Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"""
# These wildcard imports are okay because these modules declare __all__.
# pylint: disable=wildcard-import
from ..apps.authoring.collections.models import *
from ..apps.authoring.components.models import *
from ..apps.authoring.contents.models import *
from ..apps.authoring.publishing.model_mixins import *
Expand Down
30 changes: 14 additions & 16 deletions openedx_learning/apps/authoring/collections/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@

def create_collection(
learning_package_id: int,
Comment on lines 23 to 24

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def create_collection(
learning_package_id: int,
def create_collection(
learning_package_id: int,
created_by: int | None,

I realized the created_by value was missing from the params and in the .create below, would you mind adding them in this PR since I needed it when I was working on the REST endpoints.

@rpenido rpenido Aug 19, 2024 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch @yusuf-musleh!
Fixed here: a30e04a

name: str,
title: str,
description: str = "",
) -> Collection:
"""
Create a new Collection
"""
collection = Collection.objects.create(
learning_package_id=learning_package_id,
name=name,
title=title,
description=description,
)
Comment on lines 36 to 37

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description=description,
)
description=description,
created_by=created_by,
)

Likewise we need to add the created_by here

return collection
Expand All @@ -45,26 +45,26 @@ def get_collection(collection_id: int) -> Collection:

def update_collection(
collection_id: int,
name: str | None = None,
title: str | None = None,
description: str | None = None,
) -> Collection:
"""
Update a Collection
"""
lp = Collection.objects.get(id=collection_id)
collection = Collection.objects.get(id=collection_id)

# If no changes were requested, there's nothing to update, so just return
# the Collection as-is
if all(field is None for field in [name, description]):
return lp
if all(field is None for field in [title, description]):
return collection

if name is not None:
lp.name = name
if title is not None:
collection.title = title
if description is not None:
lp.description = description
collection.description = description

lp.save()
return lp
collection.save()
return collection


def get_learning_package_collections(learning_package_id: int) -> QuerySet[Collection]:
Expand All @@ -73,8 +73,6 @@ def get_learning_package_collections(learning_package_id: int) -> QuerySet[Colle

Only enabled collections are returned
"""
return (
Collection.objects
.filter(learning_package_id=learning_package_id, enabled=True)
.select_related("learning_package")
)
return Collection.objects \
.filter(learning_package_id=learning_package_id, enabled=True) \
.select_related("learning_package")
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Generated by Django 4.2.14 on 2024-08-14 14:20

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

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


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('oel_collections', '0001_initial'),
]

operations = [
migrations.RemoveField(
model_name='collection',
name='name',
),
migrations.AddField(
model_name='collection',
name='created_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='collection',
name='title',
field=openedx_learning.lib.fields.MultiCollationCharField(db_collations={'mysql': 'utf8mb4_unicode_ci', 'sqlite': 'NOCASE'}, default='Collection', help_text='The title of the collection.', max_length=500),
preserve_default=False,
),
migrations.AlterField(
model_name='collection',
name='created',
field=models.DateTimeField(auto_now_add=True, validators=[openedx_learning.lib.validators.validate_utc_datetime]),
),
migrations.AlterField(
model_name='collection',
name='description',
field=openedx_learning.lib.fields.MultiCollationTextField(blank=True, db_collations={'mysql': 'utf8mb4_unicode_ci', 'sqlite': 'NOCASE'}, default='', help_text='Provides extra information for the user about this collection.', max_length=10000),
),
migrations.AlterField(
model_name='collection',
name='modified',
field=models.DateTimeField(auto_now=True, validators=[openedx_learning.lib.validators.validate_utc_datetime]),
),
migrations.AddIndex(
model_name='collection',
index=models.Index(fields=['learning_package', 'title'], name='oel_collect_learnin_dfaf89_idx'),
),
]
55 changes: 45 additions & 10 deletions openedx_learning/apps/authoring/collections/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@
"""
from __future__ import annotations

from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _

from ....lib.fields import case_insensitive_char_field
from ....lib.fields import MultiCollationTextField, case_insensitive_char_field
from ....lib.validators import validate_utc_datetime
from ..publishing.models import LearningPackage

__all__ = [
"Collection",
]


class Collection(models.Model):
"""
Expand All @@ -20,22 +26,29 @@ class Collection(models.Model):
# Each collection belongs to a learning package
learning_package = models.ForeignKey(LearningPackage, on_delete=models.CASCADE)

name = case_insensitive_char_field(
title = case_insensitive_char_field(
null=False,
max_length=255,
db_index=True,
blank=False,
max_length=500,
help_text=_(
"The name of the collection."
"The title of the collection."
),
)
description = case_insensitive_char_field(
null=False,

description = MultiCollationTextField(
blank=True,
null=False,
default="",
max_length=10_000,
help_text=_(
"Provides extra information for the user about this collection."
),
db_collations={
"sqlite": "NOCASE",
"mysql": "utf8mb4_unicode_ci",
}
)

# We don't have api functions to handle the enabled field. This is a placeholder for future use and
# a way to "soft delete" collections.
enabled = models.BooleanField(
Expand All @@ -44,11 +57,33 @@ class Collection(models.Model):
"Whether the collection is enabled or not."
),
)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)

created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
)

created = models.DateTimeField(
auto_now_add=True,
validators=[
validate_utc_datetime,
],
)

modified = models.DateTimeField(
auto_now=True,
validators=[
validate_utc_datetime,
],
)

class Meta:
verbose_name_plural = "Collections"
indexes = [
models.Index(fields=["learning_package", "title"]),
]
Comment on lines +80 to +86

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just wanted to confirm, since the ForeignKey field defined is learning_package, should this index be learning_package instead of learning_package_id? I was looking at other places in the code where indexes are defined, and the ForeignKey fields are defined without the _id, so I just wanted to make sure if I am missing something.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are right @yusuf-musleh !
Fixed here: 9417609


def __repr__(self) -> str:
"""
Expand All @@ -60,4 +95,4 @@ def __str__(self) -> str:
"""
User-facing string representation of a Collection.
"""
return f"<{self.__class__.__name__}> ({self.id}:{self.name})"
return f"<{self.__class__.__name__}> ({self.id}:{self.title})"
32 changes: 16 additions & 16 deletions tests/openedx_learning/apps/authoring/collections/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,17 @@ def setUpTestData(cls) -> None:
super().setUpTestData()
cls.collection1 = collection_api.create_collection(
cls.learning_package.id,
name="Collection 1",
title="Collection 1",
description="Description of Collection 1",
)
cls.collection2 = collection_api.create_collection(
cls.learning_package.id,
name="Collection 2",
title="Collection 2",
description="Description of Collection 2",
)
cls.disabled_collection = collection_api.create_collection(
cls.learning_package.id,
name="Disabled Collection",
title="Disabled Collection",
description="Description of Disabled Collection",
)
cls.disabled_collection.enabled = False
Expand Down Expand Up @@ -106,11 +106,11 @@ def test_create_collection(self):
with freeze_time(created_time):
collection = collection_api.create_collection(
self.learning_package.id,
name="My Collection",
title="My Collection",
description="This is my collection",
)

assert collection.name == "My Collection"
assert collection.title == "My Collection"
assert collection.description == "This is my collection"
assert collection.enabled
assert collection.created == created_time
Expand All @@ -122,9 +122,9 @@ def test_create_collection_without_description(self):
"""
collection = collection_api.create_collection(
self.learning_package.id,
name="My Collection",
title="My Collection",
)
assert collection.name == "My Collection"
assert collection.title == "My Collection"
assert collection.description == ""
assert collection.enabled

Expand All @@ -142,37 +142,37 @@ def setUp(self) -> None:
super().setUp()
self.collection = collection_api.create_collection(
self.learning_package.id,
name="Collection",
title="Collection",
description="Description of Collection",
)

def test_update_collection(self):
"""
Test updating a collection's name and description.
Test updating a collection's title and description.
"""
modified_time = datetime(2024, 8, 8, tzinfo=timezone.utc)
with freeze_time(modified_time):
collection = collection_api.update_collection(
self.collection.pk,
name="New Name",
title="New Title",
description="",
)

assert collection.name == "New Name"
assert collection.title == "New Title"
assert collection.description == ""
assert collection.modified == modified_time
assert collection.created == self.collection.created # unchanged

def test_update_collection_partial(self):
"""
Test updating a collection's name.
Test updating a collection's title.
"""
collection = collection_api.update_collection(
self.collection.pk,
name="New Name",
title="New Title",
)

assert collection.name == "New Name"
assert collection.title == "New Title"
assert collection.description == self.collection.description # unchanged

def test_update_collection_empty(self):
Expand All @@ -185,7 +185,7 @@ def test_update_collection_empty(self):
self.collection.pk,
)

assert collection.name == self.collection.name # unchanged
assert collection.title == self.collection.title # unchanged
assert collection.description == self.collection.description # unchanged
assert collection.modified == self.collection.modified # unchanged

Expand All @@ -194,4 +194,4 @@ def test_update_collection_not_found(self):
Test updating a collection that doesn't exist.
"""
with self.assertRaises(ObjectDoesNotExist):
collection_api.update_collection(12345, name="New Name")
collection_api.update_collection(12345, title="New Title")