diff --git a/Dockerfile b/Dockerfile index b1bbbb147..e863ef3a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,8 @@ RUN apt-get update --yes --quiet \ libpq5 \ && pip install --upgrade pip \ && pip install pip-tools \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && apt-get install -y git WORKDIR /opt RUN python -m venv venv ENV PATH="/opt/venv/bin:$PATH" diff --git a/notifications/README.md b/admin_notifications/README.md similarity index 100% rename from notifications/README.md rename to admin_notifications/README.md diff --git a/notifications/__init__.py b/admin_notifications/__init__.py similarity index 100% rename from notifications/__init__.py rename to admin_notifications/__init__.py diff --git a/admin_notifications/apps.py b/admin_notifications/apps.py new file mode 100644 index 000000000..48789da9a --- /dev/null +++ b/admin_notifications/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AdminNotificationsConfig(AppConfig): + name = 'admin_notifications' diff --git a/notifications/migrations/0001_initial.py b/admin_notifications/migrations/0001_initial.py similarity index 83% rename from notifications/migrations/0001_initial.py rename to admin_notifications/migrations/0001_initial.py index 8fbdf6886..7294fda61 100644 --- a/notifications/migrations/0001_initial.py +++ b/admin_notifications/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.1.14 on 2022-07-29 13:17 +# Generated by Django 5.2.4 on 2025-09-04 11:37 from django.db import migrations, models @@ -13,13 +13,13 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Notification', + name='AdminNotification', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('head', models.CharField(max_length=255)), ('body', models.TextField()), ('url', models.URLField(blank=True, null=True)), - ('groups', models.ManyToManyField(related_name='notifications', to='auth.Group')), + ('groups', models.ManyToManyField(related_name='admin_notifications', to='auth.group')), ], ), ] diff --git a/notifications/migrations/__init__.py b/admin_notifications/migrations/__init__.py similarity index 100% rename from notifications/migrations/__init__.py rename to admin_notifications/migrations/__init__.py diff --git a/notifications/models.py b/admin_notifications/models.py similarity index 80% rename from notifications/models.py rename to admin_notifications/models.py index 464f46cf7..82b661427 100644 --- a/notifications/models.py +++ b/admin_notifications/models.py @@ -1,12 +1,12 @@ from django.db import models - -class Notification(models.Model): +class AdminNotification(models.Model): head = models.CharField(max_length=255) body = models.TextField() url = models.URLField(null=True, blank=True) - groups = models.ManyToManyField(to='auth.Group', related_name='notifications') + groups = models.ManyToManyField(to='auth.Group', related_name='admin_notifications') def __str__(self): return self.head + diff --git a/notifications/processors.py b/admin_notifications/processors.py similarity index 100% rename from notifications/processors.py rename to admin_notifications/processors.py diff --git a/admin_notifications/views.py b/admin_notifications/views.py new file mode 100644 index 000000000..938f32541 --- /dev/null +++ b/admin_notifications/views.py @@ -0,0 +1,61 @@ +import json +from user_notifications.models import NotificationMeta, NotificationLog +from wagtail_modeladmin.views import CreateView +from webpush import send_user_notification +from notifications.signals import notify +from notifications.models import Notification +from iogt_users.models import User + + +class CreateNotificationView(CreateView): + def form_valid(self, form): + payload = form.cleaned_data.copy() + groups = payload.pop('groups') + users = User.objects.filter(groups__in=groups).distinct() + for user in users: + try: + # 1. Create Notification + notify.send( + sender=self.request.user, + recipient=user, + verb=payload.get('head', 'New Notification'), + description=payload.get('body', ''), + url=payload.get("url", "/") + ) + + # 2. Get latest Notification for user (created just now) + notif_instance = Notification.objects.filter(recipient=user).order_by('-timestamp').first() + if not notif_instance: + continue # Shouldn't happen, but guard just in case + + # 3. Avoid duplicate meta creation + NotificationMeta.objects.get_or_create(notification=notif_instance) + + # 4. Send Web Push + send_user_notification( + user=user, + payload={ + "title": payload.get("head", "IoGT Notification"), + "body": payload.get("body", ""), + "url": payload.get("url", "/"), + "notification_id": notif_instance.id + }, + ttl=1000) + NotificationLog.objects.create( + user=user, + notification_key=payload.get("head", "IoGT Notification"), + tags='', + state="sent", + notification=notif_instance + ) + except Exception as e: + # Optional: log failure for user if user or template not found + NotificationLog.objects.create( + user=user, + notification_key=payload.get("head", "IoGT Notification"), + tags='', + state="failed", + error_message=f"Task failure: {str(e)}", + notification=None + ) + return super().form_valid(form) diff --git a/admin_notifications/wagtail_hooks.py b/admin_notifications/wagtail_hooks.py new file mode 100644 index 000000000..44329a0ad --- /dev/null +++ b/admin_notifications/wagtail_hooks.py @@ -0,0 +1,16 @@ +from django.conf import settings +from wagtail_modeladmin.options import ModelAdmin +from admin_notifications.models import AdminNotification +from admin_notifications.views import CreateNotificationView + + +class NotificationModelAdmin(ModelAdmin): + model = AdminNotification + menu_label = 'Admin Notifications' + menu_icon = 'mail' + list_display = ('head', 'body', 'url',) + list_filter = ('groups',) + search_fields = ('head', 'body', 'url',) + menu_order = 601 + create_view_class = CreateNotificationView + diff --git a/comments/button_helpers.py b/comments/button_helpers.py index d37c35609..cfad23142 100644 --- a/comments/button_helpers.py +++ b/comments/button_helpers.py @@ -1,5 +1,5 @@ from django.urls import reverse -from wagtail.contrib.modeladmin.helpers import ButtonHelper +from wagtail_modeladmin.helpers import ButtonHelper class XtdCommentAdminButtonHelper(ButtonHelper): diff --git a/comments/templates/comment_reply.html b/comments/templates/comment_reply.html index 95f448a9c..a8ce979e1 100644 --- a/comments/templates/comment_reply.html +++ b/comments/templates/comment_reply.html @@ -6,7 +6,7 @@
-

{% icon name='openquote' class_name="header-title-icon" %} +

{% icon name='openquote' classname="header-title-icon" %} Add reply for Page {{ comment.content_object }}

diff --git a/comments/views.py b/comments/views.py index 270669079..2463e9cfa 100644 --- a/comments/views.py +++ b/comments/views.py @@ -8,7 +8,7 @@ from django.db.models import Q from django.shortcuts import get_object_or_404, redirect from django.utils.decorators import method_decorator -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views import View from django.views.decorators.csrf import csrf_protect from django.views.decorators.http import require_POST diff --git a/comments/wagtail_hooks.py b/comments/wagtail_hooks.py index 3b9a6333c..fccece6f4 100644 --- a/comments/wagtail_hooks.py +++ b/comments/wagtail_hooks.py @@ -4,8 +4,8 @@ from django.utils import timezone from django.utils.html import format_html from django_comments_xtd.models import XtdComment -from wagtail.contrib.modeladmin.options import ModelAdminGroup, ModelAdmin, modeladmin_register -from wagtail.contrib.modeladmin.helpers.permission import PermissionHelper +from wagtail_modeladmin.options import ModelAdminGroup, ModelAdmin, modeladmin_register +from wagtail_modeladmin.helpers.permission import PermissionHelper from .button_helpers import XtdCommentAdminButtonHelper from .filters import FlaggedFilter, ModerationFilter, PublishedFilter diff --git a/common/translation_utils/translation_status.csv b/common/translation_utils/translation_status.csv index a5d3fb642..4c3b67cd4 100644 --- a/common/translation_utils/translation_status.csv +++ b/common/translation_utils/translation_status.csv @@ -716,3 +716,7 @@ Custom permissions,,,,needs translation,,, Submit,,translate,,has partial translation,,, Farsi,,not needed,,not needed,,, Burmese,,not needed,,not needed,,, +Hausa,,not needed,,not needed,,, +Yoruba,,not needed,,not needed,,, +Igbo,,not needed,,not needed,,, +Pidgin,,not needed,,not needed,,, diff --git a/common/translation_utils/translations.csv b/common/translation_utils/translations.csv index 3ad9e949a..3e651baed 100644 --- a/common/translation_utils/translations.csv +++ b/common/translation_utils/translations.csv @@ -1,7 +1,7 @@ -Row type,is in use,comment,English,Spanish,French,Portuguese,Arabic,Swahili,Chichewa,Kinyarwanda,Ndebele,Shona,Kirundi,Malagasy,Nepali,Urdu,Kichwa/Quichua,Russian,Zulu,Tigrinya,Tajik,Kurdish,Khmer,Uzbek,Karakalpak,Indonesian,Sinhala,Tamil,Bengali,Dari,Pashto,Hindi,Ukraine,Turkish,Farsi,Burmese -Language,,,English,Spanish,French,Portuguese,Arabic,Swahili,Chichewa,Kinyarwanda,Ndebele,Shona,Kirundi,Malagasy,Nepali,Urdu,Kichwa/Quichua,Russian,Zulu,Tigrinya,Tajik,Kurdish,Khmer,Uzbek,Karakalpak,Indonesian,Sinhala,Tamil,Bengali,Dari,Pashto,Hindi,Ukraine,Turkish,Farsi,Burmese -ll-LL,,,en,es,fr-CG,pt-MZ,ar-MA,sw-KE,ny-MW,rw-RW,nr-ZW,sn-ZW,rn-BI,mg-MG,ne-NP,ur-PK,qu-EC,ru-RU,zu-ZA,ti-ET,tg-TJ,ku,km-KH,uz-UZ,kaa,id,si,ta,bn-BN,prs,ps,hi,uk,tr,fa,my -ll,,,en,es,fr,pt,ar,sw,ny,rw,nr,sn,rn,mg,ne,ur,qu,ru,zu,ti,tg,ku,km,uz,kaa,id,si,ta,bn,prs,ps,hi,uk,tr,fa,my +Row type,is in use,comment,English,Spanish,French,Portuguese,Arabic,Swahili,Chichewa,Kinyarwanda,Ndebele,Shona,Kirundi,Malagasy,Nepali,Urdu,Kichwa/Quichua,Russian,Zulu,Tigrinya,Tajik,Kurdish,Khmer,Uzbek,Karakalpak,Indonesian,Sinhala,Tamil,Bengali,Dari,Pashto,Hindi,Ukraine,Turkish,Farsi,Burmese,Hausa,Yoruba,Igbo,Pidgin +Language,,,English,Spanish,French,Portuguese,Arabic,Swahili,Chichewa,Kinyarwanda,Ndebele,Shona,Kirundi,Malagasy,Nepali,Urdu,Kichwa/Quichua,Russian,Zulu,Tigrinya,Tajik,Kurdish,Khmer,Uzbek,Karakalpak,Indonesian,Sinhala,Tamil,Bengali,Dari,Pashto,Hindi,Ukraine,Turkish,Farsi,Burmese,Hausa,Yoruba,Igbo,Pidgin +ll-LL,,,en,es,fr-CG,pt-MZ,ar-MA,sw-KE,ny-MW,rw-RW,nr-ZW,sn-ZW,rn-BI,mg-MG,ne-NP,ur-PK,qu-EC,ru-RU,zu-ZA,ti-ET,tg-TJ,ku,km-KH,uz-UZ,kaa,id,si,ta,bn-BN,prs,ps,hi,uk,tr,fa,my,ha,yo,ig,pcm +ll,,,en,es,fr,pt,ar,sw,ny,rw,nr,sn,rn,mg,ne,ur,qu,ru,zu,ti,tg,ku,km,uz,kaa,id,si,ta,bn,prs,ps,hi,uk,tr,fa,my,ha,yo,ig,pcm Region,,,English,Latin America,"West African, DRC","Angola, Mozambique",Morocco,Kenya,"Malawi, Zambia",Rwanda,Zimbabwe,Zimbabwe,Burundi,Madagascar,Nepal,Pakistan,Ecuador,Russia,South Africa,Ethiopia,Tajikistan,Sorani,Cambodia,Uzbekistan,Karakalpakstan,Indonesia,Sri Lanka,Sri Lanka,Bangladesh,Afghanistan,Afghanistan,India,Ukraine,Turkey,Iran,Myanmar Section,,,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Homepage,Головна,,, ,,,Polls,Encuestas,Sondages,Votações,استطلاعات الرأي,Utafiti,Kafukufuku ofunsa maganizo a anthu,Amatora,Ikhetho,Sarudzo,Amatora,Fitsapan-kevitra,चुनावहरू,پولز,Tapuykuna,Голосования,Ukuvota,መረጻታት,Назарсанҷиҳо,دەنگدانەکان,ការបោះឆ្នោតស្ទង់មតិ,Tanlovlar,Sorawnama,Jajak pendapat,,,,,,,Опитування,,, diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 445b2fcf0..4efbcd761 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -19,4 +19,4 @@ services: environment: POSTGRES_USER: iogt POSTGRES_PASSWORD: iogt - POSTGRES_DB: iogt + POSTGRES_DB: iogt \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 3bd1b4755..99316da04 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,10 +5,43 @@ services: target: dev args: requirements: requirements.dev.txt - environment: - DJANGO_SETTINGS_MODULE: iogt.settings.dev + env_file: + - .env image: iogt:latest + ports: - "8000:8000" + depends_on: + - db volumes: - ./:/app/ +# db: +# image: postgres:14-alpine +# environment: +# POSTGRES_USER: postgres +# POSTGRES_PASSWORD: postgresiogt +# POSTGRES_DB: iogt + celery: + build: + context: ./ + target: dev + args: + requirements: requirements.dev.txt + command: celery -A iogt worker -l info + depends_on: + - django + - redis + env_file: + - .env + volumes: + - ./:/app/ + + redis: + image: redis:7 + + db: + image: postgres:14-alpine + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgresiogt + POSTGRES_DB: iogt \ No newline at end of file diff --git a/home/admin.py b/home/admin.py index 0580f8748..722912c9a 100644 --- a/home/admin.py +++ b/home/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register +from wagtail_modeladmin.options import ModelAdmin, modeladmin_register from django.db.models import Avg, Count from django.utils.html import format_html from django.urls import reverse diff --git a/home/forms.py b/home/forms.py index a88ee6b3e..e04bf18f0 100644 --- a/home/forms.py +++ b/home/forms.py @@ -15,4 +15,4 @@ def clean(self): f'This section is not eligible for showing the progress bar. ' f'Disable "Show Progress Bar" on "{progress_bar_enabled_ancestor_title}" section first.') - return cleaned_data + return cleaned_data \ No newline at end of file diff --git a/home/management/commands/copy_page_revisions.py b/home/management/commands/copy_page_revisions.py new file mode 100644 index 000000000..c23a56146 --- /dev/null +++ b/home/management/commands/copy_page_revisions.py @@ -0,0 +1,60 @@ +import logging +from django.core.management.base import BaseCommand +from django.contrib.contenttypes.models import ContentType +from django.db import connection +from wagtail.models import Page, Revision + +logger = logging.getLogger(__name__) + +class Command(BaseCommand): + help = "Copy missing revisions from wagtailcore_pagerevision to wagtail_revision" + + def handle(self, *args, **options): + page_ct = ContentType.objects.get_for_model(Page) + copied, skipped, errors = 0, 0, 0 + + logger.info("Starting migration from wagtailcore_pagerevision → wagtail_revision") + + with connection.cursor() as cursor: + cursor.execute(""" + SELECT id, created_at, content, approved_go_live_at, page_id, user_id + FROM wagtailcore_pagerevision + """) + rows = cursor.fetchall() + + for row in rows: + pr_id, created_at, content, approved_go_live_at, page_id, user_id = row + + page = Page.objects.filter(id=page_id).first() + if not page: + logger.warning(f"Skipping orphaned revision {pr_id}, page_id={page_id} not found") + continue + + # Deduplication check + if Revision.objects.filter( + content_type=page_ct, + object_id=str(page.id), + created_at=created_at, + ).exists(): + skipped += 1 + logger.debug(f"Skipped duplicate revision {pr_id} for page {page_id}") + continue + + try: + Revision.objects.create( + base_content_type=page_ct, + content=content, + approved_go_live_at=approved_go_live_at, + created_at=created_at, + user_id=user_id, + content_type=page_ct, + object_id=str(page.id), + object_str=str(page), + ) + copied += 1 + logger.info(f"Copied revision {pr_id} → page {page_id}") + except Exception as e: + errors += 1 + logger.error(f"Error copying revision {pr_id}: {e}") + + logger.info(f"Migration completed. Copied={copied}, Skipped={skipped}, Errors={errors}") diff --git a/home/migrations/0060_auto_20250703_0253.py b/home/migrations/0060_auto_20250703_0253.py new file mode 100644 index 000000000..c5e3f151a --- /dev/null +++ b/home/migrations/0060_auto_20250703_0253.py @@ -0,0 +1,88 @@ +# Generated by Django 3.2.25 on 2025-07-03 02:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0059_burmese_locale'), + ] + + operations = [ + migrations.AlterField( + model_name='articlefeedback', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='articlerecommendation', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='articletaggeditem', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='featuredcontent', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='feedbacksettings', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='homepagebanner', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='iogtflatmenuitem', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='localedetail', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='manifestsettings', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='sectiontaggeditem', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='sitesettings', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='svgtopngmap', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='themesettings', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='v1pageurltov2pagemap', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='v1tov2objectmap', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/home/migrations/0061_article_notification_tags.py b/home/migrations/0061_article_notification_tags.py new file mode 100644 index 000000000..bbbceb1e2 --- /dev/null +++ b/home/migrations/0061_article_notification_tags.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.25 on 2025-07-28 12:22 + +from django.db import migrations +import modelcluster.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('user_notifications', '0003_auto_20250728_1222'), + ('home', '0060_auto_20250703_0253'), + ] + + operations = [ + migrations.AddField( + model_name='article', + name='notification_tags', + field=modelcluster.fields.ParentalManyToManyField(blank=True, to='user_notifications.NotificationTag'), + ), + ] diff --git a/home/migrations/0062_alter_manifestsettings_language.py b/home/migrations/0062_alter_manifestsettings_language.py new file mode 100644 index 000000000..d634d7a82 --- /dev/null +++ b/home/migrations/0062_alter_manifestsettings_language.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2025-08-18 10:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0061_article_notification_tags'), + ] + + operations = [ + migrations.AlterField( + model_name='manifestsettings', + name='language', + field=models.CharField(choices=[('ar', 'Arabic'), ('bn', 'Bengali'), ('ny', 'Chichewa'), ('prs', 'Dari'), ('en', 'English'), ('fa', 'Farsi'), ('fr', 'French'), ('hi', 'Hindi'), ('id', 'Indonesian'), ('kaa', 'Karakalpak'), ('km', 'Khmer'), ('rw', 'Kinyarwanda'), ('rn', 'Kirundi'), ('ku', 'Kurdish'), ('mg', 'Malagasy'), ('my', 'Burmese'), ('ne', 'Nepali'), ('nr', 'Ndebele'), ('ps', 'Pashto'), ('pt', 'Portuguese'), ('qu', 'Quechua'), ('ru', 'Russian'), ('sn', 'Shona'), ('si', 'Sinhala'), ('es', 'Spanish'), ('sw', 'Swahili'), ('tg', 'Tajik'), ('ta', 'Tamil'), ('ti', 'Tigrinya'), ('tr', 'Turkish'), ('uk', 'Ukraine'), ('ur', 'Urdu'), ('uz', 'Uzbek'), ('zu', 'Zulu'), ('xy', 'Testing'), ('ha', 'Hausa'), ('yo', 'Yoruba'), ('ig', 'Igbo'), ('pcm', 'Pidgin')], default='en', help_text='Choose language', max_length=3, verbose_name='Language'), + ), + ] diff --git a/home/migrations/0063_section_notification_tags_and_more.py b/home/migrations/0063_section_notification_tags_and_more.py new file mode 100644 index 000000000..3dc4d44ca --- /dev/null +++ b/home/migrations/0063_section_notification_tags_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 5.2.4 on 2025-09-04 06:06 + +import django.db.models.deletion +import modelcluster.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0062_alter_manifestsettings_language'), + ('taggit', '0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx'), + ('user_notifications', '0005_seed_notification_tables'), + ] + + operations = [ + migrations.AddField( + model_name='section', + name='notification_tags', + field=modelcluster.fields.ParentalManyToManyField(blank=True, to='user_notifications.notificationtag'), + ), + migrations.AlterField( + model_name='articletaggeditem', + name='tag', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_items', to='taggit.tag'), + ), + migrations.AlterField( + model_name='sectiontaggeditem', + name='tag', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_items', to='taggit.tag'), + ), + ] diff --git a/home/models.py b/home/models.py index e29442647..21c24f6ab 100644 --- a/home/models.py +++ b/home/models.py @@ -14,8 +14,8 @@ from django.utils.encoding import force_str from django.utils.translation import gettext_lazy as _ from modelcluster.contrib.taggit import ClusterTaggableManager +from modelcluster.fields import ParentalKey, ParentalManyToManyField from iogt.settings.base import WAGTAIL_CONTENT_LANGUAGES -from modelcluster.fields import ParentalKey from rest_framework import status from taggit.models import TaggedItemBase from wagtail.admin.panels import ( @@ -25,7 +25,7 @@ ObjectList, TabbedInterface, ) -from wagtail.contrib.settings.models import BaseSetting +from wagtail.contrib.settings.models import BaseSiteSetting as BaseSetting from wagtail.contrib.settings.registry import register_setting from wagtail import blocks from wagtail.fields import StreamField @@ -55,7 +55,7 @@ ) import iogt.iogt_globals as globals_ from django.db.models import Avg, Count - +from user_notifications.models import NotificationTag, NotificationPreference User = get_user_model() logger = logging.getLogger(__name__) @@ -125,8 +125,12 @@ def get_context(self, request): ): banners.append(banner_specific) context['banners'] = banners + show_notification_nudge = False + if request.user and request.user.is_authenticated: + pref = NotificationPreference.objects.filter(user=request.user).first() + context["notification_preference"] = pref + context['user'] = request.user return context - @property def offline_urls(self): return [self.url] + collect_urls_from_streamfield(self.home_featured_content) @@ -214,7 +218,7 @@ class Section(Page, PageUtilsMixin, CommentableMixin, TitleIconMixin): blank=True, use_json_field=True, ) - + notification_tags = ParentalManyToManyField(NotificationTag, blank=True) tags = ClusterTaggableManager(through='SectionTaggedItem', blank=True) show_progress_bar = models.BooleanField(default=False) larger_image_for_top_page_in_list_as_in_v1 = models.BooleanField(default=False) @@ -222,7 +226,7 @@ class Section(Page, PageUtilsMixin, CommentableMixin, TitleIconMixin): show_in_menus_default = True promote_panels = Page.promote_panels + [ - MultiFieldPanel([FieldPanel("tags"), ], heading='Metadata'), + MultiFieldPanel([FieldPanel("tags"), FieldPanel("notification_tags"),], heading='Metadata'), ] content_panels = Page.content_panels + [ @@ -482,7 +486,7 @@ class Article(AbstractArticle): # New fields for precomputed values average_rating = models.FloatField(default=0.0, null=True) number_of_reviews = models.PositiveIntegerField(default=0, null=True) - + notification_tags = ParentalManyToManyField(NotificationTag, blank=True) content_panels = AbstractArticle.content_panels + [ MultiFieldPanel([ InlinePanel('recommended_articles', @@ -492,7 +496,7 @@ class Article(AbstractArticle): ] promote_panels = AbstractArticle.promote_panels + [ - MultiFieldPanel([FieldPanel("tags"), ], heading='Metadata'), + MultiFieldPanel([FieldPanel("tags"), FieldPanel("notification_tags"),], heading='Metadata'), ] edit_handler_list = [ @@ -573,7 +577,6 @@ class BannerIndexPage(Page): parent_page_types = ['home.HomePage'] subpage_types = ['home.BannerPage'] - class BannerPage(Page, PageUtilsMixin): parent_page_types = ['home.BannerIndexPage'] subpage_types = [] diff --git a/home/static/css/global/admin.css b/home/static/css/global/admin.css index 75c2f1bda..b3583a70b 100644 --- a/home/static/css/global/admin.css +++ b/home/static/css/global/admin.css @@ -1,24 +1,282 @@ .action-add-block-paragraph_v1_legacy { - text-decoration: line-through; - pointer-events: none; - background-color: gray; + text-decoration: line-through; + pointer-events: none; + background-color: gray; } .red-help-text p.help { - color: red; - font-weight: bold; + color: red; + font-weight: bold; } .disabled-clean-name textarea { - pointer-events: none; - background-color: #e6e6e6; - color: #4d4d4d; + pointer-events: none; + background-color: #e6e6e6; + color: #4d4d4d; } -.object > h2.title-wrapper:before { - height: 40px; +.object>h2.title-wrapper:before { + height: 40px; } -.object > h2.title-wrapper { - position: unset; +.object>h2.title-wrapper { + position: unset; } + +.w-field--collection_choice_field select, +.w-w-field .w-field--commentable select { + width: 100%; + max-width: 100%; + box-sizing: border-box; +} + +.listing-filter select, +.c-dropdown select { + min-height: 40px; + padding: 6px 10px; + border-radius: 6px; + font-size: 14px; +} + +#collection_chooser_collection_id-label { + margin-right: 20px; + white-space: nowrap; +} + +#collection_chooser_collection_id { + width: 100%; + max-width: 100%; +} + +@media (min-width: 768px) { + #collection_chooser_collection_id { + max-width: 600px; + } +} + +@media (min-width: 1200px) { + #collection_chooser_collection_id { + max-width: 800px; + } +} + +.w-field__input input:not(#id_q), +.w-field__input select[id="collection_chooser_collection_id"] { + background-color: #f8f8f8 !important; + border: 1px solid #b1b4b6 !important; + border-radius: 4px !important; + transition: background-color 0.2s ease, border-color 0.2s ease; +} + +.w-field__input select#collection_chooser_collection_id:hover { + background-color: #fff !important; +} + +#wrapper #collection_chooser_collection_id:focus, +.w-field__input select#collection_chooser_collection_id:focus { + background-color: #fff !important; + border-color: #ffbf47 !important; + box-shadow: 0 0 0 3px rgba(255, 191, 71, 0.5) !important; + outline: 0 !important; +} + + +.w-field__input select#id_adddocument_collection { + width: 100% !important; + max-width: none !important; + box-sizing: border-box; +} + +.w-field__input select[id="id_collection"] { + width: 100% !important; + min-width: unset; + max-width: 880px; + background-color: #f8f8f8 !important; + border: 1px solid #b1b4b6 !important; + border-radius: 4px !important; + padding: 14px 15px !important; + font-size: 18px !important; + line-height: 1.4 !important; +} + +@media (min-width: 768px) and (max-width: 1199px) { + .w-field__input select[id="id_collection"] { + width: 100% !important; + max-width: 750px; + } +} + +@media (min-width: 1200px) { + .w-field__input select[id="id_collection"] { + max-width: 880px; + } +} + +.w-field { + display: block; + /* margin-bottom: 1rem; */ +} + +.w-field__label { + display: block; + margin-bottom: 0.5rem; + max-width: 100%; +} + +.w-field__input { + display: block; + width: 100%; +} + +.w-field__input select[id="id_language_code"] { + width: 100%; + box-sizing: border-box; + background-color: #fafafa; + border: 1px solid var(--w-color-border-field, #ccc); + border-radius: 4px; + padding: 0.5rem; +} + +@media screen and (min-width: 50em) { + .w-field--choice_field { + box-sizing: border-box; + display: flex; + align-items: center; + gap: 1rem; + } + + .w-field__input { + max-width: 100.3333%; + } + + + .w-field__input select[id="id_language_code"] { + width: 100% !important; + background-color: #fafafa; + border: 1px solid var(--w-color-border-field, #ccc); + border-radius: 4px; + padding: 0.5rem; + } +} + +header .left:first-child { + float: left; + padding-bottom: 0; +} + +header .right { + float: right; + text-align: end; +} + +header h1 { + color: #2e1f5e; + position: relative; +} + +header svg { + max-height: 1em; + max-width: 1em; +} + + +.row-flex { + display: flex; + align-items: center; +} + +.row-flex .left { + flex: auto; + align-items: center; + display: flex; +} + +.fields-inline { + display: flex; + gap: 0.75rem; + align-items: center; +} + +.fields-inline>li { + margin: 0; + flex: 0 0 auto; +} + +.changelist-filter ul li a { + display: block; + padding: 8px 12px; + margin: 6px 0; + border: 1px solid rgb(0, 125, 126); + border-radius: 3px; + background-color: #fff; + text-decoration: none; + color: rgb(0, 125, 126); + font-size: .9em; + font-weight: 400; + line-height: 1.2em; + box-sizing: border-box; + font-size:12.24px +} + +.changelist-filter ul li a:hover { + background-color: #e0f2f1; +} + + + +.changelist-filter ul li.selected a { + background-color:rgb(0, 125, 126) !important; + border-color: rgb(0, 125, 126) !important; + color: #fff !important; +} + +.dropdown-container { + margin-left: 10px; + display: flex; + align-items: center; + background-color: rgb(0, 125, 126); + font-size: .875rem; + height: 3em; + line-height: calc(3em - 2px); + padding: 0 1.4em; + -webkit-font-smoothing: auto; + border: 1px solid rgb(0, 125, 126); + border-radius: .1875rem; + display: inline-block; + font-weight: 400; + outline-offset: 3px; + padding: 0 1em; + position: relative; + text-decoration: none; + transition: background-color .1s ease; + vertical-align: middle; + white-space: nowrap; + width: auto; +} + +.dropdown-container:hover { + background-color: rgb(0, 91, 94); + border-color: #0000; +} + +.dropdown-container button { + height: 100%; +} + +.header-title { + margin-right: 1.5rem; +} + +.dropdown-container .w-dropdown__toggle { + color: white; +} + +.c-wagtailautocomplete__suggestions__item--active, .c-wagtailautocomplete__selection { + background-color: rgb(0, 125, 126) !important; + color: white !important; +} + +svg.c-wagtailautocomplete__search-icon { + height: 30px !important; + width: 30px !important; +} \ No newline at end of file diff --git a/home/static/css/global/global.css b/home/static/css/global/global.css index 4df37737f..42dfb5b1e 100644 --- a/home/static/css/global/global.css +++ b/home/static/css/global/global.css @@ -1,4 +1,3 @@ - /* home start */ .home-page__featured-content { padding: 0 22px; @@ -23,6 +22,7 @@ .banner-holder { margin-bottom: 20px; } + .banner-holder img { margin-bottom: 6px; width: 100vw; @@ -30,6 +30,7 @@ border-radius: 12px; } } + /* home end */ @@ -52,11 +53,11 @@ display: -ms-flexbox; display: flex; -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + -ms-flex-align: center; + align-items: center; } .featured-content a { @@ -71,7 +72,8 @@ text-align: left; } -.featured-content svg, .section-featured-content svg { +.featured-content svg, +.section-featured-content svg { stroke: currentColor; } @@ -82,7 +84,9 @@ margin-bottom: 19px; padding: 13px 12px; } - .featured-content a, .section-featured-content-title { + + .featured-content a, + .section-featured-content-title { font-size: 16px; line-height: 22px; } @@ -122,14 +126,14 @@ display: flex; -webkit-box-orient: horizontal; -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + -ms-flex-align: center; + align-items: center; -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; padding: 8px 0px; border-bottom: 1px solid #EFEFEF; font-style: normal; @@ -178,11 +182,11 @@ } } -.rtl .quiz__imageholder { +.rtl .quiz__imageholder { transform: rotate(180deg); } -.rtl .featured-content__imageholder { +.rtl .featured-content__imageholder { transform: rotate(180deg); } @@ -231,7 +235,8 @@ margin-bottom: 10px; } -.article__content--video a, .article__content--audio a { +.article__content--video a, +.article__content--audio a { color: #303030; } @@ -243,14 +248,14 @@ display: flex; -webkit-box-orient: horizontal; -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; + -ms-flex-pack: center; + justify-content: center; -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + -ms-flex-align: center; + align-items: center; padding: 8px 16px; border: 1px solid #303030; border-radius: 8px; @@ -298,8 +303,8 @@ display: -ms-flexbox; display: flex; -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; padding-top: 12px; } @@ -309,14 +314,14 @@ display: flex; -webkit-box-orient: horizontal; -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; + -ms-flex-pack: center; + justify-content: center; -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + -ms-flex-align: center; + align-items: center; padding: 8px 12px; text-decoration: none; width: 105px; @@ -346,18 +351,21 @@ .article__navigation { padding-top: 24px; } + .article__navigation a { padding: 12px 16px; width: 132px; height: 46px; border-radius: 24px; } + .article__navigation--previous { height: 22px; font-size: 16px; line-height: 22px; border: 1.5px solid #d8d8d8; } + .article__navigation--next { height: 22px; font-size: 16px; @@ -377,11 +385,11 @@ display: flex; -webkit-box-orient: horizontal; -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + -ms-flex-align: center; + align-items: center; padding: 12px; background: #F7F7F9; font-size: 10px; @@ -408,11 +416,11 @@ display: -ms-flexbox; display: flex; -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; + -ms-flex-pack: center; + justify-content: center; -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + -ms-flex-align: center; + align-items: center; margin-left: 10px; } @@ -455,25 +463,25 @@ display: flex; -webkit-box-orient: horizontal; -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; + -ms-flex-pack: center; + justify-content: center; -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + -ms-flex-align: center; + align-items: center; padding: 0; background-color: #303030; border-radius: 24px; -webkit-box-flex: 0; - -ms-flex-positive: 0; - flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; margin: 16px 10px; width: 100%; } -.comments__submit + .comments__submit { +.comments__submit+.comments__submit { margin-left: 10px; } @@ -493,15 +501,18 @@ .comments { padding-top: 40px; } + .comments h2 { padding: 20px; font-size: 16px; line-height: 22px; } + .comments h3 { font-size: 24px; line-height: 28px; } + .comments__count { width: 30px; height: 30px; @@ -509,18 +520,22 @@ line-height: 16px; margin-left: 10px; } + .comments__form { padding: 0px 20px; } + .comments__login { padding: 20px; font-size: 16px; line-height: 24px; } + .comments__submit { padding: 0; margin: 16px 0px; } + .comments__submit input, .comments__submit a { font-size: 16px; @@ -550,8 +565,8 @@ display: -ms-flexbox; display: flex; -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + -ms-flex-align: center; + align-items: center; margin: 0; } @@ -566,11 +581,11 @@ display: -ms-flexbox; display: flex; -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; + -ms-flex-pack: center; + justify-content: center; -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + -ms-flex-align: center; + align-items: center; } .individual-comment span p:nth-of-type(2n) { @@ -586,11 +601,11 @@ display: -ms-flexbox; display: flex; -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + -ms-flex-align: center; + align-items: center; } .individual-comment__date { @@ -601,7 +616,8 @@ opacity: 0.4; } -.individual-comment .content, .individual-comment__moderator { +.individual-comment .content, +.individual-comment__moderator { font-weight: 500; font-size: 10px; line-height: 16px; @@ -634,34 +650,42 @@ .individual-comment { padding: 20px; } + .individual-comment span p { font-size: 16px; line-height: 24px; margin: 0; } + .individual-comment span p:first-of-type { width: 32px; margin-right: 8px; height: 32px; border-radius: 50%; } + .individual-comment span p:nth-of-type(2n) { font-size: 16px; line-height: 24px; letter-spacing: 0.01em; } + .individual-comment__date { font-size: 12px; line-height: 12px; } - .individual-comment .content, .individual-comment__moderator { + + .individual-comment .content, + .individual-comment__moderator { font-size: 16px; line-height: 24px; } + .individual-comment .report-comment { font-size: 14px; line-height: 20px; } + .individual-comment .report-comment__disclaimer { font-size: 14px; line-height: 20px; @@ -676,7 +700,7 @@ width: 100%; border: 1px solid #EFEFEF; -webkit-box-sizing: border-box; - box-sizing: border-box; + box-sizing: border-box; border-radius: 8px; padding: 7px 12px; font-style: normal; @@ -697,14 +721,17 @@ line-height: 24px; } } + /*# sourceMappingURL=article.css.map */ -.article-card .img-holder, .section-card .img-holder { +.article-card .img-holder, +.section-card .img-holder { overflow: hidden; position: relative; } -.article-card .img-holder.complete:after, .section-card .img-holder.complete:after { +.article-card .img-holder.complete:after, +.section-card .img-holder.complete:after { position: absolute; left: 0; right: 0; @@ -715,7 +742,8 @@ background: linear-gradient(0deg, rgba(26, 144, 144, 0.7), rgba(26, 144, 144, 0.7)); } -.article-card .img-holder.complete:before, .section-card .img-holder.complete:before { +.article-card .img-holder.complete:before, +.section-card .img-holder.complete:before { background-image: url("/static/icons/tick-square.svg"); content: ""; height: 28px; @@ -728,6 +756,7 @@ transform: translate(-50%, -50%); z-index: 2; } + /* article end */ @@ -752,7 +781,8 @@ body.rtl { padding: 0; } -.article-card p.article-title, .section-card p.section-title { +.article-card p.article-title, +.section-card p.section-title { width: 40%; font-weight: bold; margin-right: 5px; @@ -761,12 +791,14 @@ body.rtl { letter-spacing: 0.01em; } -.article-card .article-header, .section-card .section-header { +.article-card .article-header, +.section-card .section-header { padding-left: 10px; padding-right: 10px; } -.article-card .article-header p, .section-card .section-header p { +.article-card .article-header p, +.section-card .section-header p { width: 100%; } @@ -775,11 +807,11 @@ body.rtl { display: -ms-flexbox; display: flex; -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + -ms-flex-align: center; + align-items: center; padding: 6px 0px; border-bottom: 1px solid #EFEFEF; text-decoration: none; @@ -804,13 +836,15 @@ body.rtl { height: auto; } -.article-card .img-holder, .section-card .img-holder { +.article-card .img-holder, +.section-card .img-holder { border-radius: 8px; flex: 0 0 50%; height: auto; } -.article-card .img-holder img, .section-card .img-holder img { +.article-card .img-holder img, +.section-card .img-holder img { border-radius: 8px; width: 100%; height: auto; @@ -830,22 +864,27 @@ body.rtl { .article-card { /* margin: 0px 20px; */ } + .article-card h2 { height: 28px; font-weight: 800; font-size: 24px; line-height: 28px; } - .article-card p.article-title, .section-card p.section-title { + + .article-card p.article-title, + .section-card p.section-title { width: 50%; font-size: 16px; margin-right: 5px; line-height: 24px; } - .article-card .article-header p, .section-card .section-header p { + .article-card .article-header p, + .section-card .section-header p { width: 100%; } + .article-card a { padding: 16px 0px; font-size: 16px; @@ -853,9 +892,11 @@ body.rtl { letter-spacing: 0.01em; color: #303030; } + .article-card__img--xs { display: none; } + .article-card__img--sm { display: block; } @@ -869,14 +910,14 @@ body.rtl { display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; + -ms-flex-pack: center; + justify-content: center; -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + -ms-flex-align: center; + align-items: center; } .questionnaire-components a { @@ -885,11 +926,11 @@ body.rtl { display: -ms-flexbox; display: flex; -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + -ms-flex-align: center; + align-items: center; -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; color: inherit; } @@ -933,18 +974,22 @@ body.rtl { .questionnaire-components { margin: 0px 20px; } + .questionnaire-components p:first-of-type { font-size: 12px; line-height: 16px; } + .questionnaire-components p { padding-right: 20px; margin: 0; } + .questionnaire-components p:nth-of-type(2n) { font-size: 16px; line-height: 22px; } + .questionnaire-components__component { margin: 16px; padding: 8px 12px; @@ -968,37 +1013,40 @@ body.rtl { display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; - -ms-flex-direction: column; - flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; padding: 12px; padding-top: 8px; left: 0px; top: 0px; background-color: #E0F2FD; } + .cache-banner p { font-style: normal; font-weight: normal; font-size: 16px; line-height: 24px; } + .cache-banner p:nth-of-type(2n) { font-weight: 700; } + .cache-banner__download { display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-orient: horizontal; -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; + -ms-flex-pack: center; + justify-content: center; -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + -ms-flex-align: center; + align-items: center; padding: 5px 12px; border: none; height: 26px; @@ -1010,14 +1058,16 @@ body.rtl { line-height: 16px; color: #FFFFFF; } + .cache-banner__close-holder { display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-pack: end; - -ms-flex-pack: end; - justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; } + .cache-banner__close-holder__button { padding: 0px 4px; background: #0094F4; @@ -1030,17 +1080,21 @@ body.rtl { border: none; } } + /*# sourceMappingURL=global.css.map */ .other-links, -.content .footer__copyright {display: none;} +.content .footer__copyright { + display: none; +} @media screen and (max-width: 767px) and (min-width: 414px) { .footer { - padding-left: 32px; - padding-right: 32px; + padding-left: 32px; + padding-right: 32px; } + .section-container { max-width: 600px; margin: 0 auto; @@ -1093,7 +1147,8 @@ body.rtl { .footer .footer__copyright { display: none; } - .nav-bar{ + + .nav-bar { position: static; width: 100%; } @@ -1104,7 +1159,7 @@ body.rtl { width: 100%; } - .nav-bar__item a{ + .nav-bar__item a { display: flex; margin-bottom: 8px; padding: 9px 12px; @@ -1152,7 +1207,9 @@ body.rtl { margin-top: 0; } - .content .footer__copyright {display: block;} + .content .footer__copyright { + display: block; + } .footer__copyright { margin: 0; @@ -1161,7 +1218,10 @@ body.rtl { } @media screen and (min-width: 1400px) { - .footer .bottom-level {display: none;} + .footer .bottom-level { + display: none; + } + .footer-main, .other-links { display: block; @@ -1169,6 +1229,7 @@ body.rtl { width: 29%; padding: 0 20px; } + .content { max-width: 600px; width: 40%; @@ -1245,20 +1306,101 @@ body.rtl { } .language__select { - display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */ - display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */ - display: -ms-flexbox; /* TWEENER - IE 10 */ - display: -webkit-flex; /* NEW - Chrome */ - display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */ + display: -webkit-box; + /* OLD - iOS 6-, Safari 3.1-6 */ + display: -moz-box; + /* OLD - Firefox 19- (buggy but mostly works) */ + display: -ms-flexbox; + /* TWEENER - IE 10 */ + display: -webkit-flex; + /* NEW - Chrome */ + display: flex; + /* NEW, Spec - Opera 12.1, Firefox 20+ */ -webkit-box-pack: end; - -ms-flex-pack: end; - justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +.dropdown-menu { + position: absolute; + right: 10px; + top: 40px; + background: white; + border: 1px solid #ccc; + z-index: 1000; + box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.15); + padding: 10px; + border-radius: 6px; +} + +.notification-holder { + display: flex; + align-items: center; + margin-left: 5px; +} + +.notification-bell-wrapper { + position: relative; + display: inline-block; +} + +.notification-bell { + font-size: 1.5rem; + transition: color 0.3s ease; + display: flex; +} + +#notif-bell.highlighted { + fill: #007bff !important; /* blue glow */ +} + +.notif-badge { + position: absolute; + top: -8px; + right: -8px; + background-color: red; + color: white; + border-radius: 50%; + padding: 2px 6px; + font-size: 0.7rem; + font-weight: bold; + display: inline-block; + line-height: 1; + min-width: 16px; + text-align: center; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.3); +} + +.notification_language select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 100%; +} + +.notification_language { + position: relative; + width: 100%; + max-width: 100%; +} + +.notification_language::after { + content: ""; + position: absolute; + top: 50%; + right: 12px; + transform: translateY(-50%); + pointer-events: none; + + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 6px solid #555; } .language__select select { -webkit-appearance: none; - -moz-appearance: none; - appearance: none; + -moz-appearance: none; + appearance: none; margin-right: 10px; background: #FDD256; border: none; @@ -1273,14 +1415,14 @@ body.rtl { display: flex; -webkit-box-orient: horizontal; -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; + -ms-flex-pack: center; + justify-content: center; -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + -ms-flex-align: center; + align-items: center; padding: 5px 12px; background-image: url("/static/icons/down.svg"); background-repeat: no-repeat; @@ -1302,17 +1444,17 @@ body.rtl { display: flex; -webkit-box-orient: horizontal; -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; + -ms-flex-pack: center; + justify-content: center; -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; + -ms-flex-align: center; + align-items: center; } -.language_drop > a { +.language_drop>a { background: #FDD256; display: flex; flex-direction: column; @@ -1332,7 +1474,7 @@ body.rtl { z-index: 999; } -.language_drop > a > span { +.language_drop>a>span { position: absolute; border-left: 4px solid transparent; border-right: 4px solid transparent; @@ -1361,9 +1503,9 @@ body.rtl { z-index: 99; } -.language_drop:hover > a > span, -.language_drop:active > a > span, -.language_drop:focus > a > span { +.language_drop:hover>a>span, +.language_drop:active>a>span, +.language_drop:focus>a>span { border-top: 4px solid transparent; border-bottom: 4px solid #303030; margin: -2px 0 0; @@ -1389,14 +1531,19 @@ body.rtl { position: relative; } -.language_drop .drop li{ +.language_drop .drop li { width: 100%; margin: 0 0 8px; - display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */ - display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */ - display: -ms-flexbox; /* TWEENER - IE 10 */ - display: -webkit-flex; /* NEW - Chrome */ - display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */ + display: -webkit-box; + /* OLD - iOS 6-, Safari 3.1-6 */ + display: -moz-box; + /* OLD - Firefox 19- (buggy but mostly works) */ + display: -ms-flexbox; + /* TWEENER - IE 10 */ + display: -webkit-flex; + /* NEW - Chrome */ + display: flex; + /* NEW, Spec - Opera 12.1, Firefox 20+ */ } .language_drop .drop .selected a { @@ -1420,11 +1567,16 @@ body.rtl { box-sizing: border-box; border-radius: 0 0 8px 8px; align-items: center; - display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */ - display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */ - display: -ms-flexbox; /* TWEENER - IE 10 */ - display: -webkit-flex; /* NEW - Chrome */ - display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */ + display: -webkit-box; + /* OLD - iOS 6-, Safari 3.1-6 */ + display: -moz-box; + /* OLD - Firefox 19- (buggy but mostly works) */ + display: -ms-flexbox; + /* TWEENER - IE 10 */ + display: -webkit-flex; + /* NEW - Chrome */ + display: flex; + /* NEW, Spec - Opera 12.1, Firefox 20+ */ } #header:after { @@ -1437,31 +1589,46 @@ body.rtl { align-items: center; padding: 12px 32px; flex: 1; - display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */ - display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */ - display: -ms-flexbox; /* TWEENER - IE 10 */ - display: -webkit-flex; /* NEW - Chrome */ - display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */ + display: -webkit-box; + /* OLD - iOS 6-, Safari 3.1-6 */ + display: -moz-box; + /* OLD - Firefox 19- (buggy but mostly works) */ + display: -ms-flexbox; + /* TWEENER - IE 10 */ + display: -webkit-flex; + /* NEW - Chrome */ + display: flex; + /* NEW, Spec - Opera 12.1, Firefox 20+ */ max-width: 664px; margin: 0 auto; } .header-content { - display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */ - display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */ - display: -ms-flexbox; /* TWEENER - IE 10 */ - display: -webkit-flex; /* NEW - Chrome */ - display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */ + display: -webkit-box; + /* OLD - iOS 6-, Safari 3.1-6 */ + display: -moz-box; + /* OLD - Firefox 19- (buggy but mostly works) */ + display: -ms-flexbox; + /* TWEENER - IE 10 */ + display: -webkit-flex; + /* NEW - Chrome */ + display: flex; + /* NEW, Spec - Opera 12.1, Firefox 20+ */ flex: 1; justify-content: space-between; } .logo-holder { - display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */ - display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */ - display: -ms-flexbox; /* TWEENER - IE 10 */ - display: -webkit-flex; /* NEW - Chrome */ - display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */ + display: -webkit-box; + /* OLD - iOS 6-, Safari 3.1-6 */ + display: -moz-box; + /* OLD - Firefox 19- (buggy but mostly works) */ + display: -ms-flexbox; + /* TWEENER - IE 10 */ + display: -webkit-flex; + /* NEW - Chrome */ + display: flex; + /* NEW, Spec - Opera 12.1, Firefox 20+ */ flex: 1; } @@ -1489,7 +1656,7 @@ body.rtl { margin-right: 12px; } -.form-holder .js-search-btn img{ +.form-holder .js-search-btn img { margin: 0 0 0 12px; border-left: 1px solid #EFEFEF; padding: 5px 0; @@ -1502,6 +1669,7 @@ body.rtl { border-radius: 0; border-width: 0 0 1px 0; } + .footer-head #header, .footer-head .header-holder { /* padding-left: 0; @@ -1531,15 +1699,18 @@ body.rtl { .header-holder { padding: 12px 20px; } + .logo-holder a { max-width: 68px; } + .language__select select { max-width: 74px; font-size: 10px; background-position: 60px; } - .language_drop > a { + + .language_drop>a { min-width: 80px; /* max-width: 80px; */ } @@ -1570,15 +1741,18 @@ body.rtl { padding-top: 20px; } } + @media screen and (max-width: 767px) { .content { padding: 16px 0; } } -.rtl .language_drop > a { + +.rtl .language_drop>a { padding: 5px 12px 5px 20px; } -.rtl .language_drop > a > span { + +.rtl .language_drop>a>span { right: auto; left: 11px; } @@ -1587,22 +1761,30 @@ body.rtl { .footer-head { display: none; } + #header { border-radius: 0; } + .header-holder { padding: 12px 0; max-width: 1000px; } + .logo-holder { flex: initial; } + #header .search__form { display: block; width: 100%; max-width: 600px; } - .form-holder .js-search-btn {display: none;} + + .form-holder .js-search-btn { + display: none; + } + .language__select { margin-left: 12px; } @@ -1616,18 +1798,25 @@ body.rtl { justify-content: flex-start; padding: 0 16px; } + .form-holder { - display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */ - display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */ - display: -ms-flexbox; /* TWEENER - IE 10 */ - display: -webkit-flex; /* NEW - Chrome */ - display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */ + display: -webkit-box; + /* OLD - iOS 6-, Safari 3.1-6 */ + display: -moz-box; + /* OLD - Firefox 19- (buggy but mostly works) */ + display: -ms-flexbox; + /* TWEENER - IE 10 */ + display: -webkit-flex; + /* NEW - Chrome */ + display: flex; + /* NEW, Spec - Opera 12.1, Firefox 20+ */ max-width: 600px; width: 60%; flex: 1; padding: 0 16px; flex-basis: 24.5%; } + .main-wrapper { width: 100%; display: flex; @@ -1635,6 +1824,7 @@ body.rtl { box-sizing: border-box; } } + @media screen and (min-width: 1400px) { .header-holder { max-width: 1400px; @@ -1642,12 +1832,14 @@ body.rtl { padding-right: 0; justify-content: space-between; } + #header .btn-holder { display: block; width: 29%; max-width: 400px; padding: 0 20px; } + .form-holder { width: 42%; flex-basis: 42%; @@ -1657,6 +1849,7 @@ body.rtl { #header .btn-holder .nav-bar__item__icon { /* display: flex; */ } + #header .btn-holder .nav-bar__item a { margin: 0; } @@ -1666,6 +1859,7 @@ body.rtl { width: 29%; max-width: 400px; } + .link-login { display: none; } @@ -1673,30 +1867,30 @@ body.rtl { .content .article__content__link-btn { - margin-top: 20px; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; - -webkit-box-pack: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - padding: 8px 16px; - border: 1px solid #303030; - border-radius: 8px; - width: 100%; - font-style: normal; - font-weight: 600; - font-size: 10px; - line-height: 16px; - color: #303030; - text-decoration: none; + margin-top: 20px; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding: 8px 16px; + border: 1px solid #303030; + border-radius: 8px; + width: 100%; + font-style: normal; + font-weight: 600; + font-size: 10px; + line-height: 16px; + color: #303030; + text-decoration: none; } #app .content-holder .content h1.large { @@ -1713,7 +1907,8 @@ body.rtl { margin: 16px 0; } -#app .content-holder .content h2, .block-heading { +#app .content-holder .content h2, +.block-heading { font-weight: bold; font-size: 16px; line-height: 18px; @@ -1773,11 +1968,11 @@ body.rtl { } } -@media only screen and (min-width: 360px){ +@media only screen and (min-width: 360px) { .content .article__content__link-btn { - font-size: 16px; - line-height: 22px; - padding: 12px 16px; + font-size: 16px; + line-height: 22px; + padding: 12px 16px; } .block-heading { @@ -1802,7 +1997,8 @@ body.rtl { margin: 22px 0; } - #app .content-holder .content h2, .block-heading { + #app .content-holder .content h2, + .block-heading { font-weight: bold; font-size: 22px; line-height: 24px; @@ -1914,9 +2110,9 @@ body.rtl { } #content-wrap .first-content { - flex-direction: column-reverse; - align-items: flex-start; - width: 100%; + flex-direction: column-reverse; + align-items: flex-start; + width: 100%; } #content-wrap .first-content .overlay-holder { @@ -1930,8 +2126,10 @@ body.rtl { width: 100% !important; } -#content-wrap .first-content .img-holder, #content-wrap .first-content .article-header, #content-wrap .first-content .section-header { - width: 100%; +#content-wrap .first-content .img-holder, +#content-wrap .first-content .article-header, +#content-wrap .first-content .section-header { + width: 100%; } /* questionnaires start */ @@ -1943,4 +2141,240 @@ body.rtl { width: 100%; height: auto; } -/* questionnaires end */ \ No newline at end of file + +/* questionnaires end */ + + + +/* Notification Section */ +.notification-overlay { + position: fixed; + display: none; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.4); + z-index: 999; +} + +/* Notification panel */ +.notification-panel { + position: fixed; + top: 0; + right: 0; + width: 460px; + height: 100%; + background: #fff; + box-shadow: -4px 0 12px rgba(0,0,0,0.2); + padding: 24px; + font-family: Arial, sans-serif; + display: none; + flex-direction: column; + z-index: 1000; + overflow-y: auto; +} + +/* Header */ +.notification-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #e0e0e0; + font-size: 16px; + font-weight: 600; + margin-bottom: 10px; +} + +/* Close Button */ +.notification-close-btn { + background: transparent; + border: none; + font-size: 20px; + cursor: pointer; + width: 20%; +} + +.notification-list { + margin: 0; + padding: 0; +} + +.notification-item { + display: block; + width: 100%; + padding: 1rem; + text-decoration: none; + color: inherit; + border-bottom: 1px solid #e6e6e6; + border-radius: 8px; + transition: background 0.2s ease; + box-sizing: border-box; + background-color: #f0f8ff; + margin-bottom: 5px; + box-shadow: 0 1px 4px rgba(0,0,0,0.05); +} + +.notification-item:hover { + background-color: #f1f5fa; +} + +.notification-item.read { + background-color: #fff; + font-weight: normal; +} + +.notification-body { + margin: 0; + padding: 0; + display: flex; + align-items: center; + justify-content: space-between; +} + +.notification-text { + margin: 0; + font-size: 0.8rem; +} + +.notification-title { + font-weight: 600; + font-size: 0.85rem; +} + +.notification-empty { + padding: 24px; + text-align: center; + color: #999; + font-style: italic; +} + +.notification-footer { + text-align: center; + padding: 12px 0; + background: #fafafa; + border-top: 1px solid #e0e0e0; +} + +.notification-footer a { + color: #3367d6; + font-weight: 500; + text-decoration: none; +} + +.notification-footer a:hover { + text-decoration: underline; +} + +.bold { + font-weight: bold; +} + +.unread-dot { + display: inline-block; + width: 8px; + height: 8px; + background-color: #007bff; /* Bootstrap primary blue */ + border-radius: 50%; + margin-left: 8px; +} + +@media (max-width: 768px) { + #notification-overlay { + width: 100%; + } +} + +.highlight-yes { +background-color: #4caf50 !important; +color: white !important; +} +.highlight-no { +background-color: #f44336 !important; +color: white !important; +} +#yes_button, #no_button { + background-color: #e0e0e0; + color: black; + border: none; + cursor: pointer; + width: 92px; + height: 40 px; + height: 40px; + margin-left: 10px; +} +div.notification-confirmation{ + margin-bottom: 10px; + display: flex; + align-items: center; +} + + +.notification-callout-text { + background-color: #eaf7ff; + color: #0c5460; + font-size: 13px; + padding: 4px 8px; + margin-top: 5px; + border-radius: 4px; +} + +.enable-notifications-nudge { + position: fixed; + bottom: 20px; + left: 20px; + background-color: #fff8e1; + color: black; + border-left: 5px solid goldenrod; + box-shadow: 0 4px 8px rgba(0,0,0,0.15); + padding: 15px 10px; + border-radius: 8px; + display: flex; + align-items: center; + gap: 10px; + z-index: 9999; + font-family: "Segoe UI", sans-serif; + animation: slideIn 0.5s ease-out; + margin-right: 15px; + font-size: large; +} + +.notification-preference-on-registration-survey { + background-color: #fff8e1; + color: black; + border-left: 5px solid goldenrod; + box-shadow: 0 4px 8px rgba(0,0,0,0.15); + padding: 15px 10px; + border-radius: 8px; + display: flex; + align-items: center; + font-family: "Segoe UI", sans-serif; + animation: slideIn 0.5s ease-out; + margin-top: 15px; + font-size: large; +} + +.notification-toast a { + color: #007bff; + text-decoration: underline; + font-weight: 500; + cursor: pointer; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0%); + opacity: 1; + } +} + +.notification-name { + font-size: 0.75rem; + color: #555; + margin: 0; + margin-bottom: 5px; +} \ No newline at end of file diff --git a/home/templates/blocks/image.html b/home/templates/blocks/image.html index a0de1a5ad..6180cef8b 100644 --- a/home/templates/blocks/image.html +++ b/home/templates/blocks/image.html @@ -1,3 +1,2 @@ {% load home_tags %} - -{% render_image value %} +{% render_image value %} \ No newline at end of file diff --git a/home/templates/home/home_page.html b/home/templates/home/home_page.html index 99c3ffaba..1556fdfaa 100644 --- a/home/templates/home/home_page.html +++ b/home/templates/home/home_page.html @@ -9,6 +9,11 @@ {% block content %}
+ {% if user.is_authenticated and not notification_preference.receive_notifications %} +
+ 🔔 Don’t miss out! Turn on notifications to stay updated with the latest from IOGT." +
+ {% endif %} {% include 'home/tags/banners_list.html' with banners=banners %}
{% if page.home_featured_content %} diff --git a/home/templates/wagtailimages/images/edit.html b/home/templates/wagtailimages/edit.html similarity index 73% rename from home/templates/wagtailimages/images/edit.html rename to home/templates/wagtailimages/edit.html index 93881a2e9..cc9f69c2c 100644 --- a/home/templates/wagtailimages/images/edit.html +++ b/home/templates/wagtailimages/edit.html @@ -1,16 +1,15 @@ {% extends "wagtailadmin/base.html" %} {% load wagtailimages_tags wagtailadmin_tags i18n l10n %} +{% load home_tags %} {% block titletag %}{% blocktrans trimmed with title=image.title %}Editing image {{ title }}{% endblocktrans %}{% endblock %} {% block extra_css %} {{ block.super }} {{ form.media.css }} - - - {% endblock %} + {% block extra_js %} {{ block.super }} @@ -24,11 +23,6 @@ }); }); - - - - - {% endblock %} {% block content %} @@ -44,11 +38,22 @@
    {% for field in form %} {% if field.name == 'file' %} - {% include "wagtailimages/images/_file_field_as_li.html" with li_classes="label-above" %} + {% if form.instance.pk and form.instance.file %} + + {{ form.instance.file.name }} + + {% endif %} + {% include "wagtailadmin/shared/field.html" with field=field %} {% elif field.is_hidden %} {{ field }} {% else %} - {% include "wagtailadmin/shared/field_as_li.html" with li_classes="label-above" %} + {% if field.name == "collection" %} +
    + {% include "wagtailadmin/shared/field.html" with field=field %} +
    + {% else %} + {% include "wagtailadmin/shared/field.html" with field=field %} + {% endif %} {% if field.name == 'title' %}

    Remember title also act as alt text, so include a brief description for screen readers. @@ -59,9 +64,9 @@

- {% if user_can_delete %} + {% if user_can_delete %} {% trans "Delete image" %} - {% endif %} + {% endif %}
@@ -99,27 +104,20 @@

{% trans "Focal point" %} {{ original_image.width }}x{{ original_image.height }}
{% trans "Filesize" %}
{% if filesize %}{{ filesize|filesizeformat }}{% else %}{% trans "File not found" %}{% endif %}
- - {% usage_count_enabled as uc_enabled %} - {% if uc_enabled %} -
{% trans "Usage" %}
-
- {% blocktrans trimmed count usage_count=image.get_usage.count %}Used {{ usage_count }} time{% plural %}Used {{ usage_count }} times{% endblocktrans %} -
- {% endif %} +
{% trans "Usage" %}
+
+ {% blocktrans trimmed count usage_count=image.get_usage.count %}Used {{ usage_count }} time{% plural %}Used {{ usage_count }} times{% endblocktrans %} +

-
+ {% endblock %} \ No newline at end of file diff --git a/home/templatetags/clean_html.py b/home/templatetags/clean_html.py new file mode 100644 index 000000000..040347ea8 --- /dev/null +++ b/home/templatetags/clean_html.py @@ -0,0 +1,9 @@ +from django import template +from bs4 import BeautifulSoup + +register = template.Library() + +@register.filter +def strip_html(value): + soup = BeautifulSoup(value, "html.parser") + return soup.get_text(separator=" ", strip=True) diff --git a/home/templatetags/home_tags.py b/home/templatetags/home_tags.py index dc729fa75..46f4e3565 100644 --- a/home/templatetags/home_tags.py +++ b/home/templatetags/home_tags.py @@ -170,4 +170,4 @@ def social_meta_tags(context): if page: context['site_name'] = page.get_site().site_name - return context + return context \ No newline at end of file diff --git a/home/wagtail_hooks.py b/home/wagtail_hooks.py index c508f2610..fef1c169f 100644 --- a/home/wagtail_hooks.py +++ b/home/wagtail_hooks.py @@ -5,14 +5,14 @@ from django.core.exceptions import PermissionDenied from django.db.models import Q from django.templatetags.static import static -from django.urls import resolve +from django.urls import resolve, reverse from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from wagtail import __version__ from wagtail.admin import widgets as wagtailadmin_widgets -from wagtail.admin.menu import MenuItem, SubmenuMenuItem -from wagtail.contrib.modeladmin.menus import SubMenu -from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register +from wagtail.admin.menu import MenuItem, SubmenuMenuItem, Menu +from wagtail_modeladmin.options import ModelAdmin, modeladmin_register +from wagtail.documents.models import Document from wagtail import hooks from wagtail.models import Page, PageViewRestriction from wagtailcache.cache import clear_cache @@ -21,8 +21,11 @@ Section, SectionIndexPage, BannerPage, HomePageBanner, HomePage) from home.translatable_strings import translatable_strings from translation_manager.models import TranslationEntry -from wagtail.core.signals import page_published +from wagtail.signals import page_published from django.dispatch import receiver +from iogt.utils import NotifyAndPublishMenuItem, notify_and_publish_view +from .models import Article +from django.urls import path @hooks.register('after_publish_page') @@ -127,8 +130,7 @@ def global_admin_js(): @hooks.register('register_page_listing_buttons') -def page_listing_buttons(page, page_perms, is_parent=False, next_url=None): - # Using more menu's "Sort menu order" button from wagtail +def page_listing_buttons(page, is_parent=False, next_url=None, user=None, **kwargs): if is_parent: yield wagtailadmin_widgets.PageListingButton( _('Sort child pages'), @@ -150,10 +152,9 @@ def about(): url=f"http://github.com/wagtail/wagtail/releases/tag/v{__version__}" ) ] - return SubmenuMenuItem( label="About", - menu=SubMenu(items), + menu=Menu(register_hook_name=None, items=items), icon_name="info-circle", order=999999, ) @@ -223,6 +224,18 @@ class LocaleDetailAdmin(ModelAdmin): modeladmin_register(LocaleDetailAdmin) + +class DocumentAdmin(ModelAdmin): + model = Document + menu_label = "Documents" + menu_icon = "doc-full" + list_display = ("title", "collection", "created_at") + search_fields = ("title",) + list_filter = ("collection",) # ensures collection dropdown in listing page + +modeladmin_register(DocumentAdmin) + + @hooks.register("insert_global_admin_css") def hide_add_article_button(): """ @@ -238,3 +251,41 @@ def create_home_page_banner(sender, instance, **kwargs): parent = parent.get_parent().specific if parent: HomePageBanner.objects.get_or_create(source=parent, banner_page=instance) + + +@hooks.register('register_admin_urls') +def register_custom_form_pages_list_view(): + return [ + path("notify-and-publish//", notify_and_publish_view, name="notify_and_publish"), + ] + + +@hooks.register('register_page_action_menu_item') +def register_notify_and_publish_menu_item(): + return NotifyAndPublishMenuItem(order=100, allowed_models=Article) # + +@hooks.register('register_page_action_menu_item') +def register_notify_and_publish_menu_item(): + return NotifyAndPublishMenuItem(order=100, allowed_models=Section) # + + +@hooks.register("register_context_modifier") +def enable_url_generator(context, request): + # only apply on image edit views + if request.resolver_match and request.resolver_match.url_name == "wagtailimages_edit": + context["url_generator_enabled"] = True + + +@hooks.register('register_page_listing_more_buttons') +def page_listing_more_buttons(page, user, next_url=None): + page_perms = page.permissions_for_user(user) + url = reverse('notify_and_publish', args=[page.id]) + print('classname', page.specific_class.__name__) + if page_perms.can_publish(): + if page.specific_class.__name__=='Article' or page.specific_class.__name__=='Survey' or page.specific_class.__name__=='Section': + yield wagtailadmin_widgets.Button( + 'Notify & Publish', + url, + priority=40, + icon_name='mail' + ) \ No newline at end of file diff --git a/interactive/wagtail_hooks.py b/interactive/wagtail_hooks.py index dd0d8329b..c453b1950 100644 --- a/interactive/wagtail_hooks.py +++ b/interactive/wagtail_hooks.py @@ -1,4 +1,4 @@ -from wagtail.contrib.modeladmin.options import ( +from wagtail_modeladmin.options import ( ModelAdmin, ModelAdminGroup, modeladmin_register, diff --git a/iogt/__init__.py b/iogt/__init__.py index e69de29bb..fb989c4e6 100644 --- a/iogt/__init__.py +++ b/iogt/__init__.py @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +__all__ = ('celery_app',) diff --git a/iogt/apps.py b/iogt/apps.py new file mode 100644 index 000000000..280b6398b --- /dev/null +++ b/iogt/apps.py @@ -0,0 +1,3 @@ +from wagtail.users.apps import WagtailUsersAppConfig +class CustomUsersAppConfig(WagtailUsersAppConfig): + user_viewset = "iogt_users.viewsets.UserViewSet" \ No newline at end of file diff --git a/iogt/celery.py b/iogt/celery.py new file mode 100644 index 000000000..3adc0f663 --- /dev/null +++ b/iogt/celery.py @@ -0,0 +1,10 @@ +import os +from celery import Celery + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'iogt.settings.base') + +app = Celery('iogt') + +app.config_from_object('django.conf:settings', namespace='CELERY') + +app.autodiscover_tasks() diff --git a/iogt/settings/base.py b/iogt/settings/base.py index 500c7ec7d..2f9edd9d4 100644 --- a/iogt/settings/base.py +++ b/iogt/settings/base.py @@ -15,11 +15,19 @@ PROFANITIES_LIST ) +# Monkey patch for deprecated ugettext_lazy used in third-party packages +import django.utils.translation +from django.utils.translation import gettext_lazy + +# Patch only if missing (for Django 4+ compatibility with old packages) +if not hasattr(django.utils.translation, 'ugettext_lazy'): + django.utils.translation.ugettext_lazy = gettext_lazy + PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(PROJECT_DIR) INSTALLED_APPS = [ - 'allauth', + 'allauth', 'allauth.account', 'allauth.socialaccount', 'comments', @@ -49,6 +57,8 @@ 'matomo', 'messaging', 'modelcluster', + 'admin_notifications', + 'user_notifications', 'notifications', 'questionnaires', 'rest_framework', @@ -57,10 +67,11 @@ 'search', 'taggit', 'translation_manager', + 'wagtailautocomplete', 'wagtail', 'wagtail.admin', 'wagtail.contrib.forms', - 'wagtail.contrib.modeladmin', + 'wagtail_modeladmin', 'wagtail.contrib.redirects', 'wagtail.contrib.settings', 'wagtail.documents', @@ -69,7 +80,8 @@ 'wagtail.search', 'wagtail.sites', 'wagtail.snippets', - 'wagtail.users', + "iogt.apps.CustomUsersAppConfig", + # 'wagtail.users', 'wagtail_localize', 'wagtail_localize.locales', 'wagtail_transfer', @@ -91,6 +103,7 @@ 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', + 'django.middleware.locale.LocaleMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', @@ -102,6 +115,10 @@ 'iogt.middleware.GlobalDataMiddleware', # 'admin_login.middleware.CustomAdminLoginRequiredMiddleware', 'wagtailcache.cache.FetchFromCacheMiddleware', + # 'wagtail.contrib.statcache.middleware.StatCacheMiddleware', + 'django.middleware.cache.UpdateCacheMiddleware', + 'django.middleware.cache.FetchFromCacheMiddleware', + "allauth.account.middleware.AccountMiddleware", ] # Prevent Wagtail's built in menu from showing in Admin > Settings @@ -129,7 +146,7 @@ 'home.processors.commit_hash', 'home.processors.show_footers', 'messaging.processors.add_vapid_public_key', - 'notifications.processors.push_notification', + 'admin_notifications.processors.push_notification', 'home.processors.jquery', ], }, @@ -226,6 +243,9 @@ SITE_ID = 1 +#Notifications +DJANGO_NOTIFICATIONS_CONFIG = { 'USE_JSONFIELD': True} + # Comments COMMENTS_APP = 'django_comments_xtd' COMMENTS_XTD_MAX_THREAD_LEVEL = 1 @@ -299,6 +319,11 @@ ('uz', _('Uzbek')), ('zu', _('Zulu')), ('xy', _('Testing')), + ('ha', _('Hausa')), + ('yo', _('Yoruba')), + ('ig', _('Igbo')), + ('pcm', _('Pidgin')), + ] EXTRA_LANG_INFO = { @@ -398,6 +423,24 @@ 'name': 'Testing', 'name_local': 'Testing', }, + 'ha': { + 'bidi': False, + 'code': 'ha', + 'name': 'Hausa', + 'name_local': 'Hausa', + }, + 'yo': { + 'bidi': False, + 'code': 'yo', + 'name': 'Yoruba', + 'name_local': 'Yoruba', + }, + 'pcm': { + 'bidi': False, + 'code': 'pcm', + 'name': 'Pidgin', + 'name_local': 'Pidgin', + }, } django.conf.locale.LANG_INFO.update(EXTRA_LANG_INFO) @@ -475,15 +518,15 @@ 'ACCESS_TOKEN_LIFETIME': timedelta(days=365), } -CACHE = os.getenv('CACHE', '') == 'enable' +CACHE = os.getenv('CACHE', 'enable') == 'enable' if CACHE: - CACHE_LOCATION = os.getenv('CACHE_LOCATION') + CACHE_LOCATION = os.getenv('CACHE_LOCATION', 'redis://redis:6379/0') if not CACHE_LOCATION: raise ImproperlyConfigured( "CACHE_LOCATION must be set if CACHE is set to 'enable'") CACHE_BACKEND = os.getenv( 'CACHE_BACKEND', - 'wagtailcache.compat_backends.django_redis.RedisCache') + 'django_redis.cache.RedisCache') DJANGO_REDIS_IGNORE_EXCEPTIONS = True SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' WAGTAIL_CACHE = True @@ -606,6 +649,13 @@ CSRF_COOKIE_SECURE = True CSRF_COOKIE_HTTPONLY = True +CELERY_BROKER_URL = 'redis://redis:6379/0' +CELERY_RESULT_BACKEND = 'redis://redis:6379/0' +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TASK_SERIALIZER = 'json' + +CSRF_TRUSTED_ORIGINS = os.getenv("CSRF_TRUSTED_ORIGINS", "").split(",") + # Enforce HTTPS and HSTS # SECURE_SSL_REDIRECT = True # SECURE_HSTS_SECONDS = 31536000 diff --git a/iogt/settings/dev.py b/iogt/settings/dev.py index eb76e60df..45e829218 100644 --- a/iogt/settings/dev.py +++ b/iogt/settings/dev.py @@ -1,4 +1,5 @@ from .base import * +from os import getenv WAGTAILADMIN_BASE_URL = 'http://localhost:8000' DEBUG = True @@ -7,6 +8,17 @@ ALLOWED_HOSTS = ['*'] EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': getenv('DB_NAME', 'postgres'), + 'USER': getenv('DB_USER', 'postgres'), + 'PASSWORD': getenv('DB_PASSWORD', 'iogt'), + 'HOST': getenv('DB_HOST', 'database'), + 'PORT': getenv('DB_PORT', '5432'), + } +} + if DEBUG and DEBUG_TOOLBAR_ENABLE: INSTALLED_APPS += ("debug_toolbar",) MIDDLEWARE += ("debug_toolbar.middleware.DebugToolbarMiddleware",) @@ -18,7 +30,16 @@ } INSTALLED_APPS += ("django_extensions",) - +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': getenv('DB_NAME', 'postgres'), + 'USER': getenv('DB_USER', 'postgres'), + 'PASSWORD': getenv('DB_PASSWORD', 'iogt'), + 'HOST': getenv('DB_HOST', 'database'), + 'PORT': getenv('DB_PORT', '5432'), + } +} try: from .local import * except ImportError: diff --git a/iogt/settings/production.py b/iogt/settings/production.py index bfb3dbb75..39299b078 100644 --- a/iogt/settings/production.py +++ b/iogt/settings/production.py @@ -33,7 +33,7 @@ }, } -SITE_VERSION = '3.0.8' +SITE_VERSION = '3.0.9-rc.31' try: from .local import * diff --git a/iogt/static/css/accounts.css b/iogt/static/css/accounts.css index 21f90ba07..e2f0540b3 100644 --- a/iogt/static/css/accounts.css +++ b/iogt/static/css/accounts.css @@ -42,7 +42,7 @@ form label { display: block; } -form label[for='id_terms_accepted'], form label[for='id_remember'] { +form label[for='id_terms_accepted'], form label[for='id_remember'], form span[id='id_password_helptext'] { display: inline-block; font-size: 10px; font-weight: 500; @@ -73,6 +73,7 @@ form input[type="checkbox"] { .profile-form__btn { margin-bottom: 8px; + margin-left: 0px; } .profile-form__btn:last-child { @@ -95,3 +96,39 @@ form input[type="checkbox"] { font-weight: 500; line-height: 14px; } + .multiselect-dropdown { + position: relative; + width: 540px; + user-select: none; + } + + .dropdown-btn { + padding: 10px; + border: 1px solid #ccc; + background: #fff; + cursor: pointer; + border-radius: 5px; + } + + .dropdown-content { + display: none; + position: absolute; + background-color: white; + border: 1px solid #ccc; + border-top: none; + max-height: 200px; + overflow-y: auto; + z-index: 1000; + width: 100%; + border-radius: 0 0 5px 5px; + } + + .dropdown-content label { + display: block; + padding: 8px 10px; + cursor: pointer; + } + + .dropdown-content label:hover { + background-color: #f0f0f0; + } \ No newline at end of file diff --git a/iogt/static/css/iogt.css b/iogt/static/css/iogt.css index f1a00457d..b43454acc 100644 --- a/iogt/static/css/iogt.css +++ b/iogt/static/css/iogt.css @@ -2205,8 +2205,9 @@ } .load-more a, button{ + margin:10px; font-size: 14px; - line-height: 20px; + line-height: 10px; font-weight: 600; border-radius: 24px; padding: 12px 16px; @@ -2214,7 +2215,7 @@ color: #303030; display: block; text-align: center; - width: 100%; + width: 20%; cursor: pointer; transition: all 0.2s linear; text-decoration: none; diff --git a/iogt/static/css/user-profile.css b/iogt/static/css/user-profile.css index 98ea54269..552916828 100644 --- a/iogt/static/css/user-profile.css +++ b/iogt/static/css/user-profile.css @@ -9,6 +9,10 @@ clear: both } +p.notification-enable { + font-size: 16px; + color: #EE4B2B; +} .profile__icon { background-color: #0070e2; border-radius: 50%; @@ -47,7 +51,8 @@ } .profile-form__btn { - margin-bottom: 8px + margin-bottom: 8px; + margin-left: 0px; } .profile-form__btn:last-child { diff --git a/iogt/static/icons/bell-solid.svg b/iogt/static/icons/bell-solid.svg new file mode 100644 index 000000000..96f35738e --- /dev/null +++ b/iogt/static/icons/bell-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/iogt/static/js/idb.js b/iogt/static/js/idb.js index be95ff2ac..c1c2f00d1 100644 --- a/iogt/static/js/idb.js +++ b/iogt/static/js/idb.js @@ -9,13 +9,11 @@ function openDB() { request.onupgradeneeded = event => { const db = event.target.result; if (!db.objectStoreNames.contains(STORE_NAME)) { - console.log("🔧 Creating object store:", STORE_NAME); db.createObjectStore(STORE_NAME, { keyPath: "id", autoIncrement: true }); } }; request.onsuccess = event => { - console.log("✅ IndexedDB Opened Successfully"); resolve(event.target.result); }; @@ -28,8 +26,6 @@ function openDB() { // ✅ Properly save request with correct body handling async function saveRequest(request) { - console.log("💾 Saving request to IndexedDB:", request.url); - const db = await openDB(); const clonedRequest = request.clone(); @@ -72,12 +68,10 @@ async function saveRequest(request) { const addRequest = store.add(requestData); addRequest.onsuccess = () => { - console.log("✅ Request stored in IndexedDB!", requestData); resolve(); }; addRequest.onerror = (event) => { - console.error("❌ Error saving request:", event.target.error); reject(event.target.error); }; }); @@ -85,7 +79,6 @@ async function saveRequest(request) { // ✅ Retrieve all stored requests async function getAllRequests() { - console.log("📂 Retrieving all stored requests..."); const db = await openDB(); @@ -95,12 +88,10 @@ async function getAllRequests() { const req = store.getAll(); req.onsuccess = () => { - console.log("📦 Stored Requests:", req.result); resolve(req.result); }; req.onerror = () => { - console.error("❌ Error retrieving requests:", req.error); reject(req.error); }; }); @@ -108,8 +99,6 @@ async function getAllRequests() { // ✅ Delete request after successful sync async function deleteRequest(id) { - console.log("🗑️ Deleting request with ID:", id); - const db = await openDB(); return new Promise((resolve, reject) => { @@ -118,12 +107,10 @@ async function deleteRequest(id) { const req = store.delete(id); req.onsuccess = () => { - console.log("✅ Successfully deleted request from IndexedDB!"); resolve(); }; req.onerror = (event) => { - console.error("❌ Error deleting request:", event.target.error); reject(event.target.error); }; }); diff --git a/iogt/static/js/iogt-no-jquery.js b/iogt/static/js/iogt-no-jquery.js index de4b1c069..95fe8746a 100644 --- a/iogt/static/js/iogt-no-jquery.js +++ b/iogt/static/js/iogt-no-jquery.js @@ -1,303 +1,353 @@ const ready = (callback) => { - if (document.readyState !== "loading") callback(); - else document.addEventListener("DOMContentLoaded", callback); + if (document.readyState !== "loading") callback(); + else document.addEventListener("DOMContentLoaded", callback); }; - -function showToast(message, type = 'info') { - const toast = document.getElementById('toast-notification'); - - if (!toast) return; - - toast.textContent = message; - - // Set color - switch (type) { - case 'success': - toast.style.backgroundColor = '#4caf50'; - break; - case 'error': - toast.style.backgroundColor = '#f44336'; - break; - case 'warning': - toast.style.backgroundColor = '#ff9800'; - break; - default: - toast.style.backgroundColor = '#333'; - } - - toast.style.display = 'block'; - toast.style.opacity = '1'; - toast.style.transform = 'translateY(0)'; - - // Force reflow for iOS animation - void toast.offsetHeight; - - setTimeout(() => { - toast.style.opacity = '0'; - toast.style.transform = 'translateY(-20px)'; - }, 3000); - - // Hide after transition - setTimeout(() => { - toast.style.display = 'none'; - }, 3500); - } +function showToast(message, type = "info") { + const toast = document.getElementById("toast-notification"); + + if (!toast) return; + + toast.textContent = message; + + // Set color + switch (type) { + case "success": + toast.style.backgroundColor = "#4caf50"; + break; + case "error": + toast.style.backgroundColor = "#f44336"; + break; + case "warning": + toast.style.backgroundColor = "#ff9800"; + break; + default: + toast.style.backgroundColor = "#333"; + } + + toast.style.display = "block"; + toast.style.opacity = "1"; + toast.style.transform = "translateY(0)"; + + // Force reflow for iOS animation + void toast.offsetHeight; + + setTimeout(() => { + toast.style.opacity = "0"; + toast.style.transform = "translateY(-20px)"; + }, 3000); + + // Hide after transition + setTimeout(() => { + toast.style.display = "none"; + }, 3500); +} const init = () => { - const show = (el) => el.style.display = ''; - const hide = (el) => el.style.display = 'none'; - - const externalLinkOverlay = document.querySelector('#external-link-overlay'); - externalLinkOverlay?.addEventListener('click', (event) => hide(event.target)); - - const submitWhenOffline = gettext('You cannot submit when offline'); - - const readContent = document.querySelectorAll('.complete'); - const commentLikeHolders = document.querySelectorAll('.like-holder'); - const replyLinks = document.querySelectorAll('.reply-link'); - const offlineAppBtns = document.querySelectorAll('.offline-app-btn'); - const chatbotBtns = document.querySelectorAll('.chatbot-btn'); - const questionnaireSubmitBtns = document.querySelectorAll('.questionnaire-submit-btn'); - - // Save original button label - questionnaireSubmitBtns.forEach(btn => { - const span = btn.querySelector('span'); - if (span && !span.dataset.originalLabel) { - span.dataset.originalLabel = span.textContent.trim(); - } + const show = (el) => (el.style.display = ""); + const hide = (el) => (el.style.display = "none"); + + const externalLinkOverlay = document.querySelector("#external-link-overlay"); + externalLinkOverlay?.addEventListener("click", (event) => hide(event.target)); + + const submitWhenOffline = gettext("You cannot submit when offline"); + + const readContent = document.querySelectorAll(".complete"); + const commentLikeHolders = document.querySelectorAll(".like-holder"); + const replyLinks = document.querySelectorAll(".reply-link"); + const offlineAppBtns = document.querySelectorAll(".offline-app-btn"); + const chatbotBtns = document.querySelectorAll(".chatbot-btn"); + const questionnaireSubmitBtns = document.querySelectorAll( + ".questionnaire-submit-btn" + ); + + // Save original button label + questionnaireSubmitBtns.forEach((btn) => { + const span = btn.querySelector("span"); + if (span && !span.dataset.originalLabel) { + span.dataset.originalLabel = span.textContent.trim(); + } + }); + + const externalLinks = document.querySelectorAll( + 'a[href*="/external-link/?next="]' + ); + const elementsToToggle = [ + ".download-app-btn", + ".login-create-account-btn", + ".change-digital-pin", + ".comments__form", + ".logout-btn", + ".notification-pref-btn", + ".progress-holder", + ".report-comment", + ".search-form-holder", + ].flatMap((selector) => Array.from(document.querySelectorAll(selector))); + + const blockExternalLinks = (event) => { + event.preventDefault(); + show(externalLinkOverlay); + }; + + const hideFooterMenu = () => { + hide(document.querySelector(".footer-head")); + }; + + if ("serviceWorker" in navigator) { + navigator.serviceWorker.addEventListener("message", (event) => { + const data = event.data; + if (!data) return; + + switch (data.type) { + case "sync-success": + showToast(`✅ Synced back offline filled form.`, "success"); + break; + case "sync-failed": + showToast(`⚠️ Offline form sync failed for: ${data.url}`, "error"); + break; + case "sync-error": + showToast(`❌ Sync error: ${data.error}`, "error"); + break; + } + }); + } + + const disableForOfflineAccess = () => { + elementsToToggle.forEach(hide); + replyLinks.forEach(hide); + readContent.forEach((el) => el.classList.remove("complete")); + commentLikeHolders.forEach((el) => + el.setAttribute("style", "display:none !important") + ); + offlineAppBtns.forEach(show); + chatbotBtns.forEach((btn) => { + btn.style.pointerEvents = "none"; + btn.style.background = "#808080"; + }); + questionnaireSubmitBtns.forEach((btn) => { + btn.style.pointerEvents = "all"; // ensure it's still clickable + const span = btn.querySelector("span"); + if (span) { + span.textContent = + span.dataset.originalLabel || span.textContent.trim(); + } + }); + externalLinks.forEach((link) => + link.addEventListener("click", blockExternalLinks) + ); + }; + + const enableForOnlineAccess = () => { + elementsToToggle.forEach(show); + readContent.forEach((el) => el.classList.add("complete")); + commentLikeHolders.forEach((el) => + el.setAttribute("style", "display:inline-block !important") + ); + replyLinks.forEach((el) => + el.setAttribute("style", "display:inline-block") + ); + offlineAppBtns.forEach(hide); + chatbotBtns.forEach((btn) => { + btn.style.pointerEvents = "all"; + btn.style.background = "#F7F7F9"; + }); + questionnaireSubmitBtns.forEach((btn) => { + btn.style.pointerEvents = "all"; + const span = btn.querySelector("span"); + if (span) { + span.textContent = + span.dataset.originalLabel || span.textContent.split("(")[0].trim(); + } }); + externalLinks.forEach((link) => { + show(link); + link.removeEventListener("click", blockExternalLinks); + }); + }; - const externalLinks = document.querySelectorAll('a[href*="/external-link/?next="]'); - const elementsToToggle = [ - '.download-app-btn', - '.login-create-account-btn', - '.change-digital-pin', - '.comments__form', - '.logout-btn', - '.progress-holder', - '.report-comment', - '.search-form-holder', - ].flatMap(selector => Array.from(document.querySelectorAll(selector))); - - const blockExternalLinks = (event) => { - event.preventDefault(); - show(externalLinkOverlay); - }; - - const hideFooterMenu = () => { - hide(document.querySelector('.footer-head')); - }; - - if ('serviceWorker' in navigator) { - navigator.serviceWorker.addEventListener('message', event => { - const data = event.data; - if (!data) return; - - switch (data.type) { - case 'sync-success': - showToast(`✅ Synced back offline filled form.`, 'success'); - break; - case 'sync-failed': - showToast(`⚠️ Offline form sync failed for: ${data.url}`, 'error'); - break; - case 'sync-error': - showToast(`❌ Sync error: ${data.error}`, 'error'); - break; - } - }); + window.addEventListener("offline", () => { + console.warn("🔌 Offline detected."); + disableForOfflineAccess(); + if (getItem("offlineReady") === true) { + setTimeout(() => location.reload(), 3000); } + }); - const disableForOfflineAccess = () => { - elementsToToggle.forEach(hide); - replyLinks.forEach(hide); - readContent.forEach(el => el.classList.remove('complete')); - commentLikeHolders.forEach(el => el.setAttribute('style', 'display:none !important')); - offlineAppBtns.forEach(show); - chatbotBtns.forEach(btn => { - btn.style.pointerEvents = 'none'; - btn.style.background = '#808080'; - }); - questionnaireSubmitBtns.forEach(btn => { - btn.style.pointerEvents = 'all'; // ensure it's still clickable - const span = btn.querySelector('span'); - if (span) { - span.textContent = span.dataset.originalLabel || span.textContent.trim(); - } - }); - externalLinks.forEach(link => link.addEventListener('click', blockExternalLinks)); - }; - - const enableForOnlineAccess = () => { - elementsToToggle.forEach(show); - readContent.forEach(el => el.classList.add('complete')); - commentLikeHolders.forEach(el => el.setAttribute('style', 'display:inline-block !important')); - replyLinks.forEach(el => el.setAttribute('style', 'display:inline-block')); - offlineAppBtns.forEach(hide); - chatbotBtns.forEach(btn => { - btn.style.pointerEvents = 'all'; - btn.style.background = '#F7F7F9'; - }); - questionnaireSubmitBtns.forEach(btn => { - btn.style.pointerEvents = 'all'; - const span = btn.querySelector('span'); - if (span) { - span.textContent = span.dataset.originalLabel || span.textContent.split('(')[0].trim(); - } - }); - externalLinks.forEach(link => { - show(link); - link.removeEventListener('click', blockExternalLinks); - }); - }; - - window.addEventListener('offline', () => { - console.warn("🔌 Offline detected."); - disableForOfflineAccess(); - if (getItem('offlineReady') === true) { - console.log("📦 Page cached. Reloading offline view..."); - setTimeout(() => location.reload(), 3000); - } - }); - - window.addEventListener('online', enableForOnlineAccess); + window.addEventListener("online", enableForOnlineAccess); - window.navigator.onLine ? enableForOnlineAccess() : disableForOfflineAccess(); + window.navigator.onLine ? enableForOnlineAccess() : disableForOfflineAccess(); - hideFooterMenu(); + hideFooterMenu(); - // Force re-check of online status - fetch(window.location.href, { method: 'HEAD', cache: 'no-cache' }) + // Force re-check of online status + fetch(window.location.href, { method: "HEAD", cache: "no-cache" }) .then(() => { - console.log("✅ Verified online via HEAD request"); - enableForOnlineAccess(); + enableForOnlineAccess(); }) .catch(() => { - console.warn("⚠️ Verified offline via HEAD request"); - disableForOfflineAccess(); + console.warn("⚠️ Verified offline via HEAD request"); + disableForOfflineAccess(); }); }; -const download = pageId => { - showToast("📥 Downloading…"); - console.log("Starting download for page:", pageId); - - fetch(`/page-tree/${pageId}/`) - .then(resp => { - if (!resp.ok) { - throw new Error(`Failed to fetch URLs for caching. Status: ${resp.status}`); - } - return resp.json(); - }) - .then(urls => { - if (!Array.isArray(urls) || urls.length === 0) { - throw new Error("No URLs received for caching."); - } - - return caches.open('iogt').then(cache => { - console.log("URLs to cache:", urls); - return Promise.all(urls.map(url => - fetch(url, { method: 'HEAD' }) - .then(response => { - if (response.ok) { - return cache.add(url).catch(error => { - if (error.name === 'QuotaExceededError') { - alert("⚠️ Your storage limit has been reached! Please free up space."); - throw new Error("Storage full! Cannot cache more content."); - } - throw error; - }); - } else { - console.warn(`Skipping invalid URL: ${url} (Status: ${response.status})`); - } - }) - .catch(err => console.warn(`Skipping ${url} due to error:`, err)) - )); - }); - }) - .then(() => { - setItem('offlineReady', true); // ✅ Mark ready for offline - console.log("✅ Content cached successfully!"); - showToast("✅ Content is now available offline!", "success"); - location.reload(); - }) - .catch(error => { - console.error("❌ Download error:", error); - alert("⚠️ Download failed. Please try again."); - }); +const download = (pageId) => { + showToast("📥 Downloading…"); + + fetch(`/page-tree/${pageId}/`) + .then((resp) => { + if (!resp.ok) { + throw new Error( + `Failed to fetch URLs for caching. Status: ${resp.status}` + ); + } + return resp.json(); + }) + .then((urls) => { + if (!Array.isArray(urls) || urls.length === 0) { + throw new Error("No URLs received for caching."); + } + + return caches.open("iogt").then((cache) => { + return Promise.all( + urls.map((url) => + fetch(url, { method: "HEAD" }) + .then((response) => { + if (response.ok) { + return cache.add(url).catch((error) => { + if (error.name === "QuotaExceededError") { + alert( + "⚠️ Your storage limit has been reached! Please free up space." + ); + throw new Error( + "Storage full! Cannot cache more content." + ); + } + throw error; + }); + } else { + console.warn( + `Skipping invalid URL: ${url} (Status: ${response.status})` + ); + } + }) + .catch((err) => + console.warn(`Skipping ${url} due to error:`, err) + ) + ) + ); + }); + }) + .then(() => { + setItem("offlineReady", true); // ✅ Mark ready for offline + showToast("✅ Content is now available offline!", "success"); + location.reload(); + }) + .catch((error) => { + console.error("❌ Download error:", error); + alert("⚠️ Download failed. Please try again."); + }); }; -function getItem (key, defaultValue = null) { - try { - return JSON.parse(localStorage.getItem(key)) ?? defaultValue; - } catch { - return defaultValue; - } -}; +function getItem(key, defaultValue = null) { + try { + return JSON.parse(localStorage.getItem(key)) ?? defaultValue; + } catch { + return defaultValue; + } +} const setItem = (key, value) => { - localStorage.setItem(key, JSON.stringify(value)); + localStorage.setItem(key, JSON.stringify(value)); }; -const registerPushNotification = registration => { - if (!registration.showNotification) return; - if (Notification.permission === 'denied') return; - if (!'PushManager' in window) return; +const registerPushNotification = (registration) => { + if (!registration.showNotification) return; + if (!"PushManager" in window) return; + if (Notification.permission === "default") { + Notification.requestPermission(function (permission) { + if (permission === "granted") { + subscribe(registration); + } + }); + } else if (Notification.permission === "granted") { subscribe(registration); + } }; -const urlB64ToUint8Array = base64String => { - const padding = '='.repeat((4 - base64String.length % 4) % 4); - const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/'); - const rawData = window.atob(base64); - return Uint8Array.from([...rawData].map(char => char.charCodeAt(0))); +const urlB64ToUint8Array = (base64String) => { + const padding = "=".repeat((4 - (base64String.length % 4)) % 4); + const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); + + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + + return outputArray; }; -const subscribe = registration => { - registration.pushManager.getSubscription() - .then(subscription => { - if (subscription) { - sendSubscriptionToServer(subscription, 'subscribe'); - return; - } - const vapidKey = document.querySelector('meta[name="vapid-key"]')?.content; - const options = { userVisibleOnly: true }; - if (vapidKey) options.applicationServerKey = urlB64ToUint8Array(vapidKey); - - registration.pushManager.subscribe(options) - .then(subscription => sendSubscriptionToServer(subscription, 'subscribe')) - .catch(error => console.log("Error during subscribe()", error)); - }) - .catch(error => console.log("Error during getSubscription()", error)); +const subscribe = (registration) => { + registration.pushManager + .getSubscription() + .then((subscription) => { + if (subscription) { + sendSubscriptionToServer(subscription, "subscribe"); + return; + } + const vapidKey = document.querySelector( + 'meta[name="vapid-key"]' + )?.content; + const options = { userVisibleOnly: true }; + if (vapidKey) options.applicationServerKey = urlB64ToUint8Array(vapidKey); + + registration.pushManager + .subscribe(options) + .then((subscription) => + sendSubscriptionToServer(subscription, "subscribe") + ) + .catch((error) => console.log("Error during subscribe()", error)); + }) + .catch((error) => console.log("Error during getSubscription()", error)); }; const sendSubscriptionToServer = (subscription, statusType) => { - const browser = navigator.userAgent.match(/(firefox|msie|chrome|safari|trident)/ig)[0].toLowerCase(); - const data = { - status_type: statusType, - subscription: subscription.toJSON(), - browser: browser, - }; - - fetch('/webpush/subscribe/', { - method: 'POST', - body: JSON.stringify(data), - headers: { 'content-type': 'application/json' }, - credentials: "include" - }).then(() => { - setItem('isPushNotificationRegistered', statusType === 'subscribe'); - }); + const browser = navigator.userAgent + .match(/(firefox|msie|chrome|safari|trident)/gi)[0] + .toLowerCase(); + const data = { + status_type: statusType, + subscription: subscription.toJSON(), + browser: browser, + }; + + fetch("/webpush/subscribe/", { + method: "POST", + body: JSON.stringify(data), + headers: { "content-type": "application/json" }, + credentials: "include", + }).then(() => { + setItem("isPushNotificationRegistered", statusType === "subscribe"); + }); }; const unSubscribePushNotifications = () => { - const isPushNotificationRegistered = getItem('isPushNotificationRegistered', false); - if (isPushNotificationRegistered && isAuthenticated && 'serviceWorker' in navigator) { - navigator.serviceWorker.ready.then(registration => { - registration.pushManager.getSubscription().then(subscription => { - if (subscription) sendSubscriptionToServer(subscription, 'unsubscribe'); - }); - }); - } + const isPushNotificationRegistered = getItem( + "isPushNotificationRegistered", + false + ); + if ( + isPushNotificationRegistered && + isAuthenticated && + "serviceWorker" in navigator + ) { + navigator.serviceWorker.ready.then((registration) => { + registration.pushManager.getSubscription().then((subscription) => { + if (subscription) sendSubscriptionToServer(subscription, "unsubscribe"); + }); + }); + } }; ready(init); diff --git a/iogt/static/js/iogt.js b/iogt/static/js/iogt.js index 6e21fcb0e..da1a46426 100644 --- a/iogt/static/js/iogt.js +++ b/iogt/static/js/iogt.js @@ -1,314 +1,355 @@ -function showToast(message, type = 'info') { - const toast = document.getElementById('toast-notification'); - - if (!toast) return; - - toast.textContent = message; - - // Set color - switch (type) { - case 'success': - toast.style.backgroundColor = '#4caf50'; - break; - case 'error': - toast.style.backgroundColor = '#f44336'; - break; - case 'warning': - toast.style.backgroundColor = '#ff9800'; - break; - default: - toast.style.backgroundColor = '#333'; - } - - toast.style.display = 'block'; - toast.style.opacity = '1'; - toast.style.transform = 'translateY(0)'; - - // Force reflow for iOS animation - void toast.offsetHeight; - - setTimeout(() => { - toast.style.opacity = '0'; - toast.style.transform = 'translateY(-20px)'; - }, 3000); - - // Hide after transition - setTimeout(() => { - toast.style.display = 'none'; - }, 3500); - } +function showToast(message, type = "info") { + const toast = document.getElementById("toast-notification"); + + if (!toast) return; + + toast.textContent = message; + + // Set color + switch (type) { + case "success": + toast.style.backgroundColor = "#4caf50"; + break; + case "error": + toast.style.backgroundColor = "#f44336"; + break; + case "warning": + toast.style.backgroundColor = "#ff9800"; + break; + default: + toast.style.backgroundColor = "#333"; + } + + toast.style.display = "block"; + toast.style.opacity = "1"; + toast.style.transform = "translateY(0)"; + + // Force reflow for iOS animation + void toast.offsetHeight; + + setTimeout(() => { + toast.style.opacity = "0"; + toast.style.transform = "translateY(-20px)"; + }, 3000); + + // Hide after transition + setTimeout(() => { + toast.style.display = "none"; + }, 3500); +} $(document).ready(() => { - const externalLinkOverlay = $('#external-link-overlay'); - externalLinkOverlay.click(() => externalLinkOverlay.css('display', 'none')); - - const submitWhenOffline = gettext('You cannot submit when offline'); - - const searchFormHolder = $('.search-form-holder'); - const readContent = $('.complete'); - const commentForm = $('.comments__form'); - const commentLikeHolders = $('.like-holder'); - const reportComment = $('.report-comment'); - const commentReplyLinks = $('.reply-link'); - const downloadAppBtns = $('.download-app-btn'); - const offlineAppBtns = $('.offline-app-btn'); - const chatbotBtns = $('.chatbot-btn'); - const questionnaireSubmitBtns = $('.questionnaire-submit-btn'); - const progressHolder = $('.progress-holder'); - const changeDigitalPinBtn = $('.change-digital-pin'); - const loginCreateAccountBtns = $('.login-create-account-btn'); - const logoutBtn = $('.logout-btn'); - const externalLinks = $('a[href*="/external-link/?next="]'); - + const externalLinkOverlay = $("#external-link-overlay"); + externalLinkOverlay.click(() => externalLinkOverlay.css("display", "none")); + + const submitWhenOffline = gettext("You cannot submit when offline"); + + const searchFormHolder = $(".search-form-holder"); + const readContent = $(".complete"); + const commentForm = $(".comments__form"); + const commentLikeHolders = $(".like-holder"); + const reportComment = $(".report-comment"); + const commentReplyLinks = $(".reply-link"); + const downloadAppBtns = $(".download-app-btn"); + const offlineAppBtns = $(".offline-app-btn"); + const chatbotBtns = $(".chatbot-btn"); + const questionnaireSubmitBtns = $(".questionnaire-submit-btn"); + const progressHolder = $(".progress-holder"); + const changeDigitalPinBtn = $(".change-digital-pin"); + const loginCreateAccountBtns = $(".login-create-account-btn"); + const logoutBtn = $(".logout-btn"); + const notificationPreferenceButton = $(".notification-pref-btn"); + const externalLinks = $('a[href*="/external-link/?next="]'); + + questionnaireSubmitBtns.each((index, btn) => { + const $btn = $(btn); + const span = $btn.find("span"); + if (!span.attr("data-original-label")) { + span.attr("data-original-label", span.text().trim()); + } + }); + + const disableForOfflineAccess = () => { + searchFormHolder.hide(); + readContent.removeClass("complete"); + commentForm.hide(); + commentLikeHolders.attr("style", "display: none !important"); + reportComment.hide(); + commentReplyLinks.hide(); + downloadAppBtns.hide(); + offlineAppBtns.show(); + chatbotBtns.each((index, btn) => { + const $btn = $(btn); + $btn.css("pointer-events", "none"); + $btn.css("background", "#808080"); + }); questionnaireSubmitBtns.each((index, btn) => { - const $btn = $(btn); - const span = $btn.find('span'); - if (!span.attr('data-original-label')) { - span.attr('data-original-label', span.text().trim()); - } + const $btn = $(btn); + const span = $btn.find("span"); + span.text(span.attr("data-original-label")); // reset label (no "submit when offline") }); - - const disableForOfflineAccess = () => { - searchFormHolder.hide(); - readContent.removeClass('complete'); - commentForm.hide(); - commentLikeHolders.attr('style', 'display: none !important'); - reportComment.hide(); - commentReplyLinks.hide(); - downloadAppBtns.hide(); - offlineAppBtns.show(); - chatbotBtns.each((index, btn) => { - const $btn = $(btn); - $btn.css('pointer-events', 'none'); - $btn.css('background', '#808080'); - }); - questionnaireSubmitBtns.each((index, btn) => { - const $btn = $(btn); - const span = $btn.find('span'); - span.text(span.attr('data-original-label')); // reset label (no "submit when offline") - }); - progressHolder.hide(); - changeDigitalPinBtn.hide(); - loginCreateAccountBtns.hide(); - logoutBtn.hide(); - externalLinks.each((index, link) => { - const $link = $(link); - if (!$link.data('offline-bound')) { - $link.on('click.offline', e => { - e.preventDefault(); - externalLinkOverlay.css('display', 'block'); - }); - $link.data('offline-bound', true); - } - }); - }; - - const enableForOnlineAccess = () => { - searchFormHolder.show(); - readContent.addClass('complete'); - commentForm.show(); - commentLikeHolders.attr('style', 'display: inline-block !important'); - reportComment.show(); - commentReplyLinks.show(); - downloadAppBtns.show(); - offlineAppBtns.hide(); - chatbotBtns.each((index, btn) => { - const $btn = $(btn); - $btn.css('pointer-events', 'all'); - $btn.css('background', '#F7F7F9'); - }); - questionnaireSubmitBtns.each((index, btn) => { - const $btn = $(btn); - $btn.css('pointer-events', 'all'); - const span = $btn.find('span'); - const original = span.attr('data-original-label') || span.text().trim(); - span.text(original); - }); - progressHolder.show(); - changeDigitalPinBtn.show(); - loginCreateAccountBtns.show(); - logoutBtn.show(); - externalLinks.show(); - externalLinks.each((index, link) => { - $(link).off('click.offline'); + progressHolder.hide(); + changeDigitalPinBtn.hide(); + loginCreateAccountBtns.hide(); + logoutBtn.hide(); + notificationPreferenceButton.hide(); + externalLinks.each((index, link) => { + const $link = $(link); + if (!$link.data("offline-bound")) { + $link.on("click.offline", (e) => { + e.preventDefault(); + externalLinkOverlay.css("display", "block"); }); - }; - - if ('serviceWorker' in navigator) { - navigator.serviceWorker.addEventListener('message', event => { - const data = event.data; - if (!data) return; - - switch (data.type) { - case 'sync-success': - showToast(`✅ Synced back offline filled form.`, "success"); - break; - case 'sync-failed': - showToast(`⚠️ Offline form sync failed for: ${data.url}`, "error"); - break; - case 'sync-error': - showToast(`❌ Sync error: ${data.error}`, "error"); - break; - } - }); - } - - $(window).on('offline', () => { - console.warn("🔌 Offline detected."); - disableForOfflineAccess(); - if (getItem('offlineReady') === true) { - console.log("📦 Page cached. Reloading offline view..."); - setTimeout(() => location.reload(), 500); - } + $link.data("offline-bound", true); + } }); - - $(window).on('online', () => { - enableForOnlineAccess(); + }; + + const enableForOnlineAccess = () => { + searchFormHolder.show(); + readContent.addClass("complete"); + commentForm.show(); + commentLikeHolders.attr("style", "display: inline-block !important"); + reportComment.show(); + commentReplyLinks.show(); + downloadAppBtns.show(); + offlineAppBtns.hide(); + chatbotBtns.each((index, btn) => { + const $btn = $(btn); + $btn.css("pointer-events", "all"); + $btn.css("background", "#F7F7F9"); + }); + questionnaireSubmitBtns.each((index, btn) => { + const $btn = $(btn); + $btn.css("pointer-events", "all"); + const span = $btn.find("span"); + const original = span.attr("data-original-label") || span.text().trim(); + span.text(original); }); + progressHolder.show(); + changeDigitalPinBtn.show(); + loginCreateAccountBtns.show(); + logoutBtn.show(); + notificationPreferenceButton.show(); + externalLinks.show(); + externalLinks.each((index, link) => { + $(link).off("click.offline"); + }); + }; + + if ("serviceWorker" in navigator) { + navigator.serviceWorker.addEventListener("message", (event) => { + const data = event.data; + if (!data) return; + + switch (data.type) { + case "sync-success": + showToast(`✅ Synced back offline filled form.`, "success"); + break; + case "sync-failed": + showToast(`⚠️ Offline form sync failed for: ${data.url}`, "error"); + break; + case "sync-error": + showToast(`❌ Sync error: ${data.error}`, "error"); + break; + } + }); + } + + $(window).on("offline", () => { + console.warn("🔌 Offline detected."); + disableForOfflineAccess(); + if (getItem("offlineReady") === true) { + setTimeout(() => location.reload(), 500); + } + }); + + $(window).on("online", () => { + enableForOnlineAccess(); + }); - window.navigator.onLine ? enableForOnlineAccess() : disableForOfflineAccess(); + window.navigator.onLine ? enableForOnlineAccess() : disableForOfflineAccess(); - fetch(window.location.href, { method: 'HEAD', cache: 'no-cache' }) + fetch(window.location.href, { method: "HEAD", cache: "no-cache" }) .then(() => { - console.log("✅ Verified online via HEAD request"); - enableForOnlineAccess(); + enableForOnlineAccess(); }) .catch(() => { - console.warn("⚠️ Verified offline via HEAD request"); - disableForOfflineAccess(); + console.warn("⚠️ Verified offline via HEAD request"); + disableForOfflineAccess(); }); - $('.footer-head').hide(); + $(".footer-head").hide(); }); -const download = pageId => { - showToast("📥 Download starting…"); - console.log("Starting download for page:", pageId); - - fetch(`/page-tree/${pageId}/`) - .then(resp => { - if (!resp.ok) { - throw new Error(`Failed to fetch URLs for caching. Status: ${resp.status}`); - } - return resp.json(); - }) - .then(urls => { - if (!Array.isArray(urls) || urls.length === 0) { - throw new Error("No URLs received for caching."); - } - - return caches.open('iogt').then(cache => { - console.log("URLs to cache:", urls); - - return Promise.all(urls.map(url => - fetch(url, { method: 'HEAD' }) - .then(response => { - if (response.ok) { - return cache.add(url).catch(error => { - if (error.name === 'QuotaExceededError') { - alert("⚠️ Your storage limit has been reached! Please free up space."); - throw new Error("Storage full! Cannot cache more content."); - } - throw error; - }); - } else { - console.warn(`Skipping invalid URL: ${url} (Status: ${response.status})`); - } - }) - .catch(err => console.warn(`Skipping ${url} due to error:`, err)) - )); - }); - }) - .then(() => { - setItem('offlineReady', true); // ✅ Set offline-ready flag - console.log("✅ Content cached successfully!"); - showToast("✅ Content is now available offline!", "success"); - location.reload(); // ✅ Reload after caching - }) - .catch(error => { - console.error("❌ Download error:", error); - alert("⚠️ Download failed. Please try again."); - }); +const download = (pageId) => { + showToast("📥 Download starting…"); + + fetch(`/page-tree/${pageId}/`) + .then((resp) => { + if (!resp.ok) { + throw new Error( + `Failed to fetch URLs for caching. Status: ${resp.status}` + ); + } + return resp.json(); + }) + .then((urls) => { + if (!Array.isArray(urls) || urls.length === 0) { + throw new Error("No URLs received for caching."); + } + + return caches.open("iogt").then((cache) => { + + return Promise.all( + urls.map((url) => + fetch(url, { method: "HEAD" }) + .then((response) => { + if (response.ok) { + return cache.add(url).catch((error) => { + if (error.name === "QuotaExceededError") { + alert( + "⚠️ Your storage limit has been reached! Please free up space." + ); + throw new Error( + "Storage full! Cannot cache more content." + ); + } + throw error; + }); + } else { + console.warn( + `Skipping invalid URL: ${url} (Status: ${response.status})` + ); + } + }) + .catch((err) => + console.warn(`Skipping ${url} due to error:`, err) + ) + ) + ); + }); + }) + .then(() => { + setItem("offlineReady", true); // ✅ Set offline-ready flag + showToast("✅ Content is now available offline!", "success"); + location.reload(); // ✅ Reload after caching + }) + .catch((error) => { + console.error("❌ Download error:", error); + alert("⚠️ Download failed. Please try again."); + }); }; -function getItem (key, defaultValue = null) { - try { - return JSON.parse(localStorage.getItem(key)) ?? defaultValue; - } catch { - return defaultValue; - } -}; +function getItem(key, defaultValue = null) { + try { + return JSON.parse(localStorage.getItem(key)) ?? defaultValue; + } catch { + return defaultValue; + } +} const setItem = (key, value) => { - localStorage.setItem(key, JSON.stringify(value)); + localStorage.setItem(key, JSON.stringify(value)); }; -const registerPushNotification = registration => { - if (!registration.showNotification || Notification.permission === 'denied' || !('PushManager' in window)) { - return; - } - subscribe(registration); +const registerPushNotification = (registration) => { + if ( + !registration.showNotification || + !("PushManager" in window) + ) { + return; + } + + // Ask permission first if needed + if (Notification.permission === "default") { + Notification.requestPermission().then(permission => { + if (permission === "granted") { + subscribe(registration); // Now safe to subscribe + } + }); + } else if (Notification.permission === "granted") { + subscribe(registration); // Already granted + } }; -const urlB64ToUint8Array = base64String => { - const padding = '='.repeat((4 - base64String.length % 4) % 4); - const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); - const rawData = window.atob(base64); - return Uint8Array.from([...rawData].map(char => char.charCodeAt(0))); +const urlB64ToUint8Array = (base64String) => { + const padding = "=".repeat((4 - (base64String.length % 4)) % 4); + const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); + + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + + return outputArray; }; -const subscribe = registration => { - registration.pushManager.getSubscription() - .then(subscription => { - if (subscription) { - sendSubscriptionToServer(subscription, 'subscribe'); - return; - } - - const vapidKey = $('meta[name="vapid-key"]').attr('content'); - const options = { - userVisibleOnly: true, - ...(vapidKey && { applicationServerKey: urlB64ToUint8Array(vapidKey) }) - }; - - registration.pushManager.subscribe(options) - .then(subscription => { - sendSubscriptionToServer(subscription, 'subscribe'); - }) - .catch(error => console.log("Error during subscribe()", error)); +const subscribe = (registration) => { + registration.pushManager + .getSubscription() + .then((subscription) => { + if (subscription) { + sendSubscriptionToServer(subscription, "subscribe"); + return; + } + + const vapidKey = $('meta[name="vapid-key"]').attr("content"); + const options = { + userVisibleOnly: true, + ...(vapidKey && { applicationServerKey: urlB64ToUint8Array(vapidKey) }), + }; + + registration.pushManager + .subscribe(options) + .then((subscription) => { + sendSubscriptionToServer(subscription, "subscribe"); }) - .catch(error => console.log("Error during getSubscription()", error)); + .catch((error) => console.log("Error during subscribe()", error)); + }) + .catch((error) => console.log("Error during getSubscription()", error)); }; const sendSubscriptionToServer = (subscription, statusType) => { - const browser = navigator.userAgent.match(/(firefox|msie|chrome|safari|trident)/ig)[0].toLowerCase(); - const data = { - status_type: statusType, - subscription: subscription.toJSON(), - browser: browser, - }; - - fetch('/webpush/subscribe/', { - method: 'POST', - body: JSON.stringify(data), - headers: { - 'content-type': 'application/json' - }, - credentials: "include" - }).then(() => { - setItem('isPushNotificationRegistered', statusType === 'subscribe'); - }); + const browser = navigator.userAgent + .match(/(firefox|msie|chrome|safari|trident)/gi)[0] + .toLowerCase(); + const data = { + status_type: statusType, + subscription: subscription.toJSON(), + browser: browser, + }; + + fetch("/webpush/subscribe/", { + method: "POST", + body: JSON.stringify(data), + headers: { + "content-type": "application/json", + }, + credentials: "include", + }).then(() => { + setItem("isPushNotificationRegistered", statusType === "subscribe"); + }); }; const unSubscribePushNotifications = () => { - const isPushNotificationRegistered = getItem('isPushNotificationRegistered', false); - if (isPushNotificationRegistered && isAuthenticated && 'serviceWorker' in navigator) { - navigator.serviceWorker.ready.then(registration => { - registration.pushManager.getSubscription().then(subscription => { - if (subscription) { - sendSubscriptionToServer(subscription, 'unsubscribe'); - } - }); - }); - } + const isPushNotificationRegistered = getItem( + "isPushNotificationRegistered", + false + ); + if ( + isPushNotificationRegistered && + isAuthenticated && + "serviceWorker" in navigator + ) { + navigator.serviceWorker.ready.then((registration) => { + registration.pushManager.getSubscription().then((subscription) => { + if (subscription) { + sendSubscriptionToServer(subscription, "unsubscribe"); + } + }); + }); + } }; diff --git a/iogt/static/js/sw-init.js b/iogt/static/js/sw-init.js index 40c23d174..2a06ac02e 100644 --- a/iogt/static/js/sw-init.js +++ b/iogt/static/js/sw-init.js @@ -13,7 +13,6 @@ const syncStoredRequests = async () => { if (response.ok) { await deleteRequest(req.id); - console.log("✅ Synced:", req.url); showToast(`✅ Synced back offline filled form.`, "success"); } } catch (err) { diff --git a/iogt/templates/base.html b/iogt/templates/base.html index bfc887f66..ecfdaa013 100644 --- a/iogt/templates/base.html +++ b/iogt/templates/base.html @@ -5,26 +5,26 @@ - + {% block title %} - {% if page.seo_title %} - {{ page.seo_title }} - {% else %} - {{ page.title }} - {% endif %} + {% if page.seo_title %} + {{ page.seo_title }} + {% else %} + {{ page.title }} + {% endif %} {% endblock %} {% block title_suffix %} - {% with page.get_site.site_name as site_name %} - {% if site_name %}- {{ site_name }}{% endif %} - {% endwith %} + {% with page.get_site.site_name as site_name %} + {% if site_name %}- {{ site_name }}{% endif %} + {% endwith %} {% endblock %} {% image settings.home.SiteSettings.favicon width-60 as favicon_img %} - - - + + + {% social_meta_tags %} @@ -44,15 +44,16 @@ {% block extra_css %} - {# Override this in templates to add extra stylesheets #} + {# Override this in templates to add extra stylesheets #} {% endblock %} {% matomo_tracking_tags %} {% matomo_tag_manager settings.home.SiteSettings.mtm_container_id %} {% get_current_language_bidi as LANGUAGE_BIDI %} + -{% wagtailuserbar %} + {% wagtailuserbar %}
+ {% with page_=page %}
{% include "messages.html" %} @@ -94,59 +95,70 @@
{% include "footer.html" with page=page_ %} - {% endwith %} + {% endwith %} +
+ - - - -{# Global javascript #} - - -{% if jquery %} + {% endif %} + + + {% if jquery %} -{% else %} + {% else %} -{% endif %} - - - -{% block extra_js %}{% endblock %} + {% endif %} + + + + {% block extra_js %}{% endblock %} - + + \ No newline at end of file diff --git a/iogt/templates/header.html b/iogt/templates/header.html index 6b4b5de47..0656356e1 100644 --- a/iogt/templates/header.html +++ b/iogt/templates/header.html @@ -4,7 +4,7 @@ {% get_language_info for LANGUAGE_CODE as lang %} {% get_available_languages as LANGUAGES %} {% get_language_info_list for LANGUAGES as languages %} - +{% load notifications_tags %} + +{% if user.is_authenticated %} + +{% endif %} \ No newline at end of file diff --git a/iogt/templates/sw.js b/iogt/templates/sw.js index 544f68b57..76c3a2be2 100644 --- a/iogt/templates/sw.js +++ b/iogt/templates/sw.js @@ -1,5 +1,5 @@ -importScripts('../../static/js/workbox/workbox-v6.1.5/workbox-sw.js'); -importScripts('../../static/js/idb.js'); // Import IndexedDB helper +importScripts("../../static/js/workbox/workbox-v6.1.5/workbox-sw.js"); +importScripts("../../static/js/idb.js"); // Import IndexedDB helper const PRECACHE_ASSETS = [ '/', // your home page '/static/js/iogt.js', @@ -29,33 +29,100 @@ self.addEventListener('install', event => { ); }); +self.addEventListener("push", function (event) { + let data = {}; + + try { + if (event.data && event.data.json) { + // Try to parse JSON + data = event.data.json(); + } else if (event.data && event.data.text) { + // Fallback to text and wrap in object + const text = event.data.text(); + data = { body: text }; + } + } catch (e) { + console.warn("❌ Failed to parse push data", e); + data = { body: "You have a new message." }; + } + + const title = data.title || "New Notification"; + const options = { + body: data.body || "You have a new message.", + icon: + data.icon || "https://cdn-icons-png.flaticon.com/512/3119/3119338.png", + data: { + url: data.url || "/", + notification_id: data.notification_id || null, + }, + requireInteraction: true, + }; + + event.waitUntil(self.registration.showNotification(title, options)); +}); + // ✅ Activate Service Worker -self.addEventListener('activate', event => { - console.log("🚀 Service Worker Activated!"); - event.waitUntil(self.clients.claim()); +self.addEventListener("activate", (event) => { + event.waitUntil(self.clients.claim()); }); -// ✅ Handle Fetch Requests -self.addEventListener('fetch', event => { - const { request } = event; +self.addEventListener("notificationclick", function (event) { + // Optional: close the notification + event.notification.close(); - console.log("🔎 Fetch event triggered:", request.url, request.method); + const notificationData = event.notification.data || {}; + const targetUrl = notificationData.url || "/"; - // ✅ Handle POST Requests (Save to IndexedDB if offline) - if (request.method === 'POST') { - event.respondWith( - fetch(request.clone()).catch(async () => { - console.warn("⚠️ Offline - saving request locally", request.url); + // Track click via fetch to server + if (notificationData.notification_id) { + fetch(`/notifications/mark-clicked/${notificationData.notification_id}/`, { + method: "POST", + "X-CSRFToken": "{{ csrf_token }}", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }).catch((err) => + console.warn("❌ Failed to log notification click:", err) + ); + } - try { - await saveRequest(request); - console.log("💾 Request saved successfully:", request.url); + // Focus tab or open new one + event.waitUntil( + clients + .matchAll({ type: "window", includeUncontrolled: true }) + .then((clientList) => { + for (let client of clientList) { + if (client.url === targetUrl && "focus" in client) { + return client.focus(); + } + } + return clients.openWindow(targetUrl); + }) + ); +}); - if ('sync' in self.registration) { - self.registration.sync.register('sync-forms') - .then(() => console.log("🔄 Sync registered successfully!")) - .catch(err => console.error("❌ Sync registration failed:", err)); - } +// ✅ Handle Fetch Requests +self.addEventListener("fetch", (event) => { + const { request } = event; + + // ✅ Handle POST Requests (Save to IndexedDB if offline) + if (request.method === "POST") { + event.respondWith( + fetch(request.clone()).catch(async () => { + console.warn("⚠️ Offline - saving request locally", request.url); + + try { + await saveRequest(request); + + if ("sync" in self.registration) { + self.registration.sync + .register("sync-forms") + .then(() => console.log("🔄 Sync registered successfully!")) + .catch((err) => + console.error("❌ Sync registration failed:", err) + ); + } // ✅ Dynamically use referrer or fallback to home page const redirectUrl = request.referrer || '/'; @@ -142,20 +209,17 @@ self.addEventListener('fetch', event => { }); // ✅ Background Sync for Form Submissions -self.addEventListener('sync', event => { - if (event.tag === 'sync-forms') { - console.log("🔄 Sync event triggered!"); - event.waitUntil(syncRequests()); - } +self.addEventListener("sync", (event) => { + if (event.tag === "sync-forms") { + event.waitUntil(syncRequests()); + } }); // ✅ Function to Sync Requests from IndexedDB async function syncRequests() { - console.log("🚀 Syncing stored requests..."); - - const requests = await getAllRequests(); - for (const req of requests) { - console.log("📤 Syncing request:", req); + const requests = await getAllRequests(); + for (const req of requests) { + console.log("📤 Syncing request:", req); const fetchOptions = { method: req.method, @@ -164,29 +228,38 @@ async function syncRequests() { credentials: 'include' // Important for authentication }; - try { - const response = await fetch(req.url, fetchOptions); - - if (response.ok) { - console.log("✅ Sync successful, deleting request from IndexedDB..."); - await deleteRequest(req.id); - // ✅ Notify client about successful sync - sendMessageToClients({ type: 'sync-success', url: req.url }); - } else { - console.warn("⚠️ Sync failed with status:", response.status); - sendMessageToClients({ type: 'sync-failed', url: req.url, status: response.status }); - } - } catch (err) { - console.error("❌ Sync error:", err); - sendMessageToClients({ type: 'sync-error', url: req.url, error: err.message }); - } + try { + const response = await fetch(req.url, fetchOptions); + + if (response.ok) { + await deleteRequest(req.id); + // ✅ Notify client about successful sync + sendMessageToClients({ type: "sync-success", url: req.url }); + } else { + console.warn("⚠️ Sync failed with status:", response.status); + sendMessageToClients({ + type: "sync-failed", + url: req.url, + status: response.status, + }); + } + } catch (err) { + console.error("❌ Sync error:", err); + sendMessageToClients({ + type: "sync-error", + url: req.url, + error: err.message, + }); } + } } function sendMessageToClients(message) { - self.clients.matchAll({ includeUncontrolled: true, type: 'window' }).then(clients => { - clients.forEach(client => { - client.postMessage(message); - }); + self.clients + .matchAll({ includeUncontrolled: true, type: "window" }) + .then((clients) => { + clients.forEach((client) => { + client.postMessage(message); + }); }); -} \ No newline at end of file +} diff --git a/iogt/templates/wagtailadmin/chooser/external_link.html b/iogt/templates/wagtailadmin/chooser/external_link.html index e7c960acc..284b87d04 100644 --- a/iogt/templates/wagtailadmin/chooser/external_link.html +++ b/iogt/templates/wagtailadmin/chooser/external_link.html @@ -13,7 +13,7 @@ {% csrf_token %} diff --git a/iogt/templates/wagtailusers/users/create.html b/iogt/templates/wagtailusers/users/create.html index e7976a7b6..88c39d133 100644 --- a/iogt/templates/wagtailusers/users/create.html +++ b/iogt/templates/wagtailusers/users/create.html @@ -2,19 +2,19 @@ {% block fields %} {% if form.separate_username_field %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.username_field %} + {% include "wagtailadmin/shared/field.html" with field=form.username_field %} {% endif %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.email %} + {% include "wagtailadmin/shared/field.html" with field=form.email %} {% block extra_fields %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.display_name %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.first_name %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.last_name %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.terms_accepted %} + {% include "wagtailadmin/shared/field.html" with field=form.display_name %} + {% include "wagtailadmin/shared/field.html" with field=form.first_name %} + {% include "wagtailadmin/shared/field.html" with field=form.last_name %} + {% include "wagtailadmin/shared/field.html" with field=form.terms_accepted %} {% endblock extra_fields %} {% if form.password1 %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.password1 %} + {% include "wagtailadmin/shared/field.html" with field=form.password1 %} {% endif %} {% if form.password2 %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.password2 %} + {% include "wagtailadmin/shared/field.html" with field=form.password2 %} {% endif %} {% endblock fields %} diff --git a/iogt/templates/wagtailusers/users/edit.html b/iogt/templates/wagtailusers/users/edit.html index 3c099849a..2137dc775 100644 --- a/iogt/templates/wagtailusers/users/edit.html +++ b/iogt/templates/wagtailusers/users/edit.html @@ -2,23 +2,61 @@ {% block fields %} {% if form.separate_username_field %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.username_field %} + {% include "wagtailadmin/shared/field.html" with field=form.username_field %} {% endif %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.email %} + {% include "wagtailadmin/shared/field.html" with field=form.email %} {% block extra_fields %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.display_name %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.first_name %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.last_name %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.terms_accepted %} + {% include "wagtailadmin/shared/field.html" with field=form.display_name %} + {% include "wagtailadmin/shared/field.html" with field=form.first_name %} + {% include "wagtailadmin/shared/field.html" with field=form.last_name %} + {% include "wagtailadmin/shared/field.html" with field=form.terms_accepted %} +
+ + Notification Preference + +
+ {% if user.notificationpreference %} +
+ +
Opted in: + {% if user.notificationpreference.receive_notifications %} + Yes + {% else %} + No + {% endif %} +
+
+ Language: {{ user.notificationpreference.get_preferred_language_display }} +
+ {% if user.notificationpreference.content_tags.exists %} +
+ Tags: + {{ user.notificationpreference.content_tags.all|join:", " }} +
+ {% endif %} +
+ + Edit + +
+
+ {% else %} + No preferences found.
+ + Add. + + {% endif %} + +
+
{% endblock extra_fields %} {% if form.password1 %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.password1 %} + {% include "wagtailadmin/shared/field.html" with field=form.password1 %} {% endif %} {% if form.password2 %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.password2 %} + {% include "wagtailadmin/shared/field.html" with field=form.password2 %} {% endif %} {% if form.is_active %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.is_active %} + {% include "wagtailadmin/shared/field.html" with field=form.is_active %} {% endif %} - {% endblock fields %} \ No newline at end of file diff --git a/iogt/urls.py b/iogt/urls.py index 3abc9662e..a84484a5e 100644 --- a/iogt/urls.py +++ b/iogt/urls.py @@ -18,8 +18,6 @@ from wagtail_transfer import urls as wagtailtransfer_urls from admin_login import urls as admin_login_urls from admin_login.views import AzureADSignupView - - from iogt.views import ( TransitionPageView, SitemapAPIView, @@ -28,11 +26,12 @@ OfflineContentNotFoundPageView, CustomLogoutView, ) - +from wagtailautocomplete.urls.admin import urlpatterns as autocomplete_admin_urls api_url_patterns = [ path('api/v1/questionnaires/', include('questionnaires.api.v1.urls')), path('api/interactive/', include('interactive.api.urls')), + # path("notifications/save-preference/", save_notification_preference, name="save_notification_preference"), ] @@ -51,6 +50,7 @@ path('django-admin/', admin.site.urls), path('admin/logout/', CustomLogoutView.as_view(), name='admin_logout'), path('admin/login/', AzureADSignupView.as_view(), name='azure_signup_view'), # Override Wagtail admin login + re_path(r'^admin/autocomplete/', include(autocomplete_admin_urls)), path('admin/', include(wagtailadmin_urls)), path('documents/', include(wagtaildocs_urls)), path('admin-login/', include(admin_login_urls), name='admin_login_urls'), @@ -59,8 +59,6 @@ *i18n_patterns(path('users/', include(users_urls), name='users_urls')), *i18n_patterns(path('accounts/', include('allauth.urls'), name='allauth-urls')), *i18n_patterns(path('comments/', include('django_comments_xtd.urls'))), - #*i18n_patterns(path('admin-login/', include('admin_login.urls'))), - path( 'sw.js', pwa_views.ServiceWorkerView.as_view(), @@ -81,6 +79,8 @@ path('page-tree//', PageTreeAPIView.as_view(), name='page_tree'), path('api/docs/', schema_view.with_ui('swagger'), name='swagger'), path('webpush/subscribe/', save_info, name='save_webpush_info'), + path('inbox/notifications/', include('notifications.urls', namespace='notifications')), + path('notifications/', include('user_notifications.urls')), ] if settings.DEBUG: diff --git a/iogt/utils.py b/iogt/utils.py index 7c4fd9132..ac27597f8 100644 --- a/iogt/utils.py +++ b/iogt/utils.py @@ -1,5 +1,94 @@ +from django.contrib.admin.views.decorators import staff_member_required from django.conf import settings +from django.shortcuts import get_object_or_404, redirect +from wagtail.admin.action_menu import ActionMenuItem +from django.urls import path, reverse +from django.utils.html import format_html_join, format_html +from questionnaires.models import Survey +from home.models import Article, Section +from wagtail.models import Site, Page +from wagtail.admin import messages +from user_notifications.tasks import send_app_notifications +# from wagtail_modeladmin.helpers import AdminURLHelper def has_md5_hash(name): return bool(settings.HAS_MD5_HASH_REGEX.search(name)) + + +class NotifyAndPublishMenuItem(ActionMenuItem): + label = "Notify & Publish" + name = "notify_and_publish" + + def __init__(self, order=100, allowed_models=None): + super().__init__(order=order) + if allowed_models is None: + self.allowed_models = tuple() + else: + self.allowed_models = (allowed_models,) + + def is_shown(self, context): # ✅ Correct for Wagtail 3.2 + page = context.get("page") + return isinstance(getattr(page, "specific", page), self.allowed_models) + + def render_html(self, context): + page = context["page"] + url = reverse("notify_and_publish", args=[page.id]) + return format_html( + '' + ' {}' + '', + url, + self.label, + ) + + +# shared view +@staff_member_required +def notify_and_publish_view(request, page_id): + page = Page.objects.get(id=page_id).specific + latest_revision = page.get_latest_revision() + + if latest_revision: + latest_revision.publish() + else: + revision = page.save_revision(user=request.user) + revision.publish() + if isinstance(page, Survey): + full_url = get_site_for_locale(page) + send_app_notifications.delay(page.id, full_url, 'survey') + messages.success(request, f"Survey '{page.title}' published and notified.") + + elif isinstance(page, Article): + full_url = get_site_for_locale(page) + send_app_notifications.delay(page.id, full_url, 'article') + messages.success(request, f"Article '{page.title}' published and notified.") + + elif isinstance(page, Section): + full_url = get_site_for_locale(page) + send_app_notifications.delay(page.id, full_url, 'section') + messages.success(request, f"Section '{page.title}' published and notified.") + else: + messages.error(request, "Not a valid Survey or Article page.") + return redirect('wagtailadmin_explore', page.get_parent().id) + + # modeladmin_url = AdminURLHelper(type(page)) + # return redirect(modeladmin_url.index_url) + + +def get_site_for_locale(instance): + """ + Return the Wagtail Site object matching the given locale. + """ + for site in Site.objects.all(): + if site.root_page.locale.language_code == instance.locale.language_code: + if not site: + return + relative = instance.relative_url(site) + if not relative: + return + full_url = settings.WAGTAILADMIN_BASE_URL + relative + return full_url + return None \ No newline at end of file diff --git a/iogt_users/forms.py b/iogt_users/forms.py index d17ce539d..8266983c5 100644 --- a/iogt_users/forms.py +++ b/iogt_users/forms.py @@ -6,10 +6,14 @@ from django.utils.translation import gettext_lazy as _ from wagtail.users.forms import UserEditForm as WagtailUserEditForm, \ UserCreationForm as WagtailUserCreationForm +from user_notifications.models import UserNotificationTemplate +from user_notifications.tasks import send_app_notifications from .fields import IogtPasswordField from .models import User +from notifications.signals import notify + class AccountSignupForm(SignupForm): display_name = forms.CharField( @@ -44,6 +48,12 @@ def __init__(self, *args, **kwargs): if hasattr(self, "field_order"): set_form_field_order(self, self.field_order) + def save(self, request): + user = super().save(request) + # 🔁 Run this logic in background + send_app_notifications.delay(user.id, notification_type='signup') + return user + def clean_username(self): username = self.cleaned_data.get('username') if User.objects.filter(username__iexact=username): @@ -96,6 +106,9 @@ def clean_displayname(self): if User.objects.filter(display_name__iexact=display_name): raise ValidationError(_('Display name not available.')) return display_name + class Meta(WagtailUserCreationForm.Meta): + model = User + fields = WagtailUserCreationForm.Meta.fields | {'first_name', 'last_name', 'username', 'display_name', 'terms_accepted', 'groups'} class WagtailAdminUserEditForm(WagtailUserEditForm): @@ -105,3 +118,7 @@ class WagtailAdminUserEditForm(WagtailUserEditForm): last_name = forms.CharField(required=False, label='Last Name') terms_accepted = forms.BooleanField(label=_('I accept the Terms and Conditions.')) + + class Meta(WagtailUserEditForm.Meta): + model = User + fields = WagtailUserEditForm.Meta.fields | {'first_name', 'last_name', 'username', 'display_name', 'terms_accepted', 'groups'} \ No newline at end of file diff --git a/iogt_users/models.py b/iogt_users/models.py index 801eee996..9423d91ec 100644 --- a/iogt_users/models.py +++ b/iogt_users/models.py @@ -22,7 +22,12 @@ class User(AbstractUser): has_viewed_registration_survey = models.BooleanField(default=False) interactive_uuid = models.CharField(max_length=255, null=True, blank=True) + + autocomplete_search_field = 'username' + def autocomplete_label(self): + return self.username + @property def is_rapidpro_bot_user(self): return self.groups.filter(name=settings.RAPIDPRO_BOT_GROUP_NAME).exists() @@ -59,6 +64,13 @@ def record_article_read(cls, request, article): def get_display_name(self): return self.display_name or self.username + def notification_opt_in(self): + try: + pref = self.notificationpreference + return True if pref.receive_notifications else False + except Exception: + return False # no preference set + class Meta: ordering = ('id',) diff --git a/iogt_users/templates/iogt_users/wagtailusers/users/index.html b/iogt_users/templates/iogt_users/wagtailusers/users/index.html new file mode 100644 index 000000000..753cc99f1 --- /dev/null +++ b/iogt_users/templates/iogt_users/wagtailusers/users/index.html @@ -0,0 +1,286 @@ +{% extends "wagtailusers/users/index.html" %} +{% load wagtailadmin_tags i18n %} + +{% block content %} + {% trans "Users" as users_str %} + + {% include "wagtailadmin/shared/header.html" with subtitle=group.name title=users_str action_url=add_link action_text=add_a_user_str icon="user" search_url="wagtailusers_users:index" %} +
+
+ + + + Add a user + +
+
+
+
+ {% include "wagtailadmin/generic/index_results.html" %} + +
+ {% trans "Select all users in listing" as select_all_text %} + {% include 'wagtailadmin/bulk_actions/footer.html' with select_all_obj_text=select_all_text app_label=app_label model_name=model_name objects=users %} +
+ + + + +{% endblock %} + +{% block extra_js %} + {{ block.super }} + + + + + +{% endblock %} \ No newline at end of file diff --git a/iogt_users/templates/modeladmin/iogt_users/user/index.html b/iogt_users/templates/modeladmin/iogt_users/user/index.html index 0288b45a0..963c83a42 100644 --- a/iogt_users/templates/modeladmin/iogt_users/user/index.html +++ b/iogt_users/templates/modeladmin/iogt_users/user/index.html @@ -17,94 +17,116 @@ {% block header %}
-
+
- {% block h1 %}

{{ view.get_page_title }}

{% endblock %} + {% block h1 %} +

+ {{ view.get_page_title }} +

+ {% endblock %}
- {% block search %}{% search_form %}{% endblock %} + {% block search %} + {% search_form %} + {% endblock %}
+ {% block header_extra %} -
+
{% if user_can_create %}
{% include 'modeladmin/includes/button.html' with button=view.button_helper.add_button %}
{% endif %} - + + {% if view.list_export %} - {% endblock %}
- {% with total=paginator.count %} - {{ total }} User{{ total|pluralize }} - {% endwith %} + + {% with total=paginator.count %} + {{ total }} User{{ total|pluralize }} + {% endwith %}
{% endblock %} {% block content_main %} -
+
- {% block content_cols %} - - {% block filters %} - {% if view.has_filters and all_count %} -
-

{% translate 'Filter' %}

- {% for spec in view.filter_specs %}{% admin_list_filter view spec %}{% endfor %} -
+
+ {% block result_list %} + {% if not all_count %} +
+ {% if no_valid_parents %} +

{% blocktranslate trimmed with view.verbose_name_plural as name %} + No {{ name }} have been created yet. One of the following must be created before you can add any {{ name }}: + {% endblocktranslate %}

+
    + {% for type in required_parent_types %} +
  • {{ type|title }}
  • + {% endfor %} +
+ {% else %} +

{% blocktranslate trimmed with view.verbose_name_plural as name %} + No {{ name }} have been created yet. + {% endblocktranslate %} + {% if user_can_create %} + {% blocktranslate trimmed with view.create_url as url %} + Why not add one? + {% endblocktranslate %} + {% endif %}

+ {% endif %} +
+ {% else %} + {% result_list %} {% endif %} {% endblock %} - -
- {% block result_list %} - {% if not all_count %} -
- {% if no_valid_parents %} -

{% blocktranslate trimmed with view.verbose_name_plural as name %}No {{ name }} have been created yet. One of the following must be created before you can add any {{ name }}:{% endblocktranslate %}

-
    - {% for type in required_parent_types %}
  • {{ type|title }}
  • {% endfor %} -
- {% else %} -

{% blocktranslate trimmed with view.verbose_name_plural as name %}No {{ name }} have been created yet.{% endblocktranslate %} - {% if user_can_create %} - {% blocktranslate trimmed with view.create_url as url %} - Why not add one? - {% endblocktranslate %} - {% endif %}

- {% endif %} -
- {% else %} - {% result_list %} - {% endif %} - {% endblock %} +
+ {% block filters %} + {% if view.has_filters and all_count %} +
+
+

{% translate 'Filter' %}

+ {% for spec in view.filter_specs %} + {% admin_list_filter view spec %} + {% endfor %} +
- - {% block pagination %} - - {% endblock %} - - {% endblock %} + {% endif %}
+ {% endblock %} + + {% block pagination %} + + {% endblock %}
{% endblock %} - {% endblock %} diff --git a/iogt_users/templates/profile.html b/iogt_users/templates/profile.html index 21419b40f..38d42e375 100644 --- a/iogt_users/templates/profile.html +++ b/iogt_users/templates/profile.html @@ -17,12 +17,15 @@

{% endblocktranslate %} {{ user.username | first | upper }}

- {% endif %} + {% endif %} + {# Show notification banner only if preference is not yet set #}
+ {% url 'notification_settings' as notify_url %} + {% primary_button title='Notification Preference' href=notify_url icon_path='icons/bell-solid.svg' extra_classnames='notification-pref-btn' %} {% url 'account_change_password' as change_password_url %} {% primary_button title='Change Digital Pin' href=change_password_url icon_path='icons/lock.svg' extra_classnames='change-digital-pin' %} - {% url 'account_logout' as logout_url %} {% primary_button title='Log out' href=logout_url icon_path='icons/arrow_icon_left.svg' extra_classnames='logout-btn' %}
-{% endblock %} + +{% endblock %} \ No newline at end of file diff --git a/iogt_users/templates/user_notification.html b/iogt_users/templates/user_notification.html new file mode 100644 index 000000000..d39dc9fdc --- /dev/null +++ b/iogt_users/templates/user_notification.html @@ -0,0 +1,204 @@ +{% extends 'account/base.html' %} +{% load i18n static image_tags generic_components %} + +{% block title %}{% translate "Notification Preference" %}{% endblock %} + + +{% block content %} +{% if request.user.is_authenticated %} +

{% translate "Notification Preference" %}

+
+ {% translate "Do you want to receive notifications?" %} +
+
+
+
+
+ +
+ +
+ + +
+ + +
+
+ +
+ +{% endif %} + + + +{% endblock %} \ No newline at end of file diff --git a/iogt_users/templates/wagtailusers/users/index.html b/iogt_users/templates/wagtailusers/users/index.html index 2e54a9cc6..addaf1cfb 100644 --- a/iogt_users/templates/wagtailusers/users/index.html +++ b/iogt_users/templates/wagtailusers/users/index.html @@ -1,249 +1,191 @@ {% extends "wagtailusers/users/index.html" %} {% load wagtailadmin_tags i18n %} + {% block content %} - {% trans "Users" as users_str %} - - {% include "wagtailadmin/shared/header.html" with subtitle=group.name title=users_str action_url=add_link action_text=add_a_user_str icon="user" search_url="wagtailusers_users:index" %} -
-
- - - - Add a user - -
-
-
-
- {% include "wagtailusers/users/results.html" %} +{{ block.super }} +