django-whiteneuron 0.2.48.5__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.5 → django_whiteneuron-0.3.0}/PKG-INFO +16 -3
  2. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/README.md +15 -2
  3. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/django_whiteneuron.egg-info/PKG-INFO +16 -3
  4. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/django_whiteneuron.egg-info/SOURCES.txt +3 -0
  5. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/pyproject.toml +1 -1
  6. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/urls.py +1 -0
  7. django_whiteneuron-0.3.0/whiteneuron/file_management/admin.py +163 -0
  8. django_whiteneuron-0.3.0/whiteneuron/file_management/migrations/0004_excelfile_hash_pdffile_hash.py +23 -0
  9. django_whiteneuron-0.3.0/whiteneuron/file_management/models.py +121 -0
  10. django_whiteneuron-0.3.0/whiteneuron/file_management/urls.py +7 -0
  11. django_whiteneuron-0.3.0/whiteneuron/file_management/views.py +50 -0
  12. django_whiteneuron-0.3.0/whiteneuron/locale/vi/LC_MESSAGES/django.mo +0 -0
  13. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/locale/vi/LC_MESSAGES/django.po +56 -0
  14. django_whiteneuron-0.3.0/whiteneuron/static/base/css/styles.css +2 -0
  15. {django_whiteneuron-0.2.48.5/whiteneuron/file_management → django_whiteneuron-0.3.0/whiteneuron/synchronization}/views.py +0 -0
  16. django_whiteneuron-0.3.0/whiteneuron/templates/file_management/widgets/clearable_file_input_no_download.html +20 -0
  17. django_whiteneuron-0.2.48.5/whiteneuron/file_management/admin.py +0 -32
  18. django_whiteneuron-0.2.48.5/whiteneuron/file_management/models.py +0 -65
  19. django_whiteneuron-0.2.48.5/whiteneuron/locale/vi/LC_MESSAGES/django.mo +0 -0
  20. django_whiteneuron-0.2.48.5/whiteneuron/static/base/css/styles.css +0 -2
  21. django_whiteneuron-0.2.48.5/whiteneuron/synchronization/views.py +0 -3
  22. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/LICENSE +0 -0
  23. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/django_whiteneuron.egg-info/dependency_links.txt +0 -0
  24. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/django_whiteneuron.egg-info/requires.txt +0 -0
  25. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/django_whiteneuron.egg-info/top_level.txt +0 -0
  26. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/setup.cfg +0 -0
  27. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/__init__.py +0 -0
  28. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/__init__.py +0 -0
  29. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/admin.py +0 -0
  30. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/apps.py +0 -0
  31. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/asgi.py +0 -0
  32. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/ckeditor.py +0 -0
  33. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/filters.py +0 -0
  34. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/forms.py +0 -0
  35. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/management/__init__.py +0 -0
  36. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/management/commands/__init__.py +0 -0
  37. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/management/commands/init_admin.py +0 -0
  38. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/management/commands/init_groupuser.py +0 -0
  39. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/management/commands/init_guest.py +0 -0
  40. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/middleware.py +0 -0
  41. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0001_initial.py +0 -0
  42. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0002_imagemodel_userprofile_user_avatar_user_biography_and_more.py +0 -0
  43. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0003_notification_obj_link.py +0 -0
  44. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0004_delete_notification.py +0 -0
  45. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0005_user_show_softdelete.py +0 -0
  46. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0006_user_is_bot.py +0 -0
  47. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0007_app.py +0 -0
  48. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0008_app_created_at_app_created_by_app_deleted_at_and_more.py +0 -0
  49. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0009_alter_app_table.py +0 -0
  50. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0010_app_category.py +0 -0
  51. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0011_alter_app_options.py +0 -0
  52. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0012_app_permission.py +0 -0
  53. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0013_app_thumbnail_url.py +0 -0
  54. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0014_alter_app_created_at_alter_app_updated_at_and_more.py +0 -0
  55. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0015_ipblacklist.py +0 -0
  56. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0016_uablacklist.py +0 -0
  57. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0017_alter_uablacklist_pattern.py +0 -0
  58. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/__init__.py +0 -0
  59. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/modeladmin.py +0 -0
  60. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/models.py +0 -0
  61. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/resources.py +0 -0
  62. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/settings.py +0 -0
  63. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/sites.py +0 -0
  64. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/tasks.py +0 -0
  65. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/templatetags/__init__.py +0 -0
  66. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/templatetags/unfold_list_custom.py +0 -0
  67. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/templatetags/wn_filters.py +0 -0
  68. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/tests.py +0 -0
  69. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/thread_local.py +0 -0
  70. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/utils.py +0 -0
  71. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/views.py +0 -0
  72. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/widgets.py +0 -0
  73. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/__init__.py +0 -0
  74. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/__init__.py +0 -0
  75. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/admin.py +0 -0
  76. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/constants.py +0 -0
  77. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/mixins.py +0 -0
  78. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/tasks.py +0 -0
  79. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/utils.py +0 -0
  80. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_modeltranslation/__init__.py +0 -0
  81. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_modeltranslation/admin.py +0 -0
  82. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_modeltranslation/utils.py +0 -0
  83. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/__init__.py +0 -0
  84. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/admin.py +0 -0
  85. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/apps.py +0 -0
  86. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/migrations/__init__.py +0 -0
  87. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/models.py +0 -0
  88. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/tests.py +0 -0
  89. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/views.py +0 -0
  90. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/__init__.py +0 -0
  91. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/admin.py +0 -0
  92. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/apps.py +0 -0
  93. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/0001_initial.py +0 -0
  94. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/0002_feedbackdata_created_by_feedbackdata_deleted_at_and_more.py +0 -0
  95. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/0003_feedbackdata_note.py +0 -0
  96. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/0004_alter_feedbackdata_object_id.py +0 -0
  97. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/0005_feedbackdata_field.py +0 -0
  98. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/0006_alter_feedbackdata_created_at_and_more.py +0 -0
  99. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/__init__.py +0 -0
  100. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/models.py +0 -0
  101. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/tests.py +0 -0
  102. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/urls.py +0 -0
  103. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/utils.py +0 -0
  104. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/views.py +0 -0
  105. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/file_management/__init__.py +0 -0
  106. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/file_management/apps.py +0 -0
  107. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/file_management/migrations/0001_initial.py +0 -0
  108. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/file_management/migrations/0002_alter_excelfile_options_alter_pdffile_options_and_more.py +0 -0
  109. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/file_management/migrations/0003_alter_excelfile_created_at_and_more.py +0 -0
  110. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/file_management/migrations/__init__.py +0 -0
  111. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/file_management/tests.py +0 -0
  112. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/file_management/utils.py +0 -0
  113. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/manage.py +0 -0
  114. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/__init__.py +0 -0
  115. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/admin.py +0 -0
  116. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/apps.py +0 -0
  117. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/consumers.py +0 -0
  118. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/management/__init__.py +0 -0
  119. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/management/commands/__init__.py +0 -0
  120. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/management/commands/call_demo_noti.py +0 -0
  121. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0001_initial.py +0 -0
  122. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0002_alter_notificationconfig_event_type.py +0 -0
  123. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0003_notification.py +0 -0
  124. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0004_alter_notification_flag.py +0 -0
  125. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0005_notification_keywords.py +0 -0
  126. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0006_remove_notification_keywords.py +0 -0
  127. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0007_notification_changed_data.py +0 -0
  128. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0008_alter_notification_changed_data.py +0 -0
  129. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0009_notification_action_by_alter_notification_user.py +0 -0
  130. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0010_alter_notification_user.py +0 -0
  131. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0011_alter_notification_user.py +0 -0
  132. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0012_alter_notification_action_and_more.py +0 -0
  133. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/__init__.py +0 -0
  134. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/models.py +0 -0
  135. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/routing.py +0 -0
  136. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/tests.py +0 -0
  137. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/utils.py +0 -0
  138. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/views.py +0 -0
  139. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/css/btn-styles.css +0 -0
  140. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/css/ckeditor5.css +0 -0
  141. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/css/loading.css +0 -0
  142. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/forms/css/html-editor.css +0 -0
  143. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/forms/js/format.html.js +0 -0
  144. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/forms/js/trix.config.js +0 -0
  145. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/delete.svg +0 -0
  146. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/edit.svg +0 -0
  147. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/icon-viewlink.svg +0 -0
  148. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/loading.gif +0 -0
  149. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/login-bg.jpg +0 -0
  150. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/login_bg.jpg +0 -0
  151. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/logo/WhiteNeuron.png +0 -0
  152. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/js/chart.min.js +0 -0
  153. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/js/d3.v7.min.js +0 -0
  154. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/js/loading.js +0 -0
  155. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/js/whiteneuron.js +0 -0
  156. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/__init__.py +0 -0
  157. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/admin.py +0 -0
  158. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/apps.py +0 -0
  159. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/migrations/__init__.py +0 -0
  160. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/models.py +0 -0
  161. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/tests.py +0 -0
  162. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/400.html +0 -0
  163. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/403.html +0 -0
  164. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/404.html +0 -0
  165. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/429.html +0 -0
  166. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/500.html +0 -0
  167. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/auth/user/add_form.html +0 -0
  168. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/base/app_change_list.html +0 -0
  169. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/base/image_change_form.html +0 -0
  170. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/base/userprofile_change_form.html +0 -0
  171. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/base.html +0 -0
  172. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/change_list.html +0 -0
  173. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/feedbacks/feedback_change_form.html +0 -0
  174. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/grid_view_results.html +0 -0
  175. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/header.html +0 -0
  176. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/index.html +0 -0
  177. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/login.html +0 -0
  178. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/login_after.html +0 -0
  179. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/search_form.html +0 -0
  180. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/submit_line.html +0 -0
  181. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/wn_change_form.html +0 -0
  182. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/wn_history_panel.html +0 -0
  183. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/base/forms/helpers/toolbar.html +0 -0
  184. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/base/forms/wysiwyg.html +0 -0
  185. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/base.html +0 -0
  186. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/unfold/helpers/account_links.html +0 -0
  187. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/unfold/helpers/app_list_badge.html +0 -0
  188. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/unfold/helpers/field_password.html +0 -0
  189. {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/unfold/helpers/navigation_user.html +0 -0
  190. {django_whiteneuron-0.2.48.5 → 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.5
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.3
99
+ - 0.3.0
100
100
 
101
101
  ## Compatibility
102
102
 
@@ -107,7 +107,20 @@ A modern Django Admin extension focused on UI/UX, dashboard, feedback, file mana
107
107
 
108
108
  ## Changelog
109
109
 
110
- ### v0.2.48.4 (2026-04-11) — 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)
111
124
  **Security: Nâng cấp cbor2 5.9.0 và ujson 5.12.0**
112
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.
113
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.
@@ -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.3
14
+ - 0.3.0
15
15
 
16
16
  ## Compatibility
17
17
 
@@ -22,7 +22,20 @@ A modern Django Admin extension focused on UI/UX, dashboard, feedback, file mana
22
22
 
23
23
  ## Changelog
24
24
 
25
- ### v0.2.48.4 (2026-04-11) — 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)
26
39
  **Security: Nâng cấp cbor2 5.9.0 và ujson 5.12.0**
27
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.
28
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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-whiteneuron
3
- Version: 0.2.48.5
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.3
99
+ - 0.3.0
100
100
 
101
101
  ## Compatibility
102
102
 
@@ -107,7 +107,20 @@ A modern Django Admin extension focused on UI/UX, dashboard, feedback, file mana
107
107
 
108
108
  ## Changelog
109
109
 
110
- ### v0.2.48.4 (2026-04-11) — 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)
111
124
  **Security: Nâng cấp cbor2 5.9.0 và ujson 5.12.0**
112
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.
113
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.
@@ -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.5"
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"}
@@ -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
  ]
@@ -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)
@@ -1409,6 +1409,62 @@ msgstr "Trước"
1409
1409
  msgid "Next"
1410
1410
  msgstr "Tiếp theo"
1411
1411
 
1412
+ #: whiteneuron/file_management/admin.py
1413
+ msgid "Integrity"
1414
+ msgstr "Toàn vẹn"
1415
+
1416
+ #: whiteneuron/file_management/admin.py
1417
+ msgid "Stored hash"
1418
+ msgstr "Hash lưu trữ"
1419
+
1420
+ #: whiteneuron/file_management/admin.py
1421
+ msgid "Current hash (live)"
1422
+ msgstr "Hash hiện tại (thực tế)"
1423
+
1424
+ #: whiteneuron/file_management/admin.py
1425
+ msgid "Cannot read file"
1426
+ msgstr "Không đọc được file"
1427
+
1428
+ #: whiteneuron/file_management/admin.py
1429
+ msgid "Cannot read file from storage"
1430
+ msgstr "Không đọc được file từ storage"
1431
+
1432
+ #: whiteneuron/file_management/admin.py
1433
+ msgid "File has been modified"
1434
+ msgstr "File bị thay đổi"
1435
+
1436
+ #: whiteneuron/file_management/admin.py
1437
+ msgid "Verified Download"
1438
+ msgstr "Tải xuống (đã xác thực)"
1439
+
1440
+ #: whiteneuron/file_management/admin.py
1441
+ msgid "Download (Verified)"
1442
+ msgstr "Tải xuống (đã xác thực)"
1443
+
1444
+ #: whiteneuron/file_management/admin.py
1445
+ msgid "File missing"
1446
+ msgstr "File không tồn tại"
1447
+
1448
+ #: whiteneuron/file_management/admin.py
1449
+ msgid "Tampered — download blocked"
1450
+ msgstr "File bị thay đổi — không thể tải"
1451
+
1452
+ #: whiteneuron/file_management/admin.py
1453
+ msgid "Cannot read the file from storage. It may have been deleted externally."
1454
+ msgstr "Không đọc được file từ storage. File có thể đã bị xóa bên ngoài hệ thống."
1455
+
1456
+ #: whiteneuron/file_management/admin.py
1457
+ msgid "File integrity check failed: the file on storage does not match its stored hash. It may have been tampered with or corrupted."
1458
+ msgstr "Kiểm tra toàn vẹn thất bại: file trên storage không khớp với hash đã lưu. File có thể đã bị thay đổi hoặc bị hỏng."
1459
+
1460
+ #: whiteneuron/file_management/views.py
1461
+ msgid "Cannot read file from storage. The file may be missing."
1462
+ msgstr "Không đọc được file từ storage. File có thể đã bị xóa."
1463
+
1464
+ #: whiteneuron/file_management/views.py
1465
+ msgid "File integrity check failed. This file may have been tampered with and cannot be downloaded."
1466
+ msgstr "Kiểm tra toàn vẹn thất bại. File có thể đã bị thay đổi và không thể tải xuống."
1467
+
1412
1468
  #~ msgid "Feedback marked as resolved"
1413
1469
  #~ msgstr "Phản hồi đã được đánh dấu là đã giải quyết"
1414
1470