django-whiteneuron 0.2.41__tar.gz → 0.2.44__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/PKG-INFO +35 -4
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/README.md +34 -3
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/django_whiteneuron.egg-info/PKG-INFO +35 -4
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/pyproject.toml +1 -1
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/modeladmin.py +60 -17
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/utils.py +6 -2
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/admin.py +39 -26
- django_whiteneuron-0.2.44/whiteneuron/feedbacks/urls.py +81 -0
- django_whiteneuron-0.2.44/whiteneuron/feedbacks/utils.py +10 -0
- django_whiteneuron-0.2.44/whiteneuron/locale/vi/LC_MESSAGES/django.mo +0 -0
- django_whiteneuron-0.2.44/whiteneuron/locale/vi/LC_MESSAGES/django.po +1423 -0
- django_whiteneuron-0.2.44/whiteneuron/static/base/css/styles.css +2 -0
- django_whiteneuron-0.2.44/whiteneuron/templates/admin/change_list.html +145 -0
- django_whiteneuron-0.2.44/whiteneuron/templates/admin/feedbacks/feedback_change_form.html +132 -0
- django_whiteneuron-0.2.44/whiteneuron/templates/admin/submit_line.html +157 -0
- django_whiteneuron-0.2.41/whiteneuron/feedbacks/urls.py +0 -69
- django_whiteneuron-0.2.41/whiteneuron/feedbacks/utils.py +0 -5
- django_whiteneuron-0.2.41/whiteneuron/locale/vi/LC_MESSAGES/django.mo +0 -0
- django_whiteneuron-0.2.41/whiteneuron/locale/vi/LC_MESSAGES/django.po +0 -1182
- django_whiteneuron-0.2.41/whiteneuron/static/base/css/styles.css +0 -2
- django_whiteneuron-0.2.41/whiteneuron/templates/admin/change_list.html +0 -59
- django_whiteneuron-0.2.41/whiteneuron/templates/admin/feedbacks/feedback_change_form.html +0 -41
- django_whiteneuron-0.2.41/whiteneuron/templates/admin/submit_line.html +0 -67
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/LICENSE +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/django_whiteneuron.egg-info/SOURCES.txt +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/django_whiteneuron.egg-info/dependency_links.txt +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/django_whiteneuron.egg-info/requires.txt +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/django_whiteneuron.egg-info/top_level.txt +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/setup.cfg +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/admin.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/apps.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/asgi.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/ckeditor.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/filters.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/forms.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/management/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/management/commands/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/management/commands/init_admin.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/management/commands/init_groupuser.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/management/commands/init_guest.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/middleware.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0001_initial.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0002_imagemodel_userprofile_user_avatar_user_biography_and_more.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0003_notification_obj_link.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0004_delete_notification.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0005_user_show_softdelete.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0006_user_is_bot.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0007_app.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0008_app_created_at_app_created_by_app_deleted_at_and_more.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0009_alter_app_table.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0010_app_category.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0011_alter_app_options.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0012_app_permission.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0013_app_thumbnail_url.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0014_alter_app_created_at_alter_app_updated_at_and_more.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0015_ipblacklist.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0016_uablacklist.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0017_alter_uablacklist_pattern.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/models.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/resources.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/settings.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/sites.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/tasks.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/templatetags/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/templatetags/unfold_list_custom.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/templatetags/wn_filters.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/tests.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/thread_local.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/urls.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/views.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/widgets.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_import_export/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_import_export/admin.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_import_export/constants.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_import_export/mixins.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_import_export/tasks.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_import_export/utils.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_modeltranslation/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_modeltranslation/admin.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_modeltranslation/utils.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/dashboard/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/dashboard/admin.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/dashboard/apps.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/dashboard/migrations/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/dashboard/models.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/dashboard/tests.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/dashboard/views.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/apps.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/migrations/0001_initial.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/migrations/0002_feedbackdata_created_by_feedbackdata_deleted_at_and_more.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/migrations/0003_feedbackdata_note.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/migrations/0004_alter_feedbackdata_object_id.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/migrations/0005_feedbackdata_field.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/migrations/0006_alter_feedbackdata_created_at_and_more.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/migrations/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/models.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/tests.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/views.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/admin.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/apps.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/migrations/0001_initial.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/migrations/0002_alter_excelfile_options_alter_pdffile_options_and_more.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/migrations/0003_alter_excelfile_created_at_and_more.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/migrations/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/models.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/tests.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/utils.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/views.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/manage.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/admin.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/apps.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/consumers.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/management/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/management/commands/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/management/commands/call_demo_noti.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0001_initial.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0002_alter_notificationconfig_event_type.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0003_notification.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0004_alter_notification_flag.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0005_notification_keywords.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0006_remove_notification_keywords.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0007_notification_changed_data.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0008_alter_notification_changed_data.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0009_notification_action_by_alter_notification_user.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0010_alter_notification_user.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0011_alter_notification_user.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0012_alter_notification_action_and_more.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/models.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/routing.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/tests.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/utils.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/views.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/css/btn-styles.css +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/css/ckeditor5.css +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/css/loading.css +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/forms/css/html-editor.css +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/forms/js/format.html.js +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/forms/js/trix.config.js +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/images/delete.svg +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/images/edit.svg +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/images/icon-viewlink.svg +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/images/loading.gif +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/images/login-bg.jpg +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/images/login_bg.jpg +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/images/logo/WhiteNeuron.png +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/js/chart.min.js +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/js/d3.v7.min.js +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/js/loading.js +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/js/whiteneuron.js +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/synchronization/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/synchronization/admin.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/synchronization/apps.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/synchronization/migrations/__init__.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/synchronization/models.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/synchronization/tests.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/synchronization/views.py +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/400.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/403.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/404.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/429.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/500.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/auth/user/add_form.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/base/app_change_list.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/base/image_change_form.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/base/userprofile_change_form.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/base.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/grid_view_results.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/header.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/index.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/login.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/login_after.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/search_form.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/base/forms/helpers/toolbar.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/base/forms/wysiwyg.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/base.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/unfold/helpers/account_links.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/unfold/helpers/app_list_badge.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/unfold/helpers/field_password.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/unfold/helpers/navigation_user.html +0 -0
- {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/unfold/helpers/pagination.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-whiteneuron
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.44
|
|
4
4
|
Summary: A modern Django Admin extension with enhanced UI/UX, dashboard, feedback, file management, rate limiting, IP blacklisting, and advanced admin integrations built on django-unfold.
|
|
5
5
|
Author-email: White Neuron <anhnt@whiteneuron.com>
|
|
6
6
|
License: MIT License
|
|
@@ -90,11 +90,11 @@ A modern Django Admin extension focused on UI/UX, dashboard, feedback, file mana
|
|
|
90
90
|
[](LICENSE)
|
|
91
91
|
[](https://pypi.org/project/django-whiteneuron/)
|
|
92
92
|
|
|
93
|
-

|
|
93
|
+

|
|
94
94
|
|
|
95
95
|
## Current Version
|
|
96
96
|
|
|
97
|
-
- 0.2.
|
|
97
|
+
- 0.2.44
|
|
98
98
|
|
|
99
99
|
## Compatibility
|
|
100
100
|
|
|
@@ -105,7 +105,38 @@ A modern Django Admin extension focused on UI/UX, dashboard, feedback, file mana
|
|
|
105
105
|
|
|
106
106
|
## Changelog
|
|
107
107
|
|
|
108
|
-
### v0.2.
|
|
108
|
+
### v0.2.44 (2026-04-02) — latest
|
|
109
|
+
**Fixes & Improvements: Feedback System — Error handling, input limits, i18n lazy evaluation**
|
|
110
|
+
- **Fixed**: `changeform_view` now returns `JsonResponse({"success": True})` after a successful resolve instead of falling through to `super()` with a POST request — prevents a spurious HTTP 403 caused by `has_change_permission = False`.
|
|
111
|
+
- **Fixed**: `note` is read from `request.POST` instead of `request.GET` — prevents HTTP 414 (URI Too Long) when the note is lengthy.
|
|
112
|
+
- **Fixed**: JS `fetch` upgraded to POST + `FormData` with CSRF token; error handling now maps status codes (403/404/500/network) to specific user-facing messages.
|
|
113
|
+
- **Added**: Two-sided character limits — `maxlength` attribute + live counter on client; server-side validation: `note ≤ 500`, `feedback_message ≤ 2000`.
|
|
114
|
+
- **Added**: `get_short_message()` in `FeedbackDataAdmin.list_display` — truncates to 200 words via Django's `Truncator`.
|
|
115
|
+
- **Fixed**: `search_help_text` moved from `__init__` to a `@property` using `format_lazy` — now re-evaluates per request and responds correctly to language switches instead of being frozen at server startup.
|
|
116
|
+
|
|
117
|
+
### v0.2.43 (2026-04-01)
|
|
118
|
+
**Feature: Feedback System — DaisyUI modals, anti-spam cooldown, i18n, security hardening**
|
|
119
|
+
- **Added**: DaisyUI `<dialog>` modals replace native `prompt()`/`alert()` in both `feedback_change_form.html` (Mark as Resolved flow) and `submit_line.html` (Feedback submission) — smooth animations, no browser dialogs.
|
|
120
|
+
- **Added**: Server-side anti-spam cooldown — 60s per user globally; HTTP 429 with dynamically calculated remaining seconds (`remaining = 60 - elapsed`).
|
|
121
|
+
- **Security**: Replaced `@csrf_exempt` with `@require_POST` in feedback endpoint — CSRF protection fully enforced via `X-CSRFToken` header.
|
|
122
|
+
- **Security**: Auth check moved before body parse; removed redundant `User.objects.get()` in favor of `request.user`.
|
|
123
|
+
- **Fixed**: `FeedbackDataAdmin.changeform_view()` — null guard added after `get_object()` to prevent `AttributeError` when object does not exist.
|
|
124
|
+
- **Fixed**: OK button in result modal now wrapped in `<form method="dialog">` — dialog closes correctly, `close` event fires `location.reload()`.
|
|
125
|
+
- **Fixed**: Buttons missing `type="button"` were inadvertently submitting the admin form — fixed across all modals.
|
|
126
|
+
- **Fixed**: Modals moved to `document.body` via JS — eliminates content flash at page bottom during closing animation.
|
|
127
|
+
- **Fixed**: Import cleanup in `feedbacks/admin.py` — removed unused `display` import, moved `FEEDBACK_COOLDOWN_SECONDS` import to top.
|
|
128
|
+
- **Added**: Full i18n (`{% trans %}`, `{% blocktrans %}`, `gettext_lazy`) across feedback templates and `modeladmin.py` (12 strings); Vietnamese translations updated in `locale/vi`.
|
|
129
|
+
- **Added**: `feedback_cooldown_ms` passed from server via `FeedbackBaseAdmin.render_change_form()` — no more hardcoded 60000ms in template.
|
|
130
|
+
|
|
131
|
+
### v0.2.42 (2026-03-31)
|
|
132
|
+
**Improve: persist filter/search state in admin changelist + fix Django duplicate filter bug**
|
|
133
|
+
- **Added**: Filter/search state is now persisted via `sessionStorage` across navigation — selecting a filter, navigating to a detail page, then returning restores the exact filter state automatically.
|
|
134
|
+
- **Fixed**: Django bug — `add_preserved_filters()` uses `dict(parse_qsl())` which drops duplicate filter params (e.g. `?chapter__id__exact=29&chapter__id__exact=31` → only `=31` kept after save). Fixed server-side in `ModelAdmin._fix_preserved_filters()` — rebuilds the redirect URL with `parse_qsl()` (preserves all values) when duplicates are detected.
|
|
135
|
+
- **Fixed**: `_changelist_filters` URL param (Django's format for preserved filters when navigating from change form) — decoded to individual params before saving to storage, then the URL is redirected to clean form (no ugly `_changelist_filters=...` in browser bar).
|
|
136
|
+
- **Fixed**: README image broken on PyPI — changed from relative path `docs/images/main.png` to absolute GitHub raw URL.
|
|
137
|
+
- **Improved**: Single redirect point in changelist JS — eliminated multiple chained `window.location.replace()` calls; at most one redirect per page load.
|
|
138
|
+
|
|
139
|
+
### v0.2.41 (2026-03-31)
|
|
109
140
|
**Fix: App dashboard menu not syncing after SIDEBAR changes**
|
|
110
141
|
- **Fixed**: `init_app_db()` used `cache.set(..., timeout=None)` (permanent cache) — after deploy/SIDEBAR changes, the App DB was never re-synced, causing dashboard menu to show stale data.
|
|
111
142
|
- **Fixed**: Cache TTL changed to 300s (5 minutes) — SIDEBAR changes now propagate automatically within 5 minutes without manual cache clearing.
|
|
@@ -7,11 +7,11 @@ A modern Django Admin extension focused on UI/UX, dashboard, feedback, file mana
|
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
[](https://pypi.org/project/django-whiteneuron/)
|
|
9
9
|
|
|
10
|
-

|
|
10
|
+

|
|
11
11
|
|
|
12
12
|
## Current Version
|
|
13
13
|
|
|
14
|
-
- 0.2.
|
|
14
|
+
- 0.2.44
|
|
15
15
|
|
|
16
16
|
## Compatibility
|
|
17
17
|
|
|
@@ -22,7 +22,38 @@ A modern Django Admin extension focused on UI/UX, dashboard, feedback, file mana
|
|
|
22
22
|
|
|
23
23
|
## Changelog
|
|
24
24
|
|
|
25
|
-
### v0.2.
|
|
25
|
+
### v0.2.44 (2026-04-02) — latest
|
|
26
|
+
**Fixes & Improvements: Feedback System — Error handling, input limits, i18n lazy evaluation**
|
|
27
|
+
- **Fixed**: `changeform_view` now returns `JsonResponse({"success": True})` after a successful resolve instead of falling through to `super()` with a POST request — prevents a spurious HTTP 403 caused by `has_change_permission = False`.
|
|
28
|
+
- **Fixed**: `note` is read from `request.POST` instead of `request.GET` — prevents HTTP 414 (URI Too Long) when the note is lengthy.
|
|
29
|
+
- **Fixed**: JS `fetch` upgraded to POST + `FormData` with CSRF token; error handling now maps status codes (403/404/500/network) to specific user-facing messages.
|
|
30
|
+
- **Added**: Two-sided character limits — `maxlength` attribute + live counter on client; server-side validation: `note ≤ 500`, `feedback_message ≤ 2000`.
|
|
31
|
+
- **Added**: `get_short_message()` in `FeedbackDataAdmin.list_display` — truncates to 200 words via Django's `Truncator`.
|
|
32
|
+
- **Fixed**: `search_help_text` moved from `__init__` to a `@property` using `format_lazy` — now re-evaluates per request and responds correctly to language switches instead of being frozen at server startup.
|
|
33
|
+
|
|
34
|
+
### v0.2.43 (2026-04-01)
|
|
35
|
+
**Feature: Feedback System — DaisyUI modals, anti-spam cooldown, i18n, security hardening**
|
|
36
|
+
- **Added**: DaisyUI `<dialog>` modals replace native `prompt()`/`alert()` in both `feedback_change_form.html` (Mark as Resolved flow) and `submit_line.html` (Feedback submission) — smooth animations, no browser dialogs.
|
|
37
|
+
- **Added**: Server-side anti-spam cooldown — 60s per user globally; HTTP 429 with dynamically calculated remaining seconds (`remaining = 60 - elapsed`).
|
|
38
|
+
- **Security**: Replaced `@csrf_exempt` with `@require_POST` in feedback endpoint — CSRF protection fully enforced via `X-CSRFToken` header.
|
|
39
|
+
- **Security**: Auth check moved before body parse; removed redundant `User.objects.get()` in favor of `request.user`.
|
|
40
|
+
- **Fixed**: `FeedbackDataAdmin.changeform_view()` — null guard added after `get_object()` to prevent `AttributeError` when object does not exist.
|
|
41
|
+
- **Fixed**: OK button in result modal now wrapped in `<form method="dialog">` — dialog closes correctly, `close` event fires `location.reload()`.
|
|
42
|
+
- **Fixed**: Buttons missing `type="button"` were inadvertently submitting the admin form — fixed across all modals.
|
|
43
|
+
- **Fixed**: Modals moved to `document.body` via JS — eliminates content flash at page bottom during closing animation.
|
|
44
|
+
- **Fixed**: Import cleanup in `feedbacks/admin.py` — removed unused `display` import, moved `FEEDBACK_COOLDOWN_SECONDS` import to top.
|
|
45
|
+
- **Added**: Full i18n (`{% trans %}`, `{% blocktrans %}`, `gettext_lazy`) across feedback templates and `modeladmin.py` (12 strings); Vietnamese translations updated in `locale/vi`.
|
|
46
|
+
- **Added**: `feedback_cooldown_ms` passed from server via `FeedbackBaseAdmin.render_change_form()` — no more hardcoded 60000ms in template.
|
|
47
|
+
|
|
48
|
+
### v0.2.42 (2026-03-31)
|
|
49
|
+
**Improve: persist filter/search state in admin changelist + fix Django duplicate filter bug**
|
|
50
|
+
- **Added**: Filter/search state is now persisted via `sessionStorage` across navigation — selecting a filter, navigating to a detail page, then returning restores the exact filter state automatically.
|
|
51
|
+
- **Fixed**: Django bug — `add_preserved_filters()` uses `dict(parse_qsl())` which drops duplicate filter params (e.g. `?chapter__id__exact=29&chapter__id__exact=31` → only `=31` kept after save). Fixed server-side in `ModelAdmin._fix_preserved_filters()` — rebuilds the redirect URL with `parse_qsl()` (preserves all values) when duplicates are detected.
|
|
52
|
+
- **Fixed**: `_changelist_filters` URL param (Django's format for preserved filters when navigating from change form) — decoded to individual params before saving to storage, then the URL is redirected to clean form (no ugly `_changelist_filters=...` in browser bar).
|
|
53
|
+
- **Fixed**: README image broken on PyPI — changed from relative path `docs/images/main.png` to absolute GitHub raw URL.
|
|
54
|
+
- **Improved**: Single redirect point in changelist JS — eliminated multiple chained `window.location.replace()` calls; at most one redirect per page load.
|
|
55
|
+
|
|
56
|
+
### v0.2.41 (2026-03-31)
|
|
26
57
|
**Fix: App dashboard menu not syncing after SIDEBAR changes**
|
|
27
58
|
- **Fixed**: `init_app_db()` used `cache.set(..., timeout=None)` (permanent cache) — after deploy/SIDEBAR changes, the App DB was never re-synced, causing dashboard menu to show stale data.
|
|
28
59
|
- **Fixed**: Cache TTL changed to 300s (5 minutes) — SIDEBAR changes now propagate automatically within 5 minutes without manual cache clearing.
|
{django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/django_whiteneuron.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-whiteneuron
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.44
|
|
4
4
|
Summary: A modern Django Admin extension with enhanced UI/UX, dashboard, feedback, file management, rate limiting, IP blacklisting, and advanced admin integrations built on django-unfold.
|
|
5
5
|
Author-email: White Neuron <anhnt@whiteneuron.com>
|
|
6
6
|
License: MIT License
|
|
@@ -90,11 +90,11 @@ A modern Django Admin extension focused on UI/UX, dashboard, feedback, file mana
|
|
|
90
90
|
[](LICENSE)
|
|
91
91
|
[](https://pypi.org/project/django-whiteneuron/)
|
|
92
92
|
|
|
93
|
-

|
|
93
|
+

|
|
94
94
|
|
|
95
95
|
## Current Version
|
|
96
96
|
|
|
97
|
-
- 0.2.
|
|
97
|
+
- 0.2.44
|
|
98
98
|
|
|
99
99
|
## Compatibility
|
|
100
100
|
|
|
@@ -105,7 +105,38 @@ A modern Django Admin extension focused on UI/UX, dashboard, feedback, file mana
|
|
|
105
105
|
|
|
106
106
|
## Changelog
|
|
107
107
|
|
|
108
|
-
### v0.2.
|
|
108
|
+
### v0.2.44 (2026-04-02) — latest
|
|
109
|
+
**Fixes & Improvements: Feedback System — Error handling, input limits, i18n lazy evaluation**
|
|
110
|
+
- **Fixed**: `changeform_view` now returns `JsonResponse({"success": True})` after a successful resolve instead of falling through to `super()` with a POST request — prevents a spurious HTTP 403 caused by `has_change_permission = False`.
|
|
111
|
+
- **Fixed**: `note` is read from `request.POST` instead of `request.GET` — prevents HTTP 414 (URI Too Long) when the note is lengthy.
|
|
112
|
+
- **Fixed**: JS `fetch` upgraded to POST + `FormData` with CSRF token; error handling now maps status codes (403/404/500/network) to specific user-facing messages.
|
|
113
|
+
- **Added**: Two-sided character limits — `maxlength` attribute + live counter on client; server-side validation: `note ≤ 500`, `feedback_message ≤ 2000`.
|
|
114
|
+
- **Added**: `get_short_message()` in `FeedbackDataAdmin.list_display` — truncates to 200 words via Django's `Truncator`.
|
|
115
|
+
- **Fixed**: `search_help_text` moved from `__init__` to a `@property` using `format_lazy` — now re-evaluates per request and responds correctly to language switches instead of being frozen at server startup.
|
|
116
|
+
|
|
117
|
+
### v0.2.43 (2026-04-01)
|
|
118
|
+
**Feature: Feedback System — DaisyUI modals, anti-spam cooldown, i18n, security hardening**
|
|
119
|
+
- **Added**: DaisyUI `<dialog>` modals replace native `prompt()`/`alert()` in both `feedback_change_form.html` (Mark as Resolved flow) and `submit_line.html` (Feedback submission) — smooth animations, no browser dialogs.
|
|
120
|
+
- **Added**: Server-side anti-spam cooldown — 60s per user globally; HTTP 429 with dynamically calculated remaining seconds (`remaining = 60 - elapsed`).
|
|
121
|
+
- **Security**: Replaced `@csrf_exempt` with `@require_POST` in feedback endpoint — CSRF protection fully enforced via `X-CSRFToken` header.
|
|
122
|
+
- **Security**: Auth check moved before body parse; removed redundant `User.objects.get()` in favor of `request.user`.
|
|
123
|
+
- **Fixed**: `FeedbackDataAdmin.changeform_view()` — null guard added after `get_object()` to prevent `AttributeError` when object does not exist.
|
|
124
|
+
- **Fixed**: OK button in result modal now wrapped in `<form method="dialog">` — dialog closes correctly, `close` event fires `location.reload()`.
|
|
125
|
+
- **Fixed**: Buttons missing `type="button"` were inadvertently submitting the admin form — fixed across all modals.
|
|
126
|
+
- **Fixed**: Modals moved to `document.body` via JS — eliminates content flash at page bottom during closing animation.
|
|
127
|
+
- **Fixed**: Import cleanup in `feedbacks/admin.py` — removed unused `display` import, moved `FEEDBACK_COOLDOWN_SECONDS` import to top.
|
|
128
|
+
- **Added**: Full i18n (`{% trans %}`, `{% blocktrans %}`, `gettext_lazy`) across feedback templates and `modeladmin.py` (12 strings); Vietnamese translations updated in `locale/vi`.
|
|
129
|
+
- **Added**: `feedback_cooldown_ms` passed from server via `FeedbackBaseAdmin.render_change_form()` — no more hardcoded 60000ms in template.
|
|
130
|
+
|
|
131
|
+
### v0.2.42 (2026-03-31)
|
|
132
|
+
**Improve: persist filter/search state in admin changelist + fix Django duplicate filter bug**
|
|
133
|
+
- **Added**: Filter/search state is now persisted via `sessionStorage` across navigation — selecting a filter, navigating to a detail page, then returning restores the exact filter state automatically.
|
|
134
|
+
- **Fixed**: Django bug — `add_preserved_filters()` uses `dict(parse_qsl())` which drops duplicate filter params (e.g. `?chapter__id__exact=29&chapter__id__exact=31` → only `=31` kept after save). Fixed server-side in `ModelAdmin._fix_preserved_filters()` — rebuilds the redirect URL with `parse_qsl()` (preserves all values) when duplicates are detected.
|
|
135
|
+
- **Fixed**: `_changelist_filters` URL param (Django's format for preserved filters when navigating from change form) — decoded to individual params before saving to storage, then the URL is redirected to clean form (no ugly `_changelist_filters=...` in browser bar).
|
|
136
|
+
- **Fixed**: README image broken on PyPI — changed from relative path `docs/images/main.png` to absolute GitHub raw URL.
|
|
137
|
+
- **Improved**: Single redirect point in changelist JS — eliminated multiple chained `window.location.replace()` calls; at most one redirect per page load.
|
|
138
|
+
|
|
139
|
+
### v0.2.41 (2026-03-31)
|
|
109
140
|
**Fix: App dashboard menu not syncing after SIDEBAR changes**
|
|
110
141
|
- **Fixed**: `init_app_db()` used `cache.set(..., timeout=None)` (permanent cache) — after deploy/SIDEBAR changes, the App DB was never re-synced, causing dashboard menu to show stale data.
|
|
111
142
|
- **Fixed**: Cache TTL changed to 300s (5 minutes) — SIDEBAR changes now propagate automatically within 5 minutes without manual cache clearing.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "django-whiteneuron"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.44"
|
|
4
4
|
description = "A modern Django Admin extension with enhanced UI/UX, dashboard, feedback, file management, rate limiting, IP blacklisting, and advanced admin integrations built on django-unfold."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "White Neuron", email = "anhnt@whiteneuron.com"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from unfold.admin import ModelAdmin as UnfoldAdmin
|
|
2
2
|
|
|
3
|
-
from django.http import HttpRequest
|
|
3
|
+
from django.http import HttpRequest, HttpResponseRedirect
|
|
4
|
+
from django.http.response import HttpResponseRedirectBase
|
|
4
5
|
from django.db.models.query import QuerySet
|
|
6
|
+
from urllib.parse import parse_qsl, urlencode, urlparse
|
|
5
7
|
from django.utils.safestring import mark_safe
|
|
6
8
|
from django.contrib.admin.views.main import ChangeList
|
|
7
9
|
from collections import OrderedDict
|
|
@@ -22,6 +24,9 @@ from .filters import FieldSelectionFilter
|
|
|
22
24
|
from django.urls import path
|
|
23
25
|
from django.shortcuts import render
|
|
24
26
|
|
|
27
|
+
from django.utils.translation import gettext_lazy as _
|
|
28
|
+
from django.utils.text import format_lazy
|
|
29
|
+
|
|
25
30
|
from unfold.contrib.filters.admin import (
|
|
26
31
|
AutocompleteSelectFilter,
|
|
27
32
|
AutocompleteSelectMultipleFilter,
|
|
@@ -76,9 +81,13 @@ class ModelAdmin(UnfoldAdmin):
|
|
|
76
81
|
|
|
77
82
|
default_toggle_sidebar= True
|
|
78
83
|
|
|
84
|
+
@property
|
|
85
|
+
def search_help_text(self):
|
|
86
|
+
fields = ', '.join([str(get_verbose_name_field(self.model, f)) for f in self.search_fields])
|
|
87
|
+
return format_lazy(_('Search by {fields}'), fields=fields)
|
|
88
|
+
|
|
79
89
|
def __init__(self, model, admin_site):
|
|
80
90
|
super().__init__(model, admin_site)
|
|
81
|
-
self.search_help_text= f'Search by {", ".join([get_verbose_name_field(model, f) for f in self.search_fields])}'
|
|
82
91
|
|
|
83
92
|
if self.text_field_widget == 'ckeditor':
|
|
84
93
|
self.formfield_overrides[models.TextField]["widget"]= CKEditor5Widget(
|
|
@@ -89,6 +98,40 @@ class ModelAdmin(UnfoldAdmin):
|
|
|
89
98
|
def has_module_permission(self, request: HttpRequest) -> bool:
|
|
90
99
|
return super().has_module_permission(request)
|
|
91
100
|
|
|
101
|
+
def _fix_preserved_filters(self, request, response):
|
|
102
|
+
"""Fix Django bug: add_preserved_filters() uses dict(parse_qsl()) which drops duplicate
|
|
103
|
+
filter params. E.g. ?chapter__id__exact=29&chapter__id__exact=31 → only =31 kept after save.
|
|
104
|
+
|
|
105
|
+
Root cause: get_preserved_filters() wraps filters in _changelist_filters=..., then
|
|
106
|
+
add_preserved_filters() decodes via dict(parse_qsl(...)) losing repeated keys.
|
|
107
|
+
|
|
108
|
+
Fix: read _changelist_filters directly from request.GET (inner filter string),
|
|
109
|
+
detect duplicates, then rebuild the redirect URL with parse_qsl() which keeps all values.
|
|
110
|
+
"""
|
|
111
|
+
if not isinstance(response, HttpResponseRedirectBase):
|
|
112
|
+
return response
|
|
113
|
+
# Inner filter string e.g. 'chapter__id__exact=29&chapter__id__exact=31'
|
|
114
|
+
inner = request.GET.get('_changelist_filters', '')
|
|
115
|
+
if not inner:
|
|
116
|
+
return response
|
|
117
|
+
pairs = parse_qsl(inner, keep_blank_values=True)
|
|
118
|
+
keys = [k for k, v in pairs]
|
|
119
|
+
if len(keys) == len(set(keys)):
|
|
120
|
+
return response # No duplicate keys — Django handled it correctly
|
|
121
|
+
# Rebuild URL: strip the incorrectly-added filter params, then append all pairs correctly
|
|
122
|
+
parsed = urlparse(response.url)
|
|
123
|
+
filter_keys = {k for k, v in pairs}
|
|
124
|
+
base_params = [(k, v) for k, v in parse_qsl(parsed.query) if k not in filter_keys]
|
|
125
|
+
return HttpResponseRedirect(
|
|
126
|
+
parsed._replace(query=urlencode(base_params + pairs)).geturl()
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def response_post_save_change(self, request, obj):
|
|
130
|
+
return self._fix_preserved_filters(request, super().response_post_save_change(request, obj))
|
|
131
|
+
|
|
132
|
+
def response_post_save_add(self, request, obj):
|
|
133
|
+
return self._fix_preserved_filters(request, super().response_post_save_add(request, obj))
|
|
134
|
+
|
|
92
135
|
# Soft delete
|
|
93
136
|
def get_actions(self, request: HttpRequest) -> OrderedDict[Any, Any]:
|
|
94
137
|
actions = super().get_actions(request)
|
|
@@ -102,8 +145,8 @@ class ModelAdmin(UnfoldAdmin):
|
|
|
102
145
|
|
|
103
146
|
def soft_delete(self, cl, request, queryset):
|
|
104
147
|
for obj in queryset:
|
|
105
|
-
title=
|
|
106
|
-
content_html=
|
|
148
|
+
title= _('%(name)s "%(obj)s" has been soft-deleted by user "%(user)s"') % {'name': obj._meta.verbose_name, 'obj': obj, 'user': request.user}
|
|
149
|
+
content_html= _('%(name)s "%(obj)s" has been soft-deleted by user "%(user)s"') % {'name': obj._meta.verbose_name, 'obj': obj, 'user': request.user}
|
|
107
150
|
obj.delete()
|
|
108
151
|
# send notification to superuser when delete successfully
|
|
109
152
|
for user in User.objects.filter(is_superuser= True):
|
|
@@ -112,12 +155,12 @@ class ModelAdmin(UnfoldAdmin):
|
|
|
112
155
|
flag= 'danger',
|
|
113
156
|
action= 'delete',
|
|
114
157
|
content= content_html)
|
|
115
|
-
soft_delete.short_description = 'Delete selected records'
|
|
158
|
+
soft_delete.short_description = _('Delete selected records')
|
|
116
159
|
|
|
117
160
|
def hard_delete(self, cl, request, queryset):
|
|
118
161
|
for obj in queryset:
|
|
119
|
-
title=
|
|
120
|
-
content_html=
|
|
162
|
+
title= _('%(name)s "%(obj)s" has been hard-deleted by user "%(user)s"') % {'name': obj._meta.verbose_name, 'obj': obj, 'user': request.user}
|
|
163
|
+
content_html= _('%(name)s "%(obj)s" has been hard-deleted by user "%(user)s"') % {'name': obj._meta.verbose_name, 'obj': obj, 'user': request.user}
|
|
121
164
|
if hasattr(obj, 'hard_delete'):
|
|
122
165
|
obj.hard_delete()
|
|
123
166
|
else:
|
|
@@ -129,12 +172,12 @@ class ModelAdmin(UnfoldAdmin):
|
|
|
129
172
|
flag= 'danger',
|
|
130
173
|
action= 'delete',
|
|
131
174
|
content= content_html)
|
|
132
|
-
hard_delete.short_description = 'Hard delete selected records'
|
|
175
|
+
hard_delete.short_description = _('Hard delete selected records')
|
|
133
176
|
|
|
134
177
|
def restore(self, cl, request, queryset):
|
|
135
178
|
for obj in queryset:
|
|
136
|
-
title=
|
|
137
|
-
content_html=
|
|
179
|
+
title= _('%(name)s "%(obj)s" has been restored by user "%(user)s"') % {'name': obj._meta.verbose_name, 'obj': obj, 'user': request.user}
|
|
180
|
+
content_html= _('%(name)s "%(obj)s" has been restored by user "%(user)s"') % {'name': obj._meta.verbose_name, 'obj': obj, 'user': request.user}
|
|
138
181
|
obj.restore(request)
|
|
139
182
|
# send notification to superuser when delete successfully
|
|
140
183
|
for user in User.objects.filter(is_superuser= True):
|
|
@@ -143,7 +186,7 @@ class ModelAdmin(UnfoldAdmin):
|
|
|
143
186
|
flag= 'info',
|
|
144
187
|
action= 'restore',
|
|
145
188
|
content= content_html)
|
|
146
|
-
restore.short_description = 'Restore selected records'
|
|
189
|
+
restore.short_description = _('Restore selected records')
|
|
147
190
|
|
|
148
191
|
def get_queryset(self, request: HttpRequest) -> QuerySet[Any]:
|
|
149
192
|
if request.user.is_superuser and request.user.show_softdelete:
|
|
@@ -152,8 +195,8 @@ class ModelAdmin(UnfoldAdmin):
|
|
|
152
195
|
return self.model.objects.all()
|
|
153
196
|
|
|
154
197
|
def delete_model(self, request, obj):
|
|
155
|
-
title=
|
|
156
|
-
content_html=
|
|
198
|
+
title= _('%(name)s "%(obj)s" has been soft-deleted by user "%(user)s"') % {'name': obj._meta.verbose_name, 'obj': obj, 'user': request.user}
|
|
199
|
+
content_html= _('%(name)s "%(obj)s" has been soft-deleted by user "%(user)s"') % {'name': obj._meta.verbose_name, 'obj': obj, 'user': request.user}
|
|
157
200
|
super().delete_model(request, obj)
|
|
158
201
|
# send notification to superuser when delete successfully
|
|
159
202
|
for user in User.objects.filter(is_superuser= True):
|
|
@@ -358,7 +401,7 @@ class ModelAdmin(UnfoldAdmin):
|
|
|
358
401
|
meta_fields.remove('updated_at')
|
|
359
402
|
meta_fields.remove('updated_by')
|
|
360
403
|
meta_fields.append(('updated_at', 'updated_by'))
|
|
361
|
-
fieldsets.append(('Meta', {'fields': meta_fields, 'classes': ["
|
|
404
|
+
fieldsets.append((_('Meta'), {'fields': meta_fields, 'classes': ["collapse"]}))
|
|
362
405
|
return fieldsets
|
|
363
406
|
|
|
364
407
|
def buttons(self, obj):
|
|
@@ -378,15 +421,15 @@ class ModelAdmin(UnfoldAdmin):
|
|
|
378
421
|
path_delete= reverse_lazy(path + '_delete', args=[obj.pk])
|
|
379
422
|
c= 0
|
|
380
423
|
if self.has_change_permission(self.request, obj):
|
|
381
|
-
button_edit = f'<a class="btn-edit col-span-1" href="{path_change}"> <img src="{svg_url_edit}" alt="
|
|
424
|
+
button_edit = f'<a class="btn-edit col-span-1" href="{path_change}"> <img src="{svg_url_edit}" alt="{_("Edit")}" style="width: {width}px; height: {height}px;"></a>'
|
|
382
425
|
c+= 1
|
|
383
426
|
else:
|
|
384
|
-
button_view = f'<a class="btn-view col-span-1" href="{path_change}"> <img src="{svg_url_view}" alt="
|
|
427
|
+
button_view = f'<a class="btn-view col-span-1" href="{path_change}"> <img src="{svg_url_view}" alt="{_("View")}" style="width: {width}px; height: {height}px;"></a>'
|
|
385
428
|
c+= 1
|
|
386
429
|
|
|
387
430
|
if self.has_delete_permission(self.request, obj):
|
|
388
431
|
c+= 1
|
|
389
|
-
button_delete = f'<a class="btn-delete btn-danger col-span-1" href="{path_delete}"> <img src="{svg_url_delete}" alt="
|
|
432
|
+
button_delete = f'<a class="btn-delete btn-danger col-span-1" href="{path_delete}"> <img src="{svg_url_delete}" alt="{_("Delete")}" style="width: {width}px; height: {height}px;"></a>'
|
|
390
433
|
|
|
391
434
|
return mark_safe(f'''
|
|
392
435
|
<div id="action_buttoms_{obj.pk}" class="action_buttoms grid gap-1 grid-cols-{c}" style="width: {33*c}px;">{button_view} {button_edit} {button_delete} </div>
|
|
@@ -15,7 +15,7 @@ def timeit(func):
|
|
|
15
15
|
from django.utils import timezone
|
|
16
16
|
from django.core.exceptions import FieldDoesNotExist
|
|
17
17
|
|
|
18
|
-
def base_badge_callback(request, model):
|
|
18
|
+
def base_badge_callback(request, model, filter_kwargs=None):
|
|
19
19
|
today = timezone.localtime().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
20
20
|
|
|
21
21
|
# try:
|
|
@@ -42,7 +42,11 @@ def base_badge_callback(request, model):
|
|
|
42
42
|
|
|
43
43
|
c = getattr(model, 'objects_all', model.objects).filter(
|
|
44
44
|
updated_at__gte=today,
|
|
45
|
-
)
|
|
45
|
+
)
|
|
46
|
+
if filter_kwargs:
|
|
47
|
+
c = c.filter(**filter_kwargs).count()
|
|
48
|
+
else:
|
|
49
|
+
c = c.count()
|
|
46
50
|
return c
|
|
47
51
|
|
|
48
52
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from django.contrib import admin, messages
|
|
2
|
+
from django.http import JsonResponse
|
|
3
|
+
from django.urls import reverse
|
|
4
|
+
from django.utils.html import format_html
|
|
5
|
+
from django.utils.text import Truncator
|
|
2
6
|
from django.utils.translation import gettext_lazy as _
|
|
3
|
-
from unfold.decorators import display
|
|
4
7
|
|
|
5
8
|
from whiteneuron.base.admin import base_admin_site, ModelAdmin
|
|
6
9
|
from .models import FeedbackData
|
|
7
|
-
|
|
8
|
-
from django.urls import reverse
|
|
10
|
+
from .urls import FEEDBACK_COOLDOWN_SECONDS
|
|
9
11
|
|
|
10
12
|
from whiteneuron.notification.models import Notification
|
|
11
13
|
|
|
@@ -18,7 +20,7 @@ class FeedbackDataAdmin(ModelAdmin):
|
|
|
18
20
|
'user',
|
|
19
21
|
'content_type',
|
|
20
22
|
'get_related_object_link', # Hiển thị link đến object gốc
|
|
21
|
-
'
|
|
23
|
+
'get_short_message',
|
|
22
24
|
'is_resolved',
|
|
23
25
|
'created_at',
|
|
24
26
|
)
|
|
@@ -26,26 +28,39 @@ class FeedbackDataAdmin(ModelAdmin):
|
|
|
26
28
|
search_fields = ('message', 'user__username', 'content_type__model')
|
|
27
29
|
|
|
28
30
|
fieldsets = (
|
|
29
|
-
('Feedback Data', {
|
|
30
|
-
'fields': ('user', ('content_type', 'object_id'), ('get_related_object_link', 'field'), 'message')
|
|
31
|
+
(_('Feedback Data'), {
|
|
32
|
+
# 'fields': ('user', ('content_type', 'object_id'), ('get_related_object_link', 'field'), 'message')
|
|
33
|
+
'fields': ('user', ('content_type', 'object_id'), 'get_related_object_link', 'message')
|
|
31
34
|
}),
|
|
32
|
-
('Status', {
|
|
35
|
+
(_('Status'), {
|
|
33
36
|
'fields': ('is_resolved', 'note')
|
|
34
37
|
})
|
|
35
38
|
)
|
|
36
39
|
|
|
37
40
|
readonly_fields = ('get_related_object_link',) # Chỉ hiển thị link, không cho sửa
|
|
38
41
|
|
|
42
|
+
def get_queryset(self, request):
|
|
43
|
+
qs = super().get_queryset(request)
|
|
44
|
+
if request.user.is_superuser:
|
|
45
|
+
return qs
|
|
46
|
+
|
|
47
|
+
# Người liên quan có thể xem feedback
|
|
48
|
+
return qs.filter(user=request.user)
|
|
49
|
+
|
|
50
|
+
def get_short_message(self, obj):
|
|
51
|
+
return Truncator(obj.message).words(200)
|
|
52
|
+
get_short_message.short_description = _('Message')
|
|
53
|
+
|
|
39
54
|
def get_related_object_link(self, obj):
|
|
40
55
|
"""Hiển thị link đến object gốc"""
|
|
41
56
|
if obj.content_object:
|
|
42
57
|
admin_url = reverse(
|
|
43
|
-
f'admin:{obj.content_type.app_label}_{obj.content_type.model}_change',
|
|
58
|
+
f'admin:{obj.content_type.app_label}_{obj.content_type.model}_change',
|
|
44
59
|
args=[obj.object_id]
|
|
45
60
|
)
|
|
46
|
-
return format_html(
|
|
61
|
+
return format_html('<a href="{}" target="_blank">{}</a>', admin_url, obj.content_object)
|
|
47
62
|
return "-"
|
|
48
|
-
get_related_object_link.short_description = "Related Object" # Tiêu đề trong Admin
|
|
63
|
+
get_related_object_link.short_description = _("Related Object") # Tiêu đề trong Admin
|
|
49
64
|
|
|
50
65
|
# Định nghĩa action để đánh dấu đã xử lý
|
|
51
66
|
actions = ['mark_as_resolved']
|
|
@@ -83,13 +98,16 @@ class FeedbackDataAdmin(ModelAdmin):
|
|
|
83
98
|
if request.GET.get('resolved') == '1' and object_id:
|
|
84
99
|
# chỉ cho phép superuser thực hiện
|
|
85
100
|
if not request.user.is_superuser:
|
|
86
|
-
|
|
87
|
-
return super().changeform_view(request, object_id, form_url, extra_context)
|
|
101
|
+
return JsonResponse({"success": False, "message": str(_("Only superuser can mark feedback as resolved"))}, status=403)
|
|
88
102
|
|
|
89
103
|
# note
|
|
90
|
-
note= request.
|
|
104
|
+
note= request.POST.get('note', '')
|
|
105
|
+
if len(note) > 500:
|
|
106
|
+
return JsonResponse({"success": False, "message": str(_("Note must not exceed 500 characters."))}, status=400)
|
|
91
107
|
|
|
92
108
|
obj = self.get_object(request, object_id)
|
|
109
|
+
if obj is None:
|
|
110
|
+
return JsonResponse({"success": False, "message": str(_("Feedback not found."))}, status=404)
|
|
93
111
|
obj.is_resolved = True
|
|
94
112
|
obj.note = note
|
|
95
113
|
obj.save()
|
|
@@ -98,30 +116,25 @@ class FeedbackDataAdmin(ModelAdmin):
|
|
|
98
116
|
noti= Notification(
|
|
99
117
|
user= obj.user,
|
|
100
118
|
title= _("Feedback resolved"),
|
|
101
|
-
content=
|
|
102
|
-
<p>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
119
|
+
content= format_html(
|
|
120
|
+
'<p>{}</p><p>{}: {}</p><p>{}: {}</p>',
|
|
121
|
+
_("Your feedback has been resolved."),
|
|
122
|
+
_("Object"), self.get_related_object_link(obj),
|
|
123
|
+
_("Note"), note,
|
|
124
|
+
),
|
|
106
125
|
action='update',
|
|
107
126
|
flag= 'success',
|
|
108
127
|
)
|
|
109
128
|
noti.save()
|
|
110
129
|
|
|
111
|
-
|
|
112
|
-
self.message_user(request, _("Feedback marked as resolved"), messages.SUCCESS)
|
|
130
|
+
return JsonResponse({"success": True})
|
|
113
131
|
|
|
114
132
|
return super().changeform_view(request, object_id, form_url, extra_context)
|
|
115
133
|
|
|
116
134
|
# FeedBack base admin
|
|
117
135
|
# Cho phép các model khác kế thừa để gọi
|
|
118
|
-
from django.utils.html import format_html
|
|
119
136
|
class FeedbackBaseAdmin(ModelAdmin):
|
|
120
137
|
def render_change_form(self, request, context, *args, **kwargs):
|
|
121
|
-
# Thêm show_feedback = True vào context để hiển thị feedbacks
|
|
122
138
|
context['show_feedback'] = True
|
|
139
|
+
context['feedback_cooldown_ms'] = FEEDBACK_COOLDOWN_SECONDS * 1000
|
|
123
140
|
return super().render_change_form(request, context, *args, **kwargs)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from django.urls import path
|
|
4
|
+
from django.http import JsonResponse
|
|
5
|
+
from django.views.decorators.http import require_POST
|
|
6
|
+
from django.contrib.contenttypes.models import ContentType
|
|
7
|
+
from django.utils import timezone
|
|
8
|
+
from django.utils.translation import gettext as _
|
|
9
|
+
from datetime import timedelta
|
|
10
|
+
from whiteneuron.feedbacks.models import FeedbackData
|
|
11
|
+
|
|
12
|
+
FEEDBACK_COOLDOWN_SECONDS = 60
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
@require_POST
|
|
17
|
+
def receive_feedback(request):
|
|
18
|
+
if not request.user.is_authenticated:
|
|
19
|
+
return JsonResponse({"success": False, "message": _("User is not authenticated")}, status=403)
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
# Đọc dữ liệu từ request
|
|
23
|
+
body_unicode = request.body.decode("utf-8")
|
|
24
|
+
|
|
25
|
+
data = json.loads(body_unicode)
|
|
26
|
+
object_id = data.get("object_id")
|
|
27
|
+
field = data.get("field")
|
|
28
|
+
feedback_message = data.get("feedback_message")
|
|
29
|
+
model_name = data.get("model_name")
|
|
30
|
+
app_label = data.get("app_label")
|
|
31
|
+
|
|
32
|
+
# Kiểm tra dữ liệu bắt buộc
|
|
33
|
+
if not object_id or not feedback_message or not model_name or not app_label:
|
|
34
|
+
return JsonResponse({"success": False, "message": _("Missing required fields")}, status=400)
|
|
35
|
+
|
|
36
|
+
if len(feedback_message) > 2000:
|
|
37
|
+
return JsonResponse({"success": False, "message": _("Feedback message must not exceed 2000 characters.")}, status=400)
|
|
38
|
+
|
|
39
|
+
# Lấy ContentType
|
|
40
|
+
try:
|
|
41
|
+
content_type = ContentType.objects.get(model=model_name, app_label=app_label)
|
|
42
|
+
except ContentType.DoesNotExist:
|
|
43
|
+
return JsonResponse({"success": False, "message": _("Invalid model or app_label")}, status=400)
|
|
44
|
+
|
|
45
|
+
# Kiểm tra cooldown chống spam — per user, không phân biệt đối tượng
|
|
46
|
+
cooldown_since = timezone.now() - timedelta(seconds=FEEDBACK_COOLDOWN_SECONDS)
|
|
47
|
+
last_feedback = FeedbackData.objects.filter(
|
|
48
|
+
user=request.user,
|
|
49
|
+
created_at__gte=cooldown_since,
|
|
50
|
+
).order_by('-created_at').first()
|
|
51
|
+
if last_feedback:
|
|
52
|
+
remaining = FEEDBACK_COOLDOWN_SECONDS - int((timezone.now() - last_feedback.created_at).total_seconds())
|
|
53
|
+
return JsonResponse(
|
|
54
|
+
{"success": False, "message": _("Please wait %(seconds)d seconds before submitting another feedback.") % {"seconds": max(remaining, 1)}},
|
|
55
|
+
status=429,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Tạo feedback mới
|
|
59
|
+
feedback = FeedbackData(
|
|
60
|
+
user=request.user,
|
|
61
|
+
content_type=content_type,
|
|
62
|
+
object_id=object_id,
|
|
63
|
+
field=field,
|
|
64
|
+
message=feedback_message,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
feedback.save()
|
|
68
|
+
|
|
69
|
+
logger.info("Feedback saved: %s", feedback)
|
|
70
|
+
return JsonResponse({"success": True, "message": _("Feedback saved successfully")})
|
|
71
|
+
|
|
72
|
+
except json.JSONDecodeError:
|
|
73
|
+
logger.error("Invalid JSON format in feedback submission")
|
|
74
|
+
return JsonResponse({"success": False, "message": _("Invalid JSON format")}, status=400)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.error("Error saving feedback: %s", e, exc_info=True)
|
|
77
|
+
return JsonResponse({"success": False, "message": _("An unexpected error occurred. Please try again.")}, status=500)
|
|
78
|
+
|
|
79
|
+
urlpatterns = [
|
|
80
|
+
path("feedback/", receive_feedback, name="feedback_endpoint"),
|
|
81
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from whiteneuron.base.utils import base_badge_callback
|
|
2
|
+
from .models import FeedbackData
|
|
3
|
+
|
|
4
|
+
def feedback_data_badge_callback(request):
|
|
5
|
+
# Lọc feedback theo user hiện tại
|
|
6
|
+
if request.user.is_superuser:
|
|
7
|
+
filter_kwargs = None # Superuser xem tất cả feedback
|
|
8
|
+
else:
|
|
9
|
+
filter_kwargs = {'user': request.user}
|
|
10
|
+
return base_badge_callback(request, FeedbackData, filter_kwargs)
|
|
Binary file
|