checkandfix.py 29.2 KB
Newer Older
1
""" harvest_tools.checkandfix
2 3

"""
4
import logging
5
import numpy as np
6 7
import re

8
from .base import search_synonym, ToolException
9
from datetime import datetime
10
from .exception import CheckException
11
from gluon import current
12 13 14 15 16 17 18
from store_tools import (MSG_NO_CONF,
                         MSG_NO_THESIS,
                         OAI_URL,
                         RecordConf,
                         RecordThesis,
                         REG_OAI,
                         REG_YEAR)
19

20
from store_tools.pluginpublicationinfo import PAPER_REFERENCE_KEYS
21

22
from plugin_dbui import CLEAN_SPACES, get_id, UNDEF_ID
23

24
DECODE_ARXIV = re.compile(r"arXiv:(\d{2})(\d{2})\.")
25 26 27 28 29

# Decode submitted date: DD MMM YYYY or DD MM YYY
DECODE_DD_MMM_YYYY = re.compile(r"(\d{1,2}) *([A-Za-z]{3}) *(\d{4})")
DECODE_DD_MM_YYYY = re.compile(r"(\d{1,2}) +(\d{1,2}) +(\d{4})")

30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
MONTHS = {"Jan": "01",
          "Feb": "02",
          "Fev": "02",
          "Mar": "03",
          "Apr": "04",
          "Avr": "04",
          "May": "05",
          "Mai": "05",
          "Jun": "06",
          "Jul": "07",
          "Aug": "08",
          "Sep": "09",
          "Oct": "10",
          "Nov": "11",
          "Dec": "12"}
45

LE GAC Renaud's avatar
LE GAC Renaud committed
46
MSG_FAUTHOR_COLLABORATION = "Reject first author is a Collaboration"
47
MSG_NO_AUTHOR = "Reject no author(s)"
48
MSG_NO_CONF_DATE = "Reject no conference date"
49
MSG_NO_DATE = "Reject no submission date"
50
MSG_NO_MY_AUTHOR = "Reject no authors of my institute"
51
MSG_NO_REF = "Reject incomplete paper reference. Check "
52

53
MSG_TEMPORARY_RECORD = "Temporary record"
54 55 56
MSG_UNKNOWN_COLLABORATION = "Reject collaboration is unknown."
MSG_UNKNOWN_COUNTRY = "Reject country is unknown."
MSG_UNKNOWN_PUBLISHER = "Reject publisher is unknown."
57
MSG_WELL_FORMED_DATE = "Reject submission date is not well formed"
58

59 60
REG_CONF_DATES_1 = \
    re.compile("0?(\d+) *-? *0?(\d+) *([A-Z][a-z]{2})[a-z]* *(\d{4})")
61 62

REG_CONF_DATES_2 = \
63
    re.compile("0?(\d+) *([A-Z][a-z]{2})[a-z]* *-? *0?(\d+) *([A-Z][a-z]{2})[a-z]* *(\d{4})")
64

65 66
REG_DOI = re.compile(r"\d+\.\d+/([a-zA-Z]+)\.(\d+)\.(\w+)")

LE GAC Renaud's avatar
LE GAC Renaud committed
67 68
REG_WELL_FORMED_CONF_DATES_1 = \
    re.compile("\d{1,2}-\d{1,2} [A-Z][a-z]{2} \d{4}")
69 70

REG_WELL_FORMED_CONF_DATES_2 = \
LE GAC Renaud's avatar
LE GAC Renaud committed
71
    re.compile("\d{1,2} [A-Z][a-z]{2} - \d{1,2} [A-Z][a-z]{2} \d{4}")
72

73
T6 = " "*6
74 75
UNIVERSITY = "University"

76 77

class CheckAndFix(object):
78 79
    """A collection of tools to check and repair the content of record.

80
    """
LE GAC Renaud's avatar
LE GAC Renaud committed
81

82
    def __init__(self):
83

84
        self.db = current.db
85
        self.logger = logging.getLogger("web2py.app.limbra")
86 87 88 89 90 91 92
        self.reg_institute = self._get_reg_institute()

        # private cache for my_author rescue list
        self.__par = None
        self.__reference = None

        # private cache for my authors list
93
        self._my_authors = {}
94

95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
    @staticmethod
    def _get_conference_dates(record):
        """Return the opening and closing dates of a conference.

        Args:
            record (RecordConf):
                record describing a conference proceeding or talk.

        Returns:
            tuple of datetime.date:
                opening and closing dates.

        Raise:
            ToolException:
                no conference date found.

        """
112
        if "meeting_name" not in record:
113 114
            raise ToolException(MSG_NO_CONF_DATE)

115
        meeting = record["meeting_name"]
116 117 118
        meeting = (meeting[0] if isinstance(meeting, list) else meeting)

        # CDS has the opening and closing dates encoded as 20141231
119
        if "opening_date" in meeting and "closing_date" in meeting:
120

LE GAC Renaud's avatar
LE GAC Renaud committed
121 122
            val = meeting["opening_date"].replace("-", "")
            opening = datetime.strptime(val, "%Y%m%d")
123

LE GAC Renaud's avatar
LE GAC Renaud committed
124 125
            val = meeting["closing_date"].replace("-", "")
            closing = datetime.strptime(val, "%Y%m%d")
126 127 128 129

            return (opening, closing)

        # both CDS and INSPIRE have the dates subfield
130
        val = meeting["date"]
131 132 133 134 135 136 137

        # date is encode as 12 - 15 Mar 2014
        m = REG_CONF_DATES_1.match(val)
        if m:

            fmt = "%d-%b-%Y"

138
            val = "%s-%s-%s" % (m.group(1), m.group(3), m.group(4))
139 140
            opening = datetime.strptime(val, fmt)

141
            val = "%s-%s-%s" % (m.group(2), m.group(3), m.group(4))
142 143 144 145 146 147
            closing = datetime.strptime(val, fmt)

            return (opening, closing)

        # dates are encoded 29 Feb - 1 Mar 2014
        m = REG_CONF_DATES_2.match(val)
148
        if m:
149

150
            fmt = "%d-%b-%Y"
151

152 153
            val = "%s-%s-%s" % (m.group(1), m.group(2), m.group(5))
            opening = datetime.strptime(val, fmt)
154

155 156 157 158
            val = "%s-%s-%s" % (m.group(3), m.group(4), m.group(5))
            closing = datetime.strptime(val, fmt)

            return (opening, closing)
159

160
        raise ToolException(MSG_NO_CONF_DATE)
161

162
    def _get_reg_institute(self):
163 164 165 166 167 168
        """Get the regular expression defining the affiliation of my institute.

        It is obtained by concatenating the affiliation keys.
        Affiliation key can contains character like ``(``, ``)`` or ``&``.
        They are replaced by ``\(`` *etc*.

169
        Returns:
170
            str:
171 172 173

        """
        # alias
174
        db = self.db
175 176 177
        app = current.app
        reg_institute = app.reg_institute

178 179 180
        # regular expression for the affiliation keys
        # protect special character
        # add start and end of string for an exact match
181 182
        if not reg_institute:

183 184 185
            lst = []
            for row in db(db.affiliation_keys.id > 0).iterselect():
                val = row.key_u
186

187 188 189 190 191 192 193 194 195 196 197 198 199
                val = (val
                       .replace("(", "\(")
                       .replace(")", "\)")
                       .replace("&", "\&")
                       .replace("$", "\$")
                       .replace("+", "\+")
                       .replace("?", "\?"))

                val = r"(^|\|){}($|\|)" .format(val)

                lst.append(val)

            reg_institute = r"|".join(lst)
200

201 202 203 204 205
        return reg_institute

    def _get_author_rescue_list(self, record, id_project, id_team):
        """Get the rescue list for my authors.

206
        Args:
207 208 209 210 211 212 213 214
            record (RecordPubli):
                record describing a publication.

            id_project (int):
                identifier of the project in the database.

            id_team (int):
                identifier of the team in the database.
215

216
        Returns:
217 218
            list:
                empty when not defined
219 220

        """
221
        year = record.submitted()
222 223 224 225

        # try to recover year when not defined
        if not year:
            # published article, proceeding
226 227
            if record["publication_info"].year.iloc[0] != "":
                year = record["publication_info"].year.iloc[0]
228 229

            # start date of a conference
230 231
            elif record._get("meeting_name", "opening_date") != "":
                year = record._get("meeting_name", "opening_date")
232 233

            # end date of a conference
234 235
            elif record._get("meeting_name", "closing_date") != "":
                year = record._get("meeting_name", "closing_date")
236 237 238 239 240

            else:
                return []

        #
241 242
        # protection
        # submitted and paper year are protect against erratum, but ...
243 244 245 246 247 248 249 250 251 252 253 254 255 256
        #
        if isinstance(year, list):
            year.sort()
            year = year[0]

        # the value can have several format 1992, 1992-12-31, ....
        m = REG_YEAR.search(year)
        if m:
            year = m.group(1)

        else:
            return []

        # caching
LE GAC Renaud's avatar
LE GAC Renaud committed
257
        t = (year, id_project, id_team)
258 259 260 261 262
        if t == self.__par:
            return self.__reference

        # extract the list from the database
        row = self.db.my_authors(year=year,
LE GAC Renaud's avatar
LE GAC Renaud committed
263 264
                                 id_projects=id_project,
                                 id_teams=id_team)
265 266

        if row:
267
            self.__reference = row['authors'].strip("\n"). split(', ')
268 269 270 271
        else:
            self.__reference = []

        return self.__reference
272

273 274 275 276
    def _is_synonym(self, tablename, value):
        """Check that the synonym field contains *value*.

        Args:
277 278
            tablename (str): name of the database table
            value (str): value to be searched
279 280 281 282 283

        Returns:
            bool: ``True`` if *one* row is found, ``False`` otherwise.

        """
284
        query = self.db[tablename].synonyms.contains(value)
285 286 287 288 289
        if db(query).count() == 1:
            return True

        return False

290
    def _recover_submitted(self, record):
291 292 293
        """Recover submitted date using conference, preprint or thesis
        information.

294
        Args:
295 296
            record (RecordPubli):
                record describing a publication.
297

298
        Returns:
299
            str:
300
                target at least YYYY-MM
301
                empty when procedure failed
302 303

        """
304
        val = ""
305
        if isinstance(record, RecordConf):
306

LE GAC Renaud's avatar
LE GAC Renaud committed
307
            opening = self._get_conference_dates(record)[0]
308
            val = opening.strftime("%Y-%m-%d")
309

310
        elif isinstance(record, RecordThesis):
311 312 313 314 315 316 317 318 319
            val = record.these_defense()

        else:
            report = record.preprint_number()
            if report:
                m_arxiv = DECODE_ARXIV.match(report)
                if m_arxiv:
                    val = "20%s-%s" % (m_arxiv.group(1), m_arxiv.group(2))

320
        # last change use the creation date for the record
321 322
        if val == "" or len(val) < 7:
            val = record["creation_date"][0:7]
323

324 325
        return val

326
    def authors(self, record):
LE GAC Renaud's avatar
LE GAC Renaud committed
327
        """Check that:
LE GAC Renaud's avatar
LE GAC Renaud committed
328

LE GAC Renaud's avatar
LE GAC Renaud committed
329 330
            * author fields are defined.
            * first author is not like ATLAS Collaboration
331

332
        Args:
333 334
            record (RecordPubli):
                record describing a publication.
335

336
        Raises:
337 338
            CheckException:
                when there is no authors.
339 340

        """
341
        self.logger.debug(f"{T6}check authors")
342

343
        if not record.is_authors():
344 345
            raise CheckException(MSG_NO_AUTHOR)

LE GAC Renaud's avatar
LE GAC Renaud committed
346 347 348
        if "collaboration" in record.first_author().lower():
            raise CheckException(MSG_FAUTHOR_COLLABORATION)

349
    def collaboration(self, record):
350
        """Check synonyms for collaboration by using by the proper value.
351

352
        Args:
353 354
            record (RecordPubli):
                record describing a publication.
355

356
        Raises:
357
            CheckException:
LE GAC Renaud's avatar
LE GAC Renaud committed
358
                * the collaboration is unknown (neither collaborationnor synonym)
359
                * more than one synonym found.
360
        """
361
        self.logger.debug(f"{T6}check collaboration")
362

363
        val = record.collaboration()
364 365 366
        if not val:
            return

367
        try:
368 369 370 371 372 373 374 375 376 377
            db = self.db
            dbid = search_synonym(db.collaborations, "collaboration", val)

            if dbid == UNDEF_ID:
                raise ToolException(MSG_UNKNOWN_COLLABORATION)

            collaboration = db.collaborations[dbid].collaboration
            if collaboration != val:

                # one collaboration
378 379
                if isinstance(record["corporate_name"], dict):
                    record["corporate_name"]["collaboration"] = collaboration
380 381 382 383

                # several collaboration
                # replace the list of dictionary by a single one
                else:
384 385
                    record["corporate_name"] = \
                        {"collaboration": collaboration}
386

387 388 389
        except ToolException as e:
            raise CheckException(*e.args)

390
    def country(self, record):
391 392 393 394 395 396 397 398 399 400 401
        """Check synonyms for conference country by using by the proper value.

        Args:
            record (RecordPubli):
                record describing a publication.

        Raises:
            CheckException:
                * the country is unknown (neither country nor synonym)
                * more than one synonym found.

402
        """
403
        self.logger.debug(f"{T6}check country")
404

405
        if not isinstance(record, RecordConf):
406 407
            return

408
        val = record.conference_country()
409 410
        if len(val) == 0:
            raise CheckException(MSG_UNKNOWN_COUNTRY)
411 412

        try:
413 414 415 416 417 418 419 420 421
            db = self.db
            dbid = search_synonym(db.countries, "country", val)

            if dbid == UNDEF_ID:
                raise ToolException(MSG_UNKNOWN_COUNTRY)

            country = db.countries[dbid].country

            if country != val:
422
                obj = record["meeting_name"]
423 424

                if isinstance(obj, dict):
425 426
                    location = obj["location"].replace(val, country)
                    record["meeting_name"]["location"] = location
427 428 429

                else:
                    for di in obj:
430 431 432
                        if "location" in di:
                            di["location"] = \
                                di["location"].replace(val, country)
433

434
                    record["meeting_name"] = obj
435

436 437
        except ToolException as e:
            raise CheckException(*e.args)
438

439
    def conference_date(self, record):
LE GAC Renaud's avatar
LE GAC Renaud committed
440
        """Check conference date exists and well formatted.
441

442
        Args:
443 444
            record (RecordConf):
                record describing a talk or a proceeding.
445

446
        Raises:
447 448
            CheckException:
                dates are not found.
449 450

        """
451
        self.logger.debug(f"{T6}check conference date")
452

453 454 455 456
        # conference information are available, i.e proceeding
        if not isinstance(record, RecordConf):
            return

457 458 459
        val = record.conference_dates()
        if len(val) == 0:
            raise CheckException(MSG_NO_CONF_DATE)
460

461 462
        # is it well formed
        if REG_WELL_FORMED_CONF_DATES_1.match(val):
463 464
            return

465 466
        if REG_WELL_FORMED_CONF_DATES_2.match(val):
            return
467

468 469
        # format the date properly
        opening, closing = self._get_conference_dates(record)
470

471
        if opening.month == closing.month:
LE GAC Renaud's avatar
LE GAC Renaud committed
472 473 474 475
            val = "%i-%i %s %i" % (opening.day,
                                   closing.day,
                                   opening.strftime("%b"),
                                   opening.year)
476
        else:
LE GAC Renaud's avatar
LE GAC Renaud committed
477 478 479 480 481
            val = "%i %s - %i %s %i" % (opening.day,
                                        opening.strftime("%b"),
                                        closing.day,
                                        closing.strftime("%b"),
                                        opening.year)
482

483
        meeting = record["meeting_name"]
484
        meeting = (meeting[0] if isinstance(meeting, list) else meeting)
485
        meeting["date"] = val
486

487
    def is_bad_oai_used(self, record):
488 489 490
        """Bad OAI is when the ``id`` in the OAI field is different from
        the ``record id``. This happens when an old record is redirected
        to new one.
491

492
        Args:
493 494
            record (RecordPubli):
                record describing a publication.
495

496
        Returns:
497 498
            bool:
                ``True`` when a record is found in the database with
499
                the bad OAI.
500

501
        """
502
        self.logger.debug(f"{T6}check is bad oai used")
503

504 505 506
        value = record.oai()
        match = REG_OAI.match(value)

LE GAC Renaud's avatar
LE GAC Renaud committed
507
        if int(match.group(2)) != record.id():
508 509
            db = self.db

510
            # a record with the bad OAI exists in the database
511 512 513
            bad_oai_url = OAI_URL % (match.group(1), match.group(2))
            if get_id(db.publications, origin=bad_oai_url):
                return True
514

515
        return False
516

517
    def is_oai(self, record):
518
        """``True`` when the OAI is not defined in the record.
519

520 521 522
        Note:
            make sense only for record from cds.cern.ch or old.inspirehep.net

523 524 525
        Args:
            record (RecordPubli): record describing a publication.

526 527 528
        Returns:
            bool:
                ``True`` when the OAI is not defined in the record.
529
        """
530
        self.logger.debug(f"{T6}check is oai")
531

532 533 534 535
        # make no sense for record from new inspirehep.net (March 2020)
        if record.host() == "inspirehep.net":
            return True

536
        # field / subfield depends on the store
537 538
        test = ("oai" in record and "value" in record["oai"]) or \
               ("FIXME_OAI" in record and "id" in record["FIXME_OAI"])
539

540
        return test
541

542
    def format_authors(self, record, fmt="Last, First"):
543
        """Format the author names.
544

545
        Args:
LE GAC Renaud's avatar
LE GAC Renaud committed
546 547 548
            record (RecordPubli):
                record describing a publication.

549 550
            fmt (str):
                define the format for author names.
LE GAC Renaud's avatar
LE GAC Renaud committed
551 552
                Possible values are ``First, Last``, ``F. Last``, ``Last``,
                ``Last, First`` and ``Last F.``
553 554

        """
555
        self.logger.debug(f"{T6}format authors")
556

557
        record.reformat_authors(fmt)
558

559
    def format_editor(self, record):
560 561
        """Format the editor abbreviation.
        The encoding depends on the store::
562

563 564
            INVENIO:    Phys. Lett. B + volume 673
            INSPIREHEP: Phys.Lett + volume B673
565

566
        Standardise the answer as ``Phys. Lett. B``.
567

568
        Args:
569 570
            record (RecordPubli):
                record describing a publication.
571

572
        Raises:
573 574
            CheckException:
                when the editor is not well formed.
575 576

        """
577
        self.logger.debug(f"{T6}format editor")
578

579 580 581
        if not record.is_published():
            return

582
        df = record["publication_info"].iloc[0]
583

584 585
        editor = df.title
        volume = df.volume
586

587 588
        # add space after the dot  Phys.Rev -> Phys. Rev
        editor = re.sub(r'\.([A-Z])', r'. \1', editor)
589

590 591 592 593 594
        # get the volume letter
        m = re.match(r'([A-Z]+) *(\d+)', volume)
        if m and m.group(1) != editor[-1]:
            editor = "%s %s" % (editor, m.group(1))
            volume = m.group(2)
595

596 597
        # remove stupid mistake
        editor = CLEAN_SPACES(editor)
598

599
        df[["title", "volume"]] = [editor, volume]
600

601 602 603
    def format_universities(self, record):
        """Format the name of the university for PhD:

604 605
            * Fix the name of Aix-Marseille University
            * Replace U. by University
606

607
        Args:
608 609
            record (RecordThesis):
                record describing a thesis.
610 611

        """
612
        self.logger.debug(f"{T6}format university")
613

614
        # protection
615
        if not isinstance(record, RecordThesis):
616 617
            return

618
        values = record["dissertation_note"]["university"]
619

620
        # CPPM -- fix the name of Aix-Marseille university
LE GAC Renaud's avatar
LE GAC Renaud committed
621 622 623 624 625 626
        affiliations = record.first_author_institutes()

        if "CPPM" in affiliations:

            # name of the university depends on the year
            year = re.search(r"(\d{4})", record.these_defense()).group(1)
627 628

            if int(year) < 2012:
LE GAC Renaud's avatar
LE GAC Renaud committed
629
                university = \
630
                    "Université de la Méditerrannée Aix-Marseille II"
631
            else:
632
                university = "Aix Marseille Université"
633

LE GAC Renaud's avatar
LE GAC Renaud committed
634 635 636 637 638 639 640 641 642 643
            # single affiliation
            affiliations = affiliations.split("|")
            if len(affiliations) == 1:
                values = university

            # multiple affiliation are separated by "|"
            else:
                li = [el for el in affiliations if "CPPM" in el]
                if len(li) == 1:
                    values = values.replace(li[0], university)
644

645
        # Other -- replace U. by University
LE GAC Renaud's avatar
LE GAC Renaud committed
646 647
        university = current.T(UNIVERSITY).decode("utf8")
        values = values.replace('U.', university)
648

649
        record["dissertation_note"]["university"] = values
650

651
    def get_my_authors(self, record, sep=", ", sort=False):
652
        """Get authors of my institutes signing the record.
653 654
        The information is append to the Record object via the attribute
        ``my_authors``.
655

656
        Args:
657 658 659
            record (RecordPubli):
                record describing a publication.

660
            sep (str):
661
                string separating author names. The default is the comma.
662

663 664 665
            sort (bool):
                sort authors by family name when true otherwise use the
                order of authors at the creation of the record
666

667
        Returns:
668
            str:
669
                the list of authors separated by the ``sep`` argument.
670

671
        Raises:
672
            CheckException:
LE GAC Renaud's avatar
LE GAC Renaud committed
673
                the list is empty
674 675

        """
676
        self.logger.debug(f"{T6}get my authors")
677

678 679
        # might have been computed when affiliation is checked
        rec_id = record.id()
680 681 682
        if rec_id in self._my_authors:
            li = self._my_authors[rec_id]
            value = sep.join(li)
683 684 685 686

        # find authors of my institute signing the record
        else:
            reg_institute = self.reg_institute
687 688
            value = \
                record.find_authors_by_affiliation(reg_institute, sep, sort)
689

690
        if len(value) == 0:
691 692
            raise CheckException(MSG_NO_MY_AUTHOR)

LE GAC Renaud's avatar
LE GAC Renaud committed
693
        record.my_authors = value
694

695
    def is_conference(self, record):
LE GAC Renaud's avatar
LE GAC Renaud committed
696
        """Check that the record contains conference data.
697

698
        Args:
699 700
            record (RecordPubli):
                record describing a publication.
701

702
        Raises:
703 704
            CheckException:
                the record is not associated to a conference.
705 706

        """
707
        self.logger.debug(f"{T6}is conference")
708

709 710 711
        if not isinstance(record, RecordConf):
            raise CheckException(MSG_NO_CONF)

712
        if "meeting_name" not in record:
LE GAC Renaud's avatar
LE GAC Renaud committed
713 714
            raise CheckException(MSG_NO_CONF)

715
    def is_thesis(self, record):
716
        """Check that the record described a thesis.
717

718
        Args:
719 720
            record (RecordPubli):
                record describing a publication.
721

722
        Raises:
723 724
            CheckException:
                the record does not describe a thesis.
725 726

        """
727
        self.logger.debug(f"{T6}is thesis")
728

729 730 731
        if not isinstance(record, RecordThesis):
            raise CheckException(MSG_NO_THESIS)

732 733 734 735 736 737 738
    def my_affiliation(
            self,
            record,
            id_project,
            id_team,
            fmt_rescue="F. Last",
            sort=False):
739 740 741 742
        """Check that authors of my institute are signatories.

        Launch a recovery procedure when affiliations are not defined.
        It is based on the author rescue list stored in the database.
743

744
        Args:
745 746 747 748 749 750 751 752 753
            record (RecordPubli):
                record describing a publication.

            id_project (int):
                identifier of the project in the database

            id_team (int):
                identifier of the team in the database

754 755
            fmt_rescue (str):
                the format for the authors used in the rescue list
756

757 758 759 760
            sort (bool):
                sort authors by family name when true otherwise use the
                order of authors at the creation of the record

LE GAC Renaud's avatar
LE GAC Renaud committed
761
        Returns:
762 763 764
            str:
                * the found affiliation
                * an empty string when the rescue list is used.
765

766
        Raises:
767 768 769 770
            CheckException:
                when the rescue list is required but empty
                or because the intersection between the rescue list
                and the author is null.
771 772

        """
773
        self.logger.debug(f"{T6}check my affiliation")
774

775
        value = record.find_affiliation(self.reg_institute)
776
        if len(value) > 0:
777
            return value
778

779 780 781 782 783
        # affiliation is not defined
        # try to recover using the authors rescue list
        rescue_list = self._get_author_rescue_list(record, id_project, id_team)
        if not rescue_list:
            raise CheckException(MSG_NO_MY_AUTHOR)
784

785
        # format the author in the same way as the rescue list
786 787 788 789
        fmt_ref = record._last_fmt_author
        record.reformat_authors(fmt_rescue)

        if sort:
790
            authors = (record.df_authors[["last_name", "fmt_name"]]
791 792 793 794
                       .sort_values(by="last_name")
                       .fmt_name)

        else: