Skip to content

Commit 02b8aed

Browse files
Merge branch 'web/bugfix/language-picker-mobile' of https://github.com/Couchers-org/couchers into web/bugfix/language-picker-mobile
2 parents 3202d81 + 86f0bc8 commit 02b8aed

249 files changed

Lines changed: 7331 additions & 7121 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/autoreviewers.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Defines PR reviewers based on modified file paths.
2+
# This is an input to the dorny/paths-filter GitHub action,
3+
# configured to match rules if EVERY glob applies (to support exclusions).
4+
# Filter names are a label plus a comma-delimited people/teams to assign.
5+
backend Couchers-org/backend:
6+
- 'app/backend/**'
7+
- '!app/backend/**/locales/*.json'
8+
media Couchers-org/backend:
9+
- 'app/media/**'
10+
web Couchers-org/web:
11+
- 'app/web/**'
12+
- '!app/web/**/locales/*.json'
13+
mobile Couchers-org/mobile:
14+
- 'app/mobile/**'
15+
- '!app/mobile/**/locales/*.json'
16+
locales tristanlabelle:
17+
- 'app/**/locales/*.json'
18+
i18n tristanlabelle:
19+
- 'app/**/i18n/**'
20+
docs aapeliv,nabramow:
21+
- 'docs/**'

.github/pull_request_template.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Fill applicable checklists below, or remove those that don't apply.
2929
- [x] Maintainers can merge this PR for me
3030

3131
<!--
32-
Remember to request review from couchers-org/web, couchers-org/backend or an individual.
32+
Create the code review as a draft if still iterating or validating via CI.
33+
Once published, reviewers will be added based on changes, but feel free to add more.
3334
Once your code is approved, you can merge it if you have write access.
3435
--->
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# [WIP] Currently fails to add team reviewers
2+
# Adds reviewers upon publishing a PR based on changed files.
3+
# Works around limitations of GitHub's CODEOWNERS, which can only match one rule,
4+
# whereas our changes can straddle frontend and backend, for example.
5+
name: Add reviewers
6+
7+
# on:
8+
# pull_request:
9+
# types: [opened, ready_for_review, reopened]
10+
11+
concurrency:
12+
group: ${{ github.workflow }}-${{ github.head_ref }}
13+
cancel-in-progress: true
14+
15+
permissions:
16+
contents: read
17+
pull-requests: write
18+
19+
jobs:
20+
add-reviewers:
21+
name: Add reviewers
22+
runs-on: ubuntu-latest
23+
if: "!github.event.pull_request.draft"
24+
steps:
25+
- uses: actions/checkout@v4
26+
with:
27+
sparse-checkout: .github/autoreviewers.yml
28+
29+
- name: Match rules from autoreviewers.yml
30+
uses: dorny/paths-filter@v4
31+
id: filter
32+
with:
33+
filters: .github/autoreviewers.yml
34+
# Require every glob to match for a rule to match,
35+
# otherwise we cannot express exclusions.
36+
# e.g. 'app/web/**' + '!**/en.json' needs to be AND, not OR
37+
predicate-quantifier: every
38+
39+
- name: Collect reviewers from matched rules
40+
id: collect-reviewers
41+
env:
42+
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
43+
# The "changes" output is a JSON array of filter names.
44+
FILTER_CHANGES: ${{ steps.filter.outputs.changes }}
45+
run: |
46+
# Filter keys are "<name> <reviewer1,reviewer2,...>"; process matched filters.
47+
REVIEWERS=()
48+
while IFS= read -r entry; do
49+
[[ -n "$entry" ]] || continue
50+
RULE_NAME="${entry%% *}"
51+
REVIEWERS_STR="${entry#* }"
52+
53+
echo "Matched rule \"$RULE_NAME\" with reviewers: $REVIEWERS_STR"
54+
IFS=',' read -ra reviewers <<< "$REVIEWERS_STR"
55+
for reviewer in "${reviewers[@]}"; do
56+
[[ -n "$reviewer" ]] || continue
57+
# The PR author cannot be a reviewer on their own PR
58+
[[ "$reviewer" == "$PR_AUTHOR" ]] && continue
59+
# Avoid adding the same reviewer twice
60+
[[ " ${REVIEWERS[*]} " == *" $reviewer "* ]] && continue
61+
REVIEWERS+=("$reviewer")
62+
done
63+
done < <(echo "$FILTER_CHANGES" | jq -r '.[]')
64+
65+
if [[ ${#REVIEWERS[@]} -gt 0 ]]; then
66+
echo "Collected reviewers: $(IFS=','; echo "${REVIEWERS[*]}")"
67+
else
68+
echo "No reviewers collected"
69+
fi
70+
71+
echo "reviewers=$(IFS=','; echo "${REVIEWERS[*]}")" >> "$GITHUB_OUTPUT"
72+
73+
- name: Add reviewers
74+
if: steps.collect-reviewers.outputs.reviewers != ''
75+
env:
76+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
77+
GH_REPO: ${{ github.repository }}
78+
PR_NUMBER: ${{ github.event.pull_request.number }}
79+
REVIEWERS: ${{ steps.collect-reviewers.outputs.reviewers }}
80+
run: |
81+
GH_PR_EDIT_ARGS=()
82+
IFS=',' read -ra reviewers <<< "$REVIEWERS"
83+
for reviewer in "${reviewers[@]}"; do
84+
GH_PR_EDIT_ARGS+=(--add-reviewer "$reviewer")
85+
done
86+
87+
echo "Adding reviewers: $REVIEWERS"
88+
gh pr edit "$PR_NUMBER" "${GH_PR_EDIT_ARGS[@]}"

app/backend.dev.env

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ ACTIVENESS_PROBES_ENABLED=1
8787
# Experimentation (feature flags)
8888
EXPERIMENTATION_ENABLED=0
8989
EXPERIMENTATION_PASS_ALL_GATES=1
90-
STATSIG_SERVER_SECRET_KEY=secret-...
91-
STATSIG_ENVIRONMENT=development
90+
GROWTHBOOK_API_HOST=https://gbapi.couchershq.org
91+
GROWTHBOOK_CLIENT_KEY=sdk-cGmljeXu1BCzHffE
9292

9393
# Moderation auto-approval deadline in seconds (0 to disable)
9494
MODERATION_AUTO_APPROVE_DEADLINE_SECONDS=10

app/backend/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ dependencies = [
4141
"stripe>=13.1.0",
4242
"ua-parser>=1.0.1",
4343
"user-agents>=2.2.0",
44-
"statsig-python-core>=0.8.0",
44+
"growthbook>=1.2.0",
4545
]
4646

4747
[project.scripts]
@@ -136,7 +136,7 @@ module = ["couchers.migrations.versions.*"]
136136
ignore_errors = true
137137

138138
[[tool.mypy.overrides]]
139-
module = ["http_ece", "py_vapid", "luhn", "sqlalchemy_utils.*", "statsig_python_core", "user_agents"]
139+
module = ["http_ece", "py_vapid", "luhn", "sqlalchemy_utils.*", "growthbook", "user_agents"]
140140
# These libraries don't have type stubs or py.typed markers
141141
ignore_missing_imports = true
142142

app/backend/src/app.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,6 @@ def main() -> None:
9696
setup_tracing()
9797

9898
# Initialize the experimentation framework for feature flags in the main process.
99-
# IMPORTANT: This MUST be called AFTER worker processes are spawned (above).
100-
# The underlying SDK uses internal threading that doesn't survive fork().
10199
# Worker processes initialize their own instance in _run_forever().
102100
setup_experimentation()
103101

app/backend/src/couchers/config.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,13 @@
115115
("RECAPTHCA_SITE_KEY", str),
116116
# Whether we're in test
117117
("IN_TEST", bool, "0"),
118-
# Experimentation (feature flags via Statsig)
118+
# Experimentation (feature flags via GrowthBook)
119119
("EXPERIMENTATION_ENABLED", bool, "0"),
120120
# When enabled, all feature gates return True (useful for development/testing)
121121
("EXPERIMENTATION_PASS_ALL_GATES", bool, "0"),
122-
# Statsig SDK configuration
123-
("STATSIG_SERVER_SECRET_KEY", str, ""),
124-
("STATSIG_ENVIRONMENT", str, "development"),
122+
# GrowthBook SDK configuration
123+
("GROWTHBOOK_API_HOST", str, "https://cdn.growthbook.io"),
124+
("GROWTHBOOK_CLIENT_KEY", str, ""),
125125
# Moderation auto-approval deadline in seconds (0 to disable auto-approval)
126126
("MODERATION_AUTO_APPROVE_DEADLINE_SECONDS", int),
127127
# User ID of the bot user for automated moderation actions
@@ -171,8 +171,8 @@ def check_config(cfg: dict[str, Any]) -> None:
171171
raise Exception("MyPostcard API credentials not configured but postal verification enabled")
172172

173173
if cfg["EXPERIMENTATION_ENABLED"]:
174-
if not cfg["STATSIG_SERVER_SECRET_KEY"]:
175-
raise Exception("No Statsig server secret key but experimentation enabled")
174+
if not cfg["GROWTHBOOK_CLIENT_KEY"]:
175+
raise Exception("No GrowthBook client key but experimentation enabled")
176176

177177

178178
def make_config() -> dict[str, Any]:

app/backend/src/couchers/context.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
from typing import NoReturn, cast
1+
from typing import TYPE_CHECKING, NoReturn, cast
22

33
import grpc
44

55
from couchers.i18n import LocalizationContext
66

7+
if TYPE_CHECKING:
8+
from growthbook import GrowthBook
9+
710

811
class NonInteractiveContextException(Exception):
912
"""If this exception is raised, it is a programming error"""
@@ -79,6 +82,7 @@ def __init__(
7982
self.__is_interactive = is_interactive
8083
self.__logged_in = self._user_id is not None
8184
self.__cookies: list[str] = []
85+
self._growthbook: GrowthBook | None = None
8286

8387
if self.__is_interactive:
8488
if not self._grpc_context:

app/backend/src/couchers/email/calendar_events.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,23 @@ def create_host_request_ics(
2525
host_request: HostRequest, other_name: str, hosting: bool, loc_context: LocalizationContext
2626
) -> str:
2727
event = create_host_request_event(host_request, other_name, hosting, loc_context)
28-
return event_to_ics(event, loc_context)
28+
29+
# METHOD:PUBLISH means this is part of a stream of calendar event information.
30+
# It allows for later cancellation, and doesn't expose accept/decline functionality.
31+
return event_to_ics(event, "PUBLISH", loc_context)
2932

3033

3134
def create_host_request_event(
32-
host_request: HostRequest, other_name: str, hosting: bool, loc_context: LocalizationContext
35+
host_request: HostRequest, other_name: str, hosting: bool, loc_context: LocalizationContext, sequence: int = 0
3336
) -> Event:
3437
"""Creates an ics event for a host request."""
3538

3639
event = Event()
3740
event.uid = get_host_request_event_uid(host_request.host_request_id)
3841

42+
# Explicitly allow later sequencing of a cancellation with SEQUENCE:1
43+
event.extra.append(ContentLine(name="SEQUENCE", value=str(sequence)))
44+
3945
if hosting:
4046
event.name = get_emails_i18next().localize(
4147
"calendar_events.host_requests.title_host", loc_context.locale, {"name": other_name}
@@ -70,23 +76,23 @@ def create_host_request_cancellation_attachment(
7076
def create_host_request_cancellation_ics(
7177
host_request: HostRequest, other_name: str, hosting: bool, loc_context: LocalizationContext
7278
) -> str:
73-
event = create_host_request_event(host_request, other_name, hosting, loc_context)
79+
event = create_host_request_event(host_request, other_name, hosting, loc_context, sequence=1)
7480
event.name = get_emails_i18next().localize(
7581
"calendar_events.title_cancelled", loc_context.locale, {"title": event.name}
7682
)
7783
event.status = "CANCELLED"
7884

79-
# Cancellation is sequenced after creation (which defaults to sequence number 0)
80-
event.extra.append(ContentLine(name="SEQUENCE", value="1"))
81-
82-
return event_to_ics(event, loc_context)
85+
# METHOD:PUBLISH means this is part of a stream of calendar event information.
86+
# Gmail™ will immediately remove the event from the user's calendar.
87+
# METHOD:CANCEL might leave the event in cancelled state or not work.
88+
return event_to_ics(event, "PUBLISH", loc_context)
8389

8490

85-
def event_to_ics(event: Event, loc_context: LocalizationContext) -> str:
91+
def event_to_ics(event: Event, method: str | None, loc_context: LocalizationContext) -> str:
8692
# PRODID is mandatory and generally follows "-//[Organization]//[Product Name]//[Language]"
8793
calendar = Calendar(creator=f"-//Couchers.org//Couchers//{loc_context.locale.upper()}")
88-
if event.status == "CANCELLED":
89-
calendar.method = "CANCEL"
94+
if method:
95+
calendar.method = method
9096
calendar.events.add(event)
9197
return cast(str, calendar.serialize())
9298

0 commit comments

Comments
 (0)