Repository: cfpb/consumerfinance.gov

Issue:
Changelog - Link to semver info from readme
https://github.com/cfpb/cfgov-refresh/blob/flapjack/CHANGELOG.md links to readme at the top, but could connect direct to https://github.com/cfpb/cfgov-refresh/blob/flapjack/README.md#how-this-repo-is-versioned

OR

semver section from readme could possibly be moved entirely over to the top or bottom of the changelog.



Discussion:

Resolving patch:
diff --git a/_lib/wordpress_event_processor.py b/_lib/wordpress_event_processor.py
new file mode 100644
--- /dev/null
+++ b/_lib/wordpress_event_processor.py
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+
+import sys
+import json
+import os.path
+import requests
+import dateutil.parser
+
+
+def posts_at_url(url):
+    """
+    Get WordPress posts as JSON from the given URL.
+    """
+    current_page = 1
+    max_page = sys.maxint
+
+    while current_page <= max_page:
+        url = os.path.expandvars(url)
+        resp = requests.get(url, params={'page':current_page, 'count': '-1'})
+        results = json.loads(resp.content)
+        current_page += 1
+        max_page = results['pages']
+        for p in results['posts']:
+            yield p
+
+
+def documents(name, url, **kwargs):
+    """
+    Yield documents to be indexed in Elasticsearch, one at a time.
+    """
+    for event in posts_at_url(url):
+        yield process_event(event)
+
+
+def process_event(event):
+    """
+    Process an event as provided by the WordPress API and return
+    JSON suitable for indexing in Elasticsearch.
+    """
+
+    ## First some helper functions
+    format_datetime = lambda d: dateutil.parser.parse(d).strftime('%Y-%m-%dT%H:%M:%SZ')
+    get_custom_field = lambda f, d=[]: event['custom_fields'].get(f, d)
+    get_custom_field_member = lambda f, i=0, d='': event['custom_fields'].get(f)[i] \
+                                                   if len(event['custom_fields'].get(f, [])) > i \
+                                                   else d
+
+    # Do some work on default WP fields
+    del event['comments']
+    event['_id'] = event['slug']
+    event['tags'] = [tag['title'] for tag in event['taxonomy_fj_tag']]
+    event['date'] = format_datetime(event['date'])
+    event['author'] = event['author']['name']
+
+    # Basic Event Fields
+    event['title'] = get_custom_field_member('event_information_title')
+    event['event_intro'] = get_custom_field_member('event_information_event_intro', d='')
+    event['event_desc'] = get_custom_field_member('event_information_event_desc', d='')
+
+    # Venue Fields
+    event['venue'] = {}
+    event['venue']['name'] = get_custom_field_member('event_information_venue_name', d='')
+    event['venue']['address'] = {}
+    event['venue']['address']['address'] = get_custom_field_member('event_information_venue_address_address', d='')
+    event['venue']['address']['suite'] = get_custom_field_member('event_information_venue_address_suite', d='')
+    event['venue']['address']['city'] = get_custom_field_member('event_information_venue_address_city', d='')
+    event['venue']['address']['state'] = get_custom_field_member('event_information_venue_address_state', d='')
+    event['venue']['address']['zip_code'] = get_custom_field_member('event_information_venue_address_zip_code', d='')
+
+    # Agenda Fields
+    event['agenda'] = {}
+    event['agenda']['speaker'] = get_custom_field_member('event_agenda_speaker', d='')
+    event['agenda']['desc'] = get_custom_field_member('event_agenda_desc', d='')
+    event['agenda']['venue_location'] = get_custom_field_member('event_agenda_venue_location', d='')
+
+    # Timestamps
+    event['timestamps'] = {}
+    event['timestamps']['event_date'] = get_custom_field_member('event_date')
+    event['timestamps']['event_time'] = get_custom_field_member('event_time')
+    event['timestamps']['agenda_beginning'] = get_custom_field_member('event_agenda_beginning_time')
+    event['timestamps']['agenda_ending'] = get_custom_field_member('event_agenda_ending_time')
+    event['timestamps']['published_date'] = get_custom_field_member('event_information_published_date')
+    event['timestamps']['post_event'] = get_custom_field_member('become_post_event')
+    event['timestamps']['become_live'] = get_custom_field_member('become_live_event')
+
+    # Livestream
+    event['livestream'] = {}
+    event['livestream']['available'] = get_custom_field_member('live_event_live_stream_available', d='')
+
+    # Post-Event
+    event['post_event'] = {}
+    event['post_event']['video_transcript'] = get_custom_field_member('post_event_video_transcript', d='')
+    event['post_event']['speech_transcript'] = get_custom_field_member('post_event_speech_transcript', d='')
+    event['post_event']['flickr_url'] = get_custom_field_member('post_event_flickr_url', d='')
+    event['post_event']['youtube_url'] = get_custom_field_member('post_event_youtube_url', d='')
+
+    # Images
+    event['images'] = {}
+    event['images']['live_event_preview'] = get_custom_field_member('live_event_preview_image')
+    event['images']['live_event'] = get_custom_field_member('live_event_live_event_image')
+    event['images']['pre-event'] = get_custom_field_member('event_information_pre-event_image')
+    event['images']['event'] = get_custom_field_member('event_information_event_image')
+    event['images']['post_event_pre'] = get_custom_field_member('post_event_pre-image')
+    event['images']['post_event'] = get_custom_field_member('post_event_image')
+
+    # Reservations
+    event['reservation'] = {}
+    event['reservation']['desc'] = get_custom_field_member('event_information_reservation_desc', d='')
+    event['reservation']['contact_info'] = get_custom_field_member('event_information_reservation_contact_info', d='')
+
+    return event
+
diff --git a/_lib/wordpress_view_processor.py b/_lib/wordpress_view_processor.py
--- a/_lib/wordpress_view_processor.py
+++ b/_lib/wordpress_view_processor.py
@@ -44,6 +44,9 @@ def process_view(post):
         if key in custom_fields:
             related.append(custom_fields[key])
     post['related_links'] = related
+    dt = dateutil.parser.parse(post['date'])
+    dt_string = dt.strftime('%Y-%m-%dT%H:%M:%SZ')
+    post['date'] = dt_string
 
     # append the hero information
     if 'related_hero' in custom_fields and custom_fields['related_hero'][0] != '':


Test patch:
diff --git a/_tests/macro_testing/README.md b/_tests/macro_testing/README.md
new file mode 100644
--- /dev/null
+++ b/_tests/macro_testing/README.md
@@ -0,0 +1,15 @@
+# Template Macro Tests
+
+## Running
+
+From within this directory:
+
+1. Install the requirements: `pip install -r requirements.txt`
+2. Run the test runner: `python test_macros.py`
+
+## Writing Tests
+
+Please see [Macro Polo](https://github.com/cfpb/macropolo) for 
+documentation about writing tests.
+
+
diff --git a/_tests/macro_testing/requirements.txt b/_tests/macro_testing/requirements.txt
new file mode 100644
--- /dev/null
+++ b/_tests/macro_testing/requirements.txt
@@ -0,0 +1 @@
+macropolo==0.1.0
diff --git a/_tests/macro_testing/test_macros.py b/_tests/macro_testing/test_macros.py
new file mode 100644
--- /dev/null
+++ b/_tests/macro_testing/test_macros.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+
+import unittest
+import os.path
+from macropolo import MacroTestCase, JSONTestCaseLoader
+from macropolo.environments import SheerEnvironment
+
+class CFGovTestCase(SheerEnvironment, MacroTestCase):
+    """
+    A MacroTestCase subclass for cfgov-refresh.
+    """
+
+    def search_root(self):
+        """
+        Return the root of the search path for templates.
+        """
+        # Get the cfgov-refresh root dir, ../../../
+        # PLEASE NOTE: This presumes that the file containing the test always
+        # lives three levels above the cfgov-refresh root.
+        root_dir = os.path.abspath(os.path.join(os.path.dirname( __file__ ),
+                                                os.pardir, os.pardir))
+        return root_dir
+
+
+    def search_exceptions(self):
+        """
+        Return a list of a subdirectory names that should not be searched
+        for templates.
+        """
+        return ['_defaults', '_lib', '_queries', '_settings', '_tests']
+
+
+# Create CFGovTestCase subclasses for all JSON tests.
+tests_path = os.path.abspath(os.path.join(os.path.dirname( __file__ ), 'tests'))
+JSONTestCaseLoader(tests_path, CFGovTestCase, globals())
+
+
+# Run the tests if we're executed
+if __name__ == '__main__':
+    unittest.main()
+
+
diff --git a/_tests/macro_testing/tests/event-landing-macros.json b/_tests/macro_testing/tests/event-landing-macros.json
new file mode 100644
--- /dev/null
+++ b/_tests/macro_testing/tests/event-landing-macros.json
@@ -0,0 +1,39 @@
+{
+    "file": "_event-landing-macros.html",
+    "tests": [
+        {
+            "macro_name": "event_preview",
+            "arguments": [
+                {
+                    "title": "Test Post",
+                    "dek": "A dek",
+                    "excerpt": "An excerpt",
+                    "author": ["Me"],
+                    "date": "2014-10-28"
+                 }
+            ],
+            "assertions": [
+                {
+                    "selector": ".summary_heading",
+                    "index": 0,
+                    "assertion": "in",
+                    "value": "Test Post"
+                },
+                {
+                    "selector": ".calendar-icon span",
+                    "index": 0,
+                    "value": "u-visually-hidden",
+                    "assertion": "equal",
+                    "attribute": "class"
+                },
+                {
+                    "selector": ".calendar-icon span",
+                    "index": 0,
+                    "value": "Oct 28, 2014",
+                    "assertion": "in"
+                }
+            
+            ]
+        }
+    ]
+}
diff --git a/_tests/macro_testing/tests/post-macros.json b/_tests/macro_testing/tests/post-macros.json
new file mode 100644
--- /dev/null
+++ b/_tests/macro_testing/tests/post-macros.json
@@ -0,0 +1,43 @@
+{
+    "file": "post-macros.html",
+    "tests": [
+        {
+            "macro_name": "category_icon",
+            "arguments": ["announcements & updates", ""],
+            "assertions": [
+                {
+                    "selector": ".cf-icon-bullhorn",
+                    "assertion": "exists"
+                }
+            ]
+        },
+        {
+            "macro_name": "category_icon",
+            "arguments": ["innovation & data", ""],
+            "assertions": [
+                {
+                    "selector": ".cf-icon-lightbulb",
+                    "assertion": "exists"
+                }
+            ]
+        },
+        {
+            "test_name": "post summary",
+            "macro_name": "post_summary",
+            "arguments": [
+                    {"title": "Test Post",
+                     "dek": "A dek",
+                     "excerpt": "An excerpt",
+                     "author": ["Me"]}
+            ],
+            "assertions": [
+                {
+                    "selector": ".summary_heading",
+                    "index": 0,
+                    "assertion": "in",
+                    "value": "Test Post"
+                }
+            ]
+        }
+    ]
+}
diff --git a/_tests/processor_tests/tests/test_wordpress_event_processor.json b/_tests/processor_tests/tests/test_wordpress_event_processor.json
new file mode 100644
--- /dev/null
+++ b/_tests/processor_tests/tests/test_wordpress_event_processor.json
@@ -0,0 +1,204 @@
+{
+    "status": "ok",
+    "count": 1,
+    "count_total": 1,
+    "pages": 1,
+    "posts": [
+    {
+        "id": 1,
+        "type": "event",
+        "slug": "auto-draft",
+        "url": "http://somewhere/event/test-event/",
+        "status": "publish",
+        "title": "Test Event",
+        "title_plain": "Test Event",
+        "content": "",
+        "excerpt": "",
+        "date": "2015-03-17 17:38:58",
+        "modified": "2015-03-25 11:47:16",
+        "categories": [],
+        "tags": [],
+        "author":{  
+            "id":1,
+            "slug":"aauthor",
+            "name":"aauthor",
+            "first_name":"Arthur",
+            "last_name":"Author",
+            "nickname":"aauthor",
+            "url":"",
+            "description":""
+        },
+        "comments": [],
+        "attachments": [],
+        "comment_count": 0,
+        "comment_status": "closed",
+        "custom_fields": {
+            "event_information_title": [
+                "Title of the Event"
+            ],
+            "event_information_venue_name":[  
+                "Title of the event location"
+            ],
+            "event_information_venue_address_address":[  
+                "1234 A Boulevard"
+            ],
+            "event_information_venue_address_suite":[  
+                "A Suite"
+            ],
+            "event_information_venue_address_city":[  
+                "Big City"
+            ],
+            "event_information_venue_address_state":[  
+                "NY"
+            ],
+            "event_information_venue_address_zip_code":[  
+                "12345"
+            ],
+            "event_information_event_intro":[  
+                "Some event introduction."
+            ],
+            "event_information_event_desc":[  
+                "This is an event's description."
+            ],
+            "event_information_reservation_desc":[  
+                "This describes how to make a reservation."
+            ],
+            "event_information_reservation_contact_info":[  
+                "Event contact info"
+            ],
+            "event_time_timezone": [
+                "a:1:{i:0;s:0:\"\";}"
+            ],
+            "event_agenda_beginning_time_timezone": [
+                "a:1:{i:0;s:0:\"\";}"
+            ],
+            "event_agenda_ending_time_timezone": [
+                "a:1:{i:0;s:0:\"\";}"
+            ],
+            "become_live_event_timezone": [
+                "a:1:{i:0;s:17:\"America/Anchorage\";}"
+            ],
+            "become_post_event_timezone": [
+                "a:1:{i:0;s:12:\"America/Adak\";}"
+            ],
+            "event_information_venue_address_suite": [
+                "A Suite"
+            ],
+            "event_information_reservation_contact_info": [
+                "Event contact info"
+            ],
+            "event_agenda_speaker": [
+                "Speaker Name"
+            ],
+            "event_agenda_desc": [
+                "Agenda description"
+            ],
+            "event_agenda_venue_location": [
+                "vanue location in agenda"
+            ],
+            "live_event_live_stream_available": [
+                "on"
+            ],
+            "post_event_flickr_url": [
+                "http://flickr.com/something"
+            ],
+            "post_event_youtube_url": [
+                "http://youtube.com/something"
+            ],
+            "published_date": [
+                "2015-01-01T00:00:00+0000"
+            ],
+            "event_date": [
+                "2015-02-01T00:00:00+0000"
+            ],
+            "event_time": [
+                "2015-03-25T01:00:00-0900"
+            ],
+            "event_agenda_beginning_time": [
+                "2015-03-25T01:00:00-0900"
+            ],
+            "event_agenda_ending_time": [
+                "2015-03-25T02:00:00-0900"
+            ],
+            "become_live_event": [
+                "2015-03-01T03:00:00-1000"
+            ],
+            "become_post_event": [
+                "2015-04-01T04:00:00-0900"
+            ]
+        },
+        "dek": "",
+        "order": 0,
+        "parent": 0,
+        "taxonomy_fj_tag": [],
+        "taxonomy_published_date": [
+        {
+            "id": 624,
+            "slug": "january-1-2015",
+            "title": "January 1 2015",
+            "description": "",
+            "post_count": 1
+        }
+        ],
+        "taxonomy_event_date": [
+        {
+            "id": 625,
+            "slug": "february-1-2015",
+            "title": "February 1 2015",
+            "description": "",
+            "post_count": 1
+        }
+        ],
+        "taxonomy_event_time": [
+        {
+            "id": 626,
+            "slug": "100am-americaadak",
+            "title": "1:00am America/Adak",
+            "description": "",
+            "post_count": 1
+        }
+        ],
+        "taxonomy_event_agenda_beginning_time": [
+        {
+            "id": 627,
+            "slug": "100am-americaadak",
+            "title": "1:00am America/Adak",
+            "description": "",
+            "post_count": 1
+        }
+        ],
+        "taxonomy_event_agenda_ending_time": [
+        {
+            "id": 628,
+            "slug": "200am-americaadak",
+            "title": "2:00am America/Adak",
+            "description": "",
+            "post_count": 1
+        }
+        ],
+        "taxonomy_become_live_event": [
+        {
+            "id": 629,
+            "slug": "march-1-2015-300am-americaadak",
+            "title": "March 1 2015 3:00am America/Adak",
+            "description": "",
+            "post_count": 1
+        }
+        ],
+        "taxonomy_become_post_event": [
+        {
+            "id": 630,
+            "slug": "april-1-2015-400am-americaadak",
+            "title": "April 1 2015 4:00am America/Adak",
+            "description": "",
+            "post_count": 1
+        }
+        ]
+    }
+    ],
+    "query": {
+        "ignore_sticky_posts": true,
+        "post_type": "event"
+    }
+}
+
diff --git a/_tests/processor_tests/tests/test_wordpress_event_processor.py b/_tests/processor_tests/tests/test_wordpress_event_processor.py
new file mode 100644
--- /dev/null
+++ b/_tests/processor_tests/tests/test_wordpress_event_processor.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+
+import unittest
+import mock
+
+import os
+
+import json
+import wordpress_event_processor
+
+class WordpressEventProcessorTestCase(unittest.TestCase):
+    """
+    wordpress_event_processor grabs events from the WordPress API and
+    returns them for indexing in Elasticsearch by Sheer.
+
+    This doesn't unittest individual functions within the module. It
+    tests the `documents()` function, which is what Sheer calls, and
+    ensures that the output is appropriate for the input.
+    """
+
+    @mock.patch('requests.get')
+    def test_event(self, mock_requests_get):
+        # /api/get_posts/?post_type=event
+        mock_response = mock.Mock()
+        mock_response.content = open(os.path.join(os.path.dirname(__file__),
+                                    'test_wordpress_event_processor.json')).read()
+        mock_requests_get.return_value = mock_response
+
+        name = 'event'
+        url = 'http://mockmockmock/api/get_posts/?post_type=event'
+
+        documents = list(wordpress_event_processor.documents(name, url))
+        document = documents[0]
+        custom_fields = document['custom_fields']
+
+        # Ensure that the custom fields are present in the document
+        self.assertIn('title', document)
+        self.assertIn('published_date', document)
+        self.assertIn('venue_name', document)
+        self.assertIn('venue_address_address', document)
+        self.assertIn('venue_address_suite', document)
+        self.assertIn('venue_address_city', document)
+        self.assertIn('venue_address_state', document)
+        self.assertIn('venue_address_zip_code', document)
+        self.assertIn('pre-event_image', document)
+        self.assertIn('event_image', document)
+        self.assertIn('event_date', document)
+        self.assertIn('event_time', document)
+        self.assertIn('event_intro', document)
+        self.assertIn('event_desc', document)
+        self.assertIn('reservation_desc', document)
+        self.assertIn('reservation_contact_info', document)
+        self.assertIn('agenda_beginning_time', document)
+        self.assertIn('agenda_ending_time', document)
+        self.assertIn('agenda_speaker', document)
+        self.assertIn('agenda_desc', document)
+        self.assertIn('agenda_venue_location', document)
+        self.assertIn('live_event_image', document)
+        self.assertIn('live_event_preview_image', document)
+        self.assertIn('live_event_live_stream_available', document)
+        self.assertIn('live_event_become_live_event', document)
+        self.assertIn('post_event_become_post_event', document)
+        self.assertIn('post_event_video_transcript', document)
+        self.assertIn('post_event_speech_transcript', document)
+        self.assertIn('post_event_image', document)
+        self.assertIn('post_event_pre-image', document)
+        self.assertIn('post_event_flickr_url', document)
+        self.assertIn('post_event_youtube_url', document)
+
+