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

Issue:
Event's start date must be earlier than it's end date
We should enforce that the start date is early that the end date.



Discussion:

Resolving patch:
diff --git a/workshops/forms.py b/workshops/forms.py
--- a/workshops/forms.py
+++ b/workshops/forms.py
@@ -206,6 +206,15 @@ def clean_slug(self):
 
         return data
 
+    def clean_end(self):
+        """Ensure end >= start."""
+        start = self.cleaned_data['start']
+        end = self.cleaned_data['end']
+
+        if start and end and end < start:
+            raise forms.ValidationError('Must not be earlier than start date.')
+        return end
+
     class Meta:
         model = Event
         # reorder fields, don't display 'deleted' field


Test patch:
diff --git a/workshops/test/test_event.py b/workshops/test/test_event.py
--- a/workshops/test/test_event.py
+++ b/workshops/test/test_event.py
@@ -324,6 +324,43 @@ def test_unique_empty_slug(self):
         response = self.client.post(reverse('event_add'), data)
         assert response.status_code == 302
 
+    def test_start_date_gte_end_date(self):
+        """Ensure event's start date is earlier than it's end date.
+
+        This is a regression test for
+        https://github.com/swcarpentry/amy/issues/436"""
+        data = {
+            'site': self.test_site.id,
+            'tags': [self.test_tag.id],
+            'slug': 'test-event',
+            'start': date(2015, 7, 20),
+            'end': date(2015, 7, 19),
+        }
+        response = self.client.post(reverse('event_add'), data)
+        assert response.status_code == 200
+        content = response.content.decode('utf-8')
+        assert 'Must not be earlier than start date' in content
+
+        data = {
+            'site': self.test_site.id,
+            'tags': [self.test_tag.id],
+            'slug': 'test-event',
+            'start': date(2015, 7, 20),
+            'end': date(2015, 7, 20),
+        }
+        response = self.client.post(reverse('event_add'), data)
+        assert response.status_code == 302
+
+        data = {
+            'site': self.test_site.id,
+            'tags': [self.test_tag.id],
+            'slug': 'test-event2',
+            'start': date(2015, 7, 20),
+            'end': date(2015, 7, 21),
+        }
+        response = self.client.post(reverse('event_add'), data)
+        assert response.status_code == 302
+
 
 class TestEventNotes(TestBase):
     """Make sure notes once written are saved forever!"""
Repository: carpentries/amy

Issue:
Add to Problems report: missing address information
If we are lacking country/latitude/longitude, report the workshop as a problem.



Discussion:

Resolving patch:
diff --git a/workshops/views.py b/workshops/views.py
--- a/workshops/views.py
+++ b/workshops/views.py
@@ -1247,23 +1247,21 @@ def instructors_over_time(request):
 def problems(request):
     '''Display problems in the database.'''
 
-    subject = 'attendance figures for '
-    body_pre = 'Hi,\nCan you please send us an attendance list (or even just a head count) for the '
-    body_post = ' workshop?\nThanks,\nSoftware Carpentry'
-
     host = Role.objects.get(name='host')
     instructor = Role.objects.get(name='instructor')
-    missing_attendance = Event.objects.past_events().\
-        filter(Q(attendance=None) | Q(attendance=0))
-    for e in missing_attendance:
+    events = Event.objects.past_events().\
+        filter(Q(attendance=None) | Q(attendance=0) |
+               Q(country=None) |
+               Q(start__gt=F('end')))
+    for e in events:
         tasks = Task.objects.filter(event=e).\
             filter(Q(role=host) | Q(role=instructor))
-        e.mailto = [t.person.email for t in tasks if t.person.email]
+        e.mailto_ = ','.join([t.person.email for t in tasks if t.person.email])
+        e.missing_attendance_ = (not e.attendance)
+        e.missing_location_ = (not e.country)
+        e.bad_dates_ = e.start and e.end and (e.start > e.end)
     context = {'title': 'Problems',
-               'missing_attendance': missing_attendance,
-               'subject': subject,
-               'body_pre': body_pre,
-               'body_post': body_post}
+               'events': events}
     return render(request, 'workshops/problems.html', context)
 
 #------------------------------------------------------------


Test patch:
Repository: carpentries/amy

Issue:
Representing online courses
1. Create a "country" for the World-Wide Web in AMY.
2. Add a flag (icon to be chosen) to represent that "country" on the main website.
3. Make sure the two agree on the 2-letter ISO code for "online" :-)



Discussion:
1. Done (via 3.)
2. It's hard to do a 16px by 11px flag
3. I went with 'W3'.

2: I decided to use a black-white oblate globe for that.

The PR is coming…


Resolving patch:
diff --git a/amy/settings.py b/amy/settings.py
--- a/amy/settings.py
+++ b/amy/settings.py
@@ -206,9 +206,9 @@
 
 # explicitely add European Union as a country
 COUNTRIES_OVERRIDE = {
-    'EU': _('European Union'),
     'GB': _('United Kingdom'),
     'US': _('United States'),
+    'W3': _('Online'),
 }
 
 # settings for REST API
diff --git a/workshops/migrations/0040_add_country_to_online_events.py b/workshops/migrations/0040_add_country_to_online_events.py
new file mode 100644
--- /dev/null
+++ b/workshops/migrations/0040_add_country_to_online_events.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+def add_country_to_online_events(apps, schema_editor):
+    """Add an 'Online' country to all events tagged with 'online' tag."""
+    Event = apps.get_model('workshops', 'Event')
+
+    Tag = apps.get_model('workshops', 'Tag')
+    online, _ = Tag.objects.get_or_create(
+        name='online',
+        defaults={'details': 'Events taking place entirely online'},
+    )
+
+    # Oceanic Pole of Inaccessibility coordinates:
+    # https://en.wikipedia.org/wiki/Pole_of_inaccessibility#Oceanic_pole_of_inaccessibility
+    latitude = -48.876667
+    longitude = -123.393333
+
+    Event.objects.filter(country__isnull=True, tags__in=[online]) \
+                 .update(country='W3', latitude=latitude, longitude=longitude,
+                         venue='Internet')
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('workshops', '0039_add_permission_groups'),
+    ]
+
+    operations = [
+        migrations.RunPython(add_country_to_online_events),
+    ]
diff --git a/workshops/migrations/0050_merge.py b/workshops/migrations/0050_merge.py
new file mode 100644
--- /dev/null
+++ b/workshops/migrations/0050_merge.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('workshops', '0040_add_country_to_online_events'),
+        ('workshops', '0049_auto_20150916_0544'),
+    ]
+
+    operations = [
+    ]


Test patch:
Repository: carpentries/amy

Issue:
Forms are not styled with AMY logo or other logos
The workshop request forms and the instructor info update form should display the AMY logo and the SWC or DC logos.



Discussion:

Resolving patch:
diff --git a/workshops/views.py b/workshops/views.py
--- a/workshops/views.py
+++ b/workshops/views.py
@@ -1418,6 +1418,8 @@ class SWCEventRequest(View):
     form_class = SWCEventRequestForm
     form_helper = bootstrap_helper_wider_labels
     page_title = 'Request a Software Carpentry Workshop'
+    form_template = 'forms/workshop_swc_request.html'
+    success_template = 'forms/workshop_request_confirm.html'
 
     def get(self, request, *args, **kwargs):
         form = self.form_class()
@@ -1426,7 +1428,7 @@ def get(self, request, *args, **kwargs):
             'form': form,
             'form_helper': self.form_helper,
         }
-        return render(request, 'forms/workshop_request.html', context)
+        return render(request, self.form_template, context)
 
     def post(self, request, *args, **kwargs):
         form = self.form_class(request.POST)
@@ -1475,8 +1477,7 @@ def post(self, request, *args, **kwargs):
             context = {
                 'title': 'Thank you for requesting a workshop',
             }
-            return render(request, 'forms/workshop_request_confirm.html',
-                          context)
+            return render(request, self.success_template, context)
         else:
             messages.error(request, 'Fix errors below.')
             context = {
@@ -1484,12 +1485,14 @@ def post(self, request, *args, **kwargs):
                 'form': form,
                 'form_helper': self.form_helper,
             }
-            return render(request, 'forms/workshop_request.html', context)
+            return render(request, self.form_template, context)
 
 
 class DCEventRequest(SWCEventRequest):
     form_class = DCEventRequestForm
     page_title = 'Request a Data Carpentry Workshop'
+    form_template = 'forms/workshop_dc_request.html'
+
 
 class AllEventRequests(LoginRequiredMixin, ListView):
     queryset = EventRequest.objects.filter(active=True).order_by('-created_at')


Test patch:
Repository: carpentries/amy

Issue:
Links to open in new tab
Links for github page, eventbrite, and others (like pre/post surveys if they get added in) should open in new tab so I can keep working on that record in AMY.



Discussion:
I don't think we should implement this. If you need to open a link in new tab, just use CTRL+click or middle mouse button or similar shortcut in Mac.

Any downside to implementing it?  "User wants, user gets..."

The downside is: "I wanted it to open in the same tab, but it didn't". The rare (or possible to fix) downside may be: "That other link opened in the same new tab" (I'd need to check if this still happens).

Open in new tabs would be my preference for any _external_ links -- internal links should open in same tab. Maybe we can poll the most frequent AMY users to decide?

@maneesha it's a good idea to ask everyone for their preference. I'll do that.

The smart poll decided to use `target="_blank"` for external URLs. Will do this for v1.0.


Resolving patch:
diff --git a/workshops/templatetags/links.py b/workshops/templatetags/links.py
new file mode 100644
--- /dev/null
+++ b/workshops/templatetags/links.py
@@ -0,0 +1,15 @@
+from django import template
+from django.template.defaultfilters import stringfilter
+from django.utils.html import urlize as urlize
+from django.utils.safestring import mark_safe
+
+register = template.Library()
+
+
+@register.filter(is_safe=True, needs_autoescape=True)
+@stringfilter
+def urlize_newtab(value, autoescape=True):
+    "Converts URLs in plain text into clickable links that open in new tabs."
+    url = urlize(value, nofollow=True, autoescape=autoescape)
+    url = url.replace('a href', 'a target="_blank" href')
+    return mark_safe(url)


Test patch:
Repository: carpentries/amy

Issue:
Bulk-upload: empty entries in csv files
Today I got a strange output form a bulk upload:
"Successfully uploaded 37 persons and 24 tasks"
The entries were actully 24 and this number was recorded in the workshop page.

Piotr found out that there were empty cells in the csv files. 

Empty cells should be ignored.



Discussion:

Resolving patch:
diff --git a/workshops/util.py b/workshops/util.py
--- a/workshops/util.py
+++ b/workshops/util.py
@@ -71,6 +71,10 @@ def upload_person_task_csv(stream):
     empty_fields = set()
 
     for row in reader:
+        # skip empty lines in the CSV
+        if not any(row.values()):
+            continue
+
         entry = {}
         for col in Person.PERSON_UPLOAD_FIELDS:
             try:


Test patch:
diff --git a/workshops/test/test_util.py b/workshops/test/test_util.py
--- a/workshops/test/test_util.py
+++ b/workshops/test/test_util.py
@@ -56,6 +56,15 @@ def test_csv_with_mislabeled_field(self):
         person_tasks, empty_fields = self.compute_from_string(bad_csv)
         self.assertTrue('email' in empty_fields)
 
+    def test_csv_with_empty_lines(self):
+        csv = """personal,middle,family,emailaddress
+john,m,doe,john@doe.com
+,,,"""
+        person_tasks, empty_fields = self.compute_from_string(csv)
+        self.assertEqual(len(person_tasks), 1)
+        person = person_tasks[0]
+        self.assertEqual(person['personal'], 'john')
+
     def test_empty_field(self):
         ''' Ensure we don't mis-order fields given blank data '''
         csv = """personal,middle,family,email
Repository: carpentries/amy

Issue:
New blurred database for development
Update the `db.sql` file.



Discussion:
:+1:


Resolving patch:
diff --git a/scripts/anonymizer.py b/scripts/anonymizer.py
--- a/scripts/anonymizer.py
+++ b/scripts/anonymizer.py
@@ -147,6 +147,8 @@ def url(cursor, i):
 @tuplify
 def lorem_ipsum(cursor, i):
     '''Fill in a large text field.'''
+    if random.uniform(0.0, 1.0) < 0.05:
+        return ''
     result = '\n'.join(LOREM_IPSUM[0:random.randrange(len(LOREM_IPSUM))])
     return result
 
@@ -188,6 +190,17 @@ def email(cursor, i):
 def empty_string(cursor, i):
     return ''
 
+
+@tuplify
+def rand_latitude(cursor, i):
+    return random.uniform(-90, 90)
+
+
+@tuplify
+def rand_longitude(cursor, i):
+    return random.uniform(0, 180)
+
+
 #------------------------------------------------------------
 
 def main():
@@ -205,9 +218,9 @@ def main():
     cnx = sqlite3.connect(db_dst)
     cur = cnx.cursor()
 
-    change(cur, 'workshops_site', 'domain', domain)
-    change(cur, 'workshops_site', 'fullname', multi_word, 1.0)
-    change(cur, 'workshops_site', 'notes', lorem_ipsum)
+    change(cur, 'workshops_host', 'domain', domain)
+    change(cur, 'workshops_host', 'fullname', multi_word, 1.0)
+    change(cur, 'workshops_host', 'notes', lorem_ipsum)
 
     change(cur, 'workshops_person', 'personal', multi_word, 0.1)
     change(cur, 'workshops_person', 'middle', multi_word, 0.0, 0.9)
@@ -219,10 +232,16 @@ def main():
     change(cur, 'workshops_person', 'url', url)
     change(cur, 'workshops_person', 'username', monicker)
     change(cur, 'workshops_person', 'password', empty_string)
+    change(cur, 'workshops_person', 'affiliation', empty_string)
 
     change(cur, 'workshops_event', ('start', 'end'), dates)
     change(cur, 'workshops_event', 'slug', event_slug)
     change(cur, 'workshops_event', 'url', url)
+    change(cur, 'workshops_event', 'contact', empty_string)
+    change(cur, 'workshops_event', 'venue', empty_string)
+    change(cur, 'workshops_event', 'address', empty_string)
+    change(cur, 'workshops_event', 'latitude', rand_latitude)
+    change(cur, 'workshops_event', 'longitude', rand_longitude)
     change(cur, 'workshops_event', 'reg_key', event_reg_key)
     change(cur, 'workshops_event', 'notes', lorem_ipsum)
 
@@ -238,4 +257,6 @@ def main():
     main()
     # we need to populate reversion_* tables so that no-one needs to do that
     # upon every `make database` call
-    print("REMEMBER! to run `./manage.py createinitialrevisions` on the new database NOW")
+    print("REMEMBER! to run `./manage.py createinitialrevisions` on the new "
+          "database NOW.")
+    print("Next, run `sqlite3 SRC -cmd '.dump' > DEST.sql`")


Test patch:
Repository: carpentries/amy

Issue:
Add created by & last modified by to event
Each  page (event, person, etc) should include 

Created by username on YYYY-MM-DD
Last modified by username on YYYY-MM-DD



Discussion:
+1

We're kind of are able to track this data down with django-reversion.


Resolving patch:
diff --git a/workshops/models.py b/workshops/models.py
--- a/workshops/models.py
+++ b/workshops/models.py
@@ -20,6 +20,8 @@
 
 #------------------------------------------------------------
 
+
+@reversion.register
 class Host(models.Model):
     '''Represent a workshop's host.'''
 
@@ -39,6 +41,8 @@ class Meta:
 
 #------------------------------------------------------------
 
+
+@reversion.register
 class Airport(models.Model):
     '''Represent an airport (used to locate instructors).'''
 
@@ -804,6 +808,7 @@ def helpers(self):
         return self.get_queryset().filter(role__name="helper")
 
 
+@reversion.register
 class Task(models.Model):
     '''Represent who did what at events.'''
 
diff --git a/workshops/templatetags/revisions.py b/workshops/templatetags/revisions.py
new file mode 100644
--- /dev/null
+++ b/workshops/templatetags/revisions.py
@@ -0,0 +1,30 @@
+from django import template
+
+import reversion
+
+register = template.Library()
+
+
+@register.inclusion_tag('workshops/last_modified.html')
+def last_modified(obj):
+    """Get all versions for specific object, display:
+
+    "Created on ASD by DSA."
+    "Last modified on ASD by DSA."
+    """
+    versions = reversion.get_for_object(obj)
+
+    try:
+        last, *_, created = versions
+    except ValueError:  # either len(versions) == 0 or len(versions) == 1
+        try:
+            created = versions[0]
+            last = None
+        except IndexError:  # len(versions) == 0
+            created = None
+            last = None
+
+    return {
+        'created': created,
+        'last_modified': last,
+    }


Test patch:
Repository: carpentries/amy

Issue:
Add link to NumFocus Invoice Request Form
Add link under or next to Invoiced (which is going to become Invoice status per #494  and #469) to the form we use to submit invoice request to Leah. Open this link in new tab.

https://docs.google.com/forms/d/1XljyEam4LERRXW0ebyh5eoZXjT1xR4bHkPxITLWiIyA/viewform?c=0&w=1



Discussion:
...and pre-populate with as much information as possible?

You just have to find the entry codes for each corresponding item and make a template.

Example for "Organization to Invoice" is `entry.823772951`:
https://docs.google.com/forms/d/1XljyEam4LERRXW0ebyh5eoZXjT1xR4bHkPxITLWiIyA/viewform?c=0&w=1&entry.823772951=Mythical%20University

Found info on how to do it here: http://justingale.com/2013/09/url-tricks-for-google-forms-pre-populate-and-automatically-submitting-responses/

@jduckles thanks a lot for the pointer on pre-populating (TIL). It'll make a lot of things easier here.

@maneesha do you fill up 'Event or item ID' on that form with event's slug (e.g. `2015-10-03-Krakow-tech-space`)?


Resolving patch:
diff --git a/workshops/models.py b/workshops/models.py
--- a/workshops/models.py
+++ b/workshops/models.py
@@ -1,5 +1,6 @@
 import datetime
 import re
+from urllib.parse import urlencode
 
 from django.contrib.auth.models import (
     AbstractBaseUser, BaseUserManager, PermissionsMixin)
@@ -553,6 +554,27 @@ def website_url(self):
             # KeyError: mo.groupdict doesn't supply required names to format
             return self.url
 
+    @property
+    def uninvoiced(self):
+        """Indicate if the event has been invoiced or not."""
+        return self.invoice_status == 'not-invoiced'
+
+    def get_invoice_form_url(self):
+        query = {
+            'entry.823772951': self.venue,  # Organization to invoice
+            'entry.351294200': 'Workshop administrative fee',  # Reason
+
+            # Date of event
+            'entry.1749215879': '{:%Y-%m-%d}'.format(self.start),
+            'entry.508035854': self.slug,  # Event or item ID
+            'entry.821460022': self.admin_fee,  # Total invoice amount
+            'entry.1316946828': 'US dollars',  # Currency
+        }
+        url = ("https://docs.google.com/forms/d/"
+               "1XljyEam4LERRXW0ebyh5eoZXjT1xR4bHkPxITLWiIyA/viewform?")
+        url += urlencode(query)
+        return url
+
     def get_ident(self):
         if self.slug:
             return str(self.slug)


Test patch:
Repository: carpentries/amy

Issue:
Disallow "/" in event's slug
"/" and other illegal characters break the site…



Discussion:

Resolving patch:
diff --git a/workshops/forms.py b/workshops/forms.py
--- a/workshops/forms.py
+++ b/workshops/forms.py
@@ -218,6 +218,15 @@ class DebriefForm(forms.Form):
 
 
 class EventForm(forms.ModelForm):
+    slug = forms.CharField(
+        max_length=Event._meta.get_field('slug').max_length,
+        required=not Event._meta.get_field('slug').blank,
+        validators=[
+            RegexValidator(
+                '[^\w-]+', inverse_match=True,
+                message='Only alphanumeric characters and "-" are allowed.')
+        ],
+    )
 
     host = selectable.AutoCompleteSelectField(
         lookup_class=lookups.HostLookup,


Test patch:
diff --git a/workshops/test/test_event.py b/workshops/test/test_event.py
--- a/workshops/test/test_event.py
+++ b/workshops/test/test_event.py
@@ -481,6 +481,26 @@ def test_number_of_attendees_increasing(self):
         event.refresh_from_db()
         assert event.attendance == 1
 
+    def test_slug_against_illegal_characters(self):
+        """Regression test: disallow events with slugs with wrong characters.
+
+        Only [\w-] are allowed."""
+        data = {
+            'slug': '',
+            'host_1': Host.objects.all()[0].pk,
+            'tags': Tag.objects.all(),
+        }
+        for slug in ['a/b', 'a b', 'a!b', 'a.b', 'a\\b', 'a?b']:
+            with self.subTest(slug=slug):
+                data['slug'] = slug
+                rv = self.client.post(reverse('event_add'), data, follow=False)
+                self.assertEqual(rv.status_code, 200)
+
+        # allow dashes in the slugs
+        data['slug'] = 'a-b'
+        rv = self.client.post(reverse('event_add'), data, follow=False)
+        self.assertEqual(rv.status_code, 200)
+
 
 class TestEventNotes(TestBase):
     """Make sure notes once written are saved forever!"""
Repository: carpentries/amy

Issue:
Event display page should highlight problematic fields
The "event display" page should mark all the things that the "events with problems" report complains about.



Discussion:

Resolving patch:
diff --git a/workshops/views.py b/workshops/views.py
--- a/workshops/views.py
+++ b/workshops/views.py
@@ -1378,7 +1378,9 @@ def workshop_issues(request):
     instructor = Role.objects.get(name='instructor')
     events = Event.objects.past_events().\
         filter(Q(attendance=None) | Q(attendance=0) |
-               Q(country=None) | Q(venue=None) | Q(address=None) |
+               Q(country=None) |
+               Q(venue=None) | Q(venue__exact='') |
+               Q(address=None) | Q(address__exact='') |
                Q(start__gt=F('end')))
     for e in events:
         tasks = Task.objects.filter(event=e).\


Test patch:
Repository: carpentries/amy

Issue:
Allow search in event URLS
Search should look at event URLs as well as everything else (because that's often an easy way to find workshops).



Discussion:

Resolving patch:
diff --git a/workshops/views.py b/workshops/views.py
--- a/workshops/views.py
+++ b/workshops/views.py
@@ -1351,8 +1351,12 @@ def search(request):
                     Q(slug__contains=term) |
                     Q(notes__contains=term) |
                     Q(host__domain__contains=term) |
-                    Q(host__fullname__contains=term)) \
-                    .order_by('-slug')
+                    Q(host__fullname__contains=term) |
+                    Q(url__contains=term) |
+                    Q(contact__contains=term) |
+                    Q(venue__contains=term) |
+                    Q(address__contains=term)
+                ).order_by('-slug')
                 results += list(events)
 
             if form.cleaned_data['in_persons']:


Test patch: