diff --git a/course/models.py b/course/models.py index 0becff550..575f1590c 100644 --- a/course/models.py +++ b/course/models.py @@ -2,7 +2,7 @@ import json import logging import string -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING import urllib.request import urllib.parse from random import choice @@ -44,6 +44,9 @@ from userprofile.models import User, UserProfile, GraderUser from .sis import get_sis_configuration, StudentInfoSystem +if TYPE_CHECKING: + from edit_course.operations.configure import ConfigParts + logger = logging.getLogger('aplus.course') @@ -1113,6 +1116,9 @@ def get_url_kwargs(self): # python dicts in single line: http://stackoverflow.com/a/26853961 return dict(instance_slug=self.url, **self.course.get_url_kwargs()) # pylint: disable=use-dict-literal + # A ConfigPart object is cached for each instance during an update. Cache is cleared + # when the instance is deleted. The edit_course_operations.configure logic is responsible + # for setting the cache on update. @property def config_cache_key(self) -> str: return f"instance.config.{self.id}" @@ -1120,10 +1126,10 @@ def config_cache_key(self) -> str: def delete_cached_config(self) -> None: cache.delete(self.config_cache_key) - def set_cached_config(self, obj: Any) -> None: + def set_cached_config(self, obj: "ConfigParts") -> None: cache.set(self.config_cache_key, obj) - def get_cached_config(self, default=None) -> Any: + def get_cached_config(self, default=None) -> Optional["ConfigParts"]: return cache.get(self.config_cache_key, default) diff --git a/edit_course/operations/configure.py b/edit_course/operations/configure.py index 4b9a01507..0bf6f8c30 100644 --- a/edit_course/operations/configure.py +++ b/edit_course/operations/configure.py @@ -408,7 +408,10 @@ class ConfigParts: the lobject class type. - *_keys fields contain whatever the config contained. I.e. they contain the set of all such keys. E.g. module_keys contains the keys of - all modules in the config. + all modules in the config. They can be used to determined whether + a module, exercise, etc. still exists in the course config even if nothing + has changed (e.g. module config disappears from the modules field in the + .diff() result if it had no changes). """ config: Dict[Any, Any] @@ -490,7 +493,8 @@ def get_children(config, n = 0): ) @staticmethod - def diff(old: Optional["ConfigParts"], new: "ConfigParts"): + def diff(old: Optional["ConfigParts"], new: "ConfigParts") -> "ConfigParts": + """Return a new ConfigParts object containing the differences between the old and new configs""" if old is None: return new @@ -502,6 +506,7 @@ def diff(old: Optional["ConfigParts"], new: "ConfigParts"): keep_unchanged=True ) + # Get the changes between the configs categories_config = get_config_changes(old.categories, new.categories) if categories_config is None: categories_config = {} @@ -550,8 +555,11 @@ def configure(instance: CourseInstance, new_config: dict) -> Tuple[bool, List[st errors.append(_('COURSE_CONFIG_ERROR_MODULES_REQUIRED_ARRAY')) return False, errors + # Get the previous config (the one used in the last successful update) old_cparts = instance.get_cached_config() new_cparts = ConfigParts.from_config(new_config, errors) + # Get the changes between the new config and the previous config with some auxiliary information. + # See ConfigParts and ConfigParts.diff for more info cparts = ConfigParts.diff(old_cparts, new_cparts) config = cparts.config @@ -761,6 +769,7 @@ def configure(instance: CourseInstance, new_config: dict) -> Tuple[bool, List[st for child_key in lobject_config["children"] ) + # Delete or hide learning objects that are not included in the module anymore for lobject in outdated_lobjects: if ( not isinstance(lobject, BaseExercise)