Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 1 deletion docs/community-team/adding-a-city.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Adding a city establishes the Forum space, organiser access, and Pretix team use
4. Add the agreed organisers using [Managing organiser access](managing-organisers.md).
5. Confirm the Forum topic is visible and that an organiser can access the correct Pretix dashboard.

Each `CityInfo` entry stores the region, country, slug, display name, IANA time zone, latitude, and longitude, in that order. Coordinates are decimal degrees (WGS 84): latitude ranges from -90 to 90, and longitude from -180 to 180. Both coordinate values are required for every record in `CITIES`; the registry validates that they are paired and in range, while tests running in CI also check that they are finite floats. The city registry rejects duplicate slugs and display names. Coordinates are recorded for map use; no geocoding is performed. For a regional meetup such as NOVA, use a representative point for the area.
Each `CityInfo` entry stores the region, country, slug, display name, IANA time zone, latitude, and longitude, in that order. Coordinates are decimal degrees (WGS 84): latitude ranges from -90 to 90, and longitude from -180 to 180. Both coordinate values are required for every record in `CITIES`; the registry validates that they are paired, finite, and in range, and rejects duplicate slugs and display names. Coordinates are recorded for map use; no geocoding is performed. For a regional meetup such as NOVA, use a representative point for the area.

For example, add a Meetup Pro city to `MEETUP_PRO_CITIES` as a `CityInfo` entry, keeping the section sorted by region, country, and city:

Expand Down
19 changes: 19 additions & 0 deletions docs/community-team/meetup-worldmap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generate the meetup map

## Regenerate the page

The public map is generated from the canonical location list in `tools/common/ansible_event_cities.py`. After adding or changing meetup locations, run this command from the repository root:

```bash
uv run python tools/website/generate_worldmap.py
```

Add `-v` to see timestamped progress and the generation runtime:

```bash
uv run python tools/website/generate_worldmap.py -v
```

The generator reads city names, regions, timezones, and coordinates from `CityInfo` in `tools/common/ansible_event_cities.py` and updates `docs/map.html`. It does not look up or geocode locations. Review and commit the location registry and generated HTML when locations change.

For local preview, run `uv run python -m http.server 8000 --directory docs` from the repository root, then open `http://localhost:8000/map.html`. Serving it over HTTP avoids browser restrictions that can affect map requests opened with `file://`.
8 changes: 8 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

Ansible Meetups are community events for learning, sharing practical experience, and meeting people interested in Ansible. The Ansible Community Forum is the home for event information and discussion. Pretix handles registration.

## Find your local Ansible meetup

Search meetup locations by city, country, or region, or browse the map.

<a class="md-button md-button--primary" href="map.html">Open the map on its own page</a>

<iframe src="map.html" title="Interactive map of Ansible meetup locations" style="width:100%;height:82vh;min-height:700px;border:1px solid #d7dde2;border-radius:8px" loading="lazy" allowfullscreen></iframe>

Choose the guide for what you want to do:

- **Attending?** [Find an event and RSVP](attendees/find-and-rsvp.md), then [join the discussion and follow up](attendees/participate-and-follow-up.md).
Expand Down
294 changes: 294 additions & 0 deletions docs/map.html

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ theme:
name: Switch to light mode
nav:
- Start here: index.md
- Meetup map: map.html
- Attendees:
- attendees/index.md
- Find an event and RSVP: attendees/find-and-rsvp.md
Expand All @@ -47,6 +48,7 @@ nav:
- After the Meetup: organisers/after-the-meetup.md
- Ansible Community Team:
- community-team/index.md
- Generate the meetup map: community-team/meetup-worldmap.md
- Provisioning: community-team/provisioning.md
- Adding a city: community-team/adding-a-city.md
- Creating an event: community-team/creating-an-event.md
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ select = ["E", "F", "B", "S", "T20", "DTZ", "UP", "SIM", "G"]
"tools/environment/check_setup.py" = ["T201"]
"tools/meetup_pro/meetup_pro_lib.py" = ["S105"]
"tools/meetup_pro/meetup_list_organisers.py" = ["E501"]
"tools/website/generate_worldmap.py" = ["E501"]

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand Down
113 changes: 113 additions & 0 deletions tests/test_worldmap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from __future__ import annotations

import json
import re
import pytest

from tools.common.ansible_event_cities import CITIES, CityInfo
from tools.website.generate_worldmap import MAP_MARKER_COLOR, OUTPUT, normalise_region, render_page


def test_registry_has_coordinates_for_every_location() -> None:
assert CITIES
assert all(city.latitude is not None and city.longitude is not None for city in CITIES)
assert all(-90 <= city.latitude <= 90 and -180 <= city.longitude <= 180 for city in CITIES) # type: ignore[operator]


@pytest.mark.parametrize(
("slug", "latitude_range", "longitude_range"),
[
("mclean", (38.5, 39.5), (-78.0, -76.0)),
("pisa", (43.6, 43.8), (10.3, 10.5)),
("nova", (38.5, 39.3), (-77.6, -76.9)),
("sfbay", (36.7, 38.8), (-123.5, -121.3)),
],
)
def test_ambiguous_location_coordinates_are_in_the_expected_region(
slug: str,
latitude_range: tuple[float, float],
longitude_range: tuple[float, float],
) -> None:
city = next(city for city in CITIES if city.slug == slug)
assert city.latitude is not None and city.longitude is not None
assert latitude_range[0] < city.latitude < latitude_range[1]
assert longitude_range[0] < city.longitude < longitude_range[1]


def test_city_coordinates_must_be_a_pair() -> None:
with pytest.raises(ValueError, match="both latitude and longitude"):
CityInfo("Europe", "UK", "london", "London", "Europe/London", latitude=51.5)


@pytest.mark.parametrize(
("latitude", "longitude"),
[(91, 0), (-91, 0), (0, 181), (0, -181), (float("nan"), 0), (0, float("inf"))],
)
def test_city_coordinates_must_be_finite_and_in_range(latitude: float, longitude: float) -> None:
with pytest.raises(ValueError, match="Invalid (latitude|longitude)"):
CityInfo("Europe", "UK", "london", "London", "Europe/London", latitude, longitude)


@pytest.mark.parametrize(
("source", "expected"),
[
("Europe", "Europe, Middle East and Africa"),
("EMEA", "Europe, Middle East and Africa"),
("NA", "Americas"),
("LATAM", "Latin America"),
],
)
def test_normalise_region(source: str, expected: str) -> None:
assert normalise_region(source) == expected


def test_rendered_page_uses_ansible_theme_and_map_controls() -> None:
page = render_page(
[
{
"slug": "london",
"city": "<script>alert(1)</script>",
"country": "UK",
"region": "Europe",
"timezone": "Europe/London",
"lat": 51.5,
"lon": -0.1,
}
],
"2026-11-06 10:00 UTC",
)
match = re.search(r'<script id="meetup-data" type="application/json">(.*?)</script>', page, re.S)
assert match is not None
data = json.loads(match.group(1))
assert data["locations"][0]["city"] == "<script>alert(1)</script>"
assert data["locations"][0]["timezone"] == "Europe/London"
assert r"\u003cscript\u003e" in match.group(1)
assert "events" not in data
assert "maplibre-gl@^5" in page
assert "assets/stylesheets/ansible.css" in page
assert "Red+Hat+Display" in page
assert "#5bbdbf" in page
assert MAP_MARKER_COLOR == "#5bbdbf"
assert f"'circle-color': '{MAP_MARKER_COLOR}'" in page
assert "new maplibregl.FullscreenControl()" in page
assert "map.on('click', 'meetup-cluster-count', expandCluster)" in page
assert "async function expandCluster(event)" in page
assert "await map.getSource('meetup-locations').getClusterExpansionZoom(feature.properties.cluster_id)" in page
assert OUTPUT.name == "map.html"
assert 'placeholder="Search city, country, or region"' in page
assert "location.region" in page and "location.timezone" in page
assert "timezone.textContent = formatTimezone(location.timezone)" in page
assert "window.matchMedia('(max-width: 760px)').matches" in page
assert "map.flyTo({center: [location.lon, location.lat], zoom: 8}" in page
assert "document.body.dataset.mobileView = 'list'" in page
assert "scrollIntoView({block: 'nearest'" in page
assert ".maplibregl-popup-content { color: var(--ink) !important; background: var(--surface) !important;" in page
assert "America/Argentina/Buenos_Aires" not in page
assert '<br>${esc(location.country)}<br><a class="subscribe"' in page
assert "Upcoming events" not in page
assert "Location subscriptions are managed" not in page
assert 'id="meetup-data"' in page
assert "--surface: #202428" in page
assert "prefers-color-scheme: dark" in page
assert "tile.openstreetmap.org" not in page
assert "OpenMapTiles" in page and "OpenStreetMap contributors" in page
1 change: 1 addition & 0 deletions tools/website/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Public community pages and data generators."""
Loading
Loading