From b84fd64aa7906436aa9505833ee7b6c8837bbdb7 Mon Sep 17 00:00:00 2001 From: Brian Clark Date: Thu, 1 May 2025 12:24:55 -0400 Subject: [PATCH 01/10] Add Actions workflow for building container images --- .github/workflows/build-and-deploy.yaml | 60 +++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/build-and-deploy.yaml diff --git a/.github/workflows/build-and-deploy.yaml b/.github/workflows/build-and-deploy.yaml new file mode 100644 index 0000000..551c3b7 --- /dev/null +++ b/.github/workflows/build-and-deploy.yaml @@ -0,0 +1,60 @@ +name: 'Build and deploy application containers' +on: + push: +jobs: + build-tag-push-deploy: + runs-on: ubuntu-latest + # CI/CD will run on these branches + if: > + github.ref == 'refs/heads/main' || + github.ref == 'refs/heads/staging' || + github.ref == 'refs/heads/development' + + strategy: + matrix: + # Specify the docker-compose services to build images from. These should match the service + # names in the docker-compose.yml file. + service: [epwebapp, epnginx] + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Login to GitHub container registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: cmu-delphi-deploy-machine + password: ${{ secrets.CMU_DELPHI_DEPLOY_MACHINE_PAT }} + - name: Create container image tags + id: image-tag + run: | + baseRef="${GITHUB_REF#*/}" + baseRef="${baseRef#*/}" + case "${baseRef}" in + main) + image_tag="latest" + ;; + *) + image_tag="${baseRef//\//_}" # replace `/` with `_` in branch name + ;; + esac + echo "IMAGE_TAG=${image_tag}" >> $GITHUB_OUTPUT + - name: Copy env file + run: | + cp ./.ci.env ./.env + - name: Set up docker-compose + uses: ndeloof/install-compose-action@v0.0.1 + - name: docker-compose build --push + run: | + docker-compose build --push ${{ matrix.service }} + env: + TAG: ":${{ steps.image-tag.outputs.IMAGE_TAG }}" + REGISTRY: "ghcr.io/${{ github.repository_owner }}/" + - name: docker-compose down + run: | + docker-compose down + - name: Trigger smee.io webhook to pull new container images + run: | + curl -H "Authorization: Bearer ${{ secrets.DELPHI_DEPLOY_WEBHOOK_TOKEN }}" \ + -X POST ${{ secrets.DELPHI_DEPLOY_WEBHOOK_URL }} \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "repository=ghcr.io/${{ github.repository }}-${{ matrix.service }}&tag=${{ steps.image-tag.outputs.IMAGE_TAG }}" From c0c6d7a111865e8678d07260cd1165c070cb66e2 Mon Sep 17 00:00:00 2001 From: Brian Clark Date: Thu, 1 May 2025 13:20:46 -0400 Subject: [PATCH 02/10] Switch to camel case file name --- src/staticfiles/js/indicatorsetstable.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/staticfiles/js/indicatorsetstable.js b/src/staticfiles/js/indicatorsetstable.js index 3c3edf1..a59472f 100644 --- a/src/staticfiles/js/indicatorsetstable.js +++ b/src/staticfiles/js/indicatorsetstable.js @@ -103,3 +103,5 @@ function format(indicatorSetId, relatedIndicators, indicatorSetDescription) { } return data; } + + From 3aedc8690a8346750dad12493dcb77078c910edd Mon Sep 17 00:00:00 2001 From: Brian Clark Date: Thu, 1 May 2025 13:25:37 -0400 Subject: [PATCH 03/10] Add .ci.env --- .ci.env | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .ci.env diff --git a/.ci.env b/.ci.env new file mode 100644 index 0000000..9648744 --- /dev/null +++ b/.ci.env @@ -0,0 +1,22 @@ +# This file is used to allow CI to start the compose services. It will typcially +# not need to be modified. + +MYSQL_DATABASE=mysql_database +MYSQL_USER=mysql_user +MYSQL_PASSWORD=mysql_password +MYSQL_PORT=3306 +MYSQL_ROOT_PASSWORD=test123! +MYSQL_HOST=db + +ALLOWED_HOSTS='127.0.0.1,localhost' +CORS_ORIGIN_WHITELIST='http://127.0.0.1:3000,http://localhost:3000' +CSRF_TRUSTED_ORIGINS='http://127.0.0.1:8000,http://localhost:8000' + +SECRET_KEY='secret_key' +DEBUG='True' + +# Add the following to your local .env file. They will be used in the CI process +# and you can largely forget about them, but including them in your .env file +# will act like a safe default and help suppress warnings. +REGISTRY="" +TAG="" From 07880e14431ea45e13860c3a6f02a4aec32e3ee5 Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Fri, 2 May 2025 23:30:23 +0300 Subject: [PATCH 04/10] Removed old static files --- src/staticfiles/js/indicatorsets_filters.js | 31 -- src/staticfiles/js/indicatorsetstable.js | 107 ------ src/staticfiles/js/select_signals.js | 92 ----- src/staticfiles/js/signal_sets.js | 354 -------------------- 4 files changed, 584 deletions(-) delete mode 100644 src/staticfiles/js/indicatorsets_filters.js delete mode 100644 src/staticfiles/js/indicatorsetstable.js delete mode 100644 src/staticfiles/js/select_signals.js delete mode 100644 src/staticfiles/js/signal_sets.js diff --git a/src/staticfiles/js/indicatorsets_filters.js b/src/staticfiles/js/indicatorsets_filters.js deleted file mode 100644 index da1259a..0000000 --- a/src/staticfiles/js/indicatorsets_filters.js +++ /dev/null @@ -1,31 +0,0 @@ -// Add an event listener to each 'bulk-select' element -let bulkSelectDivs = document.querySelectorAll('.bulk-select'); -bulkSelectDivs.forEach(div => { - div.addEventListener('click', function(event) { - let form = this.nextElementSibling; - let showMoreLink = form.querySelector('a'); - let checkboxes = form.querySelectorAll('input[type="checkbox"]'); - - if (event.target.checked === true) { - checkboxes.forEach((checkbox, index) => { - checkbox.checked = true; - if (index > 4) { - checkbox.parentElement.style.display = checkbox.parentElement.style.display === 'none' ? 'block' : null; - } - }) - if (showMoreLink) { - showMoreLink.innerText = 'Show less...'; - } - } else if (event.target.checked === false) { - checkboxes.forEach((checkbox, index) => { - checkbox.checked = false - if (index > 4) { - checkbox.parentElement.style.display = checkbox.parentElement.style.display === 'block' ? 'none' : null; - } - }); - if (showMoreLink) { - showMoreLink.innerText = 'Show more...'; - } - } - }); -}); \ No newline at end of file diff --git a/src/staticfiles/js/indicatorsetstable.js b/src/staticfiles/js/indicatorsetstable.js deleted file mode 100644 index a59472f..0000000 --- a/src/staticfiles/js/indicatorsetstable.js +++ /dev/null @@ -1,107 +0,0 @@ -function calculate_table_height() { - var h = Math.max( - document.documentElement.clientHeight, - window.innerHeight || 0 - ); - var percent = 60; - if (h > 1000) { - percent = 70; - } - return (percent * h) / 100; -} - -var table = new DataTable("#indicatorSetsTable", { - fixedHeader: true, - searching: false, - paging: false, - scrollCollapse: true, - scrollX: true, - scrollY: calculate_table_height() + 75, - info: false, - fixedColumns: { - left: 2, - }, - ordering: false, - mark: true, - - language: { - buttons: { - colvis: "Toggle Columns", - }, - }, -}); - -new DataTable.Buttons(table, { - buttons: [ - { - extend: "colvis", - columns: "th:nth-child(n+3)", - prefixButtons: ["colvisRestore"], - }, - ], -}); - -table.buttons(0, null).container().appendTo("#colvis"); - -function format(indicatorSetId, relatedIndicators, indicatorSetDescription) { - console.lopg; - var indicators = relatedIndicators.filter( - (indicator) => indicator.indicator_set === indicatorSetId - ); - var disabled, restricted; - - if (indicators.length > 0) { - var data = `

${indicatorSetDescription}

`; - var tableMarkup = - '' + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - ""; - indicators.forEach((indicator) => { - checked = checkedIndicatorMembers.filter( - (obj) => - obj.data_source == indicator.source && - obj.indicator == indicator.name - ).length; - var checkboxTitle = ""; - checked = checked ? "checked" : ""; - disabled = indicator.endpoint ? "" : "disabled"; - var restricted = indicator.restricted != "No"; - if (disabled === "disabled") { - checkboxTitle = - "Visualization functionality for this endpoint is coming soon."; - } - if (restricted) { - disabled = "disabled"; - checkboxTitle = - "Access to this data source is restricted. Contact delphi-support@andrew.cmu.edu for more information."; - } - tableMarkup += - "" + - `` + - `` + - `` + - `` + - '' + - ""; - }); - tableMarkup += "
Indicator NameIndicator API NameIndicator Description
${indicator.display_name}${indicator.member_name}${indicator.member_description}
"; - if (disabled === "disabled" || restricted) { - data += - `"; - } - data += tableMarkup; - } else { - data = "

No available indicators yet.

"; - } - return data; -} - - diff --git a/src/staticfiles/js/select_signals.js b/src/staticfiles/js/select_signals.js deleted file mode 100644 index 02cb73b..0000000 --- a/src/staticfiles/js/select_signals.js +++ /dev/null @@ -1,92 +0,0 @@ -// Function to update the modal content -function updateSelectedSignalsModal() { - var selectedSignals = localStorage.getItem('selectedSignals'); - var signals = selectedSignals ? JSON.parse(selectedSignals) : {}; - - var selectedSignalsList = document.getElementById('selectedSignalsList'); - selectedSignalsList.innerHTML = ''; // Clear existing items - - - for (const signal in signals) { - let data = JSON.parse(localStorage.getItem("selectedSignals"))[signal]; - console.log(data); - var tr = document.createElement('tr'); - var memberName = document.createElement('td'); - memberName.textContent = data['info']['memberName']; - tr.appendChild(memberName); - var memberDescription = document.createElement('td'); - memberDescription.textContent = data['info']['memberDescription']; - tr.appendChild(memberDescription); - var dataSource = document.createElement('td'); - dataSource.textContent = data['epivis']['params']['data_source']; - tr.appendChild(dataSource); - var signalName = document.createElement('td'); - signalName.textContent = data['epivis']['params']['signal']; - tr.appendChild(signalName); - var timeType = document.createElement('td'); - timeType.textContent = data['epivis']['params']['time_type']; - tr.appendChild(timeType); - var geoType = document.createElement('td'); - geoType.textContent = data['epivis']['params']['geo_type']; - tr.appendChild(geoType); - var geoValue = document.createElement('td'); - geoValue.textContent = data['epivis']['params']['geo_value']; - tr.appendChild(geoValue); - selectedSignalsList.appendChild(tr); - } -} - -function addSelectedSignals(dataSource, timeType, signalSetEndpoint) { - let selectedSignals = localStorage.getItem("selectedSignals"); - selectedSignals = selectedSignals ? JSON.parse(selectedSignals) : {}; - var dataSignals = Array.from(document.getElementsByName('selectedSignal'), (signal) => (signal.checked) ? signal : null).filter((el) => el !== null); - var geographicType = document.getElementById('geographic_type').value; - // geographicValue is a comma separated string of geographic values. type can be string or integer - // in case of string, it should be converted to lowercase - // else it will be treated as integer - var geographicValue = $('#geographic_value').select2('data').map((el) => (typeof el.id === 'string') ? el.id.toLowerCase() : el.id).join(','); - - if (geographicType === 'Choose...' || geographicValue === '') { - showWarningAlert("Geographic Type or Geographic Value is not selected."); - return; - } - - var geographicValues = geographicValue.split(','); - dataSignals.forEach((signal) => { - geographicValues.forEach((geographicValue) => { - selectedSignals[`${signal.value}_${geographicValue}`] = { - info: { - memberName: signal.getAttribute('data-member-name'), - memberDescription: signal.getAttribute('data-member-description'), - }, - epivis: { - color: '#' + (Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0'), - title: "value", - params: { - _endpoint: signalSetEndpoint, - data_source: dataSource, - signal: signal.value, - time_type: timeType, - geo_type: geographicType, - geo_value: geographicValue - } - } - }; - }); - }); - localStorage.setItem("selectedSignals", JSON.stringify(selectedSignals)); - updateSelectedSignalsModal(); - $("#showSelectedSignalsButton").show(); -} - - - -document.addEventListener('DOMContentLoaded', function() { - // Call the function to update the modal content when the page loads - updateSelectedSignalsModal(); - -}); - - - -//for (const signal in JSON.parse(localStorage.getItem("selectedSignals"))) {console.log(JSON.parse(localStorage.getItem("selectedSignals"))[signal])} \ No newline at end of file diff --git a/src/staticfiles/js/signal_sets.js b/src/staticfiles/js/signal_sets.js deleted file mode 100644 index e4760e2..0000000 --- a/src/staticfiles/js/signal_sets.js +++ /dev/null @@ -1,354 +0,0 @@ -const indicatorHandler = new IndicatorHandler(); - -function initSelect2(elementId, data) { - $(`#${elementId}`).select2({ - data: data, - minimumInputLength: 0, - maximumSelectionLength: 5, - }); -} - -let checkedSignalMembers = [] - -function showWarningAlert(warningMessage, slideUpTime = 2000) { - $("#warning-alert").html(warningMessage); - $("#warning-alert").fadeTo(2000, 500).slideUp(slideUpTime, function() { - $("#warning-alert").slideUp(slideUpTime); - }); -} - -async function checkGeoCoverage(geoType, geoValue) { - const notCoveredSignals = []; - - try { - const result = await $.ajax({ - url: "epidata/covidcast/geo_coverage/", - type: 'GET', - data: { - 'geo': `${geoType}:${geoValue}` - } - }); - - checkedSignalMembers.filter(signal => signal["_endpoint"] === "covidcast").forEach(signal => { - const covered = result["epidata"].some( - e => (e.source === signal.data_source && e.signal === signal.signal) - ); - if (!covered) {s - notCoveredSignals.push(signal); - } - }); - - return notCoveredSignals; - } catch (error) { - console.error('Error fetching geo coverage:', error); - return notCoveredSignals; - } -} - -// Function to update the modal content -function updateSelectedSignals(dataSource, signalDisplayName, signalSet, signal) { - var selectedSignalsList = document.getElementById('selectedSignalsList'); - - var tr = document.createElement('tr'); - tr.setAttribute('id', `${dataSource}_${signal}`); - var signalSetName = document.createElement('td'); - signalSetName.textContent = signalSet; - tr.appendChild(signalSetName); - var signalName = document.createElement('td'); - signalName.textContent = signalDisplayName; - tr.appendChild(signalName); - selectedSignalsList.appendChild(tr); -} - -function addSelectedSignal(element) { - if (element.checked) { - checkedSignalMembers.push({ - _endpoint: element.dataset.endpoint, - data_source: element.dataset.datasource, - signal: element.dataset.signal, - time_type: element.dataset.timeType, - signal_set: element.dataset.signalSet, - display_name: element.dataset.signalDisplayname, - signal_set_short_name: element.dataset.signalSetShortName, - member_short_name: element.dataset.memberShortName - }); - updateSelectedSignals(element.dataset.datasource, element.dataset.signalDisplayname, element.dataset.signalSet, element.dataset.signal); - } else { - checkedSignalMembers = checkedSignalMembers.filter(signal => signal.signal !== element.dataset.signal); - document.getElementById(`${element.dataset.datasource}_${element.dataset.signal}`).remove(); - } - - indicatorHandler.indicators = checkedSignalMembers; - - if (checkedSignalMembers.length > 0) { - $("#showSelectedSignalsButton").show(); - } else { - $("#showSelectedSignalsButton").hide(); - } -} - -$("#showSelectedSignalsButton").click(function() { - alertPlaceholder.innerHTML = ""; - if (!indicatorHandler.checkForCovidcastIndicators()) { - $("#geographic_value").prop("disabled", true); - } else { - $("#geographic_value").prop("disabled", false); - } - $('#geographic_value').select2("data").forEach(geo => { - checkGeoCoverage(geo.geoType, geo.id).then((notCoveredSignals) => { - if (notCoveredSignals.length > 0) { - showNotCoveredGeoWarningMessage(notCoveredSignals, geo.text); - } - }) - }); - var otherEndpointLocationsWarning = `` - if (indicatorHandler.getFluviewIndicators().length > 0) { - $("#differentLocationNote").html(otherEndpointLocationsWarning) - if (document.getElementsByName("fluviewRegions").length === 0) { - indicatorHandler.showFluviewRegions(); - } - } -}); - -// Add an event listener to each 'bulk-select' element -let bulkSelectDivs = document.querySelectorAll('.bulk-select'); -bulkSelectDivs.forEach(div => { - div.addEventListener('click', function(event) { - let form = this.nextElementSibling; - let showMoreLink = form.querySelector('a'); - let checkboxes = form.querySelectorAll('input[type="checkbox"]'); - - if (event.target.checked === true) { - checkboxes.forEach((checkbox, index) => { - checkbox.checked = true; - if (index > 4) { - checkbox.parentElement.style.display = checkbox.parentElement.style.display === 'none' ? 'block' : null; - } - }) - if (showMoreLink) { - showMoreLink.innerText = 'Show less...'; - } - } else if (event.target.checked === false) { - checkboxes.forEach((checkbox, index) => { - checkbox.checked = false - if (index > 4) { - checkbox.parentElement.style.display = checkbox.parentElement.style.display === 'block' ? 'none' : null; - } - }); - if (showMoreLink) { - showMoreLink.innerText = 'Show more...'; - } - } - }); -}); - -var tableHeight = window.screen.width / 3.4; - -function calculate_table_height() { - var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); - var percent = 60; - if (h > 1000) { - percent = 70; - } - return (percent * h) / 100; -} - - -var table = new DataTable('#signalSetsTable', { - fixedHeader: true, - searching: false, - paging: false, - scrollCollapse: true, - scrollX: true, - scrollY: calculate_table_height() + 75, - info: false, - fixedColumns: { - left: 2 - }, - ordering: false, - mark: true, - - language: { - buttons: { - colvis: "Toggle Columns" - } - } -}); - - -new DataTable.Buttons(table, { - buttons: [ - { - extend: 'colvis', - columns: 'th:nth-child(n+3)', - prefixButtons: ['colvisRestore'] - } - ] -}); - -table - .buttons(0, null) - .container() - .appendTo("#colvis"); - - -function format (signalSetId, relatedSignals, signalSetDescription) { - var signals = relatedSignals.filter((signal) => signal.signal_set === signalSetId) - var disabled, restricted; - - if (signals.length > 0) { - var data = `

${signalSetDescription}

` - var tableMarkup = ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - '' - signals.forEach((signal) => { - checked = checkedSignalMembers.filter((obj) => obj.data_source == signal.source && obj.signal == signal.name).length; - var checkboxTitle = "" - checked = checked ? "checked" : "" - disabled = signal.endpoint ? "" : "disabled"; - var restricted = signal.restricted != "No"; - if (disabled === "disabled") { - checkboxTitle = "Visualization functionality for this endpoint is coming soon." - } - if (restricted) { - disabled = "disabled"; - checkboxTitle = "Access to this data source is restricted. Contact delphi-support@andrew.cmu.edu for more information." - } - tableMarkup += ''+ - ``+ - ``+ - ``+ - ``+ - ''+ - '' - }) - tableMarkup += '
Indicator NameIndicator API NameIndicator Description
${signal.display_name}${signal.member_name}${signal.member_description}
' - if (disabled === "disabled" || restricted) { - data += `' - } - data += tableMarkup; - } else { - data = "

No available indicators yet.

" - } - return data; -} - -// Plot/Export/Preview data block - -var currentMode = 'epivis'; - - -function handleModeChange(mode) { - $('#modeSubmitResult').html(''); - - var choose_dates = document.getElementsByName('choose_date'); - - if (mode === 'epivis') { - currentMode = 'epivis'; - choose_dates.forEach((el) => { - el.style.display = 'none'; - }); - $('#modeSubmitResult').html(''); - } else if (mode === 'export') { - currentMode = 'export'; - choose_dates.forEach((el) => { - el.style.display = 'flex'; - }); - $('#modeSubmitResult').html(''); - } else { - currentMode = 'preview'; - choose_dates.forEach((el) => { - el.style.display = 'flex'; - }); - } - document.getElementsByName("modes").forEach((el) => { - if (currentMode === el.value) { - el.checked = true; - } - }); -} - -function hideAlert(alertId) { - const alert = document.getElementById(alertId); - if (alert) { - alert.remove(); - } -} - - - -const alertPlaceholder = document.getElementById('warning-alert') -const appendAlert = (message, type) => { - const wrapper = document.createElement('div') - const alertId = `alert-${Date.now()}`; - wrapper.innerHTML = [ - `' - ].join('') - - alertPlaceholder.append(wrapper) - wrapper.getElementsByClassName('btn-close')[0].addEventListener('click', () => hideAlert(alertId)) - } - -function showNotCoveredGeoWarningMessage(notCoveredSignals, geoValue) { - var warningMessage = ""; - notCoveredSignals.forEach(signal => { - if (currentMode === 'epivis') { - warningMessage += `Indicator ${signal.display_name} is not available for Location ${geoValue}
` - } else { - var startDate = document.getElementById("start_date").value; - var endDate = document.getElementById("end_date").value; - warningMessage += `Indicator ${signal.display_name} is not available for Location ${geoValue} for the time period from ${startDate} to ${endDate}
` - } - }) - appendAlert(warningMessage, "warning") -} - -$('#geographic_value').on('select2:select', function (e) { - var geo = e.params.data; - checkGeoCoverage(geo.geoType, geo.id).then((notCoveredSignals) => { - if (notCoveredSignals.length > 0) { - showNotCoveredGeoWarningMessage(notCoveredSignals, geo.text); - } - } - ); -}); - - -function submitMode(event) { - event.preventDefault(); - var geographicValues = $('#geographic_value').select2('data'); - if (indicatorHandler.checkForCovidcastIndicators()) { - if (geographicValues.length === 0) { - appendAlert("Please select at least one geographic location", "warning") - return; - } - } - - if (currentMode === 'epivis') { - indicatorHandler.plotData(); - } else if (currentMode === 'export') { - indicatorHandler.exportData(); - } else { - indicatorHandler.previewData(); - } -} - -const isPlural = num => Math.abs(num) !== 1; -const simplePlural = word => `${word}s`; -const pluralize = (num, word, plural = simplePlural) => - isPlural(num) ? plural(word) : word; \ No newline at end of file From 244e730f6eaf16888900ce5eea3ac257e861c1aa Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Fri, 2 May 2025 23:31:12 +0300 Subject: [PATCH 05/10] Removed old static files --- src/staticfiles/js/indicatorsets_filters.js | 31 -- src/staticfiles/js/indicatorsetstable.js | 107 ------ src/staticfiles/js/select_signals.js | 92 ----- src/staticfiles/js/signal_sets.js | 354 -------------------- 4 files changed, 584 deletions(-) delete mode 100644 src/staticfiles/js/indicatorsets_filters.js delete mode 100644 src/staticfiles/js/indicatorsetstable.js delete mode 100644 src/staticfiles/js/select_signals.js delete mode 100644 src/staticfiles/js/signal_sets.js diff --git a/src/staticfiles/js/indicatorsets_filters.js b/src/staticfiles/js/indicatorsets_filters.js deleted file mode 100644 index da1259a..0000000 --- a/src/staticfiles/js/indicatorsets_filters.js +++ /dev/null @@ -1,31 +0,0 @@ -// Add an event listener to each 'bulk-select' element -let bulkSelectDivs = document.querySelectorAll('.bulk-select'); -bulkSelectDivs.forEach(div => { - div.addEventListener('click', function(event) { - let form = this.nextElementSibling; - let showMoreLink = form.querySelector('a'); - let checkboxes = form.querySelectorAll('input[type="checkbox"]'); - - if (event.target.checked === true) { - checkboxes.forEach((checkbox, index) => { - checkbox.checked = true; - if (index > 4) { - checkbox.parentElement.style.display = checkbox.parentElement.style.display === 'none' ? 'block' : null; - } - }) - if (showMoreLink) { - showMoreLink.innerText = 'Show less...'; - } - } else if (event.target.checked === false) { - checkboxes.forEach((checkbox, index) => { - checkbox.checked = false - if (index > 4) { - checkbox.parentElement.style.display = checkbox.parentElement.style.display === 'block' ? 'none' : null; - } - }); - if (showMoreLink) { - showMoreLink.innerText = 'Show more...'; - } - } - }); -}); \ No newline at end of file diff --git a/src/staticfiles/js/indicatorsetstable.js b/src/staticfiles/js/indicatorsetstable.js deleted file mode 100644 index a59472f..0000000 --- a/src/staticfiles/js/indicatorsetstable.js +++ /dev/null @@ -1,107 +0,0 @@ -function calculate_table_height() { - var h = Math.max( - document.documentElement.clientHeight, - window.innerHeight || 0 - ); - var percent = 60; - if (h > 1000) { - percent = 70; - } - return (percent * h) / 100; -} - -var table = new DataTable("#indicatorSetsTable", { - fixedHeader: true, - searching: false, - paging: false, - scrollCollapse: true, - scrollX: true, - scrollY: calculate_table_height() + 75, - info: false, - fixedColumns: { - left: 2, - }, - ordering: false, - mark: true, - - language: { - buttons: { - colvis: "Toggle Columns", - }, - }, -}); - -new DataTable.Buttons(table, { - buttons: [ - { - extend: "colvis", - columns: "th:nth-child(n+3)", - prefixButtons: ["colvisRestore"], - }, - ], -}); - -table.buttons(0, null).container().appendTo("#colvis"); - -function format(indicatorSetId, relatedIndicators, indicatorSetDescription) { - console.lopg; - var indicators = relatedIndicators.filter( - (indicator) => indicator.indicator_set === indicatorSetId - ); - var disabled, restricted; - - if (indicators.length > 0) { - var data = `

${indicatorSetDescription}

`; - var tableMarkup = - '' + - "" + - "" + - "" + - "" + - "" + - "" + - "" + - ""; - indicators.forEach((indicator) => { - checked = checkedIndicatorMembers.filter( - (obj) => - obj.data_source == indicator.source && - obj.indicator == indicator.name - ).length; - var checkboxTitle = ""; - checked = checked ? "checked" : ""; - disabled = indicator.endpoint ? "" : "disabled"; - var restricted = indicator.restricted != "No"; - if (disabled === "disabled") { - checkboxTitle = - "Visualization functionality for this endpoint is coming soon."; - } - if (restricted) { - disabled = "disabled"; - checkboxTitle = - "Access to this data source is restricted. Contact delphi-support@andrew.cmu.edu for more information."; - } - tableMarkup += - "" + - `` + - `` + - `` + - `` + - '' + - ""; - }); - tableMarkup += "
Indicator NameIndicator API NameIndicator Description
${indicator.display_name}${indicator.member_name}${indicator.member_description}
"; - if (disabled === "disabled" || restricted) { - data += - `"; - } - data += tableMarkup; - } else { - data = "

No available indicators yet.

"; - } - return data; -} - - diff --git a/src/staticfiles/js/select_signals.js b/src/staticfiles/js/select_signals.js deleted file mode 100644 index 02cb73b..0000000 --- a/src/staticfiles/js/select_signals.js +++ /dev/null @@ -1,92 +0,0 @@ -// Function to update the modal content -function updateSelectedSignalsModal() { - var selectedSignals = localStorage.getItem('selectedSignals'); - var signals = selectedSignals ? JSON.parse(selectedSignals) : {}; - - var selectedSignalsList = document.getElementById('selectedSignalsList'); - selectedSignalsList.innerHTML = ''; // Clear existing items - - - for (const signal in signals) { - let data = JSON.parse(localStorage.getItem("selectedSignals"))[signal]; - console.log(data); - var tr = document.createElement('tr'); - var memberName = document.createElement('td'); - memberName.textContent = data['info']['memberName']; - tr.appendChild(memberName); - var memberDescription = document.createElement('td'); - memberDescription.textContent = data['info']['memberDescription']; - tr.appendChild(memberDescription); - var dataSource = document.createElement('td'); - dataSource.textContent = data['epivis']['params']['data_source']; - tr.appendChild(dataSource); - var signalName = document.createElement('td'); - signalName.textContent = data['epivis']['params']['signal']; - tr.appendChild(signalName); - var timeType = document.createElement('td'); - timeType.textContent = data['epivis']['params']['time_type']; - tr.appendChild(timeType); - var geoType = document.createElement('td'); - geoType.textContent = data['epivis']['params']['geo_type']; - tr.appendChild(geoType); - var geoValue = document.createElement('td'); - geoValue.textContent = data['epivis']['params']['geo_value']; - tr.appendChild(geoValue); - selectedSignalsList.appendChild(tr); - } -} - -function addSelectedSignals(dataSource, timeType, signalSetEndpoint) { - let selectedSignals = localStorage.getItem("selectedSignals"); - selectedSignals = selectedSignals ? JSON.parse(selectedSignals) : {}; - var dataSignals = Array.from(document.getElementsByName('selectedSignal'), (signal) => (signal.checked) ? signal : null).filter((el) => el !== null); - var geographicType = document.getElementById('geographic_type').value; - // geographicValue is a comma separated string of geographic values. type can be string or integer - // in case of string, it should be converted to lowercase - // else it will be treated as integer - var geographicValue = $('#geographic_value').select2('data').map((el) => (typeof el.id === 'string') ? el.id.toLowerCase() : el.id).join(','); - - if (geographicType === 'Choose...' || geographicValue === '') { - showWarningAlert("Geographic Type or Geographic Value is not selected."); - return; - } - - var geographicValues = geographicValue.split(','); - dataSignals.forEach((signal) => { - geographicValues.forEach((geographicValue) => { - selectedSignals[`${signal.value}_${geographicValue}`] = { - info: { - memberName: signal.getAttribute('data-member-name'), - memberDescription: signal.getAttribute('data-member-description'), - }, - epivis: { - color: '#' + (Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0'), - title: "value", - params: { - _endpoint: signalSetEndpoint, - data_source: dataSource, - signal: signal.value, - time_type: timeType, - geo_type: geographicType, - geo_value: geographicValue - } - } - }; - }); - }); - localStorage.setItem("selectedSignals", JSON.stringify(selectedSignals)); - updateSelectedSignalsModal(); - $("#showSelectedSignalsButton").show(); -} - - - -document.addEventListener('DOMContentLoaded', function() { - // Call the function to update the modal content when the page loads - updateSelectedSignalsModal(); - -}); - - - -//for (const signal in JSON.parse(localStorage.getItem("selectedSignals"))) {console.log(JSON.parse(localStorage.getItem("selectedSignals"))[signal])} \ No newline at end of file diff --git a/src/staticfiles/js/signal_sets.js b/src/staticfiles/js/signal_sets.js deleted file mode 100644 index e4760e2..0000000 --- a/src/staticfiles/js/signal_sets.js +++ /dev/null @@ -1,354 +0,0 @@ -const indicatorHandler = new IndicatorHandler(); - -function initSelect2(elementId, data) { - $(`#${elementId}`).select2({ - data: data, - minimumInputLength: 0, - maximumSelectionLength: 5, - }); -} - -let checkedSignalMembers = [] - -function showWarningAlert(warningMessage, slideUpTime = 2000) { - $("#warning-alert").html(warningMessage); - $("#warning-alert").fadeTo(2000, 500).slideUp(slideUpTime, function() { - $("#warning-alert").slideUp(slideUpTime); - }); -} - -async function checkGeoCoverage(geoType, geoValue) { - const notCoveredSignals = []; - - try { - const result = await $.ajax({ - url: "epidata/covidcast/geo_coverage/", - type: 'GET', - data: { - 'geo': `${geoType}:${geoValue}` - } - }); - - checkedSignalMembers.filter(signal => signal["_endpoint"] === "covidcast").forEach(signal => { - const covered = result["epidata"].some( - e => (e.source === signal.data_source && e.signal === signal.signal) - ); - if (!covered) {s - notCoveredSignals.push(signal); - } - }); - - return notCoveredSignals; - } catch (error) { - console.error('Error fetching geo coverage:', error); - return notCoveredSignals; - } -} - -// Function to update the modal content -function updateSelectedSignals(dataSource, signalDisplayName, signalSet, signal) { - var selectedSignalsList = document.getElementById('selectedSignalsList'); - - var tr = document.createElement('tr'); - tr.setAttribute('id', `${dataSource}_${signal}`); - var signalSetName = document.createElement('td'); - signalSetName.textContent = signalSet; - tr.appendChild(signalSetName); - var signalName = document.createElement('td'); - signalName.textContent = signalDisplayName; - tr.appendChild(signalName); - selectedSignalsList.appendChild(tr); -} - -function addSelectedSignal(element) { - if (element.checked) { - checkedSignalMembers.push({ - _endpoint: element.dataset.endpoint, - data_source: element.dataset.datasource, - signal: element.dataset.signal, - time_type: element.dataset.timeType, - signal_set: element.dataset.signalSet, - display_name: element.dataset.signalDisplayname, - signal_set_short_name: element.dataset.signalSetShortName, - member_short_name: element.dataset.memberShortName - }); - updateSelectedSignals(element.dataset.datasource, element.dataset.signalDisplayname, element.dataset.signalSet, element.dataset.signal); - } else { - checkedSignalMembers = checkedSignalMembers.filter(signal => signal.signal !== element.dataset.signal); - document.getElementById(`${element.dataset.datasource}_${element.dataset.signal}`).remove(); - } - - indicatorHandler.indicators = checkedSignalMembers; - - if (checkedSignalMembers.length > 0) { - $("#showSelectedSignalsButton").show(); - } else { - $("#showSelectedSignalsButton").hide(); - } -} - -$("#showSelectedSignalsButton").click(function() { - alertPlaceholder.innerHTML = ""; - if (!indicatorHandler.checkForCovidcastIndicators()) { - $("#geographic_value").prop("disabled", true); - } else { - $("#geographic_value").prop("disabled", false); - } - $('#geographic_value').select2("data").forEach(geo => { - checkGeoCoverage(geo.geoType, geo.id).then((notCoveredSignals) => { - if (notCoveredSignals.length > 0) { - showNotCoveredGeoWarningMessage(notCoveredSignals, geo.text); - } - }) - }); - var otherEndpointLocationsWarning = `` - if (indicatorHandler.getFluviewIndicators().length > 0) { - $("#differentLocationNote").html(otherEndpointLocationsWarning) - if (document.getElementsByName("fluviewRegions").length === 0) { - indicatorHandler.showFluviewRegions(); - } - } -}); - -// Add an event listener to each 'bulk-select' element -let bulkSelectDivs = document.querySelectorAll('.bulk-select'); -bulkSelectDivs.forEach(div => { - div.addEventListener('click', function(event) { - let form = this.nextElementSibling; - let showMoreLink = form.querySelector('a'); - let checkboxes = form.querySelectorAll('input[type="checkbox"]'); - - if (event.target.checked === true) { - checkboxes.forEach((checkbox, index) => { - checkbox.checked = true; - if (index > 4) { - checkbox.parentElement.style.display = checkbox.parentElement.style.display === 'none' ? 'block' : null; - } - }) - if (showMoreLink) { - showMoreLink.innerText = 'Show less...'; - } - } else if (event.target.checked === false) { - checkboxes.forEach((checkbox, index) => { - checkbox.checked = false - if (index > 4) { - checkbox.parentElement.style.display = checkbox.parentElement.style.display === 'block' ? 'none' : null; - } - }); - if (showMoreLink) { - showMoreLink.innerText = 'Show more...'; - } - } - }); -}); - -var tableHeight = window.screen.width / 3.4; - -function calculate_table_height() { - var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); - var percent = 60; - if (h > 1000) { - percent = 70; - } - return (percent * h) / 100; -} - - -var table = new DataTable('#signalSetsTable', { - fixedHeader: true, - searching: false, - paging: false, - scrollCollapse: true, - scrollX: true, - scrollY: calculate_table_height() + 75, - info: false, - fixedColumns: { - left: 2 - }, - ordering: false, - mark: true, - - language: { - buttons: { - colvis: "Toggle Columns" - } - } -}); - - -new DataTable.Buttons(table, { - buttons: [ - { - extend: 'colvis', - columns: 'th:nth-child(n+3)', - prefixButtons: ['colvisRestore'] - } - ] -}); - -table - .buttons(0, null) - .container() - .appendTo("#colvis"); - - -function format (signalSetId, relatedSignals, signalSetDescription) { - var signals = relatedSignals.filter((signal) => signal.signal_set === signalSetId) - var disabled, restricted; - - if (signals.length > 0) { - var data = `

${signalSetDescription}

` - var tableMarkup = ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - ''+ - '' - signals.forEach((signal) => { - checked = checkedSignalMembers.filter((obj) => obj.data_source == signal.source && obj.signal == signal.name).length; - var checkboxTitle = "" - checked = checked ? "checked" : "" - disabled = signal.endpoint ? "" : "disabled"; - var restricted = signal.restricted != "No"; - if (disabled === "disabled") { - checkboxTitle = "Visualization functionality for this endpoint is coming soon." - } - if (restricted) { - disabled = "disabled"; - checkboxTitle = "Access to this data source is restricted. Contact delphi-support@andrew.cmu.edu for more information." - } - tableMarkup += ''+ - ``+ - ``+ - ``+ - ``+ - ''+ - '' - }) - tableMarkup += '
Indicator NameIndicator API NameIndicator Description
${signal.display_name}${signal.member_name}${signal.member_description}
' - if (disabled === "disabled" || restricted) { - data += `' - } - data += tableMarkup; - } else { - data = "

No available indicators yet.

" - } - return data; -} - -// Plot/Export/Preview data block - -var currentMode = 'epivis'; - - -function handleModeChange(mode) { - $('#modeSubmitResult').html(''); - - var choose_dates = document.getElementsByName('choose_date'); - - if (mode === 'epivis') { - currentMode = 'epivis'; - choose_dates.forEach((el) => { - el.style.display = 'none'; - }); - $('#modeSubmitResult').html(''); - } else if (mode === 'export') { - currentMode = 'export'; - choose_dates.forEach((el) => { - el.style.display = 'flex'; - }); - $('#modeSubmitResult').html(''); - } else { - currentMode = 'preview'; - choose_dates.forEach((el) => { - el.style.display = 'flex'; - }); - } - document.getElementsByName("modes").forEach((el) => { - if (currentMode === el.value) { - el.checked = true; - } - }); -} - -function hideAlert(alertId) { - const alert = document.getElementById(alertId); - if (alert) { - alert.remove(); - } -} - - - -const alertPlaceholder = document.getElementById('warning-alert') -const appendAlert = (message, type) => { - const wrapper = document.createElement('div') - const alertId = `alert-${Date.now()}`; - wrapper.innerHTML = [ - `' - ].join('') - - alertPlaceholder.append(wrapper) - wrapper.getElementsByClassName('btn-close')[0].addEventListener('click', () => hideAlert(alertId)) - } - -function showNotCoveredGeoWarningMessage(notCoveredSignals, geoValue) { - var warningMessage = ""; - notCoveredSignals.forEach(signal => { - if (currentMode === 'epivis') { - warningMessage += `Indicator ${signal.display_name} is not available for Location ${geoValue}
` - } else { - var startDate = document.getElementById("start_date").value; - var endDate = document.getElementById("end_date").value; - warningMessage += `Indicator ${signal.display_name} is not available for Location ${geoValue} for the time period from ${startDate} to ${endDate}
` - } - }) - appendAlert(warningMessage, "warning") -} - -$('#geographic_value').on('select2:select', function (e) { - var geo = e.params.data; - checkGeoCoverage(geo.geoType, geo.id).then((notCoveredSignals) => { - if (notCoveredSignals.length > 0) { - showNotCoveredGeoWarningMessage(notCoveredSignals, geo.text); - } - } - ); -}); - - -function submitMode(event) { - event.preventDefault(); - var geographicValues = $('#geographic_value').select2('data'); - if (indicatorHandler.checkForCovidcastIndicators()) { - if (geographicValues.length === 0) { - appendAlert("Please select at least one geographic location", "warning") - return; - } - } - - if (currentMode === 'epivis') { - indicatorHandler.plotData(); - } else if (currentMode === 'export') { - indicatorHandler.exportData(); - } else { - indicatorHandler.previewData(); - } -} - -const isPlural = num => Math.abs(num) !== 1; -const simplePlural = word => `${word}s`; -const pluralize = (num, word, plural = simplePlural) => - isPlural(num) ? plural(word) : word; \ No newline at end of file From 12f619b1507ebcfa416b909bfc7eea58d8c2bc52 Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Fri, 2 May 2025 23:33:43 +0300 Subject: [PATCH 06/10] Added missing http errors html files --- src/templates/http_errors/400.html | 16 ++++++++ src/templates/http_errors/403.html | 13 +++++++ src/templates/http_errors/404.html | 15 ++++++++ src/templates/http_errors/500.html | 13 +++++++ src/templates/http_errors/error.html | 56 ++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+) create mode 100644 src/templates/http_errors/400.html create mode 100644 src/templates/http_errors/403.html create mode 100644 src/templates/http_errors/404.html create mode 100644 src/templates/http_errors/500.html create mode 100644 src/templates/http_errors/error.html diff --git a/src/templates/http_errors/400.html b/src/templates/http_errors/400.html new file mode 100644 index 0000000..26d9026 --- /dev/null +++ b/src/templates/http_errors/400.html @@ -0,0 +1,16 @@ +{% extends "http_errors/error.html" %} +{% load static %} +{% load i18n %} + +{% block content %} +
+ +
+

400

+

Sorry, the request could not be understood by the server due to malformed syntax.

+ Back to home +
+ +
+{% endblock %} + diff --git a/src/templates/http_errors/403.html b/src/templates/http_errors/403.html new file mode 100644 index 0000000..a63efc3 --- /dev/null +++ b/src/templates/http_errors/403.html @@ -0,0 +1,13 @@ +{% extends "http_errors/error.html" %} +{% load static %} +{% load i18n %} + +{% block content %} +
+
+

403

+

You don't have permission to access this resource.

+ Back to home +
+
+{% endblock %} \ No newline at end of file diff --git a/src/templates/http_errors/404.html b/src/templates/http_errors/404.html new file mode 100644 index 0000000..796d499 --- /dev/null +++ b/src/templates/http_errors/404.html @@ -0,0 +1,15 @@ +{% extends "http_errors/error.html" %} + +{% load static %} + +{% load i18n %} + +{% block content %} +
+
+

404

+

Not Found

+ Back to home +
+
+{% endblock %} diff --git a/src/templates/http_errors/500.html b/src/templates/http_errors/500.html new file mode 100644 index 0000000..5cd1b2e --- /dev/null +++ b/src/templates/http_errors/500.html @@ -0,0 +1,13 @@ +{% extends "http_errors/error.html" %} +{% load static %} +{% load i18n %} + +{% block content %} +
+
+

500

+

Internal Server Error.

+ Back to home +
+
+{% endblock %} \ No newline at end of file diff --git a/src/templates/http_errors/error.html b/src/templates/http_errors/error.html new file mode 100644 index 0000000..86beea1 --- /dev/null +++ b/src/templates/http_errors/error.html @@ -0,0 +1,56 @@ +{% load static %} {% load i18n %} + + + + + + + + Error + + + + + + + + + + + + + + + + + + + +
+ + {% block content %} + + {% endblock %} +
+ + + + + + + From bcdc6e087dcef8cf3192752c5e7c703b44f90a83 Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Fri, 2 May 2025 23:40:21 +0300 Subject: [PATCH 07/10] Fixed wrong url --- src/templates/http_errors/400.html | 2 +- src/templates/http_errors/403.html | 2 +- src/templates/http_errors/404.html | 2 +- src/templates/http_errors/500.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/templates/http_errors/400.html b/src/templates/http_errors/400.html index 26d9026..cac38d2 100644 --- a/src/templates/http_errors/400.html +++ b/src/templates/http_errors/400.html @@ -8,7 +8,7 @@

400

Sorry, the request could not be understood by the server due to malformed syntax.

- Back to home + Back to home
diff --git a/src/templates/http_errors/403.html b/src/templates/http_errors/403.html index a63efc3..21b8ec9 100644 --- a/src/templates/http_errors/403.html +++ b/src/templates/http_errors/403.html @@ -7,7 +7,7 @@

403

You don't have permission to access this resource.

- Back to home + Back to home
{% endblock %} \ No newline at end of file diff --git a/src/templates/http_errors/404.html b/src/templates/http_errors/404.html index 796d499..ae3f65e 100644 --- a/src/templates/http_errors/404.html +++ b/src/templates/http_errors/404.html @@ -9,7 +9,7 @@

404

Not Found

- Back to home + Back to home
{% endblock %} diff --git a/src/templates/http_errors/500.html b/src/templates/http_errors/500.html index 5cd1b2e..ddf0308 100644 --- a/src/templates/http_errors/500.html +++ b/src/templates/http_errors/500.html @@ -7,7 +7,7 @@

500

Internal Server Error.

- Back to home + Back to home
{% endblock %} \ No newline at end of file From 8786cbaf990c43ce00e12f5e42d538a6ad5bd44b Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Fri, 2 May 2025 23:40:21 +0300 Subject: [PATCH 08/10] Fixed wrong url --- src/templates/http_errors/400.html | 2 +- src/templates/http_errors/403.html | 2 +- src/templates/http_errors/404.html | 2 +- src/templates/http_errors/500.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/templates/http_errors/400.html b/src/templates/http_errors/400.html index 26d9026..cac38d2 100644 --- a/src/templates/http_errors/400.html +++ b/src/templates/http_errors/400.html @@ -8,7 +8,7 @@

400

Sorry, the request could not be understood by the server due to malformed syntax.

- Back to home + Back to home
diff --git a/src/templates/http_errors/403.html b/src/templates/http_errors/403.html index a63efc3..21b8ec9 100644 --- a/src/templates/http_errors/403.html +++ b/src/templates/http_errors/403.html @@ -7,7 +7,7 @@

403

You don't have permission to access this resource.

- Back to home + Back to home
{% endblock %} \ No newline at end of file diff --git a/src/templates/http_errors/404.html b/src/templates/http_errors/404.html index 796d499..ae3f65e 100644 --- a/src/templates/http_errors/404.html +++ b/src/templates/http_errors/404.html @@ -9,7 +9,7 @@

404

Not Found

- Back to home + Back to home
{% endblock %} diff --git a/src/templates/http_errors/500.html b/src/templates/http_errors/500.html index 5cd1b2e..ddf0308 100644 --- a/src/templates/http_errors/500.html +++ b/src/templates/http_errors/500.html @@ -7,7 +7,7 @@

500

Internal Server Error.

- Back to home + Back to home
{% endblock %} \ No newline at end of file From 51a5519ba0fdc21b6bbc74d05302b285b441a5b5 Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Mon, 5 May 2025 19:55:36 +0300 Subject: [PATCH 09/10] Fixed issue when 'Original data provider' filter's values were not updated --- .../migrations/0004_alter_indicator_base.py | 19 +++++++++++++++++++ src/indicators/models.py | 2 +- src/indicatorsets/filters.py | 18 +++++------------- src/indicatorsets/forms.py | 15 ++------------- src/indicatorsets/utils.py | 10 ++++++++++ 5 files changed, 37 insertions(+), 27 deletions(-) create mode 100644 src/indicators/migrations/0004_alter_indicator_base.py diff --git a/src/indicators/migrations/0004_alter_indicator_base.py b/src/indicators/migrations/0004_alter_indicator_base.py new file mode 100644 index 0000000..b01df46 --- /dev/null +++ b/src/indicators/migrations/0004_alter_indicator_base.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.7 on 2025-05-05 16:22 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('indicators', '0003_alter_indicator_geographic_scope_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='indicator', + name='base', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='base_for', to='indicators.indicator', verbose_name='Base Indicator'), + ), + ] diff --git a/src/indicators/models.py b/src/indicators/models.py index 3279652..ca7ed2e 100644 --- a/src/indicators/models.py +++ b/src/indicators/models.py @@ -367,7 +367,7 @@ class Indicator(models.Model): "indicators.Indicator", verbose_name="Base Indicator", related_name="base_for", - on_delete=models.PROTECT, + on_delete=models.CASCADE, null=True, blank=True, ) diff --git a/src/indicatorsets/filters.py b/src/indicatorsets/filters.py index e98d511..dd82525 100644 --- a/src/indicatorsets/filters.py +++ b/src/indicatorsets/filters.py @@ -7,24 +7,16 @@ from indicatorsets.models import IndicatorSet -from indicatorsets.utils import get_list_of_indicators_filtered_by_geo +from indicatorsets.utils import ( + get_list_of_indicators_filtered_by_geo, + get_original_data_provider_choices, +) from indicators.models import Indicator from base.models import Pathogen, GeographicScope, Geography, SeverityPyramidRung logger = logging.getLogger(__name__) -try: - ORIGINAL_DATA_PROVIDER_CHOICES = [ - (el, el) - for el in set( - IndicatorSet.objects.values_list("original_data_provider", flat=True) - ) - ] -except Exception as e: - ORIGINAL_DATA_PROVIDER_CHOICES = [("", "No original data provider available")] - print(f"Error fetching original data provider choices: {e}") - class IndicatorSetFilter(django_filters.FilterSet): @@ -60,7 +52,7 @@ class IndicatorSetFilter(django_filters.FilterSet): original_data_provider = django_filters.MultipleChoiceFilter( field_name="original_data_provider", - choices=ORIGINAL_DATA_PROVIDER_CHOICES, + choices=get_original_data_provider_choices, widget=QueryArrayWidget, lookup_expr="exact", required=False, diff --git a/src/indicatorsets/forms.py b/src/indicatorsets/forms.py index 11e0813..abf0b8b 100644 --- a/src/indicatorsets/forms.py +++ b/src/indicatorsets/forms.py @@ -2,18 +2,7 @@ from base.models import Pathogen, GeographicScope, Geography, SeverityPyramidRung from indicatorsets.models import IndicatorSet - - -try: - ORIGINAL_DATA_PROVIDER_CHOICES = [ - (el, el) - for el in set( - IndicatorSet.objects.values_list("original_data_provider", flat=True) - ) - ] -except Exception as e: - ORIGINAL_DATA_PROVIDER_CHOICES = [("", "No original data provider available")] - print(f"Error fetching original data provider choices: {e}") +from indicatorsets.utils import get_original_data_provider_choices class IndicatorSetFilterForm(forms.ModelForm): @@ -44,7 +33,7 @@ class IndicatorSetFilterForm(forms.ModelForm): ) original_data_provider = forms.ChoiceField( - choices=ORIGINAL_DATA_PROVIDER_CHOICES, + choices=get_original_data_provider_choices, widget=forms.CheckboxSelectMultiple(), ) diff --git a/src/indicatorsets/utils.py b/src/indicatorsets/utils.py index daeb7ae..1bf84f0 100644 --- a/src/indicatorsets/utils.py +++ b/src/indicatorsets/utils.py @@ -5,6 +5,7 @@ import requests from django.conf import settings from epiweeks import Week +from indicatorsets.models import IndicatorSet def list_to_dict(lst): @@ -53,3 +54,12 @@ def get_epiweek(start_date, end_date): end_date = Week.fromdate(end_date) end_date = f"{end_date.year}{end_date.week if end_date.week >= 10 else '0' + str(end_date.week)}" return [start_date, end_date] + + +def get_original_data_provider_choices(): + return [ + (el, el) + for el in IndicatorSet.objects.values_list("original_data_provider", flat=True) + .order_by("original_data_provider") + .distinct() + ] From 3a1588c1a08abb58e8eee495a8d64e0823e4ecc5 Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Mon, 5 May 2025 23:06:39 +0300 Subject: [PATCH 10/10] Added custom warning message for non-delphi indicators --- src/assets/js/indicatorSetsTable.js | 20 +++++++++++----- .../migrations/0005_indicator_source_type.py | 18 ++++++++++++++ src/indicators/models.py | 24 +++++++++++++++---- src/indicators/resources.py | 12 ++++++++++ src/indicatorsets/views.py | 1 + src/staticfiles/js/indicatorSetsTable.js | 20 +++++++++++----- 6 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 src/indicators/migrations/0005_indicator_source_type.py diff --git a/src/assets/js/indicatorSetsTable.js b/src/assets/js/indicatorSetsTable.js index a59472f..f20ec82 100644 --- a/src/assets/js/indicatorSetsTable.js +++ b/src/assets/js/indicatorSetsTable.js @@ -44,11 +44,10 @@ new DataTable.Buttons(table, { table.buttons(0, null).container().appendTo("#colvis"); function format(indicatorSetId, relatedIndicators, indicatorSetDescription) { - console.lopg; var indicators = relatedIndicators.filter( (indicator) => indicator.indicator_set === indicatorSetId ); - var disabled, restricted; + var disabled, restricted, sourceType; if (indicators.length > 0) { var data = `

${indicatorSetDescription}

`; @@ -71,6 +70,7 @@ function format(indicatorSetId, relatedIndicators, indicatorSetDescription) { var checkboxTitle = ""; checked = checked ? "checked" : ""; disabled = indicator.endpoint ? "" : "disabled"; + sourceType = indicator.source_type; var restricted = indicator.restricted != "No"; if (disabled === "disabled") { checkboxTitle = @@ -92,11 +92,19 @@ function format(indicatorSetId, relatedIndicators, indicatorSetDescription) { }); tableMarkup += ""; if (disabled === "disabled" || restricted) { - data += - `"; + if (sourceType === "non_delphi") { + data += + `"; + } else { + data += + `"; + } } + data += tableMarkup; } else { data = "

No available indicators yet.

"; diff --git a/src/indicators/migrations/0005_indicator_source_type.py b/src/indicators/migrations/0005_indicator_source_type.py new file mode 100644 index 0000000..0ee50e4 --- /dev/null +++ b/src/indicators/migrations/0005_indicator_source_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.7 on 2025-05-05 19:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('indicators', '0004_alter_indicator_base'), + ] + + operations = [ + migrations.AddField( + model_name='indicator', + name='source_type', + field=models.CharField(blank=True, choices=[('covidcast', 'Covidcast'), ('other_endpoint', 'Other Endpoint'), ('non_delphi', 'Non Delphi')], default='covidcast', help_text='Type of source for the indicator', max_length=255, null=True, verbose_name='Source Type'), + ), + ] diff --git a/src/indicators/models.py b/src/indicators/models.py index ca7ed2e..07f7ae0 100644 --- a/src/indicators/models.py +++ b/src/indicators/models.py @@ -4,6 +4,13 @@ from django.db import models +SOURCE_TYPES = [ + ("covidcast", "Covidcast"), + ("other_endpoint", "Other Endpoint"), + ("non_delphi", "Non Delphi"), +] + + # Create your models here. class IndicatorType(models.Model): @@ -122,9 +129,7 @@ def display_name(self): class Indicator(models.Model): - name: models.CharField = models.CharField( - verbose_name="Name", max_length=255 - ) + name: models.CharField = models.CharField(verbose_name="Name", max_length=255) display_name: models.CharField = models.CharField( verbose_name="Display Name", max_length=255, blank=True ) @@ -380,6 +385,16 @@ class Indicator(models.Model): blank=True, ) + source_type: models.CharField = models.CharField( + verbose_name="Source Type", + max_length=255, + choices=SOURCE_TYPES, + default="covidcast", + help_text="Type of source for the indicator", + blank=True, + null=True, + ) + class Meta: verbose_name = "Indicator" verbose_name_plural = "Indicators" @@ -397,7 +412,8 @@ class Meta: fields=["name", "source"], name="unique_indicator_name" ), models.UniqueConstraint( - fields=["name", "indicator_set"], name="unique_indicator_indicator_set_name" + fields=["name", "indicator_set"], + name="unique_indicator_indicator_set_name", ), ] diff --git a/src/indicators/resources.py b/src/indicators/resources.py index b09a365..cc2c1de 100644 --- a/src/indicators/resources.py +++ b/src/indicators/resources.py @@ -428,6 +428,10 @@ def before_import_row(self, row, **kwargs) -> None: def after_import_row(self, row, row_result, **kwargs): process_indicator_geography(row) + def after_save_instance(self, instance, row, **kwargs): + instance.source_type = "covidcast" + instance.save() + def skip_row(self, instance, original, row, import_validation_errors=None): if not row["Include in indicator app"]: return True @@ -601,6 +605,10 @@ def skip_row(self, instance, original, row, import_validation_errors=None): def after_import_row(self, row, row_result, **kwargs): process_indicator_geography(row) + def after_save_instance(self, instance, row, **kwargs): + instance.source_type = "other_endpoint" + instance.save() + class NonDelphiIndicatorResource(resources.ModelResource): @@ -633,3 +641,7 @@ def before_import_row(self, row, **kwargs) -> None: def skip_row(self, instance, original, row, import_validation_errors=None): if not row["Include in indicator app"]: return True + + def after_save_instance(self, instance, row, **kwargs): + instance.source_type = "non_delphi" + instance.save() diff --git a/src/indicatorsets/views.py b/src/indicatorsets/views.py index 473b29b..641ca33 100644 --- a/src/indicatorsets/views.py +++ b/src/indicatorsets/views.py @@ -89,6 +89,7 @@ def get_related_indicators(self, queryset, indicator_set_ids: list): "description": indicator.description if indicator.description else "", "member_description": indicator.member_description if indicator.member_description else indicator.description, "restricted": indicator.indicator_set.dua_required if indicator.indicator_set else "", + "source_type": indicator.source_type, } ) return related_indicators diff --git a/src/staticfiles/js/indicatorSetsTable.js b/src/staticfiles/js/indicatorSetsTable.js index a59472f..f20ec82 100644 --- a/src/staticfiles/js/indicatorSetsTable.js +++ b/src/staticfiles/js/indicatorSetsTable.js @@ -44,11 +44,10 @@ new DataTable.Buttons(table, { table.buttons(0, null).container().appendTo("#colvis"); function format(indicatorSetId, relatedIndicators, indicatorSetDescription) { - console.lopg; var indicators = relatedIndicators.filter( (indicator) => indicator.indicator_set === indicatorSetId ); - var disabled, restricted; + var disabled, restricted, sourceType; if (indicators.length > 0) { var data = `

${indicatorSetDescription}

`; @@ -71,6 +70,7 @@ function format(indicatorSetId, relatedIndicators, indicatorSetDescription) { var checkboxTitle = ""; checked = checked ? "checked" : ""; disabled = indicator.endpoint ? "" : "disabled"; + sourceType = indicator.source_type; var restricted = indicator.restricted != "No"; if (disabled === "disabled") { checkboxTitle = @@ -92,11 +92,19 @@ function format(indicatorSetId, relatedIndicators, indicatorSetDescription) { }); tableMarkup += ""; if (disabled === "disabled" || restricted) { - data += - `"; + if (sourceType === "non_delphi") { + data += + `"; + } else { + data += + `"; + } } + data += tableMarkup; } else { data = "

No available indicators yet.

";