### Install Dependencies Source: https://github.com/wmo-raf/climweb/blob/main/CONTRIBUTING.md Install the project in editable mode. ```bash pip install -e climweb ``` -------------------------------- ### Quick Start All Services Source: https://github.com/wmo-raf/climweb/blob/main/rtl-audit.md A consolidated script to start PostgreSQL, Redis, and the ClimWeb server in sequence. ```bash export PATH="/Applications/Postgres.app/Contents/Versions/18/bin:$PATH" pg_ctl -D "$HOME/Library/Application Support/Postgres/var-18" -l /tmp/pg_server.log start brew services start redis cd ~/Downloads/climweb && \ export $(cat .env | grep -v '^#' | grep -v '^$' | xargs) && \ venv/bin/climweb runserver 8000 ``` -------------------------------- ### Quickstart development environment Source: https://github.com/wmo-raf/climweb/blob/main/docs/_docs/technical/development/running-dev-environment.md Launch the development environment using the provided shell script. ```bash $ git clone https://github.com/wmo-raf/climweb.git # Our supplied ./dev.sh script wraps Docker Compose setting the correct env vars for # you to get hot code reloading working well. $ ./dev.sh # Run ./dev.sh help for further details. $ ./dev.sh help ``` -------------------------------- ### Run Development Server Source: https://github.com/wmo-raf/climweb/blob/main/CONTRIBUTING.md Start the local development server. ```bash climweb runserver ``` -------------------------------- ### Start Redis Service Source: https://github.com/wmo-raf/climweb/blob/main/rtl-audit.md Starts the Redis background service using Homebrew. ```bash brew services start redis ``` -------------------------------- ### Start PostgreSQL Server Source: https://github.com/wmo-raf/climweb/blob/main/rtl-audit.md Initializes the PostgreSQL database instance using the specified data directory. ```bash export PATH="/Applications/Postgres.app/Contents/Versions/18/bin:$PATH" pg_ctl -D "$HOME/Library/Application Support/Postgres/var-18" \ -l /tmp/pg_server.log start ``` -------------------------------- ### Verify installation requirements Source: https://github.com/wmo-raf/climweb/blob/main/docs/_docs/technical/development/running-dev-environment.md Check that Docker and Git are correctly installed and meet the minimum version requirements. ```bash $ docker -v Docker version 28.1.1, build 4eba377 $ docker compose version Docker Compose version v2.35.1-desktop.1 $ git --version git version 2.37.1 ``` -------------------------------- ### Start Redis Source: https://github.com/wmo-raf/climweb/blob/main/CONTRIBUTING.md Start the Redis service based on the operating system. ```bash # macOS brew services start redis # Linux sudo systemctl start redis ``` -------------------------------- ### Start Docker containers Source: https://github.com/wmo-raf/climweb/blob/main/docs/_docs/technical/extending-climweb/plugin-installation.md Command to initialize the Climweb services after configuring environment variables. ```sh docker compose up ``` -------------------------------- ### Initialize Plugin with Cookiecutter Source: https://github.com/wmo-raf/climweb/blob/main/docs/_docs/technical/extending-climweb/creating-a-plugin.md Install the cookiecutter tool and execute the template command to generate the plugin structure. ```sh pip install cookiecutter cookiecutter gh:wmo-raf/climweb --directory plugin-boilerplate ``` -------------------------------- ### Start ClimWeb Development Server Source: https://github.com/wmo-raf/climweb/blob/main/rtl-audit.md Configures the environment variables and launches the Django development server on port 8000. ```bash cd ~/Downloads/climweb export PATH="/Applications/Postgres.app/Contents/Versions/18/bin:$PATH" export $(cat .env | grep -v '^#' | grep -v '^$' | xargs) venv/bin/climweb runserver 8000 ``` -------------------------------- ### Start development containers manually Source: https://github.com/wmo-raf/climweb/blob/main/docs/_docs/technical/development/running-dev-environment.md Bring up the development environment using Docker Compose without the helper script. ```bash $ git clone https://github.com/wmo-raf/climweb.git Cloning into 'climweb'... ... $ cd climweb ``` ```bash $ docker compose -f docker-compose.yml -f docker-compose.dev.yml up ``` -------------------------------- ### Conventional Commits Examples Source: https://github.com/wmo-raf/climweb/blob/main/CONTRIBUTING.md Examples of commit messages following the project's required format. ```text feat(cap): add multi-language support for alert descriptions fix(dashboard): correct date display format in admin area docs(installation): update Docker Compose environment variable reference i18n(sw): add Swahili translations for forecast pages ``` -------------------------------- ### Cookiecutter Prompt Configuration Source: https://github.com/wmo-raf/climweb/blob/main/docs/_docs/technical/extending-climweb/creating-a-plugin.md Example of the interactive prompts displayed when generating a new plugin. ```sh cookiecutter gh:wmo-raf/climweb --directory plugin-boilerplate project_name [My Climweb Plugin]: project_slug [my-climweb-plugin]: project_module [my_climweb_plugin]: ``` -------------------------------- ### Populated Forecast CSV Example Source: https://github.com/wmo-raf/climweb/blob/main/docs/_docs/Manage-City-Forecasts.md An example of a completed forecast CSV file with numerical data and valid condition labels. ```csv City,Max Temperature,Min Temperature,Wind Speed,Relative Humidity,Condition Eldoret,26,14,3,65,Partly Cloudy Kisumu,31,18,4,72,Rain Mombasa,33,24,6,80,Clear Sky Nairobi,28,15,3,60,Partly Cloudy Nakuru,27,13,2,58,Clear Sky ``` -------------------------------- ### Generate and update documentation translation files Source: https://github.com/wmo-raf/climweb/blob/main/translations.md Commands to generate gettext files, install the necessary internationalization tool, and update PO files for a specific language. ```sh sphinx-build -b gettext . _build/gettext ``` ```sh pip install sphinx-intl ``` ```sh sphinx-intl update -p _build/gettext -l fr ``` -------------------------------- ### Style Guide Utility Scripts Source: https://github.com/wmo-raf/climweb/blob/main/climweb/src/climweb/config/templates/base/style_guide.html JavaScript utilities for copying content to the clipboard and managing active sidebar navigation states. ```javascript function copyText(id) { const el = document.getElementById(id); if (!el) return; navigator.clipboard.writeText(el.textContent.trim()); } function copyHtml(previewId) { const el = document.getElementById(previewId); if (!el) return; navigator.clipboard.writeText(el.innerHTML.trim()); } // Highlight active sidebar link on scroll const sections = document.querySelectorAll('.sg-section'); const navLinks = document.querySelectorAll('.sg-nav a'); const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { navLinks.forEach(a => a.classList.remove('active')); const link = document.querySelector(`.sg-nav a[href="#${entry.target.id}"]`); if (link) link.classList.add('active'); } }); }, {rootMargin: '-30% 0px -60% 0px'}); sections.forEach(s => observer.observe(s)); ``` -------------------------------- ### Update Homepage Forecast Widget text-align Source: https://github.com/wmo-raf/climweb/blob/main/rtl-audit.md Replace physical text-align left with logical start. ```css text-align: left; ``` -------------------------------- ### Update Homepage Climate Section text-align Source: https://github.com/wmo-raf/climweb/blob/main/rtl-audit.md Replace physical text-align left with logical start to support RTL layouts. ```css text-align: left; /* line 73 */ text-align: left; /* line 87 */ ``` -------------------------------- ### Update Stations Detail Table Alignment Source: https://github.com/wmo-raf/climweb/blob/main/rtl-audit.md Replace text-align: left with text-align: start to support RTL text flow. ```css .std-row-val { text-align: left; } ``` -------------------------------- ### HTML text-align: left in plugin_manager.html Source: https://github.com/wmo-raf/climweb/blob/main/rtl-audit.md Inline text-align style in the plugin manager template that should be changed to text-align: start. ```html style="text-align: left;" ``` -------------------------------- ### Set Up Database Source: https://github.com/wmo-raf/climweb/blob/main/CONTRIBUTING.md Create the database and enable the PostGIS extension. ```bash psql -U postgres -c "CREATE DATABASE climweb;" psql -U postgres -d climweb -c "CREATE EXTENSION IF NOT EXISTS postgis;" ``` -------------------------------- ### Create the environment configuration file Source: https://github.com/wmo-raf/climweb/blob/main/docs/_docs/technical/development/running-dev-environment.md Initialize the .env file by copying the provided sample configuration. ```bash $ cp .env.dev.sample .env ``` -------------------------------- ### Set Up Development Data Source: https://github.com/wmo-raf/climweb/blob/main/CONTRIBUTING.md Commands to download, restore, and extract initial development data. ```bash curl -L -O https://github.com/wmo-raf/climweb/releases/download/initial-data/climweb_dump.dump curl -L -O https://github.com/wmo-raf/climweb/releases/download/initial-data/media_backup.tar.gz ``` ```bash pg_restore -U postgres -d climweb --no-owner --no-acl climweb_dump.dump ``` ```bash # macOS with Postgres.app /Applications/Postgres.app/Contents/Versions/18/bin/pg_restore \ -U postgres -d climweb --no-owner --no-acl climweb_dump.dump ``` ```bash tar -xzf media_backup.tar.gz -C climweb/src/climweb/ ``` ```bash climweb update_index climweb generate_forecast ``` -------------------------------- ### Initialize Product Detail UI Components Source: https://github.com/wmo-raf/climweb/blob/main/climweb/src/climweb/pages/products/templates/products/product_detail.html Handles tab switching, date pill selection, and initial state loading based on URL hashes. ```javascript $(document).ready(function () { const tabs = $(".pd-tab"); const contents = $(".pd-content"); const datePills = $(".pd-date-pill"); const productItems = $(".pd-item"); /* ── activate a content panel and its first date pill + product item ── */ function activateContent($content) { if (!$content || !$content.length) return; /* show this content panel */ contents.removeClass("active"); $content.addClass("active"); /* reset date pills within this panel — activate first */ const $pills = $content.find(".pd-date-pill"); $pills.removeClass("active"); $pills.first().addClass("active"); /* reset ALL product items — then activate the first in this panel */ productItems.removeClass("is-active"); const $items = $content.find(".pd-item"); $items.first().addClass("is-active"); } /* ── activate a specific product item via date pill ── */ function activateItem($pill) { const target = $pill.data("target"); if (!target) return; /* deactivate siblings only */ $pill.closest(".pd-date-pills").find(".pd-date-pill").removeClass("active"); $pill.addClass("active"); /* deactivate items in this content panel only, then activate target */ const $panel = $pill.closest(".pd-content"); $panel.find(".pd-item").removeClass("is-active"); $("#" + target).addClass("is-active"); } /* ── TAB CLICKS ── */ tabs.on("click", function (e) { e.preventDefault(); const contentId = $(this).attr("aria-controls"); tabs.removeClass("active"); $(this).addClass("active"); if (contentId) activateContent($("#" + contentId)); }); /* ── DATE PILL CLICKS ── */ datePills.on("click", function () { activateItem($(this)); }); /* ── INITIAL STATE ── */ /* check for hash in URL first */ const hash = window.location.hash; let activated = false; if (hash) { const hashId = hash.replace("#", ""); const $target = $("#" + hashId); if ($target.length) { /* find the tab that controls this content */ const $tab = $("[aria-controls=" + hashId + "]"); if ($tab.length) { $tab.addClass("active"); activateContent($target); activated = true; } } } if (!activated) { /* activate first tab and its content */ const $firstTab = tabs.first(); $firstTab.addClass("active"); const firstContentId = $firstTab.attr("aria-controls"); if (firstContentId) activateContent($("#" + firstContentId)); } }); ``` -------------------------------- ### Initialize City Analysis UI and Data Source: https://github.com/wmo-raf/climweb/blob/main/climweb/src/climweb/pages/home/templates/home/city_analysis.html Sets up initial query parameters, static paths, and event listeners for city selection changes. ```javascript let query_params = { city__id:'{{city_id}}', start_date:getCurrentDate(), // end_date:getSevenDaysFromToday(), effective_period__whole_day:true } const static_path = '{% static "forecastmanager/img/" %}' // Make an AJAX request to fetch the URL $(document).on('change', '#city-option', function() { var selectedOption = $(this).find(':selected'); var dataUrl = selectedOption.data('url'); window.location.pathname = dataUrl }); ``` -------------------------------- ### Initialize Home Page Components Source: https://github.com/wmo-raf/climweb/blob/main/climweb/src/climweb/pages/home/templates/home/home_page.html Handles the initialization of the weather widget and YouTube playlist content on document ready. ```javascript Please try again.') ); } if (loader.length) loader.removeClass('is-active'); if (forecastSkeleton.length) forecastSkeleton.hide(); }); }; /* ── boot ── */ $(document).ready(function () { const isSsrRendered = weatherWidgetWrapper.length && weatherWidgetWrapper.children().length > 0 && !$('.forecast-widget-skeleton').length; if (isSsrRendered) { initWeatherWidget(null); } else if (weatherWidgetWrapper.length && homeWeatherWidgetUrl) { renderWeatherWidgetForCity(); } else { const forecastSkeleton = $('.forecast-widget-skeleton'); if (forecastSkeleton.length) forecastSkeleton.hide(); } if (page_youtube_playlist) { const url = "{{ youtube_playlist_url }}"; const $videoWrap = $('#videoWrapper'); if ($videoWrap.length) { $videoWrap.scheletrone({ onComplete: function () { $.ajax({ url, dataType: 'html', success: function (data) { if (data) { $('#videoWrapper').html('').append(data).ready(); } else { $('#videoSection').remove(); } }, error: function () { $('#videoSection').remove(); }, }); }, }); } } }); ``` -------------------------------- ### Configure Environment Variables Source: https://github.com/wmo-raf/climweb/blob/main/CONTRIBUTING.md Initialize the environment configuration file and set required variables. ```bash cp .env.dev.sample .env ``` ```dotenv DATABASE_URL=postgis://postgres:postgres@localhost:5432/climweb REDIS_HOST=localhost REDIS_PORT=6379 ``` -------------------------------- ### Create and Activate Virtual Environment Source: https://github.com/wmo-raf/climweb/blob/main/CONTRIBUTING.md Set up an isolated Python environment for project dependencies. ```bash python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate ``` -------------------------------- ### Run Migrations Source: https://github.com/wmo-raf/climweb/blob/main/CONTRIBUTING.md Apply database migrations. ```bash climweb migrate ``` -------------------------------- ### Subscriber Form Event Handlers Source: https://github.com/wmo-raf/climweb/blob/main/climweb/src/climweb/pages/email_subscription/templates/subscriber_optin_js.html Handles form interactions for list selection, merge fields, email updates, and interest category management using jQuery. These functions rely on global helper functions to get and set subscriber JSON data. ```javascript $("input[name='{{ widget_name }}']").val(JSON.stringify(json_data)); } $("select[name='list-selection-{{ widget_name }}']").change(function () { var subscriber_data = get_subscriber_json_data_for_{{ widget_js_name }}(); subscriber_data['list_id'] = $(this).val(); set_subscriber_json_data_for_{{ widget_js_name }}(subscriber_data); populate_fields_for_{{ widget_js_name }}($(this).val()); }); $("div[name='merge-fields-{{ widget_name }}']").on('input', 'input', function () { var subscriber_data = get_subscriber_json_data_for_{{ widget_js_name }}(); subscriber_data['merge_fields'][$(this).attr('name')] = $(this).val(); set_subscriber_json_data_for_{{ widget_js_name }}(subscriber_data); }); $("div[name='email-field-{{ widget_name }}']").on('input', 'input', function () { var subscriber_data = get_subscriber_json_data_for_{{ widget_js_name }}(); subscriber_data['email_field'] = $(this).val(); set_subscriber_json_data_for_{{ widget_js_name }}(subscriber_data); }); $("div[name='interest-categories-{{ widget_name }}']").on('change', 'input', function () { var subscriber_data = get_subscriber_json_data_for_{{ widget_js_name }}(); var checked; var category_id = $(this).data("category-id").toString(); var interest_id = $(this).attr("name").toString(); var interest_name = $(this).data("interest-name").toString(); if ($(this).is(':checked')) { checked = true; } else { checked = false; } if (!subscriber_data['interest_categories'].hasOwnProperty(category_id)) { subscriber_data['interest_categories'][category_id] = {}; subscriber_data['interest_categories'][category_id]['interests'] = {}; } if (checked) { if (!subscriber_data['interests_mapping'].hasOwnProperty(interest_id)) { subscriber_data['interests_mapping'][interest_id] = interest_name } } else { delete subscriber_data['interests_mapping'][interest_id] } subscriber_data['interest_categories'][category_id]['interests'][interest_id] = checked; set_subscriber_json_data_for_{{ widget_js_name }}(subscriber_data); } ); ``` -------------------------------- ### Initialize Chart Rendering and Data Fetching Source: https://github.com/wmo-raf/climweb/blob/main/climweb/src/climweb/pages/wdqms/templates/wdqms/report_index.html Sets up initial chart states and triggers data fetching for daily, monthly, and yearly graphs. ```javascript function initializeChartRendering() { // Fetch initial data and render charts $('#year-select').val("{{latest_date|date:'Y'}}"); $('#month-select').val(parseInt("{{latest_date|date:'m'}}").toString()); var selectedMonth = $('#month-select').find(':selected').val() < 10 ? `0${$('#month-select').find(':selected').val()}` : `${$('#month-select').find(':selected').val()}`; monthly_heat_map.on('load', function () { renderMonthlyMap(selectedMonth); }) fetchSynopData('daily-graph', 'column', `{% trans 'Daily ' %} ${$('#variable-select').find(':selected').text()} {% trans ' transmissions for ' %} ${$('#station-select').find(':selected').text()}`, 'daily_synop'); fetchSynopData('monthly-graph', 'column', `{% trans 'Monthly ' %} ${$('#variable-select').find(':selected').text()} {% trans ' transmissions for ' %} ${$('#station-select').find(':selected').text()}`, 'monthly_synop'); fetchSynopData('yearly-graph', 'column', `{% trans 'Yearly ' %} ${$('#variable-select').find(':selected').text()} {% trans ' transmissions for ' %} ${$('#station-select').find(':selected').text()}`, 'yearly_synop'); fetchMonthlyAverages(); fetchYearlyAverages(); } // Initialize chart rendering initializeChartRendering(); ``` -------------------------------- ### Initialize Video and Application Components Source: https://github.com/wmo-raf/climweb/blob/main/climweb/src/climweb/config/templates/services_page_base.html JavaScript logic to conditionally load YouTube video content via AJAX and initialize the Slick slider for applications based on page attributes. ```javascript const has_applications ={{ page.applications|yesno:'true,false' }}; const page_youtube_playlist ={{ page.youtube_playlist|yesno:'true,false' }}; if (page_youtube_playlist) { $('#videoWrapper').scheletrone({ onComplete: function () { $.ajax({ url: '{{ current_site.root_url }}/api/videos/{{ page.youtube_playlist.pk }}', dataType: "html", success: function (data) { // if we have videos append them, else hide whole section if (data.length > 1) { $('#videoWrapper').html('').append(data).ready(); } else { $('#videoSection').hide() } }, error: function () { $('#videoSection').remove() } }); } }); } if (has_applications) { //APPLICATIONS Slider $('.slider').slick({ dots: true, speed: 300, slidesToShow: 4, slidesToScroll: 3, arrows: false, responsive: [ { breakpoint: 1025, settings: { slidesToShow: 3, slidesToScroll: 2, infinite: true } }, { breakpoint: 601, settings: { slidesToShow: 2, slidesToScroll: 2 } }, { breakpoint: 480, settings: { slidesToShow: 1, slidesToScroll: 1, } } ] }); } ``` -------------------------------- ### Initialize Homepage JavaScript Variables Source: https://github.com/wmo-raf/climweb/blob/main/climweb/src/climweb/pages/home/templates/home/home_page.html Sets up global configuration variables for the homepage, including API URLs and map bounds, using Django template tags. ```javascript const homeMapSettingsUrl = "{{ home_map_settings_url|default_if_none:'' }}"; const homeWeatherWidgetUrl = "{{ home_weather_widget_url|default_if_none:'' }}"; const page_youtube_playlist = {{ page.youtube_playlist|yesno:'true,false' }}; const citySearchUrl = "{{ city_search_url }}"; const cityDetailUrl = "{{ city_detail_page_url|default_if_none:'' }}"; const countryBounds = {{ country_bounds|default_if_none:'[]' }}; ``` -------------------------------- ### Initialize MapLibre GL JS and Station Preview Source: https://github.com/wmo-raf/climweb/blob/main/climweb/src/climweb/pages/stations/templates/stations/stations_preview.html Initializes the map with custom tile sources and handles station data updates via form inputs. ```javascript const mapConfigObj = {{ mapConfig | safe }}; const {stationBounds, stationsVectorTilesUrl} = mapConfigObj const bounds = [[stationBounds[0], stationBounds[1]], [stationBounds[2], stationBounds[3]]] const defaultStyle = { 'version': 8, 'sources': { 'carto-dark': { 'type': 'raster', 'tiles': [ "https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png", "https://b.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png", "https://c.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png", "https://d.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}@2x.png" ] }, 'carto-light': { 'type': 'raster', 'tiles': [ "https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png", "https://b.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png", "https://c.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png", "https://d.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png" ] }, 'wikimedia': { 'type': 'raster', 'tiles': [ "https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png" ] } }, 'layers': [{ 'id': 'carto-light-layer', 'source': 'carto-light', 'type': 'raster', 'minzoom': 0, 'maxzoom': 22 }] } $(document).ready(async function () { const $columnsInput = $("#id_columns") const map = new maplibregl.Map({ container: "preview-map", style: defaultStyle, doubleClickZoom: false, }); await new Promise((resolve) => map.on("load", resolve)); if (bounds) { map.fitBounds(bounds, {padding: 20}) } // add source map.addSource("stations-source", { type: "vector", tiles: [stationsVectorTilesUrl], } ) // add layer map.addLayer({ 'id': 'stations-circle', 'type': 'circle', 'source': 'stations-source', "source-layer": "default", 'paint': { 'circle-color': "red", 'circle-radius': 10, } }); const $stationLabelInput = $(".station_input") $stationLabelInput.on("change", function () { const $this = $(this) const inputType = $this.attr('type') const columnName = $this.data("column") const updateType = $this.data("updatetype") if (columnName && updateType) { let inputValue = $this.val() if (inputType === "checkbox") { inputValue = $this.is(':checked') } updateColumnsData(columnName, updateType, inputValue) } }) const getColumnsData = () => { let columnsData = $columnsInput.val() if (columnsData) { columnsData = JSON.parse(columnsData) return columnsData } return [] } const getPopupFields = () => { const allFields = getColumnsData() return allFields.filter(c => c && c.popup) } const updateColumnsData = (columnName, updateType, value) => { let columnsData = getColumnsData() columnsData = columnsData.reduce((all, item) => { if (item.name && item.name === columnName) { item[updateType] = value } all.push(item) return all }, []) if (columnsData) { $columnsInput.val(JSON.stringify(columnsData)) } } function featureHtml(f) { const p = f.properties; const popupFields = getPopupFields() const popupProps = Object.keys(p).reduce((all, key) => { if (popupFields.find(f => f.name === key)) { all[key] = p[key] } return all }, {}) if (popupProps && !!Object.keys(popupProps).length) { let h = "
" + `${column.label ? column.label : k}` + ": " + popupProps[k] + "