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
4 changes: 3 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
[run]
branch = True
data_file = .coverage
source=openedx_learning
source =
openedx_learning
openedx_tagging
omit =
test_settings
*migrations*
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ include LICENSE.txt
include README.rst
include requirements/base.in
recursive-include openedx_learning *.html *.png *.gif *.js *.css *.jpg *.jpeg *.svg *.py
recursive-include openedx_tagging *.html *.png *.gif *.js *.css *.jpg *.jpeg *.svg *.py
14 changes: 11 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ docs: ## generate Sphinx HTML documentation, including API docs
$(BROWSER)docs/_build/html/index.html

# Define PIP_COMPILE_OPTS=-v to get more information during make upgrade.
PIP_COMPILE = pip-compile --rebuild --upgrade $(PIP_COMPILE_OPTS)
PIP_COMPILE = pip-compile --rebuild $(PIP_COMPILE_OPTS)

upgrade: export CUSTOM_COMPILE_COMMAND=make upgrade
upgrade: ## update the requirements/*.txt files with the latest packages satisfying requirements/*.in
compile-requirements: export CUSTOM_COMPILE_COMMAND=make upgrade
compile-requirements: ## update the requirements/*.txt files with the latest packages satisfying requirements/*.in
pip install -qr requirements/pip-tools.txt
# Make sure to compile files after any other files they include!
$(PIP_COMPILE) -o requirements/pip-tools.txt requirements/pip-tools.in
Expand All @@ -47,6 +47,9 @@ upgrade: ## update the requirements/*.txt files with the latest packages satisfy
sed '/^[dD]jango==/d' requirements/test.txt > requirements/test.tmp
mv requirements/test.tmp requirements/test.txt

upgrade: ## update the pip requirements files to use the latest releases satisfying our constraints
make compile-requirements PIP_COMPILE_OPTS="--upgrade"

quality: ## check coding style with pycodestyle and pylint
tox -e quality

Expand Down Expand Up @@ -77,12 +80,16 @@ extract_translations: ## extract strings to be translated, outputting .mo files
rm -rf docs/_build
cd openedx_learning && ../manage.py makemessages -l en -v1 -d django
cd openedx_learning && ../manage.py makemessages -l en -v1 -d djangojs
cd openedx_tagging && ../manage.py makemessages -l en -v1 -d django
cd openedx_tagging && ../manage.py makemessages -l en -v1 -d djangojs

compile_translations: ## compile translation files, outputting .po files for each supported language
cd openedx_learning && ../manage.py compilemessages
cd openedx_tagging && ../manage.py compilemessages

detect_changed_source_translations:
cd openedx_learning && i18n_tool changed
cd openedx_tagging && i18n_tool changed

pull_translations: ## pull translations from Transifex
tx pull -a -f -t --mode reviewed
Expand All @@ -92,6 +99,7 @@ push_translations: ## push source translation files (.po) from Transifex

dummy_translations: ## generate dummy translation (.po) files
cd openedx_learning && i18n_tool dummy
cd openedx_tagging && i18n_tool dummy

build_dummy_translations: extract_translations dummy_translations compile_translations ## generate and compile dummy translation files

Expand Down
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ 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_tagging.core`` contains the core Tagging app, which provides data structures and apis for tagging Open edX objects.

App Dependencies
~~~~~~~~~~~~~~~~
Expand Down
3 changes: 3 additions & 0 deletions docs/decisions/0009-tagging-administrators.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ But because permissions #2 + #3 require access to the edx-platform CMS model `Co

Per `OEP-9`_, ``openedx_tagging`` will allow applications to use the standard Django API to query permissions, for example: ``user.has_perm('openedx_tagging.edit_taxonomy', taxonomy)``, and the appropriate permissions will be applied in that application's context.

These rules will be enforced in the tagging `views`_, not the API or models, so that external code using this library need not have a logged-in user in order to call the API. So please use with care.

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.

@ChrisChV CC @ormsbee @bradenmacdonald

Does this seem reasonable?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@pomegranited It's reasonable to me. Maybe it is good to add this also as a comment in the API so that it is taken into account when using it


Rejected Alternatives
---------------------

Expand All @@ -37,3 +39,4 @@ This is a standard way to grant access in Django apps, but it is not used in Ope
.. _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
.. _OEP-9: https://open-edx-proposals.readthedocs.io/en/latest/best-practices/oep-0009-bp-permissions.html
.. _views: https://github.com/dfunckt/django-rules#permissions-in-views
7 changes: 1 addition & 6 deletions openedx_learning/lib/collations.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ def __init__(self, *args, db_collations=None, db_collation=None, **kwargs):
it for Django 3.2 compatibility (see the ``db_collation`` method
docstring for details).
"""
if db_collation is not None:
raise ValueError(
f"Cannot use db_collation with {self.__class__.__name__}. "
+ "Please use a db_collations dict instead."
)

super().__init__(*args, **kwargs)
self.db_collations = db_collations or {}
Expand Down Expand Up @@ -106,7 +101,7 @@ def db_parameters(self, connection):
def deconstruct(self):
"""
How to serialize our Field for the migration file.

For our mixin fields, this is just doing what the field's superclass
would do and then tacking on our custom ``db_collations`` dict data.
"""
Expand Down
6 changes: 4 additions & 2 deletions openedx_tagging/core/tagging/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
""" Tagging app admin """
from django.contrib import admin

from .models import TagContent
from .models import ObjectTag, Tag, Taxonomy

admin.site.register(TagContent)
admin.site.register(Taxonomy)
admin.site.register(Tag)
admin.site.register(ObjectTag)
109 changes: 109 additions & 0 deletions openedx_tagging/core/tagging/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
""""
Tagging API

Anyone using the openedx_tagging app should use these APIs instead of creating
or modifying the models directly, since there might be other related model
changes that you may not know about.

No permissions/rules are enforced by these methods -- these must be enforced in the views.

Please look at the models.py file for more information about the kinds of data
are stored in this app.
"""
from typing import List, Type

from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _

from .models import ObjectTag, Tag, Taxonomy


def create_taxonomy(
name,
description=None,
enabled=True,
required=False,
allow_multiple=False,
allow_free_text=False,
) -> Taxonomy:
"""
Creates, saves, and returns a new Taxonomy with the given attributes.
"""
return Taxonomy.objects.create(
name=name,
description=description,
enabled=enabled,
required=required,
allow_multiple=allow_multiple,
allow_free_text=allow_free_text,
)


def get_taxonomies(enabled=True) -> QuerySet:
"""
Returns a queryset containing the enabled taxonomies, sorted by name.
If you want the disabled taxonomies, pass enabled=False.
If you want all taxonomies (both enabled and disabled), pass enabled=None.
"""
queryset = Taxonomy.objects.order_by("name", "id")
if enabled is None:
return queryset.all()
return queryset.filter(enabled=enabled)


def get_tags(taxonomy: Taxonomy) -> List[Tag]:
"""
Returns a list of predefined tags for the given taxonomy.

Note that if the taxonomy allows free-text tags, then the returned list will be empty.
"""
return taxonomy.get_tags()


def resync_object_tags(object_tags: QuerySet = None) -> int:
"""
Reconciles ObjectTag entries with any changes made to their associated taxonomies and tags.

By default, we iterate over all ObjectTags. Pass a filtered ObjectTags queryset to limit which tags are resynced.
"""
if not object_tags:
object_tags = ObjectTag.objects.all()

num_changed = 0
for object_tag in object_tags:
changed = object_tag.resync()
if changed:
object_tag.save()
num_changed += 1
return num_changed


def get_object_tags(
taxonomy: Taxonomy, object_id: str, object_type: str, valid_only=True
) -> List[ObjectTag]:
"""
Returns a list of tags for a given taxonomy + content.

Pass valid_only=False when displaying tags to content authors, so they can see invalid tags too.
Invalid tags will likely be hidden from learners.
"""
tags = ObjectTag.objects.filter(
taxonomy=taxonomy, object_id=object_id, object_type=object_type
).order_by("id")
return [tag for tag in tags if not valid_only or taxonomy.validate_object_tag(tag)]


def tag_object(
taxonomy: Taxonomy, tags: List, object_id: str, object_type: str
) -> List[ObjectTag]:
"""
Replaces the existing ObjectTag entries for the given taxonomy + object_id with the given list of tags.

If taxonomy.allows_free_text, then the list should be a list of tag values.
Otherwise, it should be a list of existing Tag IDs.

Raised ValueError if the proposed tags are invalid for this taxonomy.
Preserves existing (valid) tags, adds new (valid) tags, and removes omitted (or invalid) tags.
"""

return taxonomy.tag_object(tags, object_id, object_type)
2 changes: 1 addition & 1 deletion openedx_tagging/core/tagging/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ class TaggingConfig(AppConfig):

name = "openedx_tagging.core.tagging"
verbose_name = "Tagging"
default_auto_field = 'django.db.models.BigAutoField'
default_auto_field = "django.db.models.BigAutoField"
label = "oel_tagging"
Loading