From 438548f7b0ebab1cee58e71ff92a9d1d3e61f039 Mon Sep 17 00:00:00 2001 From: Colin Copeland Date: Wed, 8 Jan 2025 13:52:16 -0500 Subject: [PATCH] add app user frontend --- apps/odk_publish/etl/load.py | 2 +- .../commands/populate_sample_odk_data.py | 10 +- apps/odk_publish/middleware.py | 36 +++++ apps/odk_publish/urls.py | 17 ++ apps/odk_publish/views.py | 22 +++ .../templates/patterns/tables/table.html | 2 +- config/assets/styles/tailwind-entry.css | 16 +- config/settings/base.py | 1 + config/templates/base.html | 152 ++++++++--------- config/templates/home.html | 4 +- config/templates/includes/_navbar.html | 153 ++++++++++++++++++ config/templates/odk_publish/app_users.html | 106 ++++++++++++ config/urls.py | 1 + tailwind.config.js | 38 ----- 14 files changed, 426 insertions(+), 134 deletions(-) create mode 100644 apps/odk_publish/middleware.py create mode 100644 apps/odk_publish/urls.py create mode 100644 apps/odk_publish/views.py create mode 100644 config/templates/includes/_navbar.html create mode 100644 config/templates/odk_publish/app_users.html diff --git a/apps/odk_publish/etl/load.py b/apps/odk_publish/etl/load.py index cbfc189..fe59b72 100644 --- a/apps/odk_publish/etl/load.py +++ b/apps/odk_publish/etl/load.py @@ -33,7 +33,7 @@ def generate_and_save_app_user_collect_qrcodes(project: Project): """Generate and save QR codes for all app users in the project.""" app_users = project.app_users.all() logger.info("Generating QR codes", project=project.name, app_users=len(app_users)) - with ODKPublishClient.new_client(base_url=project.central_server.base_url) as client: + with ODKPublishClient(base_url=project.central_server.base_url) as client: central_app_users = client.odk_publish.get_app_users( project_id=project.project_id, display_names=[app_user.name for app_user in app_users], diff --git a/apps/odk_publish/management/commands/populate_sample_odk_data.py b/apps/odk_publish/management/commands/populate_sample_odk_data.py index 833d820..58bbf56 100644 --- a/apps/odk_publish/management/commands/populate_sample_odk_data.py +++ b/apps/odk_publish/management/commands/populate_sample_odk_data.py @@ -25,16 +25,22 @@ def handle(self, *args, **options): if file.is_file(): logger.info("Removing file", file=file.name) file.unlink() - logger.info("Creating CentralServer...") + logger.info("Creating CentralServers...") central_server = odk_publish.CentralServer.objects.create( base_url="https://odk-central.caktustest.net/" ) - logger.info("Creating Project...") + myodkcloud = odk_publish.CentralServer.objects.create(base_url="https://myodkcloud.com/") + logger.info("Creating Projects...") project = odk_publish.Project.objects.create( name="Caktus Test", project_id=1, central_server=central_server, ) + odk_publish.Project.objects.create( + name="Other Project", + project_id=5, + central_server=myodkcloud, + ) logger.info("Creating TemplateVariable...") center_id_var = odk_publish.TemplateVariable.objects.create(name="center_id") center_label_var = odk_publish.TemplateVariable.objects.create(name="center_label") diff --git a/apps/odk_publish/middleware.py b/apps/odk_publish/middleware.py new file mode 100644 index 0000000..90fe4bb --- /dev/null +++ b/apps/odk_publish/middleware.py @@ -0,0 +1,36 @@ +import structlog + +from django.http import HttpRequest +from django.urls import ResolverMatch + +from .models import Project + +logger = structlog.getLogger(__name__) + + +class ODKProjectMiddleware: + """Middleware to lookup the current ODK project based on the URL. + + The `odk_project` and `odk_projects` attributes are added to the request object. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request: HttpRequest): + return self.get_response(request) + + def process_view(self, request: HttpRequest, view_func, view_args, view_kwargs): + # Set common context for all views + request.odk_project = None + request.odk_projects = Project.objects.select_related() + # Automatically lookup the current project + resolver_match: ResolverMatch = request.resolver_match + if ( + "odk_publish" in resolver_match.namespaces + and "odk_project_pk" in resolver_match.captured_kwargs + ): + odk_project_pk = resolver_match.captured_kwargs["odk_project_pk"] + project = Project.objects.select_related().filter(pk=odk_project_pk).first() + logger.debug("odk_project_pk detected", odk_project_pk=odk_project_pk, project=project) + request.odk_project = project diff --git a/apps/odk_publish/urls.py b/apps/odk_publish/urls.py new file mode 100644 index 0000000..45540e6 --- /dev/null +++ b/apps/odk_publish/urls.py @@ -0,0 +1,17 @@ +from django.urls import path + +from . import views + +app_name = "odk_publish" +urlpatterns = [ + path( + "/app-users/", + views.app_users_list, + name="app-users-list", + ), + path( + "/app-users/generate-qr-codes/", + views.app_users_generate_qr_codes, + name="app-users-generate-qr-codes", + ), +] diff --git a/apps/odk_publish/views.py b/apps/odk_publish/views.py new file mode 100644 index 0000000..456c575 --- /dev/null +++ b/apps/odk_publish/views.py @@ -0,0 +1,22 @@ +import logging + +from django.http import HttpRequest +from django.contrib.auth.decorators import login_required +from django.shortcuts import render, redirect + +from .etl.load import generate_and_save_app_user_collect_qrcodes + + +logger = logging.getLogger(__name__) + + +@login_required +def app_users_list(request: HttpRequest, odk_project_pk): + app_users = request.odk_project.app_users.prefetch_related("app_user_forms__form_template") + return render(request, "odk_publish/app_users.html", {"app_users": app_users}) + + +@login_required +def app_users_generate_qr_codes(request: HttpRequest, odk_project_pk): + generate_and_save_app_user_collect_qrcodes(project=request.odk_project) + return redirect("odk_publish:app-users-list", odk_project_pk=odk_project_pk) diff --git a/apps/patterns/templates/patterns/tables/table.html b/apps/patterns/templates/patterns/tables/table.html index d44e7ff..e0ab04c 100644 --- a/apps/patterns/templates/patterns/tables/table.html +++ b/apps/patterns/templates/patterns/tables/table.html @@ -2,7 +2,7 @@ {% load i18n l10n django_tables2 %} {% block table.thead %} {% if table.show_header %} - {% for column in table.columns %} diff --git a/config/assets/styles/tailwind-entry.css b/config/assets/styles/tailwind-entry.css index ccc1bb6..551fb41 100644 --- a/config/assets/styles/tailwind-entry.css +++ b/config/assets/styles/tailwind-entry.css @@ -7,11 +7,19 @@ @theme { --font-display: "Satoshi", "sans-serif"; --color-primary: oklch(37.53% 0.0438 226.2); + --color-primary-100: oklch(97.02% 0.0067 233.64); /* #F1F6F9 */ + --color-primary-200: oklch(90.96% 0.0211 232.15); /* #D4E4ED */ + --color-primary-300: oklch(84.85% 0.0357 232.13); /* #B7D2E1 */ + --color-primary-400: oklch(78.72% 0.0505 232.42); /* #9AC0D5 */ + --color-primary-500: oklch(72.64% 0.0644 233.46); /* #7EAEC9 */ + --color-primary-600: oklch(66.28% 0.0791 235.44); /* #619BBD */ + --color-primary-700: oklch(59.96% 0.0867 236.06); /* #4888AD */ --color-green: oklch(63.04% 0.1013 183.03); --color-yellow: oklch(83.42% 0.117 87.43); --color-orange: oklch(78.06% 0.1269 57.86); --color-red: oklch(67.83% 0.1559 35.18); --color-error: var(--color-red-500); + --color-danger-medium: var(--color-red-500); } @layer components { @@ -19,15 +27,15 @@ @apply font-medium text-center text-sm px-5 py-2.5 rounded-lg cursor-pointer; } .btn-primary { - @apply text-white bg-brand-primary-dark hover:bg-brand-primary-medium focus:ring-4 focus:outline-none focus:ring-primary-300; + @apply text-white bg-primary hover:bg-primary-500 focus:ring-4 focus:outline-none focus:ring-primary-300; } .btn-danger { - @apply text-brand-danger-medium bg-white rounded-lg border border-brand-danger-medium hover:bg-brand-danger-light hover:text-brand-danger-medium focus:outline-none focus:ring-4 focus:ring-gray-200; + @apply text-danger-medium bg-white rounded-lg border border-danger-medium hover:bg-danger-medium hover:text-danger-medium focus:outline-none focus:ring-4 focus:ring-gray-200; } .btn-outline { - @apply text-brand-primary-dark bg-white border border-blue-900 hover:bg-brand-primary-medium hover:text-white focus:ring-4 focus:outline-none focus:ring-primary-300; + @apply text-primary bg-white border border-blue-900 hover:bg-primary-500 hover:text-white focus:ring-4 focus:outline-none focus:ring-primary-300; } .btn-active { - @apply text-white bg-brand-primary-medium border border-blue-900 hover:bg-brand-primary-medium hover:text-white focus:ring-4 focus:outline-none focus:ring-primary-300; + @apply text-white bg-primary-500 border border-blue-900 hover:bg-primary-500 hover:text-white focus:ring-4 focus:outline-none focus:ring-primary-300; } } diff --git a/config/settings/base.py b/config/settings/base.py index 8b0066a..dff0b6a 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -78,6 +78,7 @@ "django_htmx.middleware.HtmxMiddleware", "django_structlog.middlewares.RequestMiddleware", "allauth.account.middleware.AccountMiddleware", + "apps.odk_publish.middleware.ODKProjectMiddleware", ] ROOT_URLCONF = "config.urls" diff --git a/config/templates/base.html b/config/templates/base.html index 50398c0..6b642df 100644 --- a/config/templates/base.html +++ b/config/templates/base.html @@ -25,94 +25,24 @@ {% block extra-css %} {% endblock extra-css %} - -
- -
-
-
- {% for message in messages %} - {% if message.tags == 'info' or message.tags == 'success' %} - - {% else %} - - {% endif %} - {% endfor %} -
+ {% endif %} {% block content %} {% endblock content %}
@@ -137,6 +67,54 @@ ` messages.insertAdjacentHTML("beforeend", message) }) + + // On page load or when changing themes, best to add inline in `head` to avoid FOUC + if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark') + } + + var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon'); + var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon'); + + // Change the icons inside the button based on previous settings + if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { + themeToggleLightIcon.classList.remove('hidden'); + } else { + themeToggleDarkIcon.classList.remove('hidden'); + } + + var themeToggleBtn = document.getElementById('theme-toggle'); + + themeToggleBtn.addEventListener('click', function() { + + // toggle icons inside button + themeToggleDarkIcon.classList.toggle('hidden'); + themeToggleLightIcon.classList.toggle('hidden'); + + // if set via local storage previously + if (localStorage.getItem('color-theme')) { + if (localStorage.getItem('color-theme') === 'light') { + document.documentElement.classList.add('dark'); + localStorage.setItem('color-theme', 'dark'); + } else { + document.documentElement.classList.remove('dark'); + localStorage.setItem('color-theme', 'light'); + } + + // if NOT set via local storage previously + } else { + if (document.documentElement.classList.contains('dark')) { + document.documentElement.classList.remove('dark'); + localStorage.setItem('color-theme', 'light'); + } else { + document.documentElement.classList.add('dark'); + localStorage.setItem('color-theme', 'dark'); + } + } + + }); diff --git a/config/templates/home.html b/config/templates/home.html index ecc77aa..9cb1e19 100644 --- a/config/templates/home.html +++ b/config/templates/home.html @@ -3,7 +3,9 @@

Welcome to ODK Publish

-

ODK Publish is a tool for managing and publishing forms to ODK Central.

+

+ ODK Publish is a tool for managing and publishing forms to ODK Central. +

+
+
+ + ODK Publish + + {% if request.user.is_authenticated %} + + + + {% endif %} +
+
+ + + + + {% if request.user.is_authenticated %} +
+ + + +
+ {% endif %} + +
+
+ + {% if request.odk_project %} + + {% endif %} + diff --git a/config/templates/odk_publish/app_users.html b/config/templates/odk_publish/app_users.html new file mode 100644 index 0000000..99e2558 --- /dev/null +++ b/config/templates/odk_publish/app_users.html @@ -0,0 +1,106 @@ +{% extends "base.html" %} +{% block content %} +
+
+ +
+
+ +

App Users

+
+
+ + +
+
+
+ {% for app_user in app_users %} +
+ +
+ {{ app_user }} +

+ Forms: + {% for app_user_form in app_user.app_user_forms.all %}{{ app_user_form.form_template.title_base }}{% endfor %} +

+
+
+ {% endfor %} +
+
+
+{% endblock content %} diff --git a/config/urls.py b/config/urls.py index ef47242..2b652f7 100644 --- a/config/urls.py +++ b/config/urls.py @@ -23,6 +23,7 @@ urlpatterns = [ path("accounts/", include("allauth.urls")), path("admin/", admin.site.urls), + path("odk/", include("apps.odk_publish.urls", namespace="odk_publish")), path(r"", TemplateView.as_view(template_name="home.html"), name="home"), ] diff --git a/tailwind.config.js b/tailwind.config.js index aed7ef0..11a3db2 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -8,43 +8,5 @@ module.exports = { "./node_modules/flowbite/**/*.js", "./apps/chat/templatetags/chat_tags.py", ], - theme: { - extend: { - colors: { - primary: { - light: "#B9D5DF", - 100: "#33BBFF", - 200: "#1FB4FF", - 300: "#0AADFF", - 400: "#00A3F5", - 500: "#0096E0", - 600: "#0088CC", - 700: "#007CBA", - 800: "#006DA3", - 900: "#005F85", - dark: "#363249", - }, - brand: { - primary: { - light: "#595379", - medium: "#413C58", - dark: "#264653", - }, - accent: { - light: "#E8E7EE", // input bg - medium: "#363249", // input border - }, - gray: { - light: "rgb(107 114 128)", - medium: "rgb(107 114 128)", - }, - danger: { - light: "rgb(254 229 229)", - medium: "rgb(244 72 77)", - }, - }, - }, - }, - }, plugins: [require("flowbite/plugin")], };