Artigo
· Jul. 8 19min de leitura

Um portal para administrar armazenamento feito com Django - Parte 2

Nosso objetivo

Hoje vamos continuar expandindo o último artigo compartilhando informações sobre alguns recursos que adicionamos em nosso portal. Vamos incluir uma pitada de CSS para visualizar os dados disponíveis melhor e exportá-los. Finalmente, estudaremos como adicionar opções de filtros e ordenação. Ao fim deste artigo, você deve ser capaz de exibir esteticamente uma query simples completa.

Anteriormente, em "Um portal para administrar armazenamento feito com Django"...

Vamos voltar de onde paramos antes de seguir o desenvolvimento do portal. Anteriormente, criamos a a base do projeto em linha de comando com alguns códigos prontos do Django como startproject. Então, adicionamos os requisitos para conectar à base de dados no arquivo requirements.txt e seus parâmetros em settings.py para simplificar a instalação. Então, criamos uma aplicação Globals com as URLs apropriadas e alguns caminhos direcionando os usuários do projeto principal até as views, que por sua vez se comunicam com os dados por meio de modelos. Também criamos uma pasta API com alguns métodos de levantar informações sobre uma base de dados remota. Com tudo mencionado acima, deixamos uma opção para criar comunicação entre instâncias IRIS diferentes. Finalmente, fizemos o index.html para exibir tudo isso no link http://127.0.0.1:8000/globals/.

Nessa altura, nossos bastidores devem parecer com a imagem abaixo.

No entanto, não tão arrumado quanto a ilustração anterior, o nosso palco principal se parece como o print de tela que segue.

Adicionando CSS

A Cascading Style Sheets (CSS - folhas de estilo em cascata) pode transformar esse projeto em algo muito mais apresentável, de melhor aparência e mais agradável de se trabalhar. Nessa seção, você aprenderá como usar CSS no seu projeto. Também vamos te guiar por alguns conceitos básicos dessa linguagem. No entand=to, nosso objetivo principal será conectar o arquivo de estilos e o projeto.

Em primeiro lugar, crie uma pasta chamada "static" dentro do diretório /globals. Precisaremos dela para guardar todos os nossos arquivos estáticos posteriormente. Dentro dessa pasta, você pode adicionar o arquivo styles.css.

Agora nós finalmente podemos tornar tudo limpo e colorido. Contudo, se você não conhece nada sobre CSS, não se preocupe! Tenho boas notícias par você: é bastante intuitivo para começar. Tudo o que você precisa é escrever o elemento e adicionar o design entre chaves {}. Você pode acessar classes, sinalizando-as com um ponto, e IDs, sinalizando-os com uma hashtag #. Se quiser uma dica profissional, você pode usar um asterisco para adicionar o estilo a cada elemento dentro desse arquivo HTML.

Por exemplo, se adicionarmos um ID e uma classe aos últimos três parágrafos do index.html, poderemos acessá-los separadamente e adicionar diferentes cores, fundos e fontes.

<p>showing results for {{globals.count}} globals</p>
<p class="totalSize">total size: {{sumSize.size__sum}}</p>
<p id="allocatedSize">total allocated size: {{sumAllocated.allocatedsize__sum}}</p>
* {
    font-family: 'Lucida Sans';
}

p {
    background-color: yellow;
}

.totalSize {
    color: brown;
}

#allocatedSize {
    font-weight: bold;
}

De acordo com esse código, depois de configurar as conexões apropriadas, toda fonte exibida na página deve ser da família Lucida Sans, os três parágrafos devem ter um fundo amarelo, o parágrafo com a classe totalSize deve ter uma fonte marrom e o parágrafo com o ID allocatedSize terá uma fonte em negrito.

Você deve notar algumas peculiaridades se brincar um pouco com os elementos adicionando IDs, classes e estilos. O primeiro é hierarquia: as definições de IDs, sobrescrevem as de classes, que por sua vez sobrescrevem os estilos gerais. Segundo, você pode ter vários elementos compartilhando uma mesma classe. Além disso, itens filhos herdarão a maioria do estilo de seus itens pais (algumas coisas não serão herdadas, mas isso é um tópico mais avançada que não cobriremos nesse artigo). Se desejar, compartilhe conosco nos comentários qual desses comportamentos você notou e quais você ainda não sabia.

Em seguida, precisamos fazer o projeto entender o que é uma folha de estilo e onde encontrá-la. Então, podemos colocar a referência ao styles.css no arquivo HTML.

Primeiro, vá para settings.py, procure pela variável STATIC_URL e mude seu valor para o que segue.

STATIC_URL = '/static/'

Essa configuração deve notificar o projeto que todo arquivo estático pode ser encontrado numa pasta chamada "static", dentro da pasta do app. Nesse caso, é o diretório /globals/static. Se tivermos outro app sendo usado por esse projeto, ele só reconheceria sua própria pasta interna static (a não ser que seja especificado que haja uma geral).

Arquivos estáticos são aqueles que não mudam quando a aplicação está rodando. Você pode usá-los para guardar funções, definições, constantes e imagens que dão assistência ao código (nesse caso, arquivos .py) durante sua execução. Na prática, é aqui que você guarda JavaScript e CSS.

O próximo passo é referenciá-la do index.html. Adicione uma tag <head>> antes do corpo <body>. Dentro do <body>, use uma tag <link> para relacionar a uma folha de estilo do tipo text/CSS com a referência "{% static 'styles.css' %}" e media "screen". Além disso, você precisa dizer ao Django para carregar os estáticos. Lembre-se que podemos "dizer ao Django" coisas por meio do arquivo HTML adicionando código entre chaves e símbolos de porcentagem, como mostrado no exemplo: "{% seu código aqui %}".

<head>
    {% load static %}
    <link rel="stylesheet" type="text/CSS" href="{% static 'styles.css' %}" media="screen"/>
</head>

Eu mudei nossa lista de globais para uma tabela com uma sintaxe similar. Então, adicionei ao projeto um bom espaçamento entre os elementos assim como as cores da Innovatium. Você pode checar o arquivo CSS que eu criei no meu GitHub e ver como essa tabela ganhou a aparência ilustrada abaixo. Não sou especialista em CSS ou web design. No entanto, se tiver questões eu ficarei feliz em ajudar e quem sabe até aprender novas coisas juntos. Deixe um comentário ou me mande uma mensagem!

<table>
    <thead>
        <tr>
            <th>Database</th>
            <th>Global</th>
            <th>Size</th>
            <th>Allocated</th>
        </tr>
    </thead>
    <tbody>
        {% for global in globals %}
        <tr>
            <td>{{ global.database }}</td>
            <td>{{ global.name }}</td>
            <td>{{ global.size }}</td>
            <td>{{ global.allocatedsize }}</td>
        </tr>
        {% endfor %}
    </tbody>
</table>


PS.: se os arquivos estáticos estão em cache, você pode não ver mudanças ao recarregar. Você pode apertar Ctrl+Shift+Delete e remover o cache armazenado quando você usa o Google Chrome como navegador. Então, recarregue a página outra vez. Se tiver feito tudo corretamente, verá o seguinte no seu terminal:

 

DICA: para evitar a necessidade de limpar o cache toda vez que usa o programa, adicione a linha a seguir à <head> do seu arquivo HTML: <meta http-equiv="cache-control" content="no-cache" />. Essa tag deve previnir a página de armazenar o cache.

Exportando dados

Nessa seção, vamos retornar ao backend. Pode ser uma boa ideia revisar a parte 1 dessa série de artigos caso você não esteja familiarizado com o caminho que as requisições seguem (template -> URL -> view -> base de dados e então de volta).

Vamos fazer um formulário para que o usuário escolha exportar para CSV, XML ou JSON, já que essas linguagens já cobrem a maioria dos usos para transferência de dados. Ainda assim, você pode adicionar quantas linguagens quiser. Para cumprir nossa tarefa, precisaremos de um formulário com um método HTTP, um token de segurança, três inputs de radio (você vai ver em breve o que raios é isso) e um botão de enviar.

<form method="GET">
    {% csrf_token %}
    <input type="radio" name="exportLanguage" value="CSV"/>
    <label for="CSV">CSV</label>
    <input type="radio" name="exportLanguage" value="XML"/>
    <label for="XML">XML</label>
    <input type="radio" name="exportLanguage" value="JSON"/>
    <label for="JSON">JSON</label>
    <button type="submit" formaction="{% url 'export' %}"> Export </button>
</form>

Se quiser que as tags <label> mostrem um texto "clicável" para o valor correspondente, adicione a propriedade "onclick" na tag com o valor getElementById(‘The corresponding ID’).checked = true, assim como IDs correspondentes para cada opção. A propriedade formaction especificada no botão, direciona à URL. Como resultado, você pode ter quantos botões quiser apontando para URLs diferentes, para enviar o formulário de acordo com a ação desejada.

Quando finalizado o passo anterior, podemos adicionar o caminho que nos direciona à view em urls.py e finalmente criar a view em views.py. Essa view deve parecer um pouco mais complexa em comparação às que já fizemos anteriormente mas, passo a passo, vamos conseguir criá-la juntos.

from .views import home, update, export

urlpatterns = [
    path('', home),
    path('update', update, name="update"),
    path('export', export, name="export")
]

Primeiro, precisamos atribuir uma variável para referenciar as globais. Então, precisamos criar um caminho onde o arquivo deve aparecer quando o usuário clica no botão de exportar (podemos fazê-lo editável no lado do cliente). Além disso, precisamos saber qual linguagem foi selecionada e fazer a exportação apropriada para cada uma.

import os

def export(request):
    globals = irisGlobal.objects.all()           # pega as globais
    cd = os.getcwd()                             # pega o diretório atual
    language = request.GET.get("exportLanguage") # pega a linguagem selecionada no formulário
    if language == "CSV":
        pass
    elif language == "XML":
        pass
    elif language == "JSON":
        pass
    return redirect(home)

Para o CSV, tudo o que precisamos fazer é colocar cada registro numa linha e cada coluna separada por uma vírgula. A maneira mais intuitiva é concatenar toda a informação de cada global numa string entre vírgulas, seguida por um terminador de linha e escrever cada uma em um arquivo.

if language == "CSV":
    with open(cd+"\\test.csv", "w") as file:
        for eachGlobal in globals:
            row = eachGlobal.database+", "+eachGlobal.name+", "+str(eachGlobal.size)+", "+str(eachGlobal.allocatedsize)+"\n"
            file.write(row)
        

Para o JSON e XML, vamos precisar dos serializadores Django. Eles podem parecer complexos mas, na realidade, são bem simples. O módulo de serializadores tem dois métodos: serialize deserialize, que podem converter informação de e para a sua linguagem de preferência. Você também pode projetar serializadores customizados. Por sorte, os de XML e JSON já existem.

from django.core import serializers

[...]

elif language =="XML":
    with open(cd+"\\test.xml", "w") as file:
        globals = serializers.serialize("xml", globals)
        file.write(globals)
        
elif language =="JSON":
    with open(cd+"\\test.json", "w") as file:
        globals = serializers.serialize("json", globals)
        file.write(globals)

Muito bem! A aplicação está finalmente pronta para recarregar e testar. Depois de exportar sua área de trabalho deve magicamente se parecer como a imagem abaixo:
 

Filtros

Vamos começar filtrando por base de dados. Precisamos fazer uma tag de formulário com input de texto para colocar o nome da base de dados e referência à URL. Podemos usar a view home com algumas adaptações.

<form method="GET" action="{% url 'home' %}">
    {% csrf_token %}
    <input type="text" name="database" placeholder="Database"/>
</form>

Já que agora estamos referenciando o caminho home no index, vamos dar um nome a ele nas patterns, em urls.py.

path('', home, name="home"),

Lembre-se que na view home, pegamos todas as globais do modelo com irisGlobal.objects.all() e as retornamos ao index. Nesse ponto, precisamos retornar apenas um conjunto filtrado delas, ao invés de todas. A boa notícia é que vamos resolver esse problema com apenas quatro linhas de código.

Primeiro, assim como fizemos na exportação, precisamos buscar informação do input com request.GET.get() e reduzir nosso conjunto de globais de acordo com o que o usuário deseja. Graças à ajuda do objeto de consulta do Django.db.models, seremos capazes de usar a função de filtro para atingir nosso objetivo.

from django.db.models import Sum, Q
from .models import irisGlobal

def home(request):
    globals = irisGlobal.objects.all()
    databaseFilter = request.GET.get("database")
    
    if databaseFilter:
        query = Q(database__contains=databaseFilter)
        globals = globals.filter(query)
        

O Q() é o objeto de consulta (Query). Dentro desse objeto, você pode adicionar o nome de uma coluna, dois underlines e uma declaração SQL para refinar a busca. Depois disso, você pode passar o quaisquer objetos Q que você quiser como argumentos da função filter (filtro), e serão unidos por operadores "AND" (a não ser que você especifique para juntar com "OR"). Tem muitas outras coisas que você pode fazer com a classe Q. Se quiser, pode ler tudo sobre ele e outras maneiras de fazer consultas no Django na documentação oficial.

Agora você pode recarregar e testar seus filtros. Lembre-se de prestar atenção em como os agregadores no fim da página se adaptam aos filtros, já que os construímos a partir da variável globals.

Mais filtros!

Se você se sentiu confortável com a última seção, podemos ficar mais confiantes com o assunto. É hora de adicionar mais filtros e combiná-los. Para começar, vamos colocar alguns outros inputs no nosso formulário.

<form method="GET">
    {% csrf_token %}
    <input type="text" name="database" placeholder="Database"/>
    <input type="text" name="global" placeholder="Global name"/>
    <input type="number" name="size" placeholder="Size" step="0.001"/>
    <input type="number" name="allocated" placeholder="Allocated size" step="0.001"/>
    <button type="submit" formaction="{% url 'home' %}"> Filter </button>
</form>

Na view, adicione os novos filtros à variável globals. Eles devem formar uma cadeia e serão equivalentes à declaração SQL abaixo.

SELECT * FROM globals

WHERE database LIKE ‘%databaseFilter%’ AND

      globals LIKE ‘%globalsFilter%’ AND

      size >=sizeFilter AND

      allocatedsize>=allocFilter

globals = irisGlobal.objects.all()
databaseFilter = request.GET.get("database")
globalFilter = request.GET.get("global")
sizeFilter = request.GET.get("size")
allocFilter = request.GET.get("allocated")

if databaseFilter:
    globals = globals.filter(Q(database__contains=databaseFilter))
else:
    databaseFilter=""
if globalFilter:
    globals = globals.filter(Q(name__contains=globalFilter))
else:
    globalFilter=""
if sizeFilter:
    globals = globals.filter(Q(size__gte=sizeFilter))
else:
    sizeFilter=""
if allocFilter:
    globals = globals.filter(Q(allocatedsize__gte=allocFilter))
else:
    allocFilter=""

O filtro já está funcionando, mas ainda podemos melhorar um pouco. Se passarmos as variáveis recebidas no return, poderemos usá-las como valores nos inputs. Então, elas não irão desaparecer quando recarregarmos a página ou apertarmos o botão filter.

return render(request, "index.html", {"globals": globals,
    "sumSize": sumSize,
    "sumAllocated":sumAllocated,
    "database":databaseFilter
    "global":globalFilter
    "size":sizeFilter,
    "allocated":allocFilter
})
<form method="GET">
    {% csrf_token %}
    <input type="text" name="{{database}}" placeholder="Database"/>
    <input type="text" name="{{global}}" placeholder="Global name"/>
    <input type="number" name="{{size}}" placeholder="Size" step="0.001"/>
    <input type="number" name="{{allocated}}" placeholder="Allocated size" step="0.001"/>
    <button type="submit" formaction="{% url 'home' %}"> Filter </button>
</form>

Podemos fornecer aos filtros opções para atualizações que são menores ou iguais que um valor específico. Também podemos programar atualizações ao vivo (atualização a cada tecla que o usuário aperta enquanto digita os inputs). No entanto, por hora vamos deixar isso em mente como apenas ideias e vamos seguir para adicionar ordenação.

Exportando um conjunto filtrado

Depois de tudo o que fizemos, é natural que você queira adaptar a view de exportação para funcionar com os filtros que adicionamos.

Podemos colocar toda a lógica que adicionamos aos filtros em uma função, que chamaremos de handle_filters, e ao invés de usar a array criada no irisGlobals.objects.all() para obter a informação, vamos usar a criada por essa função. Para fazer tudo isso funcionar, vamos colocar os dois formulários juntos.

def handle_filters(request):
    globals = irisGlobal.objects.all()
    databaseFilter = request.GET.get("database")
    globalFilter = request.GET.get("global")
    sizeFilter = request.GET.get("size")
    allocFilter = request.GET.get("allocated")
    
    if databaseFilter:
        globals = globals.filter(Q(database__contains=databaseFilter))
    else:
        databaseFilter=""
    if globalFilter:
        globals = globals.filter(Q(name__contains=globalFilter))
    else:
        globalFilter=""
    if sizeFilter:
        globals = globals.filter(Q(size__gte=sizeFilter))
    else:
        sizeFilter=""
    if allocFilter:
        globals = globals.filter(Q(allocatedsize__gte=allocFilter))
    else:
        allocFilter=""
    return globals, databaseFilter, globalFilter, sizeFilter, allocFilter
def export(request):
    globals = handle_filters(request)[0]
<form method="GET">
    {% csrf_token %}
    <input type="radio" name="exportLanguage" value="CSV"/>
    <label for="CSV">CSV</label>
    <input type="radio" name="exportLanguage" value="XML"/>
    <label for="XML">XML</label>
    <input type="radio" name="exportLanguage" value="JSON"/>
    <label for="JSON">JSON</label>
    <button type="submit" formaction="{% url 'export' %}"> Export </button>
    <br/>
    <input type="text" name="{{database}}" placeholder="Database"/>
    <input type="text" name="{{global}}" placeholder="Global name"/>
    <input type="number" name="{{size}}" placeholder="Size" step="0.001"/>
    <input type="number" name="{{allocated}}" placeholder="Allocated size" step="0.001"/>
    <button type="submit" formaction="{% url 'home' %}"> Filter </button>
</form>

 

Ordenar tabela (com JavaScript)

Essa seção irá te mostrar como eu adicionei opções de ordenação à tabela. Há muitas maneiras de fazer isso, mas hoje vamos usar JavaScript para aprender algo diferente do que já fizemos antes. É claro que será melhor se você já tiver algum conhecimento dos conceitos básicos da linguagem, mas você será capaz de acompanhar de qualquer maneira. Entretanto, o foco principal aqui é entender as conexões. Elas serão bastante similares às do CSS e abrirão ainda mais portas para você administrar dados no portal.

Vamos começar! Abra o index.html e adicione duas tags <script> lá no final do <body>. A primeira deve fazer referência às dependências. A segunda aponta para o arquivo .js no nosso projeto (vamos criá-lo no passo seguinte). Já que já adicionamos o "{% load static %}", isso será suficiente para o arquivo JavaScript começar a funcionar.

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="{% static 'index.js'%}"></script>

Uma vez que essa conexão for estabelecida, crie um arquivo dentro da pasta /static e nomeie como index.js (ou qualquer que seja o nome que você chamou na referência). Se, nesse ponto, você adicionar um  console.log(“Hello world!”); ao arquivo e recarregar a página, poderá ver que já está funcionando. Isso significa que podemos finalmente começar a criar a lógica de ordenação.

Queremos que a tabela seja ordenada quando o usuário clica em alguma header. Para tornar isso possível, diremos ao dispositivo de exibição para escutar ao usuário quando ele clica em qualquer uma das headers. Temos o documento, do qual selecionamos cada elemento <th> da tabela e, para cada um deles, queremos saber quando foi clicado.

document.querySelectorAll("table th").forEach(header => {
    header.addEventListener("click", () => {
        
    });
})

Agora podemos dizer o que fazer quando ouve o evento "click". Precisaremos da tabela (item pai de <thead>, que é o item pai de <tr>, que por sua vez é o item pai do <th> clicado) e um número indicando qual header foi selecionada. Também precisamos saber se a tabela já está em ordem crescente por essa coluna. Então, chamamos uma função para ordenar a tabela.

document.querySelectorAll("table th").forEach(header => {
    header.addEventListener("click", () => {
        const table = header.parentElement.parentElement.parentElement;
        const columnIndex = Array.prototype.indexOf.call(header.parentElement.children, header);
        const isAscending = header.classList.contains("ascending");
        sortTable(table, columnIndex+1, !isAscending);
    });
})

Existem muitos algoritmos de ordenação de tabela já criados. Você pode até fazer o download de algum e usá-lo. Eu quero evitar adicionar mais dependências a esse projeto, então vou usar o algoritmo a seguir.

function sortTable(table, columnIndex, ascending = true) {
    const direction = ascending ? 1 : -1;                 // se for crescente, a direção é 1, se não, é -1
    const header = $('th:nth-child('+columnIndex+')');    // pega a header com índice da coluna
    const body = table.children[1]                        // pega o body (segundo item filho da tabela)
    const rows = Array.from(body.querySelectorAll("tr")); // cria um array para as linhas no body
    // remove ordenação anterior de todas as headers
    table.querySelectorAll("th").forEach(th => th.classList.remove("ascending", "descending"));

    // adiciona ordenação à header selecionada
    header[0].classList.toggle("ascending", ascending);
    header[0].classList.toggle("descending", !ascending);

    // algoritmo de ordenação das linhas
    const sorted = rows.sort((a, b) => {
        const aColumn = a.querySelector('td:nth-child('+columnIndex+')').textContent;
        const bColumn = b.querySelector('td:nth-child('+columnIndex+')').textContent;

        return aColumn > bColumn ? (1*direction) : (-1*direction);
    });

    // remove as linhas do body e adiciona as linhas ordenadas
    while (body.firstChild) {
        body.removeChild(body.firstChild);
    }
    body.append(...sorted);
    
}

O link a seguir explica o algoritmo mostrado acima. https://dcode.domenade.com/tutorials/how-to-easily-sort-html-tables-with-css-and-javascript

Agora, com algum CSS, você tem uma tabela bonita e ordenada.

.ascending::after {
    content: "\25be";
}

.descending::after {
    content: "\25b4";
}

.ascending, .descending {
    background-color: #ffffff30;
    box-shadow: .1px .1px 10.px #aaa;
}

 

Uma abordagem diferente para adicionar ordenação

Uma outra forma de adicionar ordem é criar um formulário com uma opção para escolher cada coluna (com um pouco de JavaScript, você também pode usar as headers para seleção), direcionar a uma view que usa a função .order_by(“column name”) no objeto globals e retorná-las ordenadas na função render. As duas opções estão implementadas no meu  GitHub se quiser dar uma olhada. Preste atenção ao fato que, no primeiro método, não estamos mudando os dados ou o conjunto de resultados. Já que estamos apenas ajustando a forma que é exibida, não afeta a exportação.

 

Mais uma dica sobre o JavaScript

Aqui eu preparei só mais uma coisa para você que pode ser útil ao usar Django e JavaScript. Se quiser redirecionar a uma URL (por exemplo, o update), você pode usar document.location.href = “update”; no arquivo .js.

 

Por que eu iria querer saber tudo isso se sou um desenvolvedor IRIS?

Você pode ter notado que esse artigo inteiro não usou o IRIS diretamente. Os tutoriais da parte 2 seriam muito similares se estivéssemos usando qualquer outra base de dados. Porém, se conseguimos construir tudo explicado acima usando a tecnologia InterSystems como base, podemos adaptar esse portal para mostrar muito mais com facilidade. Se você já é um desenvolvedor IRIS, pode imaginar facilmente como esse simples portal pode combinar informações de múltiplas origens (não necessariamente globais), além da habilidade de providenciar todo tipo de tratamento e análise de dados, com as ferramentas de Cloud, IntegratedML (Machine Learning), Business Inteligence, Text Analytics e Natural Language Processing (NLP0, etc. Quando todas as ferramentas da InterSystems se encontram com uma completa, fácil e bonita interação com o usuário, fornecida com toda segurança necessária, as possibilidades vão além da imaginação.

Aqui eu coletei alguns exemplos para ilustrar meu ponto. Você pode ter uma máquina sensorial que envia informações à InterSystems Callin API ,integrada com um programa C++. Essa data pode ser interpretada, tratada e guardada no IRIS e sua análise pode ser exibida com Django, permitindo ao usuário interagir com ela. Combinando com o InterSystems IRIS for Health e HealthShare, esse portal rapidamente se torna uma ferramenta de rastreio para pacientes em terapia. Ele pode usar Machine Learning e BI para detectar e prever mudanças perigosas e enviar alertas com SAM (System Alerting and Monitoring) ao doutor responsável, que pode ser exibido no portal junto com toda infor,ação sobre o caso necessária pra tomar uma decisão rápida.

Em outro contexto, mas com uma lógica similar, o InterSystems IRIS for Supply Chain pode ser usado com esse portal para permitir que o usuário veja o que pode estar comprometendo a conformidade do sistema. Tal colaboração nos permite fazer e transmitir decisões de uma maneira que toda a operação seja facilmente compreendida e controlada.

Discussão (0)1
Entre ou crie uma conta para continuar