Skip to content

Commit 0f0998f

Browse files
committed
Handle merge conflicts
2 parents 42e9242 + c4aca28 commit 0f0998f

161 files changed

Lines changed: 5021 additions & 633 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/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

app/backend/src/couchers/i18n/locales/de.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,11 @@
246246
"public_trip_user_mismatch": "Der Empfänger der Host-Anfrage stimmt nicht mit dem Reisenden des öffentlichen Trips überein.",
247247
"public_trips_not_enabled": "Öffentliche Trips sind in dieser Community nicht aktiviert.",
248248
"duplicate_host_request_for_trip": "Du hast bereits angeboten, diesen Trip zu hosten.",
249-
"public_trip_same_gender_only": "Dieser Trip akzeptiert nur Angebote von Personen des gleichen Geschlechts."
249+
"public_trip_same_gender_only": "Dieser Trip akzeptiert nur Angebote von Personen des gleichen Geschlechts.",
250+
"incomplete_profile_send_friend_request": "Du musst dein Profil vervollständigen, bevor du eine Freundschafts-Anfrage senden kannst.",
251+
"admin_note_data_must_be_valid_json": "Die Admin-Notiz-Daten müssen gültiges JSON sein.",
252+
"admin_note_requires_exactly_one_of_note_or_data": "Gib genau eines an, admin_note oder data.",
253+
"invalid_password_login": "Benutzername/E-Mail oder Passwort ist falsch."
250254
},
251255
"quick_links": {
252256
"do_not_email": "Du wirst keine E-Mails mehr erhalten, die nicht sicherheitsrelevant sind, und dein Hosting-Status wurde zurückgesetzt. Der Newsletter muss separat abbestellt werden.",

app/backend/src/couchers/i18n/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
"admin_blog_title_too_long": "The blog post title is too long.",
88
"admin_cannot_edit_badge": "Admins cannot edit that badge.",
99
"admin_note_cant_be_empty": "The admin note cannot be empty.",
10+
"admin_note_data_must_be_valid_json": "The admin note data must be valid JSON.",
11+
"admin_note_requires_exactly_one_of_note_or_data": "Provide exactly one of admin_note or data.",
1012
"already_admin": "That user is already an admin.",
1113
"already_have_dm": "You already have a direct message chat with this user.",
1214
"already_in_chat": "That user is already in the chat.",
@@ -127,6 +129,7 @@
127129
"invalid_name": "Name not supported.",
128130
"invalid_notification_preference": "Invalid notification preference.",
129131
"invalid_password": "Wrong password.",
132+
"invalid_password_login": "Wrong username/email or password.",
130133
"invalid_phone": "Phone number must be in international format without punctuation.",
131134
"invalid_recipients": "Invalid recipients list.",
132135
"invalid_region": "Invalid region.",

app/backend/src/couchers/i18n/locales/en_CORP.json

Lines changed: 0 additions & 5 deletions
This file was deleted.

app/backend/src/couchers/i18n/locales/es.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,11 @@
132132
"chat_initiation_rate_limit2_many": "Has enviado mensajes a muchos usuarios en el último {{count}} de horas. Para evitar el spam, no puedes contactar con más usuarios por el momento.",
133133
"chat_initiation_rate_limit2_other": "Has enviado mensajes a muchos usuarios en las últimas {{count}} horas. Para evitar el spam, no puedes contactar con más usuarios por el momento.",
134134
"dev_apis_disabled": "Las API de desarrollo no están habilitadas en este servidor.",
135-
"event_community_invite_not_found": "No se pudo encontrar esa invitación de la comunidad al evento."
135+
"event_community_invite_not_found": "No se pudo encontrar esa invitación de la comunidad al evento.",
136+
"friend_request_rate_limit2_one": "Has enviado muchas solicitudes de amistad en la última {{count}} hora. Para evitar el spam, no puedes enviar más por ahora.",
137+
"friend_request_rate_limit2_many": "Has enviado muchas solicitudes de amistad en los últimos {{count}} de horas. Para evitar el spam, no puedes enviar más por ahora.",
138+
"friend_request_rate_limit2_other": "Has enviado muchas solicitudes de amistad en las últimas {{count}} horas. Para evitar el spam, no puedes enviar más por ahora.",
139+
"after_item_not_found": "No se encontró el elemento después del cual colocar."
136140
},
137141
"badges": {
138142
"volunteer_name": "Voluntario en activo",
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Add machine-readable data payload to admin notes
2+
3+
Revision ID: 0151
4+
Revises: 0150
5+
Create Date: 2026-05-15 05:41:24.160635
6+
7+
"""
8+
9+
import sqlalchemy as sa
10+
from alembic import op
11+
from sqlalchemy.dialects import postgresql
12+
13+
# revision identifiers, used by Alembic.
14+
revision = "0151"
15+
down_revision = "0150"
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade() -> None:
21+
op.add_column(
22+
"admin_actions", sa.Column("data", postgresql.JSONB(none_as_null=True, astext_type=sa.Text()), nullable=True)
23+
)
24+
op.create_check_constraint(
25+
constraint_name="note_xor_data",
26+
table_name="admin_actions",
27+
condition="note IS NULL OR data IS NULL",
28+
)
29+
30+
31+
def downgrade() -> None:
32+
op.drop_constraint("ck_admin_actions_note_xor_data", "admin_actions", type_="check")
33+
op.drop_column("admin_actions", "data")

0 commit comments

Comments
 (0)