Skip to content

Commit

Permalink
add app user frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
copelco committed Jan 8, 2025
1 parent 506aca1 commit 438548f
Show file tree
Hide file tree
Showing 14 changed files with 426 additions and 134 deletions.
2 changes: 1 addition & 1 deletion apps/odk_publish/etl/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
10 changes: 8 additions & 2 deletions apps/odk_publish/management/commands/populate_sample_odk_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
36 changes: 36 additions & 0 deletions apps/odk_publish/middleware.py
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions apps/odk_publish/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.urls import path

from . import views

app_name = "odk_publish"
urlpatterns = [
path(
"<int:odk_project_pk>/app-users/",
views.app_users_list,
name="app-users-list",
),
path(
"<int:odk_project_pk>/app-users/generate-qr-codes/",
views.app_users_generate_qr_codes,
name="app-users-generate-qr-codes",
),
]
22 changes: 22 additions & 0 deletions apps/odk_publish/views.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion apps/patterns/templates/patterns/tables/table.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{% load i18n l10n django_tables2 %}
{% block table.thead %}
{% if table.show_header %}
<thead class="text-xs text-white uppercase bg-brand-primary-dark dark:bg-gray-700 dark:text-gray-400"
<thead class="text-xs text-white uppercase bg-primary dark:bg-gray-700 dark:text-gray-400"
{{ table.attrs.thead.as_html }}>
<tr>
{% for column in table.columns %}
Expand Down
16 changes: 12 additions & 4 deletions config/assets/styles/tailwind-entry.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,35 @@
@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 {
.btn {
@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;
}
}
1 change: 1 addition & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
152 changes: 65 additions & 87 deletions config/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,94 +25,24 @@
{% block extra-css %}
{% endblock extra-css %}
</head>
<body class="bg-white">
<header class="antialiased">
<nav class="bg-white border-gray-200 px-4 lg:px-6 py-2.5 dark:bg-gray-800">
<div class="flex flex-wrap justify-between items-center">
<div class="flex justify-start items-center">
<a href="" class="flex mr-4">
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">ODK Publish</span>
</a>
</div>
<!-- <div class="hidden w-full md:block md:w-auto" id="navbar-default">
<ul class="font-medium flex flex-col p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:flex-row md:space-x-8 rtl:space-x-reverse md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
<li>
<a href="" class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Home</a>
</li>
<li>
<a href="" class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Search</a>
</li>
<li>
<a href="" class="block py-2 px-3 text-gray-900 rounded hover:bg-gray-100 md:hover:bg-transparent md:border-0 md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent">Chat</a>
</li>
</ul>
</div> -->
{% if request.user.is_authenticated %}
<div class="flex items-center lg:order-2">
<!-- <a href="">
<button type="button" class="btn btn-primary hidden sm:inline-flex items-center justify-center text-xs ">
<svg aria-hidden="true" class="mr-1 -ml-1 w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd">
</path>
</svg>
Add Document
</button>
</a> -->
<button type="button"
class="flex mx-3 text-sm rounded-full md:mr-0 focus:ring-4 focus:ring-red-200 dark:focus:ring-gray-600 cursor-pointer"
id="user-menu-button"
aria-expanded="false"
data-dropdown-toggle="dropdown">
<span class="sr-only">Open user menu</span>
<div class="relative inline-flex items-center justify-center w-10 h-10 overflow-hidden ring-2 ring-red-300 rounded-full dark:bg-gray-600">
<span class="font-medium text-red dark:text-gray-300">{{ request.user.first_name|first }}{{ request.user.last_name|first }}</span>
</div>
</button>
<!-- Dropdown menu -->
<div class="hidden z-50 my-4 w-56 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600"
id="dropdown">
<div class="py-3 px-4">
<span class="block text-sm font-semibold text-gray-900 dark:text-white">{{ request.user.first_name }} {{ request.user.last_name }}</span>
<span class="block text-sm text-gray-500 truncate dark:text-gray-400">{{ request.user.email }}</span>
</div>
<ul class="py-1 text-gray-500 dark:text-gray-400"
aria-labelledby="dropdown">
<li>
<a href="{% url 'socialaccount_connections' %}"
class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">Account settings</a>
</li>
{% if request.user.is_superuser %}
<li>
<a href="{% url 'admin:index' %}"
class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">Admin</a>
</li>
{% endif %}
</ul>
<ul class="py-1 text-gray-500 dark:text-gray-400"
aria-labelledby="dropdown">
<li>
<a href="{% url 'account_logout' %}"
class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Sign out</a>
</li>
</ul>
</div>
</div>
{% endif %}
<body class="bg-gray-50 dark:bg-gray-900">
{% include "includes/_navbar.html" %}
{% block extra-nav %}
{% endblock extra-nav %}
<div class="{% if request.path == '/' %} mt-8 {% elif request.path != '/accounts/login/' %} w-full top-[25px] {% endif %} mb-20">
{% if messages %}
<div class="mx-auto max-w-screen-xl px-4 lg:px-12" id="messages">
{% for message in messages %}
{% if message.tags == 'info' or message.tags == 'success' %}
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-brand-accent-light dark:bg-gray-800 dark:text-blue-400"
role="alert">{{ message }}</div>
{% else %}
<div class="p-4 mb-4 text-sm text-brand-danger-medium rounded-lg bg-brand-danger-light dark:bg-gray-800 dark:text-brand-danger-light"
role="alert">{{ message }}</div>
{% endif %}
{% endfor %}
</div>
</nav>
</header>
<div class="{% if request.path == '/' %} mt-8 {% elif request.path != '/accounts/login/' %} w-full top-[25px] p-4{% endif %} mb-20">
<div class="mx-auto max-w-screen-xl px-4 lg:px-12" id="messages">
{% for message in messages %}
{% if message.tags == 'info' or message.tags == 'success' %}
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-brand-accent-light dark:bg-gray-800 dark:text-blue-400"
role="alert">{{ message }}</div>
{% else %}
<div class="p-4 mb-4 text-sm text-brand-danger-medium rounded-lg bg-brand-danger-light dark:bg-gray-800 dark:text-brand-danger-light"
role="alert">{{ message }}</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% block content %}
{% endblock content %}
</div>
Expand All @@ -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');
}
}

});
</script>
</body>
</html>
4 changes: 3 additions & 1 deletion config/templates/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
<section class="mx-auto max-w-screen-xl px-4 lg:px-12 bg-white dark:bg-gray-900">
<div class="py-8 px-4 mx-auto max-w-2xl lg:py-4">
<h1 class="mb-4 text-2xl font-bold text-gray-900 dark:text-white">Welcome to ODK Publish</h1>
<p class="text-lg text-gray-800">ODK Publish is a tool for managing and publishing forms to ODK Central.</p>
<p class="text-lg text-gray-500 dark:text-gray-400">
ODK Publish is a tool for managing and publishing forms to ODK Central.
</p>
<dl>
<div class="flex mt-10">
<svg class="w-5 h-5 mr-2 text-green shrink-0"
Expand Down
Loading

0 comments on commit 438548f

Please sign in to comment.