diff --git a/fec/fec/constants.py b/fec/fec/constants.py index 8674b1bc87..65495c803e 100644 --- a/fec/fec/constants.py +++ b/fec/fec/constants.py @@ -207,6 +207,73 @@ for category in report_category_groups.keys(): report_child_categories.update(report_category_groups[category]) +# TODO +# commissioner_item_categories are a single taxonomy with a faked hierarchy +commissioner_item_categories = OrderedDict([ + ('ballot-measures', 'Ballot Measures'), + ('best-efforts', '"Best Efforts"'), + ('contention-language', 'Contention Language'), + ('family-member-contributions', 'Family Member Contributions'), + ('coordination', 'Coordination'), + ('coordination/super-pacs', '↳ Super PACs'), + ('coordination/leadership-pac', '↳ Leadership PAC'), + ('coordination/hybrid-pacs', '↳ Hybrid PACs'), + ('coordination/corporations', '↳ Corporations'), + ('coordination/dark-money-groups-501c4', '↳ Dark Money Groups/501(c)(4)'), + ('coordination/state-political-party', '↳ State Political Party'), + ('coordination/membership-organization-pacs', '↳ Membership Organization PACs'), + ('contract-dispute', 'Contract Dispute'), + ('corporate-spending', 'Corporate Spending'), + ('corporate-spending/employee-coercion-threats-of-reprisal', '↳ Employee Coercion/Threats of Reprisal'), + ('corporate-spending/in-kind-contributions', '↳ In-Kind Contributions'), + ('corporate-spending/extending-credit', '↳ Extending Credit'), + ('corporate-spending/preparing-mailers', '↳ Preparing Mailers'), + ('corporate-spending/business-associations', '↳ Business Associations'), + ('corporate-spending/lobbyists-delivering-funds-on-behalf-of-corporation', + '↳ Lobbyists Delivering Funds on Behalf of Corporation'), + ('corporate-spending/nonprofit-corporation', '↳ Nonprofit Corporation'), + ('corporate-spending/salary-payments', '↳ Salary Payments'), + ('corporate-spending/improper-refund', '↳ Improper Refund'), + ('corporate-spending/express-advocacy', '↳ Express Advocacy'), + ('dark-money-501c4-groups', 'Dark Money/501(c)(4) groups'), + ('dark-money-501c4-groups/political-committee-status', '↳ Political Committee Status'), + ('dark-money-501c4-groups/failing-to-disclose-independent-expenditure', + '↳ Failing to Disclose Independent Expenditure'), + ('disclaimers', 'Disclaimers'), + ('disclaimers/brochure', '↳ Brochure'), + ('disclaimers/printed-box-requirement', '↳ Printed Box Requirement'), + ('disclaimers/internet', '↳ Internet'), + ('disclaimers/newspaper', '↳ Newspaper'), + ('disclaimers/mailer', '↳ Mailer'), + ('disclaimers/robocall', '↳ Robocall'), + ('disclaimers/radio', '↳ Radio'), + ('disclaimers/signs', '↳ Signs'), + ('disclaimers/television', '↳ Television'), + ('electioneering-communications', 'Electioneering Communications'), + ('express-advocacy', 'Express Advocacy'), + ('failing-to-disclose-independent-expenditure', 'Failing to Disclose Independent Expenditure'), + ('foreign-spending', 'Foreign Spending'), + ('hloga-air-travel', 'HLOGA/Air Travel'), + ('hosting-debates', 'Hosting Debates'), + ('increased-activity', 'Increased Activity'), + ('lobbyist-activity', 'Lobbyist Activity'), + ('membership-communications-exception', 'Membership Communications Exception'), + ('mischaracterization-of-party-in-court-filing', 'Mischaracterization of Party in Court Filing'), + ('payroll-deduction', 'Payroll Deduction'), + ('personal-use', 'Personal Use'), + ('press-exemption', 'Press Exemption'), + ('soft-money-use-of-non-federal-money-on-federal-expenses', + 'Soft Money/Use of Non-Federal Money on Federal Expenses'), + ('sale-use', 'Sale & Use'), + ('straw-donors-conduit-contributions', 'Straw Donors/Conduit Contributions'), + ('super-pac-not-disclosing-till-after-election', 'Super PAC not disclosing till after election'), + ('testing-the-waters-candidate-status', 'Testing the Waters/Candidate Status'), + ('unions', 'Unions'), + ('use-of-opponents-name-without-permission', 'Use of Opponent’s Name Without Permission'), + ('volunteer-exemption', 'Volunteer Exemption'), + ('volunteer-mailers-exemption', 'Volunteer Mailers Exemption'), +]) + # Search index constants # These are the parent pages for which we want *all* descendants of, not just direct children diff --git a/fec/fec/templates/partials/commissioner-item.html b/fec/fec/templates/partials/commissioner-item.html new file mode 100644 index 0000000000..5edab6c9ea --- /dev/null +++ b/fec/fec/templates/partials/commissioner-item.html @@ -0,0 +1,37 @@ +{# Template based on /fec/fec/templates/partials/update.html #} + + +{% load filters %} + +
+
+ {{ item.display_date }} +
+
+
+

+ {% if item.links_count == 1 %} + {% if item.link_doc %} + {{ item.title }} (PDF) + {% elif item.link_html %} + {{ item.title }} (HTML) + {% elif item.link_pdf %} + {{ item.title }} (PDF) + {% elif item.link_video %} + {{ item.title }} (HTML) + {% endif %} + {% else %} + {{ item.title }} {{ item.links_string|safe }} + {% endif %} +

+ {% if item.category %} + + {% endif %} +
+
+
diff --git a/fec/fec/urls.py b/fec/fec/urls.py index 5dcfcce062..a453e86e79 100644 --- a/fec/fec/urls.py +++ b/fec/fec/urls.py @@ -1,6 +1,7 @@ -from django.conf.urls import include, url +from django.conf.urls import include, url, re_path from django.conf import settings from django.contrib import admin + from django.views.generic.base import TemplateView from wagtail.admin import urls as wagtailadmin_urls @@ -22,6 +23,10 @@ url(r'^auth/', include(uaa_urls)), url(r'^admin/', include(wagtailadmin_urls)), url(r'^calendar/$', home_views.calendar), + re_path( + r'^about/leadership-and-structure/(?P[a-z0-9]+-[a-z0-9]+[a-z0-9-]*)/statements-and-opinions/$', # noqa: E501 + home_views.commissioner_statements_and_opinions + ), url(r'^about/leadership-and-structure/commissioners/$', home_views.commissioners), url(r'^documents/', include(wagtaildocs_urls)), url(r'^help-candidates-and-committees/question-rad/$', home_views.contact_rad), diff --git a/fec/home/models.py b/fec/home/models.py index 72125a0238..03047c1d7d 100644 --- a/fec/home/models.py +++ b/fec/home/models.py @@ -15,25 +15,38 @@ from wagtail.core import blocks from wagtail.admin.edit_handlers import ( FieldPanel, - StreamFieldPanel, - PageChooserPanel, + HelpPanel, InlinePanel, - MultiFieldPanel) + MultiFieldPanel, + PageChooserPanel, + StreamFieldPanel, +) from wagtail.images.blocks import ImageChooserBlock from wagtail.images.edit_handlers import ImageChooserPanel -from wagtail.documents.blocks import DocumentChooserBlock -from wagtail.snippets.models import register_snippet from wagtail.search import index +from wagtail.snippets.models import register_snippet from django.db.models.signals import m2m_changed from wagtail.contrib.table_block.blocks import TableBlock from fec import constants -from home.blocks import (ThumbnailBlock, # AsideLinkBlock, - ContactInfoBlock, CitationsBlock, ResourceBlock, - OptionBlock, CollectionBlock, DocumentFeedBlurb, - ExampleParagraph, ExampleForms, ExampleImage, - CustomTableBlock, ReportingExampleCards, InternalButtonBlock, - ExternalButtonBlock, SnippetChooserBlock) +from home.blocks import ( + CitationsBlock, + CollectionBlock, + ContactInfoBlock, + CustomTableBlock, + DocumentChooserBlock, + DocumentFeedBlurb, + ExampleForms, + ExampleImage, + ExampleParagraph, + ExternalButtonBlock, + InternalButtonBlock, + OptionBlock, + ReportingExampleCards, + ResourceBlock, + SnippetChooserBlock, + ThumbnailBlock, +) logger = logging.getLogger(__name__) @@ -840,6 +853,149 @@ def get_context(self, request): return context +# TODO +# We aren't planning to show individual commissioner items, +# only showing them in the list in commissioner_items_feed.html +class CommissionerItem(ContentPage): + related_document = StreamField([ + ('related_document', DocumentChooserBlock( + required=False, + null=True, + verbose_name='Linked document', + ) + )], + blank=True, + ) + display_date = models.DateField(default=datetime.date.today) + link_html = models.URLField( + max_length=255, + null=True, + verbose_name='HTML link', + blank=True, + ) + link_pdf = models.URLField( + max_length=255, + # help_text='If linking to a Document, use the selector, not this field.', + null=True, + verbose_name='PDF link', + blank=True, + ) + link_video = models.URLField( + max_length=255, null=True, verbose_name='Video link', blank=True, + ) + category = models.CharField( + choices=constants.commissioner_item_categories.items(), + max_length=255, + null=True + ) + commissioners = StreamField([ + ('commissioner', blocks.PageChooserBlock( + page_type=CommissionerPage, required=False, null=True, verbose_name='Connected commissioner(s)' + )), + ]) + + content_panels = Page.content_panels + [ + HelpPanel( + content='

This item will automatically be included in the "Statements and Opinions"\ + list of any linked Commissioners.

', + heading='ITEM HELP', + ), + MultiFieldPanel( + [ + FieldPanel('link_html'), + FieldPanel('link_pdf'), + FieldPanel('link_video'), + StreamFieldPanel('related_document'), + ], + heading='Content' + ), + FieldPanel('display_date'), + FieldPanel('category'), + StreamFieldPanel('commissioners', help_text='Not required, but choose as many as needed.'), + ] + + @property + def slug_category(self): + cat_slug = '' + cat_slug = self.category.split('/')[0] + return cat_slug + + @property + def slug_subject(self): + subj_slug = '' + subj_slug = self.category.split('/')[1] + return subj_slug + + @property + def pretty_category(self): + parent_category = '' + parent_category = self.category.split('/')[0] + + pretty_cat = str(constants.commissioner_item_categories[parent_category]) + return pretty_cat + + @property + def pretty_subject(self): + pretty_subj = str(constants.commissioner_item_categories[self.category]) + # If there's no slash in the category, we're dealing with a category and not a subject + # so return nothing + # + # The 'secondary' category / taxonomy label starts with two special characters + # so they display correctly in the admin pull-downs. + # We're going to pull them off for the pretty label + if len(self.category.split('/')) == 1: + pretty_subj = '' + elif pretty_subj.find('↳ ') == 0: + pretty_subj = pretty_subj[2:] + + return pretty_subj + + @property + def link_doc(self): + # Check whether the StreamValue has a related document + rel_doc_url = False + for block in self.related_document: + rel_doc_url = block.value.url + + return rel_doc_url + + @property + def links_count(self): + count = 0 + count = count + 1 if self.link_doc else count + count = count + 1 if self.link_html else count + count = count + 1 if self.link_pdf else count + count = count + 1 if self.link_video else count + return count + + @property + def links_string(self): + links = [] + + if self.link_doc: + links.append( + '\ + PDF'.format(self.link_doc) + ) + if self.link_pdf: + links.append( + '\ + PDF'.format(self.link_pdf) + ) + if self.link_html: + links.append( + '\ + HTML'.format(self.link_html) + ) + if self.link_video: + links.append( + '\ + VIDEO'.format(self.link_video) + ) + + return ' | '.join(links) + + class CollectionPage(Page): body = stream_factory(null=True, blank=True) sidebar_title = models.CharField(max_length=255, null=True, blank=True) diff --git a/fec/home/templates/home/commissioner_items_feed.html b/fec/home/templates/home/commissioner_items_feed.html new file mode 100644 index 0000000000..c459fd29df --- /dev/null +++ b/fec/home/templates/home/commissioner_items_feed.html @@ -0,0 +1,137 @@ +{# USAGE: Template for the single commissioner pages, #} +{# /about/leadership-and-structure/[commissioner-name-slug]/ #} + +{% extends "home/feed_base.html" %} +{% load wagtailcore_tags %} +{% load staticfiles %} +{% load filters %} + + +{% block intro %} +

Search or browse the statements and opinions made by Commissioner {{ self.commissioner_name }}.

+{% endblock %} + +{% block filters %} +
+
+ + +
+
+ {% if 'coordination' == self.category %} + + + {% elif 'corporate-spending' == self.category %} + + + + + + {% elif 'disclaimers' == self.category %} + + + {% else %} + + + {% endif %} +
+
+
+ + + +
+
+
+{% endblock %} + +{% block feed %} + {% if self.items %} + {% for item in self.items %} + {% include 'partials/commissioner-item.html' with update=update show_tag=True %} + {% endfor %} +
+ Page {{ self.items.number }} of {{ self.items.paginator.num_pages }} + {% if self.items.has_previous %} + Previous + {% endif %} + {% if self.items.has_next %} + Next + {% endif %} +
+ {% else %} +
+

No results

+

We didn’t find any statements or opinions by {{ self.commissioner_name }} matching the requested filters.

+
+ {% endif %} +{% endblock %} diff --git a/fec/home/templates/home/commissioner_page.html b/fec/home/templates/home/commissioner_page.html index 25719fdda8..da37477103 100644 --- a/fec/home/templates/home/commissioner_page.html +++ b/fec/home/templates/home/commissioner_page.html @@ -1,3 +1,6 @@ +{# USAGE: Template for the single commissioner pages, #} +{# /about/leadership-and-structure/[commissioner-name-slug]/ #} + {% extends "base.html" %} {% load wagtailcore_tags %} {% load wagtailimages_tags %} diff --git a/fec/home/templates/home/commissioners.html b/fec/home/templates/home/commissioners.html index 2dca0c63b5..9510f897dc 100644 --- a/fec/home/templates/home/commissioners.html +++ b/fec/home/templates/home/commissioners.html @@ -1,3 +1,6 @@ +{# USAGE: The template for the page that lists all commissioners #} +{# This template tiles /fec/home/templates/partials/commissioner.html #} + {% extends "base.html" %} {% load wagtailcore_tags %} {% load staticfiles %} diff --git a/fec/home/templates/home/latest_updates.html b/fec/home/templates/home/latest_updates.html index 959bbe67ed..b86dd12e5b 100644 --- a/fec/home/templates/home/latest_updates.html +++ b/fec/home/templates/home/latest_updates.html @@ -119,7 +119,7 @@

No results

While all press releases, weekly digests and tips for treasurers are searchable, FEC Record articles published before 2005 exist only in PDF format and are not included in these search results. Please try another search or visit our archive of Record articles from 1975-2004.

-

+

 

For assistance, contact the FEC.

diff --git a/fec/home/views.py b/fec/home/views.py index 95dfe1bd00..74d82b9ba4 100644 --- a/fec/home/views.py +++ b/fec/home/views.py @@ -10,7 +10,7 @@ from wagtail.documents.models import Document from fec.forms import ContactRAD # form_categories -from home.models import (CommissionerPage, DigestPage, MeetingPage, +from home.models import (CommissionerItem, CommissionerPage, DigestPage, MeetingPage, PressReleasePage, RecordPage, TipsForTreasurersPage) @@ -95,6 +95,52 @@ def get_tips(year=None, search=None): return tips +def get_commissioner(slug=None): + """ + Returns either a full or filtered QuerySet of every CommissionerPage object, the commssioners themselves + """ + commissioners = CommissionerPage.objects.live() + if slug: + commissioners = commissioners.filter(slug=slug).first() + + return commissioners + + +def get_commissioner_items(commissioner_slug=None, category=None, subject=None, year=None, request=None): + items = CommissionerItem.objects.live().order_by('-display_date') # live objects, most recent first + + if category: + items = items.filter(category__contains=category) + + if subject and category: + categorySubjectCombo = category + "/" + subject + items = items.filter(category__contains=categorySubjectCombo) + + if year: + year = int(year) + items = items.filter(display_date__gte=datetime(year, 1, 1)).filter( + display_date__lte=datetime(year, 12, 31) + ) + + # Finally, remove any without the requested slug in its commissioners list + if commissioner_slug: # If we're filtering to one commissioner, + for item in items: # For each commissioner item, + # for comm_item in item.commissioners: # Check its commissioners + if (commissioner_slug not in str(item.commissioners)): + items = items.not_page(item) + + page_num = request.GET.get("page", 1) + paginator = Paginator(items, 20) + try: + items = paginator.page(page_num) + except PageNotAnInteger: + items = paginator.page(1) + except EmptyPage: + items = paginator.page(paginator.num_pages) + + return items + + def updates(request): digests = "" records = "" @@ -149,6 +195,7 @@ def updates(request): tips = tips.filter(date__gte=datetime(year, 1, 1)).filter( date__lte=datetime(year, 12, 31) ) + # Not going to filter commissioner items by year here if search: press_releases = press_releases.search(search) @@ -199,6 +246,9 @@ def calendar(request): def commissioners(request): + """ + For the list of all commissioners + """ chair_commissioner = ( CommissionerPage.objects.filter(commissioner_title__contains="Chair") .exclude(commissioner_title__contains="Vice") @@ -234,6 +284,59 @@ def commissioners(request): return render(request, "home/commissioners.html", {"self": page_context}) +def commissioner_statements_and_opinions(request, commissioner_slug): + """ + TODO: DOCUMENTATION + """ + # We're only going to look at one requested category, subject, or year + req_category = request.GET.get("category", "") + req_subject = request.GET.get("subject", "") + req_year = request.GET.get("year", "") + + commissioner = get_commissioner(slug=commissioner_slug) + commissioner_name = commissioner.title + + commissioner_items = get_commissioner_items( + commissioner_slug=commissioner_slug, + category=req_category, + subject=req_subject, + year=req_year, + request=request + ) + + page_context = { + "category": req_category, + "subject": req_subject, + "year": req_year, + "title": "Commissioner %s statements and opinions" % (commissioner_name), + "content_section": "about", + "commissioner_slug": commissioner_slug, + "commissioner_name": commissioner_name, + "items": commissioner_items, + "ancestors": [ + {"title": "About the FEC", "url": "/about/"}, + { + "title": "Leadership and structure", + "url": "/about/leadership-and-structure", + }, + { + "title": "All Commissioners", + "url": "/about/leadership-and-structure/commissioners", + }, + { + "title": commissioner_name, + "url": "/about/leadership-and-structure/commissioners/%s/" % (commissioner_slug), + }, + { + "title": "Statements and Opinions", + "url": "/about/leadership-and-structure/commissioners/%s/statements-and-opinions" % (commissioner_slug), + }, + ], + } + + return render(request, "home/commissioner_items_feed.html", {"self": page_context}) + + def contact_rad(request): page_context = { "title": "Submit a question to the Reports Analysis Division (RAD)", diff --git a/requirements.txt b/requirements.txt index 55fbd4725c..66412267b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,9 +9,10 @@ psycopg2==2.8.5 requests==2.21.0 slacker==0.8.6 whitenoise==2.0.3 -wagtail==2.7.3 +wagtail==2.7.4 Jinja2==2.10.1 django_jinja==2.4.1 +pillow==7.1.0 CacheControl==0.11.5 cachetools==1.0.2 github3.py==0.9.6