internlm/SWE-Fixer-Train-110K huggingface dataset:
Repository: apluslms/a-plus

Issue:
Could the submissions list show everything at once instead of only the 50 latest submissions?
There are two types of submission lists: 

1) in the chapter pages, in the exercise frame, the all submissions button opens the 50 latest submissions in a modal dialog.
2) Opening the full submission list takes the user to another page. The link can be found in the Your points page (course menu) and the Edit course page.

The full-view submission list page has a button for loading all submissions (instead of only 50 latest). In the modal (in chapters or participant pages), when the "show all" button is pressed, it loads the separate list page with all submissions.

The teacher would want to see all submissions without the need to open a separate page and even press a button after opening the page (after pressing the button, you need to wait again for the page to load).

From @jsorva:

I wish the submissions list in the top right-hand corner wasn’t 
folded by default (or that I could adjust whether it is folded by 
default, for me). Having the full submission history at a glance is 
frequently quite useful. For me at least, the list is definitely worth 
the screen space.

Internal ticket: https://rt.cs.aalto.fi/Ticket/Display.html?id=19964

The "top right-hand corner" refers to the corners of the modal dialog (the feedback modal in the participant page).


Discussion:

Resolving patch:
diff --git a/exercise/staff_views.py b/exercise/staff_views.py
--- a/exercise/staff_views.py
+++ b/exercise/staff_views.py
@@ -92,14 +92,15 @@ def get_common_objects(self) -> None:
 
         for submission in qs:
             format_submission(submission, self.pseudonymize)
-        self.all = self.request.GET.get('all', None)
-        self.all_url = self.exercise.get_submission_list_url() + "?all=yes"
-        self.submissions = qs if self.all else qs[:self.default_limit]
+        self.limited = self.request.GET.get('limited', False)
+        self.not_all_url = self.exercise.get_submission_list_url() + "?limited=true"
+        self.all_url = self.exercise.get_submission_list_url()
+        self.submissions = qs[:self.default_limit] if self.limited else qs
         self.count = len(self.submissions)
         self.percentage_graded = (
             f"{graded_submitters} / {total_submitters} ({int(graded_submitters / total_submitters * 100)}%)"
         )
-        self.note("all", "all_url", "submissions", "default_limit", "count", "percentage_graded")
+        self.note("limited", "not_all_url", "all_url", "submissions", "default_limit", "count", "percentage_graded")
 
 
 class SubmissionsSummaryView(ExerciseBaseView):


Test patch:
Repository: apluslms/a-plus

Issue:
Course exercise list API must not show hidden course modules to students
From the comment https://github.com/apluslms/a-plus/pull/1090#issuecomment-1615351930

After the pull request #1090, `CachedContent` includes hidden modules too. Students must not see hidden modules in the course exercise list API, but now they do. Perhaps the hidden modules should be filtered out here (in the API class `CourseExercisesViewSet`):


https://github.com/apluslms/a-plus/blob/ebf3bc012f61f219893d1e00a86acb00f08aa84e/course/api/views.py#L259-L260

`CourseModule.status` defines what is visible or hidden.
https://github.com/apluslms/a-plus/blob/ebf3bc012f61f219893d1e00a86acb00f08aa84e/course/models.py#L1212-L1228


Discussion:

Resolving patch:
diff --git a/course/api/views.py b/course/api/views.py
--- a/course/api/views.py
+++ b/course/api/views.py
@@ -257,7 +257,8 @@ def __module_to_dict(self, module, **kwargs):
     def list(self, request, *args, **kwargs):
         modules = []
         for module in self.content.data['modules']:
-            modules.append(self.__module_to_dict(module, **kwargs))
+            if module['status'] != CourseModule.STATUS.HIDDEN:
+                modules.append(self.__module_to_dict(module, **kwargs))
         return Response({"count": len(modules), "next": None, "previous": None, 'results': modules})
 
     def retrieve(self, request, *args, **kwargs):


Test patch:
Repository: apluslms/a-plus

Issue:
Language suffix is removed from remote page links on single language courses
Language suffix should only be stripped from remote page link URLs on multilingual courses.

This has been a problem on Traky/CS-A1143, because of how it is setup with two similar courses in one Git repository. The other course is single language, while the other one is multilingual. The single language course has links with the suffix `_en`, such as https://plus.cs.aalto.fi/a1143/2023/lisatehtavat/hylje_en/#sealsimulator. This link should not have the `_en` suffix removed from `hylje_en` by the `RemotePage` method `_fix_relative_urls()`.


Discussion:

Resolving patch:
diff --git a/exercise/protocol/aplus.py b/exercise/protocol/aplus.py
--- a/exercise/protocol/aplus.py
+++ b/exercise/protocol/aplus.py
@@ -164,7 +164,8 @@ def parse_page_content(
         page.is_accepted = True
         page.is_wait = False
 
-    remote_page.fix_relative_urls()
+    is_multilingual_course = len(exercise.course_module.course_instance.languages) > 1
+    remote_page.fix_relative_urls(is_multilingual_course)
     remote_page.find_and_replace('data-aplus-exercise', [{
         'id': ('chapter-exercise-' + str(o.order)),
         'data-aplus-exercise': o.get_absolute_url(),
diff --git a/lib/remote_page.py b/lib/remote_page.py
--- a/lib/remote_page.py
+++ b/lib/remote_page.py
@@ -210,7 +210,7 @@ def _remove_id_attr(
     def body(self) -> Optional[Tag]:
         return self.soup.body if self.soup else None
 
-    def fix_relative_urls(self):
+    def fix_relative_urls(self, is_multilingual_course):
         url = self.base_address()
         for tag,attr in [
             ("img","src"),
@@ -221,9 +221,10 @@ def fix_relative_urls(self):
             ("video","poster"),
             ("source","src"),
         ]:
-            self._fix_relative_urls(url, tag, attr)
+            self._fix_relative_urls(url, tag, attr, is_multilingual_course)
 
-    def _fix_relative_urls(self, url, tag_name, attr_name): # pylint: disable=too-many-locals too-many-branches
+    # pylint: disable-next=too-many-locals too-many-branches
+    def _fix_relative_urls(self, url, tag_name, attr_name, is_multilingual_course):
         # Starts with "#", "//" or "https:".
         test = re.compile('^(#|\/\/|\w+:)', re.IGNORECASE) # noqa: W605
         # Ends with filename extension ".html" and possibly "#anchor".
@@ -294,7 +295,7 @@ def _fix_relative_urls(self, url, tag_name, attr_name): # pylint: disable=too-ma
                 # Remove lang suffix in chapter2_en#anchor without modifying the #anchor.
                 # Add slash / to the end before the #anchor.
                 m = lang_suffix.search(new_val)
-                if m:
+                if m and is_multilingual_course:
                     anchor = m.group('anchor')
                     if anchor is None:
                         anchor = ''


Test patch:
Repository: apluslms/a-plus

Issue:
Error message garbled when graders down
When there’s grader trouble — like there is now — A+ shows a banner warning at the top. I assume this is an automatic message that is shown to students as well. Which is a good idea... probably, maybe.

But the message should not look like this:
![image](https://github.com/apluslms/a-plus/assets/8523129/0c0ee76e-f22d-45c2-bce0-567167d5b6bc)



Discussion:
Is the list format a problem or only the dump of all the language versions of the names?

It is easy to fix the multilingual names. The alert can render the assignment names in the currently active language.

https://github.com/apluslms/a-plus/blob/a932c5104504f3367e81448ca8a42584f88f27a1/exercise/submission_models.py#L852

`pick_localized()` selects one language version from the name (or the whole name if there are no language versions).
Something like:
```
lang = get_language()
pick_localized(entry['submission__exercise__name'], lang)
```

https://github.com/apluslms/a-plus/blob/a932c5104504f3367e81448ca8a42584f88f27a1/lib/localization_syntax.py#L13-L30
The language dump is the main problem. I don’t know how useful the list really is — I guess it depends on what sort of problem(s) the grader(s) have. I guess it’s OK to list the assignments, even though it’s not particularly informative in the case of a course like O1 (with a zillion assignments that generally break all at once if there’s grader trouble)

Resolving patch:
diff --git a/exercise/submission_models.py b/exercise/submission_models.py
--- a/exercise/submission_models.py
+++ b/exercise/submission_models.py
@@ -26,6 +26,7 @@
     safe_file_name,
     Enum,
 )
+from lib.localization_syntax import pick_localized
 from lib.models import UrlMixin
 from userprofile.models import UserProfile
 from aplus.celery import retry_submissions
@@ -873,7 +874,11 @@ def get_exercise_names_if_grader_is_unstable(self, instance):
         total_retries = sum(entry['num_retries'] for entry in total_retries_per_exercise)
         # Check if the grader can be considered unstable on this course instance
         if total_retries > settings.GRADER_STABLE_THRESHOLD:
-            exercises = ", ".join(f"'{entry['submission__exercise__name']}'" for entry in total_retries_per_exercise)
+            lang = get_language()
+            exercises = ", ".join(
+                f"'{pick_localized(entry['submission__exercise__name'], lang)}'"
+                for entry in total_retries_per_exercise
+            )
             return exercises
         return ''
 


Test patch:
Repository: apluslms/a-plus

Issue:
Hide student notifications coming from courses which are hidden to students
A+ does not delete old notifications from the database. If a student has a notification about a submission in an old course and the course instance is then hidden from students, the student sees the flashing notification icon in the top navigation bar, but is not able to open it (due to `CourseVisiblePermission` that checks the course instance `visible_to_students` boolean field). Thus, the notification will permanently keep flashing in the nav bar.

A simple solution would be to not show notifications if the target course is hidden from students. The code in notification cache could check the target course visibility here (either the target course instance or the course instance of the target submission): https://github.com/apluslms/a-plus/blob/cd10e04963557444efeb11b06b2834bf47ace903/notification/cache.py#L39

A+ notifications are used at least when manual grading results are sent from Rubyric to A+ or when a submission is manually assessed in A+ in the inspect submission page.


Discussion:
Some students occasionally complain about this and they find it very annoying.

Resolving patch:
diff --git a/edit_course/course_forms.py b/edit_course/course_forms.py
--- a/edit_course/course_forms.py
+++ b/edit_course/course_forms.py
@@ -16,6 +16,7 @@
 from lib.validators import generate_url_key_validator
 from lib.fields import UsersSearchSelectField
 from lib.widgets import DateTimeLocalInput
+from notification.cache import CachedNotifications
 from userprofile.models import UserProfile
 from course.sis import get_sis_configuration, StudentInfoSystem
 
@@ -193,6 +194,10 @@ def save(self, *args, **kwargs):
         self.instance.set_assistants(self.cleaned_data['assistants'])
         self.instance.set_teachers(self.cleaned_data['teachers'])
 
+        if not self.instance.visible_to_students:
+            for userprofile in self.instance.all_students:
+                CachedNotifications.invalidate(userprofile.user)
+
         return super().save(*args, **kwargs)
 
 
diff --git a/notification/cache.py b/notification/cache.py
--- a/notification/cache.py
+++ b/notification/cache.py
@@ -35,9 +35,11 @@ def notification_entry(n):
             }
 
         notifications = list(
-            user.userprofile.received_notifications\
-                .filter(seen=False)\
-                .select_related(
+            user.userprofile.received_notifications
+                .filter(
+                    seen=False,
+                    course_instance__visible_to_students=True,
+                ).select_related(
                     'submission',
                     'submission__exercise',
                     'course_instance',


Test patch:
Repository: apluslms/a-plus

Issue:
Module model answer chapter can be from a different course
The module edit page allows the teacher to select the model answer chapter to be from a different course than the one being edited. I do not know whether the teacher needs access to the other course whose chapter is selected or not. However, this is not intended and will cause issues with cache invalidation if someone does this, it is also very annoying for the teacher as finding the correct chapter in the list of chapters can be quite difficult. The UI should not have chapters from other course instances, and the module code needs to make sure that chapter from other course instances are not saved in the model answer field.


Discussion:

Resolving patch:
diff --git a/course/views.py b/course/views.py
--- a/course/views.py
+++ b/course/views.py
@@ -250,8 +250,8 @@ def get_common_objects(self):
         super().get_common_objects()
         self.now = timezone.now()
         try:
-            self.children = self.content.flat_module(self.module)
-            cur, _tree, prev, nex = self.content.find(self.module)
+            self.children = self.points.flat_module(self.module)
+            cur, _tree, prev, nex = self.points.find(self.module)
             self.previous = prev
             self.current = cur
             self.next = nex
diff --git a/edit_course/course_forms.py b/edit_course/course_forms.py
--- a/edit_course/course_forms.py
+++ b/edit_course/course_forms.py
@@ -13,12 +13,13 @@
 
 from aplus.api import api_reverse
 from course.models import LearningObjectCategory, CourseModule, CourseInstance, UserTag
+from course.sis import get_sis_configuration, StudentInfoSystem
+from exercise.models import CourseChapter
 from lib.validators import generate_url_key_validator
 from lib.fields import UsersSearchSelectField
 from lib.widgets import DateTimeLocalInput
 from notification.cache import CachedNotifications
 from userprofile.models import UserProfile
-from course.sis import get_sis_configuration, StudentInfoSystem
 
 
 logger = logging.getLogger("aplus.course")
@@ -88,6 +89,9 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
             instance=self.instance.active_model_solution_reveal_rule,
             prefix='model_solution',
         )
+        self.fields['model_answer'].queryset = CourseChapter.objects.filter(
+            course_module__course_instance=self.instance.course_instance,
+        )
 
     def get_fieldsets(self):
         return [


Test patch:
Repository: apluslms/a-plus

Issue:
Course chapters not accessible in module view
In the course module view the sub-chapters are shown as not accessible even though they should, [see for example here in A+ manual](https://plus.cs.aalto.fi/aplus-manual/master/set_up_environment/). This seems to relate to the [recent modification](https://github.com/apluslms/a-plus/commit/ae9258302f299a3cf148557ee10478c5c4c877ce) that applies revel rules to course chapters, and uses the new is_revealed attribute when deciding on chapter accessibility.

This is particularly harmful in situations when a course module is linked as LTI activity (e.g. from MyCourses), because the chapters in the module have become unaccessible.


Discussion:

Resolving patch:
diff --git a/course/viewbase.py b/course/viewbase.py
--- a/course/viewbase.py
+++ b/course/viewbase.py
@@ -4,6 +4,7 @@
 
 from authorization.permissions import ACCESS
 from exercise.cache.content import CachedContent
+from exercise.cache.points import CachedPoints
 from lib.helpers import remove_query_param_from_url, update_url_params
 from lib.viewbase import BaseTemplateView
 from userprofile.viewbase import UserProfileMixin
@@ -67,11 +68,12 @@ def get_resource_objects(self):
             self.url_without_language = remove_query_param_from_url(self.request.get_full_path(), 'hl')
             self.query_language = None
             self.user_language = None
+            self.points = CachedPoints(self.instance, self.request.user, self.content, self.is_course_staff)
 
             self.note(
                 "course", "instance", "content", "user_course_data", "is_student", "is_assistant",
                 "is_teacher", "is_course_staff", "get_taggings", "url_without_language",
-                "query_language", "user_language"
+                "query_language", "user_language", "points"
             )
 
             # Try to find a language that is defined for this course instance
diff --git a/course/views.py b/course/views.py
--- a/course/views.py
+++ b/course/views.py
@@ -241,8 +241,8 @@ def get_common_objects(self):
         super().get_common_objects()
         self.now = timezone.now()
         try:
-            self.children = self.content.flat_module(self.module)
-            cur, _tree, prev, nex = self.content.find(self.module)
+            self.children = self.points.flat_module(self.module)
+            cur, _tree, prev, nex = self.points.find(self.module)
             self.previous = prev
             self.current = cur
             self.next = nex


Test patch:
Repository: apluslms/a-plus

Issue:
Course staff may create duplicate student groups
Course staff may create student groups (course/models.py class StudentGroup) that contain exactly the same group members as an existing group. Duplicate groups should not be allowed. The course staff UI for editing groups is in the URL http://localhost:8000/def/current/teachers/groups/ (in the left navigation menu, it is the "Groups" link under the heading Course staff).

Course staff may also create new groups (or edit existing groups) that are empty (no members) or only have one member. Groups should always have at least two members.

When students create groups in the "form a group" page (with user personal codes), A+ already prevents empty and duplicate groups.


Discussion:

Resolving patch:
diff --git a/course/forms.py b/course/forms.py
--- a/course/forms.py
+++ b/course/forms.py
@@ -5,6 +5,7 @@
 from django.utils.safestring import mark_safe
 from django.utils.text import format_lazy
 from django.utils.translation import gettext_lazy as _
+from django.db.models import Count
 
 from aplus.api import api_reverse
 from exercise.models import SubmissionDraft
@@ -115,6 +116,30 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
         if self.instance.id:
             self.fields["members"].initial_queryset = self.instance.members.all()
 
+    def clean(self):
+        super().clean()
+        members = self.cleaned_data.get('members')
+        if members:
+            if len(members) == 1:
+                self.add_error('members', _('MUST_HAVE_TWO_MEMBERS'))
+            course_instance = self.instance.course_instance
+            # Filter all groups with course instance and that have one or more similar members as in the members list
+            filtered_groups = StudentGroup.objects.filter(course_instance=course_instance, members__in=members)
+            # Count number of members in each group
+            groups_with_member_count = filtered_groups.annotate(member_count=Count('members'))
+            # Filter only those groups that have same number of members
+            groups_with_exact_member_count = groups_with_member_count.filter(member_count=len(members))
+            # Loop through the returned groups and check if any group with exact same members exist
+            group_exists = False
+            for group in groups_with_exact_member_count:
+                group_members = group.members.all()
+                if list(group_members) == list(members):
+                    group_exists = True
+            if group_exists:
+                self.add_error('members', _('ERROR_GROUP_ALREADY_EXISTS'))
+        return self.cleaned_data
+
+
     class Meta:
         model = StudentGroup
         fields = ['members']


Test patch:
Repository: apluslms/a-plus

Issue:
Binaryornot library sometimes incorrectly classifies PDF files as non-binary
When large PDF files are detected as non-binary data, the inspect submission page tries to render them and causes the page to load slowly.


Discussion:

Resolving patch:
diff --git a/exercise/submission_models.py b/exercise/submission_models.py
--- a/exercise/submission_models.py
+++ b/exercise/submission_models.py
@@ -832,6 +832,9 @@ def get_mime(self):
         return guess_type(self.file_object.path)[0]
 
     def is_passed(self):
+        if self.file_object.path.endswith(".pdf"):
+            # PDF files are sometimes incorrectly classified as non-binary by the 'binaryornot' library
+            return True
         return is_binary(self.file_object.path)
 
 


Test patch:
Repository: apluslms/a-plus

Issue:
The Cancel button in Add Deadline Deviations behaves surprisingly
Just a detail:

If I go to a student’s page, there are now links ther to add deadline deviations for that particular student. (This is super-useful.) 

Now say I click one of those buttons but then wish to cancel. There’s the cancel button there on the add deviations page, but that doesn’t actually cancel my action in the sense of taking me back where I was. It instead sends me to the list of all deviations (which page, in the case of the O1 course, either crashes with a Bad Gateway or takes approximately forever to load). 


Discussion:
The cancel button is simply a link to the deviation list page. There is (currently) no implementation for remembering where the user came from to the form. It is possible to implement it. The "add deviation" form view could read a redirect target from a GET query parameter and use that after the form is submitted. If there is no query parameter, the default could be the list page like it has been. The links to the "add deviation" form would then need to add the GET query parameter to the URL.

The fact that the deadline deviation list loads very slowly (or crashes) on your course is not really related to this issue, but it of course makes the cancel button even more frustrating.

Resolving patch:
diff --git a/deviations/viewbase.py b/deviations/viewbase.py
--- a/deviations/viewbase.py
+++ b/deviations/viewbase.py
@@ -36,6 +36,14 @@ class AddDeviationsView(CourseInstanceMixin, BaseFormView):
     deviation_model: Type[SubmissionRuleDeviation]
     session_key: str
 
+    def get_context_data(self, **kwargs: Any) -> dict:
+        context = super().get_context_data(**kwargs)
+        if self.request.GET.get('previous'):
+            context.update({'cancel_action': self.request.GET.get('previous')})
+        else:
+            context.update({'cancel_action': self.instance.get_url('deviations-list-dl')})
+        return context
+
     def get_form_kwargs(self) -> Dict[str, Any]:
         kwargs = super().get_form_kwargs()
         kwargs["instance"] = self.instance


Test patch: