Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add endpoint /candidates/totals/aggregates/ #5090

Merged
merged 1 commit into from
Apr 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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