diff --git a/VERSION b/VERSION index cf37bb132..f5160a35e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v2.12.1rc +v3.0.0rc21 diff --git a/article/migrations/0008_add_unique_pid_v3.py b/article/migrations/0008_add_unique_pid_v3.py index 527376806..f4639808d 100644 --- a/article/migrations/0008_add_unique_pid_v3.py +++ b/article/migrations/0008_add_unique_pid_v3.py @@ -38,11 +38,4 @@ class Migration(migrations.Migration): model_name="article", name="article_art_pid_v3_2370cc_idx", ), - migrations.AlterField( - model_name="article", - name="pid_v3", - field=models.CharField( - blank=True, max_length=23, null=True, unique=True, verbose_name="PID v3" - ), - ), ] diff --git a/article/migrations/0009_alter_article_pid_v3_unique.py b/article/migrations/0009_alter_article_pid_v3_unique.py new file mode 100644 index 000000000..b1346857f --- /dev/null +++ b/article/migrations/0009_alter_article_pid_v3_unique.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.3 on 2026-03-16 23:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("article", "0008_add_unique_pid_v3"), + ] + + operations = [ + migrations.AlterField( + model_name="article", + name="pid_v3", + field=models.CharField( + blank=True, max_length=23, null=True, unique=True, verbose_name="PID v3" + ), + ), + ] diff --git a/config/menu.py b/config/menu.py index f4f7041ef..d3cb6a30d 100644 --- a/config/menu.py +++ b/config/menu.py @@ -1,6 +1,7 @@ WAGTAIL_MENU_APPS_ORDER = [ - "Tarefas", + None, "unexpected-error", + "Tasks", "processing", "migration", "journal", @@ -16,11 +17,13 @@ "Images", "Documentos", "Ajuda", + "upload", + "upload-error", ] def get_menu_order(app_name): try: - return WAGTAIL_MENU_APPS_ORDER.index(app_name) + 1 + return WAGTAIL_MENU_APPS_ORDER.index(app_name) except: return 9000 diff --git a/config/settings/base.py b/config/settings/base.py index d42c0636d..3037f7cdf 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -135,7 +135,7 @@ "proc", "publication", "researcher", - # "upload", + "upload", "pid_provider", "team", "tracker", diff --git a/core/forms.py b/core/forms.py index cbe8cae24..cbb742155 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timezone from wagtail.admin.forms import WagtailAdminModelForm @@ -9,10 +9,10 @@ def save_all(self, user): if self.instance.pk is None: model_with_creator.creator = user - model_with_creator.created = datetime.utcnow() + model_with_creator.created = datetime.now(timezone.utc) else: model_with_creator.updated_by = user - model_with_creator.updated = datetime.utcnow() + model_with_creator.updated = datetime.now(timezone.utc) self.save() return model_with_creator diff --git a/core/views.py b/core/views.py index 9c682b00f..e85fef6ed 100644 --- a/core/views.py +++ b/core/views.py @@ -1,42 +1,115 @@ -from django.http import HttpResponseRedirect +""" +ViewSet base com tracking automático de creator/updated_by. + +Qualquer app que use models com os campos creator e updated_by +(herdados de CommonControlField ou equivalente) deve usar +CommonControlFieldViewSet como base do seu SnippetViewSet. + +Uso: + class MeuViewSet(CommonControlFieldViewSet): + model = MeuModel + # add_view_class e edit_view_class já vêm configurados + # permission_policy já vem com BaseSciELOPermissionPolicy + + Se o app precisar de uma CreateView ou EditView customizada, + herde das classes internas para manter o tracking: -from django.utils.translation import gettext_lazy as _ + class MinhaCreateView(CommonControlFieldViewSet.UserTrackingCreateView): + def save_instance(self): + instance = super().save_instance() + # lógica adicional + return instance + + class MeuViewSet(CommonControlFieldViewSet): + add_view_class = MinhaCreateView +""" + +from django.utils import timezone +from django.http import HttpResponseRedirect from wagtail.snippets.views.snippets import CreateView, EditView, SnippetViewSet -from wagtail.admin import messages +from core.permission_helper import StaffWritePolicy + + +class UserTrackingCreateView(CreateView): + """ + CreateView que seta creator e updated_by automaticamente. + Chama super().save_instance() para preservar a lógica do Wagtail + (logs, revisões, etc.) e ajusta os campos antes do save. + + Subclasses que precisem de lógica adicional no form_valid + podem sobrescrever, chamando self.save_instance() para o save: + + def form_valid(self, form): + instance = self.save_instance() + # lógica adicional + return HttpResponseRedirect(self.get_success_url()) + """ + + def save_instance(self): + instance = self.form.save(commit=False) + if not instance.pk: + instance.creator = self.request.user + instance.updated_by = self.request.user + instance.save() + self.form.save_m2m() + return instance -class CommonControlFieldCreateView(CreateView): def form_valid(self, form): - self.object = form.save_all(self.request.user) + self.object = self.save_instance() + return HttpResponseRedirect(self.get_success_url()) + + +class UserTrackingEditView(EditView): + """ + EditView que seta updated_by automaticamente. + + Subclasses que precisem de lógica adicional no form_valid + podem sobrescrever, chamando self.save_instance() para o save: + + def form_valid(self, form): + instance = self.save_instance() + # lógica adicional + return HttpResponseRedirect(self.get_success_url()) + """ + + def save_instance(self): + instance = self.form.save(commit=False) + instance.updated_by = self.request.user + instance.save() + self.form.save_m2m() + return instance + + def form_valid(self, form): + self.object = self.save_instance() return HttpResponseRedirect(self.get_success_url()) class CommonControlFieldViewSet(SnippetViewSet): """ - Mixin para adicionar tracking de usuário em qualquer SnippetViewSet - Compatível com Wagtail 6.4.2 + SnippetViewSet base para models com campos creator/updated_by. + + Fornece: + - UserTrackingCreateView: seta creator (na criação) e updated_by + - UserTrackingEditView: seta updated_by + - BaseSciELOPermissionPolicy como permission_policy padrão + + Nota sobre timestamps: + - Se o model usa `updated = DateTimeField(auto_now=True)`, o Django + atualiza automaticamente ao chamar save(). + - Se NÃO usa auto_now, descomente a linha de updated nas views acima. """ - class UserTrackingCreateView(CreateView): - def save_instance(self): - instance = self.form.save(commit=False) - if not instance.pk: - instance.creator = self.request.user - instance.updated_by = self.request.user - instance.save() - if hasattr(self.form, "save_m2m"): - self.form.save_m2m() - return instance - - class UserTrackingEditView(EditView): - def save_instance(self): - instance = self.form.save(commit=False) - instance.updated_by = self.request.user - instance.save() - if hasattr(self.form, "save_m2m"): - self.form.save_m2m() - return instance - - # Define as views customizadas add_view_class = UserTrackingCreateView edit_view_class = UserTrackingEditView + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + # Se a subclasse tem model e não definiu policy própria, + # cria uma StaffWritePolicy como default + if ( + hasattr(cls, "model") + and cls.model is not None + and "permission_policy" not in cls.__dict__ + ): + cls.permission_policy = StaffWritePolicy(cls.model) \ No newline at end of file diff --git a/journal/forms.py b/journal/forms.py index 459347a04..430f6d53f 100644 --- a/journal/forms.py +++ b/journal/forms.py @@ -13,3 +13,10 @@ def save_all(self, user): self.save() return journal + + +class JournalTOCForm(CoreAdminModelForm): + def save_all(self, user): + obj = super().save_all(user) + self.save() + return obj diff --git a/journal/models.py b/journal/models.py index 234d22cd2..e934678ac 100644 --- a/journal/models.py +++ b/journal/models.py @@ -21,7 +21,7 @@ MissionGetError, SubjectCreationOrUpdateError, ) -from journal.forms import OfficialJournalForm +from journal.forms import OfficialJournalForm, JournalTOCForm from location.models import Location diff --git a/locale/README.rst b/locale/README.rst deleted file mode 100644 index c2f1dcd6f..000000000 --- a/locale/README.rst +++ /dev/null @@ -1,6 +0,0 @@ -Translations -============ - -Translations will be placed in this folder when running:: - - python manage.py makemessages diff --git a/migration/models.py b/migration/models.py index be56e0524..472fdda30 100644 --- a/migration/models.py +++ b/migration/models.py @@ -870,15 +870,6 @@ class IdFileRecord(CommonControlField, Orderable): data = models.JSONField() item_pid = models.CharField(_("PID"), max_length=23) item_type = models.CharField(_("Type"), max_length=10) - # issue_folder = models.CharField(_("Issue folder"), max_length=30) - # article_filename = models.CharField( - # _("Filename"), max_length=40, null=True, blank=True - # ) - # article_filetype = models.CharField( - # _("File type"), max_length=4, null=True, blank=True - # ) - # processing_date = models.CharField(max_length=8, null=True, blank=True) - # deleted = models.BooleanField(default=False) todo = models.BooleanField(default=True) panels = [ @@ -1047,3 +1038,10 @@ def document_records_to_migrate(cls, collection, issue_pid, force_update): if not force_update: params["todo"] = True return cls.objects.filter(item_type="article", **params) + + @classmethod + def add_issue_folder(cls, issue_pid, issue_folder): + return cls.objects.filter( + Q(item_pid__startswith=f"S{issue_pid}") | Q(item_pid=issue_pid), + issue_folder__in=["", None] + ).update(issue_folder=issue_folder) diff --git a/proc/tasks.py b/proc/tasks.py index 020443886..92a71ae7b 100644 --- a/proc/tasks.py +++ b/proc/tasks.py @@ -3,7 +3,6 @@ import traceback from django.contrib.auth import get_user_model -from django.db.models import Q from django.utils.translation import gettext_lazy as _ from article.models import Article @@ -665,6 +664,7 @@ def task_publish_issues( } try: + user = _get_user(user_id, username) params = {} if journal_acron: params["journal_proc__acron"] = journal_acron diff --git a/production.yml b/production-v3.0.0rc4.yml similarity index 100% rename from production.yml rename to production-v3.0.0rc4.yml diff --git a/team/migrations/0004_rename_team_collec_collect_idx_team_collec_collect_0ee96e_idx_and_more.py b/team/migrations/0004_rename_team_collec_collect_idx_team_collec_collect_0ee96e_idx_and_more.py new file mode 100644 index 000000000..1f47e1ce9 --- /dev/null +++ b/team/migrations/0004_rename_team_collec_collect_idx_team_collec_collect_0ee96e_idx_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 5.2.3 on 2026-02-19 18:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("team", "0003_add_role_to_collectionteammember"), + ] + + operations = [ + migrations.RenameIndex( + model_name="collectionteammember", + new_name="team_collec_collect_0ee96e_idx", + old_name="team_collec_collect_idx", + ), + migrations.RenameIndex( + model_name="collectionteammember", + new_name="team_collec_user_id_6382ef_idx", + old_name="team_collec_user_id_idx", + ), + migrations.AddField( + model_name="company", + name="certified_since", + field=models.DateField( + blank=True, null=True, verbose_name="Certified Since" + ), + ), + migrations.AddField( + model_name="company", + name="logo", + field=models.ImageField( + blank=True, null=True, upload_to="logos/", verbose_name="Logo" + ), + ), + migrations.AddField( + model_name="company", + name="personal_contact", + field=models.CharField( + blank=True, max_length=30, null=True, verbose_name="Personal Contact" + ), + ), + migrations.AddField( + model_name="company", + name="url", + field=models.URLField(blank=True, null=True, verbose_name="URL"), + ), + ] diff --git a/team/wagtail_hooks.py b/team/wagtail_hooks.py index cfea8688b..31590dec4 100644 --- a/team/wagtail_hooks.py +++ b/team/wagtail_hooks.py @@ -3,8 +3,11 @@ from wagtail.snippets.views.snippets import SnippetViewSet, SnippetViewSetGroup from config.menu import get_menu_order -from core.views import CommonControlFieldCreateView -from core.forms import CoreAdminModelForm +from core.views import UserTrackingCreateView, UserTrackingEditView +from core.permission_helper import ( + ReadOnlyPolicy, + StaffWritePolicy, +) from .models import ( CollectionTeamMember, Company, @@ -12,16 +15,33 @@ JournalCompanyContract, JournalTeamMember, ) +from .permission_helper import ( + user_is_collection_staff, + user_can_manage_journals, + get_user_journal_ids, + get_user_company_ids, +) + + +# =================================================================== +# ViewSets — Collection (somente super gerencia) +# =================================================================== class CollectionTeamMemberViewSet(SnippetViewSet): + """ + view: qualquer logado (filtrado por collection do usuário) + add/change: só super + delete: só super + """ model = CollectionTeamMember + permission_policy = ReadOnlyPolicy(CollectionTeamMember) menu_label = _("Collection Team Members") menu_icon = "group" add_to_settings_menu = False exclude_from_explorer = False - base_form_class = CoreAdminModelForm - add_view_class = CommonControlFieldCreateView + add_view_class = UserTrackingCreateView + edit_view_class = UserTrackingEditView list_display = ( "user", @@ -43,17 +63,28 @@ def get_queryset(self, request): qs = super().get_queryset(request) if request.user.is_superuser: return qs + # Usuário vê apenas membros das suas collections return CollectionTeamMember.members(request.user) +# =================================================================== +# ViewSets — Company (somente super gerencia, view filtrada) +# =================================================================== + + class CompanyViewSet(SnippetViewSet): + """ + view: qualquer logado (staff vê todas, company member vê a sua) + add/change/delete: só super + """ model = Company + permission_policy = ReadOnlyPolicy(Company) menu_label = _("Companies") menu_icon = "group" add_to_settings_menu = False exclude_from_explorer = False - add_view_class = CommonControlFieldCreateView - base_form_class = CoreAdminModelForm + add_view_class = UserTrackingCreateView + edit_view_class = UserTrackingEditView list_display = ( "name", @@ -71,19 +102,33 @@ class CompanyViewSet(SnippetViewSet): "url", ) + def get_queryset(self, request): + qs = super().get_queryset(request) + company_ids = get_user_company_ids(request.user) + if company_ids is None: + return qs # super ou staff: vê todas + if company_ids: + return qs.filter(id__in=company_ids) + return qs.none() -class JournalTeamMemberViewSet(SnippetViewSet): - model = JournalTeamMember - menu_label = _("Journal Team Members") + +class CompanyTeamMemberViewSet(SnippetViewSet): + """ + view: qualquer logado (filtrado pela company do usuário) + add/change/delete: só super + """ + model = CompanyTeamMember + permission_policy = ReadOnlyPolicy(CompanyTeamMember) + menu_label = _("Company Team Members") menu_icon = "user" add_to_settings_menu = False exclude_from_explorer = False - add_view_class = CommonControlFieldCreateView - base_form_class = CoreAdminModelForm + add_view_class = UserTrackingCreateView + edit_view_class = UserTrackingEditView list_display = ( "user", - "journal", + "company", "role", "is_active_member", "created", @@ -93,22 +138,45 @@ class JournalTeamMemberViewSet(SnippetViewSet): "user__username", "user__email", "user__name", - "journal__title", + "company__name", ) + def get_queryset(self, request): + qs = super().get_queryset(request) + company_ids = get_user_company_ids(request.user) + if company_ids is None: + return qs # super ou staff: vê todos + if company_ids: + return qs.filter(company_id__in=company_ids) + return qs.none() -class CompanyTeamMemberViewSet(SnippetViewSet): - model = CompanyTeamMember - menu_label = _("Company Team Members") + +# =================================================================== +# ViewSets — Journal (staff + journal admin gerenciam) +# =================================================================== + + +class JournalTeamMemberViewSet(SnippetViewSet): + """ + view: qualquer logado (filtrado por journals acessíveis) + add/change: super + collection staff + journal admin + delete: só super + """ + model = JournalTeamMember + permission_policy = StaffWritePolicy( + JournalTeamMember, + staff_check=user_can_manage_journals, + ) + menu_label = _("Journal Team Members") menu_icon = "user" add_to_settings_menu = False exclude_from_explorer = False - add_view_class = CommonControlFieldCreateView - base_form_class = CoreAdminModelForm + add_view_class = UserTrackingCreateView + edit_view_class = UserTrackingEditView list_display = ( "user", - "company", + "journal", "role", "is_active_member", "created", @@ -118,18 +186,37 @@ class CompanyTeamMemberViewSet(SnippetViewSet): "user__username", "user__email", "user__name", - "company__name", + "journal__title", ) + def get_queryset(self, request): + qs = super().get_queryset(request) + journal_ids = get_user_journal_ids(request.user) + if journal_ids is None: + return qs # superuser: vê todos + if journal_ids: + return qs.filter(journal_id__in=journal_ids) + return qs.none() + class JournalCompanyContractViewSet(SnippetViewSet): + """ + view: super + collection staff + journal admin (filtrado por journals) + add/change: super + collection staff + journal admin + delete: só super + """ model = JournalCompanyContract + permission_policy = StaffWritePolicy( + JournalCompanyContract, + access_check=user_can_manage_journals, + staff_check=user_can_manage_journals, + ) menu_label = _("Journal-Company Contracts") menu_icon = "doc-full" add_to_settings_menu = False exclude_from_explorer = False - add_view_class = CommonControlFieldCreateView - base_form_class = CoreAdminModelForm + add_view_class = UserTrackingCreateView + edit_view_class = UserTrackingEditView list_display = ( "journal", @@ -144,11 +231,22 @@ class JournalCompanyContractViewSet(SnippetViewSet): "company__name", ) + def get_queryset(self, request): + qs = super().get_queryset(request) + journal_ids = get_user_journal_ids(request.user) + if journal_ids is None: + return qs # superuser: vê todos + if journal_ids: + return qs.filter(journal_id__in=journal_ids) + return qs.none() + + +# =================================================================== +# Grupo de menu +# =================================================================== + class TeamViewSetGroup(SnippetViewSetGroup): - """ - Group of ViewSets for Team Management - """ items = [ CollectionTeamMemberViewSet, CompanyViewSet, @@ -161,5 +259,4 @@ class TeamViewSetGroup(SnippetViewSetGroup): menu_order = get_menu_order("team") -register_snippet(TeamViewSetGroup) - +register_snippet(TeamViewSetGroup) \ No newline at end of file diff --git a/upload/button_helper.py b/upload/button_helper.py index 34a8a5e15..76d9b5a8d 100644 --- a/upload/button_helper.py +++ b/upload/button_helper.py @@ -1,99 +1,86 @@ -# button_helper.py - ATUALIZADO +""" +Botões customizados na listagem de snippets do módulo Upload. + +Usa core.snippet_buttons para registro genérico e +upload.permissions para regras de negócio. +""" + from django.urls import reverse from django.utils.translation import gettext_lazy as _ -# Mudança: wagtail.contrib.modeladmin -> wagtail_modeladmin -from wagtail_modeladmin.helpers import ButtonHelper + +from core.snippet_buttons import register_snippet_buttons, make_button from . import choices +from .permission_helper import UploadPermissions -class UploadButtonHelper(ButtonHelper): - index_button_classnames = ["button", "button-small", "button-secondary"] - btn_default_classnames = [ - "button-small", - "icon", - ] +# --------------------------------------------------------------- +# Lazy import para evitar import circular +# --------------------------------------------------------------- +_BUTTON_MODELS = None - def get_buttons_for_obj( - self, obj, exclude=None, classnames_add=None, classnames_exclude=None - ): - """ - This function is used to gather all available buttons. - We append our custom button to the btns list. - """ - ph = self.permission_helper - usr = self.request.user - url_name = self.request.resolver_match.url_name - print(f"urlname: {url_name}") - analyst_team_member = ph.user_is_analyst_team_member(usr, obj) - - exclude = ["delete"] - if not analyst_team_member: - exclude.append("edit") - - if url_name.endswith("_modeladmin_inspect"): - exclude.append("inspect") - - btns = super().get_buttons_for_obj( - obj, exclude, classnames_add, classnames_exclude + +def _get_button_models(): + global _BUTTON_MODELS + if _BUTTON_MODELS is None: + from .models import ( + Package, + QAPackage, + ReadyToPublishPackage, + ArchivedPackage, ) - - if not obj.is_validation_finished: - return btns - - classnames = [] - if url_name.endswith("_modeladmin_inspect"): - classnames.extend(ButtonHelper.inspect_button_classnames) - if url_name.endswith("_modeladmin_index"): - classnames.extend(UploadButtonHelper.index_button_classnames) - - self.add_finish_deposit_button(btns, obj, classnames, url_name) - - if analyst_team_member: - self.add_assign_button(btns, obj, classnames) - self.add_archive_button(btns, obj, classnames) - - return btns + _BUTTON_MODELS = (Package, QAPackage, ReadyToPublishPackage, ArchivedPackage) + return _BUTTON_MODELS + + +# --------------------------------------------------------------- +# Gerador de botões condicionais +# --------------------------------------------------------------- +def _upload_buttons(obj, user): + """ + Gera botões condicionais baseados em status e permissão. + Equivalente ao antigo UploadButtonHelper.get_buttons_for_obj(). + """ + is_staff = UploadPermissions.user_is_staff(user) - def add_assign_button(self, btns, obj, classnames, label=None): - status = ( + # Só mostra botões customizados se a validação terminou + if hasattr(obj, "is_validation_finished") and not obj.is_validation_finished: + return + + # Botão: Finalizar depósito + if obj.status == choices.PS_VALIDATED_WITH_ERRORS: + yield make_button( + _("Finish deposit"), + reverse("upload:finish_deposit") + f"?package_id={obj.id}", + priority=10, + ) + + # Botões exclusivos de analista (staff) + if is_staff: + # Aceitar / Rejeitar / Delegar + if obj.status in ( choices.PS_PENDING_QA_DECISION, choices.PS_VALIDATED_WITH_ERRORS, - ) - if obj.status in status: - text = label or _("Accept / Reject the package or delegate it") - btns.append( - { - "url": reverse("upload:assign") + "?package_id=%s" % str(obj.id), - "label": text, - "classname": self.finalise_classname(classnames), - "title": text, - } + ): + yield make_button( + _("Accept / Reject the package or delegate it"), + reverse("upload:assign") + f"?package_id={obj.id}", + priority=20, ) - def add_finish_deposit_button(self, btns, obj, classnames, url_name): - status = (choices.PS_VALIDATED_WITH_ERRORS,) - if obj.status in status and url_name == "upload_package_modeladmin_inspect": - text = _("Finish deposit") - btns.append( - { - "url": reverse("upload:finish_deposit") - + "?package_id=%s" % str(obj.id), - "label": text, - "classname": self.finalise_classname(classnames), - "title": text, - } + # Arquivar + if obj.status == choices.PS_UNEXPECTED: + yield make_button( + _("Archive"), + reverse("upload:archive_package") + f"?package_id={obj.id}", + priority=30, ) - def add_archive_button(self, btns, obj, classnames, label=None): - status = (choices.PS_UNEXPECTED,) - if obj.status in status: - text = label or _("Archive") - btns.append( - { - "url": reverse("upload:archive_package") - + "?package_id=%s" % str(obj.id), - "label": text, - "classname": self.finalise_classname(classnames), - "title": text, - } - ) \ No newline at end of file + +# --------------------------------------------------------------- +# Registro do hook +# --------------------------------------------------------------- +register_snippet_buttons( + model_or_models=_get_button_models(), + access_check=UploadPermissions.user_can_access, + button_generator=_upload_buttons, +) \ No newline at end of file diff --git a/upload/controller.py b/upload/controller.py index ce4d7acc2..d5494ef8a 100644 --- a/upload/controller.py +++ b/upload/controller.py @@ -15,6 +15,9 @@ JournalDataChecker, IssueDataChecker, ) +from upload.utils import file_utils, xml_utils + +pp = PidRequester() from upload.models import ( Package, @@ -292,6 +295,7 @@ def _check_article_and_journal(package, xml_with_pre, user): # verifica se journal e issue estão registrados xmltree = xml_with_pre.xmltree + journal_checker = UploadJournalDataChecker.from_xmltree(xmltree, user) journal_checker.check(response) logging.info(f"UploadJournalDataChecker.check: {response}") diff --git a/upload/forms.py b/upload/forms.py index 57f038bd1..27b5f9e3b 100644 --- a/upload/forms.py +++ b/upload/forms.py @@ -11,17 +11,6 @@ from upload import choices -class PackageZipForm(CoreAdminModelForm): - def save_all(self, user): - pkg_zip = super().save_all(user) - - pkg_zip.name, ext = os.path.splitext(os.path.basename(pkg_zip.file.name)) - logging.info(pkg_zip.name) - self.save() - - return pkg_zip - - class UploadPackageForm(CoreAdminModelForm): ... diff --git a/upload/migrations/0001_initial.py b/upload/migrations/0001_initial.py index f819f923b..25e53659f 100644 --- a/upload/migrations/0001_initial.py +++ b/upload/migrations/0001_initial.py @@ -14,7 +14,7 @@ class Migration(migrations.Migration): ("article", "0002_remove_articleauthor_author_and_more"), ("collection", "0003_websiteconfigurationendpoint"), ("issue", "0004_issue_issue_pid_suffix_issue_order_toc_tocsection"), - ("journal", "0005_journaltoc_alter_journal_official_journal_and_more"), + ("journal", "0005_officialjournal_next_journal_title_and_more"), ("package", "0003_remove_spspkg_components_remove_spspkg_scheduled_and_more"), ("proc", "0006_issueproc_resumption_date"), ("team", "0001_initial"), diff --git a/upload/migrations/0002_package_main_doi_and_more.py b/upload/migrations/0002_package_main_doi_and_more.py index 3434f2692..aafef5a85 100644 --- a/upload/migrations/0002_package_main_doi_and_more.py +++ b/upload/migrations/0002_package_main_doi_and_more.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ ("article", "0002_remove_articleauthor_author_and_more"), ("issue", "0004_issue_issue_pid_suffix_issue_order_toc_tocsection"), - ("journal", "0005_journaltoc_alter_journal_official_journal_and_more"), + ("journal", "0005_officialjournal_next_journal_title_and_more"), ("package", "0003_remove_spspkg_components_remove_spspkg_scheduled_and_more"), ("team", "0001_initial"), ("upload", "0001_initial"), diff --git a/upload/models.py b/upload/models.py index daf5c39c1..28699ed9d 100644 --- a/upload/models.py +++ b/upload/models.py @@ -48,13 +48,11 @@ from team.models import CollectionTeamMember from upload import choices from upload.forms import ( - ReadyToPublishPackageForm, QAPackageForm, - UploadPackageForm, + ReadyToPublishPackageForm, UploadValidatorForm, ValidationResultForm, - XMLErrorReportForm, - PackageZipForm, + XMLErrorReportForm ) from upload.permission_helper import ACCESS_ALL_PACKAGES, ASSIGN_PACKAGE, FINISH_DEPOSIT from upload.utils import file_utils @@ -124,7 +122,6 @@ class PackageZip(CommonControlField): FieldPanel("show_package_validations"), ] - base_form_class = PackageZipForm class Meta: verbose_name = _("Zip file") @@ -136,6 +133,19 @@ class Meta: ] ), ] + permissions = [ + ("access_all_packages", _("Can access all packages")), + ("assign_package", _("Can assign package")), + ("finish_deposit", _("Can finish deposit")), + ( + "send_validation_error_resolution", + _("Can send validation error resolution"), + ), + ( + "analyse_validation_error_resolution", + _("Can analyse validation error resolution"), + ), + ] def __str__(self): return self.file.name @@ -314,7 +324,6 @@ class Package(CommonControlField, ClusterableModel): ObjectList(panel_event, heading=_("Events")), ] ) - base_form_class = UploadPackageForm class Meta: verbose_name = _("Package admin") @@ -1337,7 +1346,7 @@ class QAPackage(Package): QA can approve or reject """ - + base_form_class = QAPackageForm panel_id = [ AutocompletePanel("article", read_only=False), ] @@ -1369,7 +1378,6 @@ class QAPackage(Package): ObjectList(panel_event, heading=_("Events")), ] ) - base_form_class = QAPackageForm class Meta: proxy = True @@ -1381,7 +1389,7 @@ class ReadyToPublishPackage(Package): """ Package ready to publish """ - + base_form_class = ReadyToPublishPackageForm panel_id = [ AutocompletePanel("article", read_only=False), ] @@ -1422,7 +1430,6 @@ class ReadyToPublishPackage(Package): ] ) - base_form_class = ReadyToPublishPackageForm class Meta: proxy = True @@ -1449,8 +1456,8 @@ class BaseValidationResult(CommonControlField): null=True, blank=True, ) - base_form_class = ValidationResultForm + panels = [ FieldPanel("subject", read_only=True), FieldPanel("status", read_only=True), @@ -1667,6 +1674,8 @@ class XMLError(BaseXMLValidationResult, ClusterableModel): blank=True, ) + base_form_class = XMLErrorReportForm + panels = [ FieldPanel("status", read_only=True), FieldPanel("advice", read_only=True), @@ -1717,13 +1726,6 @@ def get_numbers(cls, package, report=None): .values("status", "reaction") .annotate(total=Count("id")) ): - status_key = "total_" + (item.get("status") or "").lower() - if status_key not in items: - continue - items[status_key] += item["total"] - reaction_key = "total_" + (item.get("reaction") or "") - if reaction_key in items: - items[reaction_key] += item["total"] total += item["total"] items["total"] = total @@ -1749,7 +1751,6 @@ class BaseValidationReport(CommonControlField): blank=False, ) ValidationResultClass = BaseValidationResult - base_form_class = ValidationResultForm panels = [ AutocompletePanel("package", read_only=True), @@ -1976,7 +1977,6 @@ class XMLErrorReport(BaseValidationReport, ClusterableModel): ] ) - base_form_class = XMLErrorReportForm ValidationResultClass = XMLError class Meta: @@ -2057,6 +2057,7 @@ class UploadValidator(CommonControlField): ) validation_params = models.JSONField(null=True, blank=True) + base_form_class = UploadValidatorForm panels = [ FieldPanel("max_xml_warnings_percentage"), FieldPanel("max_xml_errors_percentage"), @@ -2065,7 +2066,6 @@ class UploadValidator(CommonControlField): FieldPanel("publication_rule"), FieldPanel("validation_params"), ] - base_form_class = UploadValidatorForm # def __str__(self): # if self.collection: diff --git a/upload/permission_helper.py b/upload/permission_helper.py index d620deeac..5e5311e99 100644 --- a/upload/permission_helper.py +++ b/upload/permission_helper.py @@ -1,8 +1,15 @@ -from wagtail_modeladmin.helpers import PermissionHelper +""" +Permissões específicas do módulo Upload. +Herda de core.permissions.AppPermissions e define as regras de negócio +próprias: quem pode acessar, quem é analista, permissões granulares. +""" + +from core.permission_helper import AppPermissions from team.models import has_permission, CollectionTeamMember +# Codenames de permissões específicas do Upload ACCESS_ALL_PACKAGES = "access_all_packages" ASSIGN_PACKAGE = "assign_package" FINISH_DEPOSIT = "finish_deposit" @@ -10,34 +17,37 @@ ANALYSE_VALIDATION_ERROR_RESOLUTION = "analyse_validation_error_resolution" -class UploadPermissionHelper(PermissionHelper): - def user_can_packagezip_create(self, user, obj): - return self.user_can_use_upload_module(user, obj) +class UploadPermissions(AppPermissions): + app_label = "upload" - def user_can_use_upload_module(self, user, obj): + @staticmethod + def user_can_access(user): + """Verifica se o usuário tem acesso ao módulo Upload.""" return has_permission(user) - def user_can_access_all_packages(self, user, obj): - return self.user_has_specific_permission(user, ACCESS_ALL_PACKAGES) - - def user_can_assign_package(self, user, obj): - return self.user_has_specific_permission(user, ASSIGN_PACKAGE) + @staticmethod + def user_is_staff(user): + """Verifica se o usuário é membro de equipe analista.""" + if UploadPermissions.user_can_access(user): + return CollectionTeamMember.objects.filter(user=user).exists() + return False - def user_can_finish_deposit(self, user, obj): - return self.user_has_specific_permission(user, FINISH_DEPOSIT) + @classmethod + def user_can_access_all_packages(cls, user): + return cls.user_has_permission(user, ACCESS_ALL_PACKAGES) - def user_can_send_error_validation_resolution(self, user, obj): - return self.user_has_specific_permission(user, SEND_VALIDATION_ERROR_RESOLUTION) + @classmethod + def user_can_assign_package(cls, user): + return cls.user_has_permission(user, ASSIGN_PACKAGE) - def user_can_analyse_error_validation_resolution(self, user, obj): - return self.user_has_specific_permission( - user, ANALYSE_VALIDATION_ERROR_RESOLUTION - ) + @classmethod + def user_can_finish_deposit(cls, user): + return cls.user_has_permission(user, FINISH_DEPOSIT) - def user_is_analyst_team_member(self, user, obj): - if self.user_can_use_upload_module(user, obj): - return CollectionTeamMember.objects.filter(user=user).exists() + @classmethod + def user_can_send_error_validation_resolution(cls, user): + return cls.user_has_permission(user, SEND_VALIDATION_ERROR_RESOLUTION) - def user_is_xml_producer(self, user, obj): - if self.user_can_use_upload_module(user, obj): - return not self.user_is_analyst_team_member(user, obj) + @classmethod + def user_can_analyse_error_validation_resolution(cls, user): + return cls.user_has_permission(user, ANALYSE_VALIDATION_ERROR_RESOLUTION) diff --git a/upload/publication.py b/upload/publication.py new file mode 100644 index 000000000..0c0d3ea4f --- /dev/null +++ b/upload/publication.py @@ -0,0 +1,252 @@ +import logging +import sys + +from django.contrib.auth import get_user_model + +from collection.models import WebSiteConfiguration +from collection.choices import PUBLIC, QA +from core.utils.requester import fetch_data +from proc.models import ArticleProc, IssueProc, JournalProc +from proc.source_core_api import create_or_update_journal, create_or_update_issue +from publication.api.document import publish_article +from publication.api.issue import publish_issue +from publication.api.journal import publish_journal +from publication.api.publication import PublicationAPI +from tracker.models import UnexpectedEvent + + +User = get_user_model() + + +class PublicationError(Exception): + """Erro durante o processo de publicação""" + + pass + + +def check_article_is_published(article, website): + """ + Verifica se um artigo já está publicado em um website + + Args: + article: O artigo a ser verificado + journal_proc: O processador do journal + website: A configuração do website + user: O usuário que executa a operação + manager: O gerenciador da operação + + Returns: + bool: True se o artigo já está publicado, False caso contrário + """ + try: + article_url = f"{website.url}/j/{article.journal.journal_acron}/a/{article.pid_v3}/?format=xml" + return bool(fetch_data(article_url)) + except Exception as e: + logging.exception(e) + return False + + +def ensure_published_journal(journal_proc, website, user, api_data, force_update=None): + """ + Publica um journal em um website + + Args: + journal_proc: O processador do journal + website: A configuração do website + user: O usuário que executa a operação + api_data: Os dados da API para publicação + + Returns: + dict: Resultado da publicação com detalhes + + Raises: + PublicationError: Se ocorrer erro na publicação + """ + + try: + if not force_update: + journal_url = ( + f"{website.url}/scielo.php?pid={journal_proc.pid}&script=sci_serial" + ) + return bool(fetch_data(journal_url)) + except Exception as e: + pass + + response = journal_proc.publish( + user, + publish_journal, + website_kind=website.purpose, + api_data=api_data, + force_update=True, + content_type="journal", + ) + if not response or not response.get("completed"): + raise PublicationError( + f"Unable to publish {journal_proc} on {website.purpose}: {response}" + ) + return response + + +def ensure_published_issue(issue_proc, website, user, api_data, force_update=None): + """ + Publica um issue em um website + + Args: + manager: O gerenciador da operação + issue_proc: O processador do issue + website: A configuração do website + user: O usuário que executa a operação + api_data: Os dados da API para publicação + + Returns: + dict: Resultado da publicação com detalhes + + Raises: + PublicationError: Se ocorrer erro na publicação + """ + try: + if not force_update: + issue_url = ( + f"{website.url}/scielo.php?pid={issue_proc.pid}&script=sci_issuetoc" + ) + return bool(fetch_data(issue_url)) + except Exception as e: + pass + + response = issue_proc.publish( + user, + publish_issue, + website_kind=website.purpose, + api_data=api_data, + force_update=True, + content_type="issue", + ) + if not response or not response.get("completed"): + raise PublicationError( + f"Unable to publish {issue_proc} on {website.purpose}: {response}" + ) + return response + + +def publish_article_collection_websites( + user, manager, website_kinds, force_journal_publication, force_issue_publication +): + journal = manager.journal + for issue_proc in IssueProc.objects.filter( + issue=manager.article.issue, + ): + if journal.missing_fields: + event = issue_proc.journal_proc.start(user, "Missing journal data") + event.finish( + user, detail={"journal_missing_fields": journal.missing_fields} + ) + + collection = issue_proc.collection + qa_published = None + + if QA in website_kinds: + published_article = publish_article_on_website( + user, + manager, + issue_proc, + QA, + force_journal_publication, + force_issue_publication, + ) + qa_published = published_article and published_article.get("completed") + yield { + "collection": collection.acron, + "website": QA, + "published": published_article and published_article.get("completed"), + } + else: + try: + qa_website = WebSiteConfiguration.get(collection=collection, purpose=QA) + qa_published = check_article_is_published(manager.article, qa_website) + except WebSiteConfiguration.DoesNotExist as exc: + qa_published = None + + if qa_published and PUBLIC in website_kinds: + published_article = publish_article_on_website( + user, + manager, + issue_proc, + PUBLIC, + force_journal_publication, + force_issue_publication, + ) + yield { + "collection": collection.acron, + "website": PUBLIC, + "published": published_article and published_article.get("completed"), + } + + +def publish_article_on_website( + user, + manager, + issue_proc, + website_kind, + force_journal_publication=None, + force_issue_publication=None, +): + """ + Publica um artigo verificando e garantindo as dependências (journal e issue) + + Args: + manager: O gerenciador da operação + journal_proc: O processador do journal + issue_proc: O processador do issue + website: A configuração do website + user: O usuário que executa a operação + api_data: Os dados da API para publicação + + Returns: + dict: Resultado da publicação com detalhes e histórico + + Raises: + PublicationError: Se ocorrer erro na publicação + """ + collection = issue_proc.collection + + logging.info(f"Publishing article on {collection} {website_kind}") + try: + website = WebSiteConfiguration.get( + collection=collection, + purpose=website_kind, + ) + api = PublicationAPI( + post_data_url=website.api_url_article, + get_token_url=website.api_get_token_url, + username=website.api_username, + password=website.api_password, + timeout=15, + ) + api.get_token() + api_data = api.data + except WebSiteConfiguration.DoesNotExist as exc: + return + + journal_proc = issue_proc.journal_proc + + api_data["post_data_url"] = website.api_url_journal + response = ensure_published_journal( + journal_proc, website, user, api_data, force_journal_publication + ) + + api_data["post_data_url"] = website.api_url_issue + response = ensure_published_issue( + issue_proc, website, user, api_data, force_issue_publication + ) + + api_data["post_data_url"] = website.api_url_article + response = manager.publish( + user, + publish_article, + website_kind=website.purpose, + api_data=api_data, + force_update=True, + content_type="article", + bundle_id=issue_proc.bundle_id, + ) + return response diff --git a/upload/templates/modeladmin/upload/package/validation_report/edit.html b/upload/templates/modeladmin/upload/package/validation_report/edit.html new file mode 100644 index 000000000..e179238eb --- /dev/null +++ b/upload/templates/modeladmin/upload/package/validation_report/edit.html @@ -0,0 +1,19 @@ +{% extends "modeladmin/edit.html" %} +{% load i18n modeladmin_tags wagtailadmin_tags wagtailcore_tags %} +{% block header %} + {% include "modeladmin/includes/header_with_history.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon merged=1 latest_log_entry=latest_log_entry history_url=history_url %} +
+

+ + {% icon name="arrow-left" classname="default" %} + {% blocktrans trimmed with view.verbose_name as model_name %}Back to {{ model_name }} list{% endblocktrans %} + +

+ +
+{% endblock %} diff --git a/upload/templates/modeladmin/upload/package/xml.html b/upload/templates/modeladmin/upload/package/xml.html new file mode 100644 index 000000000..9391ddb71 --- /dev/null +++ b/upload/templates/modeladmin/upload/package/xml.html @@ -0,0 +1,16 @@ +{% extends "modeladmin/inspect.html" %} +{% load i18n modeladmin_tags wagtailadmin_tags wagtailcore_tags %} + +{% block content %} + +code + + code +
+ {{ xml }} + +{% endblock %} \ No newline at end of file diff --git a/upload/templates/modeladmin/upload/qapackage/index_results.html b/upload/templates/modeladmin/upload/qapackage/index_results.html new file mode 100644 index 000000000..76c3ca49e --- /dev/null +++ b/upload/templates/modeladmin/upload/qapackage/index_results.html @@ -0,0 +1,13 @@ +{% extends "modeladmin/listing_results.html" %} +{% extends "modeladmin/index_results.html" %} +{% load i18n wagtailadmin_tags %} + +ssssssssss +{% block no_results_message %} + {% fragment as no_results_message %} + + {{ no_results_text|capfirst }} + {% endfragment %} + +

{{ no_results_message }}

+{% endblock %} \ No newline at end of file diff --git a/upload/templates/modeladmin/upload/validationreport/edit.html b/upload/templates/modeladmin/upload/validationreport/edit.html new file mode 100644 index 000000000..501785e5c --- /dev/null +++ b/upload/templates/modeladmin/upload/validationreport/edit.html @@ -0,0 +1,19 @@ +{% extends "modeladmin/edit.html" %} +{% load i18n modeladmin_tags wagtailadmin_tags wagtailcore_tags %} +{% block header %} + {% include "modeladmin/includes/header_with_history.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon merged=1 latest_log_entry=latest_log_entry history_url=history_url %} +
+

+ + {% icon name="arrow-left" classname="default" %} + {% blocktrans trimmed with view.verbose_name as model_name %}Back to package validations{% endblocktrans %} + +

+ +
+{% endblock %} diff --git a/upload/templates/modeladmin/upload/xmlerrorreport/edit.html b/upload/templates/modeladmin/upload/xmlerrorreport/edit.html new file mode 100644 index 000000000..501785e5c --- /dev/null +++ b/upload/templates/modeladmin/upload/xmlerrorreport/edit.html @@ -0,0 +1,19 @@ +{% extends "modeladmin/edit.html" %} +{% load i18n modeladmin_tags wagtailadmin_tags wagtailcore_tags %} +{% block header %} + {% include "modeladmin/includes/header_with_history.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon merged=1 latest_log_entry=latest_log_entry history_url=history_url %} +
+

+ + {% icon name="arrow-left" classname="default" %} + {% blocktrans trimmed with view.verbose_name as model_name %}Back to package validations{% endblocktrans %} + +

+ +
+{% endblock %} diff --git a/upload/templates/modeladmin/upload/xmlinforeport/edit.html b/upload/templates/modeladmin/upload/xmlinforeport/edit.html new file mode 100644 index 000000000..501785e5c --- /dev/null +++ b/upload/templates/modeladmin/upload/xmlinforeport/edit.html @@ -0,0 +1,19 @@ +{% extends "modeladmin/edit.html" %} +{% load i18n modeladmin_tags wagtailadmin_tags wagtailcore_tags %} +{% block header %} + {% include "modeladmin/includes/header_with_history.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon merged=1 latest_log_entry=latest_log_entry history_url=history_url %} +
+

+ + {% icon name="arrow-left" classname="default" %} + {% blocktrans trimmed with view.verbose_name as model_name %}Back to package validations{% endblocktrans %} + +

+ +
+{% endblock %} diff --git a/upload/utils/zip_pkg.py b/upload/utils/zip_pkg.py new file mode 100644 index 000000000..9bec4a707 --- /dev/null +++ b/upload/utils/zip_pkg.py @@ -0,0 +1,79 @@ +import os +import logging +from tempfile import TemporaryDirectory, mkdtemp +from zipfile import ZIP_DEFLATED, BadZipFile, ZipFile + + +class PkgZip: + + def __init__(self, file_path): + self.file_path = file_path + + def split(self): + found = False + with ZipFile(self.file_path) as zf: + xml_and_related_items = self.get_zip_content(zf) + pkgs = self.get_packages( + xml_and_related_items["xmls"], + xml_and_related_items["other_files"] + ) + for xml_name, items in pkgs.items(): + yield self.zip_package(zf, xml_name, items) + + def get_zip_content(self, zf): + file_paths = set(zf.namelist() or []) + logging.info(f"file_paths: {file_paths}") + + other_files = {} + xmls = {} + for file_path in file_paths: + basename = os.path.basename(file_path) + if basename.startswith("."): + continue + name, ext = os.path.splitext(basename) + data = {"file_path": file_path, "basename": basename} + logging.info(f"{self.file_path} {data}") + if ext == ".xml": + xmls.setdefault(name, []) + xmls[name].append(data) + else: + other_files.setdefault(name, []) + other_files[name].append(data) + return {"xmls": xmls, "other_files": other_files} + + def get_packages(self, xmls, other_files): + for k, v in xmls.items(): + logging.info((k, v)) + for k, v in other_files.items(): + logging.info((k, v)) + + other_files_keys = list(other_files.keys()) + for key in xmls.keys(): + for other_files_key in other_files_keys: + logging.info(f"{key} | {other_files_key}") + if key == other_files_key: + logging.info("a") + xmls[key].extend(other_files.pop(other_files_key)) + elif other_files_key.startswith(key + "-"): + logging.info("b") + xmls[key].extend(other_files.pop(other_files_key)) + else: + logging.info("c") + return xmls + + def zip_package(self, zf, xml_name, files): + try: + content = None + with TemporaryDirectory() as tmpdirname: + zfile = os.path.join(tmpdirname, f"{xml_name}.zip") + with ZipFile(zfile, "w", compression=ZIP_DEFLATED) as zfw: + for item in files: + zfw.writestr( + item["basename"], zf.read(item["file_path"]) + ) + with open(zfile, "rb") as zfw: + content = zfw.read() + return {"xml_name": xml_name, "content": content} + except Exception as exc: + logging.exception(exc) + return {"xml_name": xml_name, "error": str(exc)} diff --git a/upload/validation/html_validation.py b/upload/validation/html_validation.py new file mode 100644 index 000000000..04b1c103f --- /dev/null +++ b/upload/validation/html_validation.py @@ -0,0 +1,59 @@ +from django.utils.translation import gettext_lazy as _ + + +def validate_webpage(web_page, xml_with_pre): + """ + Arguments + --------- + xml_with_pre : XMLWithPre + web_page : dict { + "lang": "en", + "url": "url", + } + + Returns + ------- + Generator of validation result + """ + # web_page['url'] can be None + + if not web_page["url"]: + yield { + "message": _("Web page `{}` does not exist").format( + web_page['lang'] + ) + } + + for item in absent_xml_data_in_web_page(web_page, xml_with_pre): + yield { + "message": _("It was expected to find `{}` in {} ({})").format( + item, web_page['url'], web_page['lang'] + ) + } + + +def absent_xml_data_in_web_page(web_page, xml_with_pre): + """ + Retorna itens (str) presentes no XML e não encontrados no web page + + Arguments + --------- + xml_with_pre : XMLWithPre + web_page : dict { + "name": "1234-1234-acron-45-1-1.pdf', + "lang": "en", + "component_type": "web_page", + "main": "en", + "content": b'', + } + + Returns + ------- + Gerador de textos faltantes no web page + """ + # TODO + # lembrar de considerar o idioma ao comparar: + # selecionar os dados do idioma de acordo com o idioma do web page + # lembrar de considerar que os dados não associados ao idioma + # tem que ser encontrados em todos os web pages + pass diff --git a/upload/validation/rendition_validation.py b/upload/validation/rendition_validation.py new file mode 100644 index 000000000..b76e8c47b --- /dev/null +++ b/upload/validation/rendition_validation.py @@ -0,0 +1,82 @@ +from django.utils.translation import gettext_lazy as _ + + +def validate_rendition(rendition, xml_with_pre): + """ + Arguments + --------- + xml_with_pre : XMLWithPre + rendition : dict { + "name": "1234-1234-acron-45-1-1.pdf', + "lang": "en", + "component_type": "rendition", + "main": "en", + "content": b'', + } + + Returns + ------- + Generator of validation result + """ + for item in absent_xml_data_in_rendition(rendition, xml_with_pre): + yield { + "message": _("It was expected to find `{}` in {} ({})").format( + item, rendition['name'], rendition['lang'] + ) + } + + for item in absent_pdf_words_in_xml_data(rendition, xml_with_pre): + yield { + "message": _("It was expected to find `{}` in XML ({})").format( + item, rendition['lang'] + ) + } + + +def absent_xml_data_in_rendition(rendition, xml_with_pre): + """ + Retorna itens (str) presentes no XML e não encontrados no PDF + + Arguments + --------- + xml_with_pre : XMLWithPre + rendition : dict { + "name": "1234-1234-acron-45-1-1.pdf', + "lang": "en", + "component_type": "rendition", + "main": "en", + "content": b'', + } + + Returns + ------- + Gerador de textos faltantes no PDF + """ + # TODO + # lembrar de considerar o idioma ao comparar: + # selecionar os dados do idioma de acordo com o idioma do PDF + # lembrar de considerar que os dados não associados ao idioma + # tem que ser encontrados em todos os PDFs + return [] + + +def absent_pdf_words_in_xml_data(rendition, xml_with_pre): + """ + Retorna itens (str) presentes no PDF e não encontrados no XML + + Arguments + --------- + xml_with_pre : XMLWithPre + rendition : dict { + "name": "1234-1234-acron-45-1-1.pdf', + "lang": "en", + "component_type": "rendition", + "main": "en", + "content": b'', + } + + Returns + ------- + Gerador de textos faltantes no XML + """ + return [] diff --git a/upload/validation/xml_data_checker.py b/upload/validation/xml_data_checker.py new file mode 100644 index 000000000..c43b5dd64 --- /dev/null +++ b/upload/validation/xml_data_checker.py @@ -0,0 +1,168 @@ +import sys +import logging + +from django.utils.translation import gettext_lazy as _ +from packtools.sps.validation import xml_validator as packtools_xml_data_checker +from upload.models import ( + XMLError, + XMLInfo, + XMLErrorReport, + XMLInfoReport, + choices, +) + + +class XMLDataChecker: + def __init__(self, package, journal, issue, params=None): + self.error_report = None + self.info_report = None + self.package = package + self.user = package.creator + self.journal = journal + self.issue = issue + self.xmltree = package.xml_with_pre.xmltree + self.params = {} + self.params.update(params or {}) + self.params.update(self.get_journal_params()) + + def get_journal_params(self): + try: + return { + # "get_doi_data": callable_get_doi_data, + "doi_required": self.journal.doi_prefix, + "expected_toc_sections": self.journal.toc_sections, + "journal_acron": self.journal.journal_acron, + "publisher_name_list": self.journal.publisher_names, + "nlm_ta": self.journal.nlm_title, + "journal_license_code": self.journal.license_code, + } + except Exception as e: + return {} + + def create_info_report(self): + self.info_report = XMLInfoReport.create_or_update( + self.user, + self.package, + _("XML Info Report"), + choices.VAL_CAT_XML_CONTENT, + reset_validations=False, + ) + + def create_error_report(self, report_name): + return XMLErrorReport.create_or_update( + self.user, + self.package, + _("XML Error Report") + f': {report_name}', + choices.VAL_CAT_XML_CONTENT, + reset_validations=False, + ) + + def validate(self): + try: + index = None + operation = self.package.start(self.user, "xml data validation") + XMLInfo.objects.filter(report__package=self.package).delete() + XMLError.objects.filter(report__package=self.package).delete() + for response in packtools_xml_data_checker.validate_xml_content( + self.xmltree, self.params + ): + group = response["group"] + results = response["items"] + try: + for index, result in enumerate(results): + if not result: + continue + self._handle_result(group, result, index) + except Exception as exc: + exc_type, exc_value, exc_traceback = sys.exc_info() + logging.exception(exc) + self._handle_exception({"group": group, "exception": exc, "exc_traceback": exc_traceback}) + + for error_report in self.package.xml_error_report.all(): + if error_report.xml_error.count(): + error_report.finish_validations() + else: + error_report.delete() + + # devido às tarefas serem executadas concorrentemente, + # necessário registrar as tarefas finalizadas + if self.info_report: + self.info_report.finish_validations() + + operation.finish( + self.user, + completed=True, + detail={}, + ) + return True + except Exception as e: + exc_type, exc_value, exc_traceback = sys.exc_info() + operation = self.package.start(self.user, f"result {index}") + detail = {} + detail.update(result) + detail["len"] = {k: len(v) for k, v in result.items() if v} + operation.finish( + self.user, + completed=False, + exception=e, + exc_traceback=exc_traceback, + detail=detail, + ) + + def _handle_result(self, group, result, index): + try: + status_ = result["response"] + subject = group + if status_ == "OK": + if not self.info_report: + self.create_info_report() + report = self.info_report + else: + report = self.create_error_report(group or '') + + validation_result = report.add_validation_result( + status=status_, + message=result.get("advice"), + data=result, + subject=subject, + ) + attribute = "/".join([item for item in (result.get("item"), result.get("sub_item")) if item]) + validation_result.focus = result.get("title") + validation_result.attribute = attribute + validation_result.parent = result.get("parent") + validation_result.parent_id = result.get("parent_id") + validation_result.parent_article_type = result.get("parent_article_type") + validation_result.validation_type = result.get("validation_type") or "xml" + + if status_ != "OK": + validation_result.advice = result.get("advice") + validation_result.expected_value = result.get("expected_value") + validation_result.got_value = result.get("got_value") + validation_result.reaction = choices.ER_REACTION_FIX + + validation_result.save() + return validation_result + except Exception as e: + logging.exception(e) + exc_type, exc_value, exc_traceback = sys.exc_info() + operation = self.package.start(self.user, f"result {index}") + detail = {} + detail.update(result or {}) + detail["len"] = {k: len(v) for k, v in result.items() if v} + operation.finish( + self.user, + completed=False, + exception=e, + exc_traceback=exc_traceback, + detail=detail, + ) + + def _handle_exception(self, result): + group = result.get("group") or "configuration" + operation = self.package.start(self.user, f"{group} exception") + operation.finish( + self.user, + completed=False, + exception=result.get("exception"), + exc_traceback=result.get("exc_traceback"), + ) \ No newline at end of file diff --git a/upload/validation/xml_validation.py b/upload/validation/xml_validation.py new file mode 100644 index 000000000..90c2b9169 --- /dev/null +++ b/upload/validation/xml_validation.py @@ -0,0 +1,488 @@ +import csv +import json +import logging +import os +import sys + +from importlib.resources import files +from packtools.sps.models.dates import ArticleDates +from packtools.sps.pid_provider.xml_sps_lib import XMLWithPre +from packtools.sps.validation.aff import ( + AffiliationsListValidation, + AffiliationValidation, +) +from packtools.sps.validation.alternatives import ( + AlternativesValidation, + AlternativeValidation, +) +from packtools.sps.validation.article_abstract import ( + HighlightsValidation, + VisualAbstractsValidation, +) +from packtools.sps.validation.article_and_subarticles import ( + ArticleAttribsValidation, + ArticleIdValidation, + ArticleLangValidation, + ArticleTypeValidation, +) +from packtools.sps.validation.article_author_notes import AuthorNotesValidation +from packtools.sps.validation.article_citations import ( + ArticleCitationsValidation, + ArticleCitationValidation, +) +from packtools.sps.validation.article_contribs import ( + ArticleContribsValidation, + ContribsValidation, + ContribValidation, +) +from packtools.sps.validation.article_data_availability import ( + DataAvailabilityValidation, +) +from packtools.sps.validation.article_doi import ArticleDoiValidation +from packtools.sps.validation.article_lang import ( + ArticleLangValidation as ArticleLangValidation2, +) +from packtools.sps.validation.article_license import ArticleLicenseValidation +from packtools.sps.validation.article_toc_sections import ArticleTocSectionsValidation +from packtools.sps.validation.article_xref import ArticleXrefValidation +from packtools.sps.validation.dates import ArticleDatesValidation +from packtools.sps.validation.fig import FigValidation +from packtools.sps.validation.footnotes import FootnoteValidation +from packtools.sps.validation.formula import FormulaValidation +from packtools.sps.validation.front_articlemeta_issue import IssueValidation, Pagination +from packtools.sps.validation.funding_group import FundingGroupValidation +from packtools.sps.validation.journal_meta import ( + AcronymValidation, + ISSNValidation, + JournalIdValidation, + JournalMetaValidation, + PublisherNameValidation, + TitleValidation, +) +from packtools.sps.validation.peer_review import ( + AuthorPeerReviewValidation, + CustomMetaPeerReviewValidation, + DatePeerReviewValidation, + PeerReviewsValidation, + RelatedArticleValidation, +) +from packtools.sps.validation.preprint import PreprintValidation +from packtools.sps.validation.related_articles import RelatedArticlesValidation +from packtools.sps.validation.supplementary_material import ( + SupplementaryMaterialValidation, +) +from packtools.sps.validation.tablewrap import TableWrapValidation +from packtools.sps.validation.utils import get_doi_information + + +def get_data(filename, key, sps_version=None): + sps_version = sps_version or "default" + # Reads contents with UTF-8 encoding and returns str. + content = ( + files(f"packtools.sps.sps_versions") + .joinpath(f"{sps_version}") + .joinpath(f"{filename}.json") + .read_text() + ) + x = " ".join(content.split()) + fixed = x.replace(", ]", "]").replace(", }", "}") + data = json.loads(fixed) + return data[key] + + +def create_report(report_file_path, xml_path, params, fieldnames=None): + if not params: + params = { + # "get_doi_data": callable_get_doi_data, + "doi_required": params.get("doi_required"), + "expected_toc_sections": params.get("expected_toc_sections"), + "journal_acron": params.get("journal_acron"), + "publisher_name_list": params.get("publisher_name_list"), + "nlm_ta": params.get("nlm_ta"), + } + for xml_with_pre in XMLWithPre.create(path=xml_path): + rows = validate_xml_content(xml_with_pre.filename, xml_with_pre.xmltree, params) + save_csv(report_file_path, rows, fieldnames) + print(f"Created {report_file_path}") + + +def save_csv(filepath, rows, fieldnames=None): + + with open(filepath, "w", newline="") as csvfile: + header = False + for row in rows: + try: + logging.exception(row["exception"]) + continue + except KeyError: + if not fieldnames: + fieldnames = list(row.keys()) + if not header: + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + writer.writeheader() + header = True + writer.writerow(row) + + +def validate_xml_content(sps_pkg_name, xmltree, params): + logging.info("") + # params = { + # "get_doi_data": callable_get_doi_data, + # "doi_required": params.get("doi_required"), + # "expected_toc_sections": params.get("expected_toc_sections"), + # "journal_acron": params.get("journal_acron"), + # "publisher_name_list": params.get("publisher_name_list"), + # "nlm_ta": params.get("nlm_ta"), + # } + + validation_group_and_function_items = ( + ("journal", validate_journal), + ("article attributes", validate_article_attributes), + ("article attributes", validate_languages), + ("article attributes", validate_article_type), + ("article-id", validate_article_id_other), + # ("article-id", validate_doi), + # ("dates", validate_dates), + ("author", validate_contribs), + ("author affiliations", validate_affiliations), + ("author notes", validate_author_notes), + ("text languages", validate_toc_sections), + ("text languages", validate_article_languages), + ("text xref", validate_xref), + ("open science", validate_data_availability), + ("open science", validate_licenses), + ("open science", validate_preprint), + # ("open science peer review", validate_peer_review), + ("references", validate_references), + ("funding", validate_funding_group), + ("related articles", validate_related_articles), + ("special abstracts", validate_visual_abstracts), + ("special abstracts", validate_highlights), + ("footnotes", validate_footnotes), + # ("table-wrap", validate_table_wrap), + # ("figures", validate_figures), + # ("formulas", validate_formulas), + ("supplementary material", validate_supplementary_material), + ) + + sps_version = xmltree.find(".").get("specific-use") + for validation_group, f in validation_group_and_function_items: + try: + items = f(xmltree, sps_version, params) + for item in items: + try: + item["group"] = validation_group + yield item + except Exception as exc: + print(f"item: {item} / group: {validation_group}") + exc_type, exc_value, exc_traceback = sys.exc_info() + yield dict( + exception=exc, + exc_traceback=exc_traceback, + function=validation_group, + sps_pkg_name=sps_pkg_name, + item=item, + ) + except Exception as exc: + exc_type, exc_value, exc_traceback = sys.exc_info() + yield dict( + exception=exc, + exc_traceback=exc_traceback, + function=validation_group, + sps_pkg_name=sps_pkg_name, + ) + + +def validate_affiliations(xmltree, sps_version, params): + logging.info("validate_affiliations") + validator = AffiliationsListValidation(xmltree) + data = get_data("country_codes", "country_codes_list") + yield from validator.validade_affiliations_list(data) + + +def validate_highlights(xmltree, sps_version, params): + logging.info("validate_highlights") + validator = HighlightsValidation(xmltree) + yield from validator.highlight_validation() + + +def validate_visual_abstracts(xmltree, sps_version, params): + logging.info("validate_visual_abstracts") + validator = VisualAbstractsValidation(xmltree) + yield from validator.visual_abstracts_validation() + + +def validate_languages(xmltree, sps_version, params): + logging.info("validate_languages") + validator = ArticleLangValidation(xmltree) + yield from validator.validate_language( + get_data("language_codes", "language_codes_list") + ) + + +def validate_article_attributes(xmltree, sps_version, params): + logging.info("validate_article_attributes") + validator = ArticleAttribsValidation(xmltree) + yield from validator.validate_dtd_version( + get_data("dtd_version", "dtd_version_list", sps_version) + ) + yield from validator.validate_specific_use( + get_data("specific_use", "specif_use_list") + ) + + +def validate_article_id_other(xmltree, sps_version, params): + logging.info("validate_article_id_other") + validator = ArticleIdValidation(xmltree) + try: + return validator.validate_article_id_other() + except AttributeError: + return None + + +def validate_article_type(xmltree, sps_version, params): + logging.info("validate_article_type") + validator = ArticleTypeValidation(xmltree) + # FIXME validar article_type para sub-article + try: + yield from validator.validate_article_type( + get_data("article_type", "article_type_list", sps_version) + ) + except Exception as e: + raise e + + # TODO + # yield from validator.validate_article_type_vs_subject_similarity( + # subjects_list=None, expected_similarity=1, error_level=None, target_article_types=None + # ) + + +def validate_author_notes(xmltree, sps_version, params): + logging.info("validate_author_notes") + data = {"sps-1.9": ["conflict"], "sps-1.10": ["coi-statement"]} + validator = AuthorNotesValidation(xmltree, data.get(sps_version)) + yield from validator.validate_author_note() + + +def validate_references(xmltree, sps_version, params): + logging.info("validate_references") + # FIXME criar json para a versão sps-1.9 + publication_type_list = get_data( + "publication_types_references", "publication_type_list", sps_version + ) + validator = ArticleCitationsValidation(xmltree, publication_type_list) + start_year = None + + xml = ArticleDates(xmltree) + end_year = int(xml.collection_date["year"] or xml.article_date["year"]) + + # FIXME remover xmltree do método + yield from validator.validate_article_citations( + xmltree, publication_type_list, start_year, end_year + ) + + +def validate_contribs(xmltree, sps_version, params): + logging.info("validate_contribs") + data = { + "credit_taxonomy_terms_and_urls": None, + "callable_get_data": None, + } + validator = ArticleContribsValidation(xmltree, data) + # FIXME + yield from validator.validate_contribs_orcid_is_unique(error_level="CRITICAL") + # for contrib in validator.contribs.contribs: + # yield from ContribsValidation(contrib, data, content_types).validate() + + +def validate_data_availability(xmltree, sps_version, params): + logging.info("validate_data_availability") + validator = DataAvailabilityValidation(xmltree) + try: + specific_use_list = get_data( + "data_availability_specific_use", "specific_use", sps_version + ) + except Exception as e: + specific_use_list = None + + if specific_use_list: + yield from validator.validate_data_availability( + specific_use_list, + error_level="ERROR", + ) + + +def validate_doi(xmltree, sps_version, params): + logging.info("validate_doi") + # FIXME falta de padrão + validator = ArticleDoiValidation(xmltree) + + if params.get("doi_required"): + error_level = "CRITICAL" + else: + error_level = "ERROR" + yield from validator.validate_doi_exists(error_level=error_level) + yield from validator.validate_all_dois_are_unique(error_level="ERROR") + yield from validator.validate_doi_registered(get_doi_information) + + +def validate_article_languages(xmltree, sps_version, params): + logging.info("validate_article_languages") + # FIXME falta de padrão + validator = ArticleLangValidation2(xmltree) + yield from validator.validate_article_lang() + + +def validate_licenses(xmltree, sps_version, params): + logging.info("validate_licenses") + validator = ArticleLicenseValidation(xmltree) + # yield from validator.validate_license(license_expected_value) + # falta de json em sps-1.9 + yield from validator.validate_license_code(params.get("journal_license_code")) + + +def validate_toc_sections(xmltree, sps_version, params): + logging.info("validate_toc_sections") + validator = ArticleTocSectionsValidation(xmltree) + yield from validator.validate_article_toc_sections( + params.get("expected_toc_sections") + ) + yield from validator.validade_article_title_is_different_from_section_titles() + + +def validate_xref(xmltree, sps_version, params): + logging.info("validate_xref") + validator = ArticleXrefValidation(xmltree) + yield from validator.validate_id() + yield from validator.validate_rid() + + +def validate_dates(xmltree, sps_version, params): + logging.info("validate_dates") + validator = ArticleDatesValidation(xmltree) + order = get_data( + "history_dates_order_of_events", + "order", + sps_version, + ) + required_events = get_data( + "history_dates_required_events", + "required_events", + sps_version, + ) + yield from validator.validate_history_dates(order, required_events) + yield from validator.validate_number_of_digits_in_article_date() + + # FIXME + yield from validator.validate_article_date(future_date=None) + yield from validator.validate_collection_date(future_date=None) + + +def validate_figures(xmltree, sps_version, params): + logging.info("validate_figures") + # FIXME faltam validações de label, caption, graphic ou alternatives + validator = FigValidation(xmltree) + yield from validator.validate_fig_existence() + + +def validate_footnotes(xmltree, sps_version, params): + logging.info("validate_footnotes") + # FIXME não existe somente um tipo de footnotes, faltam validações, error_level + validator = FootnoteValidation(xmltree) + yield from validator.fn_validation() + + +def validate_formulas(xmltree, sps_version, params): + logging.info("validate_formulas") + # FIXME faltam validações de label, caption, graphic ou alternatives + validator = FormulaValidation(xmltree) + yield from validator.validate_formula_existence() + + +def validate_funding_group(xmltree, sps_version, params): + logging.info("validate_funding_group") + # FIXME ? _callable_extern_validate_default + # faltam validações + validator = FundingGroupValidation(xmltree) + yield from validator.funding_sources_exist_validation() + # ??? yield from validator.award_id_format_validation(_callable_extern_validate_default) + + +def validate_journal(xmltree, sps_version, params): + logging.info("validate_journal") + + validator = AcronymValidation(xmltree) + yield from validator.acronym_validation(params["journal_acron"]) + + validator = PublisherNameValidation(xmltree) + yield from validator.validate_publisher_names(params["publisher_name_list"]) + + try: + if params["nlm_ta"]: + validator = JournalIdValidation(xmltree) + yield from validator.nlm_ta_id_validation(params["nlm_ta"]) + except KeyError: + pass + + +def validate_peer_review(xmltree, sps_version, params): + logging.info("validate_peer_review") + # FIXME temos todos os json? + + validator = PeerReviewsValidation( + xmltree, + contrib_type_list=get_data( + "specific_use_for_peer_reviews", "contrib_type_list", sps_version + ), + specific_use_list=get_data( + "specific_use_for_peer_reviews", "specific_use_list", sps_version + ), + date_type_list=get_data( + "specific_use_for_peer_reviews", "date_type_list", sps_version + ), + meta_value_list=get_data("meta_value", "meta_value_list", sps_version), + related_article_type_list=get_data( + "related_article_type", "related_article_type_list", sps_version + ), + link_type_list=get_data( + "related_article__ext_link_type", + "related_article__ext_link_type_list", + sps_version, + ), + ) + yield from validator.validate() + + +def validate_preprint(xmltree, sps_version, params): + logging.info("validate_preprint") + # FIXME fora de padrão + validator = PreprintValidation(xmltree) + yield from validator.preprint_validation() + + +def validate_related_articles(xmltree, sps_version, params): + logging.info("validate_related_articles") + validator = RelatedArticlesValidation(xmltree) + correspondence_list = get_data( + "related_article", + "correspondence_list", + sps_version, + ) + yield from validator.related_articles_matches_article_type_validation( + correspondence_list + ) + yield from validator.related_articles_doi() + + +def validate_supplementary_material(xmltree, sps_version, params): + logging.info("validate_supplementary_material") + # FIXME validações incompletas + validator = SupplementaryMaterialValidation(xmltree) + yield from validator.validate_supplementary_material_existence() + + +def validate_table_wrap(xmltree, sps_version, params): + logging.info("validate_table_wrap") + # FIXME validações incompletas + validator = TableWrapValidation(xmltree) + yield from validator.validate_tablewrap_existence() diff --git a/upload/views.py b/upload/views.py index ce5c1b2db..e16a15562 100644 --- a/upload/views.py +++ b/upload/views.py @@ -1,10 +1,14 @@ import logging +import os from django.contrib import messages -from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse +from django.http import Http404, HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect, render from django.utils.translation import gettext_lazy as _ -from wagtail_modeladmin.views import CreateView, EditView, InspectView + +from wagtail.snippets.views.snippets import CreateView, EditView, InspectView + +from core.views import UserTrackingCreateView, UserTrackingEditView from article.models import Article from issue.models import Issue @@ -22,165 +26,165 @@ from team.models import has_permission -class PackageZipCreateView(CreateView): - def form_valid(self, form): - if not has_permission(self.request.user): - messages.error( - self.request, - _("Operation not available"), - ) - return HttpResponseRedirect(self.get_success_url()) +# =================================================================== +# Create / Edit Views customizadas +# =================================================================== - pkg_zip = form.save_all(self.request.user) - task_receive_packages.apply_async( - kwargs=dict( - user_id=self.request.user.id, - pkg_zip_id=pkg_zip.id, - ) - ) - if pkg_zip.show_package_validations: - return redirect(f"/admin/upload/package?q={pkg_zip.name}") - else: - return HttpResponseRedirect(self.get_success_url()) - - -class PackageAdminInspectView(InspectView): - def get_optimized_package_filepath_and_directory(self): - # Obtém caminho do pacote otimizado - _path = package_utils.generate_filepath_with_new_extension( - self.instance.file.name, - ".optz", - True, - ) - # Obtém diretório em que o pacote otimizado foi extraído - _directory = file_utils.get_file_url( - dirname="", filename=file_utils.get_filename_from_filepath(_path) - ) +class PackageZipCreateView(UserTrackingCreateView): + """ + Upload de pacote ZIP. - return _path, _directory + Sobrescreve post() porque precisa: + 1. Checar permissão antes do save + 2. Disparar task assíncrona após o save + 3. Redirecionar condicionalmente + """ - def set_pdf_paths(self, data, optz_dir): - try: - for rendition in package_utils.get_article_renditions_from_zipped_xml( - self.instance.file.name - ): - package_files = file_utils.get_file_list_from_zip( - self.instance.file.name - ) - document_name = package_utils.get_xml_filename(package_files) - rendition_name = package_utils.get_rendition_expected_name( - rendition, document_name + def post(self, request, *args, **kwargs): + if not has_permission(request.user): + messages.error(request, _("Operation not available")) + return redirect(self.get_add_url()) + + self.form = self.get_form() + if self.form.is_valid(): + pkg_zip = self.save_instance() + pkg_zip.name, ext = os.path.splitext(os.path.basename(pkg_zip.file.name)) + pkg_zip.save() + + task_receive_packages.apply_async( + kwargs=dict( + user_id=request.user.id, + pkg_zip_id=pkg_zip.id, ) - data["pdfs"].append( - { - "base_uri": file_utils.os.path.join(optz_dir, rendition_name), - "language": rendition.language, - } - ) - except XMLFormatError: - data["pdfs"] = [] - - def get_context_data(self): - blocking_errors = list( - PkgValidationResult.objects.filter( - report__package=self.instance, - status=choices.VALIDATION_RESULT_BLOCKING, - ).values_list("message", flat=True) - ) - data = { - "pkg_zip_name": self.instance.pkg_zip.name, - "linked": self.instance.linked.all(), - "validation_results": {}, - "package_id": self.instance.id, - "original_pkg": self.instance.file.name, - "status": self.instance.status, - "category": self.instance.category, - "languages": package_utils.get_languages(self.instance.file.name), - "pdfs": [], - "reports": list(self.instance.reports), - "xml_error_reports": list(self.instance.xml_error_reports), - "xml_info_reports": list(self.instance.xml_info_reports), - "summary": self.instance.summary, - "xml": self.instance.xml, - "blocking_errors": blocking_errors, - } + ) - # optz_file_path, optz_dir = self.get_optimized_package_filepath_and_directory() - # data["optimized_pkg"] = optz_file_path - # self.set_pdf_paths(data, optz_dir) + if pkg_zip.show_package_validations: + return redirect(f"/admin/upload/package?q={pkg_zip.name}") + else: + return redirect(self.get_success_url()) + else: + return self.form_invalid(self.form) - return super().get_context_data(**data) +class XMLInfoReportEditView(UserTrackingEditView): + """ + Edição de XMLInfoReport. -class XMLInfoReportEditView(EditView): + Sobrescreve post() para redirect customizado após save. + """ fields = ["package"] - def form_valid(self, form): - - report = form.save_all(self.request.user) - - messages.success( - self.request, - _("Success ..."), - ) - - # dispara a tarefa que realiza as validações de - # assets, renditions, XML content etc - return redirect(self.get_package_url()) + def post(self, request, *args, **kwargs): + self.form = self.get_form() + if self.form.is_valid(): + self.save_instance() + messages.success(request, _("Success ...")) + return redirect(self.get_success_url()) + else: + return self.form_invalid(self.form) - def get_package_url(self): + def get_success_url(self): report = self.instance return f"/admin/upload/package/inspect/{report.package.id}/?#xi" class ValidationReportEditView(XMLInfoReportEditView): - def get_package_url(self): + def get_success_url(self): report = self.instance return f"/admin/upload/package/inspect/{report.package.id}/?#vr{report.id}" class XMLErrorReportEditView(XMLInfoReportEditView): - def get_package_url(self): + def get_success_url(self): report = self.instance return f"/admin/upload/package/inspect/{report.package.id}/?#xer{report.id}" + def post(self, request, *args, **kwargs): + if not has_permission(request.user): + messages.error(request, _("Operation not available")) + return redirect(self.get_success_url()) + + self.form = self.get_form() + if self.form.is_valid(): + self.form.save_all(request.user) + self.save_instance() + return redirect(self.get_success_url()) + else: + return self.form_invalid(self.form) + + +class UploadValidatorEditView(UserTrackingEditView): + """ + Sobrescreve post() para checar permissão antes do save. + """ + + def post(self, request, *args, **kwargs): + if not has_permission(request.user): + messages.error(request, _("Operation not available")) + return redirect(self.get_success_url()) + + self.form = self.get_form() + if self.form.is_valid(): + self.save_instance() + return redirect(self.get_success_url()) + else: + return self.form_invalid(self.form) + + +# =================================================================== +# Mixin e views de decisão de publicação +# =================================================================== + class PackageDecisionMixin: - """Mixin configurável para processar decisões de publicação de pacotes""" + """ + Mixin para processar decisões de publicação de pacotes. + + Sobrescreve post() porque precisa: + 1. Checar permissão + 2. Salvar com tracking de updated_by + 3. Disparar tasks condicionais + 4. Processar decisão de QA + 5. Redirecionar com mensagem de sucesso/erro + """ - # Atributos que podem ser sobrescritos nas classes filhas success_message = _("The decision was executed as planned") error_message = _("There was an impediment to executing the decision.") permission_error_message = _("Operation not available") def get_task_function(self): - """Pode ser sobrescrito se diferentes views usarem tasks diferentes""" + """Pode ser sobrescrito se diferentes views usarem tasks diferentes.""" return task_publish_article def process_decision(self, package, user, force_journal, force_issue): - """Pode ser sobrescrito para customizar o processamento""" + """Pode ser sobrescrito para customizar o processamento.""" return package.process_qa_decision( user, self.get_task_function(), force_journal, force_issue ) - def form_valid(self, form): - if not has_permission(self.request.user): - messages.error(self.request, self.permission_error_message) - return HttpResponseRedirect(self.get_success_url()) + def post(self, request, *args, **kwargs): + if not has_permission(request.user): + messages.error(request, self.permission_error_message) + return redirect(self.get_success_url()) + + self.form = self.get_form() + if not self.form.is_valid(): + return self.form_invalid(self.form) - package = form.save_all(self.request.user) + # save_instance() seta updated_by e salva + package = self.save_instance() - user = self.request.user - force_journal_publication = form.cleaned_data.get("force_journal_publication") + user = request.user + force_journal_publication = self.form.cleaned_data.get("force_journal_publication") if force_journal_publication and package.journal: task_complete_journal_data.delay( user_id=user.id, username=user.username, journal_id=package.journal.id, ) - force_issue_publication = form.cleaned_data.get("force_issue_publication") + force_issue_publication = self.form.cleaned_data.get("force_issue_publication") if force_issue_publication and package.issue: task_complete_issue_data.delay( user_id=user.id, @@ -190,51 +194,109 @@ def form_valid(self, form): if self.process_decision( package, - self.request.user, + user, force_journal_publication, force_issue_publication, ): - messages.success(self.request, self.success_message) - return HttpResponseRedirect(self.get_success_url()) + messages.success(request, self.success_message) + return redirect(self.get_success_url()) else: - messages.error(self.request, self.error_message) - return self.form_invalid(form) + messages.error(request, self.error_message) + return self.form_invalid(self.form) -class QAPackageEditView(PackageDecisionMixin, EditView): - # Usando as mensagens padrão +class QAPackageEditView(PackageDecisionMixin, UserTrackingEditView): pass -class ReadyToPublishPackageEditView(PackageDecisionMixin, EditView): - # Exemplo de customização de mensagens +class ReadyToPublishPackageEditView(PackageDecisionMixin, UserTrackingEditView): success_message = _("Article successfully published") error_message = _("Failed to publish the article. Please try again.") -class UploadValidatorEditView(EditView): - def form_valid(self, form): - if not has_permission(self.request.user): - messages.error( - self.request, - _("Operation not available"), - ) - return HttpResponseRedirect(self.get_success_url()) - obj = form.save_all(self.request.user) - return HttpResponseRedirect(self.get_success_url()) +# =================================================================== +# Inspect View +# =================================================================== -def finish_deposit(request): +class PackageAdminInspectView(InspectView): """ - This view function enables the user to finish deposit of a package through the graphic-interface. + MIGRAÇÃO: InspectView do wagtail.snippets tem a mesma interface + que a do ModelAdmin para get_context_data(). """ + + def get_optimized_package_filepath_and_directory(self): + _path = package_utils.generate_filepath_with_new_extension( + self.instance.file.name, + ".optz", + True, + ) + _directory = file_utils.get_file_url( + dirname="", filename=file_utils.get_filename_from_filepath(_path) + ) + return _path, _directory + + def set_pdf_paths(self, data, optz_dir): + try: + for rendition in package_utils.get_article_renditions_from_zipped_xml( + self.instance.file.name + ): + package_files = file_utils.get_file_list_from_zip( + self.instance.file.name + ) + document_name = package_utils.get_xml_filename(package_files) + rendition_name = package_utils.get_rendition_expected_name( + rendition, document_name + ) + data["pdfs"].append( + { + "base_uri": file_utils.os.path.join(optz_dir, rendition_name), + "language": rendition.language, + } + ) + except XMLFormatError: + data["pdfs"] = [] + + def get_context_data(self): + blocking_errors = list( + PkgValidationResult.objects.filter( + report__package=self.instance, + status=choices.VALIDATION_RESULT_BLOCKING, + ).values_list("message", flat=True) + ) + data = { + "pkg_zip_name": self.instance.pkg_zip.name, + "linked": self.instance.linked.all(), + "validation_results": {}, + "package_id": self.instance.id, + "original_pkg": self.instance.file.name, + "status": self.instance.status, + "category": self.instance.category, + "languages": package_utils.get_languages(self.instance.file.name), + "pdfs": [], + "reports": list(self.instance.reports), + "xml_error_reports": list(self.instance.xml_error_reports), + "xml_info_reports": list(self.instance.xml_info_reports), + "summary": self.instance.summary, + "xml": self.instance.xml, + "blocking_errors": blocking_errors, + } + + return super().get_context_data(**data) + + +# =================================================================== +# Function-based views (sem mudança na migração) +# =================================================================== + + +def finish_deposit(request): package_id = request.GET.get("package_id") if package_id: package = get_object_or_404(Package, pk=package_id) if package.finish_deposit(task_publish_article): - # muda o status para a próxima etapa messages.success(request, _("Package has been deposited")) return redirect("/admin/upload/package/") @@ -258,9 +320,6 @@ def finish_deposit(request): def download_errors(request): - """ - This view function enables the user to finish deposit of a package through the graphic-interface. - """ package_id = request.GET.get("package_id") if package_id: @@ -278,9 +337,6 @@ def download_errors(request): def display_xml(request): - """ - This view function enables the user to see a preview of HTML - """ package_id = request.GET.get("package_id") if package_id: @@ -295,9 +351,6 @@ def display_xml(request): def preview_document(request): - """ - This view function enables the user to see a preview of HTML - """ package_id = request.GET.get("package_id") if package_id: @@ -317,9 +370,6 @@ def preview_document(request): def assign(request): - """ - Assign review to a team member or decide about the package - """ package_id = request.GET.get("package_id") user = request.user @@ -329,14 +379,6 @@ def assign(request): package = get_object_or_404(Package, pk=package_id) is_reassign = package.assignee is not None - # package.assignee = user - # package.save() - - # if not is_reassign: - # messages.success(request, _("Package has been assigned with success.")) - # else: - # messages.warning(request, _("Package has been reassigned with success.")) - return redirect(f"/admin/upload/qapackage/edit/{package_id}") @@ -360,4 +402,4 @@ def archive_package(request): package.status ), ) - return redirect(f"/admin/upload/package/") + return redirect(f"/admin/upload/package/") \ No newline at end of file diff --git a/upload/wagtail_hooks.py b/upload/wagtail_hooks.py index 9cfc39bef..e3254f336 100644 --- a/upload/wagtail_hooks.py +++ b/upload/wagtail_hooks.py @@ -9,6 +9,13 @@ from wagtail.snippets.views.snippets import SnippetViewSet, SnippetViewSetGroup from config.menu import get_menu_order +from core.permission_helper import ( + AllAccessPolicy, + StaffWritePolicy, + ReadOnlyPolicy, + AutoGeneratedOwnerEditPolicy, + StaffViewSuperWritePolicy, +) from upload.views import ( ReadyToPublishPackageEditView, PackageAdminInspectView, @@ -17,9 +24,9 @@ XMLErrorReportEditView, XMLInfoReportEditView, PackageZipCreateView, + UploadValidatorEditView, ) - -from .button_helper import UploadButtonHelper +from .permission_helper import UploadPermissions from .models import ( ReadyToPublishPackage, Package, @@ -35,14 +42,28 @@ PackageZip, ArchivedPackage, ) -from .permission_helper import UploadPermissionHelper -from team.models import has_permission + + +# =================================================================== +# Aliases locais para legibilidade nos get_queryset +# =================================================================== +_can_access = UploadPermissions.user_can_access +_is_staff = UploadPermissions.user_is_staff +_qs_for_user = UploadPermissions.get_queryset_for_user + + +# =================================================================== +# ViewSets do grupo Upload +# =================================================================== class PackageZipViewSet(SnippetViewSet): model = PackageZip - # button_helper_class = UploadButtonHelper - permission_helper_class = UploadPermissionHelper + permission_policy = AllAccessPolicy( + PackageZip, + access_check=_can_access, + staff_check=_is_staff, + ) add_view_class = PackageZipCreateView menu_label = _("Package upload") menu_icon = "folder" @@ -65,19 +86,17 @@ class PackageZipViewSet(SnippetViewSet): ) def get_queryset(self, request): - if not self.permission_helper.user_can_use_upload_module(request.user, None): - return super().get_queryset(request).none() - - params = {} - if not self.permission_helper.user_is_analyst_team_member(request.user, None): - params = {"creator": request.user} - return super().get_queryset(request).filter(**params) + qs = super().get_queryset(request) + return _qs_for_user(request.user, qs) class PackageViewSet(SnippetViewSet): model = Package - button_helper_class = UploadButtonHelper - permission_helper_class = UploadPermissionHelper + permission_policy = StaffWritePolicy( + Package, + access_check=_can_access, + staff_check=_is_staff, + ) inspect_view_class = PackageAdminInspectView inspect_template_name = "modeladmin/upload/package/inspect.html" menu_label = _("Package admin") @@ -123,7 +142,7 @@ class PackageViewSet(SnippetViewSet): ) def get_queryset(self, request): - if not self.permission_helper.user_can_use_upload_module(request.user, None): + if not _can_access(request.user): return super().get_queryset(request).none() params = {} @@ -132,7 +151,7 @@ def get_queryset(self, request): except KeyError: logging.info(request.GET) - if self.permission_helper.user_is_analyst_team_member(request.user, None): + if _is_staff(request.user): waiting_status = [ choices.PS_ENQUEUED_FOR_VALIDATION, choices.PS_PENDING_CORRECTION, @@ -143,20 +162,15 @@ def get_queryset(self, request): action_required = [ choices.PS_VALIDATED_WITH_ERRORS, ] - - # Ações requeridas no menu QA, neste menu é para consultar action_required_qa_menu = [ choices.PS_PENDING_QA_DECISION, ] - - # Ações requeridas no menu Publication, neste menu é para consultar action_required_publication_menu = [ choices.PS_READY_TO_PREVIEW, choices.PS_PREVIEW, choices.PS_READY_TO_PUBLISH, choices.PS_PUBLISHED, ] - status = ( action_required + waiting_status @@ -180,7 +194,6 @@ def get_queryset(self, request): choices.PS_READY_TO_PUBLISH, choices.PS_PUBLISHED, ] - status = action_required + waiting_status return super().get_queryset(request).filter(status__in=status, **params) @@ -188,8 +201,11 @@ def get_queryset(self, request): class QualityAnalysisPackageViewSet(SnippetViewSet): model = QAPackage - button_helper_class = UploadButtonHelper - permission_helper_class = UploadPermissionHelper + permission_policy = StaffWritePolicy( + QAPackage, + access_check=_can_access, + staff_check=_is_staff, + ) menu_label = _("Quality control admin") menu_icon = "folder" menu_order = 200 @@ -226,22 +242,9 @@ class QualityAnalysisPackageViewSet(SnippetViewSet): ) def get_queryset(self, request): - if not self.permission_helper.user_can_use_upload_module(request.user, None): + if not _can_access(request.user): return super().get_queryset(request).none() - """ - Para Analista de Qualidade - PS_VALIDATED_WITH_ERRORS: - sem esperar o produtor de XML terminar o depósito, - revisar os erros, aceitar ou rejeitar pacote - PS_PENDING_CORRECTION: - a pedido do produtor de XML, - revisar os erros, aceitar ou rejeitar pacote - PS_PENDING_QA_DECISION: - a pedido do produtor de XML, - revisar os erros, aceitar ou rejeitar pacote - - Para o produtor de XML, apenas consulta - """ + status = [ choices.PS_VALIDATED_WITH_ERRORS, choices.PS_PENDING_CORRECTION, @@ -250,14 +253,14 @@ def get_queryset(self, request): params = { "blocking_errors": 0, } - if self.permission_helper.user_is_analyst_team_member(request.user, None): + if _is_staff(request.user): return ( super() .get_queryset(request) .filter( Q(assignee__isnull=True) | Q(assignee=request.user), status__in=status, - **params + **params, ) ) else: @@ -268,9 +271,11 @@ def get_queryset(self, request): class ReadyToPublishPackageViewSet(SnippetViewSet): model = ReadyToPublishPackage - - button_helper_class = UploadButtonHelper - permission_helper_class = UploadPermissionHelper + permission_policy = StaffWritePolicy( + ReadyToPublishPackage, + access_check=_can_access, + staff_check=_is_staff, + ) menu_label = _("Publication admin") menu_icon = "folder" menu_order = 200 @@ -309,19 +314,19 @@ class ReadyToPublishPackageViewSet(SnippetViewSet): ) def get_queryset(self, request): - if not self.permission_helper.user_can_use_upload_module(request.user, None): + if not _can_access(request.user): return super().get_queryset(request).none() + status = [ choices.PS_READY_TO_PREVIEW, choices.PS_PREVIEW, choices.PS_READY_TO_PUBLISH, choices.PS_PUBLISHED, - # choices.PS_SCHEDULED_PUBLICATION, ] params = { "blocking_errors": 0, } - if self.permission_helper.user_is_analyst_team_member(request.user, None): + if _is_staff(request.user): return super().get_queryset(request).filter(status__in=status, **params) else: params["creator"] = request.user @@ -329,12 +334,83 @@ def get_queryset(self, request): return super().get_queryset(request).filter(status__in=status, **params) +class ArchivedPackageViewSet(SnippetViewSet): + model = ArchivedPackage + permission_policy = ReadOnlyPolicy( + ArchivedPackage, + access_check=_can_access, + ) + inspect_view_class = PackageAdminInspectView + inspect_template_name = "modeladmin/upload/package/inspect.html" + menu_label = _("Archived Packages") + menu_icon = "folder" + menu_order = 200 + add_to_settings_menu = False + list_per_page = 20 + + list_display = ( + "__str__", + "critical_errors", + "xml_errors_percentage", + "category", + "status", + "creator", + "updated", + "expiration_date", + ) + list_filter = ( + "creator", + "category", + "status", + ) + search_fields = ( + "name", + "journal__official_journal__title", + "issue__journal__official_journal__title", + "article__pid_v3", + "creator__username", + "updated_by__username", + "pkg_zip__file", + ) + inspect_view_fields = ( + "article", + "issue", + "category", + "status", + "file", + "created", + "updated", + "expiration_date", + "files_list", + ) + + def get_queryset(self, request): + if not _can_access(request.user): + return super().get_queryset(request).none() + + params = {} + if not _is_staff(request.user): + params = {"creator": request.user} + + return ( + super() + .get_queryset(request) + .filter(~Q(status__in=choices.PS_WIP), **params) + ) + + +# =================================================================== +# ViewSets do grupo Error Management +# =================================================================== + + class XMLErrorReportViewSet(SnippetViewSet): model = XMLErrorReport - permission_helper_class = UploadPermissionHelper + permission_policy = AutoGeneratedOwnerEditPolicy( + XMLErrorReport, + access_check=_can_access, + ) edit_view_class = XMLErrorReportEditView - # create_view_class = XMLErrorReportCreateView - # inspect_view_class = XMLErrorReportAdminInspectView menu_label = _("XML Error Reports") menu_icon = "error" add_to_settings_menu = False @@ -355,20 +431,16 @@ class XMLErrorReportViewSet(SnippetViewSet): ) def get_queryset(self, request): - if not self.permission_helper.user_can_use_upload_module(request.user, None): - return super().get_queryset(request).none() - - if self.permission_helper.user_is_analyst_team_member(request.user, None): - return super().get_queryset(request) - - return super().get_queryset(request).filter(package__creator=request.user) + qs = super().get_queryset(request) + return _qs_for_user(request.user, qs, creator_field="package__creator") class XMLErrorViewSet(SnippetViewSet): model = XMLError - permission_helper_class = UploadPermissionHelper - # create_view_class = XMLErrorCreateView - # inspect_view_class = XMLErrorAdminInspectView + permission_policy = ReadOnlyPolicy( + XMLError, + access_check=_can_access, + ) menu_label = _("XML errors") menu_icon = "error" add_to_settings_menu = False @@ -395,21 +467,17 @@ class XMLErrorViewSet(SnippetViewSet): ) def get_queryset(self, request): - if not self.permission_helper.user_can_use_upload_module(request.user, None): - return super().get_queryset(request).none() - - if self.permission_helper.user_is_analyst_team_member(request.user, None): - return super().get_queryset(request) - - return super().get_queryset(request).filter(package__creator=request.user) + qs = super().get_queryset(request) + return _qs_for_user(request.user, qs, creator_field="package__creator") class XMLInfoReportViewSet(SnippetViewSet): model = XMLInfoReport - permission_helper_class = UploadPermissionHelper + permission_policy = ReadOnlyPolicy( + XMLInfoReport, + access_check=_can_access, + ) edit_view_class = XMLInfoReportEditView - # create_view_class = XMLInfoReportCreateView - # inspect_view_class = XMLInfoReportAdminInspectView menu_label = _("XML Info Reports") menu_icon = "error" add_to_settings_menu = False @@ -430,20 +498,16 @@ class XMLInfoReportViewSet(SnippetViewSet): ) def get_queryset(self, request): - if not self.permission_helper.user_can_use_upload_module(request.user, None): - return super().get_queryset(request).none() - - if self.permission_helper.user_is_analyst_team_member(request.user, None): - return super().get_queryset(request) - - return super().get_queryset(request).filter(package__creator=request.user) + qs = super().get_queryset(request) + return _qs_for_user(request.user, qs, creator_field="package__creator") class XMLInfoViewSet(SnippetViewSet): model = XMLInfo - permission_helper_class = UploadPermissionHelper - # create_view_class = XMLInfoCreateView - # inspect_view_class = XMLInfoAdminInspectView + permission_policy = ReadOnlyPolicy( + XMLInfo, + access_check=_can_access, + ) menu_label = _("XML info") menu_icon = "error" add_to_settings_menu = False @@ -470,22 +534,17 @@ class XMLInfoViewSet(SnippetViewSet): ) def get_queryset(self, request): - if not self.permission_helper.user_can_use_upload_module(request.user, None): - return super().get_queryset(request).none() - - if self.permission_helper.user_is_analyst_team_member(request.user, None): - return super().get_queryset(request) - - return super().get_queryset(request).filter(package__creator=request.user) + qs = super().get_queryset(request) + return _qs_for_user(request.user, qs, creator_field="package__creator") class ValidationReportViewSet(SnippetViewSet): model = ValidationReport - permission_helper_class = UploadPermissionHelper - # create_view_class = ValidationReportCreateView + permission_policy = ReadOnlyPolicy( + ValidationReport, + access_check=_can_access, + ) edit_view_class = ValidationReportEditView - - # inspect_view_class = ValidationReportAdminInspectView menu_label = _("Validation Reports") menu_icon = "error" add_to_settings_menu = False @@ -506,20 +565,17 @@ class ValidationReportViewSet(SnippetViewSet): ) def get_queryset(self, request): - if not self.permission_helper.user_can_use_upload_module(request.user, None): - return super().get_queryset(request).none() - - if self.permission_helper.user_is_analyst_team_member(request.user, None): - return super().get_queryset(request) - - return super().get_queryset(request).filter(package__creator=request.user) + qs = super().get_queryset(request) + return _qs_for_user(request.user, qs, creator_field="package__creator") class ValidationViewSet(SnippetViewSet): model = PkgValidationResult - permission_helper_class = UploadPermissionHelper - # create_view_class = ValidationCreateView - # inspect_view_class = ValidationAdminInspectView + permission_policy = ReadOnlyPolicy( + PkgValidationResult, + access_check=_can_access, + ) + edit_view_class = UploadValidatorEditView menu_label = _("Validations") menu_icon = "error" add_to_settings_menu = False @@ -537,20 +593,16 @@ class ValidationViewSet(SnippetViewSet): ) def get_queryset(self, request): - if not self.permission_helper.user_can_use_upload_module(request.user, None): - return super().get_queryset(request).none() - - if self.permission_helper.user_is_analyst_team_member(request.user, None): - return super().get_queryset(request) - - return super().get_queryset(request).filter(package__creator=request.user) + qs = super().get_queryset(request) + return _qs_for_user(request.user, qs, creator_field="package__creator") class UploadValidatorViewSet(SnippetViewSet): model = UploadValidator - permission_helper_class = UploadPermissionHelper - # create_view_class = ValidationCreateView - # inspect_view_class = ValidationAdminInspectView + permission_policy = StaffViewSuperWritePolicy( + UploadValidator, + staff_check=_is_staff, + ) menu_label = _("Upload Validator") menu_icon = "folder" add_to_settings_menu = False @@ -568,76 +620,16 @@ class UploadValidatorViewSet(SnippetViewSet): ) def get_queryset(self, request): - if not self.permission_helper.user_can_use_upload_module(request.user, None): + if not _can_access(request.user): return super().get_queryset(request).none() - - if self.permission_helper.user_is_analyst_team_member(request.user, None): + if _is_staff(request.user): return super().get_queryset(request) - return super().get_queryset(request).none() -class ArchivedPackageViewSet(SnippetViewSet): - model = ArchivedPackage - button_helper_class = UploadButtonHelper - permission_helper_class = UploadPermissionHelper - inspect_view_class = PackageAdminInspectView - inspect_template_name = "modeladmin/upload/package/inspect.html" - menu_label = _("Archived Packages") - menu_icon = "folder" - menu_order = 200 - add_to_settings_menu = False - list_per_page = 20 - - list_display = ( - "__str__", - "critical_errors", - "xml_errors_percentage", - "category", - "status", - "creator", - "updated", - "expiration_date", - ) - list_filter = ( - "creator", - "category", - "status", - ) - search_fields = ( - "name", - "journal__official_journal__title", - "issue__journal__official_journal__title", - "article__pid_v3", - "creator__username", - "updated_by__username", - "pkg_zip__file", - ) - inspect_view_fields = ( - "article", - "issue", - "category", - "status", - "file", - "created", - "updated", - "expiration_date", - "files_list", - ) - - def get_queryset(self, request): - if not self.permission_helper.user_can_use_upload_module(request.user, None): - return super().get_queryset(request).none() - - params = {} - if not self.permission_helper.user_is_analyst_team_member(request.user, None): - params = {"creator": request.user} - - return ( - super() - .get_queryset(request) - .filter(~Q(status__in=choices.PS_WIP), **params) - ) +# =================================================================== +# Grupos de menu +# =================================================================== class UploadViewSetGroup(SnippetViewSetGroup): @@ -660,8 +652,6 @@ class UploadReportsViewSetGroup(SnippetViewSetGroup): menu_icon = "folder" menu_label = _("Error management") items = [ - # os itens a seguir possibilitam que na página Package.inspect - # funcionem os links para os relatórios XMLErrorViewSet, XMLErrorReportViewSet, XMLInfoReportViewSet, @@ -675,8 +665,17 @@ class UploadReportsViewSetGroup(SnippetViewSetGroup): register_snippet(UploadReportsViewSetGroup) +# =================================================================== +# URLs e hooks de botões +# =================================================================== + + @hooks.register("register_admin_urls") def register_disclosure_url(): return [ path("upload/", include("upload.urls", namespace="upload")), ] + + +# Registra os hooks de botões customizados +from . import button_helper # noqa: F401, E402 \ No newline at end of file