Skip to content

Commit

Permalink
Merge branch 'master' into frontend-release
Browse files Browse the repository at this point in the history
  • Loading branch information
ndepaola committed Jan 28, 2024
2 parents 0571f4a + f2ed90e commit 51b170c
Show file tree
Hide file tree
Showing 27 changed files with 375 additions and 79 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ repos:
"django-widget-tweaks~=1.4.12",
"google-api-python-client~=2.86",
"jsonschema~=4.20.0",
"Levenshtein~=0.21.1",
"Levenshtein~=0.23.0",
"oauth2client~=4.1",
"Markdown~=3.4",
"psycopg2-binary~=2.9.6",
Expand Down
96 changes: 72 additions & 24 deletions MPCAutofill/cardpicker/integrations/patreon.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,24 @@


class Campaign(TypedDict):
# Campaign data scheme
"""Patreon 'Campaign' data schema."""

id: str
about: str


class Supporter(TypedDict):
# Patron data scheme
"""Patron 'Supporter' data schema."""

name: str
tier: str
date: str
usd: int


class SupporterTier(TypedDict):
# Patron tiers data scheme
"""Patron 'Tier' data schema."""

title: str
description: str
usd: int
Expand Down Expand Up @@ -59,6 +63,9 @@ def get_patreon_campaign_details() -> tuple[Optional[Campaign], Optional[dict[st
# Properly format campaign tiers
tiers: dict[str, SupporterTier] = {}
for tier in res["included"]:
# Ignore free tier
if tier["attributes"]["amount_cents"] < 1:
continue
# Build dictionary of tiers to reference by ID
tiers[tier["id"]] = {
"title": tier["attributes"]["title"],
Expand All @@ -71,38 +78,79 @@ def get_patreon_campaign_details() -> tuple[Optional[Campaign], Optional[dict[st
return campaign, tiers


def get_patrons(campaign_id: str, campaign_tiers: dict[str, SupporterTier]) -> Optional[list[Supporter]]:
def get_patrons(
campaign_id: str, campaign_tiers: dict[str, SupporterTier], page: Optional[str] = None
) -> Optional[list[Supporter]]:
"""
Get our patreon contributors.
:note: https://docs.patreon.com/#get-api-oauth2-v2-campaigns-campaign_id-members
:return: List of dictionaries containing patreon contributor info.
"""

if not PATREON_URL:
return None

try:
members = requests.get(
# https://docs.patreon.com/#get-api-oauth2-v2-campaigns-campaign_id-members
url=f"https://www.patreon.com/api/oauth2/v2/campaigns/{campaign_id}/members",
params={
"include": "currently_entitled_tiers",
"fields[member]": ",".join(
["full_name", "campaign_lifetime_support_cents", "pledge_relationship_start", "patron_status"]
),
},
headers=patreon_header,
).json()["data"]
# Use page if provided, otherwise build a complete query
res = (
requests.get(url=page, headers=patreon_header).json()
if page
else requests.get(
url=f"https://www.patreon.com/api/oauth2/v2/campaigns/{campaign_id}/members",
params={
"include": "currently_entitled_tiers",
"fields[member]": ",".join(
["full_name", "campaign_lifetime_support_cents", "pledge_relationship_start", "patron_status"]
),
},
headers=patreon_header,
).json()
)

# Return formatted list of patrons
return [
{
"name": mem["attributes"]["full_name"],
"tier": campaign_tiers[mem["relationships"]["currently_entitled_tiers"]["data"][0]["id"]]["title"],
"date": mem["attributes"]["pledge_relationship_start"][:10],
}
for mem in members
if len(mem["relationships"]["currently_entitled_tiers"]["data"]) > 0
]
results: list[Supporter] = []
for mem in res.get("data", []):

# Skip non-active members
mem_details = mem.get("attributes", {})
if mem_details.get("patron_status") != "active_patron":
continue

# Pull subscribed tiers for this member
mem_tiers = [
campaign_tiers[t["id"]]
for t in mem.get("relationships", {}).get("currently_entitled_tiers", {}).get("data", [])
if t.get("id") in campaign_tiers
]

# Skip members with no subscribed tiers
if not mem_tiers:
continue

# Use member's highest subscribed tier
current_tier = sorted(mem_tiers, key=lambda item: item["usd"])[0]

# Add member to results
results.append(
Supporter(
name=mem_details.get("full_name", "Unknown"),
tier=current_tier.get("title", "Unknown Tier"),
date=mem_details.get("pledge_relationship_start", "2024-01-01")[:10],
usd=current_tier.get("usd", 5),
)
)

# Check for additional page results
next_page = res.get("links", {}).get("next")
if next_page:
results.extend(get_patrons(campaign_id=campaign_id, campaign_tiers=campaign_tiers, page=next_page) or [])

# Return sorted results at top-level
if page:
return results
return sorted(results, key=lambda item: item["usd"], reverse=True)

# Unable to retrieve patrons
except KeyError:
print("Warning: Cannot locate Patreon campaign. Check Patreon access token!")
return None
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Generated by Django 4.2.5 on 2024-01-28 00:12

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [("cardpicker", "0037_tag_parent")]
operations = [
migrations.AddField(model_name="tag", name="is_enabled_by_default", field=models.BooleanField(default=True)),
]
2 changes: 2 additions & 0 deletions MPCAutofill/cardpicker/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ class Tag(models.Model):
name = models.CharField(unique=True)
# null=True is just for admin panel
aliases = ArrayField(models.CharField(max_length=200), default=list, blank=True)
is_enabled_by_default = models.BooleanField(default=True)
parent = models.ForeignKey(to="Tag", null=True, blank=True, on_delete=models.SET_NULL)

def __str__(self) -> str:
Expand All @@ -281,6 +282,7 @@ def to_dict(self) -> dict[str, Any]:
return {
"name": self.name,
"aliases": self.aliases,
"is_enabled_by_default": self.is_enabled_by_default,
"parent": (self.parent.name if self.parent else None),
# recursively serialise each child tag
"children": [x.to_dict() for x in self.tag_set.order_by("name").all()] if self.pk is not None else [],
Expand Down
35 changes: 28 additions & 7 deletions MPCAutofill/cardpicker/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,42 +581,63 @@ class TestGetTags:
def test_get_no_data_tags(self, client, django_settings):
response = client.get(reverse(views.get_tags))
assert response.json()["tags"] == [
{"name": "NSFW", "parent": None, "aliases": [], "children": []},
{"name": "NSFW", "parent": None, "aliases": [], "children": [], "is_enabled_by_default": True},
]

def test_get_one_data_tag(self, client, django_settings, tag_in_data):
response = client.get(reverse(views.get_tags))
assert response.json()["tags"] == [
{"name": "NSFW", "parent": None, "aliases": [], "children": []},
{"name": "Tag in Data", "parent": None, "aliases": ["TaginData"], "children": []},
{"name": "NSFW", "parent": None, "aliases": [], "children": [], "is_enabled_by_default": True},
{
"name": "Tag in Data",
"parent": None,
"aliases": ["TaginData"],
"children": [],
"is_enabled_by_default": True,
},
]

def test_get_two_data_tags(self, client, django_settings, tag_in_data, another_tag_in_data):
response = client.get(reverse(views.get_tags))
assert response.json()["tags"] == [
{"name": "Another Tag in Data", "parent": None, "aliases": ["AnotherTaginData"], "children": []},
{"name": "NSFW", "parent": None, "aliases": [], "children": []},
{"name": "Tag in Data", "parent": None, "aliases": ["TaginData"], "children": []},
{
"name": "Another Tag in Data",
"parent": None,
"aliases": ["AnotherTaginData"],
"children": [],
"is_enabled_by_default": True,
},
{"name": "NSFW", "parent": None, "aliases": [], "children": [], "is_enabled_by_default": True},
{
"name": "Tag in Data",
"parent": None,
"aliases": ["TaginData"],
"children": [],
"is_enabled_by_default": True,
},
]

def test_get_hierarchical_tags(self, client, django_settings, grandchild_tag):
response = client.get(reverse(views.get_tags))
assert response.json()["tags"] == [
{"name": "NSFW", "parent": None, "aliases": [], "children": []},
{"name": "NSFW", "parent": None, "aliases": [], "is_enabled_by_default": True, "children": []},
{
"name": "Tag in Data",
"parent": None,
"aliases": ["TaginData"],
"is_enabled_by_default": True,
"children": [
{
"name": "Child Tag",
"parent": "Tag in Data",
"aliases": ["ChildTag"],
"is_enabled_by_default": True,
"children": [
{
"name": "Grandchild Tag",
"parent": "Child Tag",
"aliases": ["GrandchildTag"],
"is_enabled_by_default": True,
"children": [],
}
],
Expand Down
2 changes: 1 addition & 1 deletion MPCAutofill/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jsonschema~=4.20.0
google-api-python-client~=2.95.0
google-auth-httplib2~=0.1.0
google-auth-oauthlib~=1.0.0
Levenshtein~=0.21.1
Levenshtein~=0.23.0
Markdown~=3.4.4
oauth2client~=4.1.3
pre-commit
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/common/test-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -336,12 +336,16 @@ export async function openCardSlotGridSelector(
);
}

export async function selectSlot(slot: number, face: Faces, count: number = 1) {
export async function selectSlot(
slot: number,
face: Faces,
clickType: "double" | "shift" | null = null
) {
const cardElement = screen.getByTestId(`${face}-slot${slot - 1}`);
fireEvent.click(
within(cardElement).getByLabelText(`select-${face}${slot - 1}`)!
.children[0],
{ detail: count }
{ detail: clickType === "double" ? 2 : 1, shiftKey: clickType === "shift" }
);
await waitFor(() =>
expect(
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export type SlotProjectMembers = {
export type Project = {
members: Array<SlotProjectMembers>;
cardback: string | null;
mostRecentlySelectedSlot: Slot | null;
};

export interface DFCPairs {
Expand Down Expand Up @@ -231,7 +232,8 @@ export interface NewCardsFirstPages {
[sourceKey: string]: NewCardsFirstPage;
}

export type Slots = Array<[Faces, number]>;
export type Slot = [Faces, number];
export type Slots = Array<Slot>;

export type Modals =
| "cardDetailedView"
Expand Down
Loading

0 comments on commit 51b170c

Please sign in to comment.