Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5efb611
adding in donor wall pieces from tt app
matthewdylan Jun 11, 2025
aacd815
Add route for donor wall, pull in static header content
fillerwriter Jun 12, 2025
e67d56d
Merge pull request #1250 from texastribune/adding-donor-wall-query
fillerwriter Jun 18, 2025
abd5c6c
Merge pull request #1251 from texastribune/feature/donor-wall-route
fillerwriter Jun 18, 2025
efeaac2
Add styling from queso-ui to donate css, add stub data to donor wall
fillerwriter Jun 18, 2025
5df9025
Attempt to use salesforce client
fillerwriter Jun 19, 2025
de76379
remove intellij helper files from repo
fillerwriter Jun 19, 2025
b573ecc
Connected Salesforce data, updated query using SOQL to load last 365 …
fillerwriter Jun 19, 2025
ba9c8a3
Typo in SOQL. Need greater than vs less than
fillerwriter Jun 19, 2025
90e4385
Adjust soql to use LAST_N_DAYS feature
fillerwriter Jun 23, 2025
fea1257
Move url, adjust styles for donor page
fillerwriter Jun 25, 2025
d235b6e
reskinning for waco
matthewdylan Jul 3, 2025
1af675e
Grab correct donors for Waco
matthewdylan Jul 9, 2025
fd2d79d
Pushing pieces for donor wall
matthewdylan Oct 9, 2025
def3100
corporate sponsors
matthewdylan Oct 9, 2025
05584fa
removing CreatedDate
matthewdylan Oct 9, 2025
bde00ec
all the pdf parts
matthewdylan Oct 14, 2025
cf36756
update for new function naming
matthewdylan Oct 14, 2025
731be9a
add new share graphic
matthewdylan Oct 14, 2025
9beb0ac
new pdf version
matthewdylan Oct 15, 2025
8af1306
fix tabs and add new copy
matthewdylan Oct 16, 2025
e6de06d
All the styling fixes
matthewdylan Oct 17, 2025
1d552d5
turning off /members page during AWS outage
matthewdylan Oct 20, 2025
195d266
Revert "turning off /members page during AWS outage"
matthewdylan Oct 21, 2025
a4a6717
add img for black friday '25
matthewdylan Nov 24, 2025
efe991b
add giving Tuesday '25 gif
matthewdylan Nov 25, 2025
95f26d6
add underlining to links
matthewdylan Dec 15, 2025
2f011aa
adding eoy-25 social
matthewdylan Dec 23, 2025
66746cb
copy updates and upgrade to python 3.10
matthewdylan Jan 8, 2026
8a87226
remove legislative option
matthewdylan Jan 24, 2026
7490dbf
new blast price updates
matthewdylan Jan 25, 2026
556f99f
Removing tax tier and adding corporate/group rates copy
matthewdylan Jan 30, 2026
accb56b
replace Matthew Watkins with blast email
matthewdylan Jan 30, 2026
bcdf9a2
update DAF contact info
matthewdylan Jan 30, 2026
0a98fd4
Merge pull request #1260 from texastribune/update/blast-price-26
matthewdylan Feb 2, 2026
e5ab0e6
updating share gif
matthewdylan Feb 6, 2026
b8e6f6a
remove elections share img
matthewdylan Mar 16, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ server/templates/includes/base_icons.html
.mypy_cache/
.vscode/
cov.html

.idea
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ slack-sdk==3.12.0
pydantic==1.9.0
pytest-mock==3.10.0
pytest-recording==0.12.2
boto3==1.19.7
PyPDF2==3.0.1
reportlab==4.4.4
2 changes: 1 addition & 1 deletion runtime.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
python-3.9.2
python-3.10.0
147 changes: 147 additions & 0 deletions server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@

"""
import calendar
import io
import json
import locale
import logging
# import matplotlib
# matplotlib.use("PDF")
# import matplotlib.pyplot as plt
# from matplotlib.backends.backend_pgf import PdfPages
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
import os
import re
from datetime import datetime
from pprint import pformat
from PyPDF2 import PdfMerger, PdfReader

import stripe
from amazon_pay.client import AmazonPayClient
Expand Down Expand Up @@ -67,6 +75,7 @@
send_slack_message,
send_cancellation_notification,
name_splitter,
_push_to_s3,
)

ZONE = timezone(TIMEZONE)
Expand Down Expand Up @@ -227,6 +236,8 @@
)
stripe.api_key = app.config["STRIPE_KEYS"]["secret_key"]

# matplotlib.use('Agg')

celery = make_celery(app)


Expand Down Expand Up @@ -882,6 +893,31 @@ def daf():
bundles=bundles,
)

@app.route("/members")
def donor_wall():
if NEWSROOM["name"] == "waco":
bundles = get_bundles("waco")
sorted_donors = Account.list_by_giving_this_year()
return render_template("waco-donor-wall.html", bundles=bundles, sortedDonors=sorted_donors, newsroom=NEWSROOM)
elif NEWSROOM['name'] == "texas":
bundles = get_bundles("donate")
sorted_donors = Account.list_by_giving_this_year()
return render_template("donor-wall.html", bundles=bundles, sortedDonors=sorted_donors, newsroom=NEWSROOM)
else:
bundles = get_bundles(NEWSROOM["name"] if NEWSROOM["name"] != "texas" else "old")
message = "Something went wrong!"
return render_template("error.html", message=message, bundles=bundles, newsroom=NEWSROOM), 404

@app.route("/corporate-sponsors")
def corporate_wall():
if NEWSROOM['name'] == "texas":
bundles = get_bundles("donate")
return render_template("corporate-wall.html", bundles=bundles, newsroom=NEWSROOM)
else:
bundles = get_bundles(NEWSROOM["name"] if NEWSROOM["name"] != "texas" else "old")
message = "Something went wrong!"
return render_template("error.html", message=message, bundles=bundles, newsroom=NEWSROOM), 404

@app.route("/error")
def error():
bundles = get_bundles(NEWSROOM["name"] if NEWSROOM["name"] != "texas" else "old")
Expand Down Expand Up @@ -1173,6 +1209,12 @@ def authorization_notification(payload):
send_multiple_account_warning(contact)


@celery.task(name="app.push_donor_list")
def push_donor_list():
donor_list = Account.list_by_giving_this_year()
_push_to_s3(filename='donors-waco-365.json', contents=donor_list)


def get_zip(details=None):

try:
Expand Down Expand Up @@ -1264,6 +1306,111 @@ def sync_stripe_event(event):
return stripe_event


def build_yearly_pdf():
giving_list = Account.list_by_giving_all_time()

current_page = 1
line_count = 0
max_lines = 47

donor_entries = []
pdfs = []
for amount, donors in giving_list.items():
donor_entries.append(f"{amount}:")
line_count += 1
for donor in donors:
donor_entries.append(f". {donor['attribution']}")
line_count += 1

if line_count >= max_lines:
pdfs.append(print_the_report(f"donors{current_page}.pdf", donor_entries, current_page))

# Reset for next page
donor_entries = []
line_count = 0
current_page += 1

donor_entries.append("")
line_count += 1

if donor_entries:
pdfs.append(print_the_report(f"donors{current_page}.pdf", donor_entries, current_page))

merge_em_up(pdfs=pdfs)


# def print_the_page(pdf, donor_entries, current_page):
# plt.rcParams['text.usetex'] = False # Add this before your plotting code

# fig = plt.figure(figsize=(8.5, 11), dpi=100)
# ax = fig.add_subplot(111)
# ax.axis('off')

# safe_entries = [
# entry.replace('&', r'&')
# .replace('<', r'&lt;')
# .replace('>', r'&gt;')
# for entry in donor_entries
# ]

# ax.text(
# 0.05, 0.95,
# '\n'.join(safe_entries),
# fontsize=10,
# verticalalignment='top',
# fontfamily='sans-serif'
# )

# # Add page number
# ax.text(
# 0.5, 0.02,
# f'Page {current_page}',
# horizontalalignment='center',
# fontsize=8,
# fontfamily='sans-serif'
# )

# plt.tight_layout()

# pdf.savefig(fig, bbox_inches="tight")
# plt.close(fig)


def print_the_report(pdf_path, donor_entries, current_page):
c = canvas.Canvas(pdf_path, pagesize=letter)
width, height = letter

# Write donor entries
c.setFont("Helvetica", 10)
y_position = height - 50 # Start near the top
for entry in donor_entries:
c.drawString(50, y_position, entry)
y_position -= 15 # Move down for next entry

# Add page number
c.setFont("Helvetica", 8)
c.drawCentredString(width - 50, 20, f'Page {current_page}')

c.showPage()
c.save()

return pdf_path


def merge_em_up(pdfs=[]):
merger = PdfMerger()

merger.append('letterhead.pdf')

for pdf in pdfs:
merger.append(pdf)

with open('TT_Giving.pdf', 'wb') as output_file:
merger.write(output_file)

merger.close()


# this is just a temp func version of a piece of add_donation we're
# reusing for quarantined records during the move to stripe subscriptions
def get_or_create_contact(form=None):
Expand Down
2 changes: 2 additions & 0 deletions server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ def bool_env(val):
AMAZON_MERCHANT_ID = os.getenv("AMAZON_MERCHANT_ID", "")
AMAZON_SANDBOX = bool_env("AMAZON_SANDBOX")
AMAZON_CAMPAIGN_ID = os.getenv("AMAZON_CAMPAIGN_ID", "")
AMAZON_S3_BUCKET = os.getenv("AMAZON_S3_BUCKET", "membership.texastribune.org")

#######
# Tasks
#
Expand Down
117 changes: 114 additions & 3 deletions server/npsp.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import boto3
import csv
import json
import logging
import os
from collections import defaultdict, OrderedDict
from datetime import datetime
from decimal import Decimal
from decimal import Decimal, ROUND_HALF_UP
from io import StringIO
from re import match

Expand Down Expand Up @@ -412,7 +414,7 @@ def list(
WHERE Stripe_Customer_ID__c = '{stripe_customer_id}'
AND StageName = '{stage_name}'
"""

order_by = f"""ORDER BY Expected_Giving_Date__c ASC""" if asc_order else ""

query = f"""
Expand Down Expand Up @@ -612,7 +614,7 @@ def __init__(self, id=None, contact=None, account=None, date=None, sf_connection
date = datetime.now(tz=ZONE)
else:
date = datetime.fromtimestamp(date)

date_formatted = date.strftime("%Y-%m-%d")

if contact is not None:
Expand Down Expand Up @@ -988,6 +990,115 @@ def get(

return account

@classmethod
def list_by_giving_this_year(
cls, sf_connection=None
):
"""
Adapted from donor wall pieces from the TT app.
Puts accounts into giving bins for easy and sorted display on a donor wall.
TODO: Work with our Salesforce contractor to add newsroom specific SF fields for last 365 days of giving.
"""
sf = SalesforceConnection() if sf_connection is None else sf_connection

query = """
SELECT
Name,
CreatedDate,
Text_For_Donor_Wall__c,
Total_Donor_Wall_This_Year__c
FROM Account
WHERE RecordTypeId IN ('01216000001IhHL', '01216000001IhHMAA0')
AND Total_Donor_Wall_This_Year__c > 0
"""

# query = f"""
# SELECT Opportunity.Account.Name, Opportunity.Account.Id, SUM(Opportunity.Amount) TotalGiving, MIN(Opportunity.CloseDate) CreatedDate
# FROM Opportunity
# WHERE Opportunity.StageName = 'Closed Won'
# AND Opportunity.Newsroom__c = 'Waco Bridge'
# AND Opportunity.CloseDate = THIS_YEAR
# GROUP BY Opportunity.Account.Name, Opportunity.Account.Id
# """

donors = sf.query(query)
# donor_ids = []
# for record in donors:
# donor_ids.append(record['Id'])

# get_text_query = f"""
# SELECT Id, Text_For_Donor_Wall__c
# FROM Account
# WHERE Id IN {tuple(donor_ids)}
# """
# donor_texts = sf.query(get_text_query)
print(len(donors))
return cls.bucket_donors(donors, 'Total_Donor_Wall_This_Year__c')


@classmethod
def list_by_giving_all_time(
cls, sf_connection=None
):
"""
Adapted from donor wall pieces from the TT app.
Puts accounts into giving bins for easy and sorted display on a donor wall.
TODO: Work with our Salesforce contractor to add newsroom specific SF fields for last 365 days of giving.
"""
sf = SalesforceConnection() if sf_connection is None else sf_connection

query = """
SELECT
Name,
CreatedDate,
Text_For_Donor_Wall__c,
Total_Amt_For_Donor_Wall_All_Time__c
FROM Account
WHERE RecordTypeId IN ('01216000001IhHL', '01216000001IhHMAA0')
AND Total_Amt_For_Donor_Wall_All_Time__c > 0
"""

donors = sf.query(query)

print(len(donors))
return cls.bucket_donors(donors, 'Total_Amt_For_Donor_Wall_All_Time__c')


def bucket_donors(donors, amount_field):
results = defaultdict(list)
less_than_10 = []
for record in donors:
# donor_text = list(filter(lambda x:x["Id"]==record["Id"], donor_texts))
attribution = record['Text_For_Donor_Wall__c']
attributions = {'sort_by': record['Name'],
'attribution': attribution, 'CreatedDate': record['CreatedDate']}
amount = Decimal(record[amount_field])
amount = amount.quantize(Decimal('1'), rounding=ROUND_HALF_UP)
if amount < 10:
less_than_10.append(attributions)
else:
results[amount].append(attributions)

sorted_results = OrderedDict()

# sort by decreasing amount of donation
for k, v in sorted(results.items(), key=lambda x: x, reverse=True):
items = sorted(v, key=lambda x: x['sort_by'])
sorted_results['${:0,.0f}'.format(k)] = [
{'attribution': x['attribution'], 'created': x['CreatedDate']}
for x in items]

# hacky, but tack this on the end so it'll show last:
less_than_10 = sorted(less_than_10, key=lambda x: x['sort_by'])
less_than_10 = [
{'attribution': x['attribution'], 'created': x['CreatedDate']}
for x in less_than_10]

sorted_results['Less Than $10'] = less_than_10

return sorted_results


def save(self):
self.sf.save(self)

Expand Down
Binary file added server/static/img/social-bf25.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added server/static/img/social-eoy25.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added server/static/img/social-gt25.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added server/static/img/social-primaries26.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified server/static/img/social.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion server/static/js/src/connected-elements/FormBuckets.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="grid_row grid_wrap--m form-buckets">
<div class="grid_row_centered grid_wrap--m form-buckets">
<div
v-for="bucket in buckets"
:key="bucket.id"
Expand Down
Loading