Pāriet uz saturu
ShopentlyLearn

Blogs · Pilns piemērs

Raksti, autori, moderēti komentāri un plānota publicēšana. cURL, PHP, Python.

Pārskats

Veidojiet veikala blogu ar HTML rakstiem, fotoattēliem, autoriem un meklētājprogrammu metadatiem. Saglabājiet melnrakstus, sagatavojiet tulkojumus un publicējiet uzreiz vai izvēlētā laikā noteiktā laika joslā. Pārvaldiet komentārus un moderēšanu panelī; ārējs ģenerators var izmantot API ar piešķirtajām tiesībām.

  1. Autentifikācija
  2. Jauns autors
  3. Augšupielādēt rakstu fotoattēlus
  4. Jauns raksts
  5. Rediģēt rakstu
  6. Ieplānot publicēšanu
  7. Lasīt rakstus un priekšskatīt melnrakstus

Nepieciešamās atļaujas

BASIC · api.enabled · addons.use(shopently.blog)

  • addon.shopently.blog:author_create — Veidot rakstu autorus
  • addon.shopently.blog:media_upload — Augšupielādēt rakstu fotoattēlus
  • addon.shopently.blog:post_create — Veidot rakstu melnrakstus
  • addon.shopently.blog:post_update — Rediģēt rakstu melnrakstus un tulkojumus
  • addon.shopently.blog:post_publish — Publicēt, plānot un arhivēt rakstus
  • addon.shopently.blog:post_read — Lasīt rakstus un priekšskatīt melnrakstus

Nepieciešamais instalētais modulis: shopently.blog · Moduļa līguma versija: 1.

Pieprasījums

Sagatavo piekļuvi veikala administrēšanas panelim. Izvēlies vienu vidi: cURL, PHP vai Python.

PHP 8.3 (64-bit, ext-curl, JSON) · Python 3.12 + requests · cURL 8 + jq 1.7.

SHOPENTLY_SHOP_URL: Veikala HTTPS adrese bez /api/v1 ceļa. Aizstāj piemēru ar savu adresi.

SHOPENTLY_SHOP_URL=https://shop.example

SHOPENTLY_API_KEY: Veikala administrēšanas panelī izveidotā atslēga.

Saglabājiet JSON piemēru kā BLOG_JOB_FILE un norādiet PNG laukā SHOPENTLY_IMAGE_FILE. Izvēlieties privātu BLOG_STATE_DIR. Atkārtotiem mēģinājumiem saglabājiet failus un direktoriju; jaunam rakstam vajadzīgs jauns job_id un direktorijs.

Linux/POSIX · flock · sha256sum · sync · PNG ≤ 5 MiB · 8 MP · HTML ≤ 50000 · JSON ≤ 256 KiB.

Pilns piemērs

cURL

#!/bin/sh
# Linux/POSIX shell, curl 8+, jq 1.7, flock, sha256sum, sync.
# Retain the immutable job, PNG and private state directory for retries.
set -eu
umask 077
: "${SHOPENTLY_SHOP_URL:?}" "${SHOPENTLY_API_KEY:?}" "${BLOG_JOB_FILE:?}"
: "${SHOPENTLY_IMAGE_FILE:?}" "${BLOG_STATE_DIR:?}"
base=${SHOPENTLY_SHOP_URL%/}
case "$base" in https://*) ;; *) echo 'Use an HTTPS origin' >&2; exit 1;; esac
authority=${base#https://}
case "$authority" in ''|*/*|*\?*|*\#*|*@*) echo 'Use an HTTPS origin' >&2; exit 1;; esac
state=$BLOG_STATE_DIR
mkdir -p "$state"
exec 9>"$state/.lock"
flock -n 9 || { echo 'This job is already running' >&2; exit 1; }
work=$(mktemp -d "$state/.run.XXXXXX")
trap 'rm -rf "$work"' EXIT HUP INT TERM
cp "$BLOG_JOB_FILE" "$work/job.json"
cp "$SHOPENTLY_IMAGE_FILE" "$work/article.png"
[ "$(wc -c <"$work/article.png")" -le 5242880 ] || { echo 'Image exceeds 5 MiB' >&2; exit 1; }
[ "$(od -An -N8 -tx1 "$work/article.png" | tr -d ' \n')" = 89504e470d0a1a0a ] || { echo 'Use a PNG image' >&2; exit 1; }
identity=$(jq -er '.job_id | select(type == "string" and test("^[A-Za-z0-9][A-Za-z0-9._:-]{0,79}$"))' "$work/job.json")
digest=$({ printf '%s\n' "$base"; cat "$work/job.json"; printf '\n'; cat "$work/article.png"; } | sha256sum | cut -d' ' -f1)
jq -nc --arg id "$identity" --arg sha "$digest" '{job_id:$id,sha256:$sha}' >"$work/marker.json"
if [ -f "$state/job.json" ]; then
    jq -e --slurpfile intended "$work/marker.json" '. == $intended[0]' "$state/job.json" >/dev/null ||
        { echo 'A retry must use the original origin, job and PNG bytes' >&2; exit 1; }
else
    sync -f "$work/marker.json"
    mv "$work/marker.json" "$state/job.json"
    sync -f "$state"
fi
# The token response stays in memory and is never included in persisted job results.
raw_token=$(curl --disable --globoff --fail-with-body --silent --show-error \
    --proto '=https' --connect-timeout 5 --max-time 30 --request POST \
    --header "Authorization: ApiKey $SHOPENTLY_API_KEY" --url "$base/api/v1/auth/token")
token=$(printf '%s' "$raw_token" | jq -er '.data.access_token | select(type == "string" and length > 0)')
unset raw_token
prefix=/api/v1/addons/shopently.blog
encode() { printf '%s' "$1" | jq -sRr @uri; }
api() {
    method=$1; path=$2; body_file=${3-}; key=${4-}; upload=${5-false}
    set -- --disable --globoff --silent --show-error --proto '=https' --connect-timeout 5 --max-time 30 \
        --request "$method" --header "Authorization: Bearer $token" \
        --dump-header "$work/headers" --output "$work/body" --write-out '%{http_code}'
    if [ -n "$key" ]; then set -- "$@" --header "Idempotency-Key: $key"; fi
    if [ -n "$body_file" ]; then set -- "$@" --header 'Content-Type: application/json' --data-binary "@$body_file"; fi
    if [ "$upload" = true ]; then
        set -- "$@" --form "file=@\"$work/article.png\";filename=article.png;type=image/png"
    fi
    status=$(curl "$@" --url "$base$path") || return 1
    case "$status" in
        2??) jq -ec '.data' "$work/body" ;;
        *) printf 'HTTP %s; inspect Retry-After before retrying this unchanged job\n' "$status" >&2; return 1 ;;
    esac
}
step() {
    name=$1; method=$2; path=$3; body_file=${4-}; upload=${5-false}
    if [ -f "$state/$name.json" ]; then cat "$state/$name.json"; return; fi
    api "$method" "$path" "$body_file" "$identity-$name" "$upload" >"$work/result.json"
    sync -f "$work/result.json"
    mv "$work/result.json" "$state/$name.json"
    sync -f "$state"
    cat "$state/$name.json"
}
jq -e '.author' "$work/job.json" >"$work/author-body.json"
step author POST "$prefix/authors" "$work/author-body.json" >"$work/author.json"
step media POST "$prefix/media" '' true >"$work/media.json"
jq -e --slurpfile author "$work/author.json" --slurpfile media "$work/media.json" \
    '.post + {author_id:$author[0].id,cover_media_id:$media[0].id}' "$work/job.json" >"$work/post-body.json"
step post POST "$prefix/posts" "$work/post-body.json" >"$work/post.json"
post_path="$prefix/posts/$(encode "$(jq -er .id "$work/post.json")")"
jq -e --slurpfile post "$work/post.json" '.update + {locale:.post.locale,expected_version:$post[0].version}' \
    "$work/job.json" >"$work/edit-body.json"
step edit PATCH "$post_path" "$work/edit-body.json" >"$work/edit.json"
jq -e --slurpfile edit "$work/edit.json" '.schedule + {expected_version:$edit[0].version,revision_id:$edit[0].revision_id}' \
    "$work/job.json" >"$work/schedule-body.json"
step schedule POST "$post_path/schedule" "$work/schedule-body.json" >/dev/null
wait_seconds=${BLOG_WAIT_SECONDS-180}
case "$wait_seconds" in ''|*[!0-9]*) echo 'Invalid BLOG_WAIT_SECONDS' >&2; exit 1;; esac
[ "$wait_seconds" -le 3600 ] || { echo 'BLOG_WAIT_SECONDS exceeds 3600' >&2; exit 1; }
deadline=$(($(date +%s) + wait_seconds))
locale=$(jq -er .post.locale "$work/job.json")
revision=$(jq -er .revision_id "$work/edit.json")
while :; do
    current=$(api GET "$post_path?locale=$(encode "$locale")")
    if [ "$wait_seconds" -eq 0 ] || printf '%s' "$current" | jq -e --arg r "$revision" '.published_revision_id == $r' >/dev/null; then
        printf '%s\n' "$current" | jq .
        break
    fi
    if printf '%s' "$current" | jq -e '.schedule_state == "requires_confirmation"' >/dev/null; then
        echo 'Publication requires a new confirmation in the store' >&2; exit 1
    fi
    [ "$(date +%s)" -lt "$deadline" ] || { echo 'Not published yet; retain this job and state for a later read' >&2; exit 1; }
    sleep 5
done

PHP

<?php
declare(strict_types=1);
// PHP 8.3+, 64-bit, ext-curl. Retain the job, PNG and private state directory.
function env(string $name): string {
    $value = getenv($name);
    if ($value === false || $value === '' || strpbrk($value, "\r\n") !== false) {
        throw new RuntimeException('Invalid environment variable: ' . $name);
    }
    return $value;
}
$base = rtrim(env('SHOPENTLY_SHOP_URL'), '/');
$origin = parse_url($base);
if (!$origin || ($origin['scheme'] ?? '') !== 'https' || empty($origin['host']) ||
    isset($origin['user']) || isset($origin['pass']) || isset($origin['path']) ||
    isset($origin['query']) || isset($origin['fragment']) || PHP_INT_SIZE !== 8) {
    throw new RuntimeException('Use 64-bit PHP and an HTTPS origin');
}
$rawJob = file_get_contents(env('BLOG_JOB_FILE'));
$rawImage = file_get_contents(env('SHOPENTLY_IMAGE_FILE'));
if ($rawJob === false || $rawImage === false || strlen($rawImage) > 5242880 ||
    !str_starts_with($rawImage, "\x89PNG\r\n\x1a\n")) {
    throw new RuntimeException('Use a readable job and PNG image up to 5 MiB');
}
$job = json_decode($rawJob, true, 512, JSON_THROW_ON_ERROR);
$identity = $job['job_id'];
if (!is_string($identity) || !preg_match('/^[A-Za-z0-9][A-Za-z0-9._:-]{0,79}$/D', $identity)) {
    throw new RuntimeException('job_id must identify one immutable generation job');
}
$digest = hash('sha256', $base . "\n" . $rawJob . "\n" . $rawImage);
$state = env('BLOG_STATE_DIR');
umask(0077);
if (!is_dir($state) && !mkdir($state, 0700, true)) throw new RuntimeException('Cannot create state directory');
$lock = fopen($state . '/.lock', 'ab');
if (!$lock || !flock($lock, LOCK_EX | LOCK_NB)) throw new RuntimeException('This job is already running');
function persist(string $name, array $value): void {
    global $state;
    $temporary = $state . '/' . $name . '.tmp';
    $handle = fopen($temporary, 'xb');
    if (!$handle) throw new RuntimeException('Cannot create state temporary');
    try {
        $raw = json_encode($value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
        if (fwrite($handle, $raw) !== strlen($raw) || !fflush($handle) || !fsync($handle)) {
            throw new RuntimeException('Cannot persist complete state');
        }
    } finally { fclose($handle); }
    if (!rename($temporary, $state . '/' . $name)) throw new RuntimeException('Cannot commit state');
}
try {
    foreach (['job', 'author', 'media', 'post', 'edit', 'schedule'] as $name) {
        $temporary = $state . '/' . $name . '.json.tmp';
        if (is_file($temporary)) unlink($temporary);
    }
    $marker = ['job_id' => $identity, 'sha256' => $digest];
    if (is_file($state . '/job.json')) {
        if (json_decode(file_get_contents($state . '/job.json'), true, 512, JSON_THROW_ON_ERROR) !== $marker) {
            throw new RuntimeException('A retry must use the original origin, job and PNG bytes');
        }
    } else { persist('job.json', $marker); }

    function api(string $method, string $path, ?array $body = null, ?string $key = null, bool $upload = false, bool $apiKey = false): array {
        global $base, $token, $rawImage;
        $headers = ['Authorization: ' . ($apiKey ? 'ApiKey ' . env('SHOPENTLY_API_KEY') : 'Bearer ' . $token)];
        if ($key !== null) $headers[] = 'Idempotency-Key: ' . $key;
        if ($body !== null) $headers[] = 'Content-Type: application/json';
        $responseHeaders = [];
        $h = curl_init($base . $path);
        curl_setopt_array($h, [
            CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers,
            CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 30,
            CURLOPT_HEADERFUNCTION => static function ($h, string $line) use (&$responseHeaders): int {
                if (str_contains($line, ':')) {
                    [$name, $value] = explode(':', $line, 2);
                    $responseHeaders[strtolower(trim($name))] = trim($value);
                }
                return strlen($line);
            },
        ]);
        if ($body !== null) curl_setopt($h, CURLOPT_POSTFIELDS, json_encode($body, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
        if ($upload) curl_setopt($h, CURLOPT_POSTFIELDS, ['file' => new CURLStringFile($rawImage, 'article.png', 'image/png')]);
        $raw = curl_exec($h);
        $status = curl_getinfo($h, CURLINFO_RESPONSE_CODE);
        $error = curl_error($h);
        curl_close($h);
        if ($raw === false) throw new RuntimeException('Transport error: ' . $error);
        if ($status < 200 || $status >= 300) throw new RuntimeException('HTTP ' . $status .
            '; request_id=' . ($responseHeaders['x-correlation-id'] ?? '') .
            '; retry_after=' . ($responseHeaders['retry-after'] ?? ''));
        return json_decode($raw, true, 512, JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING)['data'];
    }
    // Obtain a new short-lived token on each run; never persist credentials.
    $token = api('POST', '/api/v1/auth/token', apiKey: true)['access_token'];
    $prefix = '/api/v1/addons/shopently.blog';
    function step(string $name, string $method, string $path, ?array $body = null, bool $upload = false): array {
        global $identity, $state;
        $target = $state . '/' . $name . '.json';
        if (is_file($target)) return json_decode(file_get_contents($target), true, 512, JSON_THROW_ON_ERROR);
        $result = api($method, $path, $body, $identity . '-' . $name, $upload);
        persist($name . '.json', $result);
        return $result;
    }
    $author = step('author', 'POST', $prefix . '/authors', $job['author']);
    $media = step('media', 'POST', $prefix . '/media', upload: true);
    $post = step('post', 'POST', $prefix . '/posts', array_replace($job['post'], [
        'author_id' => $author['id'], 'cover_media_id' => $media['id'],
    ]));
    $path = $prefix . '/posts/' . rawurlencode($post['id']);
    $edit = step('edit', 'PATCH', $path, array_replace($job['update'], [
        'locale' => $job['post']['locale'], 'expected_version' => $post['version'],
    ]));
    step('schedule', 'POST', $path . '/schedule', array_replace($job['schedule'], [
        'expected_version' => $edit['version'], 'revision_id' => $edit['revision_id'],
    ]));
    $wait = filter_var(getenv('BLOG_WAIT_SECONDS') === false ? '180' : getenv('BLOG_WAIT_SECONDS'),
        FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 3600]]);
    if ($wait === false) throw new RuntimeException('BLOG_WAIT_SECONDS must be between 0 and 3600');
    $deadline = hrtime(true) / 1e9 + $wait;
    while (true) {
        $current = api('GET', $path . '?locale=' . rawurlencode($job['post']['locale']));
        if ($current['published_revision_id'] === $edit['revision_id'] || $wait === 0) {
            echo json_encode($current, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), PHP_EOL;
            break;
        }
        if ($current['schedule_state'] === 'requires_confirmation') {
            throw new RuntimeException('Publication requires a new confirmation in the store');
        }
        if (hrtime(true) / 1e9 >= $deadline) throw new RuntimeException('Not published yet; retain this job and state for a later read');
        usleep((int) (min(5, max(0, $deadline - hrtime(true) / 1e9)) * 1000000));
    }
} finally {
    flock($lock, LOCK_UN);
    fclose($lock);
}

Python

"""Python 3.12, requests, POSIX. Keep the job, PNG and state directory for retries."""

import fcntl
import hashlib
import json
import os
import re
import time
from contextlib import contextmanager
from pathlib import Path
from urllib.parse import quote, urlsplit

import requests


def env(name):
    value = os.environ[name]
    if not value or "\r" in value or "\n" in value:
        raise ValueError("Invalid environment variable: " + name)
    return value


base = env("SHOPENTLY_SHOP_URL").rstrip("/")
origin = urlsplit(base)
if (
    origin.scheme != "https"
    or not origin.hostname
    or origin.username
    or origin.password
    or origin.path
    or origin.query
    or origin.fragment
):
    raise ValueError("SHOPENTLY_SHOP_URL must be an HTTPS origin")
job_raw = Path(env("BLOG_JOB_FILE")).read_bytes()
image_path = Path(env("SHOPENTLY_IMAGE_FILE"))
image_raw = image_path.read_bytes()
if len(image_raw) > 5242880 or not image_raw.startswith(b"\x89PNG\r\n\x1a\n"):
    raise ValueError("Use a PNG image up to 5 MiB")
job = json.loads(job_raw)
identity = job["job_id"]
if not isinstance(identity, str) or not re.fullmatch(
    r"[A-Za-z0-9][A-Za-z0-9._:-]{0,79}", identity
):
    raise ValueError("job_id must identify one immutable generation job")
digest = hashlib.sha256(base.encode() + b"\n" + job_raw + b"\n" + image_raw).hexdigest()
state = Path(env("BLOG_STATE_DIR"))
state.mkdir(mode=0o700, parents=True, exist_ok=True)
os.umask(0o077)


def persist(name, value):
    temporary = state / (name + ".tmp")
    try:
        with temporary.open("x") as output:
            json.dump(value, output, ensure_ascii=False, allow_nan=False)
            output.flush()
            os.fsync(output.fileno())
        temporary.replace(state / name)
        descriptor = os.open(state, os.O_RDONLY | os.O_DIRECTORY)
        try:
            os.fsync(descriptor)
        finally:
            os.close(descriptor)
    finally:
        temporary.unlink(missing_ok=True)


@contextmanager
def lock():
    with (state / ".lock").open("a") as handle:
        fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
        # A crash can leave only our atomic-write temporary, never a completed result.
        for name in (
            "job.json",
            "author.json",
            "media.json",
            "post.json",
            "edit.json",
            "schedule.json",
        ):
            (state / (name + ".tmp")).unlink(missing_ok=True)
        yield


with lock(), requests.Session() as session:
    marker = state / "job.json"
    if marker.exists():
        if json.loads(marker.read_text()) != {"job_id": identity, "sha256": digest}:
            raise ValueError("A retry must use the original origin, job and PNG bytes")
    else:
        persist("job.json", {"job_id": identity, "sha256": digest})

    def api(method, path, *, body=None, key=None, upload=False, api_key=False):
        credential = env("SHOPENTLY_API_KEY") if api_key else token
        headers = {"Authorization": ("ApiKey " if api_key else "Bearer ") + credential}
        if key:
            headers["Idempotency-Key"] = key
        arguments = {"json": body} if body is not None else {}
        if upload:
            arguments["files"] = {"file": ("article.png", image_raw, "image/png")}
        response = session.request(
            method,
            base + path,
            headers=headers,
            timeout=(5, 30),
            allow_redirects=False,
            **arguments,
        )
        if not 200 <= response.status_code < 300:
            raise RuntimeError(
                "HTTP "
                + str(response.status_code)
                + "; request_id="
                + response.headers.get("X-Correlation-ID", "")
                + "; retry_after="
                + response.headers.get("Retry-After", "")
            )
        return response.json()["data"]

    # A restart obtains a fresh short-lived token. Tokens never enter the state files.
    token = api("POST", "/api/v1/auth/token", api_key=True)["access_token"]
    prefix = "/api/v1/addons/shopently.blog"

    def step(name, method, path, body=None, upload=False):
        target = state / (name + ".json")
        if target.exists():
            return json.loads(target.read_text())
        result = api(method, path, body=body, key=identity + "-" + name, upload=upload)
        persist(name + ".json", result)
        return result

    author = step("author", "POST", prefix + "/authors", job["author"])
    media = step("media", "POST", prefix + "/media", upload=True)
    post = step(
        "post",
        "POST",
        prefix + "/posts",
        {
            **job["post"],
            "author_id": author["id"],
            "cover_media_id": media["id"],
        },
    )
    path = prefix + "/posts/" + quote(post["id"], safe="")
    edit = step(
        "edit",
        "PATCH",
        path,
        {
            **job["update"],
            "locale": job["post"]["locale"],
            "expected_version": post["version"],
        },
    )
    step(
        "schedule",
        "POST",
        path + "/schedule",
        {
            **job["schedule"],
            "expected_version": edit["version"],
            "revision_id": edit["revision_id"],
        },
    )
    wait = int(os.environ.get("BLOG_WAIT_SECONDS", "180"))
    if not 0 <= wait <= 3600:
        raise ValueError("BLOG_WAIT_SECONDS must be between 0 and 3600")
    deadline = time.monotonic() + wait
    while True:
        current = api("GET", path + "?locale=" + quote(job["post"]["locale"], safe=""))
        if current["published_revision_id"] == edit["revision_id"] or wait == 0:
            print(json.dumps(current, ensure_ascii=False, indent=2))
            break
        if current["schedule_state"] == "requires_confirmation":
            raise RuntimeError("Publication requires a new confirmation in the store")
        if time.monotonic() >= deadline:
            raise TimeoutError(
                "Not published yet; retain this job and state for a later read"
            )
        time.sleep(min(5, max(0, deadline - time.monotonic())))

JSON

{
  "job_id": "blog-guide-20261007-001",
  "author": {
    "display_name": "Store editorial team",
    "biography": "Practical guides prepared by our editorial team.",
    "is_active": true
  },
  "post": {
    "locale": "pl",
    "title": "Jak wybrać produkt",
    "slug": "jak-wybrac-produkt",
    "excerpt": "Praktyczny poradnik zakupowy.",
    "content_html": "<h2>Porównaj właściwości</h2><p>Sprawdź opis i wybierz odpowiedni wariant.</p>",
    "meta_title": "Jak wybrać produkt",
    "meta_description": "Wskazówki dotyczące wyboru produktu i porównania wariantów.",
    "meta_keywords": [
      "poradnik",
      "wybór"
    ],
    "cover_alt": "Ilustracja poradnika",
    "cover_caption": "Zdjęcie redakcyjne",
    "comments_mode": "open"
  },
  "update": {
    "excerpt": "Praktyczny poradnik: porównanie właściwości i wariantów."
  },
  "schedule": {
    "publish_at": "2027-01-15T10:00:00+01:00",
    "timezone": "Europe/Warsaw"
  }
}

Publicēšana

Iestatiet publish_at uz nākotnes laiku ar UTC nobīdi un timezone uz atbilstošo IANA zonu. Nederīgi vai neviennozīmīgi laiki pulksteņa maiņas laikā tiek noraidīti. BLOG_WAIT_SECONDS nosaka gaidīšanu; 0 nolasa pašreizējo stāvokli. Atsauktas tiesības prasa jaunu publicēšanas apstiprinājumu.

Atkārtošanas noteikumi

Katra resursa rakstīšana prasa Idempotency-Key. Izveidojiet vienu atslēgu katrai loģiskai darbībai; atkārtojot to, saglabājiet to pašu atslēgu, metodi, mērķi un saturu.