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.
Files changed (188) hide show
  1. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/PKG-INFO +35 -4
  2. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/README.md +34 -3
  3. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/django_whiteneuron.egg-info/PKG-INFO +35 -4
  4. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/pyproject.toml +1 -1
  5. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/modeladmin.py +60 -17
  6. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/utils.py +6 -2
  7. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/admin.py +39 -26
  8. django_whiteneuron-0.2.44/whiteneuron/feedbacks/urls.py +81 -0
  9. django_whiteneuron-0.2.44/whiteneuron/feedbacks/utils.py +10 -0
  10. django_whiteneuron-0.2.44/whiteneuron/locale/vi/LC_MESSAGES/django.mo +0 -0
  11. django_whiteneuron-0.2.44/whiteneuron/locale/vi/LC_MESSAGES/django.po +1423 -0
  12. django_whiteneuron-0.2.44/whiteneuron/static/base/css/styles.css +2 -0
  13. django_whiteneuron-0.2.44/whiteneuron/templates/admin/change_list.html +145 -0
  14. django_whiteneuron-0.2.44/whiteneuron/templates/admin/feedbacks/feedback_change_form.html +132 -0
  15. django_whiteneuron-0.2.44/whiteneuron/templates/admin/submit_line.html +157 -0
  16. django_whiteneuron-0.2.41/whiteneuron/feedbacks/urls.py +0 -69
  17. django_whiteneuron-0.2.41/whiteneuron/feedbacks/utils.py +0 -5
  18. django_whiteneuron-0.2.41/whiteneuron/locale/vi/LC_MESSAGES/django.mo +0 -0
  19. django_whiteneuron-0.2.41/whiteneuron/locale/vi/LC_MESSAGES/django.po +0 -1182
  20. django_whiteneuron-0.2.41/whiteneuron/static/base/css/styles.css +0 -2
  21. django_whiteneuron-0.2.41/whiteneuron/templates/admin/change_list.html +0 -59
  22. django_whiteneuron-0.2.41/whiteneuron/templates/admin/feedbacks/feedback_change_form.html +0 -41
  23. django_whiteneuron-0.2.41/whiteneuron/templates/admin/submit_line.html +0 -67
  24. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/LICENSE +0 -0
  25. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/django_whiteneuron.egg-info/SOURCES.txt +0 -0
  26. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/django_whiteneuron.egg-info/dependency_links.txt +0 -0
  27. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/django_whiteneuron.egg-info/requires.txt +0 -0
  28. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/django_whiteneuron.egg-info/top_level.txt +0 -0
  29. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/setup.cfg +0 -0
  30. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/__init__.py +0 -0
  31. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/__init__.py +0 -0
  32. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/admin.py +0 -0
  33. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/apps.py +0 -0
  34. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/asgi.py +0 -0
  35. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/ckeditor.py +0 -0
  36. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/filters.py +0 -0
  37. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/forms.py +0 -0
  38. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/management/__init__.py +0 -0
  39. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/management/commands/__init__.py +0 -0
  40. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/management/commands/init_admin.py +0 -0
  41. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/management/commands/init_groupuser.py +0 -0
  42. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/management/commands/init_guest.py +0 -0
  43. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/middleware.py +0 -0
  44. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0001_initial.py +0 -0
  45. {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
  46. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0003_notification_obj_link.py +0 -0
  47. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0004_delete_notification.py +0 -0
  48. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0005_user_show_softdelete.py +0 -0
  49. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0006_user_is_bot.py +0 -0
  50. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0007_app.py +0 -0
  51. {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
  52. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0009_alter_app_table.py +0 -0
  53. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0010_app_category.py +0 -0
  54. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0011_alter_app_options.py +0 -0
  55. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0012_app_permission.py +0 -0
  56. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0013_app_thumbnail_url.py +0 -0
  57. {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
  58. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0015_ipblacklist.py +0 -0
  59. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0016_uablacklist.py +0 -0
  60. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/0017_alter_uablacklist_pattern.py +0 -0
  61. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/migrations/__init__.py +0 -0
  62. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/models.py +0 -0
  63. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/resources.py +0 -0
  64. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/settings.py +0 -0
  65. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/sites.py +0 -0
  66. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/tasks.py +0 -0
  67. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/templatetags/__init__.py +0 -0
  68. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/templatetags/unfold_list_custom.py +0 -0
  69. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/templatetags/wn_filters.py +0 -0
  70. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/tests.py +0 -0
  71. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/thread_local.py +0 -0
  72. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/urls.py +0 -0
  73. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/views.py +0 -0
  74. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/base/widgets.py +0 -0
  75. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/__init__.py +0 -0
  76. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_import_export/__init__.py +0 -0
  77. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_import_export/admin.py +0 -0
  78. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_import_export/constants.py +0 -0
  79. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_import_export/mixins.py +0 -0
  80. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_import_export/tasks.py +0 -0
  81. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_import_export/utils.py +0 -0
  82. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_modeltranslation/__init__.py +0 -0
  83. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_modeltranslation/admin.py +0 -0
  84. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/contrib/wn_modeltranslation/utils.py +0 -0
  85. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/dashboard/__init__.py +0 -0
  86. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/dashboard/admin.py +0 -0
  87. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/dashboard/apps.py +0 -0
  88. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/dashboard/migrations/__init__.py +0 -0
  89. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/dashboard/models.py +0 -0
  90. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/dashboard/tests.py +0 -0
  91. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/dashboard/views.py +0 -0
  92. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/__init__.py +0 -0
  93. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/apps.py +0 -0
  94. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/migrations/0001_initial.py +0 -0
  95. {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
  96. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/migrations/0003_feedbackdata_note.py +0 -0
  97. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/migrations/0004_alter_feedbackdata_object_id.py +0 -0
  98. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/migrations/0005_feedbackdata_field.py +0 -0
  99. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/migrations/0006_alter_feedbackdata_created_at_and_more.py +0 -0
  100. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/migrations/__init__.py +0 -0
  101. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/models.py +0 -0
  102. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/tests.py +0 -0
  103. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/feedbacks/views.py +0 -0
  104. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/__init__.py +0 -0
  105. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/admin.py +0 -0
  106. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/apps.py +0 -0
  107. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/migrations/0001_initial.py +0 -0
  108. {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
  109. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/migrations/0003_alter_excelfile_created_at_and_more.py +0 -0
  110. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/migrations/__init__.py +0 -0
  111. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/models.py +0 -0
  112. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/tests.py +0 -0
  113. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/utils.py +0 -0
  114. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/file_management/views.py +0 -0
  115. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/manage.py +0 -0
  116. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/__init__.py +0 -0
  117. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/admin.py +0 -0
  118. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/apps.py +0 -0
  119. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/consumers.py +0 -0
  120. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/management/__init__.py +0 -0
  121. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/management/commands/__init__.py +0 -0
  122. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/management/commands/call_demo_noti.py +0 -0
  123. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0001_initial.py +0 -0
  124. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0002_alter_notificationconfig_event_type.py +0 -0
  125. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0003_notification.py +0 -0
  126. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0004_alter_notification_flag.py +0 -0
  127. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0005_notification_keywords.py +0 -0
  128. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0006_remove_notification_keywords.py +0 -0
  129. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0007_notification_changed_data.py +0 -0
  130. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0008_alter_notification_changed_data.py +0 -0
  131. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0009_notification_action_by_alter_notification_user.py +0 -0
  132. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0010_alter_notification_user.py +0 -0
  133. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0011_alter_notification_user.py +0 -0
  134. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/0012_alter_notification_action_and_more.py +0 -0
  135. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/migrations/__init__.py +0 -0
  136. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/models.py +0 -0
  137. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/routing.py +0 -0
  138. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/tests.py +0 -0
  139. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/utils.py +0 -0
  140. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/notification/views.py +0 -0
  141. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/css/btn-styles.css +0 -0
  142. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/css/ckeditor5.css +0 -0
  143. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/css/loading.css +0 -0
  144. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/forms/css/html-editor.css +0 -0
  145. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/forms/js/format.html.js +0 -0
  146. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/forms/js/trix.config.js +0 -0
  147. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/images/delete.svg +0 -0
  148. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/images/edit.svg +0 -0
  149. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/images/icon-viewlink.svg +0 -0
  150. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/images/loading.gif +0 -0
  151. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/images/login-bg.jpg +0 -0
  152. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/images/login_bg.jpg +0 -0
  153. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/images/logo/WhiteNeuron.png +0 -0
  154. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/js/chart.min.js +0 -0
  155. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/js/d3.v7.min.js +0 -0
  156. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/js/loading.js +0 -0
  157. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/static/base/js/whiteneuron.js +0 -0
  158. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/synchronization/__init__.py +0 -0
  159. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/synchronization/admin.py +0 -0
  160. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/synchronization/apps.py +0 -0
  161. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/synchronization/migrations/__init__.py +0 -0
  162. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/synchronization/models.py +0 -0
  163. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/synchronization/tests.py +0 -0
  164. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/synchronization/views.py +0 -0
  165. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/400.html +0 -0
  166. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/403.html +0 -0
  167. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/404.html +0 -0
  168. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/429.html +0 -0
  169. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/500.html +0 -0
  170. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/auth/user/add_form.html +0 -0
  171. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/base/app_change_list.html +0 -0
  172. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/base/image_change_form.html +0 -0
  173. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/base/userprofile_change_form.html +0 -0
  174. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/base.html +0 -0
  175. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/grid_view_results.html +0 -0
  176. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/header.html +0 -0
  177. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/index.html +0 -0
  178. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/login.html +0 -0
  179. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/login_after.html +0 -0
  180. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/admin/search_form.html +0 -0
  181. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/base/forms/helpers/toolbar.html +0 -0
  182. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/base/forms/wysiwyg.html +0 -0
  183. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/base.html +0 -0
  184. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/unfold/helpers/account_links.html +0 -0
  185. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/unfold/helpers/app_list_badge.html +0 -0
  186. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/unfold/helpers/field_password.html +0 -0
  187. {django_whiteneuron-0.2.41 → django_whiteneuron-0.2.44}/whiteneuron/templates/unfold/helpers/navigation_user.html +0 -0
  188. {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.41
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: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
91
91
  [![Downloads](https://img.shields.io/pypi/dm/django-whiteneuron)](https://pypi.org/project/django-whiteneuron/)
92
92
 
93
- ![django-whiteneuron](docs/images/main.png)
93
+ ![django-whiteneuron](https://raw.githubusercontent.com/White-Neuron/django-whiteneuron/main2.0/docs/images/main.png)
94
94
 
95
95
  ## Current Version
96
96
 
97
- - 0.2.41
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.41 (2026-03-31) — latest
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: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
8
8
  [![Downloads](https://img.shields.io/pypi/dm/django-whiteneuron)](https://pypi.org/project/django-whiteneuron/)
9
9
 
10
- ![django-whiteneuron](docs/images/main.png)
10
+ ![django-whiteneuron](https://raw.githubusercontent.com/White-Neuron/django-whiteneuron/main2.0/docs/images/main.png)
11
11
 
12
12
  ## Current Version
13
13
 
14
- - 0.2.41
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.41 (2026-03-31) — latest
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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-whiteneuron
3
- Version: 0.2.41
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: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
91
91
  [![Downloads](https://img.shields.io/pypi/dm/django-whiteneuron)](https://pypi.org/project/django-whiteneuron/)
92
92
 
93
- ![django-whiteneuron](docs/images/main.png)
93
+ ![django-whiteneuron](https://raw.githubusercontent.com/White-Neuron/django-whiteneuron/main2.0/docs/images/main.png)
94
94
 
95
95
  ## Current Version
96
96
 
97
- - 0.2.41
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.41 (2026-03-31) — latest
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.41"
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= f"{obj._meta.verbose_name} \"{obj}\" has been soft-deleted by user \"{request.user}\""
106
- content_html= f"{obj._meta.verbose_name} \"{obj}\" has been soft-deleted by user \"{request.user}\""
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= f"{obj._meta.verbose_name} \"{obj}\" has been hard-deleted by user \"{request.user}\""
120
- content_html= f"{obj._meta.verbose_name} \"{obj}\" has been hard-deleted by user \"{request.user}\""
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= f"{obj._meta.verbose_name} \"{obj}\" has been restored by user \"{request.user}\""
137
- content_html= f"{obj._meta.verbose_name} \"{obj}\" has been restored by user \"{request.user}\""
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= f"{obj._meta.verbose_name} \"{obj}\" has been soft-deleted by user \"{request.user}\""
156
- content_html= f"{obj._meta.verbose_name} \"{obj}\" has been soft-deleted by user \"{request.user}\""
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': ["tab"]}))
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="Sửa" style="width: {width}px; height: {height}px;"></a>'
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="Xem" style="width: {width}px; height: {height}px;"></a>'
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="Xóa" style="width: {width}px; height: {height}px;"></a>'
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
- ).count()
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
- 'message',
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(f'<a href="{admin_url}" target="_blank">{obj.content_object}</a>')
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
- self.message_user(request, _("Only superuser can mark feedback as resolved"), messages.ERROR)
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.GET.get('note', '')
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= f"""
102
- <p>Your feedback has been resolved.</p>
103
- <p>Object: {self.get_related_object_link(obj)}</p>
104
- <p>Note: {note}</p>
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
- # reload và thông báo
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)