From b4183654b0ef59c8f2f8ebcfeeea093fd8d817d6 Mon Sep 17 00:00:00 2001 From: Ngure Nyaga Date: Sat, 17 Jul 2021 09:21:52 +0300 Subject: [PATCH] feat: wire in facility and program report placeholder pages --- config/settings/base.py | 77 +--- config/urls.py | 3 + pepfar_mle/common/admin.py | 7 +- pepfar_mle/common/migrations/0006_system.py | 34 ++ .../migrations/0007_alter_system_options.py | 17 + pepfar_mle/common/models.py | 17 +- pepfar_mle/common/urls.py | 9 + pepfar_mle/common/views.py | 8 + pepfar_mle/ops/__init__.py | 0 pepfar_mle/ops/admin.py | 22 ++ pepfar_mle/ops/apps.py | 6 + pepfar_mle/ops/migrations/0001_initial.py | 86 +++++ pepfar_mle/ops/migrations/__init__.py | 0 pepfar_mle/ops/models.py | 100 ++++++ pepfar_mle/ops/tests/__init__.py | 0 pepfar_mle/ops/tests/test_models.py | 66 ++++ pepfar_mle/ops/urls.py | 20 ++ pepfar_mle/ops/views.py | 28 ++ pepfar_mle/templates/base.html | 334 ++---------------- .../templates/fragments/atoms/brand.html | 8 + .../fragments/atoms/dashboard_link.html | 7 + .../fragments/atoms/facilities_menu.html | 24 ++ ...toring_and_evaluation_data_entry_menu.html | 25 ++ .../fragments/atoms/program_menu.html | 24 ++ .../fragments/atoms/sidebar_toggle.html | 5 + pepfar_mle/templates/fragments/footer.html | 9 + pepfar_mle/templates/fragments/head.html | 34 ++ .../templates/fragments/logout_modal.html | 19 + pepfar_mle/templates/fragments/sidebar.html | 18 + pepfar_mle/templates/fragments/topbar.html | 103 ++++++ .../templates/pages/common/facilities.html | 12 + .../templates/pages/common/systems.html | 12 + .../templates/pages/ops/activity_log.html | 12 + .../pages/ops/daily_site_updates.html | 12 + .../templates/pages/ops/site_mentorship.html | 12 + pepfar_mle/templates/pages/ops/tickets.html | 12 + .../templates/pages/ops/timesheets.html | 12 + pepfar_mle/templates/pages/ops/versions.html | 12 + 38 files changed, 828 insertions(+), 378 deletions(-) create mode 100644 pepfar_mle/common/migrations/0006_system.py create mode 100644 pepfar_mle/common/migrations/0007_alter_system_options.py create mode 100644 pepfar_mle/common/urls.py create mode 100644 pepfar_mle/ops/__init__.py create mode 100644 pepfar_mle/ops/admin.py create mode 100644 pepfar_mle/ops/apps.py create mode 100644 pepfar_mle/ops/migrations/0001_initial.py create mode 100644 pepfar_mle/ops/migrations/__init__.py create mode 100644 pepfar_mle/ops/models.py create mode 100644 pepfar_mle/ops/tests/__init__.py create mode 100644 pepfar_mle/ops/tests/test_models.py create mode 100644 pepfar_mle/ops/urls.py create mode 100644 pepfar_mle/ops/views.py create mode 100644 pepfar_mle/templates/fragments/atoms/brand.html create mode 100644 pepfar_mle/templates/fragments/atoms/dashboard_link.html create mode 100644 pepfar_mle/templates/fragments/atoms/facilities_menu.html create mode 100644 pepfar_mle/templates/fragments/atoms/monitoring_and_evaluation_data_entry_menu.html create mode 100644 pepfar_mle/templates/fragments/atoms/program_menu.html create mode 100644 pepfar_mle/templates/fragments/atoms/sidebar_toggle.html create mode 100644 pepfar_mle/templates/fragments/footer.html create mode 100644 pepfar_mle/templates/fragments/head.html create mode 100644 pepfar_mle/templates/fragments/logout_modal.html create mode 100644 pepfar_mle/templates/fragments/sidebar.html create mode 100644 pepfar_mle/templates/fragments/topbar.html create mode 100644 pepfar_mle/templates/pages/common/facilities.html create mode 100644 pepfar_mle/templates/pages/common/systems.html create mode 100644 pepfar_mle/templates/pages/ops/activity_log.html create mode 100644 pepfar_mle/templates/pages/ops/daily_site_updates.html create mode 100644 pepfar_mle/templates/pages/ops/site_mentorship.html create mode 100644 pepfar_mle/templates/pages/ops/tickets.html create mode 100644 pepfar_mle/templates/pages/ops/timesheets.html create mode 100644 pepfar_mle/templates/pages/ops/versions.html diff --git a/config/settings/base.py b/config/settings/base.py index 70de0aae..d757bed1 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -26,29 +26,17 @@ # GENERAL # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#debug DEBUG = env.bool("DJANGO_DEBUG", False) -# Local time zone. Choices are -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# though not all of them may be available with every OS. -# In Windows, this must be set to your system time zone. TIME_ZONE = "Africa/Nairobi" -# https://docs.djangoproject.com/en/dev/ref/settings/#language-code LANGUAGE_CODE = "en-us" -# https://docs.djangoproject.com/en/dev/ref/settings/#site-id SITE_ID = 1 -# https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n USE_I18N = True -# https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n USE_L10N = True -# https://docs.djangoproject.com/en/dev/ref/settings/#use-tz USE_TZ = True -# https://docs.djangoproject.com/en/dev/ref/settings/#locale-paths LOCALE_PATHS = [str(ROOT_DIR / "locale")] # DATABASES # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#databases DATABASES = { "default": { "NAME": env.str("POSTGRES_DB"), @@ -63,15 +51,9 @@ # URLS # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf ROOT_URLCONF = "config.urls" -# https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application WSGI_APPLICATION = "config.wsgi.application" -# BigAutoField needs migration of existing data and either changes to -# dependencies or overriding dependencies -DEFAULT_AUTO_FIELD = "django.db.models.AutoField" - # APPS # ------------------------------------------------------------------------------ DJANGO_APPS = [ @@ -105,40 +87,32 @@ "pepfar_mle.retention.apps.RetentionConfig", "pepfar_mle.tb.apps.TBConfig", "pepfar_mle.treatment.apps.TreatmentConfig", + "pepfar_mle.ops.apps.OpsConfig", ] -# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS # MIGRATIONS # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#migration-modules MIGRATION_MODULES = {"sites": "pepfar_mle.contrib.sites.migrations"} # AUTHENTICATION # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends AUTHENTICATION_BACKENDS = [ "django.contrib.auth.backends.ModelBackend", "allauth.account.auth_backends.AuthenticationBackend", ] -# https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model AUTH_USER_MODEL = "users.User" -# https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url LOGIN_REDIRECT_URL = "users:redirect" -# https://docs.djangoproject.com/en/dev/ref/settings/#login-url LOGIN_URL = "account_login" # PASSWORDS # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers PASSWORD_HASHERS = [ - # https://docs.djangoproject.com/en/dev/topics/auth/passwords/#using-argon2-with-django "django.contrib.auth.hashers.Argon2PasswordHasher", "django.contrib.auth.hashers.PBKDF2PasswordHasher", "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", "django.contrib.auth.hashers.BCryptSHA256PasswordHasher", ] -# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, @@ -148,7 +122,6 @@ # MIDDLEWARE # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#middleware MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "corsheaders.middleware.CorsMiddleware", @@ -165,13 +138,9 @@ # STATIC # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#static-root STATIC_ROOT = str(ROOT_DIR / "staticfiles") -# https://docs.djangoproject.com/en/dev/ref/settings/#static-url STATIC_URL = "/static/" -# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS STATICFILES_DIRS = [str(APPS_DIR / "static")] -# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders STATICFILES_FINDERS = [ "django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder", @@ -179,28 +148,20 @@ # MEDIA # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#media-root MEDIA_ROOT = str(APPS_DIR / "media") -# https://docs.djangoproject.com/en/dev/ref/settings/#media-url MEDIA_URL = "/media/" # TEMPLATES # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#templates TEMPLATES = [ { - # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND "BACKEND": "django.template.backends.django.DjangoTemplates", - # https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs "DIRS": [str(APPS_DIR / "templates")], "OPTIONS": { - # https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders - # https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types "loaders": [ "django.template.loaders.filesystem.Loader", "django.template.loaders.app_directories.Loader", ], - # https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", @@ -215,77 +176,57 @@ }, } ] - -# https://docs.djangoproject.com/en/dev/ref/settings/#form-renderer FORM_RENDERER = "django.forms.renderers.TemplatesSetting" - -# http://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs CRISPY_TEMPLATE_PACK = "bootstrap4" # FIXTURES # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#fixture-dirs FIXTURE_DIRS = (str(APPS_DIR / "fixtures"),) # SECURITY # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly SESSION_COOKIE_HTTPONLY = True -# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly CSRF_COOKIE_HTTPONLY = True -# https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter SECURE_BROWSER_XSS_FILTER = True -# https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options X_FRAME_OPTIONS = "SAMEORIGIN" # needs to be SAMEORIGIN for the jet admin to work # EMAIL # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend EMAIL_BACKEND = env( "DJANGO_EMAIL_BACKEND", - # alternative for local dev with mailhog: django.core.mail.backends.smtp.EmailBackend default="anymail.backends.mailgun.EmailBackend", ) DEFAULT_FROM_EMAIL = env( "DJANGO_DEFAULT_FROM_EMAIL", - default="Fahari ya Jamii M&E System ", + default="Fahari System ", ) # https://docs.djangoproject.com/en/dev/ref/settings/#server-email SERVER_EMAIL = env("DJANGO_SERVER_EMAIL", default=DEFAULT_FROM_EMAIL) # https://docs.djangoproject.com/en/dev/ref/settings/#email-subject-prefix EMAIL_SUBJECT_PREFIX = env( "DJANGO_EMAIL_SUBJECT_PREFIX", - default="[Fahari ya Jamii M&E System ]", + default="[Fahari System ]", ) -# https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference -# https://anymail.readthedocs.io/en/stable/esps/mailgun/ ANYMAIL = { "MAILGUN_API_KEY": env("MAILGUN_API_KEY", default=""), # blank default for local dev and tests "MAILGUN_SENDER_DOMAIN": env("MAILGUN_DOMAIN", default="mle.savannahghi.org"), "MAILGUN_API_URL": env("MAILGUN_API_URL", default="https://api.mailgun.net/v3"), } -# https://docs.djangoproject.com/en/dev/ref/settings/#email-timeout EMAIL_TIMEOUT = 5 # ADMIN # ------------------------------------------------------------------------------ -# Django Admin URL. ADMIN_URL = "admin/" -# https://docs.djangoproject.com/en/dev/ref/settings/#admins ADMINS = [ ( """Savannah Informatics Global Health Institute""", "info@savannahghi.org", ) ] -# https://docs.djangoproject.com/en/dev/ref/settings/#managers MANAGERS = ADMINS # LOGGING # ------------------------------------------------------------------------------ -# https://docs.djangoproject.com/en/dev/ref/settings/#logging -# See https://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. LOGGING = { "version": 1, "disable_existing_loggers": False, @@ -323,12 +264,10 @@ # django-compressor # ------------------------------------------------------------------------------ -# https://django-compressor.readthedocs.io/en/latest/quickstart/#installation INSTALLED_APPS += ["compressor"] STATICFILES_FINDERS += ["compressor.finders.CompressorFinder"] # django-rest-framework # ------------------------------------------------------------------------------- -# django-rest-framework - https://www.django-rest-framework.org/api-guide/settings/ REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework.authentication.SessionAuthentication", @@ -336,11 +275,15 @@ ), "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), } - -# django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup CORS_URLS_REGEX = r"^/api/.*$" -# Your stuff... + +# Project specific settings # ------------------------------------------------------------------------------ +# these are used by the base model classes for validation DECIMAL_PLACES = 4 MAX_IMAGE_HEIGHT = 4320 MAX_IMAGE_WIDTH = 7680 + +# BigAutoField needs migration of existing data and either changes to +# dependencies or overriding dependencies +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" diff --git a/config/urls.py b/config/urls.py index aacc97b0..48803782 100644 --- a/config/urls.py +++ b/config/urls.py @@ -22,6 +22,9 @@ # User management path("users/", include("pepfar_mle.users.urls", namespace="users")), path("accounts/", include("allauth.urls")), + # our apps + path("common/", include("pepfar_mle.common.urls", namespace="common")), + path("ops/", include("pepfar_mle.ops.urls", namespace="ops")), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) if settings.DEBUG: # Static file serving when using Gunicorn + Uvicorn for local web socket development diff --git a/pepfar_mle/common/admin.py b/pepfar_mle/common/admin.py index 5a002d90..e6f69aa9 100644 --- a/pepfar_mle/common/admin.py +++ b/pepfar_mle/common/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Facility, FacilityAttachment, Organisation +from .models import Facility, FacilityAttachment, Organisation, System class BaseAdmin(admin.ModelAdmin): @@ -34,6 +34,11 @@ class OrganisationAdmin(BaseAdmin): pass +class SystemAdmin(BaseAdmin): + pass + + admin.site.register(Facility, FacilityAdmin) admin.site.register(FacilityAttachment, FacilityAttachmentAdmin) admin.site.register(Organisation, OrganisationAdmin) +admin.site.register(System, SystemAdmin) diff --git a/pepfar_mle/common/migrations/0006_system.py b/pepfar_mle/common/migrations/0006_system.py new file mode 100644 index 00000000..192fd578 --- /dev/null +++ b/pepfar_mle/common/migrations/0006_system.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.5 on 2021-07-17 06:46 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0005_auto_20210714_1633'), + ] + + operations = [ + migrations.CreateModel( + name='System', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('active', models.BooleanField(default=True)), + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('created_by', models.UUIDField(blank=True, null=True)), + ('updated', models.DateTimeField(default=django.utils.timezone.now)), + ('updated_by', models.UUIDField(blank=True, null=True)), + ('name', models.CharField(max_length=128, unique=True)), + ('description', models.TextField()), + ('organisation', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='common_system_related', to='common.organisation')), + ], + options={ + 'ordering': ('name',), + 'abstract': False, + }, + ), + ] diff --git a/pepfar_mle/common/migrations/0007_alter_system_options.py b/pepfar_mle/common/migrations/0007_alter_system_options.py new file mode 100644 index 00000000..9dbb6494 --- /dev/null +++ b/pepfar_mle/common/migrations/0007_alter_system_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.5 on 2021-07-17 08:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('common', '0006_system'), + ] + + operations = [ + migrations.AlterModelOptions( + name='system', + options={'ordering': ('name', '-updated')}, + ), + ] diff --git a/pepfar_mle/common/models.py b/pepfar_mle/common/models.py index c146e8b0..0c3c0f78 100644 --- a/pepfar_mle/common/models.py +++ b/pepfar_mle/common/models.py @@ -9,7 +9,7 @@ from django.db import models, transaction from django.db.models.base import ModelBase from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField from PIL import Image @@ -411,7 +411,20 @@ class FacilityAttachment(Attachment): organisation_verify = ["facility"] - class Meta: + class Meta(AbstractBase.Meta): """Define ordering and other attributes for attachments.""" ordering = ("-updated", "-created") + + +class System(AbstractBase): + """List of systems used in the public sector e.g Kenya EMR.""" + + name = models.CharField(max_length=128, null=False, blank=False, unique=True) + description = models.TextField() + + class Meta(AbstractBase.Meta): + ordering = ( + "name", + "-updated", + ) diff --git a/pepfar_mle/common/urls.py b/pepfar_mle/common/urls.py new file mode 100644 index 00000000..cdcddd1d --- /dev/null +++ b/pepfar_mle/common/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from .views import FacilityView, SystemsView + +app_name = "common" +urlpatterns = [ + path("facilities", view=FacilityView.as_view(), name="facilities"), + path("systems", view=SystemsView.as_view(), name="systems"), +] diff --git a/pepfar_mle/common/views.py b/pepfar_mle/common/views.py index 9213e5e2..9118a497 100644 --- a/pepfar_mle/common/views.py +++ b/pepfar_mle/common/views.py @@ -15,3 +15,11 @@ class HomeView(LoginRequiredMixin, ApprovedMixin, TemplateView): class AboutView(LoginRequiredMixin, ApprovedMixin, TemplateView): template_name = "pages/about.html" + + +class FacilityView(LoginRequiredMixin, ApprovedMixin, TemplateView): + template_name = "pages/common/facilities.html" + + +class SystemsView(LoginRequiredMixin, ApprovedMixin, TemplateView): + template_name = "pages/common/systems.html" diff --git a/pepfar_mle/ops/__init__.py b/pepfar_mle/ops/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pepfar_mle/ops/admin.py b/pepfar_mle/ops/admin.py new file mode 100644 index 00000000..051fc8d3 --- /dev/null +++ b/pepfar_mle/ops/admin.py @@ -0,0 +1,22 @@ +from django.contrib import admin + +from pepfar_mle.common.admin import BaseAdmin + +from .models import FacilitySystem, FacilitySystemTicket, TimeSheet + + +class FacilitySystemAdmin(BaseAdmin): + pass + + +class FacilitySystemTicketAdmin(BaseAdmin): + pass + + +class TimeSheetAdmin(BaseAdmin): + pass + + +admin.site.register(FacilitySystem, FacilitySystemAdmin) +admin.site.register(FacilitySystemTicket, FacilitySystemTicketAdmin) +admin.site.register(TimeSheet, TimeSheetAdmin) diff --git a/pepfar_mle/ops/apps.py b/pepfar_mle/ops/apps.py new file mode 100644 index 00000000..29dde7bc --- /dev/null +++ b/pepfar_mle/ops/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class OpsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "pepfar_mle.ops" diff --git a/pepfar_mle/ops/migrations/0001_initial.py b/pepfar_mle/ops/migrations/0001_initial.py new file mode 100644 index 00000000..6d313c53 --- /dev/null +++ b/pepfar_mle/ops/migrations/0001_initial.py @@ -0,0 +1,86 @@ +# Generated by Django 3.2.5 on 2021-07-17 08:51 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('common', '0007_alter_system_options'), + ] + + operations = [ + migrations.CreateModel( + name='FacilitySystem', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('active', models.BooleanField(default=True)), + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('created_by', models.UUIDField(blank=True, null=True)), + ('updated', models.DateTimeField(default=django.utils.timezone.now)), + ('updated_by', models.UUIDField(blank=True, null=True)), + ('version', models.CharField(max_length=64)), + ('facility', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='common.facility')), + ('organisation', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='ops_facilitysystem_related', to='common.organisation')), + ('system', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='common.system')), + ], + options={ + 'ordering': ('facility__name', 'system__name', '-version', '-updated_by', '-created_by'), + }, + ), + migrations.CreateModel( + name='TimeSheet', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('active', models.BooleanField(default=True)), + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('created_by', models.UUIDField(blank=True, null=True)), + ('updated', models.DateTimeField(default=django.utils.timezone.now)), + ('updated_by', models.UUIDField(blank=True, null=True)), + ('date', models.DateField()), + ('activity', models.TextField()), + ('output', models.TextField()), + ('hours', models.IntegerField()), + ('location', models.TextField()), + ('approved_at', models.DateTimeField(blank=True, null=True)), + ('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ('organisation', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='ops_timesheet_related', to='common.organisation')), + ('staff', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='timesheet_staff', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ('-date', '-approved_at', 'staff__name'), + }, + ), + migrations.CreateModel( + name='FacilitySystemTicket', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('active', models.BooleanField(default=True)), + ('created', models.DateTimeField(default=django.utils.timezone.now)), + ('created_by', models.UUIDField(blank=True, null=True)), + ('updated', models.DateTimeField(default=django.utils.timezone.now)), + ('updated_by', models.UUIDField(blank=True, null=True)), + ('details', models.TextField()), + ('raised', models.DateTimeField(default=django.utils.timezone.now)), + ('raised_by', models.TextField()), + ('resolved', models.DateTimeField(blank=True, null=True)), + ('resolved_by', models.TextField(blank=True, null=True)), + ('facility_system', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='ops.facilitysystem')), + ('organisation', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='ops_facilitysystemticket_related', to='common.organisation')), + ], + options={ + 'ordering': ('facility_system__facility__name', 'facility_system__system__name', '-raised', '-resolved'), + }, + ), + migrations.AddConstraint( + model_name='facilitysystem', + constraint=models.UniqueConstraint(fields=('facility', 'system', 'version'), name='unique_together_facility_and_system_and_version'), + ), + ] diff --git a/pepfar_mle/ops/migrations/__init__.py b/pepfar_mle/ops/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pepfar_mle/ops/models.py b/pepfar_mle/ops/models.py new file mode 100644 index 00000000..f5b0c6f2 --- /dev/null +++ b/pepfar_mle/ops/models.py @@ -0,0 +1,100 @@ +from django.core.exceptions import ValidationError +from django.db import models +from django.utils import timezone + +from pepfar_mle.common.models import AbstractBase, Facility, System +from pepfar_mle.users.models import User + + +class TimeSheet(AbstractBase): + """Staff daily time sheets.""" + + date = models.DateField() + activity = models.TextField() + output = models.TextField() + hours = models.IntegerField() + location = models.TextField() + staff = models.ForeignKey(User, on_delete=models.PROTECT, related_name="timesheet_staff") + approved_by = models.ForeignKey(User, on_delete=models.PROTECT, null=True, blank=True) + approved_at = models.DateTimeField(null=True, blank=True) + + model_validators = ["validate_approval"] + + @property + def is_full_day(self): + return self.hours >= 8 + + @property + def is_approved(self): + return self.approved_at is not None + + def validate_approval(self): + error_msg = "approved_at and approved_by must both be set" + if self.approved_at is not None and self.approved_by is None: + raise ValidationError(error_msg) + + if self.approved_by is not None and self.approved_at is None: + raise ValidationError(error_msg) + + class Meta: + ordering = ( + "-date", + "-approved_at", + "staff__name", + ) + + +class FacilitySystem(AbstractBase): + """A registry of systems and versions for different facilities.""" + + facility = models.ForeignKey(Facility, on_delete=models.PROTECT) + system = models.ForeignKey(System, on_delete=models.PROTECT) + version = models.CharField(max_length=64) + + class Meta: + ordering = ( + "facility__name", + "system__name", + "-version", + "-updated_by", + "-created_by", + ) + constraints = [ + models.UniqueConstraint( + fields=["facility", "system", "version"], + name="unique_together_facility_and_system_and_version", + ) + ] + + +class FacilitySystemTicket(AbstractBase): + """Tickets raised against specific tickets and versions.""" + + facility_system = models.ForeignKey(FacilitySystem, on_delete=models.PROTECT) + details = models.TextField() + raised = models.DateTimeField(default=timezone.now) + raised_by = models.TextField() + resolved = models.DateTimeField(null=True, blank=True) + resolved_by = models.TextField(null=True, blank=True) + + model_validators = ["validate_resolved"] + + @property + def is_open(self): + return self.resolved is None + + def validate_resolved(self): + error_msg = "resolved and resolved_by must both be set" + if self.resolved is not None and self.resolved_by is None: + raise ValidationError(error_msg) + + if self.resolved_by is not None and self.resolved is None: + raise ValidationError(error_msg) + + class Meta: + ordering = ( + "facility_system__facility__name", + "facility_system__system__name", + "-raised", + "-resolved", + ) diff --git a/pepfar_mle/ops/tests/__init__.py b/pepfar_mle/ops/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pepfar_mle/ops/tests/test_models.py b/pepfar_mle/ops/tests/test_models.py new file mode 100644 index 00000000..5d4f6c11 --- /dev/null +++ b/pepfar_mle/ops/tests/test_models.py @@ -0,0 +1,66 @@ +import pytest +from django.core.exceptions import ValidationError +from django.utils import timezone +from model_bakery import baker + +from pepfar_mle.ops.models import FacilitySystemTicket, TimeSheet + +pytestmark = pytest.mark.django_db + + +def test_facility_ticket_status(admin_user): + open_ticket = baker.make(FacilitySystemTicket, resolved=None, resolved_by=None) + assert open_ticket.is_open is True + + closed_ticket = baker.make( + FacilitySystemTicket, resolved=timezone.now(), resolved_by=admin_user + ) + assert closed_ticket.is_open is False + + +def test_timesheet_is_full_day(): + full_day_timesheet = baker.make(TimeSheet, hours=9) + assert full_day_timesheet.is_full_day is True + + part_day_timesheet = baker.make(TimeSheet, hours=7) + assert part_day_timesheet.is_full_day is False + + +def test_timesheet_is_approved(admin_user): + approved_timesheet = baker.make(TimeSheet, approved_at=timezone.now(), approved_by=admin_user) + assert approved_timesheet.is_approved is True + + non_approved_timesheet = baker.make(TimeSheet, approved_at=None, approved_by=None) + assert non_approved_timesheet.is_approved is False + + +def test_timesheet_validate_approval(admin_user): + with pytest.raises(ValidationError) as e: + bad_timesheet = baker.prepare(TimeSheet, approved_at=timezone.now(), approved_by=None) + bad_timesheet.save() + + assert "approved_at and approved_by must both be set" in str(e) + + with pytest.raises(ValidationError) as e: + bad_timesheet = baker.prepare(TimeSheet, approved_at=None, approved_by=admin_user) + bad_timesheet.save() + + assert "approved_at and approved_by must both be set" in str(e) + + +def test_facility_ticket_validate_resolved(admin_user): + with pytest.raises(ValidationError) as e: + bad_ticket_resolve = baker.prepare( + FacilitySystemTicket, resolved=timezone.now(), resolved_by=None + ) + bad_ticket_resolve.save() + + assert "resolved and resolved_by must both be set" in str(e) + + with pytest.raises(ValidationError) as e: + bad_ticket_resolve = baker.prepare( + FacilitySystemTicket, resolved=None, resolved_by=admin_user + ) + bad_ticket_resolve.save() + + assert "resolved and resolved_by must both be set" in str(e) diff --git a/pepfar_mle/ops/urls.py b/pepfar_mle/ops/urls.py new file mode 100644 index 00000000..a31593c4 --- /dev/null +++ b/pepfar_mle/ops/urls.py @@ -0,0 +1,20 @@ +from django.urls import path + +from .views import ( + ActivityLogView, + DailySiteUpdatesView, + SiteMentorshipView, + TicketsView, + TimeSheetsView, + VersionsView, +) + +app_name = "ops" +urlpatterns = [ + path("versions", view=VersionsView.as_view(), name="versions"), + path("tickets", view=TicketsView.as_view(), name="tickets"), + path("activity_log", view=ActivityLogView.as_view(), name="activity_log"), + path("site_mentorship", view=SiteMentorshipView.as_view(), name="site_mentorship"), + path("daily_site_updates", view=DailySiteUpdatesView.as_view(), name="daily_site_updates"), + path("timesheets", view=TimeSheetsView.as_view(), name="timesheets"), +] diff --git a/pepfar_mle/ops/views.py b/pepfar_mle/ops/views.py new file mode 100644 index 00000000..0b0f652a --- /dev/null +++ b/pepfar_mle/ops/views.py @@ -0,0 +1,28 @@ +from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic import TemplateView + +from pepfar_mle.common.views import ApprovedMixin + + +class VersionsView(LoginRequiredMixin, ApprovedMixin, TemplateView): + template_name = "pages/ops/versions.html" + + +class TicketsView(LoginRequiredMixin, ApprovedMixin, TemplateView): + template_name = "pages/ops/tickets.html" + + +class ActivityLogView(LoginRequiredMixin, ApprovedMixin, TemplateView): + template_name = "pages/ops/activity_log.html" + + +class SiteMentorshipView(LoginRequiredMixin, ApprovedMixin, TemplateView): + template_name = "pages/ops/site_mentorship.html" + + +class DailySiteUpdatesView(LoginRequiredMixin, ApprovedMixin, TemplateView): + template_name = "pages/ops/daily_site_updates.html" + + +class TimeSheetsView(LoginRequiredMixin, ApprovedMixin, TemplateView): + template_name = "pages/ops/timesheets.html" diff --git a/pepfar_mle/templates/base.html b/pepfar_mle/templates/base.html index 9f2829ad..012e3785 100644 --- a/pepfar_mle/templates/base.html +++ b/pepfar_mle/templates/base.html @@ -1,325 +1,43 @@ {% load static i18n compress%} - - - - - - - {% block title %}Program Database{% endblock title %} - - - - - - - - {% block css %} - - {% compress css %} - - - {% endcompress %} {% endblock %} - {# Placed at the top of the document so pages load faster with defer #} {% block javascript %} - {% compress js %} - - {% endcompress %} - - - - {% compress js %} - {% endcompress %} {% endblock javascript %} - - +{% include "fragments/head.html" %} {% block inner %} {# pages that do not need the wrapper (e.g login) should use this slot and turn off the page block #} {% endblock inner %} - {% block page %} - - -
- - {% block sidebar %} - - - {% endblock sidebar %} - - - -
- - -
- - - {% block topbar %} - - {% endblock topbar %} - - - -
- -

- {% block pagetitle %}{%endblock pagetitle %} -

- - {% block content %} -

This is a blank page (placeholder).

- {% endblock content %} -
- - -
- - - - {% block footer %} -
-
-
- {% endblock footer %} - - -
- -
- - - - - - - - {% block modal %} - - - {% endblock modal %} - + + + + {% block modal %} + {% include "fragments/logout_modal.html" %} + {% endblock modal %} {% endblock page %} - {% block inline_javascript %} {# Script tags with only code, no src (defer by default) #} {% endblock inline_javascript %} - diff --git a/pepfar_mle/templates/fragments/atoms/brand.html b/pepfar_mle/templates/fragments/atoms/brand.html new file mode 100644 index 00000000..ab315e01 --- /dev/null +++ b/pepfar_mle/templates/fragments/atoms/brand.html @@ -0,0 +1,8 @@ +{% load static i18n compress%} + + + + + diff --git a/pepfar_mle/templates/fragments/atoms/dashboard_link.html b/pepfar_mle/templates/fragments/atoms/dashboard_link.html new file mode 100644 index 00000000..4d37eaf7 --- /dev/null +++ b/pepfar_mle/templates/fragments/atoms/dashboard_link.html @@ -0,0 +1,7 @@ +{% load static i18n compress%} + + diff --git a/pepfar_mle/templates/fragments/atoms/facilities_menu.html b/pepfar_mle/templates/fragments/atoms/facilities_menu.html new file mode 100644 index 00000000..7fff10fc --- /dev/null +++ b/pepfar_mle/templates/fragments/atoms/facilities_menu.html @@ -0,0 +1,24 @@ +{% load static i18n compress%} + + diff --git a/pepfar_mle/templates/fragments/atoms/monitoring_and_evaluation_data_entry_menu.html b/pepfar_mle/templates/fragments/atoms/monitoring_and_evaluation_data_entry_menu.html new file mode 100644 index 00000000..0ec120c3 --- /dev/null +++ b/pepfar_mle/templates/fragments/atoms/monitoring_and_evaluation_data_entry_menu.html @@ -0,0 +1,25 @@ +{% load static i18n compress%} + + diff --git a/pepfar_mle/templates/fragments/atoms/program_menu.html b/pepfar_mle/templates/fragments/atoms/program_menu.html new file mode 100644 index 00000000..3b6c2a09 --- /dev/null +++ b/pepfar_mle/templates/fragments/atoms/program_menu.html @@ -0,0 +1,24 @@ +{% load static i18n compress%} + + diff --git a/pepfar_mle/templates/fragments/atoms/sidebar_toggle.html b/pepfar_mle/templates/fragments/atoms/sidebar_toggle.html new file mode 100644 index 00000000..1b6148bb --- /dev/null +++ b/pepfar_mle/templates/fragments/atoms/sidebar_toggle.html @@ -0,0 +1,5 @@ +{% load static i18n compress%} + +
+ +
diff --git a/pepfar_mle/templates/fragments/footer.html b/pepfar_mle/templates/fragments/footer.html new file mode 100644 index 00000000..03a22b7e --- /dev/null +++ b/pepfar_mle/templates/fragments/footer.html @@ -0,0 +1,9 @@ +{% load static i18n compress %} + +
+
+ +
+
diff --git a/pepfar_mle/templates/fragments/head.html b/pepfar_mle/templates/fragments/head.html new file mode 100644 index 00000000..7f270e30 --- /dev/null +++ b/pepfar_mle/templates/fragments/head.html @@ -0,0 +1,34 @@ +{% load static i18n compress%} + + + + + + + {% block title %}Program Database{% endblock title %} + + + + + + + + {% block css %} + + {% compress css %} + + + {% endcompress %} {% endblock %} + {# Placed at the top of the document so pages load faster with defer #} {% block javascript %} + {% compress js %} + + {% endcompress %} + + + + {% compress js %} + {% endcompress %} {% endblock javascript %} + + diff --git a/pepfar_mle/templates/fragments/logout_modal.html b/pepfar_mle/templates/fragments/logout_modal.html new file mode 100644 index 00000000..dc506e06 --- /dev/null +++ b/pepfar_mle/templates/fragments/logout_modal.html @@ -0,0 +1,19 @@ +{% load static i18n compress %} + + diff --git a/pepfar_mle/templates/fragments/sidebar.html b/pepfar_mle/templates/fragments/sidebar.html new file mode 100644 index 00000000..fb140655 --- /dev/null +++ b/pepfar_mle/templates/fragments/sidebar.html @@ -0,0 +1,18 @@ +{% load static i18n compress%} + + diff --git a/pepfar_mle/templates/fragments/topbar.html b/pepfar_mle/templates/fragments/topbar.html new file mode 100644 index 00000000..0f36ded7 --- /dev/null +++ b/pepfar_mle/templates/fragments/topbar.html @@ -0,0 +1,103 @@ +{% load static i18n compress%} + + diff --git a/pepfar_mle/templates/pages/common/facilities.html b/pepfar_mle/templates/pages/common/facilities.html new file mode 100644 index 00000000..be83aa74 --- /dev/null +++ b/pepfar_mle/templates/pages/common/facilities.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block title %} + Facilities +{% endblock %} +{% block pagetitle %}{% endblock pagetitle %} +{% block content %} +

Facilities

+

+ Placeholder page for facilities. +

+ ← Back to Dashboard +{% endblock content %} diff --git a/pepfar_mle/templates/pages/common/systems.html b/pepfar_mle/templates/pages/common/systems.html new file mode 100644 index 00000000..fdb8ab0a --- /dev/null +++ b/pepfar_mle/templates/pages/common/systems.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block title %} + Systems +{% endblock %} +{% block pagetitle %}{% endblock pagetitle %} +{% block content %} +

Systems

+

+ Placeholder page for systems. +

+ ← Back to Dashboard +{% endblock content %} diff --git a/pepfar_mle/templates/pages/ops/activity_log.html b/pepfar_mle/templates/pages/ops/activity_log.html new file mode 100644 index 00000000..fffe1d30 --- /dev/null +++ b/pepfar_mle/templates/pages/ops/activity_log.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block title %} + Activity Log +{% endblock %} +{% block pagetitle %}{% endblock pagetitle %} +{% block content %} +

Activity Log

+

+ Placeholder page for activity log +

+ ← Back to Dashboard +{% endblock content %} diff --git a/pepfar_mle/templates/pages/ops/daily_site_updates.html b/pepfar_mle/templates/pages/ops/daily_site_updates.html new file mode 100644 index 00000000..7860647f --- /dev/null +++ b/pepfar_mle/templates/pages/ops/daily_site_updates.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block title %} + Daily Site Updates +{% endblock %} +{% block pagetitle %}{% endblock pagetitle %} +{% block content %} +

Daily Site Updates

+

+ Placeholder page for daily site updates +

+ ← Back to Dashboard +{% endblock content %} diff --git a/pepfar_mle/templates/pages/ops/site_mentorship.html b/pepfar_mle/templates/pages/ops/site_mentorship.html new file mode 100644 index 00000000..83cee64d --- /dev/null +++ b/pepfar_mle/templates/pages/ops/site_mentorship.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block title %} + Site Mentorship +{% endblock %} +{% block pagetitle %}{% endblock pagetitle %} +{% block content %} +

Site Mentorship

+

+ Placeholder page for site mentorship +

+ ← Back to Dashboard +{% endblock content %} diff --git a/pepfar_mle/templates/pages/ops/tickets.html b/pepfar_mle/templates/pages/ops/tickets.html new file mode 100644 index 00000000..09389c5a --- /dev/null +++ b/pepfar_mle/templates/pages/ops/tickets.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block title %} + Facility Tickets +{% endblock %} +{% block pagetitle %}{% endblock pagetitle %} +{% block content %} +

Facility Tickets

+

+ Placeholder page for facility tickets +

+ ← Back to Dashboard +{% endblock content %} diff --git a/pepfar_mle/templates/pages/ops/timesheets.html b/pepfar_mle/templates/pages/ops/timesheets.html new file mode 100644 index 00000000..6dea3f53 --- /dev/null +++ b/pepfar_mle/templates/pages/ops/timesheets.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block title %} + Timesheets +{% endblock %} +{% block pagetitle %}{% endblock pagetitle %} +{% block content %} +

Timesheets

+

+ Placeholder page for timesheets +

+ ← Back to Dashboard +{% endblock content %} diff --git a/pepfar_mle/templates/pages/ops/versions.html b/pepfar_mle/templates/pages/ops/versions.html new file mode 100644 index 00000000..efc7789c --- /dev/null +++ b/pepfar_mle/templates/pages/ops/versions.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block title %} + Facility System Versions +{% endblock %} +{% block pagetitle %}{% endblock pagetitle %} +{% block content %} +

Facility System Versions

+

+ Placeholder page for facility system versions. +

+ ← Back to Dashboard +{% endblock content %}