Skip to content

Commit

Permalink
Merge pull request #5090 from fecgov/feature/create_candidate_total_o…
Browse files Browse the repository at this point in the history
…ffices_endpoint

Add endpoint /candidates/totals/aggregate/
  • Loading branch information
hcaofec authored Apr 4, 2022
2 parents 9e0e60f + 1250e86 commit 75007c6
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 8 deletions.
14 changes: 14 additions & 0 deletions webservices/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,7 @@ def make_seek_args(field=fields.Int, description=None):
'max_transaction_data_complete_date': fields.Date(description=docs.MAX_TRANSACTION_DATA_COMPLETE_DATE),
}


totals_by_office = {
'election_year': fields.List(fields.Int, description=docs.RECORD_CYCLE),
'office': fields.Str(validate=validate.OneOf(['', 'H', 'S', 'P']), description=docs.OFFICE),
Expand All @@ -1013,6 +1014,19 @@ def make_seek_args(field=fields.Int, description=None):
'election_full': election_full,
}

candidate_total_aggregate = {
'election_year': fields.List(fields.Int, description=docs.RECORD_CYCLE),
'office': fields.Str(validate=validate.OneOf(['', 'H', 'S', 'P']), description=docs.OFFICE),
'is_active_candidate': fields.Bool(description=docs.ACTIVE_CANDIDATE),
'election_full': election_full,
'min_election_cycle': fields.Int(description=docs.CYCLE),
'max_election_cycle': fields.Int(description=docs.CYCLE),
'state': fields.List(IStr, description=docs.STATE),
'aggregate_by': fields.Str(validate=validate.OneOf(
['office', 'office-state', 'office-state-district', 'office-party']
)),
}

totals_by_candidate_other_costs_EC = {

'cycle': fields.List(fields.Int, description=docs.RECORD_CYCLE),
Expand Down
25 changes: 17 additions & 8 deletions webservices/common/models/candidates.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,26 +178,35 @@ class CandidateHistoryWithFuture(BaseCandidate):

class CandidateTotal(db.Model):
__tablename__ = "ofec_candidate_totals_mv"
candidate_id = db.Column(db.String, index=True, primary_key=True)
candidate_id = db.Column(db.String, index=True, primary_key=True, doc=docs.CANDIDATE_ID)
election_year = db.Column(
db.Integer, index=True, primary_key=True, autoincrement=True
db.Integer, index=True, primary_key=True, autoincrement=True, doc=docs.ELECTION_YEAR
)
cycle = db.Column(db.Integer, index=True, primary_key=True)
cycle = db.Column(db.Integer, index=True, primary_key=True, doc=docs.CYCLE)
is_election = db.Column(db.Boolean, index=True, primary_key=True)
receipts = db.Column(db.Numeric(30, 2), index=True)
disbursements = db.Column(db.Numeric(30, 2), index=True)
cash_on_hand_end_period = db.Column(db.Numeric(30, 2))
debts_owed_by_committee = db.Column(db.Numeric(30, 2))
cash_on_hand_end_period = db.Column(db.Numeric(30, 2), doc=docs.CASH_ON_HAND_END_PERIOD)
debts_owed_by_committee = db.Column(db.Numeric(30, 2), doc=docs.DEBTS_OWED_BY_COMMITTEE)
coverage_start_date = db.Column(db.Date, doc=docs.COVERAGE_START_DATE)
coverage_end_date = db.Column(db.Date, doc=docs.COVERAGE_END_DATE)
federal_funds_flag = db.Column(db.Boolean, index=True, doc=docs.FEDERAL_FUNDS_FLAG)
has_raised_funds = db.Column(db.Boolean, index=True, doc=docs.HAS_RAISED_FUNDS)
party = db.Column(db.String(3), index=True, doc=docs.PARTY)
office = db.Column(db.String(1), index=True, doc=docs.OFFICE)
candidate_inactive = db.Column(db.Boolean, doc=docs.CANDIDATE_INACTIVE)
individual_itemized_contributions = db.Column(db.Numeric(30, 2), index=True)
transfers_from_other_authorized_committee = db.Column(db.Numeric(30, 2), index=True)
other_political_committee_contributions = db.Column(db.Numeric(30, 2), index=True)
individual_itemized_contributions = db.Column(
db.Numeric(30, 2), index=True, doc=docs.INDIVIDUAL_ITEMIZED_CONTRIBUTIONS
)
transfers_from_other_authorized_committee = db.Column(
db.Numeric(30, 2), index=True, doc=docs.TRANSFERS_FROM_OTHER_AUTHORIZED_COMMITTEE
)
other_political_committee_contributions = db.Column(
db.Numeric(30, 2), index=True, doc=docs.OTHER_POLITICAL_COMMITTEE_CONTRIBUTIONS
)
state = db.Column(db.String(2), index=True, doc=docs.STATE)
district = db.Column(db.String(2), index=True, doc=docs.DISTRICT)
district_number = db.Column(db.Integer, index=True, doc=docs.DISTRICT)


class CandidateElection(db.Model):
Expand Down
162 changes: 162 additions & 0 deletions webservices/resources/candidate_aggregates.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,3 +382,165 @@ def build_query(self, **kwargs):
total.election_year,
).order_by(sa.desc(total.election_year))
return query


# endpoint: /candidates/totals/aggregate/
@doc(
tags=["candidate"], description=docs.TOTAL_BY_OFFICE_TAG,
)
class CandidateTotalAggregateView(ApiResource):
schema = schemas.CandidateTotalAggregateSchema
page_schema = schemas.CandidateTotalAggregatePageSchema

@property
def args(self):
return utils.extend(
args.paging,
args.candidate_total_aggregate,
args.make_sort_args(),
)

def build_query(self, **kwargs):
total = models.CandidateTotal
query = db.session.query(
total.election_year.label("election_year"),
sa.func.sum(total.receipts).label(
"total_receipts"),
sa.func.sum(total.disbursements).label(
"total_disbursements"),
sa.func.sum(total.individual_itemized_contributions).label(
"total_individual_itemized_contributions"),
sa.func.sum(total.transfers_from_other_authorized_committee).label(
"total_transfers_from_other_authorized_committee"),
sa.func.sum(total.other_political_committee_contributions).label(
"total_other_political_committee_contributions"),
sa.func.sum(total.cash_on_hand_end_period).label(
"total_cash_on_hand_end_period"),
)

# remove election_year=null result
query = query.filter(~total.election_year.is_(None))

if kwargs.get("election_year"):
query = query.filter(
total.election_year.in_(kwargs["election_year"])
)

# is_active_candidate=true //only show active candidate totals
# is_active_candidate=false //only show inactive candidate totals
# is_active_candidate=not specified //show full totals of both active and inactive
if kwargs.get("is_active_candidate"):
query = query.filter(total.candidate_inactive.is_(False))

elif "is_active_candidate" in kwargs and not kwargs.get("is_active_candidate"):
query = query.filter(total.candidate_inactive.is_(True))

# if not pass election_full variable, election_full default set `true` in args.py
# if pass election_full = true, election_year is candidate election year
# if pass election_full = flase, election_year is finance two-year period
if kwargs.get("election_full"):
query = query.filter(total.is_election.is_(kwargs["election_full"]))

if kwargs.get("min_election_cycle"):
query = query.filter(
total.election_year >= kwargs["min_election_cycle"]
)

if kwargs.get("max_election_cycle"):
query = query.filter(
total.election_year <= kwargs["max_election_cycle"]
)

# aggregate by office
if kwargs.get("aggregate_by") and ("office" == kwargs.get("aggregate_by")):
if kwargs.get("office"):
query = query.filter(total.office == kwargs["office"])

query = query.add_column(
total.office.label("office")
)
query = query.group_by(
total.election_year, total.office,
).order_by(sa.desc(total.election_year), sa.asc(total.office))

# aggregate by office, by state.
elif kwargs.get("aggregate_by") and "office-state" == kwargs.get("aggregate_by"):
if kwargs.get("office"):
query = query.filter(total.office == kwargs["office"])

if kwargs.get("state"):
query = query.filter(total.state.in_(kwargs['state']))

query = query.add_column(
total.office.label("office")
)
query = query.add_column(
total.state.label("state")
)
query = query.group_by(
total.election_year, total.office, total.state
).order_by(sa.desc(total.election_year), sa.asc(total.office), sa.asc(total.state))

# aggregate by office, by state, by district
elif kwargs.get("aggregate_by") and "office-state-district" == kwargs.get("aggregate_by"):
if kwargs.get("office"):
query = query.filter(total.office == kwargs["office"])

if kwargs.get("state"):
query = query.filter(total.state.in_(kwargs['state']))

query = query.add_column(
total.office.label("office")
)
query = query.add_column(
total.state.label("state")
)
query = query.add_column(
total.district.label("district")
)
query = query.group_by(
total.election_year, total.office, total.state, total.district,
).order_by(sa.desc(total.election_year), sa.asc(total.office), sa.asc(total.state), sa.asc(total.district))

# aggregate by office, by party
elif kwargs.get("aggregate_by") and "office-party" == kwargs.get("aggregate_by"):
if kwargs.get("office"):
query = query.filter(total.office == kwargs["office"])

if kwargs.get("party"):
query = query.filter(total.party == kwargs["party"])

query = query.add_column(
total.office.label("office")
)
query = query.add_column(
sa.case(
[
(total.party == "DFL", "DEM"),
(total.party == "DEM", "DEM"),
(total.party == "REP", "REP"),
],
else_="Other",
).label("party")
)

query = query.group_by(
total.election_year,
total.office,
sa.case(
[
(total.party == "DFL", "DEM"),
(total.party == "DEM", "DEM"),
(total.party == "REP", "REP"),
],
else_="Other",
),
).order_by(sa.desc(total.election_year), sa.asc(total.office))

# without `aggregate_by`, return group by election_year
else:
query = query.group_by(
total.election_year,
).order_by(sa.desc(total.election_year))

return query
4 changes: 4 additions & 0 deletions webservices/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,9 @@ def forbidden(exception):
api.add_resource(candidate_aggregates.AggregateByOfficeView, '/candidates/totals/by_office/')
api.add_resource(candidate_aggregates.AggregateByOfficeByPartyView, '/candidates/totals/by_office/by_party/')

api.add_resource(candidate_aggregates.CandidateTotalAggregateView, '/candidates/totals/aggregate/')


api.add_resource(
aggregates.CommunicationCostByCandidateView,
'/communication_costs/by_candidate/',
Expand Down Expand Up @@ -529,6 +532,7 @@ def forbidden(exception):
apidoc.register(legal.UniversalSearch, blueprint='v1')
apidoc.register(candidate_aggregates.AggregateByOfficeView, blueprint='v1')
apidoc.register(candidate_aggregates.AggregateByOfficeByPartyView, blueprint='v1')
apidoc.register(candidate_aggregates.CandidateTotalAggregateView, blueprint='v1')
apidoc.register(spending_by_others.ECTotalsByCandidateView, blueprint='v1')
apidoc.register(spending_by_others.IETotalsByCandidateView, blueprint='v1')
apidoc.register(spending_by_others.CCTotalsByCandidateView, blueprint='v1')
Expand Down
18 changes: 18 additions & 0 deletions webservices/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -1342,6 +1342,24 @@ class TotalByOfficeByPartySchema(ma.Schema):
augment_schemas(TotalByOfficeByPartySchema)


class CandidateTotalAggregateSchema(ma.Schema):
election_year = ma.fields.Int()
office = ma.fields.Str()
party = ma.fields.Str()
total_receipts = ma.fields.Decimal(places=2)
total_disbursements = ma.fields.Decimal(places=2)
total_individual_itemized_contributions = ma.fields.Decimal(places=2)
total_transfers_from_other_authorized_committee = ma.fields.Decimal(places=2)
total_other_political_committee_contributions = ma.fields.Decimal(places=2)
total_cash_on_hand_end_period = ma.fields.Decimal(places=2)
state = ma.fields.Str()
district = ma.fields.Str()
district_number = ma.fields.Int()


augment_schemas(CandidateTotalAggregateSchema)


class ECTotalsByCandidateSchema(ma.Schema):
candidate_id = ma.fields.Str()
cycle = ma.fields.Int()
Expand Down

0 comments on commit 75007c6

Please sign in to comment.