diff --git a/src/assets/js/indicatorHandler.js b/src/assets/js/indicatorHandler.js new file mode 100644 index 0000000..fc2da5e --- /dev/null +++ b/src/assets/js/indicatorHandler.js @@ -0,0 +1,413 @@ +class IndicatorHandler { + constructor() { + this.indicators = {}; + } + + fluviewIndicatorsMapping = { + "wili": "%wILI", + "ili": "%ILI", + } + + fluSurvRegions = [ + { value: 'network_all', label: 'Entire Network' }, + { value: 'network_eip', label: 'EIP Netowrk' }, + { value: 'network_ihsp', label: 'IHSP Network' }, + { value: 'CA', label: 'CA' }, + { value: 'CO', label: 'CO' }, + { value: 'CT', label: 'CT' }, + { value: 'GA', label: 'GA' }, + { value: 'IA', label: 'IA' }, + { value: 'ID', label: 'ID' }, + { value: 'MD', label: 'MD' }, + { value: 'MI', label: 'MI' }, + { value: 'MN', label: 'MN' }, + { value: 'NM', label: 'NM' }, + { value: 'NY_albany', label: 'NY (Albany)' }, + { value: 'NY_rochester', label: 'NY (Rochester)' }, + { value: 'OH', label: 'OH' }, + { value: 'OK', label: 'OK' }, + { value: 'OR', label: 'OR' }, + { value: 'RI', label: 'RI' }, + { value: 'SD', label: 'SD' }, + { value: 'TN', label: 'TN' }, + { value: 'UT', label: 'UT' }, + ] + + fluviewRegions = [ + { id: 'nat', text: 'U.S. National' }, + { id: 'hhs1', text: 'HHS Region 1' }, + { id: 'hhs2', text: 'HHS Region 2' }, + { id: 'hhs3', text: 'HHS Region 3' }, + { id: 'hhs4', text: 'HHS Region 4' }, + { id: 'hhs5', text: 'HHS Region 5' }, + { id: 'hhs6', text: 'HHS Region 6' }, + { id: 'hhs7', text: 'HHS Region 7' }, + { id: 'hhs8', text: 'HHS Region 8' }, + { id: 'hhs9', text: 'HHS Region 9' }, + { id: 'hhs10', text: 'HHS Region 10' }, + { id: 'cen1', text: 'Census Region 1' }, + { id: 'cen2', text: 'Census Region 2' }, + { id: 'cen3', text: 'Census Region 3' }, + { id: 'cen4', text: 'Census Region 4' }, + { id: 'cen5', text: 'Census Region 5' }, + { id: 'cen6', text: 'Census Region 6' }, + { id: 'cen7', text: 'Census Region 7' }, + { id: 'cen8', text: 'Census Region 8' }, + { id: 'cen9', text: 'Census Region 9' }, + { id: 'AK', text: 'AK' }, + { id: 'AL', text: 'AL' }, + { id: 'AR', text: 'AR' }, + { id: 'AZ', text: 'AZ' }, + { id: 'CA', text: 'CA' }, + { id: 'CO', text: 'CO' }, + { id: 'CT', text: 'CT' }, + { id: 'DC', text: 'DC' }, + { id: 'DE', text: 'DE' }, + { id: 'FL', text: 'FL' }, + { id: 'GA', text: 'GA' }, + { id: 'HI', text: 'HI' }, + { id: 'IA', text: 'IA' }, + { id: 'ID', text: 'ID' }, + { id: 'IL', text: 'IL' }, + { id: 'IN', text: 'IN' }, + { id: 'KS', text: 'KS' }, + { id: 'KY', text: 'KY' }, + { id: 'LA', text: 'LA' }, + { id: 'MA', text: 'MA' }, + { id: 'MD', text: 'MD' }, + { id: 'ME', text: 'ME' }, + { id: 'MI', text: 'MI' }, + { id: 'MN', text: 'MN' }, + { id: 'MO', text: 'MO' }, + { id: 'MS', text: 'MS' }, + { id: 'MT', text: 'MT' }, + { id: 'NC', text: 'NC' }, + { id: 'ND', text: 'ND' }, + { id: 'NE', text: 'NE' }, + { id: 'NH', text: 'NH' }, + { id: 'NJ', text: 'NJ' }, + { id: 'NM', text: 'NM' }, + { id: 'NV', text: 'NV' }, + { id: 'NY', text: 'NY' }, + { id: 'OH', text: 'OH' }, + { id: 'OK', text: 'OK' }, + { id: 'OR', text: 'OR' }, + { id: 'PA', text: 'PA' }, + { id: 'RI', text: 'RI' }, + { id: 'SC', text: 'SC' }, + { id: 'SD', text: 'SD' }, + { id: 'TN', text: 'TN' }, + { id: 'TX', text: 'TX' }, + { id: 'UT', text: 'UT' }, + { id: 'VA', text: 'VA' }, + { id: 'VT', text: 'VT' }, + { id: 'WA', text: 'WA' }, + { id: 'WI', text: 'WI' }, + { id: 'WV', text: 'WV' }, + { id: 'WY', text: 'WY' }, + { id: 'ny_minus_jfk', text: 'NY (minus NYC)' }, + { id: 'as', text: 'American Samoa' }, + { id: 'mp', text: 'Mariana Islands' }, + { id: 'gu', text: 'Guam' }, + { id: 'pr', text: 'Puerto Rico' }, + { id: 'vi', text: 'Virgin Islands' }, + { id: 'ord', text: 'Chicago' }, + { id: 'lax', text: 'Los Angeles' }, + { id: 'jfk', text: 'New York City' }, + ] + + checkForCovidcastIndicators() { + return this.indicators.some((indicator) => { + return indicator["_endpoint"] === "covidcast"; + }); + } + + getCovidcastIndicators() { + var covidcastIndicators = []; + this.indicators.forEach((indicator) => { + if (indicator["_endpoint"] === "covidcast") { + covidcastIndicators.push(indicator); + } + }); + return covidcastIndicators; + } + + getFluviewIndicators() { + var fluviewIndicators = []; + this.indicators.forEach((indicator) => { + if (indicator["_endpoint"] === "fluview") { + fluviewIndicators.push(indicator); + } + } + ); + return fluviewIndicators; + } + + getFromToDate(startDate, endDate, timeType) { + if (timeType === "week") { + $.ajax({ + url: "get_epiweek/", + type: 'POST', + async: false, + data: { + csrfmiddlewaretoken: csrf_token, + start_date: startDate, + end_date: endDate, + }, + success: function (result) { + startDate = result.start_date; + endDate = result.end_date; + } + }) + } + return [startDate, endDate]; + } + + + sendAsyncAjaxRequest(url, data) { + var request = $.ajax({ + url: url, + type: "GET", + data: data, + }) + return request; + } + + showFluviewRegions() { + var fluviewRegionSelect = ` +
+
+ +
+
+ +
+
` + if ($("#otherEndpointLocations").length) { + $("#otherEndpointLocations").append(fluviewRegionSelect) + $("#fluviewRegions").select2({ + placeholder: "Select ILINet Location(s)", + data: this.fluviewRegions, + allowClear: true, + width: '100%', + }); + } + } + + generateEpivisCustomTitle(indicator, geoValue) { + var epivisCustomTitle; + if (indicator["member_short_name"]) { + epivisCustomTitle = `${indicator["signal_set_short_name"]}:${indicator["member_short_name"]} : ${geoValue}` + } else { + epivisCustomTitle = `${indicator["signal_set_short_name"]} : ${geoValue}` + } + return epivisCustomTitle; + } + + plotData() { + var dataSets = {}; + var covidCastGeographicValues = $('#geographic_value').select2('data'); + var fluviewRegions = $('#fluviewRegions').select2('data'); + + this.indicators.forEach((indicator) => { + if (indicator["_endpoint"] === "covidcast") { + covidCastGeographicValues.forEach((geoValue) => { + var geographicValue = (typeof geoValue.id === 'string') ? geoValue.id.toLowerCase() : geoValue.id; + var geographicType = geoValue.geoType; + dataSets[`${indicator["signal"]}_${geographicValue}`] = { + color: '#' + (Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0'), + title: "value", + params: { + _endpoint: indicator["_endpoint"], + data_source: indicator["data_source"], + signal: indicator["signal"], + time_type: indicator["time_type"], + geo_type: geographicType, + geo_value: geographicValue, + custom_title: this.generateEpivisCustomTitle(indicator, geoValue.text) + } + } + }) + } else if (indicator["_endpoint"] === "fluview") { + fluviewRegions.forEach((region) => { + dataSets[`${indicator["signal"]}_${indicator["_endpoint"]}_${region.id}`] = { + color: '#' + (Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0'), + title: this.fluviewIndicatorsMapping[indicator["signal"]] || indicator["signal"], + params: { + _endpoint: indicator["_endpoint"], + regions: region.id, + custom_title: this.generateEpivisCustomTitle(indicator, region.text) + } + } + }) + } + // else if (indicator["_endpoint"] === "flusurv") { + // // TODO: Add support for flusurv. Need to figure out how to get the geographic value for flusurv. + // // For now, we will just use the static geographic value. + // dataSets[`${indicator["signal"]}_${indicator["_endpoint"]}`] = { + // color: '#'+(Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0'), + // title: indicator["signal"], + // params: { + // _endpoint: indicator["_endpoint"], + // locations: "network_all", + // custom_title: this.generateEpivisCustomTitle(indicator, "Entire Network") + // } + // } + // } else if (indicator["_endpoint"] === "gft") { + // // TODO: Add support for gft. Need to figure out how to get the geographic value for gft. + // // For now, we will just use the static geographic value. + // dataSets[`${indicator["signal"]}_${indicator["_endpoint"]}`] = { + // color: '#'+(Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0'), + // title: indicator["signal"], + // params: { + // _endpoint: indicator["_endpoint"], + // locations: "nat", + // custom_title: this.generateEpivisCustomTitle(indicator, "U.S. National") + // } + // } + // } else if (indicator["_endpoint"] === "wiki") { + // dataSets[`${indicator["signal"]}_${indicator["_endpoint"]}`] = { + // color: '#'+(Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0'), + // title: indicator["signal"], + // params: { + // _endpoint: indicator["_endpoint"], + // articles: "fatigue_(medical)", + // language: "en", + // resolution: "daily", + // custom_title: this.generateEpivisCustomTitle(indicator, "U.S. National") + // } + // } + // } else if (indicator["_endpoint"] === "cdc") { + // dataSets[`${indicator["signal"]}_${indicator["_endpoint"]}`] = { + // color: '#'+(Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0'), + // title: indicator["signal"], + // params: { + // _endpoint: indicator["_endpoint"], + // auth: "390da13640f61", + // locations: "nat", + // custom_title: this.generateEpivisCustomTitle(indicator, "U.S. National") + // } + // } + // } else if(indicator["_endpoint"] === "sensors") { + // dataSets[`${indicator["signal"]}_${indicator["_endpoint"]}`] = { + // color: '#'+(Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0'), + // title: indicator["signal"], + // params: { + // _endpoint: indicator["_endpoint"], + // auth: "390da13640f61", + // names: "wiki", + // locations: "nat", + // custom_title: this.generateEpivisCustomTitle(indicator, "U.S. National") + // } + // } + // } + }); + var requestParams = []; + for (var key in dataSets) { + requestParams.push(dataSets[key]); + } + + var urlParamsEncoded = btoa(`{"datasets":${JSON.stringify(requestParams)}}`); + + var linkToEpivis = `${epiVisUrl}#${urlParamsEncoded}` + window.open(linkToEpivis, '_blank').focus(); + } + + exportData() { + var manualDataExport = "To download data, please click on the link or copy/paste command into your terminal: \n\n" + var exportUrl; + + this.getCovidcastIndicators().forEach((indicator) => { + var startDate = document.getElementById('start_date').value; + var endDate = document.getElementById('end_date').value; + const [dateFrom, dateTo] = this.getFromToDate(startDate, endDate, indicator["time_type"]); + + var covidCastGeographicValues = $('#geographic_value').select2('data'); + covidCastGeographicValues = Object.groupBy(covidCastGeographicValues, ({ geoType }) => [geoType]); + var covidcastGeoTypes = Object.keys(covidCastGeographicValues); + covidcastGeoTypes.forEach((geoType) => { + var geoValues = covidCastGeographicValues[geoType].map((el) => (typeof el.id === "string") ? el.id.toLowerCase() : el.id).join(","); + exportUrl = `https://api.delphi.cmu.edu/epidata/covidcast/csv?signal=${indicator["data_source"]}:${indicator["signal"]}&start_day=${dateFrom}&end_day=${dateTo}&geo_type=${geoType}&geo_values=${geoValues}`; + manualDataExport += `wget --content-disposition ${exportUrl}\n`; + }) + }) + + if (this.getFluviewIndicators().length > 0) { + var startDate = document.getElementById('start_date').value; + var endDate = document.getElementById('end_date').value; + + const [dateFrom, dateTo] = this.getFromToDate(startDate, endDate, "week"); + + var fluviewRegions = $('#fluviewRegions').select2('data').map((region) => region.id); + fluviewRegions = fluviewRegions.join(","); + exportUrl = `https://api.delphi.cmu.edu/epidata/fluview/?regions=${fluviewRegions}&epiweeks=${dateFrom}-${dateTo}&format=csv` + manualDataExport += `wget --content-disposition ${exportUrl}\n`; + } + + $('#modeSubmitResult').html(manualDataExport); + } + + previewData(){ + $('#loader').show(); + var requests = []; + var previewExample = []; + var startDate = document.getElementById('start_date').value; + var endDate = document.getElementById('end_date').value; + + if (this.checkForCovidcastIndicators()) { + var geographicValues = $('#geographic_value').select2('data'); + geographicValues = Object.groupBy(geographicValues, ({ geoType }) => [geoType]) + var geoTypes = Object.keys(geographicValues); + this.getCovidcastIndicators().forEach((indicator) => { + const [dateFrom, dateTo] = this.getFromToDate(startDate, endDate, indicator["time_type"]); + var timeValues = indicator["time_type"] === "week" ? `${dateFrom}-${dateTo}` : `${dateFrom}--${dateTo}`; + geoTypes.forEach((geoType) => { + var geoValues = geographicValues[geoType].map((el) => (typeof el.id === "string") ? el.id.toLowerCase() : el.id).join(","); + var data = { + "time_type": indicator["time_type"], + "time_values": timeValues, + "data_source": indicator["data_source"], + "signal": indicator["signal"], + "geo_type": geoType, + "geo_values": geoValues + } + requests.push(this.sendAsyncAjaxRequest("epidata/covidcast/", data)) + }) + }) + } + + if (this.getFluviewIndicators().length > 0) { + const [dateFrom, dateTo] = this.getFromToDate(startDate, endDate, "week"); + var fluviewRegions = $('#fluviewRegions').select2('data').map((region) => region.id); + fluviewRegions = fluviewRegions.join(","); + var data = { + "regions": fluviewRegions, + "epiweeks": `${dateFrom}-${dateTo}`, + } + + requests.push(this.sendAsyncAjaxRequest("epidata/fluview/", data)) + } + + $.when.apply($, requests).then((...responses) => { + if (requests.length === 1) { + if (responses[0]["epidata"].length != 0) { + previewExample.push({ epidata: responses[0]["epidata"][0], result: responses["result"], message: responses["message"] }) + } else { + previewExample.push(responses[0]); + } + } else { + responses.forEach((response) => { + if (response[0]["epidata"].length != 0) { + previewExample.push({ epidata: response[0]["epidata"][0], result: response[0]["result"], message: response[0]["message"] }) + } else { + previewExample.push(response[0]["epidata"]); + } + }) + } + $('#loader').hide(); + $('#modeSubmitResult').html(JSON.stringify(previewExample, null, 2)); + + }) + } + +} \ No newline at end of file diff --git a/src/assets/js/signal_sets.js b/src/assets/js/signal_sets.js index 676490e..3120909 100644 --- a/src/assets/js/signal_sets.js +++ b/src/assets/js/signal_sets.js @@ -1,3 +1,5 @@ +const indicatorHandler = new IndicatorHandler(); + function initSelect2(elementId, data) { $(`#${elementId}`).select2({ data: data, @@ -15,73 +17,34 @@ function showWarningAlert(warningMessage, slideUpTime = 2000) { }); } -function checkGeoCoverage(geoType, geoValue) { - var notCoveredSignals = []; - $.ajax({ - url: "epidata/covidcast/geo_coverage/", - type: 'GET', - async: false, - data: { - 'geo': `${geoType}:${geoValue}` - }, - success: function (result) { - checkedSignalMembers.forEach(signal => { - var covered = result["epidata"].some( - e => (e.source === signal.data_source && e.signal === signal.signal) - ) - if (!covered) { - notCoveredSignals.push(signal); - } - }) - } - }) - return notCoveredSignals; -} - - - -function plotData() { - var dataSets = {}; - var geographicValues = $('#geographic_value').select2('data'); - checkedSignalMembers.forEach((signal) => { - geographicValues.forEach((geoValue) => { - var geographicValue = (typeof geoValue.id === 'string') ? geoValue.id.toLowerCase() : geoValue.id; - var geographicType = geoValue.geoType; - var epivisCustomTitle; - if (signal["member_short_name"]) { - epivisCustomTitle = `${signal["signal_set_short_name"]}:${signal["member_short_name"]} : ${geoValue.text}` - } else { - epivisCustomTitle = `${signal["signal_set_short_name"]} : ${geoValue.text}` +async function checkGeoCoverage(geoType, geoValue) { + const notCoveredSignals = []; + + try { + const result = await $.ajax({ + url: "epidata/covidcast/geo_coverage/", + type: 'GET', + data: { + 'geo': `${geoType}:${geoValue}` } - dataSets[`${signal["signal"]}_${geographicValue}`] = { - color: '#'+(Math.random() * 0xFFFFFF << 0).toString(16).padStart(6, '0'), - title: "value", - params: { - _endpoint: signal["_endpoint"], - data_source: signal["data_source"], - signal: signal["signal"], - time_type: signal["time_type"], - geo_type: geographicType, - geo_value: geographicValue, - custom_title: epivisCustomTitle - } + }); + + 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) { + notCoveredSignals.push(signal); } - }) + }); - }); - - var requestParams = []; - for (var key in dataSets) { - requestParams.push(dataSets[key]); + return notCoveredSignals; + } catch (error) { + console.error('Error fetching geo coverage:', error); + return notCoveredSignals; } - - var urlParamsEncoded = btoa(`{"datasets":${JSON.stringify(requestParams)}}`); - - var linkToEpivis = `${epiVisUrl}#${urlParamsEncoded}` - window.open(linkToEpivis, '_blank').focus(); } - // Function to update the modal content function updateSelectedSignals(dataSource, signalDisplayName, signalSet, signal) { var selectedSignalsList = document.getElementById('selectedSignalsList'); @@ -115,6 +78,7 @@ function addSelectedSignal(element) { document.getElementById(`${element.dataset.datasource}_${element.dataset.signal}`).remove(); } + indicatorHandler.indicators = checkedSignalMembers; if (checkedSignalMembers.length > 0) { $("#showSelectedSignalsButton").show(); @@ -123,6 +87,33 @@ function addSelectedSignal(element) { } } +$("#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 => { @@ -254,116 +245,6 @@ function format (signalSetId, relatedSignals, signalSetDescription) { return data; } - -function exportData() { - var geographicValues = $('#geographic_value').select2('data'); - geographicValues = Object.groupBy(geographicValues, ({ geoType }) => [geoType]) - var geoTypes = Object.keys(geographicValues); - - var startDate = document.getElementById('start_date').value; - var endDate = document.getElementById('end_date').value; - - var manualDataExport = "To download data, please click on the link or copy/paste command into your terminal: \n\n" - var requests = []; - - checkedSignalMembers.forEach((signal) => { - geoTypes.forEach((geoType) => { - var geoValues = geographicValues[geoType].map((el) => (typeof el.id === 'string') ? el.id.toLowerCase() : el.id).join(","); - if (signal["time_type"] === "week") { - var request = $.ajax({ - url: "get_epiweek/", - type: 'POST', - async: true, - data: { - csrfmiddlewaretoken: csrf_token, - start_date: startDate, - end_date: endDate, - }, - success: function (result) { - var exportUrl = `https://api.delphi.cmu.edu/epidata/covidcast/csv?signal=${signal["data_source"]}:${signal["signal"]}&start_day=${result.start_date}&end_day=${result.end_date}&geo_type=${geoType}&geo_values=${geoValues}`; - manualDataExport += `wget --content-disposition ${exportUrl}\n` - } - }) - requests.push(request); - } else { - var exportUrl = `https://api.delphi.cmu.edu/epidata/covidcast/csv?signal=${signal["data_source"]}:${signal["signal"]}&start_day=${startDate}&end_day=${endDate}&geo_type=${geoType}&geo_values=${geoValues}`; - manualDataExport += `wget --content-disposition ${exportUrl}\n` - } - }); - }); - $.when.apply($, requests).then(function() { - $('#modeSubmitResult').html(manualDataExport); - }) - -} - -function previewData() { - var geographicValues = $('#geographic_value').select2('data'); - geographicValues = Object.groupBy(geographicValues, ({ geoType }) => [geoType]) - var geoTypes = Object.keys(geographicValues); - var previewExample = []; - var requests = []; - - var startDate = document.getElementById("start_date").value; - var endDate = document.getElementById("end_date").value; - - checkedSignalMembers.forEach((signal) => { - var timeValues; - - if (signal["time_type"] === "week") { - $.ajax({ - url: "get_epiweek/", - type: 'POST', - async: false, - data: { - csrfmiddlewaretoken: csrf_token, - start_date: startDate, - end_date: endDate, - }, - success: function (result) { - timeValues = `${result.start_date}-${result.end_date}`; - } - }) - }; - - var requestSent = false; - if (!requestSent) { - geoTypes.forEach((geoType) => { - var geoValues = geographicValues[geoType].map((el) => (typeof el.id === 'string') ? el.id.toLowerCase() : el.id).join(","); - $('#loader').show(); - timeValues = signal["time_type"] === "week" ? timeValues : `${startDate}--${endDate}`; - var request = $.ajax({ - url: "epidata/covidcast/", - type: 'GET', - async: true, - data: { - 'time_type': signal["time_type"], - 'time_values': timeValues, - 'data_source': signal["data_source"], - 'signal': signal["signal"], - 'geo_type': geoType, - 'geo_values': geoValues - }, - success: function (result) { - if (result["epidata"].length != 0) { - previewExample.push({epidata: result["epidata"][0], result: result["result"], message: result["message"]}) - } else { - previewExample.push({epidata: result["epidata"], result: result["result"], message: result["message"]}) - } - } - }) - requests.push(request); - }) - } - }) - $.when.apply($, requests).then(function() { - $('#loader').hide(); - $('#modeSubmitResult').html(JSON.stringify(previewExample, null, 2)); - requestSent = true; - }) -} - - // Plot/Export/Preview data block var currentMode = 'epivis'; @@ -439,22 +320,31 @@ function showNotCoveredGeoWarningMessage(notCoveredSignals, geoValue) { $('#geographic_value').on('select2:select', function (e) { var geo = e.params.data; - var notCoveredSignals = checkGeoCoverage(geo.geoType, geo.id) - if (notCoveredSignals.length > 0) { - showNotCoveredGeoWarningMessage(notCoveredSignals, geo.text); + 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') { - plotData(); + indicatorHandler.plotData(); } else if (currentMode === 'export') { - exportData(); + indicatorHandler.exportData(); } else { - previewData(); + indicatorHandler.previewData(); } } diff --git a/src/datasources/resources.py b/src/datasources/resources.py index 15a337c..07aa6bd 100644 --- a/src/datasources/resources.py +++ b/src/datasources/resources.py @@ -27,11 +27,21 @@ def process_links(row, dua_column_name="DUA", link_column_name="Link"): links.append(link.id) else: for match in matches: - link, _ = Link.objects.get_or_create(url=match[1], defaults={'link_type': match[0], }) + link, _ = Link.objects.get_or_create( + url=match[1], + defaults={ + "link_type": match[0], + }, + ) links.append(link.id) row["Links"] = links +def process_datasource_name(row): + if row["Name"]: + row["Name"] = row["Name"].capitalize() + + def process_datasources(row): datasource, _ = DataSource.objects.get_or_create( name=row["DB Source"], @@ -67,5 +77,6 @@ class Meta: skip_unchanged = True def before_import_row(self, row, **kwargs): + process_datasource_name(row) process_links(row) process_datasources(row) diff --git a/src/fixtures/severity_pyramid_rungs.json b/src/fixtures/severity_pyramid_rungs.json index 163b91d..cff74c7 100644 --- a/src/fixtures/severity_pyramid_rungs.json +++ b/src/fixtures/severity_pyramid_rungs.json @@ -82,9 +82,10 @@ "fields": { "created": "2024-08-15T09:57:49.327Z", "modified": "2024-08-15T13:07:46.136Z", - "name": "Population", - "display_name": "Population", - "used_in": "signal_sets" + "name": "Entire Population", + "display_name": "Entire Population", + "used_in": "signal_sets", + "display_order_number": 1 } }, { @@ -95,7 +96,8 @@ "modified": "2024-08-15T13:07:46.136Z", "name": "Vaccinated", "display_name": "Vaccinated", - "used_in": "signal_sets" + "used_in": "signal_sets", + "display_order_number": 2 } }, { @@ -106,7 +108,8 @@ "modified": "2024-08-15T13:07:46.136Z", "name": "Infected", "display_name": "Infected", - "used_in": "signal_sets" + "used_in": "signal_sets", + "display_order_number": 3 } }, { @@ -117,7 +120,8 @@ "modified": "2024-08-15T13:07:46.136Z", "name": "Tested", "display_name": "Tested", - "used_in": "signal_sets" + "used_in": "signal_sets", + "display_order_number": 4 } }, { @@ -126,9 +130,10 @@ "fields": { "created": "2024-08-15T09:57:49.327Z", "modified": "2024-08-15T13:07:46.136Z", - "name": "Case", - "display_name": "Case", - "used_in": "signal_sets" + "name": "Ascertained (Case)", + "display_name": "Ascertained (Case)", + "used_in": "signal_sets", + "display_order_number": 5 } }, { @@ -139,7 +144,8 @@ "modified": "2024-08-15T13:07:46.136Z", "name": "Symptomatic", "display_name": "Symptomatic", - "used_in": "signal_sets" + "used_in": "signal_sets", + "display_order_number": 6 } }, { @@ -150,7 +156,8 @@ "modified": "2024-08-15T13:07:46.136Z", "name": "Outpatient / ED", "display_name": "Outpatient / ED", - "used_in": "signal_sets" + "used_in": "signal_sets", + "display_order_number": 7 } }, { @@ -161,7 +168,8 @@ "modified": "2024-08-15T13:07:46.136Z", "name": "Hospitalized", "display_name": "Hospitalized", - "used_in": "signal_sets" + "used_in": "signal_sets", + "display_order_number": 8 } }, { @@ -172,7 +180,8 @@ "modified": "2024-08-15T13:07:46.136Z", "name": "ICU", "display_name": "ICU", - "used_in": "signal_sets" + "used_in": "signal_sets", + "display_order_number": 9 } }, { @@ -183,7 +192,8 @@ "modified": "2024-08-15T13:07:46.136Z", "name": "Deceased", "display_name": "Deceased", - "used_in": "signal_sets" + "used_in": "signal_sets", + "display_order_number": 10 } } ] diff --git a/src/signal_sets/forms.py b/src/signal_sets/forms.py index 8c767cc..2dd9a6b 100644 --- a/src/signal_sets/forms.py +++ b/src/signal_sets/forms.py @@ -35,7 +35,7 @@ class SignalSetFilterForm(forms.ModelForm): queryset=SeverityPyramidRung.objects.filter( # id__in=SignalSet.objects.values_list("severity_pyramid_rungs", flat="True") used_in="signal_sets" - ), + ).order_by("display_order_number"), widget=forms.CheckboxSelectMultiple(), ) diff --git a/src/signal_sets/models.py b/src/signal_sets/models.py index aded6b7..d23e7b5 100644 --- a/src/signal_sets/models.py +++ b/src/signal_sets/models.py @@ -248,4 +248,4 @@ class Meta: @property def get_available_geographies(self): - return ", ".join([geo.display_name for geo in self.available_geographies.all()]) + return [geo.display_name for geo in self.available_geographies.all()] diff --git a/src/signal_sets/resources.py b/src/signal_sets/resources.py index 2eb437a..77e60aa 100644 --- a/src/signal_sets/resources.py +++ b/src/signal_sets/resources.py @@ -152,7 +152,7 @@ class SignalSetResource(resources.ModelResource): ) censoring = Field(attribute="censoring", column_name="Censoring") missingness = Field(attribute="missingness", column_name="Missingness") - dua_required = Field(attribute="dua_required", column_name="DUA required?") + dua_required = Field(attribute="dua_required", column_name="DUA Required?") license = Field(attribute="license", column_name="Data Use Terms") dataset_location = Field( attribute="dataset_location", column_name="Dataset Location" @@ -221,8 +221,10 @@ def skip_row(self, instance, original, row, import_validation_errors=None): def after_import_row(self, row, row_result, **kwargs): try: signal_set_obj = SignalSet.objects.get(id=row_result.object_id) + signal_set_obj.pathogens.clear() + signal_set_obj.severity_pyramid_rungs.clear() + signal_set_obj.available_geographies.clear() for pathogen in row["Pathogen(s)/Syndrome(s)"].split(","): - signal_set_obj.pathogens.clear() pathogen = Pathogen.objects.get(name=pathogen, used_in="signal_sets") signal_set_obj.pathogens.add(pathogen) for severity_pyramid_rung in row["Surveillance Categories"].split(","): @@ -231,7 +233,6 @@ def after_import_row(self, row, row_result, **kwargs): used_in="signal_sets" ).first() signal_set_obj.severity_pyramid_rungs.add(severity_pyramid_rung) - for available_geography in row["Geographic Granularity - Delphi"].split(","): available_geography = Geography.objects.get(name=available_geography, used_in="signal_sets") signal_set_obj.available_geographies.add(available_geography) diff --git a/src/signal_sets/views.py b/src/signal_sets/views.py index b9fae1c..1051307 100644 --- a/src/signal_sets/views.py +++ b/src/signal_sets/views.py @@ -86,9 +86,9 @@ def get_url_params(self): url_params_str = f"{url_params_str}&{param_name}={param_value}" return url_params_dict, url_params_str - def get_related_signals(self, queryset): + def get_related_signals(self, queryset, signal_set_ids): related_signals = [] - for signal in queryset.prefetch_related( + for signal in queryset.filter(signal_set__id__in=signal_set_ids).prefetch_related( "signal_set", "source", "severity_pyramid_rung" ): related_signals.append( @@ -124,7 +124,7 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]: context["filter"] = filter context["signal_sets"] = filter.qs context["related_signals"] = json.dumps( - self.get_related_signals(filter.signals_qs) + self.get_related_signals(filter.signals_qs, filter.qs.values_list("id", flat=True)) ) context["available_geographies"] = Geography.objects.filter(used_in="signals") context["geographic_granularities"] = [ diff --git a/src/signals/admin.py b/src/signals/admin.py index 13102a2..531d8fa 100644 --- a/src/signals/admin.py +++ b/src/signals/admin.py @@ -3,7 +3,7 @@ from django.contrib import admin from import_export.admin import ImportExportModelAdmin -from signals.resources import SignalResource, SignalBaseResource +from signals.resources import SignalResource, SignalBaseResource, OtherEndpointSignalResource from signals.models import ( @@ -14,6 +14,7 @@ Pathogen, SeverityPyramidRung, Signal, + OtherEndointSignal, SignalType, SignalGeography, GeographyUnit, @@ -102,7 +103,9 @@ class SeverityPyramidRungAdmin(admin.ModelAdmin): "name", "display_name", "used_in", + "display_order_number", ) + exclude = ("id",) search_fields: tuple[Literal["name"]] = ("name",) @@ -148,6 +151,35 @@ class SignalAdmin(ImportExportModelAdmin): resource_classes: list[type[SignalResource]] = [SignalResource, SignalBaseResource] +@admin.register(OtherEndointSignal) +class OtherEndpointsSignalAdmin(ImportExportModelAdmin): + """ + Admin interface for managing signal objects. + """ + + list_display: tuple[ + Literal["name"], + Literal["signal_type"], + Literal["format_type"], + Literal["category"], + Literal["geographic_scope"], + ] = ("name", "signal_type", "format_type", "category", "geographic_scope") + search_fields: tuple[ + Literal["name"], + Literal["signal_type__name"], + Literal["format_type__name"], + Literal["category__name"], + Literal["geographic_scope__name"], + ] = ( + "name", + "signal_type__name", + "format_type__name", + "category__name", + "geographic_scope__name", + ) + resource_classes: list[type[SignalResource]] = [OtherEndpointSignalResource] + + @admin.register(SignalGeography) class SignalGeographyAdmin(admin.ModelAdmin): """ diff --git a/src/signals/migrations/0017_severitypyramidrung_display_order_number.py b/src/signals/migrations/0017_severitypyramidrung_display_order_number.py new file mode 100644 index 0000000..3b60130 --- /dev/null +++ b/src/signals/migrations/0017_severitypyramidrung_display_order_number.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.7 on 2025-03-12 17:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('signals', '0016_signal_source_view'), + ] + + operations = [ + migrations.AddField( + model_name='severitypyramidrung', + name='display_order_number', + field=models.IntegerField(help_text='Display order number of the severity pyramid rung.', null=True, verbose_name='display order number'), + ), + ] diff --git a/src/signals/migrations/0018_otherendointsignal.py b/src/signals/migrations/0018_otherendointsignal.py new file mode 100644 index 0000000..3542808 --- /dev/null +++ b/src/signals/migrations/0018_otherendointsignal.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.7 on 2025-03-27 18:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('signals', '0017_severitypyramidrung_display_order_number'), + ] + + operations = [ + migrations.CreateModel( + name='OtherEndointSignal', + fields=[ + ], + options={ + 'verbose_name': 'Other Endpoint Signal', + 'verbose_name_plural': 'Other Endpoint Signals', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('signals.signal',), + ), + ] diff --git a/src/signals/models.py b/src/signals/models.py index f9f7070..b55ca0b 100644 --- a/src/signals/models.py +++ b/src/signals/models.py @@ -81,6 +81,12 @@ class SeverityPyramidRung(TimeStampedModel): default="signals", ) + display_order_number: models.IntegerField = models.IntegerField( + verbose_name=_("display order number"), + help_text=_("Display order number of the severity pyramid rung."), + null=True, + ) + class Meta: verbose_name_plural: str = "Severity Pyramid Rungs" unique_together: list[str] = ["name", "used_in"] @@ -569,6 +575,13 @@ def get_display_name(self): return self.name +class OtherEndointSignal(Signal): + class Meta: + proxy = True + verbose_name = "Other Endpoint Signal" + verbose_name_plural = "Other Endpoint Signals" + + class SignalsDbView(models.Model): id = models.BigIntegerField(primary_key=True) name = models.CharField(max_length=255) diff --git a/src/signals/resources.py b/src/signals/resources.py index 267286d..f2bd827 100644 --- a/src/signals/resources.py +++ b/src/signals/resources.py @@ -155,9 +155,14 @@ def process_available_geographies(row) -> None: "display_order_number": max_display_order_number + 1, }, ) - signal = Signal.objects.get( - name=row["Indicator"], source=row["Source Subdivision"] - ) + try: + signal = Signal.objects.get( + name=row["Indicator"], source=row["Source Subdivision"] + ) + except KeyError: + signal = Signal.objects.get( + name=row["Signal"], source=row["Source Subdivision"] + ) signal_geography, _ = SignalGeography.objects.get_or_create( geography=geography_instance, signal=signal ) @@ -167,12 +172,12 @@ def process_available_geographies(row) -> None: def process_base(row) -> None: - if row["Indicator BaseName"]: + if row["Signal BaseName"]: source: SourceSubdivision = SourceSubdivision.objects.get( name=row["Source Subdivision"] ) base_signal: Signal = Signal.objects.get( - name=row["Indicator BaseName"], source=source + name=row["Signal BaseName"], source=source ) row["base"] = base_signal.id @@ -180,7 +185,7 @@ def process_base(row) -> None: class ModelResource(resources.ModelResource): def get_field_names(self): names = [] - for field in self.get_fields(): + for field in list(self.fields.values()): names.append(self.get_field_name(field)) return names @@ -213,7 +218,7 @@ class SignalBaseResource(ModelResource): Resource class for importing Signals base. """ - name = Field(attribute="name", column_name="Indicator") + name = Field(attribute="name", column_name="Signal") display_name = Field(attribute="display_name", column_name="Name") base = Field( attribute="base", @@ -241,6 +246,200 @@ class SignalResource(ModelResource): Resource class for importing and exporting Signal models """ + name = Field(attribute="name", column_name="Signal") + display_name = Field(attribute="display_name", column_name="Name") + member_name = Field(attribute="member_name", column_name="Member API Name") + member_short_name = Field( + attribute="member_short_name", column_name="Member Short Name" + ) + member_description = Field( + attribute="member_description", column_name="Member Description" + ) + pathogen = Field( + attribute="pathogen", + column_name="Pathogen/\nDisease Area", + widget=widgets.ManyToManyWidget(Pathogen, field="name", separator=","), + ) + signal_type = Field( + attribute="signal_type", + column_name="Indicator Type", + widget=widgets.ForeignKeyWidget(SignalType, field="name"), + ) + active = Field(attribute="active", column_name="Active") + description = Field(attribute="description", column_name="Description") + short_description = Field( + attribute="short_description", column_name="Short Description" + ) + format_type = Field( + attribute="format_type", + column_name="Format", + widget=widgets.ForeignKeyWidget(FormatType, field="name"), + ) + time_type = Field(attribute="time_type", column_name="Time Type") + time_label = Field(attribute="time_label", column_name="Time Label") + reporting_cadence = Field( + attribute="reporting_cadence", column_name="Reporting Cadence" + ) + typical_reporting_lag = Field( + attribute="typical_reporting_lag", column_name="Typical Reporting Lag" + ) + typical_revision_cadence = Field( + attribute="typical_revision_cadence", column_name="Typical Revision Cadence" + ) + demographic_scope = Field( + attribute="demographic_scope", column_name="Population" + ) + severity_pyramid_rung = Field( + attribute="severity_pyramid_rung", + column_name="Surveillance Categories", + widget=widgets.ForeignKeyWidget(SeverityPyramidRung), + ) + category = Field( + attribute="category", + column_name="Category", + widget=widgets.ForeignKeyWidget(Category, "name"), + ) + geographic_scope = Field( + attribute="geographic_scope", + column_name="Geographic Coverage", + widget=widgets.ForeignKeyWidget(GeographicScope), + ) + available_geographies = Field( + attribute="available_geography", + column_name="Geographic Levels", + widget=widgets.ManyToManyWidget(Geography, field="name", separator=","), + ) + temporal_scope_start = Field( + attribute="temporal_scope_start", column_name="Temporal Scope Start" + ) + temporal_scope_start_note = Field( + attribute="temporal_scope_start_note", column_name="Temporal Scope Start Note" + ) + temporal_scope_end = Field( + attribute="temporal_scope_end", column_name="Temporal Scope End" + ) + temporal_scope_end_note = Field( + attribute="temporal_scope_end_note", column_name="Temporal Scope End Note" + ) + is_smoothed = Field(attribute="is_smoothed", column_name="Is Smoothed") + is_weighted = Field(attribute="is_weighted", column_name="Is Weighted") + is_cumulative = Field(attribute="is_cumulative", column_name="Is Cumulative") + has_stderr = Field(attribute="has_stderr", column_name="Has StdErr") + has_sample_size = Field(attribute="has_sample_size", column_name="Has Sample Size") + high_values_are = Field(attribute="high_values_are", column_name="High Values Are") + source = Field( + attribute="source", + column_name="Source Subdivision", + widget=widgets.ForeignKeyWidget(SourceSubdivision, field="name"), + ) + data_censoring = Field(attribute="data_censoring", column_name="Data Censoring") + missingness = Field(attribute="missingness", column_name="Missingness") + organization_access_list = Field( + attribute="organization_access_list", column_name="Who may access this indicator?" + ) + organization_sharing_list = Field( + attribute="organization_sharing_list", + column_name="Who may be told about this indicator?", + ) + license = Field(attribute="license", column_name="Data Use Terms") + restrictions = Field(attribute="restrictions", column_name="Use Restrictions") + signal_set = Field( + attribute="signal_set", + column_name="Indicator Set", + widget=widgets.ForeignKeyWidget(SignalSet, field="name"), + ) + + class Meta: + model = Signal + fields: list[str] = [ + "name", + "display_name", + "member_name", + "member_short_name", + "member_description", + "pathogen", + "signal_type", + "active", + "description", + "short_description", + "time_label", + "reporting_cadence", + "typical_reporting_lag", + "typical_revision_cadence", + "demographic_scope", + "category", + "geographic_scope", + "available_geographies", + "temporal_scope_start", + "temporal_scope_start_note", + "temporal_scope_end", + "temporal_scope_end_note", + "is_smoothed", + "is_weighted", + "is_cumulative", + "has_stderr", + "has_sample_size", + "high_values_are", + "source", + "data_censoring", + "missingness", + "organization_access_list", + "organization_sharing_list", + "license", + "restrictions", + "time_type", + "signal_set", + "format_type", + "severity_pyramid_rung", + ] + import_id_fields: list[str] = ["name", "source"] + store_instance = True + skip_unchanged = True + + def before_import_row(self, row, **kwargs) -> None: + fix_boolean_fields(row) + process_pathogen(row) + process_signal_type(row) + process_format_type(row) + process_severity_pyramid_rungs(row) + process_category(row) + process_geographic_scope(row) + process_source(row) + process_links(row, dua_column_name="Link to DUA", link_column_name="Link") + if not row.get("Indicator Set"): + row["Indicator Set"] = None + if not row.get("Source Subdivision"): + row["Source Subdivision"] = None + + def skip_row(self, instance, original, row, import_validation_errors=None): + if not row["Include in indicator app"]: + try: + signal = Signal.objects.get( + name=row["Signal"], source=row["Source Subdivision"] + ) + signal.delete() + except Signal.DoesNotExist: + pass + return True + + def after_import_row(self, row, row_result, **kwargs): + try: + signal_obj = Signal.objects.get(id=row_result.object_id) + for link in row["Links"]: + signal_obj.related_links.add(link) + process_available_geographies(row) + signal_obj.severity_pyramid_rung = SeverityPyramidRung.objects.get(id=row["Surveillance Categories"]) + signal_obj.format_type = row["Format"] + signal_obj.save() + except Signal.DoesNotExist as e: + print(f"Signal.DoesNotExist: {e}") + + +class OtherEndpointSignalResource(ModelResource): + """ + Resource class for importing and exporting Signal models + """ + name = Field(attribute="name", column_name="Indicator") display_name = Field(attribute="display_name", column_name="Name") member_name = Field(attribute="member_name", column_name="Member API Name") diff --git a/src/staticfiles/admin/css/autocomplete.css b/src/staticfiles/admin/css/autocomplete.css index 7478c2c..69c94e7 100644 --- a/src/staticfiles/admin/css/autocomplete.css +++ b/src/staticfiles/admin/css/autocomplete.css @@ -273,7 +273,3 @@ select.admin-autocomplete { display: block; padding: 6px; } - -.errors .select2-selection { - border: 1px solid var(--error-fg); -} diff --git a/src/staticfiles/admin/css/base.css b/src/staticfiles/admin/css/base.css index ac28326..44f2fc8 100644 --- a/src/staticfiles/admin/css/base.css +++ b/src/staticfiles/admin/css/base.css @@ -13,7 +13,6 @@ html[data-theme="light"], --body-fg: #333; --body-bg: #fff; --body-quiet-color: #666; - --body-medium-color: #444; --body-loud-color: #000; --header-color: #ffc; @@ -85,8 +84,6 @@ html[data-theme="light"], "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - - color-scheme: light; } html, body { @@ -150,6 +147,7 @@ h1 { margin: 0 0 20px; font-weight: 300; font-size: 1.25rem; + color: var(--body-quiet-color); } h2 { @@ -165,7 +163,7 @@ h2.subhead { h3 { font-size: 0.875rem; margin: .8em 0 .3em 0; - color: var(--body-medium-color); + color: var(--body-quiet-color); font-weight: bold; } @@ -173,7 +171,6 @@ h4 { font-size: 0.75rem; margin: 1em 0 .8em 0; padding-bottom: 3px; - color: var(--body-medium-color); } h5 { @@ -220,10 +217,6 @@ fieldset { border-top: 1px solid var(--hairline-color); } -details summary { - cursor: pointer; -} - blockquote { font-size: 0.6875rem; color: #777; @@ -320,7 +313,7 @@ td, th { } th { - font-weight: 500; + font-weight: 600; text-align: left; } @@ -341,7 +334,7 @@ tfoot td { } thead th.required { - font-weight: bold; + color: var(--body-loud-color); } tr.alt { @@ -489,13 +482,8 @@ textarea { vertical-align: top; } -/* -Minifiers remove the default (text) "type" attribute from "input" HTML tags. -Add input:not([type]) to make the CSS stylesheet work the same. -*/ -input:not([type]), input[type=text], input[type=password], input[type=email], -input[type=url], input[type=number], input[type=tel], textarea, select, -.vTextField { +input[type=text], input[type=password], input[type=email], input[type=url], +input[type=number], input[type=tel], textarea, select, .vTextField { border: 1px solid var(--border-color); border-radius: 4px; padding: 5px 6px; @@ -504,13 +492,9 @@ input[type=url], input[type=number], input[type=tel], textarea, select, background-color: var(--body-bg); } -/* -Minifiers remove the default (text) "type" attribute from "input" HTML tags. -Add input:not([type]) to make the CSS stylesheet work the same. -*/ -input:not([type]):focus, input[type=text]:focus, input[type=password]:focus, -input[type=email]:focus, input[type=url]:focus, input[type=number]:focus, -input[type=tel]:focus, textarea:focus, select:focus, .vTextField:focus { +input[type=text]:focus, input[type=password]:focus, input[type=email]:focus, +input[type=url]:focus, input[type=number]:focus, input[type=tel]:focus, +textarea:focus, select:focus, .vTextField:focus { border-color: var(--body-quiet-color); } @@ -894,10 +878,9 @@ a.deletelink:focus, a.deletelink:hover { margin-right: -300px; } -@media (forced-colors: active) { - #content-related { - border: 1px solid; - } +#footer { + clear: both; + padding: 10px; } /* COLUMN TYPES */ @@ -945,12 +928,6 @@ a.deletelink:focus, a.deletelink:hover { text-decoration: underline; } -@media (forced-colors: active) { - #header { - border-bottom: 1px solid; - } -} - #branding { display: flex; } diff --git a/src/staticfiles/admin/css/changelists.css b/src/staticfiles/admin/css/changelists.css index 005b776..573c389 100644 --- a/src/staticfiles/admin/css/changelists.css +++ b/src/staticfiles/admin/css/changelists.css @@ -139,12 +139,6 @@ margin: 0 0 0 30px; } -@media (forced-colors: active) { - #changelist-filter { - border: 1px solid; - } -} - #changelist-filter h2 { font-size: 0.875rem; text-transform: uppercase; @@ -159,6 +153,7 @@ font-weight: 400; padding: 0 15px; margin-bottom: 10px; + cursor: pointer; } #changelist-filter details summary > * { diff --git a/src/staticfiles/admin/css/dark_mode.css b/src/staticfiles/admin/css/dark_mode.css index 7e12a81..c49b6bc 100644 --- a/src/staticfiles/admin/css/dark_mode.css +++ b/src/staticfiles/admin/css/dark_mode.css @@ -5,8 +5,7 @@ --body-fg: #eeeeee; --body-bg: #121212; - --body-quiet-color: #d0d0d0; - --body-medium-color: #e0e0e0; + --body-quiet-color: #e0e0e0; --body-loud-color: #ffffff; --breadcrumbs-link-fg: #e0e0e0; @@ -30,8 +29,6 @@ --close-button-bg: #333333; --close-button-hover-bg: #666666; - - color-scheme: dark; } } @@ -42,8 +39,7 @@ html[data-theme="dark"] { --body-fg: #eeeeee; --body-bg: #121212; - --body-quiet-color: #d0d0d0; - --body-medium-color: #e0e0e0; + --body-quiet-color: #e0e0e0; --body-loud-color: #ffffff; --breadcrumbs-link-fg: #e0e0e0; @@ -67,8 +63,6 @@ html[data-theme="dark"] { --close-button-bg: #333333; --close-button-hover-bg: #666666; - - color-scheme: dark; } /* THEME SWITCH */ diff --git a/src/staticfiles/admin/css/forms.css b/src/staticfiles/admin/css/forms.css index 4f49b61..9a8dad0 100644 --- a/src/staticfiles/admin/css/forms.css +++ b/src/staticfiles/admin/css/forms.css @@ -44,6 +44,7 @@ label { .required label, label.required { font-weight: bold; + color: var(--body-fg); } /* RADIO BUTTONS */ @@ -75,20 +76,6 @@ form ul.inline li { padding-right: 7px; } -/* FIELDSETS */ - -fieldset .fieldset-heading, -fieldset .inline-heading, -:not(.inline-related) .collapse summary { - border: 1px solid var(--header-bg); - margin: 0; - padding: 8px; - font-weight: 400; - font-size: 0.8125rem; - background: var(--header-bg); - color: var(--header-link-color); -} - /* ALIGNED FIELDSETS */ .aligned label { @@ -97,12 +84,14 @@ fieldset .inline-heading, min-width: 160px; width: 160px; word-wrap: break-word; + line-height: 1; } .aligned label:not(.vCheckboxLabel):after { content: ''; display: inline-block; vertical-align: middle; + height: 1.625rem; } .aligned label + p, .aligned .checkbox-row + div.help, .aligned label + div.readonly { @@ -169,10 +158,6 @@ form .aligned select + div.help { padding-left: 10px; } -form .aligned select option:checked { - background-color: var(--selected-row); -} - form .aligned ul li { list-style: none; } @@ -183,7 +168,11 @@ form .aligned table p { } .aligned .vCheckboxLabel { - padding: 1px 0 0 5px; + float: none; + width: auto; + display: inline-block; + vertical-align: -3px; + padding: 0 0 5px 5px; } .aligned .vCheckboxLabel + p.help, @@ -205,8 +194,14 @@ fieldset .fieldBox { width: 200px; } -form .wide p.help, +form .wide p, form .wide ul.errorlist, +form .wide input + p.help, +form .wide input + div.help { + margin-left: 200px; +} + +form .wide p.help, form .wide div.help { padding-left: 50px; } @@ -220,16 +215,35 @@ form div.help ul { width: 450px; } -/* COLLAPSIBLE FIELDSETS */ +/* COLLAPSED FIELDSETS */ + +fieldset.collapsed * { + display: none; +} + +fieldset.collapsed h2, fieldset.collapsed { + display: block; +} + +fieldset.collapsed { + border: 1px solid var(--hairline-color); + border-radius: 4px; + overflow: hidden; +} + +fieldset.collapsed h2 { + background: var(--darkened-bg); + color: var(--body-quiet-color); +} + +fieldset .collapse-toggle { + color: var(--header-link-color); +} -.collapse summary .fieldset-heading, -.collapse summary .inline-heading { +fieldset.collapsed .collapse-toggle { background: transparent; - border: none; - color: currentColor; display: inline; - margin: 0; - padding: 0; + color: var(--link-fg); } /* MONOSPACE TEXTAREAS */ @@ -381,16 +395,14 @@ body.popup .submit-row { position: relative; } -.inline-related h4, -.inline-related:not(.tabular) .collapse summary { +.inline-related h3 { margin: 0; - color: var(--body-medium-color); + color: var(--body-quiet-color); padding: 5px; font-size: 0.8125rem; background: var(--darkened-bg); - border: 1px solid var(--hairline-color); - border-left-color: var(--darkened-bg); - border-right-color: var(--darkened-bg); + border-top: 1px solid var(--hairline-color); + border-bottom: 1px solid var(--hairline-color); } .inline-related h3 span.delete { @@ -409,6 +421,16 @@ body.popup .submit-row { width: 100%; } +.inline-related fieldset.module h3 { + margin: 0; + padding: 2px 5px 3px 5px; + font-size: 0.6875rem; + text-align: left; + font-weight: bold; + background: #bcd; + color: var(--body-bg); +} + .inline-group .tabular fieldset.module { border: none; } diff --git a/src/staticfiles/admin/css/login.css b/src/staticfiles/admin/css/login.css index 805a34b..389772f 100644 --- a/src/staticfiles/admin/css/login.css +++ b/src/staticfiles/admin/css/login.css @@ -21,7 +21,7 @@ } .login #content { - padding: 20px; + padding: 20px 20px 0; } .login #container { diff --git a/src/staticfiles/admin/css/responsive.css b/src/staticfiles/admin/css/responsive.css index 932e824..bb53945 100644 --- a/src/staticfiles/admin/css/responsive.css +++ b/src/staticfiles/admin/css/responsive.css @@ -171,14 +171,9 @@ input[type="submit"], button { /* Forms */ label { - font-size: 1rem; + font-size: 0.875rem; } - /* - Minifiers remove the default (text) "type" attribute from "input" HTML - tags. Add input:not([type]) to make the CSS stylesheet work the same. - */ - .form-row input:not([type]), .form-row input[type=text], .form-row input[type=password], .form-row input[type=email], @@ -192,7 +187,7 @@ input[type="submit"], button { margin: 0; padding: 6px 8px; min-height: 2.25rem; - font-size: 1rem; + font-size: 0.875rem; } .form-row select { @@ -451,10 +446,14 @@ input[type="submit"], button { @media (max-width: 767px) { /* Layout */ - #header, #content { + #header, #content, #footer { padding: 15px; } + #footer:empty { + padding: 0; + } + div.breadcrumbs { padding: 10px 15px; } @@ -565,6 +564,10 @@ input[type="submit"], button { padding-top: 15px; } + fieldset.collapsed .form-row { + display: none; + } + .aligned label { width: 100%; min-width: auto; diff --git a/src/staticfiles/admin/css/responsive_rtl.css b/src/staticfiles/admin/css/responsive_rtl.css index 33b5784..31dc8ff 100644 --- a/src/staticfiles/admin/css/responsive_rtl.css +++ b/src/staticfiles/admin/css/responsive_rtl.css @@ -35,6 +35,11 @@ background-position: calc(100% - 8px) 9px; } + [dir="rtl"] .related-widget-wrapper-link + .selector { + margin-right: 0; + margin-left: 15px; + } + [dir="rtl"] .selector .selector-filter label { margin-right: 0; margin-left: 8px; @@ -53,22 +58,6 @@ padding-left: 0; padding-right: 16px; } - - [dir="rtl"] .selector-add { - background-position: 0 -80px; - } - - [dir="rtl"] .selector-remove { - background-position: 0 -120px; - } - - [dir="rtl"] .active.selector-add:focus, .active.selector-add:hover { - background-position: 0 -100px; - } - - [dir="rtl"] .active.selector-remove:focus, .active.selector-remove:hover { - background-position: 0 -140px; - } } /* MOBILE */ @@ -92,20 +81,4 @@ [dir="rtl"] .aligned .vCheckboxLabel { padding: 1px 5px 0 0; } - - [dir="rtl"] .selector-remove { - background-position: 0 0; - } - - [dir="rtl"] .active.selector-remove:focus, .active.selector-remove:hover { - background-position: 0 -20px; - } - - [dir="rtl"] .selector-add { - background-position: 0 -40px; - } - - [dir="rtl"] .active.selector-add:focus, .active.selector-add:hover { - background-position: 0 -60px; - } } diff --git a/src/staticfiles/admin/css/rtl.css b/src/staticfiles/admin/css/rtl.css index b8f60e0..9027c7e 100644 --- a/src/staticfiles/admin/css/rtl.css +++ b/src/staticfiles/admin/css/rtl.css @@ -151,7 +151,6 @@ form ul.inline li { form .aligned p.help, form .aligned div.help { - margin-left: 0; margin-right: 160px; padding-right: 10px; } @@ -165,13 +164,19 @@ form .aligned p.time div.help.timezonewarning { padding-right: 0; } -form .wide p.help, -form .wide ul.errorlist, -form .wide div.help { +form .wide p.help, form .wide div.help { padding-left: 0; padding-right: 50px; } +form .wide p, +form .wide ul.errorlist, +form .wide input + p.help, +form .wide input + div.help { + margin-right: 200px; + margin-left: 0px; +} + .submit-row { text-align: right; } @@ -197,7 +202,12 @@ fieldset .fieldBox { top: 0; left: auto; right: 10px; - background: url(../img/calendar-icons.svg) 0 -15px no-repeat; + background: url(../img/calendar-icons.svg) 0 -30px no-repeat; +} + +.calendarbox .calendarnav-previous:focus, +.calendarbox .calendarnav-previous:hover { + background-position: 0 -45px; } .calendarnav-next { @@ -207,6 +217,11 @@ fieldset .fieldBox { background: url(../img/calendar-icons.svg) 0 0 no-repeat; } +.calendarbox .calendarnav-next:focus, +.calendarbox .calendarnav-next:hover { + background-position: 0 -15px; +} + .calendar caption, .calendarbox h2 { text-align: center; } @@ -282,10 +297,6 @@ form .form-row p.datetime { margin-right: 2px; } -.inline-group .tabular td.original p { - right: 0; -} - .selector .selector-chooser { margin: 0; } diff --git a/src/staticfiles/admin/css/widgets.css b/src/staticfiles/admin/css/widgets.css index cc64811..d3d4732 100644 --- a/src/staticfiles/admin/css/widgets.css +++ b/src/staticfiles/admin/css/widgets.css @@ -519,9 +519,19 @@ span.clearable-file-input label { background: url(../img/calendar-icons.svg) 0 0 no-repeat; } +.calendarbox .calendarnav-previous:focus, +.calendarbox .calendarnav-previous:hover { + background-position: 0 -15px; +} + .calendarnav-next { right: 10px; - background: url(../img/calendar-icons.svg) 0 -15px no-repeat; + background: url(../img/calendar-icons.svg) 0 -30px no-repeat; +} + +.calendarbox .calendarnav-next:focus, +.calendarbox .calendarnav-next:hover { + background-position: 0 -45px; } .calendar-cancel { diff --git a/src/staticfiles/admin/img/README.txt b/src/staticfiles/admin/img/README.txt index bf81f35..4eb2e49 100644 --- a/src/staticfiles/admin/img/README.txt +++ b/src/staticfiles/admin/img/README.txt @@ -1,4 +1,4 @@ -All icons are taken from Font Awesome (https://fontawesome.com/) project. +All icons are taken from Font Awesome (http://fontawesome.io/) project. The Font Awesome font is licensed under the SIL OFL 1.1: - https://scripts.sil.org/OFL diff --git a/src/staticfiles/admin/img/calendar-icons.svg b/src/staticfiles/admin/img/calendar-icons.svg index 04c0274..dbf21c3 100644 --- a/src/staticfiles/admin/img/calendar-icons.svg +++ b/src/staticfiles/admin/img/calendar-icons.svg @@ -1,63 +1,14 @@ - - - - - - + + + + - - + + - - + + + + diff --git a/src/staticfiles/admin/img/icon-addlink.svg b/src/staticfiles/admin/img/icon-addlink.svg index 8d5c6a3..e004fb1 100644 --- a/src/staticfiles/admin/img/icon-addlink.svg +++ b/src/staticfiles/admin/img/icon-addlink.svg @@ -1,3 +1,3 @@ - + diff --git a/src/staticfiles/admin/img/icon-changelink.svg b/src/staticfiles/admin/img/icon-changelink.svg index 592b093..bbb137a 100644 --- a/src/staticfiles/admin/img/icon-changelink.svg +++ b/src/staticfiles/admin/img/icon-changelink.svg @@ -1,3 +1,3 @@ - + diff --git a/src/staticfiles/admin/js/SelectFilter2.js b/src/staticfiles/admin/js/SelectFilter2.js index 6957412..fc59eba 100644 --- a/src/staticfiles/admin/js/SelectFilter2.js +++ b/src/staticfiles/admin/js/SelectFilter2.js @@ -1,4 +1,4 @@ -/*global SelectBox, gettext, ngettext, interpolate, quickElement, SelectFilter*/ +/*global SelectBox, gettext, interpolate, quickElement, SelectFilter*/ /* SelectFilter2 - Turns a multiple-select box into a filter interface. diff --git a/src/staticfiles/admin/js/actions.js b/src/staticfiles/admin/js/actions.js index 04b25e9..6a2ae91 100644 --- a/src/staticfiles/admin/js/actions.js +++ b/src/staticfiles/admin/js/actions.js @@ -1,4 +1,4 @@ -/*global gettext, interpolate, ngettext, Actions*/ +/*global gettext, interpolate, ngettext*/ 'use strict'; { function show(selector) { diff --git a/src/staticfiles/admin/js/admin/RelatedObjectLookups.js b/src/staticfiles/admin/js/admin/RelatedObjectLookups.js index bc3acce..32e3f5b 100644 --- a/src/staticfiles/admin/js/admin/RelatedObjectLookups.js +++ b/src/staticfiles/admin/js/admin/RelatedObjectLookups.js @@ -96,8 +96,8 @@ // Extract the model from the popup url '...//add/' or // '...///change/' depending the action (add or change). const modelName = path.split('/')[path.split('/').length - (objId ? 4 : 3)]; - // Select elements with a specific model reference and context of "available-source". - const selectsRelated = document.querySelectorAll(`[data-model-ref="${modelName}"] [data-context="available-source"]`); + // Exclude autocomplete selects. + const selectsRelated = document.querySelectorAll(`[data-model-ref="${modelName}"] select:not(.admin-autocomplete)`); selectsRelated.forEach(function(select) { if (currentSelect === select) { diff --git a/src/staticfiles/admin/js/popup_response.js b/src/staticfiles/admin/js/popup_response.js index fecf0f4..2b1d3dd 100644 --- a/src/staticfiles/admin/js/popup_response.js +++ b/src/staticfiles/admin/js/popup_response.js @@ -1,3 +1,4 @@ +/*global opener */ 'use strict'; { const initData = JSON.parse(document.getElementById('django-admin-popup-response-constants').dataset.popupResponse); diff --git a/src/staticfiles/admin/js/theme.js b/src/staticfiles/admin/js/theme.js index e79d375..794cd15 100644 --- a/src/staticfiles/admin/js/theme.js +++ b/src/staticfiles/admin/js/theme.js @@ -1,51 +1,56 @@ 'use strict'; { - function setTheme(mode) { - if (mode !== "light" && mode !== "dark" && mode !== "auto") { - console.error(`Got invalid theme mode: ${mode}. Resetting to auto.`); - mode = "auto"; + window.addEventListener('load', function(e) { + + function setTheme(mode) { + if (mode !== "light" && mode !== "dark" && mode !== "auto") { + console.error(`Got invalid theme mode: ${mode}. Resetting to auto.`); + mode = "auto"; + } + document.documentElement.dataset.theme = mode; + localStorage.setItem("theme", mode); } - document.documentElement.dataset.theme = mode; - localStorage.setItem("theme", mode); - } - function cycleTheme() { - const currentTheme = localStorage.getItem("theme") || "auto"; - const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + function cycleTheme() { + const currentTheme = localStorage.getItem("theme") || "auto"; + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; - if (prefersDark) { - // Auto (dark) -> Light -> Dark - if (currentTheme === "auto") { - setTheme("light"); - } else if (currentTheme === "light") { - setTheme("dark"); - } else { - setTheme("auto"); - } - } else { - // Auto (light) -> Dark -> Light - if (currentTheme === "auto") { - setTheme("dark"); - } else if (currentTheme === "dark") { - setTheme("light"); + if (prefersDark) { + // Auto (dark) -> Light -> Dark + if (currentTheme === "auto") { + setTheme("light"); + } else if (currentTheme === "light") { + setTheme("dark"); + } else { + setTheme("auto"); + } } else { - setTheme("auto"); + // Auto (light) -> Dark -> Light + if (currentTheme === "auto") { + setTheme("dark"); + } else if (currentTheme === "dark") { + setTheme("light"); + } else { + setTheme("auto"); + } } } - } - function initTheme() { - // set theme defined in localStorage if there is one, or fallback to auto mode - const currentTheme = localStorage.getItem("theme"); - currentTheme ? setTheme(currentTheme) : setTheme("auto"); - } + function initTheme() { + // set theme defined in localStorage if there is one, or fallback to auto mode + const currentTheme = localStorage.getItem("theme"); + currentTheme ? setTheme(currentTheme) : setTheme("auto"); + } - window.addEventListener('load', function(_) { - const buttons = document.getElementsByClassName("theme-toggle"); - Array.from(buttons).forEach((btn) => { - btn.addEventListener("click", cycleTheme); - }); - }); + function setupTheme() { + // Attach event handlers for toggling themes + const buttons = document.getElementsByClassName("theme-toggle"); + Array.from(buttons).forEach((btn) => { + btn.addEventListener("click", cycleTheme); + }); + initTheme(); + } - initTheme(); + setupTheme(); + }); } diff --git a/src/templates/signal_sets/signal_sets.html b/src/templates/signal_sets/signal_sets.html index 64e51b0..d00a952 100644 --- a/src/templates/signal_sets/signal_sets.html +++ b/src/templates/signal_sets/signal_sets.html @@ -278,6 +278,18 @@

Clear filters +
+
+
+ Report a Data Problem +
+
+ Suggest a New Data Source +
+
+ Other Feedback or Suggestion +
+
@@ -567,7 +579,11 @@

{{ signal_set.geographic_scope }} - {{ signal_set.get_available_geographies }} + {% for available_geography in signal_set.get_available_geographies %} + + {{ available_geography }} + + {% endfor %} {{ signal_set.temporal_scope_start }} @@ -576,7 +592,9 @@

{{ signal_set.temporal_scope_end }} - {{ signal_set.temporal_granularity }} + + {{ signal_set.temporal_granularity }} + {{ signal_set.reporting_cadence }} @@ -708,6 +726,10 @@

+
+
+
+