Skip to content

Commit 6a06504

Browse files
authored
feat: adding an “Importar Colunas” button (#835)
Este PR adiciona um novo botão "Importar Colunas" à interface administrativa do Django, localizado na categoria Tabelas. A funcionalidade permite que os administradores importem metadados de colunas diretamente de uma arquitetura previamente definida no Google Sheets, simplificando o processo de integração e manutenção de dados.
1 parent e481700 commit 6a06504

File tree

5 files changed

+281
-9
lines changed

5 files changed

+281
-9
lines changed

backend/apps/api/v1/urls.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from graphene_file_upload.django import FileUploadGraphQLView
66

77
from backend.apps.api.v1.search_views import DatasetFacetValuesView, DatasetSearchView
8-
from backend.apps.api.v1.views import DatasetRedirectView
8+
from backend.apps.api.v1.views import DatasetRedirectView, upload_columns
99

1010

1111
def redirect_to_graphql(request):
@@ -25,4 +25,5 @@ def graphql_view():
2525
path("facet_values/", DatasetFacetValuesView.as_view()),
2626
path("dataset/", DatasetRedirectView.as_view()),
2727
path("dataset_redirect/", DatasetRedirectView.as_view()),
28+
path("upload_columns/", upload_columns),
2829
]

backend/apps/api/v1/views.py

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# -*- coding: utf-8 -*-
22
from __future__ import annotations
33

4+
from typing import Dict, List
45
from urllib.parse import urlparse
56

6-
from django.http import HttpResponseRedirect
7+
import pandas as pd
8+
from django.http import HttpRequest, HttpResponseRedirect, JsonResponse
79
from django.views import View
810

9-
from backend.apps.api.v1.models import CloudTable, Dataset
11+
from backend.apps.api.v1.models import BigQueryType, CloudTable, Column, Dataset, Table
1012

1113
URL_MAPPING = {
1214
"localhost:8080": "http://localhost:3000",
@@ -34,3 +36,84 @@ def get(self, request, *args, **kwargs):
3436
return HttpResponseRedirect(f"{domain}/dataset/{resource.id}")
3537

3638
return HttpResponseRedirect(f"{domain}/404")
39+
40+
41+
def upload_columns(request: HttpRequest):
42+
# Aqui vai sua função
43+
44+
token, table_id, dataset_id, link = request.POST.values()
45+
46+
selected_table = Table.objects.get(id=table_id)
47+
48+
selected_table.columns.all().delete()
49+
50+
architecture = read_architecture_table(link)
51+
52+
tables_dict: Dict[str, Table] = {table.gbq_slug: table for table in Table.objects.all()}
53+
54+
columns: List[Column] = [
55+
create_columns(selected_table=selected_table, tables_dict=tables_dict, row=row)
56+
for _, row in architecture.iterrows()
57+
]
58+
59+
selected_table.columns.set(columns)
60+
61+
resultado = "Colunas Salvas com sucesso!"
62+
63+
print(token, table_id, dataset_id, link)
64+
65+
return JsonResponse({"status": "sucesso", "mensagem": resultado})
66+
67+
68+
def read_architecture_table(url: str) -> pd.DataFrame:
69+
id_spreadsheets = url.split("/")[-2]
70+
71+
spreadsheets_raw_url = (
72+
f"https://docs.google.com/spreadsheets/d/{id_spreadsheets}/gviz/tq?tqx=out:csv"
73+
)
74+
75+
df_architecture = pd.read_csv(spreadsheets_raw_url, dtype=str)
76+
77+
df_architecture = df_architecture.loc[df_architecture["name"] != "(excluido)"]
78+
79+
df_architecture.fillna("", inplace=True)
80+
81+
return df_architecture
82+
83+
84+
def create_columns(selected_table: Table, tables_dict: Dict[str, Table], row: pd.Series) -> Column:
85+
# Pegar ID do BigQueryType Model
86+
87+
row_bqtype = row["bigquery_type"].strip().upper()
88+
bqtype = BigQueryType.objects.get(name=row_bqtype)
89+
90+
# Pegar ID da coluna Diretorio
91+
92+
directory_column = None
93+
94+
if row["directory_column"]:
95+
full_slug_models = "basedosdados.{table_full_slug}"
96+
97+
table_full_slug = row["directory_column"].split(":")[0]
98+
table_full_slug = full_slug_models.format(table_full_slug=table_full_slug)
99+
100+
directory_column_name = row["directory_column"].split(":")[1]
101+
102+
table_directory = tables_dict[table_full_slug]
103+
104+
directory_column = table_directory.columns.get(name=directory_column_name)
105+
106+
# Definir Coluna
107+
108+
column = selected_table.columns.create(
109+
name=row["name"],
110+
description=row["description"],
111+
covered_by_dictionary=row["covered_by_dictionary"] == "yes",
112+
measurement_unit=row["measurement_unit"],
113+
contains_sensitive_data=row["has_sensitive_data"] == "yes",
114+
observations=row["observations"],
115+
bigquery_type=bqtype,
116+
directory_primary_key=directory_column,
117+
)
118+
119+
return column
Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,89 @@
1-
.btn-primary {
2-
color: #fff;
3-
background-color: #28a745;
4-
border-color: #28a745;
5-
box-shadow: none;
6-
}
1+
.btn-primary {
2+
color: #fff;
3+
background-color: #28a745;
4+
border-color: #28a745;
5+
box-shadow: none;
6+
}
7+
8+
.botao-imagem {
9+
padding: 0;
10+
border: none;
11+
background: none;
12+
width: 150px;
13+
height: 30px;
14+
}
15+
.botao-imagem img {
16+
width: 150px;
17+
height: 30px;
18+
}
19+
20+
.modal {
21+
display: none;
22+
position: fixed;
23+
top: 0;
24+
left: 0;
25+
width: 100%;
26+
height: 100%;
27+
background-color: rgba(0, 0, 0, 0.5);
28+
z-index: 1000;
29+
}
30+
31+
.modal-content {
32+
background-color: white;
33+
margin: 15% auto;
34+
padding: 20px;
35+
width: 80%;
36+
max-width: 500px;
37+
border-radius: 5px;
38+
}
39+
40+
.form-group {
41+
margin-bottom: 15px;
42+
}
43+
44+
.form-group label {
45+
display: block;
46+
margin-bottom: 5px;
47+
}
48+
49+
.form-group input {
50+
width: 100%;
51+
padding: 8px;
52+
border: 1px solid #ddd;
53+
border-radius: 4px;
54+
}
55+
56+
.close-button {
57+
float: right;
58+
font-size: 24px;
59+
cursor: pointer;
60+
}
61+
62+
.loading-overlay {
63+
display: none;
64+
position: absolute;
65+
top: 0;
66+
left: 0;
67+
width: 100%;
68+
height: 100%;
69+
background-color: rgba(255, 255, 255, 0.8);
70+
z-index: 1000;
71+
}
72+
73+
.loading-spinner {
74+
position: absolute;
75+
top: 50%;
76+
left: 50%;
77+
transform: translate(-50%, -50%);
78+
border: 4px solid #f3f3f3;
79+
border-top: 4px solid #3498db;
80+
border-radius: 50%;
81+
width: 40px;
82+
height: 40px;
83+
animation: spin 1s linear infinite;
84+
}
85+
86+
@keyframes spin {
87+
0% { transform: translate(-50%, -50%) rotate(0deg); }
88+
100% { transform: translate(-50%, -50%) rotate(360deg); }
89+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const modal = document.getElementById('dadosModal');
2+
const closeButton = document.querySelector('.close-button');
3+
const dadosForm = document.getElementById('dadosForm');
4+
const loadingOverlay = document.getElementById('loadingOverlay');
5+
6+
function mostrarCarregamento() {
7+
loadingOverlay.style.display = 'block';
8+
}
9+
10+
// Função para esconder a animação de carregamento
11+
function esconderCarregamento() {
12+
loadingOverlay.style.display = 'none';
13+
}
14+
15+
// Função para abrir a modal
16+
function abrirModal() {
17+
modal.style.display = 'block';
18+
}
19+
20+
// Função para fechar a modal
21+
function fecharModal() {
22+
modal.style.display = 'none';
23+
}
24+
25+
// Adicionar botão para abrir a modal
26+
// Event listeners
27+
closeButton.onclick = fecharModal;
28+
window.onclick = function(event) {
29+
if (event.target === modal) {
30+
fecharModal();
31+
}
32+
}
33+
34+
function processar() {
35+
36+
const formData = new FormData(dadosForm);
37+
mostrarCarregamento();
38+
39+
fetch('/upload_columns/', {
40+
method: 'POST',
41+
body: formData,
42+
headers: {
43+
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
44+
}
45+
})
46+
.then(response => response.json())
47+
.then(data => {
48+
alert('Dados enviados com sucesso!' + data);
49+
fecharModal();
50+
})
51+
.catch(error => {
52+
alert('Erro ao enviar dados: ' + error);
53+
})
54+
.finally(() => {
55+
esconderCarregamento(); // Esconde o carregamento independente do resultado
56+
});
57+
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{% extends "admin/change_form.html" %}
2+
{% load static %}
3+
{% block extra_actions %}
4+
{% if opts.model_name == 'table' %}
5+
6+
<button type="button" onclick="abrirModal()" class="btn btn-block {{ jazzmin_ui.button_classes.secondary }} btn-sm">
7+
Importar Colunas
8+
</button>
9+
10+
{% endif %}
11+
{% endblock %}
12+
{% block content %}
13+
{{ block.super }}
14+
{% if opts.model_name == 'table' %}
15+
<div id="dadosModal" class="modal" style="display: none;">
16+
<div class="modal-content">
17+
<div class="modal-header">
18+
<h2>Importar Colunas</h2>
19+
<span class="close-button">&times;</span>
20+
</div>
21+
<div class="modal-body">
22+
<form id="dadosForm">
23+
{% csrf_token %}
24+
<div class="form-group">
25+
<label for="table_id">Table ID:</label>
26+
<input type="text" id="table_id" name="table_id" required value="{{ original.pk }}" readonly>
27+
</div>
28+
<div class="form-group">
29+
<label for="dataset_id">Dataset ID:</label>
30+
<input type="text" id="dataset_id" name="dataset_id" required value="{{ original.dataset_id }}" readonly>
31+
</div>
32+
<div class="form-group">
33+
<label for="link_arquitetura">Link Arquitetura:</label>
34+
<input type="text" id="link_arquitetura" name="link_arquitetura" required>
35+
</div>
36+
<button type="button" class="btn btn-block {{ jazzmin_ui.button_classes.secondary }} btn-sm" onclick="processar()" class="btn">Processar</button>
37+
</form>
38+
<div class="loading-overlay" id="loadingOverlay">
39+
<div class="loading-spinner"></div>
40+
</div>
41+
</div>
42+
</div>
43+
</div>
44+
</div>
45+
<script src="{% static 'core/js/ferramentas.js' %}"></script>
46+
{% endif %}
47+
48+
{% endblock %}

0 commit comments

Comments
 (0)