# `MishkaCmsMigration.Report`
[🔗](https://github.com/mishka-group/mishka_cms_migration/blob/v0.0.1/lib/mishka_cms_migration/report.ex#L5)

What a dry run or an import did with each item, saved on the run
(`MishkaCmsMigration.MigrationRun`'s `report`) and handed to `migration:import:finished`.

    %{
      counts: %{created: 2, updated: 1, skipped: 1, failed: 0, undecided: 0},
      guests: 0,
      kinds: [%{kind: "post", created: 2, updated: 1, skipped: 1, failed: 0, undecided: 0}],
      entries: [
        %{kind: "post", source_id: "12", title: "Hello", outcome: :skipped, reason: "unchanged"},
        ...
      ]
    }

The outcome of an entry is one of `outcomes/0`. In an import it is what was done. In a dry run it
is the plan, from what the source and earlier runs say, which the dry run also records on each
item (`MishkaCmsMigration.MigrationItem`'s `plan`); a dry run asks no plugin and does not run
the CMS action, so an item a `migration:item:before_import` plugin would skip, or one the CMS
would refuse (an address another source already took, a value its validations reject), reads as
created or updated in the dry run and as skipped or failed in the import.

`reason` says why an item was skipped or failed, and, for an item created or updated, what was
written otherwise than the source has it (`MishkaCmsMigration.Workers.Helpers.Writer`), `nil` when
nothing was: its target's note (`MishkaCmsMigration.Target`'s `note/1`, such as
`"draft_from_pending"`), then the pieces of its content no plugin converted, kept as escaped text
(`"unconverted: "` and their names), joined by `"; "`. For a skipped or failed item:

  * `"left_out_by_admin"` — an admin left the item out on the Items page; a CMS record an
    import wrote for it is kept as it is.
  * `"parent_left_out"` — the item sits under one an admin left out, however deep; a CMS record an
    import wrote for it is kept as it is.
  * `"mapped_missing"` — an admin mapped the item to a CMS record that has since been deleted.
  * `"unchanged"` — the source has not changed the item since it was last written.
  * `"edited_in_cms"` — its CMS record was changed in the CMS after it was written, and is kept.
  * `"target_missing"` — its CMS record was deleted in the CMS after it was written, and an admin
    decided to leave it out (`"deleted_in_cms"`, below), so it stays deleted.
  * `"left_out_by_admin"` too for an item whose CMS record an admin archived and decided to leave
    archived (`"archived_in_cms"`, below).
  * `"no_target"` — no target is registered for the item's kind (`MishkaCmsMigration.Target`).
  * `"waits_for_blog"` — the Blog is not installed, and an admin decided the item waits for it.
  * `"waits_for_admin"` — the admin who ran the import may not allow the item's video player on
    the site, and an admin decided it waits for one who may.
  * `"waits_for_language_urls"` — the links would not match the site's language addresses, and an
    admin decided the item waits until they do.
  * `"duplicate_in_source"` — the source listed this id of this kind before; the first is used.
  * `"parent_undecided: <parent>"`, `"parent_missing: <parent>"` or `"parent_not_listed: <id>"` —
    the item sits under a parent that has no CMS record, so it is left out until a run writes the
    parent: the parent waits for an admin's decision, was never written, was skipped or failed,
    or the source does not list it. `<parent>` names it as the Items page shows it,
    `"Team (#25)"` (`MishkaCmsMigration.Workers.Helpers.Needs.parent/2`).
    An admin's Import anyway (`MishkaCmsMigration.MigrationItem`'s `"parent_missing"` answered
    with `"import"`) writes it without its parent instead, noted `"without_parent: <parent>"`.
  * a plugin's own reason, from `migration:item:before_import`; `"plugins_cannot_run"` while one
    of the plugins of that event, or of `migration:content:convert` for an item with a piece to
    convert, is not loaded; `"plugins_failed"` when one of them raised or answered something that
    is not the payload (`MishkaCmsMigration.Events`).
  * for a failure, what the source or the CMS action answered.

An item is `:undecided` when the import cannot place it safely on its own: its target found a
conflict on the site (`MishkaCmsMigration.Target`'s `existing/2`) that only an admin can settle,
and no admin has answered it yet (`MishkaCmsMigration.MigrationItem`'s `decisions`). Nothing is
written for it, and every later dry run and import asks again until an admin decides. Its reason
is the conflict:

  * `"site_has_own"` — the site already has a value of its own for the setting
    (`MishkaCmsMigration.Targets.CmsSetting`), or a stylesheet of its own with the item's name
    (`MishkaCmsMigration.Targets.CmsStylesheet`);
  * `"menu_goes_to"` — a menu that is not the site's main one waits for the admin to say where
    it goes, `"menu_goes_to: <menu>"` when a block menu has the same links as the classic menu
    `<menu>` (`MishkaCmsMigration.Targets.CmsLayout`);
  * `"header_has_links"` — the site layout's header already has links of its own, where a menu
    goes (`MishkaCmsMigration.Targets.CmsLayout`);
  * `"footer_has_links"` — the same for the footer's free slot, where a menu goes that the admin
    sent to the footer;
  * `"component_name_taken"` — the site, or a menu of the same run, already has a component with
    the name a menu sent to a component of its own would take (`"component_written_first"` while
    the other menu has not written it yet) (`MishkaCmsMigration.Targets.CmsLayout`);
  * `"slug_taken"` — the site already has a category or a tag with the item's slug, or a page at
    its path (`MishkaCmsMigration.Targets.Helpers.BySlug`, `MishkaCmsMigration.Targets.CmsPage`);
  * `"blog_missing"` — the item is of a kind the Blog writes, and the Blog is not installed
    (`MishkaCmsMigration.Application.blog_kinds/0`);
  * `"player_not_allowed"` — the item's content frames a YouTube or Vimeo player the site does
    not allow, and the admin who ran it may not allow it
    (`MishkaCmsMigration.Targets.Helpers.Frames`);
  * `"draft_pages: <labels>"` — the main menu links pages the site does not serve, as the drafts
    pages come in as, named by the links' labels (`MishkaCmsMigration.Targets.CmsLayout`): the
    conflict is the part before `": "`;
  * `"language_urls: <cause>"` — the links the item writes would not follow the source's or the
    site's language addresses, `<cause>` being `polylang`, `wpml` or `prefix_all`
    (`MishkaCmsMigration.Targets.Helpers.LanguageUrls`).
  * `"archived_in_cms"` — an admin archived the CMS record the item was written or mapped to, so
    writing it, or anything that points at it, would be refused
    (`MishkaCmsMigration.Workers.Helpers.Decision`). Restore it brings the record back, and the
    item is updated, its note led by `"restored"`; what points at it waits meanwhile.
  * `"deleted_in_cms"` — the CMS record the item was written to was deleted for good, so
    anything that points at it would lose it (`MishkaCmsMigration.Workers.Helpers.Decision`).
    Import it again creates a new record, as a first import does, and the item remembers it, once:
    should the new record be deleted too, the admin is asked again; Leave it out skips it as
    `"target_missing"` from then on; what points at it waits meanwhile.

`duplicates` counts the media files that are the same bytes as an earlier one
(`MishkaCmsMigration.Targets.CmsMedia`): `found` the ones the run met, `merged` those an admin
decided to use the original for (`"same_file_used"`, `"same_file_used_no_redirect"`) and `kept`
those decided to keep as their own (`"same_file_kept"`), the rest waiting for the decision
(`"same_file: <title of the original>"`, an `:undecided` item). A dry run downloads nothing and
counts none.

`guests` counts the comments written, or planned, as a guest's (`"guest_comment: <name>"`,
possibly followed by `"guest_email_dropped"`, the guest's email the Blog would refuse, left out
of the comment) (`MishkaCmsMigration.Targets.BlogComment`).

An item a job of picked items met (`MishkaCmsMigration.Workers.Helpers.Pipeline`) waits, in a
dry run and in an import, when what it points at has no CMS record yet:

  * `"parent_undecided: <parent>"`, `"parent_missing: <parent>"` or `"parent_not_listed: <id>"`
    — its parent, which the job was not asked for, was never imported, as above;
  * `"post_missing"` — the post it points at, such as a comment's, was never imported;
  * `"waits_for: <kinds>"` — an item of another kind it points at, such as a post's category,
    was never imported, failed or waits for an admin's decision
    (`MishkaCmsMigration.Target`'s `needs/1`), `<kinds>` naming those kinds, `"category, tag"`.

A full run's item waits as `"waits_for: <kinds>"` too, for what it points at that waits for an
admin's decision on its archived CMS record (`"archived_in_cms"`), or on one deleted for good
(`"deleted_in_cms"`).

`counts` and `kinds` count every item. `kinds` has a count for each kind that had at least one
item, in the order the kinds were walked; a kind the source listed nothing of has none.
`entries` are the last 1,000 items met (`max_entries/0`), in the order they were settled (an item
that waits for its parent comes right after it), so a report stays small however large the site:
the items left out number the sum of `counts` less the length of `entries`, and every item is
still recorded (`MishkaCmsMigration.MigrationItem`).

A job of picked items does not replace its run's report: `merge/2` puts what it did in it.

# `entry`

```elixir
@type entry() :: %{
  kind: String.t(),
  source_id: String.t() | nil,
  title: String.t() | nil,
  outcome: outcome(),
  reason: String.t() | nil
}
```

One item's entry.

# `outcome`

```elixir
@type outcome() :: :created | :updated | :skipped | :failed | :undecided
```

What happened, or would happen, to an item.

# `t`

```elixir
@type t() :: %{
  :counts =&gt; %{required(outcome()) =&gt; non_neg_integer()},
  :kinds =&gt; [map()],
  :entries =&gt; [entry()],
  :guests =&gt; non_neg_integer(),
  :duplicates =&gt; %{
    found: non_neg_integer(),
    merged: non_neg_integer(),
    kept: non_neg_integer()
  },
  optional(:kept) =&gt; non_neg_integer()
}
```

# `add`

```elixir
@spec add(t(), entry()) :: t()
```

`report` with `entry` added. Call `done/1` once every entry is in.

# `done`

```elixir
@spec done(t()) :: t()
```

`report` as it is saved: its last `max_entries/0` entries, in the order they were added.

# `entry`

```elixir
@spec entry(
  String.t(),
  String.t() | nil,
  String.t() | nil,
  outcome(),
  String.t() | nil
) :: entry()
```

One item's entry: its kind, source id and title, its outcome and why.

# `max_entries`

```elixir
@spec max_entries() :: pos_integer()
```

How many entries a report keeps: the last ones added.

# `merge`

```elixir
@spec merge(map() | nil, t()) :: map()
```

`report`, a run's saved report, with the entries of `picked`, the saved report of a job of the
run's picked items, put in, in the order `picked` met them: each one replaces the entry of the
same kind and source id, whose outcome it no longer counts, and is counted as `add/2` counts it;
one whose item the report does not list among its entries (the report never met it, or it is
older than the last `max_entries/0`) is counted as one more. With no report yet, `picked` is the
report.

# `new`

```elixir
@spec new() :: t()
```

A report with nothing in it.

# `outcomes`

```elixir
@spec outcomes() :: [outcome()]
```

The outcomes an entry may have.

# `reason`

```elixir
@spec reason(term()) :: String.t()
```

The words an entry or a failed run keeps for why, at most 1000 characters: for an Ash error, each
of its errors as `field: message`, translated with its values filled in
(`MishkaGervaz.Errors.translate_error/2`, as the core's screens say them), and `"forbidden"` for
a refusal by a policy; any other exception's message; a string as it is; an atom's name;
guarded_struct's errors as `field: message` pairs; anything else inspected.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
