Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a157711
feat: enable type hints and mypy checking for this repository
bradenmacdonald Aug 16, 2023
1cb67ac
fix: pylint issues
bradenmacdonald Aug 16, 2023
6886014
fix: improve type hints of openedx_learning and fix identified bugs
bradenmacdonald Aug 16, 2023
191cddb
fix: typing issues in tagging code
bradenmacdonald Aug 17, 2023
852b388
fix: typing issues in tagging code
bradenmacdonald Aug 17, 2023
59a80b0
fix: typing issues in tagging code
bradenmacdonald Aug 18, 2023
5729a19
fix: typing issues in tagging code
bradenmacdonald Aug 22, 2023
a710cc0
fix: typing issues in tagging code
bradenmacdonald Aug 22, 2023
d46f720
chore: update with latest master
bradenmacdonald Aug 23, 2023
ca706b2
feat: opt more code into type checking
bradenmacdonald Aug 23, 2023
75b429b
feat: type check the test suite
bradenmacdonald Aug 23, 2023
d14b590
chore: add migration to prevent taxonomy description from being null
bradenmacdonald Aug 23, 2023
2079cc0
fix: duplicate module error
bradenmacdonald Aug 23, 2023
826553d
chore: fix some mypy warnings that only appeared on CI
bradenmacdonald Aug 23, 2023
87d5840
feat: add even more type annotations
bradenmacdonald Aug 23, 2023
ee19093
fix: PEP8 / pycodestyle issues
bradenmacdonald Aug 23, 2023
6148244
fix: run isort
bradenmacdonald Aug 23, 2023
77b9ff5
fix: invalid descriptions in test fixture
bradenmacdonald Aug 23, 2023
488dac4
fix: failing test cases
bradenmacdonald Aug 23, 2023
57fb09d
fix: address review comments
bradenmacdonald Aug 23, 2023
d8e04af
chore: be more explicit about what type error we're ignoring
bradenmacdonald Aug 23, 2023
b7f5a84
feat: add a few more missing type annotations
bradenmacdonald Aug 24, 2023
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
matrix:
os: [ubuntu-latest] # Add macos-latest later?
python-version: ['3.8']
toxenv: ["py38-django32", "py38-django42", "package"]
toxenv: ["py38-django32", "py38-django42", "package", "quality"]
# 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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pip-log.txt
# Unit test / coverage reports
.cache/
.pytest_cache/
.mypy_cache/
.coverage
.coverage.*
.tox
Expand Down
2 changes: 1 addition & 1 deletion manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django # pylint: disable=unused-import, wrong-import-position
import django # pylint: disable=unused-import
except ImportError as import_error:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
Expand Down
15 changes: 15 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[mypy]
follow_imports = normal
ignore_missing_imports = False
allow_untyped_globals = False
plugins =
mypy_django_plugin.main,
mypy_drf_plugin.main
files =
openedx_learning,
openedx_tagging,
tests

[mypy.plugins.django-stubs]
# content_staging only works with CMS; others work with either, so we run mypy with CMS settings.
django_settings_module = "projects.dev"
3 changes: 3 additions & 0 deletions openedx_learning/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
"""
Open edX Learning ("Learning Core")
"""
__version__ = "0.1.5"
3 changes: 3 additions & 0 deletions openedx_learning/contrib/media_server/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Media server (for dev or low-traffic instances)
"""
3 changes: 3 additions & 0 deletions openedx_learning/contrib/media_server/apps.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""
Django app metadata for the Media Server application.
"""
from django.apps import AppConfig
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
Expand Down
3 changes: 3 additions & 0 deletions openedx_learning/contrib/media_server/urls.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""
URLs for the media server application
"""
from django.urls import path

from .views import component_asset
Expand Down
7 changes: 6 additions & 1 deletion openedx_learning/contrib/media_server/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
"""
Views for the media server application

(serves media files in dev or low-traffic instances).
"""
from pathlib import Path

from django.http import FileResponse
Expand Down Expand Up @@ -28,7 +33,7 @@ def component_asset(
learning_package_key, component_key, version_num, asset_path
)
except ObjectDoesNotExist:
raise Http404("File not found")
raise Http404("File not found") # pylint: disable=raise-missing-from

if not cvc.learner_downloadable and not (
request.user and request.user.is_superuser
Expand Down
54 changes: 22 additions & 32 deletions openedx_learning/core/components/admin.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
"""
Django admin for components models
"""
from django.contrib import admin
from django.template.defaultfilters import filesizeformat
from django.urls import reverse
from django.utils.html import format_html
from django.utils.safestring import SafeText

from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin
from .models import (
Component,
ComponentVersion,
ComponentVersionRawContent,
)
from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin


class ComponentVersionInline(admin.TabularInline):
"""Inline admin view of ComponentVersion from the Component Admin"""
Comment thread
bradenmacdonald marked this conversation as resolved.
Outdated
model = ComponentVersion
fields = ["version_num", "created", "title", "format_uuid"]
readonly_fields = ["version_num", "created", "title", "uuid", "format_uuid"]
extra = 0

@admin.display(description="UUID")
def format_uuid(self, cv_obj):
return format_html(
'<a href="{}">{}</a>',
reverse("admin:oel_components_componentversion_change", args=(cv_obj.pk,)),
cv_obj.uuid,
)

format_uuid.short_description = "UUID"


@admin.register(Component)
class ComponentAdmin(ReadOnlyModelAdmin):
"""Django admin configuration for Component"""
list_display = ("key", "uuid", "namespace", "type", "created")
readonly_fields = [
"learning_package",
Expand All @@ -44,6 +49,7 @@ class ComponentAdmin(ReadOnlyModelAdmin):


class RawContentInline(admin.TabularInline):
"""Django admin configuration for RawContent"""
model = ComponentVersion.raw_contents.through

def get_queryset(self, request):
Expand Down Expand Up @@ -75,11 +81,11 @@ def get_queryset(self, request):
def rendered_data(self, cvc_obj):
return content_preview(cvc_obj)

@admin.display(description="Size")
def format_size(self, cvc_obj):
return filesizeformat(cvc_obj.raw_content.size)

format_size.short_description = "Size"

@admin.display(description="Key")
def format_key(self, cvc_obj):
return format_html(
'<a href="{}">{}</a>',
Expand All @@ -88,11 +94,10 @@ def format_key(self, cvc_obj):
cvc_obj.key,
)

format_key.short_description = "Key"


@admin.register(ComponentVersion)
class ComponentVersionAdmin(ReadOnlyModelAdmin):
"""Django admin configuration for ComponentVersion"""
readonly_fields = [
"component",
"uuid",
Expand Down Expand Up @@ -120,29 +125,8 @@ def get_queryset(self, request):
)


def is_displayable_text(mime_type):

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.

This function was no longer used, after a prior refactor.

# Our usual text files, including things like text/markdown, text/html
media_type, media_subtype = mime_type.split("/")

if media_type == "text":
return True

# Our OLX goes here, but so do some other things like
if media_subtype.endswith("+xml"):
return True

# Other application/* types that we know we can display.
if media_subtype in ["json", "x-subrip"]:
return True

# Other formats that are really specific types of JSON
if media_subtype.endswith("+json"):
return True

return False


def link_for_cvc(cvc_obj: ComponentVersionRawContent):
def link_for_cvc(cvc_obj: ComponentVersionRawContent) -> str:
"""Get the download URL for the given ComponentVersionRawContent instance"""
return "/media_server/component_asset/{}/{}/{}/{}".format(
cvc_obj.raw_content.learning_package.key,
cvc_obj.component_version.component.key,
Expand All @@ -151,14 +135,20 @@ def link_for_cvc(cvc_obj: ComponentVersionRawContent):
)


def format_text_for_admin_display(text):
def format_text_for_admin_display(text: str) -> SafeText:
"""
Get the HTML to display the given plain text (preserving formatting)
"""
return format_html(
'<pre style="white-space: pre-wrap;">\n{}\n</pre>',
text,
)


def content_preview(cvc_obj: ComponentVersionRawContent):
def content_preview(cvc_obj: ComponentVersionRawContent) -> SafeText:
"""
Get the HTML to display a preview of the given ComponentVersionRawContent
"""
raw_content_obj = cvc_obj.raw_content

if raw_content_obj.mime_type.startswith("image/"):
Expand Down
28 changes: 24 additions & 4 deletions openedx_learning/core/components/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
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
from pathlib import Path

from django.db.models import Q
Expand All @@ -23,8 +25,14 @@


def create_component(
learning_package_id, namespace, type, local_key, created, created_by
learning_package_id: int,
namespace: str,
type: str, # pylint: disable=redefined-builtin
local_key: str,
created: datetime,
created_by: int | None,
):
"""Create a new Component (an entity like a Problem or Video)"""
key = f"{namespace}:{type}@{local_key}"
with atomic():
publishable_entity = create_publishable_entity(
Expand All @@ -41,6 +49,7 @@ def create_component(


def create_component_version(component_pk, version_num, title, created, created_by):
"""Create a new ComponentVersion"""
with atomic():
publishable_entity_version = create_publishable_entity_version(
entity_id=component_pk,
Expand All @@ -57,8 +66,15 @@ def create_component_version(component_pk, version_num, title, created, created_


def create_component_and_version(
learning_package_id, namespace, type, local_key, title, created, created_by
learning_package_id: int,
namespace: str,
type: str, # pylint: disable=redefined-builtin
local_key: str,
title: str,
created: datetime,
created_by: int | None,
):
"""Create a Component and associated ComponentVersion atomically"""
with atomic():
component = create_component(
learning_package_id, namespace, type, local_key, created, created_by
Expand Down Expand Up @@ -103,8 +119,12 @@ def get_component_version_content(


def add_content_to_component_version(
component_version, raw_content_id, key, learner_downloadable=False
):
component_version,
raw_content_id,
key,
learner_downloadable=False,
) -> ComponentVersionRawContent:
"""Add a RawContent to the given ComponentVersion"""
cvrc, _created = ComponentVersionRawContent.objects.get_or_create(
component_version=component_version,
raw_content_id=raw_content_id,
Expand Down
7 changes: 5 additions & 2 deletions openedx_learning/core/components/apps.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""
Django metadata for the Components Django application.
"""
from django.apps import AppConfig


Expand All @@ -15,7 +18,7 @@ def ready(self):
"""
Register Component and ComponentVersion.
"""
from ..publishing.api import register_content_models
from .models import Component, ComponentVersion
from ..publishing.api import register_content_models # pylint: disable=import-outside-toplevel
from .models import Component, ComponentVersion # pylint: disable=import-outside-toplevel

register_content_models(Component, ComponentVersion)
11 changes: 10 additions & 1 deletion openedx_learning/core/components/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
by convention, but it's possible we might want to have special identifiers
later.
"""
from __future__ import annotations
from django.db import models

from openedx_learning.lib.fields import (
Expand All @@ -31,7 +32,7 @@
from ..contents.models import RawContent


class Component(PublishableEntityMixin):
class Component(PublishableEntityMixin): # type: ignore[django-manager-missing]
"""
This represents any Component that has ever existed in a LearningPackage.

Expand Down Expand Up @@ -69,6 +70,10 @@ class Component(PublishableEntityMixin):
Make a foreign key to the Component model when you need a stable reference
that will exist for as long as the LearningPackage itself exists.
"""
# Tell mypy what type our objects manager has.
# It's actually PublishableEntityMixinManager, but that has the exact same
# interface as the base manager class.
objects: models.Manager[Component]

# This foreign key is technically redundant because we're already locked to
# a single LearningPackage through our publishable_entity relation. However, having
Expand Down Expand Up @@ -143,6 +148,10 @@ class ComponentVersion(PublishableEntityVersionMixin):
This holds the content using a M:M relationship with RawContent via
ComponentVersionRawContent.
"""
# Tell mypy what type our objects manager has.
# It's actually PublishableEntityVersionMixinManager, but that has the exact
# same interface as the base manager class.
objects: models.Manager[ComponentVersion]

# This is technically redundant, since we can get this through
# publishable_entity_version.publishable.component, but this is more
Expand Down
9 changes: 7 additions & 2 deletions openedx_learning/core/contents/admin.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
"""
Django admin for contents models
"""
from django.contrib import admin
from django.utils.html import format_html

from .models import RawContent

from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin
from .models import RawContent


@admin.register(RawContent)
class RawContentAdmin(ReadOnlyModelAdmin):
"""
Django admin for RawContent model
"""
list_display = [
"hash_digest",
"file_link",
Expand Down
Loading