automaton.py 21.6 KB
Newer Older
1
""" harvest_tools.automaton
2 3 4 5 6

"""
import re
import traceback

7

8 9 10 11 12
from .base import (MSG_FIX_ORIGIN,
                   MSG_IN_DB,
                   search_synonym,
                   ToolException)
from .checkandfix import CheckAndFix
13
from gluon.storage import Storage
LE GAC Renaud's avatar
LE GAC Renaud committed
14 15
from invenio_tools import (CdsException,
                           InvenioStore,
16
                           OAI_URL)
LE GAC Renaud's avatar
LE GAC Renaud committed
17
from invenio_tools.factory import build_record
18 19
from .msg import Msg
from .msgcollection import MsgCollection
20
from plugin_dbui import CALLBACK_ERRORS, get_id
21

22

23 24 25
MSG_NO_CAT = 'Select a "category" !!!'
MSG_NO_PROJECT = 'Select a "project" !!!'
MSG_NO_TEAM = 'Select a "team" !!!'
26

LE GAC Renaud's avatar
LE GAC Renaud committed
27
MSG_INSERT_FAIL = "Fail to insert the new record in the database."
28

29 30 31 32
# search collection when using inspirehep
# require for "Hal Hidden"
REG_COLLECTION = re.compile(r"cc([A-Za-z ]+)(and|$)")

33

34
class Automaton(object):
35
    """Base class to search and process publications:
36

37
        * Decode the selector defining user criteria.
LE GAC Renaud's avatar
LE GAC Renaud committed
38
        * Search in the store publications matching user criteria.
LE GAC Renaud's avatar
LE GAC Renaud committed
39
        * Instantiate the record and check it.
40
        * Insert new records in the database.
41

42 43
    Note:
        The parameters of the search are defined by the current ``request``.
44

45 46 47
    The logic implements in the ``Automaton`` class is the following:

        #. Ask to the store, all the `record_id` satisfying the user request.
LE GAC Renaud's avatar
LE GAC Renaud committed
48 49
        #. Reject `record_id` contains in the *origin* field of a
           database entry.
LE GAC Renaud's avatar
LE GAC Renaud committed
50
        #. Request to the store, the JSON description of the publications
LE GAC Renaud's avatar
LE GAC Renaud committed
51 52 53 54
           and decode them.
        #. Reject the record for which the *secondary_oai_url* is contained in
           the *origin* field of a database entry. Update the *origin* field
           of the database record.
55
        #. Check that the *oai* of the publication is defined and well formed.
LE GAC Renaud's avatar
LE GAC Renaud committed
56 57
           Recover it, if it is not the case. At this stage the OAI is always
           defined.
58 59
        #. Reject temporarily publication.
        #. Check that *authors* are defined.
60
           Reject the publication if it is not the case.
61
        #. Check that *my institute* is in the list of the institutes
62 63 64 65 66 67
           signing the publication. Reject the publication if it is
           not the case. When the affiliation are not defined,
           try to recover this case, by finding the author of my institute
           signing the publication. This recovery procedure uses
           the *author rescue list*. Reject the record when the recovery
           procedure failed.
68
        #. Check that the *collaboration*, if defined, is well formed.
69
           Reject the publication if it is not the case
70 71 72 73 74
        #. Several check are applied depending on the publication type.
        #. At the end of this process, the publisher, the authors are
           formatted and the list of signatories of my institute extracted.

    Args:
LE GAC Renaud's avatar
LE GAC Renaud committed
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
        db (gluon.DAL):
            the database connection.

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

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

        automaton (unicode):
            the name of the automaton which will be used to process the data.
            Possible values are: ``articles``, ``notes``, ``preprints``,
            ``proceedings``, ``reports``, ``talks`` and ``theses``.

        id_category (int):
            the identifier of the category of publication

        year_start (int):
            starting year for the scan

        year_end (int):
            ending year of the scan

        dry_run (bool):
            new records are not inserted in the database when ``True``.

        debug (bool):
            activate the verbose mode when ``True``.
103 104

    Raises:
LE GAC Renaud's avatar
LE GAC Renaud committed
105 106
        ToolException:
            * team or project or the publication category not defined
107

108 109 110 111 112
    """
    def __init__(self,
                 db,
                 id_team,
                 id_project,
113
                 automaton,
114 115 116 117 118 119 120
                 id_category,
                 year_start=None,
                 year_end=None,
                 dry_run=True,
                 debug=False):

        # protection team, project and/or category have to be defined
LE GAC Renaud's avatar
LE GAC Renaud committed
121
        if not id_team:
122 123
            raise ToolException(MSG_NO_TEAM)

LE GAC Renaud's avatar
LE GAC Renaud committed
124
        if not id_project:
125 126
            raise ToolException(MSG_NO_PROJECT)

LE GAC Renaud's avatar
LE GAC Renaud committed
127
        if not id_category:
128 129
            raise ToolException(MSG_NO_CAT)

130
        self.check = CheckAndFix(debug)
LE GAC Renaud's avatar
LE GAC Renaud committed
131 132 133 134 135 136 137 138 139 140 141 142 143
        self.collection_logs = []
        self.controller = automaton
        self.db = db
        self.dbg = debug
        self.dry_run = dry_run
        self.id_category = id_category
        self.id_team = id_team
        self.id_project = id_project
        self.logs = []
        self.store = None
        self.year_start = year_start
        self.year_end = year_end

144
        # Construct harvester Storage needed for the log
LE GAC Renaud's avatar
LE GAC Renaud committed
145 146 147 148
        self.harvester = Storage(id_teams=id_team,
                                 id_projects=id_project,
                                 controller=automaton,
                                 id_categories=id_category)
149

150 151 152 153 154
        # Identifier of the categories preprint and articles
        # Used by the method _is_record_in_db
        self._id_preprint = get_id(db.categories, code="PRE")
        self._id_article = get_id(db.categories, code="ACL")

155 156 157
    def _insert_in_db(self, log_year="", **fields):
        """Insert the record in the database, handling database exception.

158
        Args:
159
            log_year (str): year of the record for the log
160

161
        Keyword Args:
LE GAC Renaud's avatar
LE GAC Renaud committed
162 163
            **fields:
                keyword arguments defining the record values to be
164
                inserted in the database.
165

166
        Returns:
LE GAC Renaud's avatar
LE GAC Renaud committed
167 168
            int:
                one when the record is inserted / updated in the database,
169
                zero otherwise.
170 171 172 173 174

        """
        db = self.db

        try:
LE GAC Renaud's avatar
LE GAC Renaud committed
175
            rec_id = db.publications.insert(**fields)
LE GAC Renaud's avatar
LE GAC Renaud committed
176 177
            if rec_id:
                return 1
178

LE GAC Renaud's avatar
LE GAC Renaud committed
179
            # operation can be reject by callback table._before_insert
LE GAC Renaud's avatar
LE GAC Renaud committed
180
            else:
LE GAC Renaud's avatar
LE GAC Renaud committed
181
                msg = MSG_INSERT_FAIL
LE GAC Renaud's avatar
LE GAC Renaud committed
182 183
                if CALLBACK_ERRORS in db.publications:
                    msg = db.publications._callback_errors
184

LE GAC Renaud's avatar
LE GAC Renaud committed
185 186 187
                # reduce the error message
                if isinstance(msg, list):
                    msg = "%s %s" % (msg[0], msg[-1])
188

LE GAC Renaud's avatar
LE GAC Renaud committed
189 190
                self.logs[-1].reject(msg, log_year)
                return 0
191

LE GAC Renaud's avatar
LE GAC Renaud committed
192 193
        # operation can be rejected by the database
        except Exception as dbe:
194
            self.logs[-1].reject(str(dbe), log_year)
LE GAC Renaud's avatar
LE GAC Renaud committed
195
            return 0
196

LE GAC Renaud's avatar
LE GAC Renaud committed
197 198 199 200 201 202 203
    def _is_record_in_db(self,
                         collection_title,
                         host=None,
                         rec_id=None,
                         oai_url=None):
        """Return the database identifier when the publication is registered.
        The search is based on the ``origin`` field and on the primary OAI.
204

205 206
        Note:
            A new log entry is created when a record is found.
207

208
        Args:
209
            title (str): the title of the publication.
210 211

        Keyword Args:
LE GAC Renaud's avatar
LE GAC Renaud committed
212 213
            host (unicode):
                the store. possible values are ``cds.cern.ch`` or
214 215
                ``inspirehep.net``. To be used with *rec_id*.

LE GAC Renaud's avatar
LE GAC Renaud committed
216 217 218 219 220 221
            rec_id (int):
                the record identifier in the store

            oai_url (unicode):
                the URL of the record in the store.
                Either use *host* and *rec_id* or *oai_url*
222

223
        Returns:
LE GAC Renaud's avatar
LE GAC Renaud committed
224 225
            int:
                the id of the record in the database when a record is found,
226
                0 otherwise.
227

228
        Raises:
LE GAC Renaud's avatar
LE GAC Renaud committed
229 230
            ValueError:
                * keyword arguments are not defined properly.
231

232 233
        """
        db = self.db
234
        harvester = self.harvester
235

236 237 238 239 240 241 242 243
        # build the OAI URL
        if host is not None and rec_id is not None and oai_url is None:
            url = OAI_URL % (host, rec_id)
        elif host is None and rec_id is None and oai_url is not None:
            url = oai_url
        else:
            raise ValueError

LE GAC Renaud's avatar
LE GAC Renaud committed
244
        # protection empty URL
245 246 247
        if len(url) == 0:
            return 0

248 249 250
        # check the OAI
        query = db.publications.origin.contains(url)
        setrows = db(query)
251

252
        if setrows.count() == 0:
253
            return 0
254

255
        # one record found
256 257
        columns = [db.publications.id,
                   db.publications.id_categories,
258 259 260
                   db.publications.title,
                   db.publications.year]
        publication = setrows.select(*columns).first()
261

262 263
        # Note:
        # The category for the publication and the harvester have to be equal.
264 265 266 267 268 269 270
        # However, keep the record if it is a preprint when the harvester
        # looks for articles. This is required to transform a preprint
        # into article
        #
        # Category can disagree when the publication is an article and
        # the harvester look for preprint. In that case, keep the article
        #
271
        if publication.id_categories != harvester.id_categories:
272 273 274 275 276 277 278

            is_preprint_to_article = \
                publication.id_categories == self._id_preprint \
                and harvester.id_categories == self._id_article

            if is_preprint_to_article:
                return 0
279 280

        # log
281
        self.logs.append(Msg(harvester=harvester,
LE GAC Renaud's avatar
LE GAC Renaud committed
282
                             collection=collection_title,
283 284 285 286 287
                             record_id=rec_id,
                             title=publication.title))

        self.logs[-1].idle(MSG_IN_DB, publication.year)

LE GAC Renaud's avatar
LE GAC Renaud committed
288 289 290
        if self.dbg:
            print("\trecord already in db:", rec_id, "->", publication.id)

291
        return publication.id
292

293 294 295 296 297
    def _search_parameters(self, collection):
        """Build the keywords to steer the URL search in invenio store.
        The main parameter is the collection and the date range defined
        in the selector.

298
        Args:
LE GAC Renaud's avatar
LE GAC Renaud committed
299 300 301
            collection (unicode):
                string defining the collection in the store.
                The syntax depends on the invenio store:
302 303 304

                    * ``"find cn d0 and tc p and not tc c"``
                    * ``"LHCb Papers"``.
305

306
        Returns:
LE GAC Renaud's avatar
LE GAC Renaud committed
307 308 309
            dict:
                the key are a sub-set of those defined in
                :meth:`invenio_tools.InvenioStore.get_ids`.
310 311

        """
LE GAC Renaud's avatar
LE GAC Renaud committed
312 313
        year_start = self.year_start
        year_end = self.year_end
314 315

        # INSPIREHEP store
LE GAC Renaud's avatar
LE GAC Renaud committed
316
        if collection.startswith("find"):
317 318 319

            query = collection

LE GAC Renaud's avatar
LE GAC Renaud committed
320 321
            if year_start and not year_end:
                query += " and date %s" % year_start
322

LE GAC Renaud's avatar
LE GAC Renaud committed
323 324
            elif not year_start and year_end:
                query += " and date %s" % year_end
325

LE GAC Renaud's avatar
LE GAC Renaud committed
326
            elif year_start and year_end:
327
                query += " and date > %s and date < %s " \
LE GAC Renaud's avatar
LE GAC Renaud committed
328
                         % (year_start - 1, year_end + 1)
329 330 331

            dic = dict(p=query,  # query à la spires
                       rg=1000,  # maximum number of records returned
LE GAC Renaud's avatar
LE GAC Renaud committed
332 333
                       sf="year",  # sort by date
                       so="d")  # descending order
334

335 336 337 338 339 340 341 342 343
            # handle the cc keyword (true inspirehep collection)
            match = REG_COLLECTION.search(query)
            if match:
                dic["cc"] = match.group(1).strip()
                dic["p"] = REG_COLLECTION.sub("", query).strip()
                dic["p"] = dic["p"].replace("  ", " ")
                if dic["p"] == "find":
                    del dic["p"]

344 345 346
        # CERN INVENIO store
        else:

LE GAC Renaud's avatar
LE GAC Renaud committed
347 348
            if year_start and not year_end:
                rex = year_start
349

LE GAC Renaud's avatar
LE GAC Renaud committed
350 351
            elif not year_start and year_end:
                rex = year_end
352

LE GAC Renaud's avatar
LE GAC Renaud committed
353 354
            elif year_start and year_end:
                li = [str(el) for el in xrange(year_start, year_end + 1)]
LE GAC Renaud's avatar
LE GAC Renaud committed
355
                rex = "|".join(li)
356 357

            dic = dict(cc=collection,  # collection
LE GAC Renaud's avatar
LE GAC Renaud committed
358 359
                       f1="year",  # search on year
                       m1="r",  # use regular expression
360
                       p1=rex,  # regular expression defining year
LE GAC Renaud's avatar
LE GAC Renaud committed
361 362
                       sf="year",  # sort by date
                       so="d")  # descending order
363 364
        return dic

LE GAC Renaud's avatar
LE GAC Renaud committed
365
    def check_record(self, record):
366 367
        """Check the content of the record in order to fix non-conformities.
        Return ``False`` when non-conformities are found and can not be
368 369
        corrected.

370 371 372
        Note:
            Some checks depend on the type of publications and have to be
            implemented in inherited class.
373

374
        Note:
LE GAC Renaud's avatar
LE GAC Renaud committed
375
            The order of the checks matter. It should be OAI,
376 377
            temporary record, authors, my authors and then a series of checks
            specific to the publication type.
378

379
        Args:
LE GAC Renaud's avatar
LE GAC Renaud committed
380 381
            record (Record):
                JSON record describing the publication.
382

383
        Returns:
LE GAC Renaud's avatar
LE GAC Renaud committed
384 385
            bool:
                ``False`` when a non-conformity is found and can not be
386
                corrected.
387 388 389

        """
        if self.dbg:
390
            print("check record")
391 392

        try:
393
            self.check.is_oai(record)
394

395
            if self.check.is_bad_oai_used(record):
LE GAC Renaud's avatar
LE GAC Renaud committed
396
                self.logs[-1].idle(MSG_IN_DB, record.submitted())
397 398
                return False

399 400
            self.check.temporary_record(record)
            self.check.authors(record)
401
            self.check.my_affiliation(record, self.id_project, self.id_team)
402 403 404
            self.check.collaboration(record)

        except Exception as e:
405
            self.logs[-1].reject(e, record=record)
406 407 408 409
            return False

        return True

410
    def get_record_by_fields(self, oai_url, year, **kwargs):
411 412
        """Get database record matching fields values defined
        in the keyword arguments.
413

414
        Note:
415 416
            This method is required to deal with publication entered by hand
            and found later by an harvester.
417

418
        Args:
LE GAC Renaud's avatar
LE GAC Renaud committed
419 420 421 422
            oai_url (unicode):
                the oai_url, *e.g.* ``http://cds.cern.ch/record/123456``.
                The origin field of the existing database record is update to
                **oai_url** when a match is found.
423

LE GAC Renaud's avatar
LE GAC Renaud committed
424 425
            year (int):
                the year of the publication. It is used
426 427 428
                by the search algorithm and by the logger.

        Keyword Args:
LE GAC Renaud's avatar
LE GAC Renaud committed
429 430 431
            kwargs (unicode):
                 a series of key, value pair where the key is the name of a
                 publications database field.
432

433
        Returns:
LE GAC Renaud's avatar
LE GAC Renaud committed
434 435 436 437 438
            tuple:
                ``(id, status)`` which contains the ``id`` of the record.
                The ``id`` is equal to ``None`` when there is no matching.
                The ``status`` is equal to one when the existing record was
                modified zero otherwise.
439 440 441

        """
        if self.dbg:
442
            print("get existing record by fields")
443

444
        # alias
445
        db = self.db
446
        logs = self.logs
447

448 449 450
        # add the publication year to search criteria
        if year:
            kwargs["year"] = year
451 452 453 454 455 456 457

        # look for an existing record
        rec_id = get_id(db.publications, **kwargs)
        if not rec_id:
            return (None, 0)

        # fix origin field
458 459
        publication = db.publications[rec_id]
        ok = publication.origin and publication.origin == oai_url
460 461
        if not ok:
            if not self.dry_run:
462
                publication = dict(origin=oai_url)
463

464
            logs[-1].modify(MSG_FIX_ORIGIN, year)
465 466
            return (rec_id, 1)

467
        logs[-1].idle(MSG_IN_DB, year)
468 469
        return (rec_id, 0)

470 471
    def insert_record(self, record):
        """Insert the record in the database.
472

473 474 475
        Note:
            This method depend on the type of publications.
            It has to be implemented for each inherited class.
476

477
        Args:
LE GAC Renaud's avatar
LE GAC Renaud committed
478 479
            record (Record):
                record describing the publication.
480

481
        Returns:
LE GAC Renaud's avatar
LE GAC Renaud committed
482 483
            int:
                one when the record is inserted / updated in the database,
484
                zero otherwise.
485 486 487 488

        """
        return 0

LE GAC Renaud's avatar
LE GAC Renaud committed
489 490 491
    def process_collection(self, collection):
        """"Retrieve JSON objects from the invenio store and for the given
        collection. Corresponding records are inserted in the database.
492

493
        Args:
LE GAC Renaud's avatar
LE GAC Renaud committed
494 495
            collection (unicode):
                name of the collection to be interrogated.
496

497
        Raises:
LE GAC Renaud's avatar
LE GAC Renaud committed
498 499 500 501 502
            CdsException:
                * keyword argument is invalid;
                * the server return an HTTP error;
                * JSON object can't be decoded
                * not well formed list of ids.
503 504 505

        """
        if self.dbg:
LE GAC Renaud's avatar
LE GAC Renaud committed
506
            print("\nprocess collection", collection)
507 508

        # alias
509
        collection_logs = self.collection_logs
510
        controller = self.controller
LE GAC Renaud's avatar
LE GAC Renaud committed
511
        host = self.harvester.host
512
        project = self.db.projects[self.id_project].project
LE GAC Renaud's avatar
LE GAC Renaud committed
513
        store = self.store
514

515 516
        # log collection information
        # A collection is identified as "Project Controller collection"
LE GAC Renaud's avatar
LE GAC Renaud committed
517 518
        ctitle = "%s / %s / %s" % (project, controller, collection)
        collection_logs.append(MsgCollection(title=ctitle))
519

LE GAC Renaud's avatar
LE GAC Renaud committed
520
        # get search parameters for the collection including user criteria
521
        kwargs = self._search_parameters(collection)
522

LE GAC Renaud's avatar
LE GAC Renaud committed
523
        # get the list of record identifier matching the search criteria
524 525
        try:
            rec_ids = store.get_ids(**kwargs)
526

LE GAC Renaud's avatar
LE GAC Renaud committed
527
        except CdsException as error:
528 529 530
            collection_logs[-1].url = store.last_search_url()
            collection_logs[-1].error = error
            return
531

LE GAC Renaud's avatar
LE GAC Renaud committed
532
        # log the number of record found for the collection
533 534
        collection_logs[-1].url = store.last_search_url()
        collection_logs[-1].found = len(rec_ids)
535

LE GAC Renaud's avatar
LE GAC Renaud committed
536 537 538
        if len(rec_ids) == 0:
            if self.dbg:
                print("\tNo records found in %s" % collection)
539
            return
540

541
        if self.dbg:
LE GAC Renaud's avatar
LE GAC Renaud committed
542
            print("\t%i records found in %s" % (len(rec_ids), collection))
543

LE GAC Renaud's avatar
LE GAC Renaud committed
544 545 546 547
        # remove form the list identifier already registered in the data base
        # and log them
        func = self._is_record_in_db
        rec_ids = [el for el in rec_ids if func(ctitle, host, el) == 0]
548

LE GAC Renaud's avatar
LE GAC Renaud committed
549 550 551 552 553 554 555 556 557 558 559
        # process the remaining identifiers
        [self.process_recid(rec_id) for rec_id in rec_ids]

    def process_recid(self, rec_id):
        """Process the publication:

            * get the publication data from the store using its identifier
            * instantiate the record (RecordPubli, REcordConf, RecordThesis)
            * process OAI data
            * check the record
            * insert new record in the database
560

561
        Args:
LE GAC Renaud's avatar
LE GAC Renaud committed
562 563
            rec_id (int):
                identifier of the publication in the store.
564

LE GAC Renaud's avatar
LE GAC Renaud committed
565 566 567 568
        Raise:
            CdsException:
                * the server return an HTTP error.
                * no JSON object could be decoded.
569

LE GAC Renaud's avatar
LE GAC Renaud committed
570
        """
571
        if self.dbg:
LE GAC Renaud's avatar
LE GAC Renaud committed
572
            print("\nprocessing record", rec_id)
573

LE GAC Renaud's avatar
LE GAC Renaud committed
574 575
        collection_logs = self.collection_logs
        harvester = self.harvester
576 577
        logs = self.logs

LE GAC Renaud's avatar
LE GAC Renaud committed
578 579 580 581
        # instantiate the record
        try:
            recjson = self.store.get_record(rec_id)
            record = build_record(recjson)
582 583

            if self.dbg:
LE GAC Renaud's avatar
LE GAC Renaud committed
584
                print("\t", record.title())
585

LE GAC Renaud's avatar
LE GAC Renaud committed
586 587 588 589 590 591 592 593
        except Exception as e:
            print(traceback.format_exc())
            url = OAI_URL % (harvester.host, rec_id)
            logs.append(Msg(harvester=harvester,
                            collection=collection_logs[-1].title,
                            record_id=rec_id,
                            title=url))
            logs[-1].reject(e)
LE GAC Renaud's avatar
LE GAC Renaud committed
594
            return
LE GAC Renaud's avatar
LE GAC Renaud committed
595 596 597 598 599 600 601 602 603 604

        # start the log for the record
        logs.append(Msg(harvester=harvester,
                        collection=collection_logs[-1].title,
                        record_id=record.id(),
                        title=record.title()))

        # check that the record is well formed
        # repair non-conformity as far as possible
        if not self.check_record(record):
605
            if self.dbg:
LE GAC Renaud's avatar
LE GAC Renaud committed
606 607 608 609 610
                print("\trecord rejected", logs[-1].txt)
                return

        if self.dbg:
            print("\tinsert record in the database")
611

LE GAC Renaud's avatar
LE GAC Renaud committed
612 613
        # insert the record in the database
        self.insert_record(record)
614

LE GAC Renaud's avatar
LE GAC Renaud committed
615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658
        if self.dbg:
            log = logs[-1]
            action = log.action
            action = (action.upper() if isinstance(action, str) else action)
            print("\tlog:", action, log.txt)

    def process_url(self, host, collections):
        """Retrieve JSON objects from the invenio store and
        insert corresponding records in the database.

        Args:
            host (unicode):
                host name to query for publications, either
                ``cds.cern.ch`` or ``inspirehep.net``.

            collections (unicode):
                list of collection to be interrogated.

        Raises:
           StoreException:
               when something goes wrong interrogating the store.

           CheckException:
               when the record has non-conformities.

           Exception:
               when the python code crashes.

        """
        if self.dbg:
            print("process URL search")

        # extend harvester for logs
        self.harvester.host = host
        self.harvester.collections = collections

        # instantiate the store
        self.store = InvenioStore(host)

        # list of collections
        collections = re.sub(" *, *", ",", collections).split(",")

        # process
        [self.process_collection(collection) for collection in collections]
659 660 661 662

    def report(self):
        """Build the processing report.

663 664
        Returns:
            dict:
LE GAC Renaud's avatar
LE GAC Renaud committed
665 666
                * ``collection_logs`` list of :class:`MsgCollection`
                * ``controller`` unicode
667
                * ``logs`` list of :class:Msg
LE GAC Renaud's avatar
LE GAC Renaud committed
668
                * ``selector`` :class:`plugin_dbui.Selector`
669 670 671 672 673 674

        """

        return dict(collection_logs=self.collection_logs,
                    controller=self.controller,
                    logs=self.logs)