quinta-feira, 10 de agosto de 2023

Trocando o target de links no cabeçalho de aplicação gerada por WebApp Builder

Em um app gerado no WebApp Builder, é possível configurar um ícone de links ao lado da barra de pesquisa. Esse botão faz parte do cabeçalho e não é tratado como um widget. A configuração padrão dele é vir com o 'target=_blank' e isso não pode ser alterado na configuração com o ambiente WYSIWYG.

Para alterar diretamente no código, deve-se levar em conta que o html desse trecho é gerado dinamicamente, o que significa que alterar o arquivo Widget.html em 'themes\LaunchpadTheme -> Widgets -> Header' é um esforço em vão. Ao invés disso, altere o arquivo Widget.js no mesmo caminho:


Essa alteração em _createDynamicLinks deve afetar todos os links, mas até onde testei, afetou apenas o header (os links em popups que configurei não foram afetados).

terça-feira, 14 de abril de 2020

Copiando databases de MySQL com Heidi

A interface do HeidiSQL para copiar databases pode ser um pouco não-intuitiva. Para copiar todas as tabelas de um database para outro é preciso criar os novos databases antes e então ir na opção Tools   -> Export Database as SQL. Daí é necessário habilitar o checkbox do database original na aba esquerda e configurar a saída na aba direita da seguinte maneira:


Ao terminar de executar a janela não fecha sozinha e é necessário clicar em Close.

terça-feira, 3 de março de 2020

Tratamento de html em script Python para inserção em Sql Server

# -*- coding: utf-8 -*-
# This does not solve the problem when processed text loaded from external file contains utf-8 encodings. This helps only for literals written in the given python script itself and is just a clue for python interpreter, but has no impact on text processing.

import sys, os.path, time, re, copy, datetime as dt, locale, codecs, random, uuid

# inclui o diretorio do projeto ao ambiente
sys.path.append(os.path.join(os.path.dirname(__file__),'..'))
from conexao.local_conexao import py_odbc_conn

char_code = 'latin1'

print sys.stdout.encoding
sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout)

sql_insert = u"""
INSERT INTO [sigamgeo].[TB_FIS_VISTORIAS_AIA_TCRA]
           ([OBJECTID]
           ,[NIS]
           ,[CaracAreaRecCL]
           ,[CaracAreaRecT]
           ,[Isolamento]
           ,[PerturbacoesCL]
           ,[PerturbacoesT]
           ,[Mortalidade]
           ,[AtaqueFormigas]
           ,[MatoCompeticao]
           ,[Riqueza]
           ,[AltMedMudas]
           ,[SubBosque]
           ,[PresRegen]
           ,[RegenNatural]
           ,[TecVistResp]
           ,[Cargo]
           ,[DataElabRTV]
           ,[DataVistoria]
           ,[Acompanhante1]
           ,[Acompanhante2]
           ,[Conclusao]
           ,[StatusVistoria]
           ,[Metodologia]
           ,[Relatorio]
           ,[RecomTec]
           ,[PSMA]
           ,[HistMax]
           ,[ObsAdicMax]
           ,[MudasPlantadas]
           ,[AreaDano_ha]
           ,[GlobalID])
     VALUES
           ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11},
           {12}, {13}, {14}, {15}, {16}, {17}, {18}, {19}, {20}, {21}, {22},
           {23}, {24}, {25}, {26}, {27}, {28}, {29}, {30}, {31});
"""

sql_select = u"""
SELECT [OBJECTID] --0
           ,[NIS] --1
           ,[CaracAreaRecCL] --2
           ,[CaracAreaRecT] --3
           ,[Isolamento] --4
           ,[PerturbacoesCL] --5
           ,[PerturbacoesT] --6
           ,[Mortalidade] --7
           ,[AtaqueFormigas] --8
           ,[MatoCompeticao] --9
           ,[Riqueza] --10
           ,[AltMedMudas] --11
           ,[SubBosque] --12
           ,[PresRegen] --13
           ,[RegenNatural] --14
           ,[TecVistResp] --15
           ,[Cargo] --16
           ,[DataElabRTV] --17
           ,[DataVistoria] --18
           ,[Acompanhante1] --19
           ,[Acompanhante2] --20
           ,[Conclusao] --21
           ,[StatusVistoria] --22
           ,[Metodologia] --23
           ,REPLACE(CONVERT(NVARCHAR(MAX), [Relatorio]), '''', '''''') --24 dobra aspas simples p/ sql server reconhecer como texto
           ,CONVERT(NVARCHAR(MAX), [RecomTec]) --25
           ,[PSMA] --26
           ,CONVERT(NVARCHAR(MAX), [HistMax]) --27
           ,CONVERT(NVARCHAR(MAX), [ObsAdicMax]) --28
           ,[MudasPlantadas] --29
           ,[AreaDano_ha] --30
           ,'GlobalID' AS GlobalId --31
           FROM TB_VISTORIAS_AIA_TCRA
           WHERE DataVerifSigam IS NULL;
"""

print "{} - Iniciando conexao ao spatialdata".format(time.strftime("%x %X"))
odbc_spatialdata = py_odbc_conn('sql01', 'spatialdata', 'geodataowner', '@@X.y.z.1')

registros = odbc_spatialdata.cursor().execute(sql_select).fetchall() #tuple com todos os registros

print "{} - Iniciando loop de insert".format(time.strftime("%x %X"))
if registros <> None:
    for registro in registros:
        try:
            reg = list(registro) #tuple para list
            registro_encoded = [] #lista que ira receber o registro tratado
            for v in reg: #loop dentro da lista do registro
                if v == reg[24] and v <> None: #trata relatorio para insert
                    registro_encoded.append("'" + v + "'")
                elif v == reg[18]: #trata DataVistoria
                    data = str(v)
                    registro_encoded.append("'" + data + "'")
                elif v == reg[31]: #gera globalId para insert
                    gid = '{' + str(uuid.uuid4()) + '}'
                    registro_encoded.append("'" + gid + "'")
                elif (isinstance(v, str) or isinstance(v, unicode)):
                    p = re.compile("('|\'|\?)")
                    v = p.sub(" ",v)
                    registro_encoded.append(u"'" + v.decode(char_code, 'replace') + u"'")
                elif(isinstance(v, int) or isinstance(v, float)):
                    registro_encoded.append(v)
                elif(not isinstance(v, int) and not isinstance(v, float) and not isinstance(v, object)
                     or unicode(v) =='None' ):
                    registro_encoded.append('NULL')
                else:
                    registro_encoded.append(unicode(v))

            cursor = odbc_spatialdata.cursor().execute(sql_insert.format(*registro_encoded))
            cursor.commit()
        except Exception as e:
            print e

print "{} - Encerrando conexao ao spatialdata".format(time.strftime("%x %X"))
odbc_spatialdata.close()

dataHoje = dt.datetime.now()
dataString = str(dataHoje)
dataVerif = dataString[:-7]

print dataVerif

print 'Fim'

quarta-feira, 28 de agosto de 2019

Widget de Upload de Fotos

Este é um widget para uso com aplicativos do Web AppBuilder, da ESRI. Ele usa ArcGIS API for Javascript e permite realizar o upload de fotos por um serviço REST em um servidor ArcGIS.

O widget usa a estrutura padrão do WAB, com uma pasta com seu nome constando dentro da pasta widgets do app e com subpastas e arquivos na seguinte estrutura:


O widget usa a biblioteca EXIF.js que foi colocada na mesma pasta do widget, como pode ser verificado na imagem acima.

Abaixo segue o código de Widget.js, Widget.html e style.css:

// AppGeo - 2019

define([
'dojo/_base/declare',
'dojo/_base/lang',
'dojo/aspect',
'dojo/Deferred',
"dojo/dom",
"dojo/json",
"dojo/on",
"dojo/parser",
"dojo/sniff",
"dojo/_base/array",
"dijit/form/Button",
'dijit/_WidgetsInTemplateMixin',
'jimu/BaseWidget',
'jimu/portalUtils',
'jimu/dijit/Message',
'jimu/loaderplugins/jquery-loader!https://code.jquery.com/jquery-git1.min.js',
"esri/config",
"esri/request",
"esri/layers/FeatureLayer",
'esri/tasks/JobInfo',
'esri/tasks/query',
'esri/tasks/QueryTask',
'widgets/AddPhotoGeo/exif'
],
function (
declare,
lang,
aspect,
Deferred,
dom,
JSON,
on,
parser,
sniff,
arrayUtils,
Button,
_WidgetsInTemplateMixin,
BaseWidget,
PortalUtils,
Message,
$,
esriConfig,
request,
FeatureLayer,
JobInfo,
Query,
QueryTask,
exif
) {

// url raiz consta no config, foi declarada como global devido a limitacoes para buscar o config dentro do EXIF
// url para adicionar o ponto
var urlAF = "https://mapas.teste.com/server/rest/services/APPGEO/FOTOS_GEORREFERENCIADAS/FeatureServer/0/addFeatures";

// url basica usada na adicao de anexo
var url = "https://mapas.teste.com/server/rest/services/APPGEO/FOTOS_GEORREFERENCIADAS/FeatureServer/0/";

// object id da feicao que recebera anexo
var oid = null;

// variaveis dos atributos de texto
var idProcesso = null;
var idProcDet = null;
var obs = null;
var orientacao = null;
var proprietario = null;
var idEmpreendimento = null;
var clazz = declare([BaseWidget, _WidgetsInTemplateMixin], {

// funcao de carregamento do widget
startup: function () {

// evento de mudanca nos elementos onde o usuario insere o texto, dispara processamento
on(dom.byId("textForm"), "change", lang.hitch(this, function (event) {

// armazena campos de texto

// Se idProcesso/idEmpreendimento/idProcDet nao forem numeros, armazena null ao inves de ""
if (document.getElementById("idP").value != ""){
var idP = parseInt(document.getElementById("idP").value);
if (isNaN(idP)) {
idProcesso = null;
} else {
idProcesso = idP;
}
} else {
idProcesso = null;
}

if (document.getElementById("idE").value != "") {
var idE = parseInt(document.getElementById("idE").value);
if (isNaN(idE)) {
idEmpreendimento = null;
} else {
idEmpreendimento = idE;
}
} else {
idEmpreendimento = null;
}
if (document.getElementById("idPD").value != ""){
var idPD = parseInt(document.getElementById("idPD").value);
if (isNaN(idPD)) {
idProcDet = null;
} else {
idProcDet = idPD;
}
} else {
idProcDet = null;
}
// Se obs/orientacao/proprietario estiverem vazios, armazena null ao inves de ""
if (document.getElementById("obs").value != "") {
obs = String(document.getElementById("obs").value);
} else {
obs = null;
}
if (document.getElementById("orient").value != ""){
orientacao = String(document.getElementById("orient").value);
} else {
orientacao = null;
}
if (document.getElementById("prop").value != ""){
proprietario = String(document.getElementById("prop").value);
} else {
proprietario = null;
}

dom.byId('upload-status').innerHTML = '<p style="color:orange">Ao terminar de preencher os campos, clique em "Adicionar" para inserir a foto e enviar os dados.</p>';
}));


// evento de mudanca no elemento onde o usuario insere o arquivo, dispara processamento
on(dom.byId("uploadForm"), "change", lang.hitch(this, function (event) {

// armazena caminho do arquivo (modificado pelo navegador)
var fileName = event.target.value.toLowerCase();

//separa nome do arquivo
var name = fileName.split(".");
name = name[0].replace("c:\\fakepath\\", "");

//armazena arquivo capturado pelo evento em uma variavel
var f = event.target.files[0];

if (sniff("ie")) { //filename is full path in IE so extract the file name
var arr = fileName.split("\\");
fileName = arr[arr.length - 1];
}
if (fileName.indexOf(".jpg") !== -1) {//is file a jpg - if not notify user
this.getPhotoMetadata(f);//chama funcao que le atributos da foto e envia para o servidor
}
else {
dom.byId('upload-status').innerHTML = '<p style="color:red">Adicionar foto no formato (.jpg)</p>';
}
}));
},


// funcao que le atributos da foto, cria ponto e anexa foto
getPhotoMetadata: lang.hitch(this, function (img) {
// se arquivo nao for de imagem, sai da funcao e avisa o usuario
if (img == null) {
dom.byId('upload-status').innerHTML = '<p style="color:red">Adicionar um arquivo de foto</p>';
return;
}
// usa biblioteca para ler atributos
EXIF.getData(img, function () {

var metadata = EXIF.getAllTags(this);

// armazena data da foto
var fulldate = metadata.DateTimeOriginal || metadata.DateTimeDigitized || metadata.DateTime || null;
// formata data
if (fulldate != null) {
var date = fulldate.split(' ')[0];
var parts = date.split(':');
date = parts[0] + '-' + parts[1] + '-' + parts[2]

fulldate = date + ' ' + fulldate.split(' ')[1];
}

//validacao do geotag - se nao houver geotag, sai da funcao e avisa o usuario
var x_verif = metadata.GPSLongitude || "Sem Informacao de Longitude";
var y_verif = metadata.GPSLatitude || "Sem Informacao de Latitude";

if (x_verif == "Sem Informacao de Longitude" || y_verif == "Sem Informacao de Latitude") {
dom.byId('upload-status').innerHTML = '<p style="color:red">Adicionar foto com geotag preenchido</p>';
return;
}

// funcao para converter o objeto com coordenadas em gms para decimais
var toDecimal = function (number) {
return number[0].numerator + number[1].numerator /
(60 * number[1].denominator) + number[2].numerator / (3600 * number[2].denominator);
};

// coordenadas x, y (convertidas) e z
var x_decimal = toDecimal(metadata.GPSLongitude);
var y_decimal = toDecimal(metadata.GPSLatitude);
if (metadata.GPSLongitudeRef == "W") {
x_decimal = x_decimal * -1
}
if (metadata.GPSLatitudeRef == "S") {
y_decimal = y_decimal * -1
}
var z = metadata.GPSAltitude || null;

// Add feature e realizado dentro do EXIF devido a limitacoes para retornar outro valor
//alem de true/false ou mesmo utilizar uma variavel global fora desta funcao (o valor global
//continua o mesmo se for acionado no trecho de uploadForm change antes/apos a ativacao desta procedure)

// geometria e campos da feicao que sera adicionada
var feature = {
"geometry": { "x": x_decimal, "y": y_decimal },
"attributes": {
"idProcesso": idProcesso,
"idProcDet": idProcDet,
"obs": obs,
"dataCarga": Date.now(), //GMT 0
"latitude": y_decimal,
"longitude": x_decimal,
"altitude": z.valueOf(),
"dataFoto": fulldate,
"orientacao": orientacao,
"proprietario": proprietario,
"importadoDe": "Appgeo - widget de fotos",
"linkFoto": null,
"dataAtualizaSARE": null,
"dataAtualizaDen": null,
"dataAtualizaAP": null,
"dataAtualizaN": null,
"idEmpreendimento": idEmpreendimento
}
};

// post que envia feicao
$.post(urlAF, {
features: JSON.stringify([feature]),
f: "json"
})
.done(lang.hitch(this, function (results) {//post bem-sucedido
console.log(results);

// timeout para aguardar results e verificar se ha resultado de erro que entra em done ao inves de fail
setTimeout(function () {
var cod = results.error.code;
console.log(cod);
if (cod == 499) {
dom.byId('upload-status').innerHTML = '<p style="color:red">Voce nao tem permissao para utilizar este widget.</p>'
} else {
dom.byId('upload-status').innerHTML = '<p style="color:green">Ponto criado. Adicionando anexo. Aguarde a resposta do servidor.</p>'
}
}, 100);

//dom.byId('upload-status').innerHTML = '<p style="color:green">Ponto criado. Adicionando anexo. Aguarde a resposta do servidor.</p>';

// timeout para aguardar results preenchidos e entao anexar arquivo
setTimeout(function () {
// converte json de resposta em objeto js
var resultAF = JSON.parse(results);

// converte em integer o objectId
oid = parseInt(resultAF.addResults[0].objectId);

// Trecho que adiciona anexo se insercao do ponto foi bem-sucedida
var featureLayer = new esri.layers.FeatureLayer(url, { outFields: ["*"], visible: false });
featureLayer.addAttachment(oid, document.getElementById("uploadForm"), callback, function (err) {
console.log(err);//addAttachment falhou
dom.byId('upload-status').innerHTML = '<p style="color:red">Erro ao anexar foto. Informe a equipe do AppGeo.</p>';
});
function callback(result) {//addAttachment bem-sucedido
console.log(result);
dom.byId('upload-status').innerHTML = '<p style="color:green">Tarefa concluida: ponto criado e foto anexada.</p>';
};
}, 100);
}))
.fail(function (error) {//post falhou
console.log(error);
dom.byId('upload-status').innerHTML = '<p style="color:red">Erro na criacao do ponto. Tente novamente ou informe equipe do AppGeo.</p>';
});
});
}),

destroy: function () {
this.inherited(arguments);
}
});
return clazz;
});


<div>
<p>
<b>Preencha os dados relevantes da foto</b>
</p><p>
</p><br /><br />
<form enctype="multipart/form-data" method="post" id="textForm">
<div class="tooltip">
NIS do processo: <input type="text" name="idProcesso" id="idP" maxlength="10"/><br /><br />
<span class="tooltiptext">ID do processo, se houver</span>
</div>
<div class="tooltip">
NIS do empreendimento: <input type="text" name="idEmpreendimento" id="idE" maxlength="10" /><br /><br />
<span class="tooltiptext">ID do empreendimento, se houver</span>
</div>
<div class="tooltip">
idProcDet: <input type="text" name="idProcDet" id="idPD" maxlength="10"/><br /><br />
<span class="tooltiptext">Id da Atividade, se houver</span>
</div>
<div class="tooltip">
obs: <input type="text" name="obs" id="obs" maxlength="300"/><br /><br />
<span class="tooltiptext">Observacoes</span>
</div>
<div class="tooltip">
visada: <input type="text" name="orientacao" id="orient" maxlength="20"/><br /><br />
<span class="tooltiptext">Direcao de visada da foto</span>
</div>
<div class="tooltip">
proprietario: <input type="text" name="proprietario" id="prop" maxlength="150"/><br /><br />
<span class="tooltiptext">Nome do proprietario da foto</span>
</div>
</form><br /><br />
<p>
<b>Adicione uma foto com geotag e formato jpg</b>
</p><p>
</p>
<form enctype="multipart/form-data" method="post" id="uploadForm">
<div class="field">
<label class="file-upload">
<span><strong>Adicionar Arquivo</strong></span>
<input type="file" name="attachment" id="inFile" />
</label>
</div>
</form>
<span class="file-upload-status" style="opacity:1;" id="upload-status"></span>
<div id="fileInfo"> </div>
</div>

/*Photo upload form styling */
.field {
padding:4px 4px;
}
.file-upload {
overflow: hidden;
display: inline-block;
position: relative
vertical-align: middle;
text-align: center;
border: 2px solid #666666;
background: #444444;
-moz-border-radius: 6px;
-webkit-border-radius: 6px;
border-radius: 6px;
color: #fff;
text-shadow: 0px 2px 3px #555;
cursor:pointer;
}
  
.file-upload:hover {
background-color: #454545;
}

.file-upload input {
position: absolute;
top: 0;
left: 0;
margin: 0;
font-size: 70px;
opacity: 0;
filter: alpha(opacity=0);
z-index:2;
}
.file-upload strong {
font: normal 1.75em arial,sans-serif;
  
.file-upload span {
position: absolute;
top: 0;
left: 0;
display: inline-block;

padding-top: .45em;
}

.file-upload { height: 3em; }
.file-upload,
.file-upload spanwidth: 10em; }  

/*Text form styling*/

/* Tooltip container */
.tooltip {
position: relative;
display: inline-block;
}

/* Tooltip text */
.tooltip .tooltiptext {
visibility: hidden;
width: 120px;
background-color: #454545;
color: #fff;
text-align: center;
padding: 5px 0;
border-radius: 6px;

/* Position the tooltip text */
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -60px;

/* Fade in tooltip */
opacity: 0;
transition: opacity 0.3s;
}

/* Show the tooltip text when you mouse over the tooltip container */
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}

.tooltip input {
width: 150px;
}

segunda-feira, 27 de maio de 2019

FlaskBB - Python Anywhere

O FlaskBB é um fórum construído com Flask (um framework em Python). O tutorial simplificado cria um fórum rodando em localhost:5000 facilmente, principalmente porque se você falhar em criar um arquivo de configuração flaskbb.cfg, o comando run irá criar o servidor com as configurações padrão ao invés de falhar.

A coisa complica quando você tenta subir o fórum no PythonAnywhere (um site de hospedagem, assim como Heroku e outros).

A documentação de instalação mais extensa do FlaskBB inclui um mini tutorial enviado por usuários para instalar o FlaskBB no pythonanywhere:

Here’s what to do:

  • Sign up for a PythonAnywhere account at https://www.pythonanywhere.com/.
  • On the “Consoles” tab, start a Bash console and install/configure FlaskBB like this

git clone https://github.com/sh4nks/flaskbb.git
cd flaskbb
Before continuing the installation it is advised to create a virtualenv as is described in section Virtualenv Setup.
Finish the installation of FlaskBB by executing following commands:

pip3.5 install --user -r requirements.txt
pip3.5 install --user -e .
flaskbb makeconfig
flaskbb install

  • Click the PythonAnywhere logo to go back to the dashboard, then go to the “Web” tab, and click the “Add a new web app” button.
  • Just click “Next” on the first page.
  • On the next page, click “Flask”
  • On the next page, click “Python 3.5”
  • On the next page, just accept the default and click next
  • Wait while the website is created.
  • Click on the “Source code” link, and in the input that appears, replace the mysite at the end with flaskbb
  • Click on the “WSGI configuration file” filename, and wait for an editor to load.
  • Change the line that sets project_home to replace mysite with flaskbb again.
  • Change the line that says

from flask_app import app as application
to say

from flaskbb import create_app
application = create_app("/path/to/your/configuration/file")

  • Click the green “Save” button near the top right.
  • Go back to the “Web” tab.
  • Click the green “Reload…” button.
  • Click the link to visit the site – you’ll have a new FlaskBB install!

 Recomendo alguns cuidados para aqueles que são iniciantes como eu, pois sofri bastante para conseguir rodar:

  • Pode ser melhor prática, bonito, etc... mas NÃO utilizem um ambiente virtual, se você é iniciante como eu poderá ter problemas ao tentar rodar o servidor. No meu caso, em uma das vezes, as instalações não eram encontradas na subida do servidor, tive que instalar tudo novamente fora do ambiente virtual e agora tenho espaço desperdiçado no limitadíssimo (500MB) diretório fornecido (até descobrir se posso apagar a pasta .venv sem problemas);
  • Preste atenção no resultado do makeconfig, é possível que o arquivo flaskbb.cfg não tenha sido criado (ele deveria estar visível na pasta raiz). Isso pode influenciar no flaskbb install (usando flaskbb --config flaskbb.cfg install), em uma das vezes a instalação não encontrou o arquivo, o que resultou em um flaskbb.sqllite vazio, e consequentemente em falha ao subir o servidor;
  • Preste atenção também no arquivo de configuração WSGI, tenha certeza de que ele aponta para as pastas e arquivos corretos na estrutura do seu aplicativo.

Abaixo deixo exemplos de como deve estar um arquivo de configuração WSGI e FlaskBB (pode ser necessário incluir o flaskbb.cfg manualmente):

  • WSGI

import sys

# add your project directory to the sys.path
project_home = u'/home/yourname/flaskbb'
if project_home not in sys.path:
    sys.path = [project_home] + sys.path

# import flask app but need to call it "application" for WSGI to work
from flaskbb import create_app
application = create_app("/home/yourname/flaskbb/flaskbb.cfg")  # noqa 


  • FlaskBB


# Feel free to adjust it as needed.

import os

import datetime

from flaskbb.configs.default import DefaultConfig





# Flask Settings

# ------------------------------

# There is a whole bunch of more settings available here:

# http://flask.pocoo.org/docs/0.11/config/#builtin-configuration-values

DEBUG = False

TESTING = False



# Server Name - REQUIRED for Celery/Mailing

# The name and port number of the server.

# Required for subdomain support (e.g.: 'myapp.dev:5000') and

# URL generation without a request context but with an application context

# which we need in order to generate URLs (with the celery application)

# Note that localhost does not support subdomains so setting this to

# “localhost” does not help.

# Example for the FlaskBB forums: SERVER_NAME = "forums.flaskbb.org"

SERVER_NAME = "yourname.pythonanywhere.com"



# Prefer HTTPS over HTTP

PREFERRED_URL_SCHEME = "https"



# Don't send secure cookies over an unencrypted connection ()

SESSION_COOKIE_SECURE = True

# Don't make cookies available to JS (XSS) - browsers hide httpOnly cookies from JS

SESSION_COOKIE_HTTPONLY = True





# Database

# ------------------------------

# For PostgresSQL:

#SQLALCHEMY_DATABASE_URI = "postgresql://flaskbb@localhost:5432/flaskbb"

# For SQLite:

SQLALCHEMY_DATABASE_URI = "sqlite:////home/yourname/flaskbb/flaskbb.sqlite"



# This option will be removed as soon as Flask-SQLAlchemy removes it.

# At the moment it is just used to suppress the super annoying warning

SQLALCHEMY_TRACK_MODIFICATIONS = False

# This will print all SQL statements

SQLALCHEMY_ECHO = False





# Security - IMPORTANT

# ------------------------------

# This is the secret key that is used for session signing.

# You can generate a secure key with os.urandom(24)

SECRET_KEY = "4f173d40bb47af9463b9d03fdd6683b69ab5f223bf3cc958"



# You can generate the WTF_CSRF_SECRET_KEY the same way as you have

# generated the SECRET_KEY. If no WTF_CSRF_SECRET_KEY is provided, it will

# use the SECRET_KEY.

WTF_CSRF_ENABLED = True

WTF_CSRF_SECRET_KEY = "75e0569325b2e4545a7e6d45b282767303a77e6416fb963a"





# Auth

# ------------------------------

LOGIN_VIEW = "auth.login"

REAUTH_VIEW = "auth.reauth"

LOGIN_MESSAGE_CATEGORY = "info"

REFRESH_MESSAGE_CATEGORY = "info"



# The name of the cookie to store the “remember me” information in.

REMEMBER_COOKIE_NAME = "remember_token"

# The amount of time before the cookie expires, as a datetime.timedelta object.

# Default: 365 days (1 non-leap Gregorian year)

REMEMBER_COOKIE_DURATION = datetime.timedelta(days=365)

# If the “Remember Me” cookie should cross domains,

# set the domain value here (i.e. .example.com would allow the cookie

# to be used on all subdomains of example.com).

REMEMBER_COOKIE_DOMAIN = None

# Limits the “Remember Me” cookie to a certain path.

REMEMBER_COOKIE_PATH = "/"

# Restricts the “Remember Me” cookie’s scope to secure channels (typically HTTPS).

REMEMBER_COOKIE_SECURE = True

# Prevents the “Remember Me” cookie from being accessed by client-side scripts.

REMEMBER_COOKIE_HTTPONLY = True





# Full-Text-Search

# ------------------------------

# This will use the "whoosh_index" directory to store the search indexes

WHOOSHEE_DIR = os.path.join(DefaultConfig.basedir, "whoosh_index", DefaultConfig.py_version)

# How long should whooshee try to acquire write lock? (defaults to 2)

WHOOSHEE_WRITER_TIMEOUT = 2

# Minimum number of characters for the search (defaults to 3)

WHOOSHEE_MIN_STRING_LEN = 3





# Redis

# ------------------------------

# If redis is enabled, it can be used for:

#   - Sending non blocking emails via Celery (Task Queue)

#   - Caching

#   - Rate Limiting

REDIS_ENABLED = False

REDIS_URL = "redis://localhost:6379"

REDIS_DATABASE = 0





# Celery

# ------------------------------

CELERY_BROKER_URL = "redis://localhost:6379"

CELERY_RESULT_BACKEND = "redis://localhost:6379"





# Rate Limiting

# -------------------------------

# A full list with configuration values is available at the flask-limiter

# docs, but you actually just need those settings below.

# You can disabled the Rate Limiter here as well - it will overwrite

# the setting from the admin panel!

# RATELIMIT_ENABLED = True

# You can choose from:

#   memory:// (default)

#   redis://host:port

#   memcached://host:port

# Using the redis storage requires the installation of the redis package,

# which will be installed if you enable REDIS_ENABLE while memcached

# relies on the pymemcache package.

RATELIMIT_STORAGE_URL = "memory://"





# Caching

# ------------------------------

# For all available caching types, have a look at the Flask-Cache docs

# https://pythonhosted.org/Flask-Caching/#configuring-flask-caching

CACHE_TYPE = "simple"

CACHE_DEFAULT_TIMEOUT = 60





# Mail

# ------------------------------

# Google Mail Example

# https://support.google.com/mail/answer/7126229?hl=en

#MAIL_SERVER = "smtp.gmail.com"

#MAIL_PORT = 587

#MAIL_USE_TLS = True

#MAIL_USE_SSL = True

#MAIL_USERNAME = "your_username@gmail.com"

#MAIL_PASSWORD = "your_password"

#MAIL_DEFAULT_SENDER = ("Your Name", "your_username@gmail.com")



# Local SMTP Server

MAIL_SERVER = "smtp.gmail.com"

MAIL_PORT = 465

MAIL_USE_SSL = True

MAIL_USE_TLS = True

MAIL_USERNAME = "yourname@gmail.com"

MAIL_PASSWORD = "xyz#321"

MAIL_DEFAULT_SENDER = ("Your Name Mailer", "yourname@gmail.com")

# Where to logger should send the emails to

ADMINS = ["yourname@gmail.com"]





# Logging Settings

# ------------------------------

# This config section will deal with the logging settings

# for FlaskBB, adjust as needed.



# Logging Config Path

# see https://docs.python.org/library/logging.config.html#logging.config.fileConfig

# for more details. Should either be None or a path to a file

# If this is set to a path, consider setting USE_DEFAULT_LOGGING to False

# otherwise there may be interactions between the log configuration file

# and the default logging setting.

#

# If set to a file path, this should be an absolute file path



LOG_CONF_FILE = None





# Path to store the INFO and ERROR logs

# If None this defaults to flaskbb/logs

#

# If set to a file path, this should be an absolute path

LOG_PATH = os.path.join(DefaultConfig.basedir, 'logs')



# The default logging configuration that will be used when

# USE_DEFAULT_LOGGING is set to True

LOG_DEFAULT_CONF = {

    'version': 1,

    'disable_existing_loggers': False,



    'formatters': {

        'standard': {

            'format': '%(asctime)s %(levelname)-7s %(name)-25s %(message)s'

        },

        'advanced': {

            'format': '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'

        }

    },



    'handlers': {

        'console': {

            'level': 'NOTSET',

            'formatter': 'standard',

            'class': 'logging.StreamHandler',

        },

        'flaskbb': {

            'level': 'DEBUG',

            'formatter': 'standard',

            'class': 'logging.handlers.RotatingFileHandler',

            'filename': os.path.join(LOG_PATH, 'flaskbb.log'),

            'mode': 'a',

            'maxBytes': 10485760,  # 10MB

            'backupCount': 5,

        },



        'infolog': {

            'level': 'INFO',

            'formatter': 'standard',

            'class': 'logging.handlers.RotatingFileHandler',

            'filename': os.path.join(LOG_PATH, 'info.log'),

            'mode': 'a',

            'maxBytes': 10485760,  # 10MB

            'backupCount': 5,

        },

        'errorlog': {

            'level': 'ERROR',

            'formatter': 'standard',

            'class': 'logging.handlers.RotatingFileHandler',

            'filename': os.path.join(LOG_PATH, 'error.log'),

            'mode': 'a',

            'maxBytes': 10485760,  # 10MB

            'backupCount': 5,

        }

    },



    'loggers': {

        'flask.app': {

            'handlers': ['infolog', 'errorlog'],

            'level': 'INFO',

            'propagate': True

        },

        'flaskbb': {

            'handlers': ['console', 'flaskbb'],

            'level': 'WARNING',

            'propagate': True

        },

    }

}



# When set to True this will enable the default

# FlaskBB logging configuration which uses the settings

# below to determine logging

USE_DEFAULT_LOGGING = True



# If SEND_LOGS is set to True, the admins (see the mail configuration) will

# recieve the error logs per email.

SEND_LOGS = False





# FlaskBB Settings

# ------------------------------ #

# URL Prefixes

FORUM_URL_PREFIX = ""

USER_URL_PREFIX = "/user"

MESSAGE_URL_PREFIX = "/message"

AUTH_URL_PREFIX = "/auth"

ADMIN_URL_PREFIX = "/admin"

# Remove dead plugins - useful if you want to migrate your instance

# somewhere else and forgot to reinstall the plugins.

# If set to `False` it will NOT remove plugins that are NOT installed on

# the filesystem (virtualenv, site-packages).

REMOVE_DEAD_PLUGINS = False



# determines the warning level for FlaskBB Deprecations

DEPRECATION_LEVEL = "default"


Preste atenção no segundo trecho, na variável SERVER_NAME, ela deve apontar para seu domínio no pythonanywhere.

Use https://yourname.pythonanywhere.com.error.log para visualizar eventuais mensagens de erro e identificar o que pode ter dado errado durante a instalação/configuração e boa sorte.

P.S.: Ao tentar fazer login ou registrar uma conta depois de clicar no link sugerido pelo pythonanywhere eu tive erros de CSRF. O link fornecido utiliza http, se você trocar por https o erro não ocorre.