Skip to content

Commit

Permalink
Merge pull request #392 from datamade/minimal
Browse files Browse the repository at this point in the history
big merge in of django-councilmatic views and functionality
  • Loading branch information
derekeder authored Dec 20, 2023
2 parents 0d90b05 + f1330e8 commit cb61909
Show file tree
Hide file tree
Showing 131 changed files with 1,656 additions and 875 deletions.
7 changes: 1 addition & 6 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,7 @@
"formation": {
"web": {
"quantity": 1,
"size": "hobby"
}
},
"environments": {
"review": {
"addons": ["heroku-postgresql:hobby-basic"]
"size": "standard-1x"
}
},
"buildpacks": [],
Expand Down
284 changes: 274 additions & 10 deletions chicago/feeds.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,278 @@
from councilmatic_core.feeds import CouncilmaticFacetedSearchFeed, BillDetailActionFeed
from chicago.models import ChicagoBill
import urllib

from haystack.query import SearchQuerySet

class ChicagoCouncilmaticFacetedSearchFeed(CouncilmaticFacetedSearchFeed):
# same as CouncilmaticFacetedSearchFeed but have a better item name
# template which uses NYCBill's friendly_name() as opposed to Bill's
# friendly_name()
title_template = "feeds/chicago_search_item_title.html"
bill_model = ChicagoBill
from django.contrib.syndication.views import Feed
from django.utils.feedgenerator import Rss201rev2Feed
from django.urls import reverse, reverse_lazy
from django.conf import settings

from .models import Person, Bill, Organization, Event
from .utils import to_datetime

class ChicagoBillDetailActionFeed(BillDetailActionFeed):
title_template = "feeds/chicago_bill_actions_item_title.html"

class FacetedSearchFeed(Feed):
title_template = "feeds/search_item_title.html"
description_template = "feeds/search_item_description.html"
bill_model = Bill

all_results = None
sqs = (
SearchQuerySet()
.facet("bill_type")
.facet("sponsorships", sort="index")
.facet("controlling_body")
.facet("inferred_status")
)
query = None

def url_with_querystring(self, path, **kwargs):
return path + "?" + urllib.parse.urlencode(kwargs)

def get_object(self, request):
self.queryDict = request.GET

all_results = SearchQuerySet().all()
facets = None

if "selected_facets" in request.GET:
facets = request.GET.getlist("selected_facets")

if "q" in request.GET:
self.query = request.GET["q"]
results = all_results.filter(content=self.query)

if facets:
for facet in facets:
(facet_name, facet_value) = facet.split(":")
facet_name = facet_name.rsplit("_exact")[0]
results = results.narrow("%s:%s" % (facet_name, facet_value))
elif facets:
for facet in facets:
(facet_name, facet_value) = facet.split(":")
facet_name = facet_name.rsplit("_exact")[0]
results = all_results.narrow("%s:%s" % (facet_name, facet_value))

return results.order_by("-last_action_date")

def title(self, obj):
if self.query:
title = (
settings.SITE_META["site_name"]
+ ": Search for '"
+ self.query.capitalize()
+ "'"
)
# XXX: create a nice title based on all search parameters
else:
title = settings.SITE_META["site_name"] + ": Filtered Search"

return title

def link(self, obj):
# return the main non-RSS search URL somehow
# XXX maybe "quargs" - evz
# return reverse('councilmatic_search', args=(searchqueryset=self.sqs,))
url = self.url_with_querystring(
reverse("{}:councilmatic_search_feed".format(settings.APP_NAME)),
q=self.query,
)
return url

def item_link(self, bill):
return reverse("bill_detail", args=(bill.slug,))

def item_pubdate(self, bill):
return to_datetime(bill.last_action_date)

def description(self, obj):
return "Bills returned from search"

def items(self, query):
l_items = query[:20]
pks = [i.pk for i in l_items]
bills = self.bill_model.objects.filter(pk__in=pks).order_by("-last_action_date")
return bills


class PersonDetailFeed(Feed):
"""The PersonDetailFeed provides an RSS feed for a given committee member,
returning the most recent 20 bills for which they are the primary sponsor;
and for each bill, the list of sponsores and the action history.
"""

title_template = "feeds/person_detail_item_title.html"
description_template = "feeds/person_detail_item_description.html"
feed_type = Rss201rev2Feed
NUM_RECENT_BILLS = 20

def get_object(self, request, slug):
o = Person.objects.get(slug=slug)
return o

def title(self, obj):
return (
settings.SITE_META["site_name"]
+ ": "
+ settings.CITY_VOCAB["COUNCIL_MEMBER"]
+ " %s: Recently Sponsored Bills" % obj.name
)

def link(self, obj):
return reverse("person", args=(obj.slug,))

def item_link(self, bill):
# return the Councilmatic URL for the bill
return reverse("bill_detail", args=(bill.slug,))

def item_pubdate(self, bill):
return to_datetime(bill.last_action_date)

def description(self, obj):
return "Recent sponsored bills from " + obj.name + "."

def items(self, person):
sponsored_bills = [s.bill for s in person.primary_sponsorships][:10]
recent_sponsored_bills = sponsored_bills[: self.NUM_RECENT_BILLS]
return recent_sponsored_bills


class CommitteeDetailEventsFeed(Feed):
"""The CommitteeDetailEventsFeed provides an RSS feed for a given committee,
returning the most recent 20 events.
"""

title_template = "feeds/committee_events_item_title.html"
description_template = "feeds/committee_events_item_description.html"
feed_type = Rss201rev2Feed
NUM_RECENT_COMMITTEE_EVENTS = 20

def get_object(self, request, slug):
o = Organization.objects.get(slug=slug)
return o

def title(self, obj):
return settings.SITE_META["site_name"] + ": " + obj.name + ": Recent Events"

def link(self, obj):
# return the Councilmatic URL for the committee
return reverse("committee_detail", args=(obj.slug,))

def item_link(self, event):
# return the Councilmatic URL for the event
return reverse("event_detail", args=(event.slug,))

def item_pubdate(self, event):
return event.start_time

def description(self, obj):
return "Events for committee %s" % obj.name

def items(self, obj):
return obj.recent_events.all()[: self.NUM_RECENT_COMMITTEE_EVENTS]


class CommitteeDetailActionFeed(Feed):
"""The CommitteeDetailActionFeed provides an RSS feed for a given committee,
returning the most recent 20 actions on legislation.
"""

# instead of defining item_title() or item_description(), use templates
title_template = "feeds/committee_actions_item_title.html"
description_template = "feeds/committee_actions_item_description.html"
feed_type = Rss201rev2Feed
NUM_RECENT_COMMITTEE_ACTIONS = 20

def get_object(self, request, slug):
o = Organization.objects.get(slug=slug)
return o

def title(self, obj):
return settings.SITE_META["site_name"] + ": " + obj.name + ": Recent Actions"

def link(self, obj):
# return the Councilmatic URL for the committee
return reverse("committee_detail", args=(obj.slug,))

def item_link(self, action):
# return the Councilmatic URL for the bill
return reverse("bill_detail", args=(action.bill.slug,))

def item_pubdate(self, action):
return to_datetime(action.date_dt)

def description(self, obj):
return "Actions for committee %s" % obj.name

def items(self, obj):
return obj.recent_activity[: self.NUM_RECENT_COMMITTEE_ACTIONS]


class BillDetailActionFeed(Feed):
"""
Return the last 20 actions for a given bill.
"""

# instead of defining item_title() or item_description(), use templates
title_template = "feeds/bill_actions_item_title.html"
description_template = "feeds/bill_actions_item_description.html"
feed_type = Rss201rev2Feed
NUM_RECENT_BILL_ACTIONS = 20

def get_object(self, request, slug):
o = Bill.objects.get(slug=slug)
return o

def title(self, obj):
return (
settings.SITE_META["site_name"]
+ ": "
+ obj.friendly_name
+ ": Recent Actions"
)

def link(self, obj):
# return the Councilmatic URL for the committee
return reverse("bill_detail", args=(obj.slug,))

def item_link(self, action):
# Bill actions don't have their own pages, so just link to the Bill page (?)
return reverse("bill_detail", args=(action.bill.slug,))

def item_pubdate(self, action):
return to_datetime(action.date_dt)

def description(self, obj):
return "Actions for bill %s" % obj.friendly_name

def items(self, obj):
return obj.ordered_actions[: self.NUM_RECENT_BILL_ACTIONS]


class EventsFeed(Feed):
"""
Return the last 20 announced events as per, e.g.,
https://nyc.councilmatic.org/events/
"""

title_template = "feeds/events_item_title.html"
description_template = "feeds/events_item_description.html"
feed_type = Rss201rev2Feed
NUM_RECENT_EVENTS = 20

title = settings.CITY_COUNCIL_NAME + " " + "Recent Events"
link = reverse_lazy("events")
description = "Recently announced events."

def item_link(self, event):
# return the Councilmatic URL for the event
return reverse("event_detail", args=(event.slug,))

def item_pubdate(self, event):
return event.start_time

def description(self, obj):
return "Events"

def items(self, obj):
return Event.objects.all()[: self.NUM_RECENT_EVENTS]
71 changes: 71 additions & 0 deletions chicago/management/commands/import_shapes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import json

from django.core.management.base import BaseCommand, CommandError
from django.contrib.gis.geos import GEOSGeometry

from councilmatic_core import models


class Command(BaseCommand):
help = "Import boundary shapefiles for Post entities"

def add_arguments(self, parser):
parser.add_argument(
"geojson_file",
help=(
"The location of the GeoJSON file containing shapes for each "
"Division, relative to the project root. The file should be "
"formatted as a GeoJSON FeatureCollection where each Feature A) "
"corresponds to a distinct Division and B) has a 'division_id' "
"attribute in the 'properties' object. "
),
)

def handle(self, *args, **options):
self.stdout.write("Populating shapes for Posts...")
shapes_populated = 0

with open(options["geojson_file"]) as shapef:
shapes = json.load(shapef)

features = self._get_or_raise(
shapes, "features", 'Could not find the "features" array in the input file.'
)

for feature in features:
shape = self._get_or_raise(
feature, "geometry", 'Could not find a "geometry" key in the Feature.'
)
properties = self._get_or_raise(
feature,
"properties",
'Could not find a "properties" key in the Feature.',
)
division_id = self._get_or_raise(
properties,
"division_id",
'Could not find a "division_id" key in the Feature properties.',
)

models.Post.objects.filter(division_id=division_id).update(
shape=GEOSGeometry(json.dumps(shape))
)
shapes_populated += 1

self.stdout.write(
self.style.SUCCESS("Populated {} shapes".format(str(shapes_populated)))
)

def _get_or_raise(self, dct, key, msg):
"""
Check to see if 'dct' has a key corresponding to 'key', and raise an
error if it doesn't.
"""
format_prompt = (
"Is the input file formatted as a GeoJSON FeatureCollection "
'where each feature has a "division_id" property?'
)
if not dct.get(key):
raise CommandError(msg + " " + format_prompt)
else:
return dct[key]
Loading

0 comments on commit cb61909

Please sign in to comment.