OSM-Beiträge: "Hund willkommen?" 👍/👎 (dog=yes/no) + Umdrehen
- dog=no zusätzlich zu dog=yes (Pächterwechsel → Ort nicht mehr hundefreundlich). - Map-Popup: ein "Hund willkommen?"-Block mit Daumen hoch/runter statt zwei Buttons. Beide rufen /dog-friendly mit welcome=true|false. - Backend generisch: tag_value yes|no; vorhandene Markierung mit anderem Wert wird umgedreht (Update statt 409); submit_dog_tag(value); Confirm/Revert prüft gegen den jeweiligen tag_value; Changeset-Kommentar wertabhängig.
This commit is contained in:
parent
57849515ea
commit
9afbf24535
7 changed files with 100 additions and 67 deletions
|
|
@ -48,6 +48,7 @@ class DogFriendlyIn(BaseModel):
|
|||
poi_type: Optional[str] = None
|
||||
lat: float
|
||||
lon: float
|
||||
welcome: bool = True # True → dog=yes, False → dog=no (Pächterwechsel)
|
||||
|
||||
|
||||
def _verified_count(conn, uid: int) -> int:
|
||||
|
|
@ -60,13 +61,13 @@ def _verified_count(conn, uid: int) -> int:
|
|||
# ------------------------------------------------------------------
|
||||
# OSM-Changeset-Upload (write_api): Element holen → dog=yes → Changeset.
|
||||
# ------------------------------------------------------------------
|
||||
_CHANGESET_XML = (
|
||||
'<osm><changeset>'
|
||||
'<tag k="created_by" v="BanYaro/1.0"/>'
|
||||
'<tag k="comment" v="Hund willkommen (dog=yes) — via Ban Yaro"/>'
|
||||
'<tag k="source" v="survey"/>'
|
||||
'</changeset></osm>'
|
||||
)
|
||||
def _changeset_xml(value: str) -> str:
|
||||
note = "Hund willkommen" if value == "yes" else "Hund nicht willkommen"
|
||||
return ('<osm><changeset>'
|
||||
'<tag k="created_by" v="BanYaro/1.0"/>'
|
||||
f'<tag k="comment" v="{note} (dog={value}) — via Ban Yaro"/>'
|
||||
'<tag k="source" v="survey"/>'
|
||||
'</changeset></osm>')
|
||||
|
||||
|
||||
def _mark_submitted(contrib_id: int, etype: str, changeset_id):
|
||||
|
|
@ -78,9 +79,9 @@ def _mark_submitted(contrib_id: int, etype: str, changeset_id):
|
|||
)
|
||||
|
||||
|
||||
async def submit_dog_yes(contrib_id: int, osm_id: int, osm_type: str, token: str) -> bool:
|
||||
"""Setzt dog=yes am OSM-Element des Nutzers (eigener OAuth-Token). Idempotent.
|
||||
Wirft bei Fehler → Beitrag bleibt 'pending' (Retry über den Job)."""
|
||||
async def submit_dog_tag(contrib_id: int, osm_id: int, osm_type: str, token: str, value: str) -> bool:
|
||||
"""Setzt dog=<value> (yes|no) am OSM-Element des Nutzers (eigener OAuth-Token).
|
||||
Idempotent. Wirft bei Fehler → Beitrag bleibt 'pending' (Retry über den Job)."""
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
order = [osm_type, "way" if osm_type == "node" else "node"]
|
||||
async with httpx.AsyncClient(timeout=20) as client:
|
||||
|
|
@ -96,21 +97,21 @@ async def submit_dog_yes(contrib_id: int, osm_id: int, osm_type: str, token: str
|
|||
root = ET.fromstring(elem_xml)
|
||||
el = root.find(etype)
|
||||
existing = el.find("./tag[@k='dog']")
|
||||
if existing is not None and existing.get("v") == "yes":
|
||||
if existing is not None and existing.get("v") == value:
|
||||
_mark_submitted(contrib_id, etype, None) # schon gesetzt → fertig
|
||||
return True
|
||||
|
||||
# 2) Changeset öffnen
|
||||
cs = await client.put(f"{OSM_API_BASE}/api/0.6/changeset/create",
|
||||
headers=headers, content=_CHANGESET_XML)
|
||||
headers=headers, content=_changeset_xml(value))
|
||||
cs.raise_for_status()
|
||||
changeset_id = cs.text.strip()
|
||||
|
||||
# 3) dog=yes setzen + Element hochladen (Geometrie/andere Tags bleiben)
|
||||
# 3) dog=<value> setzen + Element hochladen (Geometrie/andere Tags bleiben)
|
||||
if existing is not None:
|
||||
existing.set("v", "yes")
|
||||
existing.set("v", value)
|
||||
else:
|
||||
ET.SubElement(el, "tag", {"k": "dog", "v": "yes"})
|
||||
ET.SubElement(el, "tag", {"k": "dog", "v": value})
|
||||
el.set("changeset", changeset_id)
|
||||
up = await client.put(f"{OSM_API_BASE}/api/0.6/{etype}/{osm_id}",
|
||||
headers=headers, content=ET.tostring(root, encoding="unicode"))
|
||||
|
|
@ -132,12 +133,17 @@ async def mark_dog_friendly(body: DogFriendlyIn, user=Depends(get_current_user))
|
|||
if not conn.execute("SELECT 1 FROM user_osm WHERE user_id=?", (uid,)).fetchone():
|
||||
raise HTTPException(409, "Bitte zuerst dein OSM-Konto verknüpfen.")
|
||||
|
||||
# 1) Dedup — 1× pro POI
|
||||
if conn.execute(
|
||||
"SELECT 1 FROM osm_contributions WHERE user_id=? AND osm_id=? AND tag_key='dog'",
|
||||
value = 'yes' if body.welcome else 'no'
|
||||
|
||||
# 1) Vorhandene Markierung? Gleicher Wert → fertig. Anderer Wert →
|
||||
# umdrehen erlaubt (Pächter wechseln → aus willkommen wird nicht mehr).
|
||||
existing = conn.execute(
|
||||
"SELECT id, tag_value FROM osm_contributions "
|
||||
"WHERE user_id=? AND osm_id=? AND tag_key='dog'",
|
||||
(uid, body.osm_id)
|
||||
).fetchone():
|
||||
raise HTTPException(409, "Diesen Ort hast du schon als hundefreundlich markiert.")
|
||||
).fetchone()
|
||||
if existing and existing['tag_value'] == value:
|
||||
raise HTTPException(409, "Diesen Ort hast du schon so markiert.")
|
||||
|
||||
# 2) Zeitkomponente: Tages-Rate-Limit
|
||||
today_n = conn.execute(
|
||||
|
|
@ -184,16 +190,27 @@ async def mark_dog_friendly(body: DogFriendlyIn, user=Depends(get_current_user))
|
|||
if poi and haversine_m(body.lat, body.lon, poi['lat'], poi['lon']) > POI_NEAR_M:
|
||||
raise HTTPException(422, "Position passt nicht zum gewählten Ort.")
|
||||
|
||||
# 5) verifiziert erfassen (pending; OSM-Upload gleich best-effort)
|
||||
cur = conn.execute(
|
||||
"""INSERT INTO osm_contributions
|
||||
(user_id, osm_id, osm_type, poi_type, tag_key, tag_value, lat, lon,
|
||||
route_id, gps_distance_m, gps_points_near, status)
|
||||
VALUES (?,?,?,?, 'dog','yes', ?,?, ?,?,?, 'pending')""",
|
||||
(uid, body.osm_id, body.osm_type, body.poi_type, body.lat, body.lon,
|
||||
best[0], round(best[1], 1), best[2])
|
||||
)
|
||||
contrib_id = cur.lastrowid
|
||||
# 5) verifiziert erfassen oder umdrehen (pending; OSM-Upload gleich best-effort)
|
||||
if existing:
|
||||
conn.execute(
|
||||
"UPDATE osm_contributions SET tag_value=?, osm_type=?, poi_type=?, "
|
||||
"lat=?, lon=?, route_id=?, gps_distance_m=?, gps_points_near=?, "
|
||||
"status='pending', changeset_id=NULL, submitted_at=NULL, "
|
||||
"created_at=datetime('now') WHERE id=?",
|
||||
(value, body.osm_type, body.poi_type, body.lat, body.lon,
|
||||
best[0], round(best[1], 1), best[2], existing['id'])
|
||||
)
|
||||
contrib_id = existing['id']
|
||||
else:
|
||||
cur = conn.execute(
|
||||
"""INSERT INTO osm_contributions
|
||||
(user_id, osm_id, osm_type, poi_type, tag_key, tag_value, lat, lon,
|
||||
route_id, gps_distance_m, gps_points_near, status)
|
||||
VALUES (?,?,?,?, 'dog',?, ?,?, ?,?,?, 'pending')""",
|
||||
(uid, body.osm_id, body.osm_type, body.poi_type, value, body.lat, body.lon,
|
||||
best[0], round(best[1], 1), best[2])
|
||||
)
|
||||
contrib_id = cur.lastrowid
|
||||
total = _verified_count(conn, uid)
|
||||
token_enc = conn.execute(
|
||||
"SELECT token_enc FROM user_osm WHERE user_id=?", (uid,)
|
||||
|
|
@ -202,14 +219,14 @@ async def mark_dog_friendly(body: DogFriendlyIn, user=Depends(get_current_user))
|
|||
# 6) OSM-Upload best-effort — Fehler → bleibt 'pending', Job versucht erneut
|
||||
submitted = False
|
||||
try:
|
||||
submitted = await submit_dog_yes(contrib_id, body.osm_id, body.osm_type, _decrypt(token_enc))
|
||||
submitted = await submit_dog_tag(contrib_id, body.osm_id, body.osm_type, _decrypt(token_enc), value)
|
||||
except Exception as e:
|
||||
logger.warning("OSM-Upload später erneut (contrib %s): %s", contrib_id, e)
|
||||
|
||||
logger.info("dog=yes erfasst: user %s, osm %s, Tour %s (%.0fm, %d Pkt), submitted=%s",
|
||||
uid, body.osm_id, best[0], best[1], best[2], submitted)
|
||||
logger.info("dog=%s erfasst: user %s, osm %s, Tour %s (%.0fm, %d Pkt), submitted=%s",
|
||||
value, uid, body.osm_id, best[0], best[1], best[2], submitted)
|
||||
return {
|
||||
"status": "erfasst", "verified": True, "submitted": submitted,
|
||||
"status": "erfasst", "value": value, "verified": True, "submitted": submitted,
|
||||
"verified_count": total, "badge": total >= BADGE_AT,
|
||||
"pro_progress": min(total, PRO_AT), "pro_at": PRO_AT,
|
||||
}
|
||||
|
|
@ -264,19 +281,20 @@ async def run_confirmation_round():
|
|||
# (1) Pending-Retry
|
||||
with db() as conn:
|
||||
pend = conn.execute(
|
||||
"SELECT c.id, c.osm_id, c.osm_type, o.token_enc FROM osm_contributions c "
|
||||
"SELECT c.id, c.osm_id, c.osm_type, c.tag_value, o.token_enc FROM osm_contributions c "
|
||||
"JOIN user_osm o ON o.user_id=c.user_id WHERE c.status='pending' LIMIT 50"
|
||||
).fetchall()
|
||||
for r in pend:
|
||||
try:
|
||||
await submit_dog_yes(r["id"], r["osm_id"], r["osm_type"] or "node", _decrypt(r["token_enc"]))
|
||||
await submit_dog_tag(r["id"], r["osm_id"], r["osm_type"] or "node",
|
||||
_decrypt(r["token_enc"]), r["tag_value"])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# (2) Confirm/Revert
|
||||
with db() as conn:
|
||||
subs = conn.execute(
|
||||
"SELECT id, user_id, osm_id, osm_type FROM osm_contributions "
|
||||
"SELECT id, user_id, osm_id, osm_type, tag_value FROM osm_contributions "
|
||||
"WHERE status='submitted' AND submitted_at < datetime('now', ?)",
|
||||
(f"-{CONFIRM_AFTER_DAYS} days",)
|
||||
).fetchall()
|
||||
|
|
@ -290,7 +308,7 @@ async def run_confirmation_round():
|
|||
if resp.status_code == 200:
|
||||
el = ET.fromstring(resp.text).find(etype)
|
||||
tag = el.find("./tag[@k='dog']") if el is not None else None
|
||||
ok = tag is not None and tag.get("v") == "yes"
|
||||
ok = tag is not None and tag.get("v") == r["tag_value"]
|
||||
new_status = "confirmed" if ok else "rejected"
|
||||
except Exception:
|
||||
continue # nächste Runde erneut
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue