diff options
-rw-r--r-- | archaeological_operations/models.py | 6 | ||||
-rw-r--r-- | ishtar_common/models.py | 9 | ||||
-rw-r--r-- | ishtar_common/static/js/ishtar-map.js | 35 | ||||
-rw-r--r-- | ishtar_common/static/js/ishtar.js | 279 | ||||
-rw-r--r-- | ishtar_common/templates/base.html | 14 | ||||
-rw-r--r-- | ishtar_common/templates/blocks/DataTables-tabs.html | 14 | ||||
-rw-r--r-- | ishtar_common/templates/blocks/DataTables.html | 64 | ||||
-rw-r--r-- | ishtar_common/views_item.py | 31 | ||||
-rw-r--r-- | version.py | 4 |
9 files changed, 387 insertions, 69 deletions
diff --git a/archaeological_operations/models.py b/archaeological_operations/models.py index 4f180c3d5..d18b76f80 100644 --- a/archaeological_operations/models.py +++ b/archaeological_operations/models.py @@ -17,6 +17,7 @@ # See the file COPYING for details. +from collections import OrderedDict import datetime from itertools import groupby @@ -530,6 +531,11 @@ class Operation(ClosedItem, DocumentItem, BaseHistorizedItem, QRCodeItem, # statistics STATISTIC_MODALITIES = ["year", "operation_type__label", "towns__cached_label"] + STATISTIC_MODALITIES_OPTIONS = OrderedDict([ + ('year', _("Year")), + ("operation_type__label", _("Operation type")), + ("towns__cached_label", _("Towns")), + ]) # search parameters BOOL_FIELDS = ['end_date__isnull', 'virtual_operation', diff --git a/ishtar_common/models.py b/ishtar_common/models.py index 7aa8706b3..8e5983a45 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -1931,10 +1931,13 @@ class TemplateItem: class StatisticItem: - STATISTIC_MODALITIES = [] # example: "operation type", "material type" + STATISTIC_MODALITIES = [] # example: "year", "operation_type__label" + STATISTIC_MODALITIES_OPTIONS = OrderedDict() # example: + # OrderedDict([('year', _("Year")), + # ("operation_type__label", _("Operation type"))]) STATISTIC_SUM_VARIABLE = OrderedDict( - (("pk", _("number")),) - ) # example: "price", "volume" + (("pk", _("Number")),) + ) # example: "Price", "Volume" class BaseHistorizedItem(StatisticItem, TemplateItem, FullSearch, Imported, diff --git a/ishtar_common/static/js/ishtar-map.js b/ishtar_common/static/js/ishtar-map.js index 1b0d4384b..a93813e61 100644 --- a/ishtar_common/static/js/ishtar-map.js +++ b/ishtar_common/static/js/ishtar-map.js @@ -27,6 +27,41 @@ var geolocation = {}; var geoloc_feature = {}; var geoloc_activated = {}; +var fetching_msg = "Fetching data..."; + +var _map_submit_search = function(query_vars, name, source){ + var modal_base_text = $('.modal-progress .modal-header').html(); + $('.modal-progress .modal-header').html(fetching_msg); + $('.modal-progress').modal('show'); + var data = search_get_query_data(query_vars, name); + var nb_select = jQuery("#id_" + name + "-length_map").val(); + if (!nb_select) nb_select = 10; + + var url = source + "json-map?length=" + nb_select + "&submited=1&" + data; + var use_map_limit = false; + if(data.indexOf("no_limit=true") == -1){ + url += "&limit=" + current_map_limit; + use_map_limit = true; + } + $.getJSON(url, function(data) { + var timestamp = Math.floor(Date.now() / 1000); + var map_id = "map-" + timestamp; + $('.modal-progress .modal-header').html("{% trans 'Render map...' %}"); + + var html = render_map(map_id, use_map_limit); + $("#tab-content-map-" + name).html(html); + $("#id_" + name + "-length_map").change(map_submit_search); + if ($('.modal-progress').length > 0){ + $('.modal-progress').modal('hide'); + $('.modal-progress .modal-header').html(modal_base_text); + } + register_map(map_id, data); + }); + + return false; + +}; + var geoloc_activated_message = function(map_id){ setTimeout(function(){ diff --git a/ishtar_common/static/js/ishtar.js b/ishtar_common/static/js/ishtar.js index 2527c1bd7..54823968c 100644 --- a/ishtar_common/static/js/ishtar.js +++ b/ishtar_common/static/js/ishtar.js @@ -1095,6 +1095,33 @@ var get_hover_div = function(table_cols, data){ return hover; }; +var _gallery_submit_search = function(image_page, query_vars, name, source){ + if (image_page) { + current_image_page = image_page; + } else { + current_image_page = 1; + } + $('.modal-progress').modal('show'); + var data = search_get_query_data(query_vars, name); + var nb_select = jQuery("#id_" + name + "-length_image").val(); + if (!nb_select) nb_select = 10; + + var url = source + "json-image?length=" + nb_select + "&submited=1&" + data; + $.getJSON(url, function(data) { + var timestamp = Math.floor(Date.now() / 1000); + var gallery_id = "gallery-" + timestamp; + $("#tab-content-gallery-" + name).html( + render_gallery(data, name, nb_select, gallery_id)); + $("#id_" + name + "-length_image").change(gallery_submit_search); + register_image_gallery(gallery_id); + $('.card[data-toggle="tooltip"]').tooltip(); + if ($('.modal-progress').length > 0){ + $('.modal-progress').modal('hide'); + } + }); + return false; +} + var render_gallery = function(data_table, table_name, nb_select, gallery_id){ var html = '<div class="ishtar-gallery-wrapper container-fluid">'; html += render_paginate_select(table_name, 'image', nb_select); @@ -1343,6 +1370,7 @@ var register_map = function(map_id, points){ var main_submit_search = function(){ if (current_tab == "table") datatable_submit_search(); if (current_tab == "gallery") gallery_submit_search(); + if (current_tab == "stats") stats_submit_search(); if (current_tab == "map") map_submit_search(); }; @@ -1376,3 +1404,254 @@ var search_get_query_data = function(query_vars, table_name){ } return data; }; + +var registered_stats = false; + +var register_stats = function(query_vars, name, source){ + if (registered_stats) return; + registered_stats = true; + $("#stats-form-" + name + " select").on('change', function() { + _stats_submit_search(query_vars, name, source); + } + ); + $('#chart-img-display-' + name).click(function(){ + $('#chart-img-' + name).hide(); + $('#img-' + name).html( + $('<img/>').attr( + 'src', $('#chart-' + name).jqplotToImageStr({}) + ) + ); + $('#chart-img-' + name).show('slow'); + }); +}; + +var _stats_submit_search = function(query_vars, name, source){ + $('.modal-progress').modal('show'); + + var data = search_get_query_data(query_vars, name); + register_stats(query_vars, name, source); + + var url = source + "json-stats?submited=1&" + data; + var modality_1 = $("#stats_modality_1-" + name).val(); + if (modality_1) url += '&stats_modality_1=' + modality_1; + var modality_2 = $("#stats_modality_2-" + name).val(); + if (modality_2) url += '&stats_modality_2=' + modality_2; + $.getJSON(url, function(data) { + var timestamp = Math.floor(Date.now() / 1000); + var stats_id = "stats-" + timestamp; + $("#tab-content-stats-" + name).html( + render_stats(data, name)); + if ($('.modal-progress').length > 0){ + $('.modal-progress').modal('hide'); + } + }); + return false; +} + +var stats_showmarker = false; +var stats_incompatible_modality = "This graph type accept only one modality."; +var stats_current_graph; + +var _render_stats_table = function(stats_values, name){ + var modality_1 = $("#stats_modality_1-" + name).val(); + var modality_2 = $("#stats_modality_2-" + name).val(); + var rows = new Array(); + + var current_row = new Array(); + var html = ""; + html += "<table class='table w-75 mt-4 pt-4 mx-auto'><thead>"; + html += "<tr><th>"; + var modality_1_lbl = $('#stats_modality_1-' + name + ' option:selected').text(); + current_row.push(modality_1_lbl); + html += modality_1_lbl; + if (modality_2 && modality_2 != modality_1){ + html += "</th><th>"; + var modality_2_lbl = $('#stats_modality_2-' + name + ' option:selected').text(); + html += modality_2_lbl; + current_row.push(modality_2_lbl); + } + html += "</th><th>"; + var sum_lbl = $('#stats_sum-' + name + ' option:selected').text(); + html += sum_lbl; + current_row.push(sum_lbl); + html += "</th></tr></thead><tbody>"; + rows.push(current_row); + + if (modality_2 && modality_2 != modality_1){ + for (idx in stats_values){ + var start_row = ""; + row_content = ""; + for (inner_idx in stats_values[idx][1]){ + current_row = new Array(); + var colspan = parseInt(inner_idx) + 1; + start_row = "<tr><td rowspan='" + colspan + "'>" + stats_values[idx][0] + "</td>"; + current_row.push(stats_values[idx][0]); + if (inner_idx > 0) row_content += "<tr>"; + row_content += "<td>" + stats_values[idx][1][inner_idx][0] + "</td>"; + current_row.push(stats_values[idx][1][inner_idx][0]); + row_content += "<td>" + stats_values[idx][1][inner_idx][1] + "</td></tr>"; + current_row.push(stats_values[idx][1][inner_idx][1]); + rows.push(current_row); + } + html += start_row + row_content; + } + } else { + for (idx in stats_values){ + current_row = new Array(); + html += "<tr><td>" + stats_values[idx][0] + "</td>"; + current_row.push(stats_values[idx][0]); + html += "<td>" + stats_values[idx][1] + "</td></tr>"; + current_row.push(stats_values[idx][1]); + rows.push(current_row); + } + } + + html += "</tbody></table>"; + $("#charts-" + name).hide(); + $("#stats-table-content-" + name).html(html); + $("#stats-table-" + name).show(); + + var csv_content = "data:text/csv;charset=utf-8," + rows.map(e => e.join(",")).join("\n"); + var encoded_uri = encodeURI(csv_content); + $("#stats-table-csv-" + name).attr("href", encoded_uri); + $("#stats-table-csv-" + name).attr("download", "ishtar-stats.csv"); +}; + +var render_stats = function(stats_values, name){ + var stats_type = $("#stats_renderer-" + name).val(); + + if (!stats_values || !stats_values['data']) return; + stats_values = stats_values['data']; + + if (stats_current_graph){ + stats_current_graph.destroy(); + } + if (stats_type == "table"){ + return _render_stats_table(stats_values, name); + } + + $("#stats-table-" + name).hide(); + $("#charts-" + name).show(); + var modality_1 = $("#stats_modality_1-" + name).val(); + var modality_2 = $("#stats_modality_2-" + name).val(); + + stats_xaxis = { + label: $('#stats_modality_1-' + name + ' option:selected').text() + }; + + var stats_help = $("#stats-zoom-help-" + name); + + if (stats_type == "pie"){ + stats_help.hide(); + if (modality_2){ + display_info(stats_incompatible_modality); + return; + } + } else { + stats_help.show(); + var stats_xaxis_tickoptions; + if (modality_1 == "year"){ + stats_xaxis_tickoptions = {formatString: "%d"}; + } else { + stats_xaxis_tickoptions = {angle:-25}; + } + stats_xaxis["tickOptions"] = stats_xaxis_tickoptions + } + + var stats_showmarker = false; + if (stats_values.length < 25){ + stats_showmarker = true; + } + var jqvalues = new Array(); + var ticks = new Array(); + var series = new Array(); + var series_conf = new Array(); + if (modality_2 && modality_2 != modality_1){ + for (idx in stats_values){ + ticks.push(stats_values[idx][0]); + var serie_values = stats_values[idx][1]; + var current_serie_values = new Array(); + for (inner_idx in serie_values){ + var serie_value = serie_values[inner_idx][0]; + if (series.indexOf(serie_value) == -1){ + series.push(serie_value); + var fill_values = new Array(); + if (idx > 0){ // put 0 for previous items + for (var fill_idx = 0 ; fill_idx < idx ; fill_idx++ ){ + fill_values.push(0); + } + } + jqvalues.push(fill_values); + } + current_serie_values.push(serie_value); + var current_value = serie_values[inner_idx][1]; + jqvalues[series.indexOf(serie_value)].push(current_value); + } + // put 0 for missing series for this item + for (idx_serie in series){ + if (current_serie_values.indexOf(series[idx_serie]) == -1){ + jqvalues[series.indexOf(series[idx_serie])].push(0); + } + } + } + stats_xaxis['ticks'] = ticks; + } else { + jqvalues = [stats_values]; + series.push("Total"); + } + if (stats_type != "pie"){ + stats_xaxis['renderer'] = $.jqplot.CategoryAxisRenderer; + stats_xaxis['labelRenderer'] = $.jqplot.CanvasAxisLabelRenderer; + stats_xaxis['tickRenderer'] = $.jqplot.CanvasAxisTickRenderer; + } + + var stats_renderer; + if (stats_type == "bar"){ + stats_renderer = $.jqplot.BarRenderer; + } else if (stats_type == "pie"){ + stats_renderer = $.jqplot.PieRenderer; + } + for (idx in series){ + var serie_conf = { + label: series[idx], + showmarker: stats_showmarker + }; + if (stats_renderer){ + serie_conf["renderer"] = stats_renderer; + } + serie_conf["rendererOptions"] = { + padding: 8, showDataLabels: true, + dataLabels: 'value', + fill: false, sliceMargin: 4 + }; + series_conf.push(serie_conf); + } + + var stats_options = { + axes: { + xaxis: stats_xaxis, + yaxis: { + label: $('#stats_sum-' + name + ' option:selected').text(), + min: 0 + } + }, + highlighter: { + show: true, + tooltipAxes: 'y', + sizeAdjust: 7.5 + }, + series: series_conf, + legend: { show:true, location: 'e', placement: 'outside' } + }; + if (stats_type != "pie"){ + stats_options["cursor"] = { + show: true, + zoom: true, + showTooltip: false + } + } + + stats_current_graph = $.jqplot('chart-' + name, + jqvalues, stats_options + ); +}; diff --git a/ishtar_common/templates/base.html b/ishtar_common/templates/base.html index 8d569711e..f88f1eba6 100644 --- a/ishtar_common/templates/base.html +++ b/ishtar_common/templates/base.html @@ -32,6 +32,17 @@ <script language="javascript" type="text/javascript" src="{{STATIC_URL}}js/ishtar.js?ver={{VERSION}}"></script> <script language="javascript" type="text/javascript" src="{{STATIC_URL}}datatables/i18n/{{LANGUAGE_CODE}}.js?ver={{VERSION}}"></script> + <!-- jqplot --> + <script language="javascript" type="text/javascript" src="{{STATIC_URL}}js/jqplot/jquery.jqplot.min.js?ver={{VERSION}}"></script> + <script language="javascript" type="text/javascript" src="{{STATIC_URL}}js/jqplot/plugins/jqplot.categoryAxisRenderer.min.js?ver={{VERSION}}"></script> + <script language="javascript" type="text/javascript" src="{{STATIC_URL}}js/jqplot/plugins/jqplot.canvasTextRenderer.min.js?ver={{VERSION}}"></script> + <script language="javascript" type="text/javascript" src="{{STATIC_URL}}js/jqplot/plugins/jqplot.canvasAxisLabelRenderer.min.js?ver={{VERSION}}"></script> + <script language="javascript" type="text/javascript" src="{{STATIC_URL}}js/jqplot/plugins/jqplot.canvasAxisTickRenderer.min.js?ver={{VERSION}}"></script> + <script language="javascript" type="text/javascript" src="{{STATIC_URL}}js/jqplot/plugins/jqplot.highlighter.min.js?ver={{VERSION}}"></script> + <script language="javascript" type="text/javascript" src="{{STATIC_URL}}js/jqplot/plugins/jqplot.barRenderer.min.js?ver={{VERSION}}"></script> + <script language="javascript" type="text/javascript" src="{{STATIC_URL}}js/jqplot/plugins/jqplot.pieRenderer.min.js?ver={{VERSION}}"></script> + <script language="javascript" type="text/javascript" src="{{STATIC_URL}}js/jqplot/plugins/jqplot.dateAxisRenderer.min.js?ver={{VERSION}}"></script> + <script language="javascript" type="text/javascript" src="{{STATIC_URL}}js/jqplot/plugins/jqplot.cursor.min.js?ver={{VERSION}}"></script> {{EXTRA_JS|safe}} <script type='text/javascript'> var static_path = "{{STATIC_URL}}"; @@ -54,6 +65,8 @@ var select_only_one_msg = "{% trans "Select only one item." %}"; var YES = "{% trans 'yes' %}"; var NO = "{% trans 'no' %}"; + var fetching_msg = "{% trans 'Fetching data...' %}"; + var stats_incompatible_modality = "{% trans 'This graph type accept only one modality.' %}"; var show_msg = "{% trans "Show" %}"; var entries_msg = "{% trans "entries" %}"; var info_show_msg = "{% trans "Showing" %}"; @@ -85,6 +98,7 @@ <link rel="stylesheet" href="{{STATIC_URL}}datatables/dataTables.bootstrap4.min.css?ver={{VERSION}}"> <link type="text/css" rel="stylesheet" href="{{STATIC_URL}}lightgallery/css/lightgallery.css?ver={{VERSION}}"> <link rel="stylesheet" href="{{STATIC_URL}}ol/ol.css?ver={{VERSION}}"> + <link rel="stylesheet" href="{{STATIC_URL}}js/jqplot/jquery.jqplot.min.css?ver={{VERSION}}" /> <link rel="stylesheet" href="{{STATIC_URL}}media/styles.css?ver={{VERSION}}"> {% for url_css in JQGRID_CSS %}<link rel="stylesheet" href="{{url_css}}?ver={{VERSION}}">{% endfor %} {{EXTRA_CSS|safe}} diff --git a/ishtar_common/templates/blocks/DataTables-tabs.html b/ishtar_common/templates/blocks/DataTables-tabs.html index 73653061d..a2f354240 100644 --- a/ishtar_common/templates/blocks/DataTables-tabs.html +++ b/ishtar_common/templates/blocks/DataTables-tabs.html @@ -22,6 +22,14 @@ {% trans "Map" %} </a> </li>{% endif %} + {% if current_model.STATISTIC_MODALITIES %} + <li class="nav-item"> + <a class="nav-link" id="tab-stats-{{name}}" data-toggle="tab" + href="#tab-content-stats-{{name}}" role="tab" + aria-controls="tab-content-stats-{{name}}" aria-selected="true"> + {% trans "Statistics" %} + </a> + </li>{% endif %} </ul> <div class="tab-content"> @@ -38,4 +46,10 @@ id="tab-content-map-{{name}}" role="tabpanel" aria-labelledby="tab-map-{{name}}"> </div>{% endif %} + {% if current_model.STATISTIC_MODALITIES %} + <div class="tab-pane" + id="tab-content-stats-{{name}}" role="tabpanel" + aria-labelledby="tab-stats-{{name}}"> + {% include "blocks/DataTables-stats.html" %} + </div>{% endif %} </div> diff --git a/ishtar_common/templates/blocks/DataTables.html b/ishtar_common/templates/blocks/DataTables.html index a20a151a9..01aa7519d 100644 --- a/ishtar_common/templates/blocks/DataTables.html +++ b/ishtar_common/templates/blocks/DataTables.html @@ -67,6 +67,12 @@ $("#tab-map-{{name}}").click(function(){ map_submit_search(); }); {% endif %} +{% if current_model.STATISTIC_MODALITIES %} +$("#tab-stats-{{name}}").click(function(){ + current_tab = "stats"; + stats_submit_search(); +}); +{% endif %} var query_vars = new Array({{col_idx|safe}}); @@ -74,65 +80,17 @@ var selItems_{{sname}} = new Array(); {% if gallery %} gallery_submit_search = function(image_page){ - if (image_page) { - current_image_page = image_page; - } else { - current_image_page = 1; - } - $('.modal-progress').modal('show'); - var data = search_get_query_data(query_vars, "{{name}}"); - var nb_select = jQuery("#id_{{name}}-length_image").val(); - if (!nb_select) nb_select = 10; - - var url = "{{source}}json-image?length=" + nb_select + "&submited=1&" + data; - $.getJSON(url, function(data) { - var timestamp = Math.floor(Date.now() / 1000); - var gallery_id = "gallery-" + timestamp; - $("#tab-content-gallery-{{name}}").html( - render_gallery(data, "{{name}}", nb_select, gallery_id)); - $("#id_{{name}}-length_image").change(gallery_submit_search); - register_image_gallery(gallery_id); - $('.card[data-toggle="tooltip"]').tooltip(); - if ($('.modal-progress').length > 0){ - $('.modal-progress').modal('hide'); - } - }); - return false; + return _gallery_submit_search(image_page, query_vars, "{{name}}", "{{source}}"); }; {% endif %} {% if use_map %} map_submit_search = function(){ - var modal_base_text = $('.modal-progress .modal-header').html(); - $('.modal-progress .modal-header').html("{% trans 'Fetching data...' %}"); - $('.modal-progress').modal('show'); - var data = search_get_query_data(query_vars, "{{name}}"); - var nb_select = jQuery("#id_{{name}}-length_map").val(); - if (!nb_select) nb_select = 10; - - var url = "{{source}}json-map?length=" + nb_select + "&submited=1&" + data; - var use_map_limit = false; - if(data.indexOf("no_limit=true") == -1){ - url += "&limit=" + current_map_limit; - use_map_limit = true; - } - $.getJSON(url, function(data) { - var timestamp = Math.floor(Date.now() / 1000); - var map_id = "map-" + timestamp; - $('.modal-progress .modal-header').html("{% trans 'Render map...' %}"); - - var html = render_map(map_id, use_map_limit); - $("#tab-content-map-{{name}}").html(html); - $("#id_{{name}}-length_map").change(map_submit_search); - if ($('.modal-progress').length > 0){ - $('.modal-progress').modal('hide'); - $('.modal-progress .modal-header').html(modal_base_text); - } - register_map(map_id, data); - }); - - return false; + return _map_submit_search(query_vars, "{{name}}", "{{source}}"); }; {% endif %} +stats_submit_search = function(){ + return _stats_submit_search(query_vars, "{{name}}", "{{source}}"); +}; datatable_submit_search = function(not_submited){ var data = search_get_query_data(query_vars, "{{name}}"); diff --git a/ishtar_common/views_item.py b/ishtar_common/views_item.py index 145c56ca4..5b32a5fce 100644 --- a/ishtar_common/views_item.py +++ b/ishtar_common/views_item.py @@ -1207,19 +1207,29 @@ def _get_data_from_query_old(items, query_table_cols, request, def _get_json_stats(items, stats_sum_variable, stats_modality_1, stats_modality_2): - q = items.values(stats_modality_1, stats_modality_2) + if stats_modality_2: + q = items.values(stats_modality_1, stats_modality_2) + else: + q = items.values(stats_modality_1) if stats_sum_variable == 'pk': q = q.annotate(sum=Count('pk')) else: q = q.annotate(sum=Sum(stats_sum_variable)) data = [] - for values in q.order_by(stats_modality_1, stats_modality_2).all(): - modality_1 = values[stats_modality_1] - if not data or data[-1][0] != modality_1: - data.append([modality_1, []]) - data[-1][1].append( - (values[stats_modality_2], values["sum"]) - ) + if stats_modality_2 and stats_modality_2 != stats_modality_1: + q = q.order_by(stats_modality_1, stats_modality_2) + for values in q.all(): + modality_1 = values[stats_modality_1] + if not data or data[-1][0] != modality_1: + data.append([modality_1, []]) + data[-1][1].append( + (values[stats_modality_2], values["sum"]) + ) + else: + q = q.order_by(stats_modality_1) + for values in q.all(): + modality_1 = values[stats_modality_1] + data.append([modality_1, values["sum"]]) data = json.dumps({"data": data}) return HttpResponse(data, content_type='application/json') @@ -1634,9 +1644,8 @@ def get_item(model, func_name, default_name, extra_request_keys=None, if not stats_modality_1 or \ stats_modality_1 not in model.STATISTIC_MODALITIES: stats_modality_1 = model.STATISTIC_MODALITIES[0] - if not stats_modality_2 or \ - stats_modality_2 not in model.STATISTIC_MODALITIES: - stats_modality_2 = model.STATISTIC_MODALITIES[1] + if stats_modality_2 not in model.STATISTIC_MODALITIES: + stats_modality_2 = None stats_sum_variable = request_items.get('stats_sum_variable', None) stats_sum_variable_keys = list(model.STATISTIC_SUM_VARIABLE.keys()) if not stats_sum_variable or \ diff --git a/version.py b/version.py index 3fa515116..7111aa41b 100644 --- a/version.py +++ b/version.py @@ -1,5 +1,5 @@ -# 2.1.dev.39 -VERSION = (2, 1, 'dev', 39) +# 2.1.dev.40 +VERSION = (2, 1, 'dev', 40) def get_version(): |