diff --git a/.importlinter b/.importlinter index 75eef9ee0..fa7630e3c 100644 --- a/.importlinter +++ b/.importlinter @@ -23,25 +23,30 @@ name = Lib / Core / Contrib Layering type = layers layers= openedx_learning.contrib - openedx_learning.core + openedx_learning.apps openedx_learning.lib -# This is layering within our Core apps. Every new Core app should be added to +# This is layering within our Authoring apps. Every new app should be added to # this list when it it created. [importlinter:contract:core_apps_layering] -name = Core App Dependency Layering +name = Authoring App Dependency Layering type = layers layers= + # The public authoring API is at the top–none of the apps should call to it. + openedx_learning.api.authoring + # The "components" app is responsible for storing versioned Components, # which is Open edX Studio terminology maps to things like individual # Problems, Videos, and blocks of HTML text. This is also the type we would # associate with a single "leaf" XBlock–one that is not a container type and # has no child elements. - openedx_learning.core.components + openedx_learning.apps.authoring.components + # The "contents" app stores the simplest pieces of binary and text data, # without versioning information. These belong to a single Learning Package. - openedx_learning.core.contents + openedx_learning.apps.authoring.contents + # The lowest layer is "publishing", which holds the basic primitives needed # to create Learning Packages and manage the draft and publish states for # various types of content. - openedx_learning.core.publishing + openedx_learning.apps.authoring.publishing diff --git a/README.rst b/README.rst index 5fad583c3..9d82684e4 100644 --- a/README.rst +++ b/README.rst @@ -1,16 +1,13 @@ -openedx-learning -============================= +Open edX Learning Core (and Tagging) +==================================== |pypi-badge| |ci-badge| |codecov-badge| |doc-badge| |pyversions-badge| |license-badge| -This is experimentation/prototyping and not in any way production ready! ------------------------------------------------------------------------- - Overview -------- -The Open edX Learning repository holds Django apps that represent core learning concepts and data models that have been extracted from edx-platform. +The ``openedx_learning`` package holds Django apps that represent core learning concepts and data models that have been extracted from edx-platform. At the moment, this repo also contains the ``openedx_tagging`` package, but this will likely be moved out in the future. Motivation ---------- @@ -26,18 +23,16 @@ Parts ~~~~~ * ``openedx_learning.lib`` is for shared utilities, and may include things like custom field types, plugin registration code, etc. -* ``openedx_learning.core`` contains our Core Django apps, where foundational data structures and APIs will live. +* ``openedx_learning.apps`` contains our Learning Core Django apps, where foundational data structures and APIs will live. The first of these is ``authoring``, which holds apps related to the editing and publishing of learning content. * ``openedx_tagging.core`` contains the core Tagging app, which provides data structures and apis for tagging Open edX objects. -App Dependencies -~~~~~~~~~~~~~~~~ +Learning Core Package Dependencies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Anything can import from ``openedx_learning.lib``. +Learning Core code should never import from ``edx-platform``. -Core apps can import from each other, but cannot import from other apps outside of core. For those apps: +We want to be very strict about dependency management internally as well. Please read the `.importlinter config file <.importlinter>`_ file and the `Python API Conventions ADR `_ for more details. -* ``learning_publishing`` has no dependencies. All the other apps depend on it. -* ``learning_composition`` and ``learning_navigation`` both depend on ``learning_partitioning`` Model Conventions ~~~~~~~~~~~~~~~~~ @@ -46,7 +41,7 @@ We have a few different identifier types in the schema, and we try to avoid ``_i * ``id`` is the auto-generated, internal row ID and primary key. This never changes. Data models should make foreign keys to this field, as per Django convention. * ``uuid`` is a randomly generated UUID4. This is the stable way to refer to a row/resource from an external service. This never changes. This is separate from ``id`` mostly because there are performance penalties when using UUIDs as primary keys with MySQL. -* ``key`` is intended to be a case-sensitive, alphanumeric key, which holds some meaning to library clients. This is usually stable, but can be changed, depending on the business logic of the client. The apps in this repo should make no assumptions about it being stable. It can be used as a suffix. +* ``key`` is intended to be a case-sensitive, alphanumeric key, which holds some meaning to library clients. This is usually stable, but can be changed, depending on the business logic of the client. The apps in this repo should make no assumptions about it being stable. It can be used as a suffix. Since ``key`` is a reserved name on certain database systems, the database field is ``_key``. * ``num`` is like ``key``, but for use when it's strictly numeric. It can also be used as a suffix. @@ -61,7 +56,7 @@ The structure of this repo follows [OEP-0049](https://open-edx-proposals.readthe Code Overview ------------- -The ``openedx_learning.apps`` package contains all our Django applications. All apps are named with a ``learning_`` prefix to better avoid name conflicts, because Django's app namespace is flat. Apps will adhere to `OEP-0049: Django App Patterns `_. +The ``openedx_learning.apps`` package contains all our Django applications. Development Workflow -------------------- @@ -75,7 +70,7 @@ One Time Setup cd openedx-learning # Set up a virtualenv using virtualenvwrapper with the same name as the repo and activate it - mkvirtualenv -p python3.8 openedx-learning + mkvirtualenv -p python3.11 openedx-learning Every time you develop something in this repo diff --git a/docs/api_reference.rst b/docs/api_reference.rst new file mode 100644 index 000000000..b46ac15a2 --- /dev/null +++ b/docs/api_reference.rst @@ -0,0 +1,8 @@ +API Reference +============= + +.. toctree:: + :maxdepth: 1 + :glob: + + public_apis/* diff --git a/docs/conf.py b/docs/conf.py index 6308e2c23..a21df1328 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -511,6 +511,9 @@ def on_init(app): # pylint: disable=unused-argument Read the Docs won't run tox or custom shell commands, so we need this to avoid checking in the generated reStructuredText files. + + Note: there is no auto-building of API docs for the entire repo. We + explicitly build reference docs for the public API in /docs/public_apis/ """ docs_path = os.path.abspath(os.path.dirname(__file__)) root_path = os.path.abspath(os.path.join(docs_path, '..')) @@ -519,9 +522,6 @@ def on_init(app): # pylint: disable=unused-argument # If we are, assemble the path manually bin_path = os.path.abspath(os.path.join(sys.prefix, 'bin')) apidoc_path = os.path.join(bin_path, apidoc_path) - check_call([apidoc_path, '-o', docs_path, os.path.join(root_path, 'openedx_learning'), - os.path.join(root_path, 'openedx_learning/migrations')]) - def setup(app): """ diff --git a/docs/decisions/0008-tagging-tree-data-arch.rst b/docs/decisions/0008-tagging-tree-data-arch.rst index f4a879fef..eaf2d0633 100644 --- a/docs/decisions/0008-tagging-tree-data-arch.rst +++ b/docs/decisions/0008-tagging-tree-data-arch.rst @@ -27,7 +27,7 @@ All taxonomies are trees We could use a tree structure for all taxonomies: flat taxonomies would have only 1 level of tags under the root, while nested taxonomies can be deeper. -To implement this, we'd link each taxonomy to a root tag, with the user-visible tags underneath. +To implement this, we'd link each taxonomy to a root tag, with the user-visible tags underneath. It was simpler instead to link the tag to the taxonomy, which removes the need for the unseen root tag. diff --git a/docs/decisions/0009-tagging-administrators.rst b/docs/decisions/0009-tagging-administrators.rst index cd5284232..0c984ff37 100644 --- a/docs/decisions/0009-tagging-administrators.rst +++ b/docs/decisions/0009-tagging-administrators.rst @@ -34,7 +34,7 @@ Rejected Alternatives Django users & groups ~~~~~~~~~~~~~~~~~~~~~ -This is a standard way to grant access in Django apps, but it is not used in Open edX. +This is a standard way to grant access in Django apps, but it is not used in Open edX. .. _get_organizations: https://github.com/openedx/edx-platform/blob/4dc35c73ffa6d6a1dcb6e9ea1baa5bed40721125/cms/djangoapps/contentstore/views/course.py#L1958 .. _CourseCreator: https://github.com/openedx/edx-platform/blob/4dc35c73ffa6d6a1dcb6e9ea1baa5bed40721125/cms/djangoapps/course_creators/models.py#L27 diff --git a/docs/decisions/0010-taxonomy-enable-context.rst b/docs/decisions/0010-taxonomy-enable-context.rst index 8e9a854ae..4c12badbc 100644 --- a/docs/decisions/0010-taxonomy-enable-context.rst +++ b/docs/decisions/0010-taxonomy-enable-context.rst @@ -31,7 +31,8 @@ When is a taxonomy field shown to course authors in a given course? | False | False | True | False | +-------------+-----------------------------+--------------------------+-------------------------------+ -.. _Course +.. _Course: + Course ~~~~~~ @@ -43,7 +44,8 @@ Disabling taxonomies for a course will remove/hide the taxonomy fields from the Future versions may add more granularity to these settings, to be determined by user needs. -.. _Instance +.. _Instance: + Instance ~~~~~~~~ @@ -52,7 +54,8 @@ Taxonomy contains a boolean ``enabled`` field. A Taxonomy can be disabled for all contexts by setting ``enabled = False``. If ``enabled = True``, then the :ref:`Organization` and :ref:`Course` contexts determine whether a taxonomy will be shown to course authors. -.. _Organization +.. _Organization: + Organization ~~~~~~~~~~~~ diff --git a/docs/decisions/0012-system-taxonomy-creation.rst b/docs/decisions/0012-system-taxonomy-creation.rst index c7bb44ff8..36c27dcde 100644 --- a/docs/decisions/0012-system-taxonomy-creation.rst +++ b/docs/decisions/0012-system-taxonomy-creation.rst @@ -46,7 +46,7 @@ In this way, the system-defined taxonomies are fully dynamic and can represent tags based on Languages, Users, or Organizations that may exist in large numbers or be constantly created. -At present, there isn't a good way to *list* all of the [potential] tags that +At present, there isn't a good way to *list* all of the potential tags that exist in a system-defined Taxonomy. We may add an API for that in the future, for example to list all of the available languages. However for other cases like users it doesn't make sense to even try to list all of the available tags. So @@ -54,7 +54,7 @@ for now, the assumption is that the UI will not even try to display a list of available tags for system-defined taxonomies. After all, system-defined tags are usually applied automatically, rather than a user manually selecting from a list. If there is a need to show a list of tags to the user, use the API that -lists the actually applied tags - i.e. the values of the ``ObjectTag``s +lists the actually applied tags - i.e. the values of the ``ObjectTags`` currently applied to objects using the taxonomy. Tags hard-coded by fixtures/migrations diff --git a/docs/decisions/0014-single-taxonomy-view-api.rst b/docs/decisions/0014-single-taxonomy-view-api.rst index c984aa0f6..cfdf24c98 100644 --- a/docs/decisions/0014-single-taxonomy-view-api.rst +++ b/docs/decisions/0014-single-taxonomy-view-api.rst @@ -135,12 +135,12 @@ Return a simple list of tags, regardless of whether it is root or leaf. Add the children to the root pagination ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Ex. If the ``page_size`` is 100, when fetching the first root tag, which has 10 children tags, +Ex. If the ``page_size`` is 100, when fetching the first root tag, which has 10 children tags, 11 tags are counted for the total and there would be reamin 89 tags to be obtained. **Cons:** -- If there is a branch with a number of tags that exceeds ``page_size``, +- If there is a branch with a number of tags that exceeds ``page_size``, it would only return that branch. - All branches are variable in size, therefore a variable number of root tags would be returned. This would cause interfaces between taxonomies to be inconsistent @@ -150,7 +150,7 @@ Ex. If the ``page_size`` is 100, when fetching the first root tag, which has 10 Search on frontend ~~~~~~~~~~~~~~~~~~ -We constrain the number of tags allowed in a taxonomy for MVP, so that the API +We constrain the number of tags allowed in a taxonomy for MVP, so that the API can return all the tags in one page. So we can perform the tag search on the frontend. **Cons:** diff --git a/docs/index.rst b/docs/index.rst index 94ed0f020..9845aeddc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,10 +3,10 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -openedx-learning -==================== +Open edX Learning Core +====================== -An experiment. +The boring, foundational bits of a learning platform that are hard to get right at scale. Currently being developed for content storage. Contents: @@ -17,11 +17,10 @@ Contents: getting_started testing internationalization - modules changelog + api_reference decisions - Indices and tables ================== diff --git a/docs/public_apis/authoring_api.rst b/docs/public_apis/authoring_api.rst new file mode 100644 index 000000000..b540e90eb --- /dev/null +++ b/docs/public_apis/authoring_api.rst @@ -0,0 +1,7 @@ +Authoring API +============= + +.. automodule:: openedx_learning.api.authoring + :members: + :imported-members: + :show-inheritance: diff --git a/docs/public_apis/authoring_models.rst b/docs/public_apis/authoring_models.rst new file mode 100644 index 000000000..9a2ca135a --- /dev/null +++ b/docs/public_apis/authoring_models.rst @@ -0,0 +1,7 @@ +Authoring Models +================ + +.. automodule:: openedx_learning.api.authoring_models + :members: + :imported-members: + :show-inheritance: diff --git a/olx_importer/management/commands/load_components.py b/olx_importer/management/commands/load_components.py index 49c07b1eb..0f5ab66c6 100644 --- a/olx_importer/management/commands/load_components.py +++ b/olx_importer/management/commands/load_components.py @@ -28,9 +28,9 @@ from django.db import transaction # Model references to remove -from openedx_learning.core.components import api as components_api -from openedx_learning.core.contents import api as contents_api -from openedx_learning.core.publishing import api as publishing_api +from openedx_learning.apps.authoring.components import api as components_api +from openedx_learning.apps.authoring.contents import api as contents_api +from openedx_learning.apps.authoring.publishing import api as publishing_api SUPPORTED_TYPES = ["problem", "video", "html"] logger = logging.getLogger(__name__) diff --git a/openedx_learning/__init__.py b/openedx_learning/__init__.py index ce3114366..bcc19e544 100644 --- a/openedx_learning/__init__.py +++ b/openedx_learning/__init__.py @@ -1,4 +1,4 @@ """ Open edX Learning ("Learning Core"). """ -__version__ = "0.9.4" +__version__ = "0.10.0" diff --git a/openedx_learning/core/__init__.py b/openedx_learning/api/__init__.py similarity index 100% rename from openedx_learning/core/__init__.py rename to openedx_learning/api/__init__.py diff --git a/openedx_learning/api/authoring.py b/openedx_learning/api/authoring.py new file mode 100644 index 000000000..57f317adc --- /dev/null +++ b/openedx_learning/api/authoring.py @@ -0,0 +1,14 @@ +""" +This is the public API for content authoring in Learning Core. + +This is the single ``api`` module that code outside of the +``openedx_learning.apps.authoring.*`` package should import from. It will +re-export the public functions from all api.py modules of all authoring apps. It +may also implement its own convenience APIs that wrap calls to multiple app +APIs. +""" +# These wildcard imports are okay because these api modules declare __all__. +# pylint: disable=wildcard-import +from ..apps.authoring.components.api import * +from ..apps.authoring.contents.api import * +from ..apps.authoring.publishing.api import * diff --git a/openedx_learning/api/authoring_models.py b/openedx_learning/api/authoring_models.py new file mode 100644 index 000000000..142fe6a03 --- /dev/null +++ b/openedx_learning/api/authoring_models.py @@ -0,0 +1,13 @@ +""" +This is where we expose a small handful of models and model mixins that we want +callers to extend or make foreign keys to. Callers importing this module should +never instantiate any of the models themselves–there are API functions in +authoring.py to create and modify data models in a way that keeps those models +consistent. +""" +# These wildcard imports are okay because these modules declare __all__. +# pylint: disable=wildcard-import +from ..apps.authoring.components.models import * +from ..apps.authoring.contents.models import * +from ..apps.authoring.publishing.model_mixins import * +from ..apps.authoring.publishing.models import * diff --git a/openedx_learning/core/components/__init__.py b/openedx_learning/apps/__init__.py similarity index 100% rename from openedx_learning/core/components/__init__.py rename to openedx_learning/apps/__init__.py diff --git a/openedx_learning/core/components/migrations/__init__.py b/openedx_learning/apps/authoring/__init__.py similarity index 100% rename from openedx_learning/core/components/migrations/__init__.py rename to openedx_learning/apps/authoring/__init__.py diff --git a/openedx_learning/core/contents/__init__.py b/openedx_learning/apps/authoring/components/__init__.py similarity index 100% rename from openedx_learning/core/contents/__init__.py rename to openedx_learning/apps/authoring/components/__init__.py diff --git a/openedx_learning/core/components/admin.py b/openedx_learning/apps/authoring/components/admin.py similarity index 100% rename from openedx_learning/core/components/admin.py rename to openedx_learning/apps/authoring/components/admin.py diff --git a/openedx_learning/core/components/api.py b/openedx_learning/apps/authoring/components/api.py similarity index 93% rename from openedx_learning/core/components/api.py rename to openedx_learning/apps/authoring/components/api.py index d4d5fef56..1839dc8ce 100644 --- a/openedx_learning/core/components/api.py +++ b/openedx_learning/apps/authoring/components/api.py @@ -21,6 +21,24 @@ from ..publishing import api as publishing_api from .models import Component, ComponentType, ComponentVersion, ComponentVersionContent +# The public API that will be re-exported by openedx_learning.apps.authoring.api +# is listed in the __all__ entries below. Internal helper functions that are +# private to this module should start with an underscore. If a function does not +# start with an underscore AND it is not in __all__, that function is considered +# to be callable only by other apps in the authoring package. +__all__ = [ + "get_or_create_component_type", + "create_component", + "create_component_version", + "create_next_version", + "create_component_and_version", + "get_component", + "get_component_by_key", + "component_exists_by_key", + "get_components", + "create_component_version_content", +] + def get_or_create_component_type(namespace: str, name: str) -> ComponentType: """ @@ -308,6 +326,9 @@ def look_up_component_version_content( Can raise a django.core.exceptions.ObjectDoesNotExist error if there is no matching ComponentVersionContent. + + This API call was only used in our proof-of-concept assets media server, and + I don't know if we wantto make it a part of the public interface. """ queries = ( Q(component_version__component__learning_package__key=learning_package_key) diff --git a/openedx_learning/core/components/apps.py b/openedx_learning/apps/authoring/components/apps.py similarity index 84% rename from openedx_learning/core/components/apps.py rename to openedx_learning/apps/authoring/components/apps.py index 60ff42ad4..a225d9aed 100644 --- a/openedx_learning/core/components/apps.py +++ b/openedx_learning/apps/authoring/components/apps.py @@ -9,8 +9,8 @@ class ComponentsConfig(AppConfig): Configuration for the Components Django application. """ - name = "openedx_learning.core.components" - verbose_name = "Learning Core: Components" + name = "openedx_learning.apps.authoring.components" + verbose_name = "Learning Core > Authoring > Components" default_auto_field = "django.db.models.BigAutoField" label = "oel_components" diff --git a/openedx_learning/core/components/migrations/0001_initial.py b/openedx_learning/apps/authoring/components/migrations/0001_initial.py similarity index 100% rename from openedx_learning/core/components/migrations/0001_initial.py rename to openedx_learning/apps/authoring/components/migrations/0001_initial.py diff --git a/openedx_learning/core/components/migrations/0002_alter_componentversioncontent_key.py b/openedx_learning/apps/authoring/components/migrations/0002_alter_componentversioncontent_key.py similarity index 100% rename from openedx_learning/core/components/migrations/0002_alter_componentversioncontent_key.py rename to openedx_learning/apps/authoring/components/migrations/0002_alter_componentversioncontent_key.py diff --git a/openedx_learning/core/contents/migrations/__init__.py b/openedx_learning/apps/authoring/components/migrations/__init__.py similarity index 100% rename from openedx_learning/core/contents/migrations/__init__.py rename to openedx_learning/apps/authoring/components/migrations/__init__.py diff --git a/openedx_learning/core/components/models.py b/openedx_learning/apps/authoring/components/models.py similarity index 98% rename from openedx_learning/core/components/models.py rename to openedx_learning/apps/authoring/components/models.py index b3d384638..1e6f52159 100644 --- a/openedx_learning/core/components/models.py +++ b/openedx_learning/apps/authoring/components/models.py @@ -19,12 +19,19 @@ from django.db import models -from ...lib.fields import case_sensitive_char_field, immutable_uuid_field, key_field -from ...lib.managers import WithRelationsManager +from ....lib.fields import case_sensitive_char_field, immutable_uuid_field, key_field +from ....lib.managers import WithRelationsManager from ..contents.models import Content from ..publishing.model_mixins import PublishableEntityMixin, PublishableEntityVersionMixin from ..publishing.models import LearningPackage +__all__ = [ + "ComponentType", + "Component", + "ComponentVersion", + "ComponentVersionContent", +] + class ComponentType(models.Model): """ diff --git a/openedx_learning/core/components/readme.rst b/openedx_learning/apps/authoring/components/readme.rst similarity index 100% rename from openedx_learning/core/components/readme.rst rename to openedx_learning/apps/authoring/components/readme.rst diff --git a/openedx_learning/core/publishing/__init__.py b/openedx_learning/apps/authoring/contents/__init__.py similarity index 100% rename from openedx_learning/core/publishing/__init__.py rename to openedx_learning/apps/authoring/contents/__init__.py diff --git a/openedx_learning/core/contents/admin.py b/openedx_learning/apps/authoring/contents/admin.py similarity index 100% rename from openedx_learning/core/contents/admin.py rename to openedx_learning/apps/authoring/contents/admin.py diff --git a/openedx_learning/core/contents/api.py b/openedx_learning/apps/authoring/contents/api.py similarity index 90% rename from openedx_learning/core/contents/api.py rename to openedx_learning/apps/authoring/contents/api.py index 06ec49acb..872ebad96 100644 --- a/openedx_learning/core/contents/api.py +++ b/openedx_learning/apps/authoring/contents/api.py @@ -11,9 +11,21 @@ from django.core.files.base import ContentFile from django.db.transaction import atomic -from ...lib.fields import create_hash_digest +from ....lib.fields import create_hash_digest from .models import Content, MediaType +# The public API that will be re-exported by openedx_learning.apps.authoring.api +# is listed in the __all__ entries below. Internal helper functions that are +# private to this module should start with an underscore. If a function does not +# start with an underscore AND it is not in __all__, that function is considered +# to be callable only by other apps in the authoring package. +__all__ = [ + "get_or_create_media_type", + "get_content", + "get_or_create_text_content", + "get_or_create_file_content", +] + def get_or_create_media_type(mime_type: str) -> MediaType: """ diff --git a/openedx_learning/core/contents/apps.py b/openedx_learning/apps/authoring/contents/apps.py similarity index 71% rename from openedx_learning/core/contents/apps.py rename to openedx_learning/apps/authoring/contents/apps.py index 97cc82733..3c23bd4f5 100644 --- a/openedx_learning/core/contents/apps.py +++ b/openedx_learning/apps/authoring/contents/apps.py @@ -9,7 +9,7 @@ class ContentsConfig(AppConfig): Configuration for the Contents Django application. """ - name = "openedx_learning.core.contents" - verbose_name = "Learning Core: Contents" + name = "openedx_learning.apps.authoring.contents" + verbose_name = "Learning Core > Authoring > Contents" default_auto_field = "django.db.models.BigAutoField" label = "oel_contents" diff --git a/openedx_learning/core/contents/migrations/0001_initial.py b/openedx_learning/apps/authoring/contents/migrations/0001_initial.py similarity index 100% rename from openedx_learning/core/contents/migrations/0001_initial.py rename to openedx_learning/apps/authoring/contents/migrations/0001_initial.py diff --git a/openedx_learning/core/publishing/migrations/__init__.py b/openedx_learning/apps/authoring/contents/migrations/__init__.py similarity index 100% rename from openedx_learning/core/publishing/migrations/__init__.py rename to openedx_learning/apps/authoring/contents/migrations/__init__.py diff --git a/openedx_learning/core/contents/models.py b/openedx_learning/apps/authoring/contents/models.py similarity index 98% rename from openedx_learning/core/contents/models.py rename to openedx_learning/apps/authoring/contents/models.py index 58565c072..c6de8bb09 100644 --- a/openedx_learning/core/contents/models.py +++ b/openedx_learning/apps/authoring/contents/models.py @@ -13,10 +13,15 @@ from django.core.validators import MaxValueValidator from django.db import models -from ...lib.fields import MultiCollationTextField, case_insensitive_char_field, hash_field, manual_date_time_field -from ...lib.managers import WithRelationsManager +from ....lib.fields import MultiCollationTextField, case_insensitive_char_field, hash_field, manual_date_time_field +from ....lib.managers import WithRelationsManager from ..publishing.models import LearningPackage +__all__ = [ + "MediaType", + "Content", +] + def get_storage() -> Storage: """ diff --git a/openedx_learning/rest_api/__init__.py b/openedx_learning/apps/authoring/publishing/__init__.py similarity index 100% rename from openedx_learning/rest_api/__init__.py rename to openedx_learning/apps/authoring/publishing/__init__.py diff --git a/openedx_learning/core/publishing/admin.py b/openedx_learning/apps/authoring/publishing/admin.py similarity index 100% rename from openedx_learning/core/publishing/admin.py rename to openedx_learning/apps/authoring/publishing/admin.py diff --git a/openedx_learning/core/publishing/api.py b/openedx_learning/apps/authoring/publishing/api.py similarity index 93% rename from openedx_learning/core/publishing/api.py rename to openedx_learning/apps/authoring/publishing/api.py index 595377573..2d5233770 100644 --- a/openedx_learning/core/publishing/api.py +++ b/openedx_learning/apps/authoring/publishing/api.py @@ -23,6 +23,34 @@ PublishLogRecord, ) +# The public API that will be re-exported by openedx_learning.apps.authoring.api +# is listed in the __all__ entries below. Internal helper functions that are +# private to this module should start with an underscore. If a function does not +# start with an underscore AND it is not in __all__, that function is considered +# to be callable only by other apps in the authoring package. +__all__ = [ + "get_learning_package", + "get_learning_package_by_key", + "create_learning_package", + "update_learning_package", + "learning_package_exists", + "create_publishable_entity", + "create_publishable_entity_version", + "get_publishable_entity", + "get_publishable_entity_by_key", + "get_last_publish", + "get_all_drafts", + "get_entities_with_unpublished_changes", + "get_entities_with_unpublished_deletes", + "publish_all_drafts", + "get_draft_version", + "get_published_version", + "set_draft_version", + "soft_delete_draft", + "reset_drafts_to_published", + "register_content_models", +] + def get_learning_package(learning_package_id: int, /) -> LearningPackage: """ @@ -105,6 +133,13 @@ def update_learning_package( return lp +def learning_package_exists(key: str) -> bool: + """ + Check whether a LearningPackage with a particular key exists. + """ + return LearningPackage.objects.filter(key=key).exists() + + def create_publishable_entity( learning_package_id: int, /, @@ -167,13 +202,6 @@ def get_publishable_entity_by_key(learning_package_id, /, key) -> PublishableEnt ) -def learning_package_exists(key: str) -> bool: - """ - Check whether a LearningPackage with a particular key exists. - """ - return LearningPackage.objects.filter(key=key).exists() - - def get_last_publish(learning_package_id: int, /) -> PublishLog | None: return PublishLog.objects \ .filter(learning_package_id=learning_package_id) \ diff --git a/openedx_learning/core/publishing/apps.py b/openedx_learning/apps/authoring/publishing/apps.py similarity index 71% rename from openedx_learning/core/publishing/apps.py rename to openedx_learning/apps/authoring/publishing/apps.py index fbd42a0e4..bfa1bbe62 100644 --- a/openedx_learning/core/publishing/apps.py +++ b/openedx_learning/apps/authoring/publishing/apps.py @@ -10,7 +10,7 @@ class PublishingConfig(AppConfig): Configuration for the publishing Django application. """ - name = "openedx_learning.core.publishing" - verbose_name = "Learning Core: Publishing" + name = "openedx_learning.apps.authoring.publishing" + verbose_name = "Learning Core > Authoring > Publishing" default_auto_field = "django.db.models.BigAutoField" label = "oel_publishing" diff --git a/openedx_learning/core/publishing/migrations/0001_initial.py b/openedx_learning/apps/authoring/publishing/migrations/0001_initial.py similarity index 100% rename from openedx_learning/core/publishing/migrations/0001_initial.py rename to openedx_learning/apps/authoring/publishing/migrations/0001_initial.py diff --git a/openedx_learning/core/publishing/migrations/0002_alter_learningpackage_key_and_more.py b/openedx_learning/apps/authoring/publishing/migrations/0002_alter_learningpackage_key_and_more.py similarity index 100% rename from openedx_learning/core/publishing/migrations/0002_alter_learningpackage_key_and_more.py rename to openedx_learning/apps/authoring/publishing/migrations/0002_alter_learningpackage_key_and_more.py diff --git a/openedx_learning/rest_api/v1/__init__.py b/openedx_learning/apps/authoring/publishing/migrations/__init__.py similarity index 100% rename from openedx_learning/rest_api/v1/__init__.py rename to openedx_learning/apps/authoring/publishing/migrations/__init__.py diff --git a/openedx_learning/core/publishing/model_mixins.py b/openedx_learning/apps/authoring/publishing/model_mixins.py similarity index 99% rename from openedx_learning/core/publishing/model_mixins.py rename to openedx_learning/apps/authoring/publishing/model_mixins.py index ce32cd45e..ab5672f1a 100644 --- a/openedx_learning/core/publishing/model_mixins.py +++ b/openedx_learning/apps/authoring/publishing/model_mixins.py @@ -11,6 +11,12 @@ from .models import PublishableEntity, PublishableEntityVersion +__all__ = [ + "PublishableEntityMixin", + "PublishableEntityVersionMixin", + "PublishableContentModelRegistry", +] + class PublishableEntityMixin(models.Model): """ diff --git a/openedx_learning/core/publishing/models.py b/openedx_learning/apps/authoring/publishing/models.py similarity index 99% rename from openedx_learning/core/publishing/models.py rename to openedx_learning/apps/authoring/publishing/models.py index c6e82560d..d62ba012f 100644 --- a/openedx_learning/core/publishing/models.py +++ b/openedx_learning/apps/authoring/publishing/models.py @@ -23,6 +23,16 @@ manual_date_time_field, ) +__all__ = [ + "LearningPackage", + "PublishableEntity", + "PublishableEntityVersion", + "Draft", + "PublishLog", + "PublishLogRecord", + "Published", +] + class LearningPackage(models.Model): # type: ignore[django-manager-missing] """ diff --git a/openedx_learning/core/publishing/readme.rst b/openedx_learning/apps/authoring/publishing/readme.rst similarity index 100% rename from openedx_learning/core/publishing/readme.rst rename to openedx_learning/apps/authoring/publishing/readme.rst diff --git a/openedx_learning/contrib/media_server/views.py b/openedx_learning/contrib/media_server/views.py index 79a8abdd5..542f041ae 100644 --- a/openedx_learning/contrib/media_server/views.py +++ b/openedx_learning/contrib/media_server/views.py @@ -8,7 +8,7 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.http import FileResponse, Http404 -from openedx_learning.core.components.api import look_up_component_version_content +from openedx_learning.apps.authoring.components.api import look_up_component_version_content def component_asset( diff --git a/openedx_learning/rest_api/apps.py b/openedx_learning/rest_api/apps.py deleted file mode 100644 index dd3da9955..000000000 --- a/openedx_learning/rest_api/apps.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -Django metadata for the Learning Core REST API app -""" -from django.apps import AppConfig - - -class RESTAPIConfig(AppConfig): - """ - Configuration for the Learning Core REST API Django app. - """ - - name = "openedx_learning.rest_api" - verbose_name = "Learning Core: REST API" - default_auto_field = "django.db.models.BigAutoField" diff --git a/openedx_learning/rest_api/urls.py b/openedx_learning/rest_api/urls.py deleted file mode 100644 index 4c7fe274f..000000000 --- a/openedx_learning/rest_api/urls.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -URLs for the Learning Core REST API -""" -from django.urls import include, path - -urlpatterns = [path("v1/", include("openedx_learning.rest_api.v1.urls"))] diff --git a/openedx_learning/rest_api/v1/components.py b/openedx_learning/rest_api/v1/components.py deleted file mode 100644 index d522f03ad..000000000 --- a/openedx_learning/rest_api/v1/components.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -This is just an example REST API endpoint. -""" -from rest_framework import viewsets - -from openedx_learning.core.components.models import Component - - -class ComponentViewSet(viewsets.ViewSet): - """ - Example endpoints for a Components REST API. Not implemented. - """ - def list(self, request): - _items = Component.objects.all() - raise NotImplementedError - - def retrieve(self, request, pk=None): - raise NotImplementedError - - def create(self, request): - raise NotImplementedError - - def update(self, request, pk=None): - raise NotImplementedError - - def partial_update(self, request, pk=None): - raise NotImplementedError - - def destroy(self, request, pk=None): - raise NotImplementedError diff --git a/openedx_learning/rest_api/v1/urls.py b/openedx_learning/rest_api/v1/urls.py deleted file mode 100644 index b1f5e3768..000000000 --- a/openedx_learning/rest_api/v1/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -URLs for the Learning Core REST API v1 -""" -from rest_framework.routers import DefaultRouter - -from . import components - -router = DefaultRouter() -router.register(r"components", components.ComponentViewSet, basename="component") -urlpatterns = router.urls diff --git a/projects/dev.py b/projects/dev.py index 2dacb56bd..5fa8034f2 100644 --- a/projects/dev.py +++ b/projects/dev.py @@ -31,9 +31,9 @@ "django.contrib.admin", "django.contrib.admindocs", # Learning Core Apps - "openedx_learning.core.components.apps.ComponentsConfig", - "openedx_learning.core.contents.apps.ContentsConfig", - "openedx_learning.core.publishing.apps.PublishingConfig", + "openedx_learning.apps.authoring.components.apps.ComponentsConfig", + "openedx_learning.apps.authoring.contents.apps.ContentsConfig", + "openedx_learning.apps.authoring.publishing.apps.PublishingConfig", # Learning Contrib Apps "openedx_learning.contrib.media_server.apps.MediaServerConfig", # Apps that don't belong in this repo in the long term, but are here to make @@ -41,7 +41,7 @@ "olx_importer.apps.OLXImporterConfig", # REST API "rest_framework", - "openedx_learning.rest_api.apps.RESTAPIConfig", + # django-rules based authorization 'rules.apps.AutodiscoverRulesConfig', # Tagging Core Apps diff --git a/projects/urls.py b/projects/urls.py index cf95abb13..583b004b4 100644 --- a/projects/urls.py +++ b/projects/urls.py @@ -8,7 +8,6 @@ path("admin/doc/", include("django.contrib.admindocs.urls")), path("admin/", admin.site.urls), path("media_server/", include("openedx_learning.contrib.media_server.urls")), - path("rest_api/", include("openedx_learning.rest_api.urls")), path("tagging/rest_api/", include("openedx_tagging.core.tagging.urls")), path('__debug__/', include('debug_toolbar.urls')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/setup.py b/setup.py index 78efb73d5..229374976 100755 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ def is_requirement(line): setup( name='openedx-learning', version=VERSION, - description="""An experiment.""", + description="""Open edX Learning Core and Tagging.""", long_description=README + '\n\n' + CHANGELOG, author='David Ormsbee', author_email='dave@tcril.org', diff --git a/test_settings.py b/test_settings.py index 67e87fc96..6be51e88e 100644 --- a/test_settings.py +++ b/test_settings.py @@ -40,9 +40,9 @@ def root(*args): # django-rules based authorization 'rules.apps.AutodiscoverRulesConfig', # Our own apps - "openedx_learning.core.components.apps.ComponentsConfig", - "openedx_learning.core.contents.apps.ContentsConfig", - "openedx_learning.core.publishing.apps.PublishingConfig", + "openedx_learning.apps.authoring.components.apps.ComponentsConfig", + "openedx_learning.apps.authoring.contents.apps.ContentsConfig", + "openedx_learning.apps.authoring.publishing.apps.PublishingConfig", "openedx_tagging.core.tagging.apps.TaggingConfig", ] diff --git a/tests/openedx_learning/core/__init__.py b/tests/openedx_learning/apps/__init__.py similarity index 100% rename from tests/openedx_learning/core/__init__.py rename to tests/openedx_learning/apps/__init__.py diff --git a/tests/openedx_learning/core/components/__init__.py b/tests/openedx_learning/apps/authoring/__init__.py similarity index 100% rename from tests/openedx_learning/core/components/__init__.py rename to tests/openedx_learning/apps/authoring/__init__.py diff --git a/tests/openedx_learning/core/contents/__init__.py b/tests/openedx_learning/apps/authoring/components/__init__.py similarity index 100% rename from tests/openedx_learning/core/contents/__init__.py rename to tests/openedx_learning/apps/authoring/components/__init__.py diff --git a/tests/openedx_learning/core/components/test_api.py b/tests/openedx_learning/apps/authoring/components/test_api.py similarity index 97% rename from tests/openedx_learning/core/components/test_api.py rename to tests/openedx_learning/apps/authoring/components/test_api.py index e8e218257..a6fca7d83 100644 --- a/tests/openedx_learning/core/components/test_api.py +++ b/tests/openedx_learning/apps/authoring/components/test_api.py @@ -5,12 +5,12 @@ from django.core.exceptions import ObjectDoesNotExist -from openedx_learning.core.components import api as components_api -from openedx_learning.core.components.models import Component, ComponentType -from openedx_learning.core.contents import api as contents_api -from openedx_learning.core.contents.models import MediaType -from openedx_learning.core.publishing import api as publishing_api -from openedx_learning.core.publishing.models import LearningPackage +from openedx_learning.apps.authoring.components import api as components_api +from openedx_learning.apps.authoring.components.models import Component, ComponentType +from openedx_learning.apps.authoring.contents import api as contents_api +from openedx_learning.apps.authoring.contents.models import MediaType +from openedx_learning.apps.authoring.publishing import api as publishing_api +from openedx_learning.apps.authoring.publishing.models import LearningPackage from openedx_learning.lib.test_utils import TestCase diff --git a/tests/openedx_learning/core/components/test_models.py b/tests/openedx_learning/apps/authoring/components/test_models.py similarity index 88% rename from tests/openedx_learning/core/components/test_models.py rename to tests/openedx_learning/apps/authoring/components/test_models.py index 322f235ba..f087d4f3a 100644 --- a/tests/openedx_learning/core/components/test_models.py +++ b/tests/openedx_learning/apps/authoring/components/test_models.py @@ -3,13 +3,13 @@ """ from datetime import datetime, timezone -from openedx_learning.core.components.api import ( +from openedx_learning.apps.authoring.components.api import ( create_component_and_version, get_component, get_or_create_component_type, ) -from openedx_learning.core.components.models import ComponentType -from openedx_learning.core.publishing.api import LearningPackage, create_learning_package, publish_all_drafts +from openedx_learning.apps.authoring.components.models import ComponentType +from openedx_learning.apps.authoring.publishing.api import LearningPackage, create_learning_package, publish_all_drafts from openedx_learning.lib.test_utils import TestCase diff --git a/tests/openedx_learning/core/publishing/__init__.py b/tests/openedx_learning/apps/authoring/contents/__init__.py similarity index 100% rename from tests/openedx_learning/core/publishing/__init__.py rename to tests/openedx_learning/apps/authoring/contents/__init__.py diff --git a/tests/openedx_learning/core/contents/test_media_types.py b/tests/openedx_learning/apps/authoring/contents/test_media_types.py similarity index 92% rename from tests/openedx_learning/core/contents/test_media_types.py rename to tests/openedx_learning/apps/authoring/contents/test_media_types.py index 3a7c93e1b..6f9b16b30 100644 --- a/tests/openedx_learning/core/contents/test_media_types.py +++ b/tests/openedx_learning/apps/authoring/contents/test_media_types.py @@ -1,7 +1,7 @@ """ A few tests to make sure our MediaType lookups are working as expected. """ -from openedx_learning.core.contents import api as contents_api +from openedx_learning.apps.authoring.contents import api as contents_api from openedx_learning.lib.test_utils import TestCase diff --git a/tests/openedx_learning/apps/authoring/publishing/__init__.py b/tests/openedx_learning/apps/authoring/publishing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/openedx_learning/core/publishing/test_api.py b/tests/openedx_learning/apps/authoring/publishing/test_api.py similarity index 98% rename from tests/openedx_learning/core/publishing/test_api.py rename to tests/openedx_learning/apps/authoring/publishing/test_api.py index cdcfd459c..03d9ebf06 100644 --- a/tests/openedx_learning/core/publishing/test_api.py +++ b/tests/openedx_learning/apps/authoring/publishing/test_api.py @@ -9,8 +9,8 @@ import pytest from django.core.exceptions import ValidationError -from openedx_learning.core.publishing import api as publishing_api -from openedx_learning.core.publishing.models import LearningPackage, PublishableEntity +from openedx_learning.apps.authoring.publishing import api as publishing_api +from openedx_learning.apps.authoring.publishing.models import LearningPackage, PublishableEntity from openedx_learning.lib.test_utils import TestCase @@ -72,7 +72,7 @@ def test_auto_datetime(self) -> None: # Auto-generated datetime checking... assert isinstance(package.created, datetime) assert package.created == package.updated - assert package.created.tzinfo == timezone.utc # pylint: disable=no-member + assert package.created.tzinfo == timezone.utc # pylint: disable=no-member,useless-suppression # Should be auto-generated assert isinstance(package.uuid, UUID) diff --git a/tests/openedx_tagging/core/tagging/test_system_defined_models.py b/tests/openedx_tagging/core/tagging/test_system_defined_models.py index 7d126f4d2..f771d82ee 100644 --- a/tests/openedx_tagging/core/tagging/test_system_defined_models.py +++ b/tests/openedx_tagging/core/tagging/test_system_defined_models.py @@ -9,7 +9,7 @@ import pytest from django.test import TestCase, override_settings -from openedx_learning.core.publishing.models import LearningPackage +from openedx_learning.apps.authoring.publishing.models import LearningPackage from openedx_tagging.core.tagging import api from openedx_tagging.core.tagging.models import Taxonomy from openedx_tagging.core.tagging.models.system_defined import ModelSystemDefinedTaxonomy, UserSystemDefinedTaxonomy