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.
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/PKG-INFO +16 -3
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/README.md +15 -2
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/django_whiteneuron.egg-info/PKG-INFO +16 -3
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/django_whiteneuron.egg-info/SOURCES.txt +3 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/pyproject.toml +1 -1
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/urls.py +1 -0
- django_whiteneuron-0.3.0/whiteneuron/file_management/admin.py +163 -0
- django_whiteneuron-0.3.0/whiteneuron/file_management/migrations/0004_excelfile_hash_pdffile_hash.py +23 -0
- django_whiteneuron-0.3.0/whiteneuron/file_management/models.py +121 -0
- django_whiteneuron-0.3.0/whiteneuron/file_management/urls.py +7 -0
- django_whiteneuron-0.3.0/whiteneuron/file_management/views.py +50 -0
- django_whiteneuron-0.3.0/whiteneuron/locale/vi/LC_MESSAGES/django.mo +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/locale/vi/LC_MESSAGES/django.po +56 -0
- django_whiteneuron-0.3.0/whiteneuron/static/base/css/styles.css +2 -0
- {django_whiteneuron-0.2.48.5/whiteneuron/file_management → django_whiteneuron-0.3.0/whiteneuron/synchronization}/views.py +0 -0
- django_whiteneuron-0.3.0/whiteneuron/templates/file_management/widgets/clearable_file_input_no_download.html +20 -0
- django_whiteneuron-0.2.48.5/whiteneuron/file_management/admin.py +0 -32
- django_whiteneuron-0.2.48.5/whiteneuron/file_management/models.py +0 -65
- django_whiteneuron-0.2.48.5/whiteneuron/locale/vi/LC_MESSAGES/django.mo +0 -0
- django_whiteneuron-0.2.48.5/whiteneuron/static/base/css/styles.css +0 -2
- django_whiteneuron-0.2.48.5/whiteneuron/synchronization/views.py +0 -3
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/LICENSE +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/django_whiteneuron.egg-info/dependency_links.txt +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/django_whiteneuron.egg-info/requires.txt +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/django_whiteneuron.egg-info/top_level.txt +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/setup.cfg +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/admin.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/apps.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/asgi.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/ckeditor.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/filters.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/forms.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/management/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/management/commands/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/management/commands/init_admin.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/management/commands/init_groupuser.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/management/commands/init_guest.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/middleware.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0001_initial.py +0 -0
- {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
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0003_notification_obj_link.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0004_delete_notification.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0005_user_show_softdelete.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0006_user_is_bot.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0007_app.py +0 -0
- {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
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0009_alter_app_table.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0010_app_category.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0011_alter_app_options.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0012_app_permission.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0013_app_thumbnail_url.py +0 -0
- {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
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0015_ipblacklist.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0016_uablacklist.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/0017_alter_uablacklist_pattern.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/migrations/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/modeladmin.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/models.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/resources.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/settings.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/sites.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/tasks.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/templatetags/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/templatetags/unfold_list_custom.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/templatetags/wn_filters.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/tests.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/thread_local.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/utils.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/views.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/base/widgets.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/admin.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/constants.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/mixins.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/tasks.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_import_export/utils.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_modeltranslation/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_modeltranslation/admin.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/contrib/wn_modeltranslation/utils.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/admin.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/apps.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/migrations/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/models.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/tests.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/dashboard/views.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/admin.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/apps.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/0001_initial.py +0 -0
- {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
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/0003_feedbackdata_note.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/0004_alter_feedbackdata_object_id.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/0005_feedbackdata_field.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/0006_alter_feedbackdata_created_at_and_more.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/migrations/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/models.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/tests.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/urls.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/utils.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/feedbacks/views.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/file_management/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/file_management/apps.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/file_management/migrations/0001_initial.py +0 -0
- {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
- {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
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/file_management/migrations/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/file_management/tests.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/file_management/utils.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/manage.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/admin.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/apps.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/consumers.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/management/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/management/commands/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/management/commands/call_demo_noti.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0001_initial.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0002_alter_notificationconfig_event_type.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0003_notification.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0004_alter_notification_flag.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0005_notification_keywords.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0006_remove_notification_keywords.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0007_notification_changed_data.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0008_alter_notification_changed_data.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0009_notification_action_by_alter_notification_user.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0010_alter_notification_user.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0011_alter_notification_user.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/0012_alter_notification_action_and_more.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/migrations/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/models.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/routing.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/tests.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/utils.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/notification/views.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/css/btn-styles.css +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/css/ckeditor5.css +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/css/loading.css +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/forms/css/html-editor.css +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/forms/js/format.html.js +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/forms/js/trix.config.js +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/delete.svg +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/edit.svg +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/icon-viewlink.svg +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/loading.gif +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/login-bg.jpg +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/login_bg.jpg +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/images/logo/WhiteNeuron.png +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/js/chart.min.js +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/js/d3.v7.min.js +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/js/loading.js +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/static/base/js/whiteneuron.js +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/admin.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/apps.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/migrations/__init__.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/models.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/synchronization/tests.py +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/400.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/403.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/404.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/429.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/500.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/auth/user/add_form.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/base/app_change_list.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/base/image_change_form.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/base/userprofile_change_form.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/base.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/change_list.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/feedbacks/feedback_change_form.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/grid_view_results.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/header.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/index.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/login.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/login_after.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/search_form.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/submit_line.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/wn_change_form.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/admin/wn_history_panel.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/base/forms/helpers/toolbar.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/base/forms/wysiwyg.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/base.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/unfold/helpers/account_links.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/unfold/helpers/app_list_badge.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/unfold/helpers/field_password.html +0 -0
- {django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/templates/unfold/helpers/navigation_user.html +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
{django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/django_whiteneuron.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-whiteneuron
|
|
3
|
-
Version: 0.
|
|
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.
|
|
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.
|
|
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.
|
{django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/django_whiteneuron.egg-info/SOURCES.txt
RENAMED
|
@@ -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.
|
|
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'
|
django_whiteneuron-0.3.0/whiteneuron/file_management/migrations/0004_excelfile_hash_pdffile_hash.py
ADDED
|
@@ -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,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)
|
|
Binary file
|
{django_whiteneuron-0.2.48.5 → django_whiteneuron-0.3.0}/whiteneuron/locale/vi/LC_MESSAGES/django.po
RENAMED
|
@@ -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
|
|