internlm/SWE-Fixer-Train-110K huggingface dataset:
Repository: carpentries/amy

Issue:
Organisation: display linked workshops by type
In list of organisation's workshops, display a column defining whether event is hosted by the organisation, sponsored, or affiliated.

"affiliated" means the organisation has `Main Organisation` role in the membership and particular event links to that membership.

This depends on #1941.


Discussion:

Resolving patch:
diff --git a/amy/fiscal/views.py b/amy/fiscal/views.py
--- a/amy/fiscal/views.py
+++ b/amy/fiscal/views.py
@@ -70,14 +70,18 @@ class OrganizationDetails(UnquoteSlugMixin, OnlyForAdminsMixin, AMYDetailView):
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
         context["title"] = "Organization {0}".format(self.object)
+        related = ["host", "sponsor", "membership"]
         context["all_events"] = (
-            self.object.hosted_events.all()
+            self.object.hosted_events.select_related(*related)
             .union(
-                self.object.sponsored_events.all(),
-                self.object.administered_events.all(),
+                self.object.sponsored_events.select_related(*related),
+                self.object.administered_events.select_related(*related),
             )
             .prefetch_related("tags")
         )
+        context["main_organisation_memberships"] = Membership.objects.filter(
+            member__role__name="main", member__organization=self.object
+        )
         return context
 
 


Test patch:
diff --git a/amy/fiscal/tests/test_organization.py b/amy/fiscal/tests/test_organization.py
--- a/amy/fiscal/tests/test_organization.py
+++ b/amy/fiscal/tests/test_organization.py
@@ -77,7 +77,7 @@ def test_organization_invalid_chars_in_domain(self):
                 self.assertIn("domain", form.cleaned_data)
                 self.assertEqual(form.cleaned_data["domain"], expected)
 
-    def test_creating_event_with_no_comment(self):
+    def test_creating_organization_with_no_comment(self):
         """Ensure that no comment is added when OrganizationCreateForm without
         comment content is saved."""
         self.assertEqual(Comment.objects.count(), 0)
@@ -90,7 +90,7 @@ def test_creating_event_with_no_comment(self):
         form.save()
         self.assertEqual(Comment.objects.count(), 0)
 
-    def test_creating_event_with_comment(self):
+    def test_creating_organization_with_comment(self):
         """Ensure that a comment is added when OrganizationCreateForm with
         comment content is saved."""
         self.assertEqual(Comment.objects.count(), 0)
Repository: carpentries/amy

Issue:
Membership: agreement document link should never rollover
Any agreement renewal will have a new agreement document, so the link field can be empty when rolling over.


Discussion:

Resolving patch:
diff --git a/amy/fiscal/forms.py b/amy/fiscal/forms.py
--- a/amy/fiscal/forms.py
+++ b/amy/fiscal/forms.py
@@ -235,6 +235,8 @@ class Meta(MembershipCreateForm.Meta):
         fields = [
             "name",
             "consortium",
+            "copy_members",
+            "copy_membership_tasks",
             "public_status",
             "variant",
             "agreement_start",
@@ -252,8 +254,6 @@ class Meta(MembershipCreateForm.Meta):
             "inhouse_instructor_training_seats_rolled_from_previous",
             "emergency_contact",
             "comment",
-            "copy_members",
-            "copy_membership_tasks",
         ]
 
     def __init__(self, *args, **kwargs):
diff --git a/amy/fiscal/views.py b/amy/fiscal/views.py
--- a/amy/fiscal/views.py
+++ b/amy/fiscal/views.py
@@ -377,7 +377,6 @@ def get_initial(self) -> Dict[str, Any]:
             ),
             "contribution_type": self.membership.contribution_type,
             "registration_code": self.membership.registration_code,
-            "agreement_link": self.membership.agreement_link,
             "workshops_without_admin_fee_per_agreement": self.membership.workshops_without_admin_fee_per_agreement,  # noqa
             "workshops_without_admin_fee_rolled_from_previous": 0,
             "public_instructor_training_seats": self.membership.public_instructor_training_seats,  # noqa


Test patch:
Repository: carpentries/amy

Issue:
Membership: pre-populate workshops and IT seats for given membership levels
When user selects a specific membership level, automatically populate the IT public seats and workshops with given values:

Membership level | IT seats (public) | Workshops
------------------|----------------|------------
Bronze                 |  0                    |  2
Silver                   |       6               | 4
Gold                    | 15                   | 6

A warning should be shown to indicate that these numbers have been pre-popuated based on our standard membership packages.


Discussion:

Resolving patch:
diff --git a/amy/fiscal/forms.py b/amy/fiscal/forms.py
--- a/amy/fiscal/forms.py
+++ b/amy/fiscal/forms.py
@@ -201,6 +201,28 @@ def __init__(self, *args, **kwargs):
             "membership."
         ).format(reverse("organization_add"))
 
+        # set up a layout object for the helper
+        self.helper.layout = self.helper.build_default_layout(self)
+
+        # add warning alert for dates falling within next 2-3 months
+        STANDARD_PACKAGES_PREPOPULATION_WARNING = (
+            "<b>Workshops without admin fee per agreement</b> and "
+            "<b>Public instructor training seats</b> were adjusted according to "
+            "standard membership packages."
+        )
+        pos_index = self.helper.layout.fields.index("variant")
+        self.helper.layout.insert(
+            pos_index + 1,
+            Div(
+                Div(
+                    HTML(STANDARD_PACKAGES_PREPOPULATION_WARNING),
+                    css_class="alert alert-warning offset-lg-2 col-lg-8 col-12",
+                ),
+                id="standard_packages_prepopulation_warning",
+                css_class="form-group row d-none",
+            ),
+        )
+
     def save(self, *args, **kwargs):
         res = super().save(*args, **kwargs)
 


Test patch:
Repository: carpentries/amy

Issue:
Update colors of progress states
The current color scheme for our progress states makes it hard to know when something needs further attention and when it does not. Previously we had three colors: Red-Failed, Yellow-Not evaluated yet and Green-Passed. 

The new 'Asked to Repeat' progress state option  is also yellow and so it conflicts with the 'Not Evaluated Yet' option.

**Please update the color for 'Asked to Repeat' to blue.. Any shade will do.**

Progress state colors can be viewed on the [More>Trainees page ](https://amy.carpentries.org/trainings/trainees/)


Discussion:
New color @Talishask:
![obraz](https://user-images.githubusercontent.com/72821/128611076-23afaca3-b8b5-4410-a3aa-548fc64cb7b2.png)


Resolving patch:
diff --git a/amy/workshops/templatetags/training_progress.py b/amy/workshops/templatetags/training_progress.py
--- a/amy/workshops/templatetags/training_progress.py
+++ b/amy/workshops/templatetags/training_progress.py
@@ -18,7 +18,7 @@ def progress_label(progress):
         switch = {
             "n": "warning",
             "f": "danger",
-            "a": "warning",
+            "a": "info",
             "p": "success",
         }
         additional_label = switch[progress.state]


Test patch:
Repository: carpentries/amy

Issue:
Community Roles: Date range validation
Currently, an end date earlier than start date is allowed.  


Discussion:

Resolving patch:
diff --git a/amy/communityroles/forms.py b/amy/communityroles/forms.py
--- a/amy/communityroles/forms.py
+++ b/amy/communityroles/forms.py
@@ -127,3 +127,11 @@ def clean(self) -> dict[str, Any]:
             raise ValidationError(errors)
 
         return cleaned_data
+
+    def clean_end(self):
+        """Validate that end >= start"""
+        start = self.cleaned_data.get("start")
+        end = self.cleaned_data.get("end")
+        if start and end and end < start:
+            raise ValidationError("Must not be earlier than start date.")
+        return end


Test patch:
diff --git a/amy/communityroles/tests/test_community_role_form.py b/amy/communityroles/tests/test_community_role_form.py
--- a/amy/communityroles/tests/test_community_role_form.py
+++ b/amy/communityroles/tests/test_community_role_form.py
@@ -177,6 +177,48 @@ def test_membership_required(self):
             ["Membership is required with community role Test"],
         )
 
+    def test_start_date_gt_end_date_is_invalid(self):
+        """Tests error raised if end < start"""
+        # Arrange
+        data = {
+            "start": date(2021, 11, 14),
+            "end": date(2021, 11, 13),  # lt start
+        }
+
+        # Act
+        form = CommunityRoleForm(data)
+
+        # Assert
+        self.assertFalse(form.is_valid())  # errors expected
+        self.assertIn("end", form.errors.keys())
+        self.assertEqual(
+            form.errors["end"],
+            ["Must not be earlier than start date."],
+        )
+
+    def test_start_end_dates_valid(self):
+        """Tests valid start date <= end date"""
+        # Arrange
+        params = [
+            (date(2021, 11, 14), date(2021, 11, 14)),
+            (date(2021, 11, 14), date(2021, 11, 15)),
+        ]
+
+        for p1, p2 in params:
+            data = {
+                "start": p1,
+                "end": p2,
+            }
+
+            # Act
+            form = CommunityRoleForm(data)
+            form.is_valid()
+
+            # Assert
+            with self.subTest():
+                self.assertEqual(form.cleaned_data.get("end"), p2)
+                self.assertNotIn("end", form.errors.keys())
+
     def test_additional_url_supported(self):
         # Arrange
         test_config = CommunityRoleConfig.objects.create(
Repository: carpentries/amy

Issue:
Instructor selection: Decline is giving an error
When an admin declines an Instructor from a sign up, we get `DoesNotExist` error


Discussion:
Relates to #2065.

Resolving patch:
diff --git a/amy/recruitment/views.py b/amy/recruitment/views.py
--- a/amy/recruitment/views.py
+++ b/amy/recruitment/views.py
@@ -399,9 +399,15 @@ def add_instructor_task(self, person: Person, event: Event) -> Task:
         return task
 
     def remove_instructor_task(self, person: Person, event: Event) -> None:
-        task = Task.objects.get(role__name="instructor", person=person, event=event)
-        self.remove_automated_email(task)
-        task.delete()
+        """Remove instructor task from a Person only if the task exists. If it doesn't,
+        don't throw errors."""
+        try:
+            task = Task.objects.get(role__name="instructor", person=person, event=event)
+        except Task.DoesNotExist:
+            pass
+        else:
+            self.remove_automated_email(task)
+            task.delete()
 
     def add_automated_email(self, task: Task) -> tuple[list[Job], list[RQJob]]:
         trigger_name = "new-instructor"


Test patch:
diff --git a/amy/recruitment/tests/test_instructor_recruitment_views.py b/amy/recruitment/tests/test_instructor_recruitment_views.py
--- a/amy/recruitment/tests/test_instructor_recruitment_views.py
+++ b/amy/recruitment/tests/test_instructor_recruitment_views.py
@@ -795,6 +795,24 @@ def test_remove_instructor_task(self) -> None:
         with self.assertRaises(Task.DoesNotExist):
             task.refresh_from_db()
 
+    def test_remove_instructor_task__no_task(self) -> None:
+        # Arrange
+        super()._setUpRoles()
+        self._prepare_email_automation_data()
+        request = RequestFactory().post("/")
+        view = InstructorRecruitmentSignupChangeState(request=request)
+        person = Person.objects.create(
+            personal="Test", family="User", username="test_user"
+        )
+        organization = Organization.objects.first()
+        event = Event.objects.create(
+            slug="test-event",
+            host=organization,
+            administrator=organization,
+        )
+        # Act & Assert - no error
+        view.remove_instructor_task(person, event)
+
     def test_post__form_valid(self) -> None:
         # Arrange
         request = RequestFactory().post("/")
Repository: carpentries/amy

Issue:
Hide lesson program specific checkout steps
Now that we have a single Instructor badge, we do not want to see the lesson program specific checkout steps. These should be hidden from view so only four options remain.

- ~DC Demo~
- ~DC Homework~
- Demo
- Discussion
- Homework
- ~LC Demo~
- ~LC Homework~
- ~SWC Demo~
- ~SWC Homework~
- Training


Discussion:
I don't know which of the following may be a better option:

1. Add a 'visibility' condition to each of the checkout steps.
2. Remove the unused checkout steps entirely and run a migration/update to make anyone who had the lesson program specific step assigned to them now have the generic step assigned to them.

@pbanaszkiewicz  in 2023 we may be considering larger changes to the checkout process. Much of that is currently unknown but we may want to keep this in mind as we consider which of these two things to do.

This will also impact how the "[eligible to be certified](https://test-amy.carpentries.org/trainings/trainees/?search=&is_instructor=eligible&training=&training_request=unknown&order_by=)" query runs.


@maneesha did you check the test server? I think only 4 checkout steps are visible now, however we still support the crossed-out ones (e.g. DC Demo) as there was no migration for old entries.
We do still see all the options when creating a new training progress record.

![image](https://user-images.githubusercontent.com/829690/187659567-82688209-35db-4176-a2fc-d75611ecee58.png)


Resolving patch:
diff --git a/amy/trainings/forms.py b/amy/trainings/forms.py
--- a/amy/trainings/forms.py
+++ b/amy/trainings/forms.py
@@ -24,6 +24,16 @@ class TrainingProgressForm(forms.ModelForm):
         queryset=Person.objects.all(),
         widget=ModelSelect2Widget(data_view="admin-lookup"),
     )
+    requirement = forms.ModelChoiceField(
+        queryset=TrainingRequirement.objects.exclude(
+            Q(name__startswith="SWC")
+            | Q(name__startswith="DC")
+            | Q(name__startswith="LC")
+        ),
+        label="Type",
+        required=True,
+    )
+
     event = forms.ModelChoiceField(
         label="Event",
         required=False,


Test patch:
Repository: carpentries/amy

Issue:
Error: Deleting an award that is required by a community role
On the real AMY, this returns a 500 Server Error.

I tried this on the test server (where it looks like `DEBUG=True`) and get an `AttributeError` - `'Award' object has no attribute 'get_absolute_url'` as well as `ProtectedError` as the Award has a protected foreign key in CommunityRole.  


Discussion:

Resolving patch:
diff --git a/amy/workshops/base_views.py b/amy/workshops/base_views.py
--- a/amy/workshops/base_views.py
+++ b/amy/workshops/base_views.py
@@ -140,6 +140,9 @@ def before_delete(self, *args, **kwargs):
     def after_delete(self, *args, **kwargs):
         return None
 
+    def back_address(self) -> Optional[str]:
+        return None
+
     def delete(self, request, *args, **kwargs):
         # Workaround for https://code.djangoproject.com/ticket/21926
         # Replicates the `delete` method of DeleteMixin
@@ -153,7 +156,10 @@ def delete(self, request, *args, **kwargs):
             messages.success(self.request, self.success_message.format(object_str))
             return HttpResponseRedirect(success_url)
         except ProtectedError as e:
-            return failed_to_delete(self.request, self.object, e.protected_objects)
+            back = self.back_address()
+            return failed_to_delete(
+                self.request, self.object, e.protected_objects, back=back
+            )
 
     def get(self, request, *args, **kwargs):
         return self.http_method_not_allowed(request, *args, **kwargs)
diff --git a/amy/workshops/views.py b/amy/workshops/views.py
--- a/amy/workshops/views.py
+++ b/amy/workshops/views.py
@@ -38,6 +38,7 @@
 from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse, reverse_lazy
+from django.utils.http import url_has_allowed_host_and_scheme
 import django_rq
 from github.GithubException import GithubException
 import requests
@@ -2221,6 +2222,17 @@ class MockAwardDelete(OnlyForAdminsMixin, PermissionRequiredMixin, AMYDeleteView
     model = Award
     permission_required = "workshops.delete_award"
 
+    def back_address(self) -> Optional[str]:
+        fallback_url = reverse(
+            "person_edit", args=[self.get_object().person.id]  # type: ignore
+        )
+        referrer = self.request.headers.get("Referer", fallback_url)
+        return (
+            referrer
+            if url_has_allowed_host_and_scheme(referrer, settings.ALLOWED_HOSTS)
+            else fallback_url
+        )
+
     def get_success_url(self):
         return reverse("badge_details", args=[self.get_object().badge.name])
 


Test patch:
Repository: carpentries/amy

Issue:
Workshop activity shall be ordered by time in the instructor profile
At the very bottom of https://amy.carpentries.org/dashboard/instructor/, there is a "Your workshop activity:" table, listing all workshop activity of an instructor. Currently, these workshops are listed in a seemingly random order, making it inconvenient to e.g. find the most recent workshop. It would make sense to sort the workshops from the most recent to the oldest ones.


Discussion:

Resolving patch:
diff --git a/amy/dashboard/views.py b/amy/dashboard/views.py
--- a/amy/dashboard/views.py
+++ b/amy/dashboard/views.py
@@ -124,7 +124,9 @@ def instructor_dashboard(request):
         "languages",
         Prefetch(
             "task_set",
-            queryset=Task.objects.select_related("event", "role"),
+            queryset=Task.objects.select_related("event", "role").order_by(
+                "event__start", "event__slug"
+            ),
         ),
         Prefetch(
             "membershiptask_set",


Test patch:
Repository: carpentries/amy

Issue:
Bug: retry button for RQJob is linked to reschedule now
(Admin Panel) Instead of rescheduling, this button should lead to retrying endpoint


Discussion:

Resolving patch:
diff --git a/amy/autoemails/admin.py b/amy/autoemails/admin.py
--- a/amy/autoemails/admin.py
+++ b/amy/autoemails/admin.py
@@ -65,28 +65,31 @@ class RQJobAdmin(admin.ModelAdmin):
         "manage_links",
         "created_at",
         "scheduled_execution",
-        "trigger",
         "status",
-        "mail_status",
         "event_slug",
         "recipients",
-        "interval",
-        "result_ttl",
     ]
     date_hierarchy = "created_at"
     readonly_fields = [
         "scheduled_execution",
         "action_name",
         "status",
-        "mail_status",
         "event_slug",
         "recipients",
-        "interval",
-        "result_ttl",
     ]
     actions = [
         "action_refresh_state",
     ]
+    list_filter = [
+        "action_name",
+        "status",
+    ]
+    search_fields = [
+        "job_id",
+        "action_name",
+        "event_slug",
+        "recipients",
+    ]
 
     def action_refresh_state(self, request, queryset):
         counter = 0
@@ -305,8 +308,7 @@ def reschedule_now(self, request, object_id):
             logger.debug(f"Job {rqjob.job_id} could not be rescheduled.")
             messages.warning(
                 request,
-                f"The job {rqjob.job_id} was not "
-                "rescheduled. It is probably already "
+                f"The job {rqjob.job_id} was not rescheduled. It is probably already "
                 "executing or has recently executed.",
             )
 


Test patch:
Repository: carpentries/amy

Issue:
Bug: Internal Server Error: /fiscal/organizations/
Production issue.

```
FieldError at /fiscal/organizations/
Cannot resolve keyword 'membership' into field. Choices are: administered_events, affiliated_organizations, country, domain, fullname, hosted_events, id, latitude, longitude, member, memberships, selforganisedsubmission, sponsored_events, workshopinquiryrequest, workshoprequest
```

https://amy.carpentries.org/fiscal/organizations/?country=&membership__variant=silver&order_by=


Discussion:
Just remembered and recreated this - it happens when you try to use the 'Memberships' filter on the Organizations page

Resolving patch:
diff --git a/amy/fiscal/filters.py b/amy/fiscal/filters.py
--- a/amy/fiscal/filters.py
+++ b/amy/fiscal/filters.py
@@ -11,7 +11,7 @@
 class OrganizationFilter(AMYFilterSet):
     country = AllCountriesFilter(widget=Select2Widget)
 
-    membership__variant = django_filters.MultipleChoiceFilter(
+    memberships__variant = django_filters.MultipleChoiceFilter(
         label="Memberships (current or past)",
         choices=Membership.MEMBERSHIP_CHOICES,
         widget=Select2MultipleWidget,


Test patch:
Repository: carpentries/amy

Issue:
Cache configuration should be parametrized through environment variables
Related to #2338: cache configuration should allow for parametrization through environment variables.


Discussion:

Resolving patch:
diff --git a/config/settings.py b/config/settings.py
--- a/config/settings.py
+++ b/config/settings.py
@@ -3,6 +3,7 @@
 """
 
 from pathlib import Path
+from typing import cast
 
 from django.core.exceptions import ImproperlyConfigured
 from django.utils.translation import gettext_lazy as _
@@ -251,14 +252,17 @@
 # -----------------------------------------------------------------------------
 # https://docs.djangoproject.com/en/2.2/topics/cache/#database-caching
 CACHES = {
-    "default": {
-        "BACKEND": "django.core.cache.backends.db.DatabaseCache",
-        "LOCATION": "default_cache_table",
-    },
-    "select2": {
-        "BACKEND": "django.core.cache.backends.db.DatabaseCache",
-        "LOCATION": "select2_cache_table",
-    },
+    # For Redis use:
+    # redis://127.0.0.1:6379/0?client_class=django_redis.client.DefaultClient
+    # redis://127.0.0.1:6379/1?client_class=django_redis.client.DefaultClient
+    "default": env.cache_url(
+        "AMY_CACHE_DEFAULT",
+        cast(environ.NoValue, "dbcache://default_cache_table"),
+    ),
+    "select2": env.cache_url(
+        "AMY_CACHE_SELECT2",
+        cast(environ.NoValue, "dbcache://select2_cache_table"),
+    ),
 }
 
 # MIDDLEWARE


Test patch: