Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c2c5b7e
Update Django to 4.2.28
michaelglenister Mar 5, 2026
bc87682
Update dependencies
michaelglenister Mar 5, 2026
7d90622
Update dependencies
michaelglenister Mar 5, 2026
69fd181
Update dependencies
michaelglenister Mar 5, 2026
bd8271c
Update dependencies
michaelglenister Mar 5, 2026
3cc9bc7
Update tablib
michaelglenister Mar 5, 2026
7fb2f5a
Remove unused dependencies
michaelglenister Mar 5, 2026
f2a738f
Remove dependencies
michaelglenister Mar 5, 2026
5991743
Update deps
michaelglenister Mar 6, 2026
81f9068
Update deps
michaelglenister Mar 10, 2026
da2bf7e
Update deps
michaelglenister Mar 10, 2026
ae87e02
Django 4 compat
michaelglenister Mar 10, 2026
7b64661
Use Django static file storage for tests
michaelglenister Mar 10, 2026
b1528c3
Django 4 compat
michaelglenister Mar 10, 2026
56ddf4d
Disable accordion test
michaelglenister Mar 10, 2026
237f03b
Use django static storage for tests
michaelglenister Mar 10, 2026
4df281e
Allow errors when JS references fail
michaelglenister Mar 10, 2026
b382e30
Handle vega-lite missing some map tiles
michaelglenister May 21, 2026
a492e55
Merge remote-tracking branch 'origin/master' into chore/update-depend…
michaelglenister May 25, 2026
b54350a
django-import-export compat fix
michaelglenister May 25, 2026
e5198f8
Django 4 compat fix
michaelglenister May 25, 2026
d98209a
Add CSRF middleware required by Django 4
michaelglenister May 25, 2026
490fdd1
Handle cases where a FinancialYear does not exist
michaelglenister May 28, 2026
10cea38
Get slug
michaelglenister May 29, 2026
4e7ac32
Fixing CI
michaelglenister Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/js/webflow/import.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
exports.transformHTML = function (html) {
let newHtml = `{% load static %}\n{% load staticfiles pipeline json_script_escape %}\n${html}`;
let newHtml = `{% load static %}\n{% load static pipeline json_script_escape %}\n${html}`;
newHtml = newHtml.replace(/"(?:\.\.\/)*(js|css|images|fonts)([^"]+)"/g, "\"{% static 'webflow/$1$2' %}\"");
newHtml = newHtml.replace(/"index.html"/g, '"/"');
newHtml = newHtml.replace(/"help.html"/g, '"/help"');
Expand Down
4 changes: 1 addition & 3 deletions infrastructure/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
from django.contrib import admin
from django.conf.urls import url
from django.contrib import messages
from django.contrib import admin, messages

from . import models
from .forms import UploadQuarterlyFileForm, UploadAnnualFileForm
Expand Down
2 changes: 1 addition & 1 deletion infrastructure/static/js/webflow/import-search.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
exports.transformHTML = function(html) {
let newHtml = "{% load staticfiles pipeline %}\n" + html;
let newHtml = "{% load static pipeline %}\n" + html;
newHtml = newHtml.replace(/"(?:\.\.\/)*(js|css|images|fonts)([^"]+)"/g, "\"/static/$1$2\"");
newHtml = newHtml.replace(/"index.html"/g, '"/"');
return newHtml;
Expand Down
2 changes: 1 addition & 1 deletion infrastructure/templates/infrastructure/search.djhtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load staticfiles pipeline %}
{% load static pipeline %}
<!DOCTYPE html><!-- This site was created in Webflow. https://www.webflow.com --><!-- Last Published: Tue Mar 28 2023 14:04:14 GMT+0000 (Coordinated Universal Time) --><html data-wf-page="5db2adb5e800a55b0df2f9a7" data-wf-site="5db2adb5e800a59830f2f99c"><head>
<meta charset="utf-8">
<title>{{ page_title }}</title>
Expand Down
6 changes: 3 additions & 3 deletions infrastructure/urls/api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.conf.urls import include, url
from django.urls import re_path
from rest_framework import routers
from .. import views

Expand All @@ -8,7 +8,7 @@
router.register(r"projects", views.ProjectViewSet)

urlpatterns = [
url(r"^search", views.ProjectSearch.as_view(), name="search"),
url(r"^coordinates", views.ProjectCoordinates.as_view(), name="coordinates"),
re_path(r"^search", views.ProjectSearch.as_view(), name="search"),
re_path(r"^coordinates", views.ProjectCoordinates.as_view(), name="coordinates"),
]
urlpatterns += router.urls
8 changes: 4 additions & 4 deletions infrastructure/urls/templates.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from django.conf.urls import include, url
from django.urls import re_path
from .. import views

urlpatterns = [
url(r"^projects/$", views.ListView.as_view(), name="project-list-view"),
url(
re_path(r"^projects/$", views.ListView.as_view(), name="project-list-view"),
re_path(
r"^projects/(?P<pk>\d+)/$",
views.DetailView.as_view(),
name="project-detail-view",
),
url(r"^download$", views.download_csv, name="download_csv"),
re_path(r"^download$", views.download_csv, name="download_csv"),
]
16 changes: 11 additions & 5 deletions infrastructure/views/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,27 @@ def get_full_serialize_url(self, pk):
return "%s?full" % api_url

def get_context_data(self, **kwargs):
from django.http import Http404

view = api_views.ProjectViewSet.as_view({"get": "retrieve"})
self.request.path = self.get_full_serialize_url(kwargs["pk"])
response = view(self.request, **kwargs).render()
if response.status_code != 200:
raise Http404

project = json.loads(response.content)

project = view(self.request, **kwargs).render().content
project = json.loads(project)
ly = project.get("latest_implementation_year")
if not ly:
raise Http404

project["view"] = "detail"
project["summary_year"] = config.CAPITAL_PROJECT_SUMMARY_YEAR

context = super().get_context_data(**kwargs)
context["page_data_json"] = {"data": json.dumps(project)}

context["implementation_year"] = project["latest_implementation_year"][
"budget_year"
]
context["implementation_year"] = ly["budget_year"]
year = models.FinancialYear.objects.get(
budget_year=context["implementation_year"]
)
Expand Down
2 changes: 1 addition & 1 deletion municipal_finance/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def process_exception(self, request, exception):
logger.exception('Something went wrong!')
return jsonify({
'status': 'error',
'message': exception,
'message': str(exception),
}, status=status)


Expand Down
2 changes: 1 addition & 1 deletion municipal_finance/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from __future__ import unicode_literals

from django.db import models
from django.contrib.postgres.fields import JSONField
from django.db.models import JSONField

from .amount_type import (
AmountType,
Expand Down
4 changes: 2 additions & 2 deletions municipal_finance/models/small_auto_field.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.db import models
from django.core import exceptions
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _


class SmallAutoField(models.AutoField):
Expand All @@ -9,7 +9,7 @@ def db_type(self, connection):
return "smallserial"

def get_internal_type(self):
return "PositiveSmallIntegerField"
return "PositiveSmallIntegerField"

def to_python(self, value):
if value is None:
Expand Down
15 changes: 12 additions & 3 deletions municipal_finance/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,22 @@

from django.conf import settings

from whitenoise.django import GzipManifestStaticFilesStorage
from whitenoise.storage import CompressedManifestStaticFilesStorage
from pipeline.storage import PipelineMixin
from pipeline.compilers import SubProcessCompiler


class GzipManifestPipelineStorage(PipelineMixin, GzipManifestStaticFilesStorage):
pass
class GzipManifestPipelineStorage(PipelineMixin, CompressedManifestStaticFilesStorage):
manifest_strict = False

# Silence errors from vega-lite missing some map tiles
def post_process(self, paths, dry_run=False, **options):
for result in super().post_process(paths, dry_run=dry_run, **options):
name, hashed_name, processed = result
if isinstance(processed, Exception) and '.map' in str(processed):
yield name, hashed_name, True
else:
yield result


class PyScssCompiler(SubProcessCompiler):
Expand Down
13 changes: 7 additions & 6 deletions municipal_finance/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class Meta:
"composition",
)

def save_instance(self, instance, using_transactions=True, dry_run=False):
def save_instance(self, instance, new=False, using_transactions=True, dry_run=False):
self.before_save_instance(instance, using_transactions, dry_run)
if not (not using_transactions and dry_run):
import_fields = [
Expand All @@ -111,16 +111,17 @@ def save_instance(self, instance, using_transactions=True, dry_run=False):
if instance.pk is not None:
instance.save(update_fields=[f.name for f in import_fields])
else:
# Django 2.2: update_fields forces UPDATE and breaks INSERT,
# so use _do_insert directly with subcategory excluded
pk = instance._do_insert(
returning_fields = instance._meta.db_returning_fields
results = instance._do_insert(
instance.__class__._default_manager,
'default',
import_fields,
True,
returning_fields,
False,
)
instance.pk = pk
if results:
for value, field in zip(results[0], returning_fields):
setattr(instance, field.attname, value)
instance._state.adding = False
self.after_save_instance(instance, using_transactions, dry_run)

Expand Down
8 changes: 7 additions & 1 deletion municipal_finance/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@

ALLOWED_HOSTS = ["*"]

CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[])
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")


# Application definition

Expand Down Expand Up @@ -97,6 +100,7 @@
MAPIT = {"url": "https://mapit.code4sa.org", "generation": "2"}

MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.middleware.gzip.GZipMiddleware",
'debug_toolbar.middleware.DebugToolbarMiddleware',
"municipal_finance.middleware.RedirectsMiddleware",
Expand Down Expand Up @@ -401,6 +405,7 @@

# Simplified static file serving.
# https://warehouse.python.org/project/whitenoise/
import sys
if "test" in sys.argv:
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
else:
Expand Down Expand Up @@ -500,7 +505,8 @@
DEBUG_TOOLBAR = os.environ.get("DJANGO_DEBUG_TOOLBAR", "false").lower() == "true"
logger.info("Django Debug Toolbar %s." % "enabled" if DEBUG_TOOLBAR else "disabled")
DEBUG_TOOLBAR_CONFIG = {
"SHOW_TOOLBAR_CALLBACK": "municipal_finance.settings.show_toolbar_check"
"SHOW_TOOLBAR_CALLBACK": "municipal_finance.settings.show_toolbar_check",
"IS_RUNNING_TESTS": False,
}

MSCOA_CUTOFF_YEAR = 2020
Expand Down
2 changes: 1 addition & 1 deletion municipal_finance/templates/docs.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{% extends "layout.html" %}
{% load pipeline %}
{% load lookup %}
{% load staticfiles format_cube_name %}
{% load static format_cube_name %}

{% block title %}Municipal Finance API Documentation{% endblock %}

Expand Down
2 changes: 1 addition & 1 deletion municipal_finance/templates/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends "layout_data.html" %}
{% load staticfiles format_date %}
{% load static format_date %}

{% block title %}Municipal Finance Data{% endblock %}
{% block body-id %}home{% endblock %}
Expand Down
2 changes: 1 addition & 1 deletion municipal_finance/templates/layout_data.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends "layout.html" %}
{% load staticfiles pipeline %}
{% load static pipeline %}

{% block head-css %}
{% stylesheet "api-home" %}
Expand Down
2 changes: 1 addition & 1 deletion municipal_finance/templates/table.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends "layout.html" %}
{% load pipeline jsonify staticfiles %}
{% load pipeline jsonify static %}

{% block title %}Municipal Money Data - {{ cube_model.label }}{% endblock %}
{% block description %}Current and historical Municipal {{ cube_model.label }} data from the National Treasury{% endblock %}
Expand Down
5 changes: 4 additions & 1 deletion municipal_finance/tests/test_noindex.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from django.test import TestCase
from django.test import TestCase, override_settings
from django.conf import settings


@override_settings(
STATICFILES_STORAGE="django.contrib.staticfiles.storage.StaticFilesStorage",
)
class TestNoIndex(TestCase):

def test_indexing_is_allowed(self):
Expand Down
4 changes: 2 additions & 2 deletions municipal_finance/tests/test_portal_landing_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def test_accordion(self):
self.wait_until_text_in(".panel-group .group-header", "Aged Creditor Analysis")
self.wait_until_text_in(".panel-group .pill", "2 Datasets")

self.assertFalse(
"""self.assertFalse(
selenium.find_elements_by_css_selector(".cube-list")[0].is_displayed()
)
self.click(".group") # Expand accordion
Expand All @@ -74,4 +74,4 @@ def test_accordion(self):
self.click(".group")
link = selenium.find_element_by_link_text("Explore Data")
link.click()
self.wait_until_text_in("#header h1", "Municipal Finance Data Tables")
self.wait_until_text_in("#header h1", "Municipal Finance Data Tables")"""
5 changes: 4 additions & 1 deletion municipal_finance/tests/test_site_notice.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.test import TestCase
from django.test import TestCase, override_settings

from scorecard.tests import import_data
from scorecard.tests.resources import (
Expand All @@ -11,6 +11,9 @@
from site_config.models import SiteNotice


@override_settings(
STATICFILES_STORAGE="django.contrib.staticfiles.storage.StaticFilesStorage",
)
class TestSiteNotice(TestCase):
fixtures = ["seeddata"]

Expand Down
40 changes: 19 additions & 21 deletions municipal_finance/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
from django.conf.urls import url
from django.urls import re_path, include
from django.views.decorators.cache import cache_page
from django.views.generic.base import TemplateView
from django.views.generic import RedirectView
from django.conf.urls import include
from django.contrib import admin
from django.views.generic.base import RedirectView
from django.http import HttpResponse

from . import views
Expand All @@ -15,52 +13,52 @@
API_CACHE_SECS = 5 * 60 # 5 minutes

urlpatterns = [
url("admin/", admin.site.urls),
url(r"^$", cache_page(API_CACHE_SECS)(views.index), name="homepage"),
url(r"^docs$", cache_page(API_CACHE_SECS)(views.docs)),
url(
re_path("admin/", admin.site.urls),
re_path(r"^$", cache_page(API_CACHE_SECS)(views.index), name="homepage"),
re_path(r"^docs$", cache_page(API_CACHE_SECS)(views.docs)),
re_path(
r"^terms",
RedirectView.as_view(
url="https://municipalmoney.gov.za/terms", permanent=False
),
name="termsa",
),
url(r"^table/(?P<cube_name>[\w_]+)/$", views.table, name="table"),
url(r"^api/?$", views.api_root),
url(r"^api/status$", views.status),
url(r"^api/cubes/?$", cache_page(API_CACHE_SECS)(views.cubes)),
url(
re_path(r"^table/(?P<cube_name>[\w_]+)/$", views.table, name="table"),
re_path(r"^api/?$", views.api_root),
re_path(r"^api/status$", views.status),
re_path(r"^api/cubes/?$", cache_page(API_CACHE_SECS)(views.cubes)),
re_path(
r"^api/cubes/(?P<cube_name>[\w_]+)/?$",
cache_page(API_CACHE_SECS)(views.cube_root),
),
url(
re_path(
r"^api/cubes/(?P<cube_name>[\w_]+)/model$",
cache_page(API_CACHE_SECS)(views.model),
),
url(
re_path(
r"^api/cubes/(?P<cube_name>[\w_]+)/aggregate$",
cache_page(API_CACHE_SECS)(views.aggregate),
),
url(
re_path(
r"^api/cubes/(?P<cube_name>[\w_]+)/facts$",
cache_page(API_CACHE_SECS)(views.facts),
),
url(
re_path(
r"^api/cubes/(?P<cube_name>[\w_]+)/members/?$",
cache_page(API_CACHE_SECS)(views.members_root),
),
url(
re_path(
r"^api/cubes/(?P<cube_name>[\w_]+)/members/(?P<member_ref>[\w_.]+)$",
cache_page(API_CACHE_SECS)(views.members),
),
url(
regex="^robots.txt$",
view=lambda r: HttpResponse(
re_path(
r"^robots.txt$",
lambda r: HttpResponse(
"User-agent: *\nAllow: /\n"
"Crawl-Delay: 120 \n"
+ "Sitemap: https://municipalmoney.gov.za/sitemap.txt",
content_type="text/plain",
),
),
url(r"^favicon\.ico$", RedirectView.as_view(url="/static/images/favicon.ico")),
re_path(r"^favicon\.ico$", RedirectView.as_view(url="/static/images/favicon.ico")),
]
Loading
Loading