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

Issue:
Bug: assignment form queryset may return duplicate results
Introduced in v2.16, AssignmentForm contains a queryset that may yield duplicate results due to the filtering used.


Discussion:

Resolving patch:
diff --git a/amy/dashboard/forms.py b/amy/dashboard/forms.py
--- a/amy/dashboard/forms.py
+++ b/amy/dashboard/forms.py
@@ -27,7 +27,7 @@ class AssignmentForm(forms.Form):
         required=False,
         queryset=Person.objects.filter(
             Q(is_superuser=True) | Q(groups__name="administrators")
-        ),
+        ).distinct(),
         widget=Select2Widget(),
     )
     helper = BootstrapHelper(


Test patch:
diff --git a/amy/dashboard/tests/test_assignment_form.py b/amy/dashboard/tests/test_assignment_form.py
new file mode 100644
--- /dev/null
+++ b/amy/dashboard/tests/test_assignment_form.py
@@ -0,0 +1,47 @@
+from django.contrib.auth.models import Group
+from django.test import TestCase
+
+from dashboard.forms import AssignmentForm
+from workshops.models import Person
+
+
+class TestAssignmentForm(TestCase):
+    def setUp(self):
+        self.superuser = Person.objects.create(
+            personal="Harry",
+            family="Potter",
+            email="hp@magic.uk",
+            username="potter_harry",
+            is_superuser=True,
+        )
+        self.administrator = Person.objects.create(
+            personal="Ron",
+            family="Weasley",
+            email="rw@magic.uk",
+            username="weasley_ron",
+        )
+        self.both = Person.objects.create(
+            personal="Hermione",
+            family="Granger",
+            email="hg@magic.uk",
+            username="granger_hermione",
+            is_superuser=True,
+        )
+
+    def test_assigned_to_queryset(self):
+        """Ensure duplicate results don't appear in the queryset.
+
+        This is a regression test for https://github.com/carpentries/amy/issues/1792.
+        """
+        admins = Group.objects.get(name='administrators')
+        committee = Group.objects.get(name='steering committee')
+        self.administrator.groups.add(admins)
+        # user has to be in 2 groups to yield duplicate
+        # results in the queryset
+        self.both.groups.add(committee)
+        self.both.groups.add(admins)
+
+        field = AssignmentForm().fields["assigned_to"]
+        self.assertEqual(list(field.queryset), [
+            self.both, self.superuser, self.administrator
+        ])
Repository: carpentries/amy

Issue:
Bug: Membership person roles formset mixup
`fiscal/membership/X/tasks/` in roles contains member roles, instead of membership person roles.

`fiscal/membership/X/members/` contains wrong title.


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
@@ -245,7 +245,7 @@ class Meta:
         ]
         widgets = {
             "person": ModelSelect2Widget(data_view="person-lookup"),
-            "role": ModelSelect2Widget(data_view="memberrole-lookup"),
+            "role": ModelSelect2Widget(data_view="membershippersonrole-lookup"),
         }
 
     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
@@ -282,7 +282,7 @@ def get_formset_queryset(self, object):
 
     def get_context_data(self, **kwargs):
         if "title" not in kwargs:
-            kwargs["title"] = "Change person roles for {}".format(self.membership)
+            kwargs["title"] = "Change members for {}".format(self.membership)
         return super().get_context_data(**kwargs)
 
 
diff --git a/amy/workshops/lookups.py b/amy/workshops/lookups.py
--- a/amy/workshops/lookups.py
+++ b/amy/workshops/lookups.py
@@ -8,6 +8,7 @@
 from django.conf.urls import url
 from django.db.models import Q, Count
 
+from fiscal.models import MembershipPersonRole
 from workshops import models
 from workshops.util import OnlyForAdminsNoRedirectMixin, LoginNotRequiredMixin
 
@@ -124,6 +125,16 @@ def get_queryset(self):
         return q
 
 
+class MembershipPersonRoleLookupView(OnlyForAdminsNoRedirectMixin, AutoResponseView):
+    def get_queryset(self):
+        q = MembershipPersonRole.objects.all()
+        if self.term:
+            return q.filter(
+                Q(name__icontains=self.term) | Q(verbose_name__icontains=self.term)
+            )
+        return q
+
+
 class PersonLookupView(OnlyForAdminsNoRedirectMixin, AutoResponseView):
     def get_queryset(self):
         results = models.Person.objects.all()
@@ -260,6 +271,11 @@ def get_queryset(self):
     ),
     url(r"^memberships/$", MembershipLookupView.as_view(), name="membership-lookup"),
     url(r"^member-roles/$", MemberRoleLookupView.as_view(), name="memberrole-lookup"),
+    url(
+        r"^membership-person-roles/$",
+        MembershipPersonRoleLookupView.as_view(),
+        name="membershippersonrole-lookup",
+    ),
     url(r"^persons/$", PersonLookupView.as_view(), name="person-lookup"),
     url(r"^admins/$", AdminLookupView.as_view(), name="admin-lookup"),
     url(r"^airports/$", AirportLookupView.as_view(), name="airport-lookup"),


Test patch:
Repository: carpentries/amy

Issue:
Bug: new Memberships structure prevents All Organistations from loading
There's issue with `fiscal/organizations/`, and also individual organisation's memberships aren't displayed.


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
@@ -62,7 +62,7 @@ class AllOrganizations(OnlyForAdminsMixin, AMYListView):
     filter_class = OrganizationFilter
     queryset = Organization.objects.prefetch_related(
         Prefetch(
-            "membership_set",
+            "memberships",
             to_attr="current_memberships",
             queryset=Membership.objects.filter(
                 agreement_start__lte=Now(),


Test patch:
Repository: carpentries/amy

Issue:
Change help text for event country
The help text for workshop country should be changed.
![image](https://user-images.githubusercontent.com/829690/108235391-c813f300-7113-11eb-81e0-7cbd96573459.png)

Instead of "Use Online for online events" this should read:

> For Data, Library, or Software Carpentry workshops, always use the country of the host organisation.  For Instructor Training, use the country only for in-person events, and use **Online** for online events.  Be sure to use the **online tag** above for all online events.


Discussion:

Resolving patch:
diff --git a/amy/workshops/migrations/0231_auto_20210217_2055.py b/amy/workshops/migrations/0231_auto_20210217_2055.py
new file mode 100644
--- /dev/null
+++ b/amy/workshops/migrations/0231_auto_20210217_2055.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.2.17 on 2021-02-17 20:55
+
+from django.db import migrations
+import django_countries.fields
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('workshops', '0230_membership_persons'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='event',
+            name='country',
+            field=django_countries.fields.CountryField(blank=True, help_text='Required in order for this event to be "published".<br />For Data, Library, or Software Carpentry workshops, always use the country of the host organisation. <br />For Instructor Training, use the country only for in-person events, and use <b>Online</b> for online events. <br />Be sure to use the <b>online tag</b> above for all online events.', max_length=2, null=True),
+        ),
+    ]
diff --git a/amy/workshops/models.py b/amy/workshops/models.py
--- a/amy/workshops/models.py
+++ b/amy/workshops/models.py
@@ -1160,7 +1160,12 @@ class Event(AssignmentMixin, RQJobsMixin, models.Model):
     country = CountryField(
         null=True,
         blank=True,
-        help_text=PUBLISHED_HELP_TEXT + "<br />Use <b>Online</b> for online events.",
+        help_text=PUBLISHED_HELP_TEXT
+        + "<br />For Data, Library, or Software Carpentry workshops, always "
+        +"use the country of the host organisation. <br />For Instructor "
+        +"Training, use the country only for in-person events, and use "
+        +"<b>Online</b> for online events. <br />Be sure to use the "
+        +"<b>online tag</b> above for all online events."
     )
     venue = models.CharField(
         max_length=STR_LONGEST,


Test patch:
Repository: carpentries/amy

Issue:
Organisation: add latitude, longitude
Organisation should be extended with geographical information (lat/long similarly to events).


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
@@ -49,7 +49,7 @@ class OrganizationForm(forms.ModelForm):
 
     class Meta:
         model = Organization
-        fields = ["domain", "fullname", "country"]
+        fields = ["domain", "fullname", "country", "latitude", "longitude"]
 
 
 class OrganizationCreateForm(OrganizationForm):
diff --git a/amy/workshops/migrations/0236_organization_coordinates.py b/amy/workshops/migrations/0236_organization_coordinates.py
new file mode 100644
--- /dev/null
+++ b/amy/workshops/migrations/0236_organization_coordinates.py
@@ -0,0 +1,23 @@
+# Generated by Django 2.2.17 on 2021-03-11 21:02
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('workshops', '0235_remove_event_sponsors'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='organization',
+            name='latitude',
+            field=models.FloatField(blank=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='organization',
+            name='longitude',
+            field=models.FloatField(blank=True, null=True),
+        ),
+    ]
diff --git a/amy/workshops/models.py b/amy/workshops/models.py
--- a/amy/workshops/models.py
+++ b/amy/workshops/models.py
@@ -76,6 +76,9 @@ class Organization(models.Model):
     fullname = models.CharField(max_length=STR_LONG, unique=True)
     country = CountryField(null=True, blank=True)
 
+    latitude = models.FloatField(null=True, blank=True)
+    longitude = models.FloatField(null=True, blank=True)
+
     objects = OrganizationManager()
 
     def __str__(self):


Test patch:
Repository: carpentries/amy

Issue:
Organisation: link affiliated organisations
M2M link, no additional information required. This is not the same as membership consortium.


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
@@ -8,6 +8,7 @@
 from markdownx.fields import MarkdownxFormField
 
 from fiscal.models import MembershipTask
+from workshops.fields import ModelSelect2MultipleWidget
 from workshops.forms import (
     BootstrapHelper,
     form_saved_add_comment,
@@ -40,7 +41,19 @@ class OrganizationForm(forms.ModelForm):
 
     class Meta:
         model = Organization
-        fields = ["domain", "fullname", "country", "latitude", "longitude"]
+        fields = [
+            "domain",
+            "fullname",
+            "country",
+            "latitude",
+            "longitude",
+            "affiliated_organizations",
+        ]
+        widgets = {
+            "affiliated_organizations": ModelSelect2MultipleWidget(
+                data_view="organization-lookup"
+            ),
+        }
 
     def clean_domain(self):
         """Convert text into URL without scheme (http/https/etc)."""
diff --git a/amy/workshops/migrations/0239_organization_affiliated_organizations.py b/amy/workshops/migrations/0239_organization_affiliated_organizations.py
new file mode 100644
--- /dev/null
+++ b/amy/workshops/migrations/0239_organization_affiliated_organizations.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.18 on 2021-03-27 18:32
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('workshops', '0238_task_seat_public'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='organization',
+            name='affiliated_organizations',
+            field=models.ManyToManyField(to='workshops.Organization', blank=True),
+        ),
+    ]
diff --git a/amy/workshops/models.py b/amy/workshops/models.py
--- a/amy/workshops/models.py
+++ b/amy/workshops/models.py
@@ -80,6 +80,8 @@ class Organization(models.Model):
     latitude = models.FloatField(null=True, blank=True)
     longitude = models.FloatField(null=True, blank=True)
 
+    affiliated_organizations = models.ManyToManyField("Organization", blank=True)
+
     objects = OrganizationManager()
 
     def __str__(self):


Test patch:
Repository: carpentries/amy

Issue:
Admin: Missing Bootstrap CSS
Django admin is missing bootstrap css. In the settings.py we have changed the default message tags `MESSAGE_TAGS`. So the default css that shows up in django admin does not work. 


Discussion:
cc @pbanaszkiewicz 

Resolving patch:
diff --git a/config/settings.py b/config/settings.py
--- a/config/settings.py
+++ b/config/settings.py
@@ -443,10 +443,11 @@
 from django.contrib.messages import constants as message_constants  # noqa
 
 MESSAGE_TAGS = {
-    message_constants.INFO: "alert-info",
-    message_constants.SUCCESS: "alert-success",
-    message_constants.WARNING: "alert-warning",
-    message_constants.ERROR: "alert-danger",
+    message_constants.DEBUG: "debug",
+    message_constants.INFO: "info alert-info",
+    message_constants.SUCCESS: "success alert-success",
+    message_constants.WARNING: "warning alert-warning",
+    message_constants.ERROR: "error alert-danger",
 }
 
 # django-countries


Test patch:
Repository: carpentries/amy

Issue:
Re-activate fields for pre/post surveys
We used to have the fields `instructors_pre` and  `instructors_post` in the `workshops_event` table. The fields still exist but are hidden from view.  We would like to put these fields back in the `Event` view.  These fields will store the html link to view the pre/post survey results for that event.  

Requesting review from @fmichonneau  before working on this task to be sure there are no privacy concerns with storing the survey results links in AMY. 


Discussion:
The survey link can be generated directly from the slug (we use a salted hash) so there is no need to store it. It can be generated on the fly. The current issue is that the default settings used by R and Python to generate the key from the slug are slightly different and lead to different results. I need to modify the R code to also accept the key generated by the Python algorithm (as I don't think it's possible to do the other way around).

In terms of privacy, including the key in the workshop event page, means that all the regional coordinators will have access to the survey results for all workshops. This is probably ok but we need to consider the implications.
Members of The Carpentries Core Team will be discussing strategies for sharing survey data with workshop hosts and instructors. We'll revisit this issue after that discussion. 
@fmichonneau  per our conversation in Asana, can we move ahead on re-enabling these fields? 
@pbanaszkiewicz  we have discussed this internally and would like to see these fields re-enabled.  The fields themselves do not need to change.

Resolving patch:
diff --git a/amy/workshops/forms.py b/amy/workshops/forms.py
--- a/amy/workshops/forms.py
+++ b/amy/workshops/forms.py
@@ -429,6 +429,8 @@ class Meta:
             "curricula",
             "lessons",
             "public_status",
+            "instructors_pre",
+            "instructors_post",
             "comment",
         ]
         widgets = {
@@ -473,6 +475,8 @@ def __init__(self, *args, **kwargs):
             "reg_key",
             "manual_attendance",
             "contact",
+            "instructors_pre",
+            "instructors_post",
             Div(
                 Div(HTML("Location details"), css_class="card-header"),
                 Div(


Test patch:
Repository: carpentries/amy

Issue:
Organisation: display affiliated organisations linking to current org.
If organisation A links to organisation B through affiliated organisations, we can see the link in organisation A details page, but it's not visible in org. B details page.

Display the organisations linking to current organisation on it's details page (in this example: display link to organisation A on organisation B's detail page).


Discussion:

Resolving patch:
diff --git a/amy/workshops/migrations/0243_auto_20210508_1954.py b/amy/workshops/migrations/0243_auto_20210508_1954.py
new file mode 100644
--- /dev/null
+++ b/amy/workshops/migrations/0243_auto_20210508_1954.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.18 on 2021-05-08 19:54
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('workshops', '0242_auto_20210503_1359'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='organization',
+            name='affiliated_organizations',
+            field=models.ManyToManyField(blank=True, related_name='_organization_affiliated_organizations_+', to='workshops.Organization'),
+        ),
+    ]
diff --git a/amy/workshops/models.py b/amy/workshops/models.py
--- a/amy/workshops/models.py
+++ b/amy/workshops/models.py
@@ -81,7 +81,9 @@ class Organization(models.Model):
     latitude = models.FloatField(null=True, blank=True)
     longitude = models.FloatField(null=True, blank=True)
 
-    affiliated_organizations = models.ManyToManyField("Organization", blank=True)
+    affiliated_organizations = models.ManyToManyField(
+        "Organization", blank=True, symmetrical=True
+    )
 
     objects = OrganizationManager()
 


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
@@ -105,3 +105,15 @@ def test_creating_event_with_comment(self):
         comment = Comment.objects.first()
         self.assertEqual(comment.comment, "This is a test comment.")
         self.assertIn(comment, Comment.objects.for_model(obj))
+
+    def test_symmetrical_affiliations(self):
+        """Make sure adding an affiliation in one organisation, automatically reveals
+        this relationship in the other organisation."""
+        # Arrange - `setUp()` creates 2 organisations we can use
+
+        # Act
+        self.org_alpha.affiliated_organizations.add(self.org_beta)
+
+        # Assert
+        self.assertIn(self.org_beta, self.org_alpha.affiliated_organizations.all())
+        self.assertIn(self.org_alpha, self.org_beta.affiliated_organizations.all())
Repository: carpentries/amy

Issue:
Memberships: make private by default (amend migration)
The migration `amy/workshops/migrations/0237_auto_20210321_1056.py` sets default value to `private`, but migration `amy/workshops/migrations/0228_auto_20210204_0658.py` introduced the value as public first.

The migrations will have to be changed in order to make production data private by default.


Discussion:

Resolving patch:
diff --git a/amy/workshops/migrations/0228_auto_20210204_0658.py b/amy/workshops/migrations/0228_auto_20210204_0658.py
--- a/amy/workshops/migrations/0228_auto_20210204_0658.py
+++ b/amy/workshops/migrations/0228_auto_20210204_0658.py
@@ -23,7 +23,7 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='membership',
             name='public_status',
-            field=models.CharField(choices=[('public', 'Public'), ('private', 'Private')], default='public', max_length=20,
+            field=models.CharField(choices=[('public', 'Public'), ('private', 'Private')], default='private', max_length=20,
                                    verbose_name="Can this membership be publicized on The carpentries websites?",
                                    help_text="Public memberships may be listed on any of The Carpentries websites."
             ),
diff --git a/amy/workshops/migrations/0237_auto_20210321_1056.py b/amy/workshops/migrations/0237_auto_20210321_1056.py
--- a/amy/workshops/migrations/0237_auto_20210321_1056.py
+++ b/amy/workshops/migrations/0237_auto_20210321_1056.py
@@ -82,9 +82,17 @@ class Migration(migrations.Migration):
             model_name='membership',
             name='self_organized_workshops_rolled_over',
         ),
-        migrations.AlterField(
-            model_name='membership',
-            name='public_status',
-            field=models.CharField(choices=[('public', 'Public'), ('private', 'Private')], default='private', help_text='Public memberships may be listed on any of The Carpentries websites.', max_length=20, verbose_name='Can this membership be publicized on The carpentries websites?'),
-        ),
+
+        # The following migration is a no-op as a result of
+        # https://github.com/carpentries/amy/pull/1949
+        # Previously 0228 introduced `public_status` field with default value of `public`,
+        # and this migration changed the default value to `private`. This is however
+        # incorrect as the existing memberships need to be migrated with default value
+        # of `private` first, thus 0228 was amended to have default value as `private`,
+        # and this migration became no-op.
+        # migrations.AlterField(
+        #     model_name='membership',
+        #     name='public_status',
+        #     field=models.CharField(choices=[('public', 'Public'), ('private', 'Private')], default='private', help_text='Public memberships may be listed on any of The Carpentries websites.', max_length=20, verbose_name='Can this membership be publicized on The carpentries websites?'),
+        # ),
     ]


Test patch: