Skip to content

Commit

Permalink
Set chapter as module model answer
Browse files Browse the repository at this point in the history
  • Loading branch information
Mikael-Lenander committed Aug 29, 2023
1 parent 08663d3 commit b39d1bd
Show file tree
Hide file tree
Showing 13 changed files with 481 additions and 43 deletions.
26 changes: 26 additions & 0 deletions course/migrations/0058_coursemodule_model_answer_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 4.2.3 on 2023-08-29 06:53

from django.db import migrations
import django.db.models.deletion
import lib.fields


class Migration(migrations.Migration):

dependencies = [
('exercise', '0047_lti1p3exercise'),
('course', '0057_usertagging_rename_index'),
]

operations = [
migrations.AddField(
model_name='coursemodule',
name='model_answer',
field=lib.fields.DefaultForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='model_answer_modules', to='exercise.coursechapter', verbose_name='LABEL_MODEL_ANSWER'),
),
migrations.AddField(
model_name='coursemodule',
name='model_solution_reveal_rule',
field=lib.fields.DefaultOneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='exercise.revealrule', verbose_name='LABEL_MODEL_SOLUTION_REVEAL_RULE'),
),
]
33 changes: 32 additions & 1 deletion course/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from apps.models import BaseTab, BasePlugin
from authorization.models import JWTAccessible
from authorization.object_permissions import register_jwt_accessible_class
from lib.fields import PercentField
from lib.fields import PercentField, DefaultOneToOneField, DefaultForeignKey
from lib.helpers import (
Enum,
get_random_string,
Expand Down Expand Up @@ -1282,6 +1282,23 @@ class CourseModule(UrlMixin, models.Model):
default=0.5,
help_text=_('MODULE_LATE_SUBMISSION_PENALTY_HELPTEXT'),
)
model_answer = DefaultForeignKey(
'exercise.CourseChapter',
verbose_name=_('LABEL_MODEL_ANSWER'),
blank=True,
null=True,
on_delete=models.SET_NULL,
related_name="model_answer_modules",
)
model_solution_reveal_rule = DefaultOneToOneField(
'exercise.RevealRule',
verbose_name=_('LABEL_MODEL_SOLUTION_REVEAL_RULE'),
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True,
)


objects = CourseModuleManager()

Expand Down Expand Up @@ -1317,6 +1334,20 @@ def clean(self):
if errors:
raise ValidationError(errors)

@property
def default_model_solution_reveal_rule(self):
from exercise.models import RevealRule # pylint: disable=import-outside-toplevel
return RevealRule(
trigger=RevealRule.TRIGGER.DEADLINE,
)

@property
def active_model_solution_reveal_rule(self):
return (
self.model_solution_reveal_rule
or self.default_model_solution_reveal_rule
)

@staticmethod
def check_is_open(reading_opening_time, opening_time, closing_time, when=None):
when = when or timezone.now()
Expand Down
30 changes: 28 additions & 2 deletions edit_course/course_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ class Meta:
'closing_time',
'late_submissions_allowed',
'late_submission_deadline',
'late_submission_penalty'
'late_submission_penalty',
'model_answer',
]
widgets = {
'opening_time': DateTimeLocalInput,
Expand All @@ -77,14 +78,39 @@ class Meta:
'late_submission_deadline': DateTimeLocalInput,
}

def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)

from .exercise_forms import RevealRuleForm # pylint: disable=import-outside-toplevel
self.model_solution_form = RevealRuleForm(
data=kwargs.get('data'),
instance=self.instance.active_model_solution_reveal_rule,
prefix='model_solution',
)

def get_fieldsets(self):
return [
{ 'legend':_('HIERARCHY'), 'fields':self.get_fields('status','order','url') },
{ 'legend':_('CONTENT'), 'fields':self.get_fields('name','introduction','points_to_pass') },
{ 'legend':_('CONTENT'), 'fields':
self.get_fields('name','introduction','points_to_pass', 'model_answer') },
{ 'legend':_('REVEAL_MODEL_SOLUTIONS'), 'fields': self.model_solution_form },
{ 'legend':_('SCHEDULE'), 'fields':self.get_fields('reading_opening_time','opening_time','closing_time',
'late_submissions_allowed','late_submission_deadline', 'late_submission_penalty') },
]

def is_valid(self) -> bool:
return (
super().is_valid()
and self.model_solution_form.is_valid()
)

def save(self, *args: Any, **kwargs: Any) -> Any:
if self.model_solution_form.has_changed():
self.instance.model_solution_reveal_rule = (
self.model_solution_form.save(*args, **kwargs)
)
return super().save(*args, **kwargs)


class CourseInstanceForm(forms.ModelForm):

Expand Down
62 changes: 49 additions & 13 deletions exercise/cache/points.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
from django.db.models.signals import post_save, post_delete, m2m_changed
from django.utils import timezone

from course.models import CourseInstance
from course.models import CourseInstance, CourseModule
from deviations.models import DeadlineRuleDeviation, MaxSubmissionsRuleDeviation, SubmissionRuleDeviation
from lib.cache import CachedAbstract
from lib.helpers import format_points
from notification.models import Notification
from userprofile.models import UserProfile
from ..models import BaseExercise, Submission, RevealRule
from ..reveal_states import ExerciseRevealState
from ..reveal_states import ExerciseRevealState, ModuleRevealState
from .content import CachedContent
from .hierarchy import ContentMixin

Expand Down Expand Up @@ -104,13 +104,19 @@ def _generate_data( # pylint: disable=arguments-differ
MaxSubmissionsRuleDeviation.objects
.get_max_deviations(user.userprofile, exercises)
)
module_instances = list(
instance.course_modules.all()
)
else:
submissions = []
deadline_deviations = []
submission_deviations = []
module_instances = []

# Generate the staff and student version of the cache, and merge them.
generate_args = (user.is_authenticated, submissions, deadline_deviations, submission_deviations)
generate_args = (
user.is_authenticated, submissions, deadline_deviations, submission_deviations, module_instances
)
staff_data = self._generate_data_internal(True, *generate_args)
student_data = self._generate_data_internal(False, *generate_args)
self._pack_tuples(staff_data, student_data) # Now staff_data is the final, combined data.
Expand All @@ -136,14 +142,15 @@ def _generate_data_internal( # noqa: MC0001
submissions: Iterable[Submission],
deadline_deviations: Iterable[DeadlineRuleDeviation],
submission_deviations: Iterable[MaxSubmissionsRuleDeviation],
module_instances: Iterable[CourseModule],
) -> Dict[str, Any]:
"""
Handles the generation of one version of the cache (staff or student).
All source data is prefetched by `_generate_data` and provided as
arguments to this method.
"""
data = deepcopy(self.content.data)
module_index = data['module_index'] # pylint: disable=unused-variable
module_index = data['module_index']
exercise_index = data['exercise_index']
modules = data['modules']
categories = data['categories']
Expand Down Expand Up @@ -171,6 +178,9 @@ def r_augment(children: List[Dict[str, Any]]) -> None:
'feedback_revealed': True,
'feedback_reveal_time': None,
})
entry.update({
'is_revealed': True,
})
r_augment(entry.get('children'))
for module in modules:
module.update({
Expand Down Expand Up @@ -232,6 +242,17 @@ def r_augment(children: List[Dict[str, Any]]) -> None:
final_submission = None
last_submission = None

def update_invalidation_time(invalidate_time: Optional[datetime.datetime]) -> None:
if (
invalidate_time is not None
and invalidate_time > timezone.now()
and (
data['invalidate_time'] is None
or invalidate_time < data['invalidate_time']
)
):
data['invalidate_time'] = invalidate_time

def check_reveal_rule() -> None:
"""
Evaluate the reveal rule of the current exercise and ensure
Expand Down Expand Up @@ -262,15 +283,7 @@ def check_reveal_rule() -> None:

# If the reveal rule depends on time, update the cache's
# invalidation time.
if (
reveal_time is not None
and reveal_time > timezone.now()
and (
data['invalidate_time'] is None
or reveal_time < data['invalidate_time']
)
):
data['invalidate_time'] = reveal_time
update_invalidation_time(reveal_time)

# Augment submission data.
for submission in submissions:
Expand Down Expand Up @@ -379,6 +392,29 @@ def check_reveal_rule() -> None:
if exercise is not None and not is_staff:
check_reveal_rule()

if not is_staff:
def update_is_revealed_recursive(entry: Dict[str, Any], is_revealed: bool) -> None:
if is_revealed:
return
entry.update({
'is_revealed': is_revealed,
})
for child in entry.get('children', []):
update_is_revealed_recursive(child, is_revealed)

for module in module_instances:
model_chapter = module.model_answer
if model_chapter is None:
continue
reveal_rule = module.active_model_solution_reveal_rule
entry = self._by_idx(modules, exercise_index[model_chapter.id])[-1]
cached_module = self._by_idx(modules, module_index[module.id])[-1]
state = ModuleRevealState(cached_module)
is_revealed = reveal_rule.is_revealed(state)
reveal_time = reveal_rule.get_reveal_time(state)
update_is_revealed_recursive(entry, is_revealed)
update_invalidation_time(reveal_time)

# Confirm points.
def r_check(parent: Dict[str, Any], children: List[Dict[str, Any]]) -> None:
for entry in children:
Expand Down
22 changes: 22 additions & 0 deletions exercise/exercise_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,16 @@ def course_instance(self):
def is_submittable(self):
return False

def can_be_shown_as_module_model_solution(self, user: User) -> bool:
"""
If this exercise's parent chapter is a model solution to modules, check if the chapter
can be revealed to the user according to the module's reveal rule.
"""
if self.parent is not None:
return self.parent.can_be_shown_as_module_model_solution(user)
return True


def is_empty(self):
return not self.service_url and self.as_leaf_class()._is_empty()

Expand Down Expand Up @@ -441,6 +451,18 @@ class Meta:
verbose_name = _('MODEL_NAME_COURSE_CHAPTER')
verbose_name_plural = _('MODEL_NAME_COURSE_CHAPTER_PLURAL')

def can_be_shown_as_module_model_solution(self, user: User) -> bool:
"""
If this chapter is a model solution to modules, check if the chapter
can be revealed to the user according to the module's reveal rule.
"""
from .cache.content import CachedContent # pylint: disable=import-outside-toplevel
from .cache.points import CachedPoints # pylint: disable=import-outside-toplevel
content = CachedContent(self.course_instance)
points = CachedPoints(self.course_instance, user, content)
entry, _, _, _ = points.find(self)
return entry['is_revealed']

def _is_empty(self):
return not self.generate_table_of_contents

Expand Down
4 changes: 4 additions & 0 deletions exercise/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ def is_object_visible(self, request, view, exercise): # pylint: disable=argument
self.error_msg(_('EXERCISE_VISIBLITY_ERROR_ONLY_EXTERNAL_USERS'))
return False

if not exercise.can_be_shown_as_module_model_solution(user):
self.error_msg(_('MODULE_MODEL_ANSWER_VISIBILITY_PERMISSION_DENIED_MSG'))
return False

return True


Expand Down
Loading

0 comments on commit b39d1bd

Please sign in to comment.