Repository: carpentries/amy

Issue:
Extend workshop metatags validation with separate latitude and longitude tags
Ref. https://github.com/carpentries/workshop-template/issues/582

Currently there's only one combined tag `latlng` supported, we should implement validation for `latlng` XOR (`lat`, `lng`).

Code reference: [`amy.workshops.util.validate_metadata_from_event_website`](https://github.com/swcarpentry/amy/blob/develop/amy/workshops/util.py#L747).


Discussion:

Resolving patch:
diff --git a/amy/workshops/util.py b/amy/workshops/util.py
--- a/amy/workshops/util.py
+++ b/amy/workshops/util.py
@@ -52,7 +52,8 @@
 
 ALLOWED_METADATA_NAMES = [
     'slug', 'startdate', 'enddate', 'country', 'venue', 'address',
-    'latlng', 'language', 'eventbrite', 'instructor', 'helper', 'contact',
+    'latlng', 'lat', 'lng', 'language', 'eventbrite', 'instructor', 'helper',
+    'contact',
 ]
 
 
@@ -649,9 +650,9 @@ def find_metadata_on_event_homepage(content):
         # get metadata to the form returned by `find_metadata_on_event_website`
         # because YAML tries to interpret values from index's header
         filtered_metadata = {key: value for key, value in metadata.items()
-                         if key in ALLOWED_METADATA_NAMES}
+                             if key in ALLOWED_METADATA_NAMES}
         for key, value in filtered_metadata.items():
-            if isinstance(value, int):
+            if isinstance(value, int) or isinstance(value, float):
                 filtered_metadata[key] = str(value)
             elif isinstance(value, datetime.date):
                 filtered_metadata[key] = '{:%Y-%m-%d}'.format(value)
@@ -686,15 +687,23 @@ def parse_metadata_from_event_website(metadata):
     if len(language) < 2:
         language = ''
 
+    # read either ('lat', 'lng') pair or (old) 'latlng' comma-separated value
+    if 'lat' in metadata and 'lng' in metadata:
+        latitude = metadata.get('lat', '')
+        longitude = metadata.get('lng', '')
+    else:
+        try:
+            latitude, longitude = metadata.get('latlng', '').split(',')
+        except (ValueError, AttributeError):
+            latitude, longitude = None, None
+
     try:
-        latitude, _ = metadata.get('latlng', '').split(',')
         latitude = float(latitude.strip())
     except (ValueError, AttributeError):
         # value error: can't convert string to float
         # attribute error: object doesn't have "split" or "strip" methods
         latitude = None
     try:
-        _, longitude = metadata.get('latlng', '').split(',')
         longitude = float(longitude.strip())
     except (ValueError, AttributeError):
         # value error: can't convert string to float
@@ -766,14 +775,27 @@ def validate_metadata_from_event_website(metadata):
         Requirement('country', None, True, TWOCHAR_FMT),
         Requirement('venue', None, True, None),
         Requirement('address', None, True, None),
-        Requirement('latlng', 'latitude / longitude', True,
-                    '^' + FRACTION_FMT + r',\s?' + FRACTION_FMT + '$'),
         Requirement('instructor', None, True, None),
         Requirement('helper', None, True, None),
         Requirement('contact', None, True, None),
         Requirement('eventbrite', 'Eventbrite event ID', False, r'^\d+$'),
     ]
 
+    # additional, separate check for latitude and longitude data
+    latlng_req = Requirement('latlng', 'latitude / longitude', True,
+                             r'^{},\s?{}$'.format(FRACTION_FMT, FRACTION_FMT))
+    lat_req = Requirement('lat', 'latitude', True, '^' + FRACTION_FMT + '$')
+    lng_req = Requirement('lng', 'longitude', True, '^' + FRACTION_FMT + '$')
+
+    # separate 'lat' and 'lng' are supported since #1461,
+    # but here we're checking which requirement to add to the list of
+    # "required" requirements
+    if 'lat' in metadata or 'lng' in metadata:
+        requirements.append(lat_req)
+        requirements.append(lng_req)
+    else:
+        requirements.append(latlng_req)
+
     for requirement in requirements:
         d_ = requirement._asdict()
         name_ = ('{display} ({name})'.format(**d_)


Test patch:
diff --git a/amy/workshops/tests/test_util.py b/amy/workshops/tests/test_util.py
--- a/amy/workshops/tests/test_util.py
+++ b/amy/workshops/tests/test_util.py
@@ -484,6 +484,177 @@ def test_no_attribute_error_missing_instructors_helpers(self):
                 self.assertEqual(expected, parse_metadata_from_event_website(metadata))
 
 
+class TestAlternativeLatitudeLongitude(TestBase):
+    maxDiff = None
+
+    html_content_old = """
+<html><head>
+<meta name="latlng" content="36.998977, -109.045173" />
+</head>
+<body>
+<h1>test</h1>
+</body></html>
+"""
+    html_content_new = """
+<html><head>
+<meta name="lat" content="36.998977" />
+<meta name="lng" content="-109.045173" />
+</head>
+<body>
+<h1>test</h1>
+</body></html>
+"""
+    yaml_content_old = """---
+layout: workshop
+root: .
+latlng: 36.998977, -109.045173
+----
+Other content.
+"""
+    yaml_content_new = """---
+layout: workshop
+root: .
+lat: 36.998977
+lng: -109.045173
+----
+Other content.
+"""
+
+    def test_finding_metadata_on_index(self):
+        content = self.yaml_content_old
+        expected = {
+            'latlng': '36.998977, -109.045173',
+        }
+        self.assertEqual(expected, find_metadata_on_event_homepage(content))
+
+        content = self.yaml_content_new
+        expected = {
+            'lat': '36.998977',
+            'lng': '-109.045173',
+        }
+        self.assertEqual(expected, find_metadata_on_event_homepage(content))
+
+    def test_finding_metadata_on_website(self):
+        content = self.html_content_old
+        expected = {
+            'latlng': '36.998977, -109.045173',
+        }
+        self.assertEqual(expected, find_metadata_on_event_website(content))
+
+        content = self.html_content_new
+        expected = {
+            'lat': '36.998977',
+            'lng': '-109.045173',
+        }
+        self.assertEqual(expected, find_metadata_on_event_website(content))
+
+    def test_parsing_old_latlng(self):
+        metadata = {
+            'slug': '2015-07-13-test',
+            'startdate': '2015-07-13',
+            'enddate': '2015-07-14',
+            'country': 'us',
+            'venue': 'Euphoric State University',
+            'address': 'Highway to Heaven 42, Academipolis',
+            'latlng': '36.998977, -109.045173',
+            'language': 'us',
+            'instructor': 'Hermione Granger|Ron Weasley',
+            'helper': 'Peter Parker|Tony Stark|Natasha Romanova',
+            'contact': 'hermione@granger.co.uk, rweasley@ministry.gov',
+            'eventbrite': '10000000',
+        }
+        expected = {
+            'slug': '2015-07-13-test',
+            'language': 'US',
+            'start': datetime.date(2015, 7, 13),
+            'end': datetime.date(2015, 7, 14),
+            'country': 'US',
+            'venue': 'Euphoric State University',
+            'address': 'Highway to Heaven 42, Academipolis',
+            'latitude': 36.998977,
+            'longitude': -109.045173,
+            'reg_key': 10000000,
+            'instructors': ['Hermione Granger', 'Ron Weasley'],
+            'helpers': ['Peter Parker', 'Tony Stark', 'Natasha Romanova'],
+            'contact': 'hermione@granger.co.uk, rweasley@ministry.gov',
+        }
+        self.assertEqual(expected, parse_metadata_from_event_website(metadata))
+
+    def test_parsing_new_latlng(self):
+        metadata = {
+            'slug': '2015-07-13-test',
+            'startdate': '2015-07-13',
+            'enddate': '2015-07-14',
+            'country': 'us',
+            'venue': 'Euphoric State University',
+            'address': 'Highway to Heaven 42, Academipolis',
+            'lat': '36.998977',
+            'lng': '-109.045173',
+            'language': 'us',
+            'instructor': 'Hermione Granger|Ron Weasley',
+            'helper': 'Peter Parker|Tony Stark|Natasha Romanova',
+            'contact': 'hermione@granger.co.uk, rweasley@ministry.gov',
+            'eventbrite': '10000000',
+        }
+        expected = {
+            'slug': '2015-07-13-test',
+            'language': 'US',
+            'start': datetime.date(2015, 7, 13),
+            'end': datetime.date(2015, 7, 14),
+            'country': 'US',
+            'venue': 'Euphoric State University',
+            'address': 'Highway to Heaven 42, Academipolis',
+            'latitude': 36.998977,
+            'longitude': -109.045173,
+            'reg_key': 10000000,
+            'instructors': ['Hermione Granger', 'Ron Weasley'],
+            'helpers': ['Peter Parker', 'Tony Stark', 'Natasha Romanova'],
+            'contact': 'hermione@granger.co.uk, rweasley@ministry.gov',
+        }
+        self.assertEqual(expected, parse_metadata_from_event_website(metadata))
+
+    def test_validating_old_latlng(self):
+        """"""
+        metadata = {
+            'slug': '2015-07-13-test',
+            'language': 'us',
+            'startdate': '2015-07-13',
+            'enddate': '2015-07-14',
+            'country': 'us',
+            'venue': 'Euphoric State University',
+            'address': 'Highway to Heaven 42, Academipolis',
+            'eventbrite': '10000000',
+            'instructor': 'Hermione Granger, Ron Weasley',
+            'helper': 'Peter Parker, Tony Stark, Natasha Romanova',
+            'contact': 'hermione@granger.co.uk, rweasley@ministry.gov',
+            'latlng': '36.998977, -109.045173',
+        }
+        errors, warnings = validate_metadata_from_event_website(metadata)
+        self.assertEqual(errors, [])
+        self.assertEqual(warnings, [])
+
+    def test_validating_new_latlng(self):
+        """"""
+        metadata = {
+            'slug': '2015-07-13-test',
+            'language': 'us',
+            'startdate': '2015-07-13',
+            'enddate': '2015-07-14',
+            'country': 'us',
+            'venue': 'Euphoric State University',
+            'address': 'Highway to Heaven 42, Academipolis',
+            'eventbrite': '10000000',
+            'instructor': 'Hermione Granger, Ron Weasley',
+            'helper': 'Peter Parker, Tony Stark, Natasha Romanova',
+            'contact': 'hermione@granger.co.uk, rweasley@ministry.gov',
+            'lat': '36.998977',
+            'lng': '-109.045173',
+        }
+        errors, warnings = validate_metadata_from_event_website(metadata)
+        self.assertEqual(errors, [])
+        self.assertEqual(warnings, [])
+
+
 class TestMembership(TestBase):
     """Tests for SCF membership."""