Coverage for portality/core.py: 88%

145 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-09-05 21:15 +0100

1import os 

2import threading 

3import yaml 

4 

5from flask import Flask 

6from flask_login import LoginManager 

7from flask_cors import CORS 

8from jinja2 import FileSystemLoader 

9from lxml import etree 

10 

11from portality import settings, constants, datasets 

12from portality.bll import exceptions 

13from portality.error_handler import setup_error_logging 

14from portality.lib import es_data_mapping 

15from portality.ui.debug_toolbar import DoajDebugToolbar 

16 

17import esprit 

18import elasticsearch 

19 

20login_manager = LoginManager() 

21 

22 

23@login_manager.user_loader 

24def load_account_for_login_manager(userid): 

25 """ 

26 ~~LoginManager:Feature->Account:Model~~ 

27 :param userid: 

28 :return: 

29 """ 

30 from portality import models 

31 out = models.Account.pull(userid) 

32 return out 

33 

34 

35def create_app(): 

36 """ 

37 ~~CreateApp:Framework->Flask:Technology~~ 

38 :return: 

39 """ 

40 app = Flask(__name__) 

41 # ~~->AppSettings:Config~~ 

42 configure_app(app) 

43 #~~->ErrorHandler:Feature~~ 

44 setup_error_logging(app) 

45 #~~->Jinja2:Environment~~ 

46 setup_jinja(app) 

47 #~~->CrossrefXML:Feature~~ 

48 app.config["LOAD_CROSSREF_THREAD"] = threading.Thread(target=load_crossref_schema, args=(app, ), daemon=True) 

49 app.config["LOAD_CROSSREF_THREAD"].start() 

50 #~~->LoginManager:Feature~~ 

51 login_manager.init_app(app) 

52 #~~->CORS:Framework~~ 

53 CORS(app) 

54 #~~->APM:Feature~~ 

55 initialise_apm(app) 

56 #~~->DebugToolbar:Framework~~ 

57 DoajDebugToolbar(app) 

58 #~~->ProxyFix:Framework~~ 

59 proxyfix(app) 

60 #~~->CMS:Build~~ 

61 build_statics(app) 

62 return app 

63 

64 

65################################################## 

66# Configure the App 

67 

68def configure_app(app): 

69 """ 

70 Configure the DOAJ from: 

71 a) the settings.py file 

72 b) the <env>.cfg file 

73 c) the local secrets config in app.cfg 

74 

75 Later imports have precedence, so e.g. app.cfg will override the same setting in production.cfg and settings.py. 

76 """ 

77 

78 # import for settings.py 

79 app.config.from_object(settings) 

80 

81 # import from <env>.cfg 

82 here = os.path.dirname(os.path.abspath(__file__)) 

83 app.config['DOAJENV'] = get_app_env(app) 

84 config_path = os.path.join(os.path.dirname(here), app.config['DOAJENV'] + '.cfg') 

85 print('Running in ' + app.config['DOAJENV']) # the app.logger is not set up yet (?) 

86 if os.path.exists(config_path): 

87 app.config.from_pyfile(config_path) 

88 print('Loaded environment config from ' + config_path) 

89 

90 # import from app.cfg 

91 config_path = os.path.join(os.path.dirname(here), 'app.cfg') 

92 if os.path.exists(config_path): 

93 app.config.from_pyfile(config_path) 

94 print('Loaded secrets config from ' + config_path) 

95 

96 

97def get_app_env(app): 

98 if not app.config.get('VALID_ENVIRONMENTS'): 

99 raise Exception('VALID_ENVIRONMENTS must be set in the config. There shouldn\'t be a reason to change it in different set ups, or not have it.') 

100 

101 env = os.getenv('DOAJENV') 

102 if not env: 

103 envpath = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../.env') 

104 if os.path.exists(envpath): 

105 with open(envpath, 'r') as f: 

106 env = f.readline().strip() 

107 

108 if not env or env not in app.config['VALID_ENVIRONMENTS']: 

109 raise Exception( 

110""" 

111Set the DOAJENV environment variable when running the app, guessing is futile and fraught with peril. 

112DOAJENV=test python portality/app.py 

113to run the app will do. 

114Or use the supervisord options - put this in the config: environment= DOAJENV="test" . 

115 

116Finally, you can create a file called .env with the text e.g. 'dev' in the root of the repo. 

117Recommended only for dev environments so you don't have to bother specifying it each time you run a script or test. 

118 

119Valid values are: {valid_doajenv_vals} 

120 

121You can put environment-specific secret settings in <environment>.cfg , e.g. dev.cfg . 

122 

123The environment specified in the DOAJENV environment variable will override that specified in the 

124application configuration (settings.py or app.cfg). 

125""".format(valid_doajenv_vals=', '.join(app.config['VALID_ENVIRONMENTS'])) 

126 ) 

127 return env 

128 

129 

130################################################ 

131# Crossref setup 

132 

133def load_crossref_schema(app): 

134 """ 

135 ~~CrossrefXML:Feature->CrossrefXML:Schema~~ 

136 :param app: 

137 :return: 

138 """ 

139 schema442_path = app.config["SCHEMAS"].get("crossref442") 

140 schema531_path = app.config["SCHEMAS"].get("crossref531") 

141 

142 if not app.config.get("CROSSREF442_SCHEMA"): 

143 path = schema442_path 

144 try: 

145 schema_doc = etree.parse(schema442_path) 

146 schema = etree.XMLSchema(schema_doc) 

147 app.config["CROSSREF442_SCHEMA"] = schema 

148 except Exception as e: 

149 raise exceptions.IngestException( 

150 message="There was an error attempting to load schema from " + path, inner=e) 

151 

152 if not app.config.get("CROSSREF531_SCHEMA"): 

153 path = schema531_path 

154 try: 

155 schema_doc = etree.parse(schema531_path) 

156 schema = etree.XMLSchema(schema_doc) 

157 app.config["CROSSREF531_SCHEMA"] = schema 

158 except Exception as e: 

159 raise exceptions.IngestException( 

160 message="There was an error attempting to load schema from " + path, inner=e) 

161 

162 

163 

164############################################ 

165# Elasticsearch initialisation 

166 

167def create_es_connection(app): 

168 # ~~ElasticConnection:Framework->Elasticsearch:Technology~~ 

169 # temporary logging config for debugging index-per-type 

170 #import logging 

171 #esprit.raw.configure_logging(logging.DEBUG) 

172 

173 # FIXME: we are removing esprit conn in favour of elasticsearch lib 

174 # make a connection to the index 

175 # if app.config['ELASTIC_SEARCH_INDEX_PER_TYPE']: 

176 # conn = esprit.raw.Connection(host=app.config['ELASTIC_SEARCH_HOST'], index='') 

177 # else: 

178 # conn = esprit.raw.Connection(app.config['ELASTIC_SEARCH_HOST'], app.config['ELASTIC_SEARCH_DB']) 

179 

180 conn = elasticsearch.Elasticsearch(app.config['ELASTICSEARCH_HOSTS'], verify_certs=app.config.get("ELASTIC_SEARCH_VERIFY_CERTS", True)) 

181 

182 return conn 

183 

184# FIXME: deprecated no longer necessary 

185# def mutate_mapping(conn, type, mapping): 

186# """ When we are using an index-per-type connection change the mappings to be keyed 'doc' rather than the type """ 

187# if conn.index_per_type: 

188# try: 

189# mapping[esprit.raw.INDEX_PER_TYPE_SUBSTITUTE] = mapping.pop(type) 

190# except KeyError: 

191# # Allow this mapping through unaltered if it isn't keyed by type 

192# pass 

193# 

194# # Add the index prefix to the mapping as we create the type 

195# type = app.config['ELASTIC_SEARCH_DB_PREFIX'] + type 

196# return type 

197 

198 

199def put_mappings(conn, mappings): 

200 # get the ES version that we're working with 

201 #es_version = app.config.get("ELASTIC_SEARCH_VERSION", "1.7.5") 

202 

203 # for each mapping (a class may supply multiple), create a mapping, or mapping and index 

204 # for key, mapping in iter(mappings.items()): 

205 # altered_key = mutate_mapping(conn, key, mapping) 

206 # ix = conn.index or altered_key 

207 # if not esprit.raw.type_exists(conn, altered_key, es_version=es_version): 

208 # r = esprit.raw.put_mapping(conn, altered_key, mapping, es_version=es_version) 

209 # print("Creating ES Type + Mapping in index {0} for {1}; status: {2}".format(ix, key, r.status_code)) 

210 # else: 

211 # print("ES Type + Mapping already exists in index {0} for {1}".format(ix, key)) 

212 

213 for key, mapping in iter(mappings.items()): 

214 altered_key = app.config['ELASTIC_SEARCH_DB_PREFIX'] + key 

215 if not conn.indices.exists(altered_key): 

216 r = conn.indices.create(index=altered_key, body=mapping) 

217 print("Creating ES Type + Mapping in index {0} for {1}; status: {2}".format(altered_key, key, r)) 

218 else: 

219 print("ES Type + Mapping already exists in index {0} for {1}".format(altered_key, key)) 

220 

221 

222def initialise_index(app, conn, only_mappings=None): 

223 """ 

224 ~~InitialiseIndex:Framework->Elasticsearch:Technology~~ 

225 :param app: 

226 :param conn: 

227 :return: 

228 """ 

229 if not app.config['INITIALISE_INDEX']: 

230 app.logger.warn('INITIALISE_INDEX config var is not True, initialise_index command cannot run') 

231 return 

232 

233 if app.config.get("READ_ONLY_MODE", False) and app.config.get("SCRIPTS_READ_ONLY_MODE", False): 

234 app.logger.warn("System is in READ-ONLY mode, initialise_index command cannot run") 

235 return 

236 

237 # get the app mappings 

238 mappings = es_data_mapping.get_mappings(app) 

239 

240 if only_mappings is not None: 

241 mappings = {key:value for (key, value) in mappings.items() if key in only_mappings} 

242 

243 # Send the mappings to ES 

244 put_mappings(conn, mappings) 

245 

246 

247################################################## 

248# APM 

249 

250def initialise_apm(app): 

251 """ 

252 ~~APM:Feature->ElasticAPM:Technology~~ 

253 :param app: 

254 :return: 

255 """ 

256 if app.config.get('ENABLE_APM', False): 

257 from elasticapm.contrib.flask import ElasticAPM 

258 app.logger.info("Configuring Elastic APM") 

259 apm = ElasticAPM(app, logging=True) 

260 

261 

262################################################## 

263# proxyfix 

264 

265def proxyfix(app): 

266 """ 

267 ~~ProxyFix:Framework~~ 

268 :param app: 

269 :return: 

270 """ 

271 if app.config.get('PROXIED', False): 

272 from werkzeug.middleware.proxy_fix import ProxyFix 

273 app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1) 

274 

275 

276################################################## 

277# Jinja2 

278 

279def setup_jinja(app): 

280 """ 

281 Jinja2:Environment->Jinja2:Technology 

282 :param app: 

283 :return: 

284 """ 

285 '''Add jinja extensions and other init-time config as needed.''' 

286 

287 app.jinja_env.add_extension('jinja2.ext.do') 

288 app.jinja_env.add_extension('jinja2.ext.loopcontrols') 

289 app.jinja_env.globals['getattr'] = getattr 

290 app.jinja_env.globals['type'] = type 

291 #~~->Constants:Config~~ 

292 app.jinja_env.globals['constants'] = constants 

293 #~~->Datasets:Data~~ 

294 app.jinja_env.globals['datasets'] = datasets 

295 _load_data(app) 

296 #~~->CMS:DataStore~~ 

297 app.jinja_env.loader = FileSystemLoader([app.config['BASE_FILE_PATH'] + '/templates', 

298 os.path.dirname(app.config['BASE_FILE_PATH']) + '/cms/fragments']) 

299 

300 # a jinja filter that prints to the Flask log 

301 def jinja_debug(text): 

302 print(text) 

303 return '' 

304 app.jinja_env.filters['debug']=jinja_debug 

305 

306 

307def _load_data(app): 

308 if not "data" in app.jinja_env.globals: 

309 app.jinja_env.globals["data"] = {} 

310 datadir = os.path.join(app.config["BASE_FILE_PATH"], "..", "cms", "data") 

311 for datafile in os.listdir(datadir): 

312 with open(os.path.join(datadir, datafile)) as f: 

313 data = yaml.load(f, Loader=yaml.FullLoader) 

314 dataname = datafile.split(".")[0] 

315 dataname = dataname.replace("-", "_") 

316 app.jinja_env.globals["data"][dataname] = data 

317 

318 

319################################################## 

320# Static Content 

321 

322def build_statics(app): 

323 """ 

324 ~~CMS:Build->CMSFragments:Build~~ 

325 ~~->CMSSASS:Build~~ 

326 :param app: 

327 :return: 

328 """ 

329 if not app.config.get("CMS_BUILD_ASSETS_ON_STARTUP", False): 

330 return 

331 from portality.cms import build_fragments, build_sass 

332 

333 here = os.path.dirname(os.path.abspath(__file__)) 

334 base_path = os.path.dirname(here) 

335 

336 print("Compiling static content") 

337 build_fragments.build(base_path) 

338 print("Compiling main SASS") 

339 build_sass.build(build_sass.MAIN_SETTINGS, base_path=base_path) 

340 

341 

342app = create_app() 

343es_connection = create_es_connection(app)