django-whiteneuron 0.2.48.2__tar.gz → 0.3.0__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 (190) hide show
  1. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/PKG-INFO +27 -3
  2. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/README.md +26 -2
  3. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/django_whiteneuron.egg-info/PKG-INFO +27 -3
  4. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/django_whiteneuron.egg-info/SOURCES.txt +3 -0
  5. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/pyproject.toml +1 -1
  6. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/admin.py +1 -0
  7. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/modeladmin.py +10 -2
  8. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/settings.py +2 -2
  9. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/urls.py +1 -0
  10. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/utils.py +10 -2
  11. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/admin.py +1 -0
  12. django_whiteneuron-0.3.0/whiteneuron/file_management/admin.py +163 -0
  13. django_whiteneuron-0.3.0/whiteneuron/file_management/migrations/0004_excelfile_hash_pdffile_hash.py +23 -0
  14. django_whiteneuron-0.3.0/whiteneuron/file_management/models.py +121 -0
  15. django_whiteneuron-0.3.0/whiteneuron/file_management/urls.py +7 -0
  16. django_whiteneuron-0.3.0/whiteneuron/file_management/views.py +50 -0
  17. django_whiteneuron-0.3.0/whiteneuron/locale/vi/LC_MESSAGES/django.mo +0 -0
  18. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/locale/vi/LC_MESSAGES/django.po +123 -30
  19. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/admin.py +92 -4
  20. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/models.py +11 -10
  21. django_whiteneuron-0.3.0/whiteneuron/static/base/css/styles.css +2 -0
  22. {django_whiteneuron-0.2.48.2/whiteneuron/file_management → django_whiteneuron-0.3.0/whiteneuron/synchronization}/views.py +0 -0
  23. django_whiteneuron-0.3.0/whiteneuron/templates/file_management/widgets/clearable_file_input_no_download.html +20 -0
  24. django_whiteneuron-0.2.48.2/whiteneuron/file_management/admin.py +0 -32
  25. django_whiteneuron-0.2.48.2/whiteneuron/file_management/models.py +0 -65
  26. django_whiteneuron-0.2.48.2/whiteneuron/locale/vi/LC_MESSAGES/django.mo +0 -0
  27. django_whiteneuron-0.2.48.2/whiteneuron/static/base/css/styles.css +0 -2
  28. django_whiteneuron-0.2.48.2/whiteneuron/synchronization/views.py +0 -3
  29. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/LICENSE +0 -0
  30. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/django_whiteneuron.egg-info/dependency_links.txt +0 -0
  31. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/django_whiteneuron.egg-info/requires.txt +0 -0
  32. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/django_whiteneuron.egg-info/top_level.txt +0 -0
  33. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/setup.cfg +0 -0
  34. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/__init__.py +0 -0
  35. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/__init__.py +0 -0
  36. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/apps.py +0 -0
  37. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/asgi.py +0 -0
  38. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/ckeditor.py +0 -0
  39. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/filters.py +0 -0
  40. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/forms.py +0 -0
  41. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/management/__init__.py +0 -0
  42. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/management/commands/__init__.py +0 -0
  43. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/management/commands/init_admin.py +0 -0
  44. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/management/commands/init_groupuser.py +0 -0
  45. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/management/commands/init_guest.py +0 -0
  46. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/middleware.py +0 -0
  47. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0001_initial.py +0 -0
  48. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0002_imagemodel_userprofile_user_avatar_user_biography_and_more.py +0 -0
  49. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0003_notification_obj_link.py +0 -0
  50. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0004_delete_notification.py +0 -0
  51. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0005_user_show_softdelete.py +0 -0
  52. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0006_user_is_bot.py +0 -0
  53. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0007_app.py +0 -0
  54. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0008_app_created_at_app_created_by_app_deleted_at_and_more.py +0 -0
  55. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0009_alter_app_table.py +0 -0
  56. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0010_app_category.py +0 -0
  57. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0011_alter_app_options.py +0 -0
  58. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0012_app_permission.py +0 -0
  59. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0013_app_thumbnail_url.py +0 -0
  60. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0014_alter_app_created_at_alter_app_updated_at_and_more.py +0 -0
  61. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0015_ipblacklist.py +0 -0
  62. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0016_uablacklist.py +0 -0
  63. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0017_alter_uablacklist_pattern.py +0 -0
  64. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/__init__.py +0 -0
  65. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/models.py +0 -0
  66. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/resources.py +0 -0
  67. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/sites.py +0 -0
  68. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/tasks.py +0 -0
  69. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/templatetags/__init__.py +0 -0
  70. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/templatetags/unfold_list_custom.py +0 -0
  71. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/templatetags/wn_filters.py +0 -0
  72. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/tests.py +0 -0
  73. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/thread_local.py +0 -0
  74. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/views.py +0 -0
  75. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/base/widgets.py +0 -0
  76. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/contrib/__init__.py +0 -0
  77. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/__init__.py +0 -0
  78. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/admin.py +0 -0
  79. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/constants.py +0 -0
  80. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/mixins.py +0 -0
  81. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/tasks.py +0 -0
  82. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/utils.py +0 -0
  83. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_modeltranslation/__init__.py +0 -0
  84. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_modeltranslation/admin.py +0 -0
  85. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_modeltranslation/utils.py +0 -0
  86. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/__init__.py +0 -0
  87. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/admin.py +0 -0
  88. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/apps.py +0 -0
  89. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/migrations/__init__.py +0 -0
  90. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/models.py +0 -0
  91. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/tests.py +0 -0
  92. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/views.py +0 -0
  93. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/__init__.py +0 -0
  94. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/apps.py +0 -0
  95. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/0001_initial.py +0 -0
  96. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/0002_feedbackdata_created_by_feedbackdata_deleted_at_and_more.py +0 -0
  97. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/0003_feedbackdata_note.py +0 -0
  98. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/0004_alter_feedbackdata_object_id.py +0 -0
  99. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/0005_feedbackdata_field.py +0 -0
  100. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/0006_alter_feedbackdata_created_at_and_more.py +0 -0
  101. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/__init__.py +0 -0
  102. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/models.py +0 -0
  103. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/tests.py +0 -0
  104. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/urls.py +0 -0
  105. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/utils.py +0 -0
  106. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/views.py +0 -0
  107. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/file_management/__init__.py +0 -0
  108. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/file_management/apps.py +0 -0
  109. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/file_management/migrations/0001_initial.py +0 -0
  110. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/file_management/migrations/0002_alter_excelfile_options_alter_pdffile_options_and_more.py +0 -0
  111. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/file_management/migrations/0003_alter_excelfile_created_at_and_more.py +0 -0
  112. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/file_management/migrations/__init__.py +0 -0
  113. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/file_management/tests.py +0 -0
  114. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/file_management/utils.py +0 -0
  115. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/manage.py +0 -0
  116. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/__init__.py +0 -0
  117. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/apps.py +0 -0
  118. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/consumers.py +0 -0
  119. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/management/__init__.py +0 -0
  120. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/management/commands/__init__.py +0 -0
  121. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/management/commands/call_demo_noti.py +0 -0
  122. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0001_initial.py +0 -0
  123. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0002_alter_notificationconfig_event_type.py +0 -0
  124. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0003_notification.py +0 -0
  125. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0004_alter_notification_flag.py +0 -0
  126. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0005_notification_keywords.py +0 -0
  127. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0006_remove_notification_keywords.py +0 -0
  128. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0007_notification_changed_data.py +0 -0
  129. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0008_alter_notification_changed_data.py +0 -0
  130. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0009_notification_action_by_alter_notification_user.py +0 -0
  131. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0010_alter_notification_user.py +0 -0
  132. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0011_alter_notification_user.py +0 -0
  133. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0012_alter_notification_action_and_more.py +0 -0
  134. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/__init__.py +0 -0
  135. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/routing.py +0 -0
  136. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/tests.py +0 -0
  137. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/utils.py +0 -0
  138. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/notification/views.py +0 -0
  139. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/static/base/css/btn-styles.css +0 -0
  140. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/static/base/css/ckeditor5.css +0 -0
  141. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/static/base/css/loading.css +0 -0
  142. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/static/base/forms/css/html-editor.css +0 -0
  143. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/static/base/forms/js/format.html.js +0 -0
  144. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/static/base/forms/js/trix.config.js +0 -0
  145. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/delete.svg +0 -0
  146. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/edit.svg +0 -0
  147. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/icon-viewlink.svg +0 -0
  148. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/loading.gif +0 -0
  149. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/login-bg.jpg +0 -0
  150. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/login_bg.jpg +0 -0
  151. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/logo/WhiteNeuron.png +0 -0
  152. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/static/base/js/chart.min.js +0 -0
  153. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/static/base/js/d3.v7.min.js +0 -0
  154. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/static/base/js/loading.js +0 -0
  155. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/static/base/js/whiteneuron.js +0 -0
  156. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/__init__.py +0 -0
  157. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/admin.py +0 -0
  158. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/apps.py +0 -0
  159. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/migrations/__init__.py +0 -0
  160. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/models.py +0 -0
  161. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/tests.py +0 -0
  162. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/400.html +0 -0
  163. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/403.html +0 -0
  164. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/404.html +0 -0
  165. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/429.html +0 -0
  166. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/500.html +0 -0
  167. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/auth/user/add_form.html +0 -0
  168. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/base/app_change_list.html +0 -0
  169. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/base/image_change_form.html +0 -0
  170. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/base/userprofile_change_form.html +0 -0
  171. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/base.html +0 -0
  172. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/change_list.html +0 -0
  173. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/feedbacks/feedback_change_form.html +0 -0
  174. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/grid_view_results.html +0 -0
  175. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/header.html +0 -0
  176. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/index.html +0 -0
  177. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/login.html +0 -0
  178. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/login_after.html +0 -0
  179. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/search_form.html +0 -0
  180. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/submit_line.html +0 -0
  181. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/wn_change_form.html +0 -0
  182. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/wn_history_panel.html +0 -0
  183. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/base/forms/helpers/toolbar.html +0 -0
  184. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/base/forms/wysiwyg.html +0 -0
  185. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/base.html +0 -0
  186. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/unfold/helpers/account_links.html +0 -0
  187. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/unfold/helpers/app_list_badge.html +0 -0
  188. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/unfold/helpers/field_password.html +0 -0
  189. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/whiteneuron/templates/unfold/helpers/navigation_user.html +0 -0
  190. {django_whiteneuron-0.2.48.2 → django_whiteneuron-0.3.0}/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.48.2
3
+ Version: 0.3.0
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.ai>
6
6
  License: MIT License
@@ -96,7 +96,7 @@ A modern Django Admin extension focused on UI/UX, dashboard, feedback, file mana
96
96
 
97
97
  ## Current Version
98
98
 
99
- - 0.2.48
99
+ - 0.3.0
100
100
 
101
101
  ## Compatibility
102
102
 
@@ -107,7 +107,31 @@ A modern Django Admin extension focused on UI/UX, dashboard, feedback, file mana
107
107
 
108
108
  ## Changelog
109
109
 
110
- ### v0.2.48.2 (2026-04-10) — latest
110
+ ### v0.3.0 (2026-04-13) — latest
111
+ **Feature: File integrity protection — SHA-256 hash verification for ExcelFile & PDFFile**
112
+ - **Added**: `compute_file_hash()` function — computes SHA-256 of a `FieldFile` via direct `storage.open()` to avoid Django internal state issues (works for both in-memory uploads and committed storage files).
113
+ - **Added**: `BaseFile.save()` override — automatically computes and stores SHA-256 hash on every new upload; resets `status='done'` when file is replaced after an error; backfills hash for existing files with no hash.
114
+ - **Added**: `BaseFile.verify_integrity()` — compares live storage hash against stored hash; returns `False` if missing or mismatched.
115
+ - **Added**: `download_file` view — `@login_required` endpoint at `file-management/download/<type>/<pk>/`; enrolls legacy records on first download, blocks tampered files with `403 Forbidden`, marks them `status='error'`.
116
+ - **Added**: `file_management/urls.py` — wired into `base/urls.py`.
117
+ - **Added**: `FileInputNoDownload` widget — extends `UnfoldAdminFileFieldWidget` with a custom template that removes the direct download button; adds `accepted_file_types` support per admin subclass (`.xlsx,.xls,.csv` for Excel; `.pdf` for PDF).
118
+ - **Added**: `BaseFileAdmin` enhancements — `integrity_status`, `hash_display`, `current_hash_display` readonly fields; `verified_download` smart button (primary button when OK, error badge when tampered/missing); `change_view` shows Django error message banner on integrity failure; `get_fieldsets` context-aware layout (auto/upload/new).
119
+ - **Fixed**: `status_view` / `method_view` XSS — replaced f-string + `format_html` pattern with proper `format_html(template, *args)` escaping.
120
+ - **Fixed**: `file.delete(save=False)` in `BaseFile.delete()` — prevents extra `save()` call after file deletion.
121
+ - **Improved**: `accepted_file_types` class attribute on admin subclasses — browser-level file type filtering in upload dialog.
122
+
123
+ ### v0.2.48.4 (2026-04-11)
124
+ **Security: Nâng cấp cbor2 5.9.0 và ujson 5.12.0**
125
+ - **Security**: `cbor2` nâng từ `v5.8.0` lên `v5.9.0` — bản vá lỗi và cải thiện encoding/decoding CBOR.
126
+ - **Security**: `ujson` nâng từ `v5.11.0` lên `v5.12.0` — bản vá bảo mật và cải thiện hiệu suất JSON serialization.
127
+
128
+ ### v0.2.48.3 (2026-04-10)
129
+ **Improve: Notification list — DaisyUI badge display, search field verbose names**
130
+ - **Improved**: `NotificationAdmin.display_title` rewritten — renders `[Model][#ID][Object name][Action] by [Alias][@username]` using `ui-badge ui-badge-xs` badges with correct color mapping (`ui-badge-success/info/error/warning`).
131
+ - **Fixed**: `obj_link` parsing regex updated to handle paths without `/admin/` prefix — e.g. `/techniques/technique/58316/change/`.
132
+ - **Fixed**: `get_verbose_name_field` now traverses `__`-separated lookup paths (e.g. `user__username`) to resolve proper verbose names for all search fields in `search_help_text`.
133
+
134
+ ### v0.2.48.2 (2026-04-10)
111
135
  **Security: Nâng cấp django 5.2.13 và thêm pyOpenSSL 26.0.0**
112
136
  - **Security**: `django` lower bound nâng từ `>=5.2.12` lên `>=5.2.13` — bản vá bảo mật mới nhất của Django.
113
137
  - **Security**: `pyopenssl==26.0.0` thêm vào explicit dependencies — đảm bảo phiên bản mới nhất của pyOpenSSL được sử dụng, vá các lỗ hổng tiềm ẩn trong TLS/SSL handling.
@@ -11,7 +11,7 @@ A modern Django Admin extension focused on UI/UX, dashboard, feedback, file mana
11
11
 
12
12
  ## Current Version
13
13
 
14
- - 0.2.48
14
+ - 0.3.0
15
15
 
16
16
  ## Compatibility
17
17
 
@@ -22,7 +22,31 @@ A modern Django Admin extension focused on UI/UX, dashboard, feedback, file mana
22
22
 
23
23
  ## Changelog
24
24
 
25
- ### v0.2.48.2 (2026-04-10) — latest
25
+ ### v0.3.0 (2026-04-13) — latest
26
+ **Feature: File integrity protection — SHA-256 hash verification for ExcelFile & PDFFile**
27
+ - **Added**: `compute_file_hash()` function — computes SHA-256 of a `FieldFile` via direct `storage.open()` to avoid Django internal state issues (works for both in-memory uploads and committed storage files).
28
+ - **Added**: `BaseFile.save()` override — automatically computes and stores SHA-256 hash on every new upload; resets `status='done'` when file is replaced after an error; backfills hash for existing files with no hash.
29
+ - **Added**: `BaseFile.verify_integrity()` — compares live storage hash against stored hash; returns `False` if missing or mismatched.
30
+ - **Added**: `download_file` view — `@login_required` endpoint at `file-management/download/<type>/<pk>/`; enrolls legacy records on first download, blocks tampered files with `403 Forbidden`, marks them `status='error'`.
31
+ - **Added**: `file_management/urls.py` — wired into `base/urls.py`.
32
+ - **Added**: `FileInputNoDownload` widget — extends `UnfoldAdminFileFieldWidget` with a custom template that removes the direct download button; adds `accepted_file_types` support per admin subclass (`.xlsx,.xls,.csv` for Excel; `.pdf` for PDF).
33
+ - **Added**: `BaseFileAdmin` enhancements — `integrity_status`, `hash_display`, `current_hash_display` readonly fields; `verified_download` smart button (primary button when OK, error badge when tampered/missing); `change_view` shows Django error message banner on integrity failure; `get_fieldsets` context-aware layout (auto/upload/new).
34
+ - **Fixed**: `status_view` / `method_view` XSS — replaced f-string + `format_html` pattern with proper `format_html(template, *args)` escaping.
35
+ - **Fixed**: `file.delete(save=False)` in `BaseFile.delete()` — prevents extra `save()` call after file deletion.
36
+ - **Improved**: `accepted_file_types` class attribute on admin subclasses — browser-level file type filtering in upload dialog.
37
+
38
+ ### v0.2.48.4 (2026-04-11)
39
+ **Security: Nâng cấp cbor2 5.9.0 và ujson 5.12.0**
40
+ - **Security**: `cbor2` nâng từ `v5.8.0` lên `v5.9.0` — bản vá lỗi và cải thiện encoding/decoding CBOR.
41
+ - **Security**: `ujson` nâng từ `v5.11.0` lên `v5.12.0` — bản vá bảo mật và cải thiện hiệu suất JSON serialization.
42
+
43
+ ### v0.2.48.3 (2026-04-10)
44
+ **Improve: Notification list — DaisyUI badge display, search field verbose names**
45
+ - **Improved**: `NotificationAdmin.display_title` rewritten — renders `[Model][#ID][Object name][Action] by [Alias][@username]` using `ui-badge ui-badge-xs` badges with correct color mapping (`ui-badge-success/info/error/warning`).
46
+ - **Fixed**: `obj_link` parsing regex updated to handle paths without `/admin/` prefix — e.g. `/techniques/technique/58316/change/`.
47
+ - **Fixed**: `get_verbose_name_field` now traverses `__`-separated lookup paths (e.g. `user__username`) to resolve proper verbose names for all search fields in `search_help_text`.
48
+
49
+ ### v0.2.48.2 (2026-04-10)
26
50
  **Security: Nâng cấp django 5.2.13 và thêm pyOpenSSL 26.0.0**
27
51
  - **Security**: `django` lower bound nâng từ `>=5.2.12` lên `>=5.2.13` — bản vá bảo mật mới nhất của Django.
28
52
  - **Security**: `pyopenssl==26.0.0` thêm vào explicit dependencies — đảm bảo phiên bản mới nhất của pyOpenSSL được sử dụng, vá các lỗ hổng tiềm ẩn trong TLS/SSL handling.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-whiteneuron
3
- Version: 0.2.48.2
3
+ Version: 0.3.0
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.ai>
6
6
  License: MIT License
@@ -96,7 +96,7 @@ A modern Django Admin extension focused on UI/UX, dashboard, feedback, file mana
96
96
 
97
97
  ## Current Version
98
98
 
99
- - 0.2.48
99
+ - 0.3.0
100
100
 
101
101
  ## Compatibility
102
102
 
@@ -107,7 +107,31 @@ A modern Django Admin extension focused on UI/UX, dashboard, feedback, file mana
107
107
 
108
108
  ## Changelog
109
109
 
110
- ### v0.2.48.2 (2026-04-10) — latest
110
+ ### v0.3.0 (2026-04-13) — latest
111
+ **Feature: File integrity protection — SHA-256 hash verification for ExcelFile & PDFFile**
112
+ - **Added**: `compute_file_hash()` function — computes SHA-256 of a `FieldFile` via direct `storage.open()` to avoid Django internal state issues (works for both in-memory uploads and committed storage files).
113
+ - **Added**: `BaseFile.save()` override — automatically computes and stores SHA-256 hash on every new upload; resets `status='done'` when file is replaced after an error; backfills hash for existing files with no hash.
114
+ - **Added**: `BaseFile.verify_integrity()` — compares live storage hash against stored hash; returns `False` if missing or mismatched.
115
+ - **Added**: `download_file` view — `@login_required` endpoint at `file-management/download/<type>/<pk>/`; enrolls legacy records on first download, blocks tampered files with `403 Forbidden`, marks them `status='error'`.
116
+ - **Added**: `file_management/urls.py` — wired into `base/urls.py`.
117
+ - **Added**: `FileInputNoDownload` widget — extends `UnfoldAdminFileFieldWidget` with a custom template that removes the direct download button; adds `accepted_file_types` support per admin subclass (`.xlsx,.xls,.csv` for Excel; `.pdf` for PDF).
118
+ - **Added**: `BaseFileAdmin` enhancements — `integrity_status`, `hash_display`, `current_hash_display` readonly fields; `verified_download` smart button (primary button when OK, error badge when tampered/missing); `change_view` shows Django error message banner on integrity failure; `get_fieldsets` context-aware layout (auto/upload/new).
119
+ - **Fixed**: `status_view` / `method_view` XSS — replaced f-string + `format_html` pattern with proper `format_html(template, *args)` escaping.
120
+ - **Fixed**: `file.delete(save=False)` in `BaseFile.delete()` — prevents extra `save()` call after file deletion.
121
+ - **Improved**: `accepted_file_types` class attribute on admin subclasses — browser-level file type filtering in upload dialog.
122
+
123
+ ### v0.2.48.4 (2026-04-11)
124
+ **Security: Nâng cấp cbor2 5.9.0 và ujson 5.12.0**
125
+ - **Security**: `cbor2` nâng từ `v5.8.0` lên `v5.9.0` — bản vá lỗi và cải thiện encoding/decoding CBOR.
126
+ - **Security**: `ujson` nâng từ `v5.11.0` lên `v5.12.0` — bản vá bảo mật và cải thiện hiệu suất JSON serialization.
127
+
128
+ ### v0.2.48.3 (2026-04-10)
129
+ **Improve: Notification list — DaisyUI badge display, search field verbose names**
130
+ - **Improved**: `NotificationAdmin.display_title` rewritten — renders `[Model][#ID][Object name][Action] by [Alias][@username]` using `ui-badge ui-badge-xs` badges with correct color mapping (`ui-badge-success/info/error/warning`).
131
+ - **Fixed**: `obj_link` parsing regex updated to handle paths without `/admin/` prefix — e.g. `/techniques/technique/58316/change/`.
132
+ - **Fixed**: `get_verbose_name_field` now traverses `__`-separated lookup paths (e.g. `user__username`) to resolve proper verbose names for all search fields in `search_help_text`.
133
+
134
+ ### v0.2.48.2 (2026-04-10)
111
135
  **Security: Nâng cấp django 5.2.13 và thêm pyOpenSSL 26.0.0**
112
136
  - **Security**: `django` lower bound nâng từ `>=5.2.12` lên `>=5.2.13` — bản vá bảo mật mới nhất của Django.
113
137
  - **Security**: `pyopenssl==26.0.0` thêm vào explicit dependencies — đảm bảo phiên bản mới nhất của pyOpenSSL được sử dụng, vá các lỗ hổng tiềm ẩn trong TLS/SSL handling.
@@ -91,11 +91,13 @@ whiteneuron/file_management/admin.py
91
91
  whiteneuron/file_management/apps.py
92
92
  whiteneuron/file_management/models.py
93
93
  whiteneuron/file_management/tests.py
94
+ whiteneuron/file_management/urls.py
94
95
  whiteneuron/file_management/utils.py
95
96
  whiteneuron/file_management/views.py
96
97
  whiteneuron/file_management/migrations/0001_initial.py
97
98
  whiteneuron/file_management/migrations/0002_alter_excelfile_options_alter_pdffile_options_and_more.py
98
99
  whiteneuron/file_management/migrations/0003_alter_excelfile_created_at_and_more.py
100
+ whiteneuron/file_management/migrations/0004_excelfile_hash_pdffile_hash.py
99
101
  whiteneuron/file_management/migrations/__init__.py
100
102
  whiteneuron/locale/vi/LC_MESSAGES/django.mo
101
103
  whiteneuron/locale/vi/LC_MESSAGES/django.po
@@ -173,6 +175,7 @@ whiteneuron/templates/admin/base/userprofile_change_form.html
173
175
  whiteneuron/templates/admin/feedbacks/feedback_change_form.html
174
176
  whiteneuron/templates/base/forms/wysiwyg.html
175
177
  whiteneuron/templates/base/forms/helpers/toolbar.html
178
+ whiteneuron/templates/file_management/widgets/clearable_file_input_no_download.html
176
179
  whiteneuron/templates/unfold/helpers/account_links.html
177
180
  whiteneuron/templates/unfold/helpers/app_list_badge.html
178
181
  whiteneuron/templates/unfold/helpers/field_password.html
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "django-whiteneuron"
3
- version = "0.2.48.2"
3
+ version = "0.3.0"
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.ai"}
@@ -346,6 +346,7 @@ class UserActivityAdmin(ModelAdmin):
346
346
  """
347
347
 
348
348
  compressed_fields = True
349
+ using_grid_view = False
349
350
 
350
351
  list_display = [
351
352
  "user",
@@ -44,8 +44,16 @@ from unfold.paginator import InfinitePaginator
44
44
 
45
45
  def get_verbose_name_field(model, field):
46
46
  try:
47
- return str(model._meta.get_field(field).verbose_name)
48
- except:
47
+ parts = field.split('__')
48
+ current_model = model
49
+ labels = []
50
+ for part in parts:
51
+ f = current_model._meta.get_field(part)
52
+ labels.append(str(f.verbose_name))
53
+ if hasattr(f, 'related_model') and f.related_model:
54
+ current_model = f.related_model
55
+ return ' / '.join(labels)
56
+ except Exception:
49
57
  return field
50
58
 
51
59
  class ModelAdmin(UnfoldAdmin):
@@ -556,14 +556,14 @@ UNFOLD = {
556
556
  # "title": _("Dashboard"),
557
557
  # "icon": "dashboard",
558
558
  # "link": reverse_lazy("admin:index"),
559
- # "permission": "whiteneuron.base.utils.prmission_viewer_callback",
559
+ # "permission": "whiteneuron.base.utils.permission_viewer_callback",
560
560
  # },
561
561
  {
562
562
  "title": _("Dashboard"),
563
563
  "icon": "dashboard",
564
564
  "link": reverse_lazy("admin:base_app_changelist"),
565
565
  "badge": "whiteneuron.base.utils.app_badge_callback",
566
- "permission": "whiteneuron.base.utils.prmission_viewer_callback",
566
+ "permission": "whiteneuron.base.utils.permission_viewer_callback",
567
567
  },
568
568
  {
569
569
  "title": _("Notifications"),
@@ -24,4 +24,5 @@ urlpatterns += [
24
24
  path("base/guest-login/", GuestLoginView.as_view(), name="guest_login"),
25
25
  # path("i18n/", include("django.conf.urls.i18n")),
26
26
  path("ckeditor5/", include('django_ckeditor_5.urls')),
27
+ path("file-management/", include('whiteneuron.file_management.urls')),
27
28
  ]
@@ -83,6 +83,14 @@ def app_badge_callback(request):
83
83
  except Exception:
84
84
  return 0
85
85
 
86
+ def is_guest_user(user):
87
+ if not user or not user.is_authenticated:
88
+ return True
89
+ if user.username.lower() == "guest":
90
+ return True
91
+ return user.groups.filter(name__in=["Khách thăm", "Guest", "guest"]).exists()
92
+
93
+
86
94
  def permission_callback(request):
87
95
  return False
88
96
 
@@ -95,9 +103,9 @@ def permission_admin_callback(request):
95
103
  return request.user.groups.filter(name='Quản trị viên').exists()
96
104
 
97
105
  def permission_non_guest_callback(request):
98
- return not request.user.groups.filter(name='Khách thăm').exists()
106
+ return not is_guest_user(request.user)
99
107
 
100
- def prmission_viewer_callback(request):
108
+ def permission_viewer_callback(request):
101
109
  return True
102
110
 
103
111
 
@@ -13,6 +13,7 @@ from whiteneuron.notification.models import Notification
13
13
 
14
14
  @admin.register(FeedbackData, site=base_admin_site)
15
15
  class FeedbackDataAdmin(ModelAdmin):
16
+ using_grid_view = False
16
17
  date_hierarchy = 'created_at'
17
18
  change_form_template = 'admin/feedbacks/feedback_change_form.html'
18
19
  list_display = (
@@ -0,0 +1,163 @@
1
+ from whiteneuron.base.admin import base_admin_site, ModelAdmin
2
+ from django.contrib import admin, messages
3
+ from django.urls import reverse
4
+ from django.utils.html import format_html
5
+ from django.utils.translation import gettext_lazy as _
6
+ from unfold.widgets import UnfoldAdminFileFieldWidget
7
+
8
+ from .models import ExcelFile, PDFFile, compute_file_hash
9
+
10
+
11
+ class FileInputNoDownload(UnfoldAdminFileFieldWidget):
12
+ """Unfold-styled file widget with the download button removed."""
13
+ template_name = "file_management/widgets/clearable_file_input_no_download.html"
14
+
15
+ class BaseFileAdmin(ModelAdmin):
16
+ file_type = None # Set in subclasses: 'excel' | 'pdf'
17
+ accepted_file_types = None # e.g. '.xlsx,.xls' — passed as HTML accept attribute
18
+
19
+ list_display = ("title", "method_view", "status_view", "created_at", "created_by")
20
+ search_fields = ("title", "description")
21
+ filter_horizontal = ()
22
+ list_filter = ("status", "method")
23
+ readonly_fields = (
24
+ "created_at", "created_by", "updated_at", "updated_by",
25
+ "status_view", "method_view", "integrity_status", "verified_download",
26
+ "hash_display", "current_hash_display",
27
+ )
28
+ # fieldsets is intentionally omitted — get_fieldsets() handles all cases dynamically.
29
+
30
+ def _get_current_hash(self, obj, request=None):
31
+ """Compute the current SHA-256 hash of the file on storage. No caching — always fresh."""
32
+ try:
33
+ return compute_file_hash(obj.file)
34
+ except Exception:
35
+ return None
36
+
37
+ def formfield_for_dbfield(self, db_field, request, **kwargs):
38
+ field = super().formfield_for_dbfield(db_field, request, **kwargs)
39
+ if db_field.name == 'file' and field is not None:
40
+ attrs = dict(field.widget.attrs)
41
+ if self.accepted_file_types:
42
+ attrs['accept'] = self.accepted_file_types
43
+ field.widget = FileInputNoDownload(attrs=attrs)
44
+ return field
45
+
46
+ def get_readonly_fields(self, request, obj=None):
47
+ readonly = super().get_readonly_fields(request, obj)
48
+ if obj and obj.method == 'auto':
49
+ readonly = tuple(readonly) + ('file',)
50
+ return readonly
51
+
52
+ def get_fieldsets(self, request, obj=None):
53
+ # For auto-method files: replace editable 'file' with 'verified_download'.
54
+ # For upload-method files: show 'file' widget (for replacing) + verified_download link.
55
+ # For new objects (no pk): only show 'file' upload widget.
56
+ if obj and obj.method == 'auto':
57
+ file_fields = ('verified_download',)
58
+ elif obj and obj.pk:
59
+ file_fields = ('file', 'verified_download')
60
+ else:
61
+ file_fields = ('file',)
62
+ integrity_fields = ('integrity_status', 'hash_display', 'current_hash_display') if (obj and obj.pk) else ()
63
+ return (
64
+ (_('General Information'), {
65
+ 'fields': ('title', 'description',
66
+ 'status_view',
67
+ 'method_view',
68
+ *file_fields,
69
+ *integrity_fields),
70
+ }),
71
+ (_("Meta"), {
72
+ 'fields': ('created_at', 'created_by', 'updated_at', 'updated_by')
73
+ }),
74
+ )
75
+
76
+ def integrity_status(self, obj):
77
+ if not obj.pk or not obj.hash:
78
+ return format_html('<span style="color: #aaa;">—</span>')
79
+ current = self._get_current_hash(obj)
80
+ if current is None:
81
+ return format_html('<span style="color: #dd4b39;">⚠ {}</span>', _('Cannot read file'))
82
+ if current == obj.hash:
83
+ return format_html('<span style="color: #00a65a; font-weight: bold;">✓ OK</span>')
84
+ return format_html('<span style="color: #dd4b39; font-weight: bold;">✗ {}</span>', _('File has been modified'))
85
+ integrity_status.short_description = _('Integrity')
86
+
87
+ def hash_display(self, obj):
88
+ if not obj.pk or not obj.hash:
89
+ return format_html('<span style="color: #aaa;">—</span>')
90
+ return format_html(
91
+ '<code style="font-family: monospace; font-size: 0.85em; word-break: break-all;">{}</code>',
92
+ obj.hash
93
+ )
94
+ hash_display.short_description = _('Stored hash')
95
+
96
+ def current_hash_display(self, obj):
97
+ if not obj.pk or not obj.file or not obj.file.name:
98
+ return format_html('<span style="color: #aaa;">—</span>')
99
+ current = self._get_current_hash(obj)
100
+ if current is None:
101
+ return format_html('<span style="color: #dd4b39;">⚠ {}</span>', _('Cannot read file from storage'))
102
+ color = '#dd4b39' if (obj.hash and current != obj.hash) else '#aaa'
103
+ return format_html(
104
+ '<code style="font-family: monospace; font-size: 0.85em; word-break: break-all; color: {};">{}</code>',
105
+ color, current
106
+ )
107
+ current_hash_display.short_description = _('Current hash (live)')
108
+
109
+ def change_view(self, request, object_id, form_url='', extra_context=None):
110
+ obj = self.get_object(request, object_id)
111
+ if obj and obj.hash and obj.file and obj.file.name:
112
+ current = self._get_current_hash(obj)
113
+ if current is None:
114
+ messages.error(request, _('Cannot read the file from storage. It may have been deleted externally.'))
115
+ elif current != obj.hash:
116
+ messages.error(
117
+ request,
118
+ _('File integrity check failed: the file on storage does not match its stored hash. '
119
+ 'It may have been tampered with or corrupted.')
120
+ )
121
+ return super().change_view(request, object_id, form_url, extra_context)
122
+
123
+ def verified_download(self, obj):
124
+ if not obj.pk or not self.file_type:
125
+ return format_html('<span style="color: #aaa;">—</span>')
126
+ url = reverse('file_management_download', kwargs={'file_type': self.file_type, 'pk': obj.pk})
127
+ current = self._get_current_hash(obj)
128
+ if current is None:
129
+ return format_html(
130
+ '<span class="ui-badge ui-badge-error gap-1">'
131
+ '<span class="material-symbols-outlined" style="font-size: 14px;">error</span>'
132
+ '{}'
133
+ '</span>',
134
+ _('File missing')
135
+ )
136
+ if obj.hash and current != obj.hash:
137
+ return format_html(
138
+ '<span class="ui-badge ui-badge-error gap-1">'
139
+ '<span class="material-symbols-outlined" style="font-size: 14px;">gpp_bad</span>'
140
+ '{}'
141
+ '</span>',
142
+ _('Tampered — download blocked')
143
+ )
144
+ return format_html(
145
+ '<a href="{}" target="_blank" class="ui-btn ui-btn-primary ui-btn-sm gap-1">'
146
+ '<span class="material-symbols-outlined" style="font-size: 16px;">download</span>'
147
+ '{}'
148
+ '</a>',
149
+ url, _('Download (Verified)')
150
+ )
151
+ verified_download.short_description = _('Verified Download')
152
+
153
+
154
+ @admin.register(ExcelFile, site=base_admin_site)
155
+ class ExcelFileAdmin(BaseFileAdmin):
156
+ file_type = 'excel'
157
+ accepted_file_types = '.xlsx,.xls,.csv'
158
+
159
+
160
+ @admin.register(PDFFile, site=base_admin_site)
161
+ class PDFFileAdmin(BaseFileAdmin):
162
+ file_type = 'pdf'
163
+ accepted_file_types = '.pdf'
@@ -0,0 +1,23 @@
1
+ # Generated by Django 5.2.13 on 2026-04-13 02:45
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('file_management', '0003_alter_excelfile_created_at_and_more'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='excelfile',
15
+ name='hash',
16
+ field=models.CharField(blank=True, max_length=64, null=True, verbose_name='Hash'),
17
+ ),
18
+ migrations.AddField(
19
+ model_name='pdffile',
20
+ name='hash',
21
+ field=models.CharField(blank=True, max_length=64, null=True, verbose_name='Hash'),
22
+ ),
23
+ ]
@@ -0,0 +1,121 @@
1
+ import hashlib
2
+
3
+ from whiteneuron.base.models import BaseModel
4
+ from django.db import models
5
+ from django.utils.html import format_html
6
+ from django.utils.translation import gettext_lazy as _
7
+
8
+
9
+ def compute_file_hash(file_field):
10
+ """Compute SHA-256 hash of a Django FieldFile (uncommitted upload or committed storage file)."""
11
+ sha256 = hashlib.sha256()
12
+ if not file_field._committed:
13
+ # Newly uploaded file in memory/temp – read directly from the upload object
14
+ f = file_field.file
15
+ try:
16
+ f.seek(0)
17
+ except (AttributeError, OSError):
18
+ pass
19
+ for chunk in iter(lambda: f.read(8192), b''):
20
+ sha256.update(chunk)
21
+ try:
22
+ f.seek(0) # Reset so Django can still write it to storage
23
+ except (AttributeError, OSError):
24
+ pass
25
+ else:
26
+ # File is on storage – open directly through the storage backend to avoid
27
+ # FieldFile internal state issues (e.g. when triggered from FieldFile.save()).
28
+ with file_field.storage.open(file_field.name, 'rb') as f:
29
+ for chunk in iter(lambda: f.read(8192), b''):
30
+ sha256.update(chunk)
31
+ return sha256.hexdigest()
32
+
33
+ class BaseFile(BaseModel):
34
+ title = models.CharField(max_length=255)
35
+ description = models.TextField(blank=True, null=True, verbose_name=_('Description'))
36
+ status = models.CharField(max_length=25, choices=(('pending', _('Pending')),
37
+ ('done', _('Done')),
38
+ ('error', _('Error'))),
39
+ default='done', verbose_name=_('Status'))
40
+ method= models.CharField(max_length=25, choices=(('upload', _('Upload')),
41
+ ('auto', _('Auto'))),
42
+ default='upload', verbose_name=_('Method'))
43
+ hash = models.CharField(max_length=64, blank=True, null=True, verbose_name=_('Hash'))
44
+
45
+ def status_view(self):
46
+ if self.status == 'pending':
47
+ status_color = '#f39c12'
48
+ status = _('Pending')
49
+ elif self.status == 'done':
50
+ status_color = '#00a65a'
51
+ status = _('Done')
52
+ else:
53
+ status_color = '#dd4b39'
54
+ status = _('Error')
55
+ return format_html(
56
+ '<span style="color: {}; font-weight: bold;">{}</span>',
57
+ status_color, status
58
+ )
59
+ status_view.short_description = _('Status')
60
+
61
+ def method_view(self):
62
+ if self.method == 'upload':
63
+ method_color = '#00c0ef'
64
+ method = _('Upload')
65
+ else:
66
+ method_color = '#00a65a'
67
+ method = _('Auto')
68
+ return format_html(
69
+ '<span style="color: {}; font-weight: bold;">{}</span>',
70
+ method_color, method
71
+ )
72
+ method_view.short_description = _('Method')
73
+
74
+ def save(self, *args, **kwargs):
75
+ update_fields = kwargs.get('update_fields')
76
+ # Skip hash logic when only specific fields are being updated (e.g. status)
77
+ if update_fields is None and self.file and self.file.name:
78
+ if not self.file._committed:
79
+ # New upload in memory – hash before writing to storage
80
+ self.hash = compute_file_hash(self.file)
81
+ # A new file replaces any previous error state
82
+ if self.status == 'error':
83
+ self.status = 'done'
84
+ elif not self.hash:
85
+ # File already in storage but no hash yet (auto/programmatic creation)
86
+ self.hash = compute_file_hash(self.file)
87
+ super().save(*args, **kwargs)
88
+
89
+ def verify_integrity(self):
90
+ """Verify the file on storage matches its stored hash. Returns True if OK."""
91
+ if not self.hash or not self.file or not self.file.name:
92
+ return False
93
+ try:
94
+ return compute_file_hash(self.file) == self.hash
95
+ except Exception:
96
+ return False
97
+
98
+ def __str__(self):
99
+ return self.title
100
+
101
+ class Meta:
102
+ abstract = True
103
+
104
+ def delete(self, *args, **kwargs):
105
+ self.file.delete(save=False)
106
+ super().delete(*args, **kwargs)
107
+
108
+ class ExcelFile(BaseFile):
109
+ file = models.FileField(upload_to='excels')
110
+
111
+
112
+ class Meta:
113
+ verbose_name = _('Excel File')
114
+ verbose_name_plural = _('Excel Files')
115
+
116
+ class PDFFile(BaseFile):
117
+ file = models.FileField(upload_to='pdfs')
118
+
119
+ class Meta:
120
+ verbose_name = _('PDF File')
121
+ verbose_name_plural = _('PDF Files')
@@ -0,0 +1,7 @@
1
+ from django.urls import path
2
+
3
+ from .views import download_file
4
+
5
+ urlpatterns = [
6
+ path('download/<str:file_type>/<int:pk>/', download_file, name='file_management_download'),
7
+ ]
@@ -0,0 +1,50 @@
1
+ import posixpath
2
+
3
+ from django.contrib.auth.decorators import login_required
4
+ from django.http import FileResponse, HttpResponseForbidden, Http404
5
+ from django.utils.translation import gettext_lazy as _
6
+
7
+ from .models import ExcelFile, PDFFile, compute_file_hash
8
+
9
+ _FILE_MODEL_MAP = {
10
+ 'excel': ExcelFile,
11
+ 'pdf': PDFFile,
12
+ }
13
+
14
+
15
+ @login_required
16
+ def download_file(request, file_type, pk):
17
+ """Serve a file only after verifying its SHA-256 hash integrity."""
18
+ model_class = _FILE_MODEL_MAP.get(file_type)
19
+ if model_class is None:
20
+ raise Http404
21
+
22
+ try:
23
+ instance = model_class.objects.get(pk=pk, is_deleted=False)
24
+ except model_class.DoesNotExist:
25
+ raise Http404
26
+
27
+ if not instance.hash:
28
+ # Legacy record: no hash yet. Enroll it now and allow download.
29
+ if not instance.file or not instance.file.name:
30
+ return HttpResponseForbidden(
31
+ str(_('Cannot read file from storage. The file may be missing.'))
32
+ )
33
+ try:
34
+ instance.hash = compute_file_hash(instance.file)
35
+ instance.save(update_fields=['hash'])
36
+ except Exception:
37
+ return HttpResponseForbidden(
38
+ str(_('Cannot read file from storage. The file may be missing.'))
39
+ )
40
+ elif not instance.verify_integrity():
41
+ # Hash exists but does not match – file was tampered with.
42
+ if instance.status != 'error':
43
+ instance.status = 'error'
44
+ instance.save(update_fields=['status'])
45
+ return HttpResponseForbidden(
46
+ str(_('File integrity check failed. This file may have been tampered with and cannot be downloaded.'))
47
+ )
48
+
49
+ filename = posixpath.basename(instance.file.name)
50
+ return FileResponse(instance.file.open('rb'), as_attachment=True, filename=filename)