Skip to content

Commit

Permalink
Modifica a entrada do pacote pelo upload e adiciona novas validações …
Browse files Browse the repository at this point in the history
…do XML (scieloorg#426)

* Corrige Institution.__str__, adiciona atributos de autocomplete e altera InstitutionHistory.panels de FieldPanel para Autocomplete (scieloorg#401)

* Faz correções na app journal: adiciona Journal.title, wagtail_hooks.JournalCreateView, etc  (scieloorg#402)

* Adiciona Journal.title

* Modifica os atributos de journal.models.Owner e Publisher

* Cria journal.wagtail.JournalCreateView para adicionar o usuário como creator

* Adiciona migrações de banco de dados relacionados a journal

* Adiciona filtros de journal_acron e publication_year para migrar dados de artigos (scieloorg#403)

* Adiciona filtros de journal_acron e publication_year para migrar dados de artigos, criando uma amostragem de migração

* Adiciona os parâmetros journal_acron e publication_year

* Garante que no XML migrado (seja nativo ou gerado a partir do HTML) tenha o PID v2 e o order (article-id other) (scieloorg#405)

* Corrige ou adiciona ao XML o elemento pid-v2 usando como valor o pid do artigo do site clássico

* Atualiza packtools versão 3.4.0 para ter XMLWithPre.order
Corrige ou adiciona ao XML o elemento article-id (other/order) usando como valor os últimos 5 dígitos do pid do artigo do site clássico

* Atualiza a versão da biblioteca scielo_classic_website para 1.6.4 para corrigir a obtenção de registros de artigos em serial xml

* Evita guardar versões anteriores dos arquivos

* Cria o procedimento de corrigir o valor do Pid v2 (scieloorg#410)

* Cria PidProviderXML.fix_pid_v2

* Cria FixPidV2 para controlar o que foi corrigido no upload e no core

* Cria FixPidV2ModelAdmin

* Adiciona PidProviderAPIClient.fix_pid_v2, fix_pid_v2_url. Refatora PidProviderAPIClient.enabled

* Cria APIPidProviderFixPidV2Error

* Cria provider.requester.PidRequester.fix_pid_v2

* Cria SPSPkg.fix_pid_v2

* Cria ArticleProc.fix_pid_v2 e adiciona a chamada no procedimento de generate_sps_package

* Cria tarefas para corriger o valor de pid v2 em PidProviderXML a partir de ArticleProc.pid

* Cria provider.provider.PidProvider com os métodos fix_pid_v2, get_sps_pkg_name, get_xmltree

* Adiciona a migração correspondente ao modelo FixPidV2

* Corrige ausencia de pid v3 no xml submetido do upload para o core (scieloorg#411)

* Atualiza a versão de packtools 4.1.1 para usar XMLWithPre.data e .files

* Modifica PidProviderXML.is_registered para atualizar os pids de xml_with_pre com os valores registrados, além disso, era necessário retornar se está registrado e igual ou registrado e diferente ou não registrado

* Distingue status de demanda de registro e status do registro

* Modifica PidProviderAPIClient._process_post_xml_response para atualizar ou não os valores dos pids de  xml_with_pre com os valores fornecidos pelo Core

* Adiciona registered_in_core como filtro de PidProviderXMLModelAdmin

* Atualiza dependencias base.txt e production.txt (scieloorg#409)

* Comenta app captcha

* Atualiza dependencias

---------

Co-authored-by: Roberta Takenaka <[email protected]>

* Modifica comportamento de Pid provider, que passa a aceitar mudanças de pids (scieloorg#415)

* Cria PidProviderXML.complete_pids, que completa pids com registrados ou inéditos

* Cria PidProviderXML._check_pids, que valida pid do XML é inédito e/ou registrado e/ou pertencente a outro documento

* Cria PidProviderXML.get_pids, que retorna todos os pids vigentes e outros

* Corrige PidProviderXML._is_registered_pid, adicionando a verificação em OtherPid

* Corrige PidProviderXML._get_unique_v3, que usa _is_registered_pid e agora não precisa verificar OtherPid

* Ajusta PidProviderXML._add_other_pid

* Remove PidProviderXML._complete_pids excedente

* Corrige PidProvider._add_pid_v3 e _add_pid_v2

* Corrige PidProviderXML.is_registered

* Ajusta PidProviderXML._save, removendo _add_other_pid e removendo change_pids

* Modifica PidProviderXML.register

* Melhora XMLVersion.__str__, mostrando nome do arquivo + data no lugar de pid v3

* Melhora _process_post_xml_response

* Para PidProvider.provide_pid_for_xml_with_pre, adiciona parâmetro caller, completa XML com pids registrados se ausentes no XML, adiciona xml_changed ao retorno

* Adiciona comando para completar XML com pids registrados antes de solicitar pid para Core

* Cria meio de configurar / habilitar / desabilitar fix_pid_v2 do Core (scieloorg#416)

* Cria a classe PidProviderEndpoint, inline de PidProviderConfig

* Modifica o modo de obter fix_pid_v2_url

* Adiciona modelo PidProviderEndpoint

* Adiciona 'fixed_in_core': False ao retorno de fix_pid_v2 (scieloorg#417)

* Evita que SPSPkg armazene arquivos em excesso (scieloorg#418)

* Verifica se xml registrado e xml recebido são iguais, somente após completar XML com os pids registrados (scieloorg#419)

* Compara se xml_with_pre é igual ao registrado somente após adicionar os pids registrados se aplicável

* Adiciona a funcionalidade de forçar o registro no Core mesmo que o registro está indicando que já está sincronizado

* Melhora ordem dos itens do menu (scieloorg#408)

* Refatora a funcionalidade da ordem do menu

* Reordena menu itens padrao do wagtail e remove algum deless

* Insere funcao get_menu_order em menu_order

* Altera a ordem dos app

* Move as operações anteriores de ArticleProc, IssueProc, JournalProc para um arquivo (scieloorg#420)

* Cria o modelo ArticleProcReport e ArticleProcReportModelAdmin

* Cria o modelo ProcReport para armazenar processamentos anteriores, mantendo apenas o vigente nos respectivos ArticleProc, IssueProc, JournalProc

* Adiciona as migrações de banco de dados

* Melhora o registro das operações das tarefas relacionadas à migração e publicação (scieloorg#422)

* Melhora os rótulos, deixa todos os campos não editáveis, apresenta os eventos do mais recente para o mais antigo

* Adiciona Article.data, Issue.data, Journal.data

* Adiciona retorno às função que criam instâncias de Article, Issue e Journal

* Adiciona Article.data, Issue.data, Journal.data nos detalhes das operações de entrada de dados

* Aplica black

* Adiciona

* Adiciona mais detalhes ao registro da tarefa de gerar o XML a partir do HTML

* Adiciona mais detalhes ao registro da tarefa de gerar o pacote SPS

* Corrige o valor de 'completed' dos resultados das operações de solicitação de pid v3

* Adiciona o parâmetro compression em ZipFile

* Modifica o sps_pkg_status para PENDING se o pacote não tem todos os texts

* Modifica o sps_pkg_status para DONE se o pacote não tem todos os texts

* Modifica o sps_pkg_status para PENDING se o pacote não tem todos os texts

* Corrige ausência de importação de ZIP_DEFLATED

* Adiciona o atributo order para a listagem dos itens na área administrativa

* Adiciona as migrações de banco de dados

* Adiciona detalhes do processamento da adição de arquivos no minio

* Refatora upload parte 3 - agrupa em uma tarefa as validações: assets, renditions, conteúdo do XML (scieloorg#398)

* Cria a tarefa upload.tasks.task_validate_original_zip_file

* Cria upload.tasks.task_validate_xml_content

* Cria upload.xml_validation

* Anota TODO para inserir parâmetros para as validações

* Atualiza packtools para a versão 3.3.4 que contempla mais validações

* Remove package.tasks

* Adiciona importações faltantes

* Refatora upload parte 3 - agrupa em uma tarefa as validações: assets, renditions, conteúdo do XML (scieloorg#399)

* Cria a tarefa upload.tasks.task_validate_original_zip_file

* Cria upload.tasks.task_validate_xml_content

* Cria upload.xml_validation

* Anota TODO para inserir parâmetros para as validações

* Atualiza packtools para a versão 3.3.4 que contempla mais validações

* Remove package.tasks

* Adiciona importações faltantes

* Refatora upload parte 2 - Adiciona funções em upload.controller para avaliar o pacote recém recebido (scieloorg#400)

* Cria os upload.choices.VE_UNEXPECTED_ERROR e VE_FORBIDDEN_UPDATE_ERROR

* Cria/Edita Package.get, create_or_update, _add_validation_result

* Cria funções para avaliar o XML recém-recebido (é esperado? os dados de journal e issue estão corretos?)

* Cria testes para upload.controller.*

* Adiciona a migração de banco de dados por criar novos valores de choices

* Corrige ausência de definição de variáveis

* Refatora upload parte 3 - agrupa em uma tarefa as validações: assets, renditions, conteúdo do XML (scieloorg#399)

* Cria a tarefa upload.tasks.task_validate_original_zip_file

* Cria upload.tasks.task_validate_xml_content

* Cria upload.xml_validation

* Anota TODO para inserir parâmetros para as validações

* Atualiza packtools para a versão 3.3.4 que contempla mais validações

* Remove package.tasks

* Adiciona importações faltantes

* Aplica black

* Cria função  para associar os tipos de erros com os relatórios e faz ajustes nos tipos de erros

* Associa por inferência o tipo de impacto de cada tipo de erro

* Refatora Package.check_opinions e check_resolutions; Remove article e issue do formulário

* Corrige defeitos das validações iniciais à recepção do pacote e ajusta a validação do conteúdo do XML

* Remove a verificação de article e issue no formulário

* Troca a tarefa que executará as validações

---------

Co-authored-by: Samuel Veiga Rangel <[email protected]>
  • Loading branch information
robertatakenaka and samuelveigarangel committed May 16, 2024
1 parent 039e019 commit 4f55e65
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 225 deletions.
61 changes: 24 additions & 37 deletions upload/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,26 @@
VE_PACKAGE_FILE_ERROR = "package-file-error"
VE_UNEXPECTED_ERROR = "unexpected-error"
VE_FORBIDDEN_UPDATE_ERROR = "forbidden-update-error"
VE_ARTICLE_JOURNAL_INCOMPATIBILITY_ERROR = "article-journal-incompatibility-error"
VE_ARTICLE_JOURNAL_INCOMPATIBILITY_ERROR = "journal-incompatibility-error"
VE_ARTICLE_IS_NOT_NEW_ERROR = "article-is-not-new-error"
VE_XML_FORMAT_ERROR = "xml-format-error"
VE_XML_CONTENT_ERROR = "xml-content-error"
VE_BIBLIOMETRICS_DATA_ERROR = "bibliometrics-data-error"
VE_SERVICES_DATA_ERROR = "services-data-error"
VE_DATA_CONSISTENCY_ERROR = "data-consistency-error"
VE_CRITERIA_ISSUES_ERROR = "criteria-issues-error"
VE_ASSET_ERROR = "asset-error"
VE_RENDITION_ERROR = "rendition-error"
VE_GROUP_DATA_ERROR = "group-error"

VALIDATION_ERROR_CATEGORY = (
(VE_UNEXPECTED_ERROR, "UNEXPECTED_ERROR"),
(VE_FORBIDDEN_UPDATE_ERROR, "FORBIDDEN_UPDATE_ERROR"),
(VE_ARTICLE_JOURNAL_INCOMPATIBILITY_ERROR, "ARTICLE_JOURNAL_INCOMPATIBILITY_ERROR"),
(VE_ARTICLE_IS_NOT_NEW_ERROR, "ARTICLE_IS_NOT_NEW_ERROR"),
(VE_XML_FORMAT_ERROR, "XML_FORMAT_ERROR"),
(VE_XML_CONTENT_ERROR, "VE_XML_CONTENT_ERROR"),
(VE_GROUP_DATA_ERROR, "VE_GROUP_DATA_ERROR"),
(VE_BIBLIOMETRICS_DATA_ERROR, "BIBLIOMETRICS_DATA_ERROR"),
(VE_SERVICES_DATA_ERROR, "SERVICES_DATA_ERROR"),
(VE_DATA_CONSISTENCY_ERROR, "DATA_CONSISTENCY_ERROR"),
Expand All @@ -75,56 +79,39 @@
VR_XML_OR_DTD = "xml_or_dtd"
VR_ASSET_AND_RENDITION = "asset_and_rendition"
VR_INDIVIDUAL_CONTENT = "individual_content"
VR_GROUPED_CONTENT = "grouped_content"
VR_GROUP_CONTENT = "group_content"
VR_STYLESHEET = "stylesheet"
VR_PACKAGE_FILE = "package_file"

VALIDATION_REPORT_ITEMS = {
VR_XML_OR_DTD: set(
[
VE_XML_FORMAT_ERROR,
]
),
VR_ASSET_AND_RENDITION: set(
[
VE_ASSET_ERROR,
VE_RENDITION_ERROR,
]
),
VR_INDIVIDUAL_CONTENT: set(
[
VE_ARTICLE_IS_NOT_NEW_ERROR,
VE_ARTICLE_JOURNAL_INCOMPATIBILITY_ERROR,
VE_BIBLIOMETRICS_DATA_ERROR,
VE_DATA_CONSISTENCY_ERROR,
]
),
VR_GROUPED_CONTENT: set(
[
VE_CRITERIA_ISSUES_ERROR,
VE_SERVICES_DATA_ERROR,
]
),
VR_PACKAGE_FILE: set(
[
VE_PACKAGE_FILE_ERROR,
]
),
}

VALIDATION_DICT_ERROR_CATEGORY_TO_REPORT = {
VE_XML_FORMAT_ERROR: VR_XML_OR_DTD,
VE_ASSET_ERROR: VR_ASSET_AND_RENDITION,
VE_RENDITION_ERROR: VR_ASSET_AND_RENDITION,
VE_ARTICLE_IS_NOT_NEW_ERROR: VR_INDIVIDUAL_CONTENT,
VE_ARTICLE_JOURNAL_INCOMPATIBILITY_ERROR: VR_INDIVIDUAL_CONTENT,
VE_XML_CONTENT_ERROR: VR_INDIVIDUAL_CONTENT,
VE_BIBLIOMETRICS_DATA_ERROR: VR_INDIVIDUAL_CONTENT,
VE_DATA_CONSISTENCY_ERROR: VR_INDIVIDUAL_CONTENT,
VE_CRITERIA_ISSUES_ERROR: VR_GROUPED_CONTENT,
VE_SERVICES_DATA_ERROR: VR_GROUPED_CONTENT,
VE_CRITERIA_ISSUES_ERROR: VR_INDIVIDUAL_CONTENT,
VE_SERVICES_DATA_ERROR: VR_INDIVIDUAL_CONTENT,
VE_GROUP_DATA_ERROR: VR_GROUP_CONTENT,
VE_PACKAGE_FILE_ERROR: VR_PACKAGE_FILE,
VE_UNEXPECTED_ERROR: VR_PACKAGE_FILE,
VE_FORBIDDEN_UPDATE_ERROR: VR_PACKAGE_FILE,

}


def _get_categories():
d = {}
for k, v in VALIDATION_DICT_ERROR_CATEGORY_TO_REPORT.items():
d.setdefault(v, [])
d[v].append(k)
return d


VALIDATION_REPORT_ITEMS = _get_categories()

# Model ValidationResult, Field status
VS_CREATED = "created"
VS_DISAPPROVED = "disapproved"
Expand Down
154 changes: 141 additions & 13 deletions upload/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys
from datetime import datetime

from django.utils.translation import gettext as _
from packtools.sps.models.journal_meta import Title, ISSN
from packtools.sps.pid_provider.xml_sps_lib import XMLWithPre, GetXMLItemsError
from packtools.sps.models.front_articlemeta_issue import ArticleMetaIssue
Expand All @@ -21,11 +22,19 @@
choices,
)
from .utils import file_utils, package_utils, xml_utils

from upload import xml_validation
from pid_provider.requester import PidRequester
from article.models import Article
from issue.models import Issue
from journal.models import OfficialJournal, Journal
from tracker.models import UnexpectedEvent
from upload.xml_validation import (
validate_xml_content,
add_app_data,
add_sps_data,
add_journal_data,
)

pp = PidRequester()

Expand Down Expand Up @@ -122,7 +131,7 @@ def receive_package(package):
data={},
)
# falhou, retorna response
return package
return response
# sucesso, retorna package
package._add_validation_result(
error_category=choices.VE_XML_FORMAT_ERROR,
Expand All @@ -132,11 +141,10 @@ def receive_package(package):
"xml_path": package.file.path,
},
)
return package
return response
except GetXMLItemsError as exc:
# identifica os erros do arquivo Zip / XML
_identify_file_error(package)
return package
return _identify_file_error(package)


def _identify_file_error(package):
Expand All @@ -145,13 +153,18 @@ def _identify_file_error(package):
xml_path = None
xml_str = file_utils.get_xml_content_from_zip(package.file.path, xml_path)
xml_utils.get_etree_from_xml_content(xml_str)
except (file_utils.BadPackageFileError, file_utils.PackageWithoutXMLFileError) as exc:
return {}
except (
file_utils.BadPackageFileError,
file_utils.PackageWithoutXMLFileError,
) as exc:
package._add_validation_result(
error_category=choices.VE_PACKAGE_FILE_ERROR,
message=exc.message,
status=choices.VS_DISAPPROVED,
data={"exception": str(exc), "exception_type": str(type(exc))},
)
return {"error": str(exc), "error_type": choices.VE_PACKAGE_FILE_ERROR}

except xml_utils.XMLFormatError as e:
data = {
Expand All @@ -166,6 +179,7 @@ def _identify_file_error(package):
data=data,
status=choices.VS_DISAPPROVED,
)
return {"error": str(e), "error_type": choices.VE_XML_FORMAT_ERROR}


def _check_article_and_journal(xml_with_pre):
Expand Down Expand Up @@ -198,7 +212,9 @@ def _check_article_and_journal(xml_with_pre):
if article:
# verifica a consistência dos dados de journal e issue
# no XML e na base de dados
_compare_journal_and_issue_from_xml_to_journal_and_issue_from_article(article, response)
_compare_journal_and_issue_from_xml_to_journal_and_issue_from_article(
article, response
)
if response.get("error"):
# inconsistências encontradas
return _handle_error(response, article, article_previous_status)
Expand Down Expand Up @@ -241,7 +257,9 @@ def _get_article_previous_status(article, response):
response["package_category"] = choices.PC_ERRATUM
return article_previos_status
else:
response["error"] = f"Unexpected package. Article has no need to be updated / corrected. Article status: {article_previos_status}"
response[
"error"
] = f"Unexpected package. Article has no need to be updated / corrected. Article status: {article_previos_status}"
response["error_type"] = choices.VE_FORBIDDEN_UPDATE_ERROR
response["package_category"] = choices.PC_UPDATE

Expand Down Expand Up @@ -284,12 +302,12 @@ def _get_journal(journal_title, issn_electronic, issn_print):

if not j and journal_title:
try:
j = OfficialJournal.objects.get(journal_title=journal_title)
j = OfficialJournal.objects.get(title=journal_title)
except OfficialJournal.DoesNotExist:
pass

if j:
return Journal.objects.get(official=j)
return Journal.objects.get(official_journal=j)
raise Journal.DoesNotExist(f"{journal_title} {issn_electronic} {issn_print}")


Expand All @@ -301,11 +319,11 @@ def _check_journal(origin, xmltree):
xml = ISSN(xmltree)
issn_electronic = xml.epub
issn_print = xml.ppub

return dict(journal=_get_journal(journal_title, issn_electronic, issn_print))
except Journal.DoesNotExist:
except Journal.DoesNotExist as exc:
logging.exception(exc)
return dict(
error=f"Journal in XML is not registered in Upload: {journal_title} {issn_electronic} (electronic) {issn_print} (print)",
error=f"Journal in XML is not registered in Upload: {journal_title} (electronic: {issn_electronic}, print: {issn_print})",
error_type=choices.VE_ARTICLE_JOURNAL_INCOMPATIBILITY_ERROR,
)
except Exception as e:
Expand Down Expand Up @@ -347,7 +365,9 @@ def _check_issue(origin, xmltree, journal):
return {"error": str(e), "error_type": choices.VE_UNEXPECTED_ERROR}


def _compare_journal_and_issue_from_xml_to_journal_and_issue_from_article(article, response):
def _compare_journal_and_issue_from_xml_to_journal_and_issue_from_article(
article, response
):
issue = response["issue"]
journal = response["journal"]
if article.issue is issue and article.journal is journal:
Expand All @@ -366,3 +386,111 @@ def _compare_journal_and_issue_from_xml_to_journal_and_issue_from_article(articl
error_type=choices.VE_DATA_CONSISTENCY_ERROR,
)
)


def validate_xml_content(package, journal, issue):
# VE_BIBLIOMETRICS_DATA_ERROR = "bibliometrics-data-error"
# VE_SERVICES_DATA_ERROR = "services-data-error"
# VE_DATA_CONSISTENCY_ERROR = "data-consistency-error"
# VE_CRITERIA_ISSUES_ERROR = "criteria-issues-error"

# TODO completar data
data = {}
# add_app_data(data, app_data)
# add_journal_data(data, journal, issue)
# add_sps_data(data, sps_data)

try:
for xml_with_pre in XMLWithPre.create(path=package.file.path):
_validate_xml_content(package, xml_with_pre, data)
except Exception as e:
exc_type, exc_value, exc_traceback = sys.exc_info()
UnexpectedEvent.create(
exception=e,
exc_traceback=exc_traceback,
detail={
"operation": "upload.controller.validate_xml_content",
"detail": dict(file_path=package.file.path),
},
)


def _validate_xml_content(package, xml_with_pre, data):
# TODO completar data
data = {}
# xml_validation.add_app_data(data, app_data)
# xml_validation.add_journal_data(data, journal, issue)
# xml_validation.add_sps_data(data, sps_data)

try:
results = xml_validation.validate_xml_content(
xml_with_pre.sps_pkg_name, xml_with_pre.xmltree, data
)
for result in results:
_handle_xml_content_validation_result(package, xml_with_pre.sps_pkg_name, result)
try:
error = ValidationResult.objects.filter(
package=package,
status=choices.VS_DISAPPROVED,
category__in=choices.VALIDATION_REPORT_ITEMS[choices.VR_INDIVIDUAL_CONTENT],
)[0]
package.status = choices.PS_VALIDATED_WITH_ERRORS
except IndexError:
# nenhum erro
package.status = choices.PS_VALIDATED_WITHOUT_ERRORS
package.save()
except Exception as e:
exc_type, exc_value, exc_traceback = sys.exc_info()
UnexpectedEvent.create(
exception=e,
exc_traceback=exc_traceback,
detail={
"operation": "upload.controller._validate_xml_content",
"detail": {
"file": package.file.path,
"item": xml_with_pre.sps_pkg_name,
"exception": str(e),
"exception_type": str(type(e)),
},
},
)


def _handle_xml_content_validation_result(package, sps_pkg_name, result):
# ['xpath', 'advice', 'title', 'expected_value', 'got_value', 'message', 'validation_type', 'response']

try:
if result["response"] == "OK":
status = choices.VS_APPROVED
else:
status = choices.VS_DISAPPROVED

# VE_BIBLIOMETRICS_DATA_ERROR, VE_SERVICES_DATA_ERROR,
# VE_DATA_CONSISTENCY_ERROR, VE_CRITERIA_ISSUES_ERROR,
error_category = result.get("error_category") or choices.VE_XML_CONTENT_ERROR

message = result["message"]
advice = result["advice"] or ""
message = ". ".join([_(message), _(advice)])
package._add_validation_result(
error_category=error_category,
status=status,
message=message,
data=result,
)
except Exception as e:
exc_type, exc_value, exc_traceback = sys.exc_info()
UnexpectedEvent.create(
exception=e,
exc_traceback=exc_traceback,
detail={
"operation": "upload.controller._handle_xml_content_validation_result",
"detail": {
"file": package.file.path,
"item": sps_pkg_name,
"result": result,
"exception": str(e),
"exception_type": str(type(e)),
},
},
)
8 changes: 1 addition & 7 deletions upload/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,12 @@


class UploadPackageForm(WagtailAdminModelForm):
def save_all(self, user, article, issue):
def save_all(self, user):
upload_package = super().save(commit=False)

if self.instance.pk is None:
upload_package.creator = user

if article is not None:
upload_package.article = article

if issue is not None:
upload_package.issue = issue

self.save()

return upload_package
Expand Down
Loading

0 comments on commit 4f55e65

Please sign in to comment.