Coverage for apps/ptf/display/resolver.py: 77%

352 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-02-28 09:09 +0000

1import os 

2import shutil 

3import time 

4 

5from django.conf import settings 

6 

7from ptf.cmds.xml import xml_utils 

8 

9NOW = time.gmtime()[0] 

10 

11 

12def resolve_id(id_type, id_value, force_numdam=False): 

13 if id_type == "doi": 

14 href = "https://doi.org/" + id_value 

15 elif id_type == "zbl-item-id" or id_type == "jfm-item-id": 

16 href = "https://zbmath.org/?q=an:" + id_value 

17 elif id_type == "mr-item-id": 

18 if "#" in id_value: 18 ↛ 19line 18 didn't jump to line 19, because the condition on line 18 was never true

19 id_value = id_value.replace(" #", ":") 

20 href = "https://mathscinet.ams.org/mathscinet-getitem?mr=" + id_value 

21 elif id_type == "nmid" or id_type == "numdam-id" or id_type == "mathdoc-id": 

22 if force_numdam: 22 ↛ 23line 22 didn't jump to line 23, because the condition on line 22 was never true

23 href = f"http://www.numdam.org/item/{id_value}" 

24 else: 

25 href = f"/item/{id_value}" 

26 elif id_type == "eudml-item-id": 

27 values = id_value.split(":") 

28 if len(values) > 0: 28 ↛ 30line 28 didn't jump to line 30, because the condition on line 28 was never false

29 id_value = values[-1] 

30 href = "https://eudml.org/doc/" + id_value 

31 elif id_type == "sps-id": 31 ↛ 32line 31 didn't jump to line 32, because the condition on line 31 was never true

32 href = "http://sites.mathdoc.fr/cgi-bin/spitem?id=" + id_value 

33 elif id_type == "arxiv": 

34 href = "https://arxiv.org/abs/" + id_value 

35 elif id_type == "hal": 35 ↛ 36line 35 didn't jump to line 36, because the condition on line 35 was never true

36 href = "https://hal.archives-ouvertes.fr/" + id_value 

37 elif id_type == "tel": 37 ↛ 38line 37 didn't jump to line 38, because the condition on line 37 was never true

38 href = "https://tel.archives-ouvertes.fr/" + id_value 

39 elif id_type == "theses.fr": 39 ↛ 40line 39 didn't jump to line 40, because the condition on line 39 was never true

40 href = "https://theses.fr/" + id_value 

41 elif id_type == "orcid": 41 ↛ 42line 41 didn't jump to line 42, because the condition on line 41 was never true

42 href = "https://orcid.org/" + id_value 

43 elif id_type == "semantic-scholar": 43 ↛ 45line 43 didn't jump to line 45, because the condition on line 43 was never false

44 href = "https://www.semanticscholar.org/paper/" + id_value 

45 elif id_type == "pmid": 

46 href = "https://pubmed.ncbi.nlm.nih.gov/" + id_value 

47 elif id_type == "ark": 

48 href = "http://ark.bnf.fr/" + id_value 

49 else: 

50 href = "" 

51 return href 

52 

53 

54def find_id_type(id): 

55 id_type = None 

56 if id.find("10.") == 0: 

57 id_type = "doi" 

58 elif id.find("hal-") == 0: 

59 id_type = "hal" 

60 elif id.lower().find("arxiv:") == 0: 60 ↛ 70line 60 didn't jump to line 70, because the condition on line 60 was never false

61 id_type = "arxiv" 

62 

63 # if (len(id) == 9 or len(id) == 10) and id.find(".") == 5: 

64 # year = id[0:1] 

65 # month = id[2:3] 

66 # sequence = id[5:] 

67 # if year.is_numeric() and month.is_numeric() and 1 < int(month) < 13 and sequence.is_numeric(): 

68 # id_type = "arxiv" 

69 

70 return id_type 

71 

72 

73def get_mimetype(filename): 

74 type_extension = { 

75 "pdf": "application/pdf", 

76 "djvu": "image/x.djvu", 

77 "tex": "application/x-tex", 

78 "png": "image/png", 

79 "jpg": "image/jpeg", 

80 } 

81 

82 basename = os.path.basename(filename) 

83 lower_basename = basename.lower() 

84 extension = os.path.splitext(lower_basename)[1][1:] 

85 mimetype = type_extension.get(extension, "") 

86 return mimetype 

87 

88 

89def get_article_base_url(): 

90 return settings.ARTICLE_BASE_URL 

91 

92 

93def get_issue_base_url(): 

94 return settings.ISSUE_BASE_URL 

95 

96 

97def get_icon_base_url(): 

98 return settings.ICON_BASE_URL 

99 

100 

101def get_icon_url(id_, filename): 

102 href = get_icon_base_url() + filename 

103 # path = get_relative_file_path(id, filename) 

104 # if os.path.isabs(path): 

105 # path = path[1:] 

106 # href = os.path.join(get_icon_base_url(), path) 

107 return href 

108 

109 

110def get_doi_url(doi): 

111 href = settings.DOI_BASE_URL + doi 

112 return href 

113 

114 

115def get_relative_folder(collection_id, container_id=None, article_id=None): 

116 folder = collection_id 

117 if container_id: 

118 folder += "/" + container_id 

119 if article_id: 

120 folder += "/" + article_id 

121 return folder 

122 

123 

124def embargo(wall, year): 

125 result = False 

126 y = NOW 

127 

128 if wall: 

129 try: 

130 y = int(year.split("-")[0]) 

131 except BaseException: 

132 pass 

133 

134 result = NOW - y <= wall 

135 

136 return result 

137 

138 

139# Iterate a folder with a collection 

140# The folder must look like @COL/@ISSUE/@ISSUE.XML 

141 

142 

143def iterate_collection_folder(folder, pid, first_issue=""): 

144 root_folder = os.path.join(folder, pid) 

145 

146 start = len(first_issue) == 0 

147 # first_issue = 'CRMATH_2008__346_1-2' 

148 for item in sorted(os.listdir(root_folder)): 

149 if not start and item == first_issue: 149 ↛ 150line 149 didn't jump to line 150, because the condition on line 149 was never true

150 start = True 

151 if start: # and item != 'CRMATH_2015__353_2': 151 ↛ 148line 151 didn't jump to line 148, because the condition on line 151 was never false

152 dir = os.path.join(root_folder, item) 

153 if os.path.isdir(dir): 

154 file = os.path.join(root_folder, item, item + ".xml") 

155 if os.path.isfile(file): 155 ↛ 157line 155 didn't jump to line 157, because the condition on line 155 was never false

156 yield item, file 

157 file = os.path.join(root_folder, item, item + "-cdrxml.xml") 

158 if os.path.isfile(file): 158 ↛ 159line 158 didn't jump to line 159, because the condition on line 158 was never true

159 yield item, file 

160 

161 

162def create_folder(folder): 

163 try: 

164 os.makedirs(folder) 

165 except BaseException: 

166 pass 

167 

168 if not os.path.isdir(folder): 168 ↛ 169line 168 didn't jump to line 169, because the condition on line 168 was never true

169 raise RuntimeError("Unable to create " + folder) 

170 

171 

172def copy_folder(from_dir, to_dir): 

173 if os.path.isdir(from_dir): 173 ↛ 176line 173 didn't jump to line 176, because the condition on line 173 was never false

174 create_folder(to_dir) 

175 

176 for f in os.listdir(from_dir): 

177 from_path = os.path.join(from_dir, f) 

178 if os.path.isfile(from_path): 178 ↛ 180line 178 didn't jump to line 180, because the condition on line 178 was never false

179 copy_file(from_path, to_dir) 

180 if os.path.isdir(from_path): 180 ↛ 181line 180 didn't jump to line 181, because the condition on line 180 was never true

181 copy_folder(from_path, os.path.join(to_dir, f)) 

182 

183 

184def copy_file(from_path, to_path): 

185 if os.path.isfile(from_path): 

186 if os.path.isdir(to_path): 

187 to_path = os.path.join(to_path, os.path.basename(from_path)) 

188 if to_path.startswith(settings.MATHDOC_ARCHIVE_FOLDER): 

189 # copy2 attempts to preserve all file metadata 

190 # on /mathdoc_archive, we don't want to preserve the mode, just the dates 

191 shutil.copyfile(from_path, to_path) 

192 shutil.copystat(from_path, to_path) 

193 else: 

194 shutil.copy2(from_path, to_path) 

195 

196 

197def copy_html_images(resource, to_folder, from_folder): 

198 """ 

199 Copy the figures associated with the HTML body of an article 

200 if from_archive: 

201 Images are in settings.MATHDOC/@colid/@issue_id/@a_id/src/tex/figures/ 

202 if from_cedram: 

203 Images are in settings.CEDRAM_TEX_FOLDER/@colid/@issue_id/@tex_aid/Fulltext/figures/ 

204 

205 @param resource: 

206 @param to_folder: 

207 @param from_folder: 

208 @return: nothing 

209 """ 

210 

211 if resource.classname != "Article": 

212 return 

213 

214 article_to_copy = resource 

215 issue = article_to_copy.my_container 

216 colid = article_to_copy.get_collection().pid 

217 

218 if from_folder == settings.CEDRAM_XML_FOLDER: 

219 tex_src_folder = get_cedram_issue_tex_folder(colid, issue.pid) 

220 tex_folders, _ = get_cedram_tex_folders(colid, issue.pid) 

221 

222 if len(tex_folders) > 0: 222 ↛ exitline 222 didn't return from function 'copy_html_images', because the condition on line 222 was never false

223 i = 0 

224 for article in issue.article_set.all(): 

225 if article_to_copy.pid == article.pid: 225 ↛ 252line 225 didn't jump to line 252, because the condition on line 225 was never false

226 # l'ordre d'enregistrement des articles dans la bdd est important : l'ordre du tex est SENSE correspondre au xml de l'issue 

227 

228 dest_folder = os.path.join( 

229 to_folder, 

230 get_relative_folder(colid, issue.pid, article.pid), 

231 "src/tex/figures", 

232 ) 

233 

234 if os.path.isdir(dest_folder): 

235 try: 

236 shutil.rmtree(dest_folder) 

237 except OSError: 

238 message = "Unable to remove " + dest_folder 

239 raise RuntimeError(message) 

240 

241 src_folder = os.path.join( 

242 tex_src_folder, tex_folders[i], "FullText", "figures" 

243 ) 

244 qs = article.relatedobject_set.filter(rel="html-image") 

245 if qs.count() > 0: 245 ↛ 248line 245 didn't jump to line 248, because the condition on line 245 was never false

246 create_folder(dest_folder) 

247 

248 for related_obj in qs: 

249 img_file = os.path.join(src_folder, os.path.basename(related_obj.location)) 

250 copy_file(img_file, dest_folder) 

251 

252 i += 1 

253 

254 else: 

255 # copy depuis archive, directement tout le répertoire contenant les images 

256 dest_folder = os.path.join( 

257 to_folder, 

258 get_relative_folder(colid, issue.pid, article_to_copy.pid), 

259 "src/tex/figures", 

260 ) 

261 if os.path.isdir(dest_folder): 

262 try: 

263 shutil.rmtree(dest_folder) 

264 except OSError: 

265 message = "Unable to remove " + dest_folder 

266 raise RuntimeError(message) 

267 

268 src_folder = os.path.join( 

269 from_folder, 

270 get_relative_folder(colid, issue.pid, article_to_copy.pid), 

271 "src/tex/figures", 

272 ) 

273 if os.path.isdir(src_folder): 

274 copy_folder(src_folder, dest_folder) 

275 

276 

277def copy_file_obj_to_article_folder(file_obj, colid, issue_pid, article_pid): 

278 name, extension = os.path.splitext(file_obj.name) 

279 

280 relative_folder = get_relative_folder(colid, issue_pid, article_pid) 

281 folder = os.path.join(settings.RESOURCES_ROOT, relative_folder) 

282 create_folder(folder) 

283 full_file_name = os.path.join(folder, article_pid + extension) 

284 relative_file_name = os.path.join(relative_folder, article_pid + extension) 

285 

286 with open(full_file_name, "wb+") as destination: 

287 for chunk in file_obj.chunks(): 

288 destination.write(chunk) 

289 

290 return relative_file_name 

291 

292 

293def copy_binary_files(resource, from_folder, to_folder, binary_files=None): 

294 if not from_folder == to_folder: 

295 if binary_files is None: 295 ↛ 299line 295 didn't jump to line 299, because the condition on line 295 was never false

296 copy_html_images(resource, to_folder, from_folder) 

297 binary_files = resource.get_binary_files_location() 

298 

299 for file in binary_files: 

300 to_path = os.path.join(to_folder, file) 

301 dest_folder = os.path.dirname(to_path) 

302 

303 os.makedirs(dest_folder, exist_ok=True) 

304 skip_copy = False 

305 

306 if "http" in file: 306 ↛ 307line 306 didn't jump to line 307, because the condition on line 306 was never true

307 skip_copy = True 

308 from_path = os.path.join(from_folder, file) 

309 

310 if not skip_copy and os.path.isfile(from_path): 310 ↛ 299line 310 didn't jump to line 299, because the condition on line 310 was never false

311 copy_file(from_path, to_path) 

312 

313 

314def delete_object_folder(object_folder, to_folder): 

315 folder = os.path.join(to_folder, object_folder) 

316 

317 # pas de sécurité car pour garder le mode CASCADE de la db, on supprime le rép sans s'occuper de ce qu'il y a dedans 

318 # si on veut vérifier, décommenter : 

319 # for entry in os.listdir(folder): 

320 # if entry.startswith(colid) and os.path.isdir(os.path.join(folder, entry)): 

321 # print(entry) 

322 # os.path.join(folder, entry) 

323 # raise Exception('Le répertoire a supprimer : ' + folder + ' semble encore contenir des articles/containers') 

324 # 

325 # if verify == True: 

326 # for root, dirs, files in os.walk(folder): 

327 # if len(files) > 0: 

328 # raise Exception('Le répertoire a supprimer : ' + folder + ' semble encore contenir des objects') 

329 

330 folder = os.path.normpath(folder) 

331 # garde fous :) 

332 if folder in [ 332 ↛ 337line 332 didn't jump to line 337, because the condition on line 332 was never true

333 "/mersenne_prod_data", 

334 "/mersenne_test_data", 

335 "/mathdoc_archive", 

336 ] or folder.startswith("/cedram_dev"): 

337 raise Exception("Attention, pb avec la suppression de " + folder) 

338 

339 if os.path.isdir(folder): 

340 shutil.rmtree(folder) 

341 

342 

343def delete_file(path): 

344 if os.path.isfile(path): 

345 os.remove(path) 

346 

347 

348def get_disk_location( 

349 root_folder, collection_id, ext, container_id=None, article_id=None, do_create_folder=False 

350): 

351 if do_create_folder: 351 ↛ 352line 351 didn't jump to line 352, because the condition on line 351 was never true

352 folder = os.path.join(root_folder, collection_id) 

353 create_folder(folder) 

354 

355 if container_id: 

356 folder = os.path.join(root_folder, collection_id, container_id) 

357 create_folder(folder) 

358 

359 if article_id: 

360 folder = os.path.join(root_folder, collection_id, container_id, article_id) 

361 create_folder(folder) 

362 

363 last_id = collection_id 

364 filename = os.path.join(root_folder, collection_id) 

365 if container_id: 365 ↛ 368line 365 didn't jump to line 368, because the condition on line 365 was never false

366 filename = os.path.join(filename, container_id) 

367 last_id = container_id 

368 if article_id: 368 ↛ 372line 368 didn't jump to line 372, because the condition on line 368 was never false

369 filename = os.path.join(filename, article_id) 

370 last_id = article_id 

371 

372 filename = os.path.join(filename, last_id + "." + ext) 

373 

374 return filename 

375 

376 

377def get_body(filename): 

378 with open(filename, encoding="utf-8") as file_: 

379 body = file_.read() 

380 return body 

381 

382 

383def get_archive_filename(root_folder, colid, pid, ext, do_create_folder=False, article_pid=None): 

384 """ 

385 

386 :param root_folder: root_folder of the archive. Ex: /mathdoc_archive 

387 :param colid: collection id 

388 :param pid: issue id 

389 :param ext: filename extension ("xml" or "json") 

390 :param create_folder: option to recursively create sub folders 

391 :return: 

392 """ 

393 

394 # TODO: call get_disk_location(root_folder, colid, ext, pid, None, do_create_folder) 

395 

396 if do_create_folder: 

397 folder = os.path.join(root_folder, colid) 

398 create_folder(folder) 

399 

400 if pid: 

401 folder = os.path.join(root_folder, colid, pid) 

402 create_folder(folder) 

403 

404 if article_pid: 404 ↛ 405line 404 didn't jump to line 405, because the condition on line 404 was never true

405 folder = os.path.join(folder, article_pid) 

406 create_folder(folder) 

407 

408 if pid and article_pid: 408 ↛ 409line 408 didn't jump to line 409, because the condition on line 408 was never true

409 filename = os.path.join(root_folder, colid, pid, article_pid, article_pid + "." + ext) 

410 elif pid: 

411 filename = os.path.join(root_folder, colid, pid, pid + "." + ext) 

412 else: 

413 filename = os.path.join(root_folder, colid, colid + "." + ext) 

414 

415 return filename 

416 

417 

418# Read the XML of an issue/collection within an archive folder 

419# The folder must look like @COL/@ISSUE/@ISSUE.XML 

420# @COL/@COL.XML 

421 

422 

423def get_archive_body(root_folder, colid, pid): 

424 filename = get_archive_filename(root_folder, colid, pid, "xml") 

425 return get_body(filename) 

426 

427 

428def is_tex_comment(text, i): 

429 is_comment = False 

430 while i > 0 and text[i] == " ": 430 ↛ 431line 430 didn't jump to line 431, because the condition on line 430 was never true

431 i -= 1 

432 

433 if i >= 0 and text[i] == "%": 433 ↛ 434line 433 didn't jump to line 434, because the condition on line 433 was never true

434 is_comment = True 

435 elif i > 0 and text[i] == "~" and text[i - 1] == "%": 435 ↛ 436line 435 didn't jump to line 436, because the condition on line 435 was never true

436 is_comment = True 

437 

438 return is_comment 

439 

440 

441def is_tex_def(text, i): 

442 is_def = False 

443 

444 if text[i - 5 : i - 1] == "\\def": 444 ↛ 445line 444 didn't jump to line 445, because the condition on line 444 was never true

445 is_def = True 

446 

447 return is_def 

448 

449 

450def is_tex_newcommand(text, i): 

451 is_newcommand = False 

452 

453 if text[i - 12 : i - 1] == "\\newcommand": 453 ↛ 454line 453 didn't jump to line 454, because the condition on line 453 was never true

454 is_newcommand = True 

455 

456 return is_newcommand 

457 

458 

459def get_cedram_issue_tex_folder(colid, issue_id): 

460 return os.path.join(settings.CEDRAM_TEX_FOLDER, colid, issue_id) 

461 

462 

463def get_cedram_tex_folders(colid, issue_id): 

464 """ 

465 return article filenames in cedram tex issue folder and corresponding doi if present, extracted from issue tex file 

466 @param colid: 

467 @param issue_id: 

468 @return: list of filename, list of doi 

469 """ 

470 filenames = [] 

471 dois = [] 

472 

473 body = "" 

474 issue_filename = os.path.join(get_cedram_issue_tex_folder(colid, issue_id), issue_id + ".tex") 

475 if os.path.isfile(issue_filename): 

476 try: 

477 with open(issue_filename, encoding="utf-8") as f: 

478 body = f.read() 

479 except UnicodeDecodeError: 

480 with open(issue_filename, encoding="iso-8859-1") as f: 

481 body = f.read() 

482 

483 lower_body = body.lower() 

484 

485 li = [] 

486 j = body.find("includearticle") 

487 if j >= 0: 487 ↛ 489line 487 didn't jump to line 489, because the condition on line 487 was never false

488 li.append(j) 

489 j = body.find("includeprearticle") 

490 if j >= 0: 490 ↛ 491line 490 didn't jump to line 491, because the condition on line 490 was never true

491 li.append(j) 

492 j = lower_body.find("includepreface") 

493 if j >= 0: 493 ↛ 494line 493 didn't jump to line 494, because the condition on line 493 was never true

494 li.append(j) 

495 i = min(li) if len(li) > 0 else -1 

496 

497 while i >= 0: 

498 if ( 498 ↛ 521line 498 didn't jump to line 521

499 i > 1 

500 and not is_tex_comment(body, i - 2) 

501 and not is_tex_def(body, i) 

502 and not is_tex_newcommand(body, i) 

503 ): 

504 doi = None 

505 while body[i] != "{": 

506 if len(body) > i + 4 and body[i : i + 4] == "doi=": 

507 j = i + 4 

508 while body[i] != "," and body[i] != "]": 

509 i += 1 

510 doi = xml_utils.normalize_space(body[j:i]) 

511 i += 1 

512 i += 1 

513 filename = "" 

514 while body[i] != "}": 

515 filename += body[i] 

516 i += 1 

517 if len(filename) > 0: 517 ↛ 523line 517 didn't jump to line 523, because the condition on line 517 was never false

518 filenames.append(filename) 

519 dois.append(doi) 

520 else: 

521 i += 1 

522 

523 li = [] 

524 j = body.find("includearticle", i) 

525 if j >= 0: 

526 li.append(j) 

527 j = body.find("includeprearticle", i) 

528 if j >= 0: 528 ↛ 529line 528 didn't jump to line 529, because the condition on line 528 was never true

529 li.append(j) 

530 j = lower_body.find("includepreface", i) 

531 if j >= 0: 531 ↛ 532line 531 didn't jump to line 532, because the condition on line 531 was never true

532 li.append(j) 

533 i = min(li) if len(li) > 0 else -1 

534 

535 return filenames, dois 

536 

537 

538def get_bibtex_from_tex(tex_filename): 

539 bibtex_filename = "" 

540 

541 body = "" 

542 if os.path.isfile(tex_filename): 542 ↛ 564line 542 didn't jump to line 564, because the condition on line 542 was never false

543 try: 

544 with open(tex_filename, encoding="utf-8") as f: 

545 body = f.read() 

546 except UnicodeDecodeError: 

547 with open(tex_filename, encoding="iso-8859-1") as f: 

548 body = f.read() 

549 

550 i = body.find("\\bibliography") 

551 while i >= 0: 

552 if i > 1 and not is_tex_comment(body, i - 2): 552 ↛ 560line 552 didn't jump to line 560, because the condition on line 552 was never false

553 while body[i] != "{": 

554 i += 1 

555 i += 1 

556 while body[i] != "}": 

557 bibtex_filename += body[i] 

558 i += 1 

559 else: 

560 i += 1 

561 

562 i = body.find("\\bibliography", i) 

563 

564 return bibtex_filename 

565 

566 

567PCJ_SECTIONS = { 

568 "animsci": "Animal Science", 

569 "archaeo": "Archaeology", 

570 "ecology": "Ecology", 

571 "ecotoxenvchem": "Ecotoxicology & Environmental Chemistry", 

572 "evolbiol": "Evolutionary Biology", 

573 "forestwoodsci": "Forest & Wood Sciences", 

574 "genomics": "Genomics", 

575 "healthmovsci": "Health & Movement Sciences", 

576 "infections": "Infections", 

577 "mcb": "Mathematical & Computational Biology", 

578 "microbiol": "Microbiology", 

579 "networksci": "Network Science", 

580 "neuro": "Neuroscience", 

581 "paleo": "Paleontology", 

582 "rr": "Registered Reports", 

583 "zool": "Zoology", 

584} 

585 

586PCJ_UGA_SECTION = ["healthmovsci", "rr"] 

587PCJ_CONFERENCES = ["Euring 2023"] 

588PCJ_MANDATORY_TOPICS = { 

589 "ecology": "Ecology", 

590 "evolbiol": "Evolution", 

591 "genomics": "Genetics/genomics", 

592 "paleo": "Paleontology", 

593 "archaeo": "Archaeology", 

594 "microbiol": "Microbiology", 

595 "neuro": "Neuroscience", 

596} 

597 

598 

599def get_pci(value): 

600 if value in PCJ_SECTIONS: 600 ↛ 602line 600 didn't jump to line 602, because the condition on line 600 was never false

601 return PCJ_SECTIONS[value] 

602 return "" 

603 

604 

605ARTICLE_TYPES = { 

606 "biographical-note": "Notice biographique", 

607 "congress": "Intervention en colloque", 

608 "corrigendum": "Corrigendum", 

609 "editorial": "Éditorial", 

610 "erratum": "Erratum", 

611 "expression-of-concern": "Avertissement des éditeurs", 

612 "foreword": "Avant-propos", 

613 "guest-editors": "Rédacteurs invités", 

614 "historical-commentary": "Commentaire historique", 

615 "history-of-sciences": "Histoire des sciences et des idées", 

616 "letter": "Commentaire et réponse", 

617 "news": "C'est apparu dans la presse", 

618 "opinion": "Opinion, perspective", 

619 "preliminary-communication": "Communication préliminaire", 

620 "research-article": "Article de recherche", 

621 "retraction": "Rétractation", 

622 "review": "Article de synthèse", 

623 "software-tool": "Outil logiciel", 

624}