Browse Source

Feat(OnlyOffice): Fonte Arial padrão, SweetAlert2 e fix beforeunload

- Fonte Arial forçada em todos os slots XML (ascii, hAnsi, eastAsia, cs)
  nos documentos criados via python-docx e na numeração de matéria
- Substituir alert() nativo por SweetAlert2 nos 3 templates do editor
- Corrigir beforeunload que disparava sem alterações no documento
pull/3858/head
rangelbruno 7 months ago
parent
commit
cf8b06265d
  1. 4
      sapl/base/onlyoffice_template_views.py
  2. 8
      sapl/materia/onlyoffice_materia_views.py
  3. 4
      sapl/materia/onlyoffice_views.py
  4. 4
      sapl/norma/onlyoffice_views.py
  5. 4
      sapl/protocoloadm/onlyoffice_views.py
  6. 12
      sapl/templates/materia/onlyoffice_confirmar_editor.html
  7. 29
      sapl/templates/materia/onlyoffice_editor.html
  8. 29
      sapl/templates/onlyoffice/onlyoffice_editor.html
  9. 66
      sapl/utils_template.py

4
sapl/base/onlyoffice_template_views.py

@ -117,12 +117,12 @@ def template_onlyoffice_download(request, pk):
# Se não tem arquivo, cria um documento em branco usando python-docx # Se não tem arquivo, cria um documento em branco usando python-docx
try: try:
from docx import Document
from docx.shared import Inches, Pt from docx.shared import Inches, Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.enum.text import WD_ALIGN_PARAGRAPH
from sapl.utils_template import criar_documento_padrao
from io import BytesIO from io import BytesIO
doc = Document() doc = criar_documento_padrao()
# Adiciona cabeçalho padrão # Adiciona cabeçalho padrão
section = doc.sections[0] section = doc.sections[0]

8
sapl/materia/onlyoffice_materia_views.py

@ -157,10 +157,10 @@ def materia_onlyoffice_download(request, pk):
# Fallback: Se python-docx não está instalado ou houve erro # Fallback: Se python-docx não está instalado ou houve erro
try: try:
from docx import Document from sapl.utils_template import criar_documento_padrao
from io import BytesIO from io import BytesIO
doc = Document() doc = criar_documento_padrao()
doc.add_heading(f'{materia.tipo} {materia.numero}/{materia.ano}', 0) doc.add_heading(f'{materia.tipo} {materia.numero}/{materia.ano}', 0)
doc.add_paragraph(f'Ementa: {materia.ementa}') doc.add_paragraph(f'Ementa: {materia.ementa}')
doc.add_paragraph('') doc.add_paragraph('')
@ -478,10 +478,10 @@ def docacessorio_onlyoffice_download(request, pk):
# Fallback: Se python-docx não está instalado ou houve erro # Fallback: Se python-docx não está instalado ou houve erro
try: try:
from docx import Document from sapl.utils_template import criar_documento_padrao
from io import BytesIO from io import BytesIO
doc = Document() doc = criar_documento_padrao()
doc.add_heading(f'{documento.tipo} - {documento.nome}', 0) doc.add_heading(f'{documento.tipo} - {documento.nome}', 0)
if documento.ementa: if documento.ementa:
doc.add_paragraph(f'Ementa: {documento.ementa}') doc.add_paragraph(f'Ementa: {documento.ementa}')

4
sapl/materia/onlyoffice_views.py

@ -164,10 +164,10 @@ def onlyoffice_download(request, pk):
# Fallback: Se python-docx não está instalado ou houve erro # Fallback: Se python-docx não está instalado ou houve erro
try: try:
from docx import Document from sapl.utils_template import criar_documento_padrao
from io import BytesIO from io import BytesIO
doc = Document() doc = criar_documento_padrao()
doc.add_heading(f'Proposição {proposicao.tipo}', 0) doc.add_heading(f'Proposição {proposicao.tipo}', 0)
doc.add_paragraph(f'Ementa: {proposicao.descricao}') doc.add_paragraph(f'Ementa: {proposicao.descricao}')
doc.add_paragraph('') doc.add_paragraph('')

4
sapl/norma/onlyoffice_views.py

@ -152,10 +152,10 @@ def norma_onlyoffice_download(request, pk):
# Fallback: Se python-docx não está instalado ou houve erro # Fallback: Se python-docx não está instalado ou houve erro
try: try:
from docx import Document from sapl.utils_template import criar_documento_padrao
from io import BytesIO from io import BytesIO
doc = Document() doc = criar_documento_padrao()
doc.add_heading(f'{norma.tipo} {norma.numero}/{norma.ano}', 0) doc.add_heading(f'{norma.tipo} {norma.numero}/{norma.ano}', 0)
doc.add_paragraph(f'Ementa: {norma.ementa}') doc.add_paragraph(f'Ementa: {norma.ementa}')
doc.add_paragraph('') doc.add_paragraph('')

4
sapl/protocoloadm/onlyoffice_views.py

@ -152,10 +152,10 @@ def docadm_onlyoffice_download(request, pk):
# Fallback: Se python-docx não está instalado ou houve erro # Fallback: Se python-docx não está instalado ou houve erro
try: try:
from docx import Document from sapl.utils_template import criar_documento_padrao
from io import BytesIO from io import BytesIO
doc = Document() doc = criar_documento_padrao()
doc.add_heading(f'Documento Administrativo {documento.tipo}', 0) doc.add_heading(f'Documento Administrativo {documento.tipo}', 0)
doc.add_paragraph(f'Número: {documento.numero}/{documento.ano}') doc.add_paragraph(f'Número: {documento.numero}/{documento.ano}')
doc.add_paragraph(f'Assunto: {documento.assunto}') doc.add_paragraph(f'Assunto: {documento.assunto}')

12
sapl/templates/materia/onlyoffice_confirmar_editor.html

@ -48,11 +48,13 @@
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script type="text/javascript" src="{{ onlyoffice_url }}/web-apps/apps/api/documents/api.js"></script> <script type="text/javascript" src="{{ onlyoffice_url }}/web-apps/apps/api/documents/api.js"></script>
<script type="text/javascript"> <script type="text/javascript">
var docEditor = null; var docEditor = null;
var documentSaved = false; var documentSaved = false;
var documentModified = false;
var returnUrl = "{{ voltar_url }}"; var returnUrl = "{{ voltar_url }}";
function salvarEVoltar() { function salvarEVoltar() {
@ -84,7 +86,12 @@
}, },
'onError': function(event) { 'onError': function(event) {
console.error('Erro no OnlyOffice:', event); console.error('Erro no OnlyOffice:', event);
alert('Ocorreu um erro ao carregar o editor. Por favor, tente novamente.'); Swal.fire({
title: 'Erro',
text: 'Ocorreu um erro ao carregar o editor. Por favor, tente novamente.',
icon: 'error',
confirmButtonColor: '#3085d6'
});
}, },
'onWarning': function(event) { 'onWarning': function(event) {
console.warn('Aviso do OnlyOffice:', event); console.warn('Aviso do OnlyOffice:', event);
@ -92,6 +99,7 @@
'onDocumentStateChange': function(event) { 'onDocumentStateChange': function(event) {
// event.data = true quando há alterações não salvas // event.data = true quando há alterações não salvas
console.log('Estado do documento:', event.data ? 'Alterações pendentes' : 'Salvo'); console.log('Estado do documento:', event.data ? 'Alterações pendentes' : 'Salvo');
if (event.data) documentModified = true;
documentSaved = !event.data; documentSaved = !event.data;
}, },
'onRequestClose': function() { 'onRequestClose': function() {
@ -113,7 +121,7 @@
// Aviso antes de sair se houver alterações não salvas // Aviso antes de sair se houver alterações não salvas
window.addEventListener('beforeunload', function(e) { window.addEventListener('beforeunload', function(e) {
if (!documentSaved && docEditor) { if (documentModified && !documentSaved && docEditor) {
e.preventDefault(); e.preventDefault();
e.returnValue = 'Você tem alterações não salvas. Deseja realmente sair?'; e.returnValue = 'Você tem alterações não salvas. Deseja realmente sair?';
return e.returnValue; return e.returnValue;

29
sapl/templates/materia/onlyoffice_editor.html

@ -47,11 +47,13 @@
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script type="text/javascript" src="{{ onlyoffice_url }}/web-apps/apps/api/documents/api.js"></script> <script type="text/javascript" src="{{ onlyoffice_url }}/web-apps/apps/api/documents/api.js"></script>
<script type="text/javascript"> <script type="text/javascript">
var docEditor = null; var docEditor = null;
var documentSaved = false; var documentSaved = false;
var documentModified = false;
var isSaving = false; var isSaving = false;
var documentKey = null; var documentKey = null;
var returnUrl = "{% url 'sapl.materia:proposicao_detail' proposicao.pk %}"; var returnUrl = "{% url 'sapl.materia:proposicao_detail' proposicao.pk %}";
@ -74,8 +76,14 @@
window.location.href = returnUrl; window.location.href = returnUrl;
} else if (tentativas >= maxTentativas) { } else if (tentativas >= maxTentativas) {
clearInterval(intervalo); clearInterval(intervalo);
alert('Tempo esgotado aguardando salvamento. Verifique se o documento foi salvo.'); Swal.fire({
window.location.href = returnUrl; title: 'Tempo esgotado',
text: 'Tempo esgotado aguardando salvamento. Verifique se o documento foi salvo.',
icon: 'warning',
confirmButtonColor: '#3085d6'
}).then(function() {
window.location.href = returnUrl;
});
} }
}) })
.catch(function(err) { .catch(function(err) {
@ -114,7 +122,12 @@
iniciarPolling(); iniciarPolling();
} else if (data.error === 4) { } else if (data.error === 4) {
// Sem alterações para salvar // Sem alterações para salvar
alert('Nenhuma alteração foi detectada no documento.'); Swal.fire({
title: 'Sem alterações',
text: 'Nenhuma alteração foi detectada no documento.',
icon: 'info',
confirmButtonColor: '#3085d6'
});
btn.disabled = false; btn.disabled = false;
btn.innerHTML = '<i class="fa fa-save"></i> Salvar e Voltar'; btn.innerHTML = '<i class="fa fa-save"></i> Salvar e Voltar';
isSaving = false; isSaving = false;
@ -147,7 +160,12 @@
}, },
'onError': function(event) { 'onError': function(event) {
console.error('Erro no OnlyOffice:', event); console.error('Erro no OnlyOffice:', event);
alert('Ocorreu um erro ao carregar o editor. Por favor, tente novamente.'); Swal.fire({
title: 'Erro',
text: 'Ocorreu um erro ao carregar o editor. Por favor, tente novamente.',
icon: 'error',
confirmButtonColor: '#3085d6'
});
}, },
'onWarning': function(event) { 'onWarning': function(event) {
console.warn('Aviso do OnlyOffice:', event); console.warn('Aviso do OnlyOffice:', event);
@ -155,6 +173,7 @@
'onDocumentStateChange': function(event) { 'onDocumentStateChange': function(event) {
// event.data = true quando há alterações não salvas // event.data = true quando há alterações não salvas
console.log('Estado do documento:', event.data ? 'Alterações pendentes' : 'Salvo'); console.log('Estado do documento:', event.data ? 'Alterações pendentes' : 'Salvo');
if (event.data) documentModified = true;
documentSaved = !event.data; documentSaved = !event.data;
} }
}; };
@ -173,7 +192,7 @@
// Aviso antes de sair se houver alterações não salvas // Aviso antes de sair se houver alterações não salvas
window.addEventListener('beforeunload', function(e) { window.addEventListener('beforeunload', function(e) {
if (!documentSaved && docEditor && !isSaving) { if (documentModified && !documentSaved && docEditor && !isSaving) {
e.preventDefault(); e.preventDefault();
e.returnValue = 'Você tem alterações não salvas. Deseja realmente sair?'; e.returnValue = 'Você tem alterações não salvas. Deseja realmente sair?';
return e.returnValue; return e.returnValue;

29
sapl/templates/onlyoffice/onlyoffice_editor.html

@ -57,11 +57,13 @@
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script type="text/javascript" src="{{ onlyoffice_url }}/web-apps/apps/api/documents/api.js"></script> <script type="text/javascript" src="{{ onlyoffice_url }}/web-apps/apps/api/documents/api.js"></script>
<script type="text/javascript"> <script type="text/javascript">
var docEditor = null; var docEditor = null;
var documentSaved = false; var documentSaved = false;
var documentModified = false;
var isSaving = false; var isSaving = false;
var documentKey = null; var documentKey = null;
var returnUrl = "{{ voltar_url }}"; var returnUrl = "{{ voltar_url }}";
@ -85,8 +87,14 @@
window.location.href = returnUrl; window.location.href = returnUrl;
} else if (tentativas >= maxTentativas) { } else if (tentativas >= maxTentativas) {
clearInterval(intervalo); clearInterval(intervalo);
alert('Tempo esgotado aguardando salvamento. Verifique se o documento foi salvo.'); Swal.fire({
window.location.href = returnUrl; title: 'Tempo esgotado',
text: 'Tempo esgotado aguardando salvamento. Verifique se o documento foi salvo.',
icon: 'warning',
confirmButtonColor: '#3085d6'
}).then(function() {
window.location.href = returnUrl;
});
} }
}) })
.catch(function(err) { .catch(function(err) {
@ -121,7 +129,12 @@
if (data.error === 0) { if (data.error === 0) {
iniciarPolling(); iniciarPolling();
} else if (data.error === 4) { } else if (data.error === 4) {
alert('Nenhuma alteração foi detectada no documento.'); Swal.fire({
title: 'Sem alterações',
text: 'Nenhuma alteração foi detectada no documento.',
icon: 'info',
confirmButtonColor: '#3085d6'
});
btn.disabled = false; btn.disabled = false;
btn.innerHTML = '<i class="fa fa-save"></i> Salvar e Voltar'; btn.innerHTML = '<i class="fa fa-save"></i> Salvar e Voltar';
isSaving = false; isSaving = false;
@ -154,13 +167,19 @@
}, },
'onError': function(event) { 'onError': function(event) {
console.error('Erro no OnlyOffice:', event); console.error('Erro no OnlyOffice:', event);
alert('Ocorreu um erro ao carregar o editor. Por favor, tente novamente.'); Swal.fire({
title: 'Erro',
text: 'Ocorreu um erro ao carregar o editor. Por favor, tente novamente.',
icon: 'error',
confirmButtonColor: '#3085d6'
});
}, },
'onWarning': function(event) { 'onWarning': function(event) {
console.warn('Aviso do OnlyOffice:', event); console.warn('Aviso do OnlyOffice:', event);
}, },
'onDocumentStateChange': function(event) { 'onDocumentStateChange': function(event) {
console.log('Estado do documento:', event.data ? 'Alterações pendentes' : 'Salvo'); console.log('Estado do documento:', event.data ? 'Alterações pendentes' : 'Salvo');
if (event.data) documentModified = true;
documentSaved = !event.data; documentSaved = !event.data;
} }
}; };
@ -179,7 +198,7 @@
// Aviso antes de sair se houver alterações não salvas // Aviso antes de sair se houver alterações não salvas
window.addEventListener('beforeunload', function(e) { window.addEventListener('beforeunload', function(e) {
if (!documentSaved && docEditor && !isSaving) { if (documentModified && !documentSaved && docEditor && !isSaving) {
e.preventDefault(); e.preventDefault();
e.returnValue = 'Você tem alterações não salvas. Deseja realmente sair?'; e.returnValue = 'Você tem alterações não salvas. Deseja realmente sair?';
return e.returnValue; return e.returnValue;

66
sapl/utils_template.py

@ -11,6 +11,49 @@ from django.core.files.base import ContentFile
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _forcar_fonte_estilo(style):
"""Força Arial em todos os slots de fonte de um estilo,
sobrescrevendo qualquer fonte de tema."""
from docx.oxml.ns import qn
style.font.name = 'Arial'
rPr = style.element.get_or_add_rPr()
rFonts = rPr.get_or_add_rFonts()
rFonts.set(qn('w:ascii'), 'Arial')
rFonts.set(qn('w:hAnsi'), 'Arial')
rFonts.set(qn('w:eastAsia'), 'Arial')
rFonts.set(qn('w:cs'), 'Arial')
def criar_documento_padrao():
"""
Cria um Document python-docx com fonte Arial como padrão.
Configura o estilo 'Normal', 'Title' e os estilos de heading para usar Arial,
forçando em todos os slots de fonte (ascii, hAnsi, eastAsia, cs).
"""
from docx import Document
from docx.shared import Pt
doc = Document()
# Define Arial como fonte padrão no estilo Normal
style = doc.styles['Normal']
style.font.size = Pt(12)
_forcar_fonte_estilo(style)
# Define Arial no estilo Title (usado por add_heading(text, 0))
if 'Title' in doc.styles:
_forcar_fonte_estilo(doc.styles['Title'])
# Define Arial nos estilos de heading
for i in range(1, 10):
heading_style_name = f'Heading {i}'
if heading_style_name in doc.styles:
_forcar_fonte_estilo(doc.styles[heading_style_name])
return doc
def criar_documento_com_template(tipo_conteudo, tipo_especifico, dados): def criar_documento_com_template(tipo_conteudo, tipo_especifico, dados):
""" """
Cria um novo documento usando um template existente. Cria um novo documento usando um template existente.
@ -126,10 +169,9 @@ def criar_documento_em_branco(titulo, descricao, tipo_documento='Documento'):
BytesIO com o documento ou None em caso de erro BytesIO com o documento ou None em caso de erro
""" """
try: try:
from docx import Document
from io import BytesIO from io import BytesIO
doc = Document() doc = criar_documento_padrao()
doc.add_heading(titulo, 0) doc.add_heading(titulo, 0)
if descricao: if descricao:
@ -257,6 +299,20 @@ def adicionar_cabecalho_materia(materia):
from docx import Document from docx import Document
from docx.shared import Pt, Inches from docx.shared import Pt, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
def _forcar_fonte_arial(run):
"""Força Arial em todos os slots de fonte do run,
sobrescrevendo qualquer fonte de tema do documento."""
run.bold = True
run.font.name = 'Arial'
run.font.size = Pt(14)
rPr = run._element.get_or_add_rPr()
rFonts = rPr.get_or_add_rFonts()
rFonts.set(qn('w:ascii'), 'Arial')
rFonts.set(qn('w:hAnsi'), 'Arial')
rFonts.set(qn('w:eastAsia'), 'Arial')
rFonts.set(qn('w:cs'), 'Arial')
# Carrega o documento # Carrega o documento
doc = Document(arquivo_path) doc = Document(arquivo_path)
@ -276,8 +332,7 @@ def adicionar_cabecalho_materia(materia):
# Formata o cabeçalho # Formata o cabeçalho
novo_paragrafo.alignment = WD_ALIGN_PARAGRAPH.CENTER novo_paragrafo.alignment = WD_ALIGN_PARAGRAPH.CENTER
for run in novo_paragrafo.runs: for run in novo_paragrafo.runs:
run.bold = True _forcar_fonte_arial(run)
run.font.size = Pt(14)
# Adiciona linha em branco após o cabeçalho # Adiciona linha em branco após o cabeçalho
primeiro_paragrafo.insert_paragraph_before('') primeiro_paragrafo.insert_paragraph_before('')
@ -286,8 +341,7 @@ def adicionar_cabecalho_materia(materia):
paragrafo = doc.add_paragraph(cabecalho_texto) paragrafo = doc.add_paragraph(cabecalho_texto)
paragrafo.alignment = WD_ALIGN_PARAGRAPH.CENTER paragrafo.alignment = WD_ALIGN_PARAGRAPH.CENTER
for run in paragrafo.runs: for run in paragrafo.runs:
run.bold = True _forcar_fonte_arial(run)
run.font.size = Pt(14)
doc.add_paragraph('') doc.add_paragraph('')
# Salva o documento modificado # Salva o documento modificado

Loading…
Cancel
Save