Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbdd553394 | ||
|
|
7a8412ad6b | ||
|
|
08ced00f62 | ||
|
|
0301fd2f0b | ||
|
|
c0aa0b4b2b |
@@ -0,0 +1,41 @@
|
||||
# Publish the project's wiki pages to the git host's wiki.
|
||||
#
|
||||
# On a push to main that changes docs/wiki/, this workflow calls the reusable
|
||||
# workflow of TirSystem/github-action, which copies the Markdown pages of
|
||||
# docs/wiki/ to the wiki of this repository (framework/process/wiki.md says
|
||||
# what the folder holds: English and the PO language as a minimum).
|
||||
#
|
||||
# INSTALLING THIS FILE IS THE OPT-IN. There is no marker file. A project that
|
||||
# does not copy it publishes nothing.
|
||||
#
|
||||
# Copy it to .gitea/workflows/publish-wiki.yml. Then:
|
||||
# 1. Create the secret TOKEN_FOR_GITEA_WIKI in the organization of the
|
||||
# repository: a Gitea token with scope write:repository for a user that
|
||||
# can write this repository (see guides/credentials.md of github-action).
|
||||
# 2. Enable the wiki in the repository settings.
|
||||
# 3. Keep @v1, or pin the exact release tag together with tools-ref.
|
||||
# A runner must be registered for the repository (or its organization).
|
||||
#
|
||||
# prune: "true" deletes wiki pages that are not in docs/wiki/, so the wiki
|
||||
# always equals the folder. Leave it "false" to keep hand-written pages.
|
||||
|
||||
name: Publish wiki
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- "docs/wiki/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
wiki:
|
||||
uses: TirSystem/git-workflows/.gitea/workflows/publish-wiki-pages.yml@v1
|
||||
with:
|
||||
wiki_dir: docs/wiki
|
||||
prune: "true"
|
||||
secrets:
|
||||
TOKEN_FOR_GITEA_WIKI: ${{ secrets.TOKEN_FOR_GITEA_WIKI }}
|
||||
@@ -0,0 +1,73 @@
|
||||
# Publish accepted milestones to the git host.
|
||||
#
|
||||
# On a push to main that changes docs/milestones/ or docs/sqa/reviews/, this
|
||||
# workflow runs `sync-project.sh --accepted-only --apply`: every milestone whose
|
||||
# latest Version History row is Accepted and whose latest review record is a Go
|
||||
# is created or updated as a Milestone with its tasks as Issues. A Proposed
|
||||
# milestone, or one without a Go review, is skipped. Nothing is closed or
|
||||
# deleted. Each run updates the issues of every accepted milestone (the sync is
|
||||
# idempotent, but the host records the edit).
|
||||
#
|
||||
# INSTALLING THIS FILE IS THE STANDING AUTHORIZATION TO PUBLISH. A project opts
|
||||
# in by copying it; an agent does not install it on its own.
|
||||
#
|
||||
# Copy it to .gitea/workflows/sync-milestones.yml (Gitea Actions) or
|
||||
# .github/workflows/sync-milestones.yml (GitHub Actions). Then:
|
||||
# 1. Create a CI secret and put its name in the two `secrets.` lines below
|
||||
# (SYNC_TOKEN is a placeholder). Gitea does not accept secret names that
|
||||
# start with GITEA_ or GITHUB_.
|
||||
# 2. Give the token the smallest scope that works: read and write on issues
|
||||
# and milestones. If the framework submodule is a private repository, the
|
||||
# token must also be able to read it (it is used to check it out).
|
||||
# 3. Run it once by hand from the Actions page with dry_run left on: it prints
|
||||
# the plan and creates nothing. Turn dry_run off only for a real run.
|
||||
# A runner must be registered for the repository (or its organisation).
|
||||
#
|
||||
# The steps are one command each: the logic is framework/scripts/ci-publish-milestones.sh,
|
||||
# a file that is linted and tested (QC-WF-001 criterion 20).
|
||||
|
||||
name: Publish accepted milestones
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "docs/milestones/**"
|
||||
- "docs/sqa/reviews/**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: "Preview only: print the plan and create nothing"
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
# Two merges in a row queue instead of racing.
|
||||
concurrency:
|
||||
group: publish-accepted-milestones
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Check out the project and its framework submodule
|
||||
uses: actions/checkout@v4.2.2
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ secrets.SYNC_TOKEN }}
|
||||
|
||||
- name: Make sure curl and jq are available
|
||||
run: bash framework/scripts/ci-publish-milestones.sh ensure-tools
|
||||
|
||||
- name: Publish accepted milestones
|
||||
env:
|
||||
# Gitea reads GITEA_TOKEN, GitHub's gh reads GH_TOKEN; both come from
|
||||
# the one secret named above.
|
||||
GITEA_TOKEN: ${{ secrets.SYNC_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.SYNC_TOKEN }}
|
||||
# Empty on a push (a real run); "true" or "false" on a manual run.
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
run: bash framework/scripts/ci-publish-milestones.sh publish
|
||||
@@ -8,7 +8,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
SNACK_AND_LIFT_E2E=1
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends chromium chromium-driver fonts-liberation fonts-noto-color-emoji \
|
||||
&& apt-get install -y --no-install-recommends chromium chromium-driver fonts-liberation fonts-dejavu-core fonts-noto-color-emoji \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -8,6 +8,7 @@ A Selenium bot that books classes at the fictional gym "Snack and Lift" for you:
|
||||
- [Requirements](#-requirements)
|
||||
- [Setup](#-setup)
|
||||
- [Run](#-run)
|
||||
- [Lecture steps and screenshots](#-lecture-steps-and-screenshots)
|
||||
- [Tests](#-tests)
|
||||
- [License](#-license)
|
||||
- [Links](#-links)
|
||||
@@ -112,6 +113,22 @@ docker build -t snack-and-lift .
|
||||
docker run --rm snack-and-lift
|
||||
```
|
||||
|
||||
## 📸 Lecture steps and screenshots
|
||||
|
||||
Each step has a wiki page that explains what it teaches, in [English](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Home), [Dansk](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Home-da) and [Bahasa Melayu](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Home-ms). Step 1 is a screenshot of the live site. Steps 2 to 9 are illustrations drawn from the project's local test pages (`tests/e2e/fixtures`), because the live site needs an account; a run with your own account and `--screenshot-dir` (see [Getting started](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Getting-Started)) replaces them.
|
||||
|
||||
| Step | Wiki page | Screenshot |
|
||||
| --- | --- | --- |
|
||||
| 1 | [Setup and navigation](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Step-1) |  |
|
||||
| 2 | [Automated login](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Step-2) |  |
|
||||
| 3 | [Book the next Tuesday class](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Step-3) |  |
|
||||
| 4 | [Check if a class is booked](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Step-4) |  |
|
||||
| 5 | [Counters and summary](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Step-5) |  |
|
||||
| 6 | [Every Tuesday and Thursday class](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Step-6) |  |
|
||||
| 7 | [Verify on My Bookings](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Step-7) |  |
|
||||
| 8 | [Time travel QA](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Step-8) |  |
|
||||
| 9 | [Resilience (retry)](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/wiki/Step-9) |  |
|
||||
|
||||
## 🧪 Tests
|
||||
|
||||
```bash
|
||||
|
||||
@@ -16,7 +16,7 @@ document of a type. `Primary File` may contain a glob (e.g.
|
||||
| PP | Project Plan | docs/project-plan.md | 002 |
|
||||
| MIL | Milestone / Gateway | docs/milestones/*.md | 009 |
|
||||
| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0004 |
|
||||
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 022 |
|
||||
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 023 |
|
||||
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
|
||||
| DICT | Domain Dictionary | docs/dictionary.md | 002 |
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 59 KiB |
@@ -0,0 +1,69 @@
|
||||
# SQA Review Record
|
||||
|
||||
## Metadata
|
||||
| Key | Value |
|
||||
| --- | --- |
|
||||
| ID | RC-022 |
|
||||
| CrossReference | [MIL-008], [QC-WF-001] |
|
||||
|
||||
## Version History
|
||||
| Date | Status | Author | Reviewer | Change | Commit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 2026-10-28 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S02 | Review record, the workflows publish-wiki.yml and sync-milestones.yml of MIL-008 task 5 | [0301fd2] |
|
||||
|
||||
---
|
||||
|
||||
## Artifact Under Review
|
||||
|
||||
- Instance reviewed: [MIL-008]
|
||||
- Checklist used: [QC-WF-001] (`QC-WF-001`)
|
||||
- Scope: the workflows publish-wiki.yml and sync-milestones.yml of MIL-008 task 5
|
||||
- Language and domain: n/a
|
||||
- Language reviewer: none
|
||||
- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S02`)
|
||||
|
||||
## Checklist Results
|
||||
|
||||
| # | Criterion | Status | Evidence/Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | The file meets the YAML checklist ([QC-YML-001]) | Pending | `yamllint` is not installed on the stand-in's machine, so the YAML checklist QC-YML-001 was not run; the files parse (actionlint reads them) and use 2-space indentation. |
|
||||
| 2 | The file is in `.gitea/workflows/` or `.github/workflows/` with the extension `.yml` or `.yaml`, and the same workflow does not exist in both directories | Pass | Both files are in `.gitea/workflows/` with the extension `.yml`; neither exists in `.github/workflows/`. |
|
||||
| 3 | The file declares `name`, `on` and `jobs`; `on` lists explicit events and, where it matters, `branches` or `paths` | Pass | Both files declare `name`, `on` and `jobs`; `on` lists `push` on `main` with `paths`, and `workflow_dispatch`. |
|
||||
| 4 | A header comment states the purpose, the triggers, the inputs, the secrets it needs, what it changes outside the repository, and the decision or milestone it implements | Pass | Both headers state the purpose, triggers, secrets, what changes outside the repository and the installation steps (copied from the framework template); the decision is MIL-008 task 5. |
|
||||
| 5 | Every job and step has a descriptive `name`, unique within its job | Pass | `sync-milestones.yml` names every step. `publish-wiki.yml` has one job that calls a reusable workflow and has no steps of its own. |
|
||||
| 6 | The file passes `actionlint` (or the project's workflow validation test) and the tool version is stated | Pass | `actionlint` 1.7.12 reports no finding for both files (exit code 0). |
|
||||
| 7 | Every job declares `permissions` explicitly with the least access that works (`contents: read` or none by default); write access only on the job that needs it | Pass | `sync-milestones.yml` sets `contents: read` on its job; `publish-wiki.yml` sets `contents: read` for the whole workflow (added to the framework template). The wiki write uses the token secret. |
|
||||
| 8 | Every action used is pinned to a full commit SHA or an exact release tag, never to a branch; a moving major tag (`@v1`) is accepted only for an action published by the same organisation under a stated release policy | Pass | `actions/checkout@v4.2.2` is an exact release tag. `publish-wiki.yml` uses `TirSystem/git-workflows/.gitea/workflows/publish-wiki-pages.yml@v1`, a moving major tag of a reusable workflow of the same organisation, which the framework template accepts. |
|
||||
| 9 | No secret is written in the file; a secret is read as `secrets.NAME`, passed to a step through `env:`, never placed on a command line or echoed | Pass | Both files read `secrets.SYNC_TOKEN` and `secrets.TOKEN_FOR_GITEA_WIKI` only; no secret value is in the repository. |
|
||||
| 10 | Untrusted text (`inputs.*`, event fields such as a title, branch name or label, step outputs derived from them) is never interpolated into a `run:` script; it is passed through `env:` and used as a quoted variable | Pass | No `run:` block interpolates `inputs.*` or event text; `DRY_RUN` is passed through `env:`. |
|
||||
| 11 | A trigger that gives a run secrets or a write token on code from a fork or an untrusted branch (`pull_request_target`, `workflow_run`) does not check out or execute that code | Pass | Neither workflow uses `pull_request_target` or `workflow_run`. |
|
||||
| 12 | A workflow that changes state outside the repository (release, tag, publish, sync, deploy) is started by hand or by a protected branch, and a manual run defaults to a dry run with an explicit input to apply | Pass | Both run on a push to `main` or by hand; the manual run of `sync-milestones.yml` defaults to a dry run. `publish-wiki.yml` has no dry run (it is a call of the reusable workflow). |
|
||||
| 13 | The token the workflow uses has only the scope the job needs, and where the host's built-in token is enough it is used instead of a personal token | Pending | The scope of both tokens is set by S01 when the secrets are created. |
|
||||
| 14 | Every job sets `runs-on` to an explicit label and `timeout-minutes` | Pass | `sync-milestones.yml` sets `runs-on: ubuntu-latest` and `timeout-minutes: 15`. `publish-wiki.yml` calls a reusable workflow, where the called workflow sets them (a calling job cannot). |
|
||||
| 15 | `concurrency` is set where two runs would conflict: a check cancels the older run (`cancel-in-progress: true`); a publishing job queues (`cancel-in-progress: false`) | Pass | `sync-milestones.yml` sets `concurrency` with `cancel-in-progress: false`; `publish-wiki.yml` leaves it to the reusable workflow. |
|
||||
| 16 | Triggers are filtered (`branches`, `paths`, `types`) so the workflow runs only for the changes it concerns | Pass | Both are filtered by `branches: [main]` and `paths`. |
|
||||
| 17 | Tools and dependencies are installed at pinned versions; a version is a workflow input with a default or a value in one place | Pass | `sync-milestones.yml` installs nothing itself; `curl` and `jq` are checked by the framework script. The wiki workflow installs nothing. |
|
||||
| 18 | A dependency cache is used where the install is slow enough to matter | N-A | No dependency is installed that a cache would speed up. |
|
||||
| 19 | A failing step names the tool, the file and the reason in its log (annotation or `::error::` line) | Pending | Not verified: the log messages come from the framework script and the reusable workflow, which the stand-in did not run. |
|
||||
| 20 | A `run:` block is within the limit on embedded program text of criterion 7 of [QC-YML-001]; a longer script is a file (`tools/`, or a shared repository fetched at a pinned ref) that the step calls with one command, and the workflow passes its settings through `env:` | Pass | Each `run:` block is one command; the logic is in `framework/scripts/ci-publish-milestones.sh`. |
|
||||
| 21 | Logic used by more than one workflow or repository is a reusable workflow (`workflow_call`) or a composite action; each input has a `description`, a `type` and a `default` where one is sensible | Pass | `publish-wiki.yml` calls the reusable workflow; `sync-milestones.yml` calls one framework script. The inputs of `workflow_dispatch` have a description, type and default. |
|
||||
| 22 | Every workflow feature used is verified on the instance that runs it, and the host and version are recorded; a feature the target hosts differ on is noted in the header | Pending | Neither workflow has run on the Gitea instance yet: the secrets do not exist and a merge to `main` is what triggers the first run. |
|
||||
| 23 | A reusable workflow states, in its header, how a caller pins it and the script it fetches, so the workflow and its script are the same release | N-A | Neither file is a reusable workflow; both are callers. |
|
||||
| 24 | A static test of the repository's workflows exists and runs on every pull request: valid YAML, required keys, explicit permissions and runner, no inline script above the limit | N-A | No static test of the workflows exists in this project; the checks are `actionlint` on request. |
|
||||
|
||||
## Overall Verdict
|
||||
|
||||
Go-with-conditions - the two workflows are the framework's templates, copied into `.gitea/workflows/`, and pass `actionlint`. Mandatory criteria 1 (the YAML checklist: `yamllint` was not run) and 22 (feature checked on the instance) are Pending, because the stand-in cannot create the secrets `TOKEN_FOR_GITEA_WIKI` and `SYNC_TOKEN` or watch a run on the host. The workflows will fail or skip until S01 creates them. The review is not independent: the AI wrote and reviewed it as stand-in for S02, and S01 can challenge it in the end-of-run summary.
|
||||
|
||||
## Action Items
|
||||
|
||||
| Action | Owner | Due |
|
||||
| --- | --- | --- |
|
||||
| Create the secrets `TOKEN_FOR_GITEA_WIKI` (wiki write) and `SYNC_TOKEN` (issues and milestones) in the organisation, enable the wiki, then run both workflows once by hand | S01 | before the next merge to main |
|
||||
| Run `yamllint` on `.gitea/workflows/` | S01 | when convenient |
|
||||
|
||||
---
|
||||
|
||||
[MIL-008]: ../../milestones/mil-008-documentation.md
|
||||
[QC-WF-001]: ../../../framework/qc/code/qc-workflow.md
|
||||
[0301fd2]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift/commit/0301fd2f0bc22a26ee8d71ec41dab2bce0d38dc3
|
||||
@@ -42,6 +42,7 @@ Tracks backward and forward links between artifact instances so that the Busines
|
||||
| Source and tests of [MIL-005] | Python source code | - | - | [MIL-005], [ADR-0002] | - | RC-019 (Go-with-conditions) |
|
||||
| Source and tests of [MIL-006] | Python source code | - | - | [MIL-006], [ADR-0002] | - | RC-020 (Go-with-conditions) |
|
||||
| Source and tests of [MIL-007] | Python source code | - | - | [MIL-007], [ADR-0002] | - | RC-021 (Go-with-conditions) |
|
||||
| Workflows and wiki of [MIL-008] | Workflow files, wiki pages | - | - | [MIL-008] | - | RC-022 (Go-with-conditions) |
|
||||
|
||||
## Concern Coverage
|
||||
|
||||
@@ -51,8 +52,8 @@ Tracks backward and forward links between artifact instances so that the Busines
|
||||
| CN-002 | [PP-001], [MIL-001] | [ADR-0001] | Review records RC-001 onward | Covered |
|
||||
| CN-003 | [MIL-006], [MIL-007] | [ADR-0002] | Unit tests of `retry`; container chaos run | Planned |
|
||||
| CN-004 | [MIL-003] to [MIL-007] | [ADR-0002] | Code review against QC-PY-001 | Planned |
|
||||
| CN-005 | [MIL-002], [MIL-008] | - | `check-readme.sh` | Planned |
|
||||
| CN-006 | [MIL-008] | - | Page count per language | Planned |
|
||||
| CN-005 | [MIL-002], [MIL-008] | - | `check-readme.sh` | Covered |
|
||||
| CN-006 | [MIL-008] | - | Page count per language (12 pages in each of en, da, ms) | Covered |
|
||||
| CN-007 | [MIL-001] | - | Repository API response | Covered |
|
||||
| CN-008 | [MIL-002] | [ADR-0002] | `pyproject.toml` | Planned |
|
||||
| CN-009 | [MIL-001] | - | `.gitignore` lists `.env` | Covered |
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
[English](Getting-Started) · **Dansk** · [Bahasa Melayu](Getting-Started-ms)
|
||||
|
||||
# Kom i gang
|
||||
|
||||
## Krav
|
||||
|
||||
Python 3.13 eller nyere, Chrome eller Chromium og (valgfrit) Docker.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
```
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
```bash
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
## Din konto
|
||||
|
||||
Botten indeholder ingen adgangskode. Opret en konto én gang i hånden på siden (kør med `--keep-open`, så mappen `chrome_profile` gemmer sidens data), og giv derefter botten kontoen:
|
||||
|
||||
```bash
|
||||
export SNACK_AND_LIFT_EMAIL="yourname@test.com"
|
||||
export SNACK_AND_LIFT_PASSWORD="your password"
|
||||
```
|
||||
|
||||
```powershell
|
||||
$env:SNACK_AND_LIFT_EMAIL = "yourname@test.com"
|
||||
$env:SNACK_AND_LIFT_PASSWORD = "your password"
|
||||
```
|
||||
|
||||
## Kør og test
|
||||
|
||||
```bash
|
||||
snack-and-lift
|
||||
```
|
||||
|
||||
```bash
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
Nyttige indstillinger: `--headless`, `--keep-open`, `--first-tuesday-only` (trin 3 og 4), `--retries N`, `--profile-dir DIR`, `--screenshot-dir DIR`.
|
||||
|
||||
## Exitkoder
|
||||
|
||||
0 færdig, 1 et trin fejlede (også når alle forsøg fejlede), 2 loginoplysninger mangler, 3 siden My Bookings passer ikke med de bookinger der blev lavet.
|
||||
|
||||
## Skærmbilleder (vedligeholder)
|
||||
|
||||
Siden gemmer sine data i browserens profil, så en kørsel i en container kræver en konto der findes i den profil. Brug `--env-file` til loginoplysningerne (aldrig på kommandolinjen eller i repositoriet) og et volume til profilen og skærmbillederne:
|
||||
|
||||
```bash
|
||||
docker build -t snack-and-lift .
|
||||
docker run --rm --env-file my.env -v snack-profile:/app/chrome_profile -v "$PWD/docs/images:/app/screenshots" snack-and-lift --headless --screenshot-dir /app/screenshots
|
||||
```
|
||||
@@ -0,0 +1,65 @@
|
||||
[English](Getting-Started) · [Dansk](Getting-Started-da) · **Bahasa Melayu**
|
||||
|
||||
# Mula menggunakan
|
||||
|
||||
## Keperluan
|
||||
|
||||
Python 3.13 atau lebih baharu, Chrome atau Chromium, dan (pilihan) Docker.
|
||||
|
||||
## Pemasangan
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
```
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
```bash
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
## Akaun anda
|
||||
|
||||
Bot tidak mengandungi kata laluan. Daftar sekali secara manual di laman (jalankan dengan `--keep-open` supaya folder `chrome_profile` menyimpan data laman), kemudian beri bot akaun itu:
|
||||
|
||||
```bash
|
||||
export SNACK_AND_LIFT_EMAIL="yourname@test.com"
|
||||
export SNACK_AND_LIFT_PASSWORD="your password"
|
||||
```
|
||||
|
||||
```powershell
|
||||
$env:SNACK_AND_LIFT_EMAIL = "yourname@test.com"
|
||||
$env:SNACK_AND_LIFT_PASSWORD = "your password"
|
||||
```
|
||||
|
||||
## Jalankan dan uji
|
||||
|
||||
```bash
|
||||
snack-and-lift
|
||||
```
|
||||
|
||||
```bash
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
Pilihan berguna: `--headless`, `--keep-open`, `--first-tuesday-only` (langkah 3 dan 4), `--retries N`, `--profile-dir DIR`, `--screenshot-dir DIR`.
|
||||
|
||||
## Kod keluar
|
||||
|
||||
0 selesai, 1 satu langkah gagal (juga apabila semua percubaan gagal), 2 kelayakan tiada, 3 halaman My Bookings tidak sepadan dengan tempahan yang dibuat.
|
||||
|
||||
## Mengambil tangkapan skrin (penyelenggara)
|
||||
|
||||
Laman menyimpan datanya dalam profil pelayar, jadi larian dalam bekas memerlukan akaun yang wujud dalam profil itu. Gunakan `--env-file` untuk kelayakan (jangan pada baris arahan atau dalam repositori) dan volume untuk profil serta tangkapan skrin:
|
||||
|
||||
```bash
|
||||
docker build -t snack-and-lift .
|
||||
docker run --rm --env-file my.env -v snack-profile:/app/chrome_profile -v "$PWD/docs/images:/app/screenshots" snack-and-lift --headless --screenshot-dir /app/screenshots
|
||||
```
|
||||
@@ -0,0 +1,65 @@
|
||||
**English** · [Dansk](Getting-Started-da) · [Bahasa Melayu](Getting-Started-ms)
|
||||
|
||||
# Getting started
|
||||
|
||||
## Requirements
|
||||
|
||||
Python 3.13 or newer, Chrome or Chromium, and (optional) Docker.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
```
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
```bash
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
## Your account
|
||||
|
||||
The bot contains no password. Register once by hand on the site (run with `--keep-open`, so the `chrome_profile` folder keeps the site's data), then give the bot the account:
|
||||
|
||||
```bash
|
||||
export SNACK_AND_LIFT_EMAIL="yourname@test.com"
|
||||
export SNACK_AND_LIFT_PASSWORD="your password"
|
||||
```
|
||||
|
||||
```powershell
|
||||
$env:SNACK_AND_LIFT_EMAIL = "yourname@test.com"
|
||||
$env:SNACK_AND_LIFT_PASSWORD = "your password"
|
||||
```
|
||||
|
||||
## Run and test
|
||||
|
||||
```bash
|
||||
snack-and-lift
|
||||
```
|
||||
|
||||
```bash
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
Useful options: `--headless`, `--keep-open`, `--first-tuesday-only` (steps 3 and 4), `--retries N`, `--profile-dir DIR`, `--screenshot-dir DIR`.
|
||||
|
||||
## Exit codes
|
||||
|
||||
0 done, 1 a step failed (also when every retry failed), 2 credentials missing, 3 the My Bookings page does not match the bookings made.
|
||||
|
||||
## Taking the screenshots (maintainer)
|
||||
|
||||
The site keeps its data in the browser profile, so a container run needs an account that exists in that profile. Use `--env-file` for the credentials (never write them on the command line or into the repository) and a volume for the profile and the screenshots:
|
||||
|
||||
```bash
|
||||
docker build -t snack-and-lift .
|
||||
docker run --rm --env-file my.env -v snack-profile:/app/chrome_profile -v "$PWD/docs/images:/app/screenshots" snack-and-lift --headless --screenshot-dir /app/screenshots
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
[English](Home) · **Dansk** · [Bahasa Melayu](Home-ms)
|
||||
|
||||
# Snack and Lift-bot
|
||||
|
||||
En Selenium-bot der booker hold på øvelsesgymmet [Snack and Lift](https://appbrewery.github.io/gym/). Den er løsningen på dag 49 i Udemys *100 Days of Code*. Hver side nedenfor forklarer hvad ét trin i lektionen lærer.
|
||||
|
||||
Botten logger ind, booker hvert tirsdags- og torsdagshold kl. 18 (eller kommer på ventelisten), bekræfter resultatet på siden My Bookings og prøver igen når siden fejler en forespørgsel.
|
||||
|
||||
- [Kom i gang](Getting-Started-da): `.venv`, din konto, kørsel og test.
|
||||
- De ni trin, nedenfor.
|
||||
- [Fejlfinding](Troubleshooting-da).
|
||||
|
||||
Kildekode og sager: [repository](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift).
|
||||
|
||||
## De ni trin
|
||||
|
||||
1. [Trin 1: Opsætning, Chrome-profil og navigation](Step-1-da)
|
||||
2. [Trin 2: Automatisk login](Step-2-da)
|
||||
3. [Trin 3: Book næste tirsdagshold](Step-3-da)
|
||||
4. [Trin 4: Tjek om et hold allerede er booket](Step-4-da)
|
||||
5. [Trin 5: Tællere og et resumé](Step-5-da)
|
||||
6. [Trin 6: Alle tirsdags- og torsdagshold](Step-6-da)
|
||||
7. [Trin 7: Bekræft på siden My Bookings](Step-7-da)
|
||||
8. [Trin 8: Kvalitetssikring med tidsrejse](Step-8-da)
|
||||
9. [Trin 9: Robusthed mod netværksfejl](Step-9-da)
|
||||
@@ -0,0 +1,25 @@
|
||||
[English](Home) · [Dansk](Home-da) · **Bahasa Melayu**
|
||||
|
||||
# Bot Snack and Lift
|
||||
|
||||
Bot Selenium yang menempah kelas di gim latihan [Snack and Lift](https://appbrewery.github.io/gym/). Ia penyelesaian untuk hari 49 *100 Days of Code* Udemy. Setiap halaman di bawah menerangkan apa yang diajar oleh satu langkah kuliah.
|
||||
|
||||
Bot log masuk, menempah setiap kelas Selasa dan Khamis pukul 6 petang (atau menyertai senarai menunggu), mengesahkan hasilnya di halaman My Bookings dan mencuba semula apabila laman gagal memproses permintaan.
|
||||
|
||||
- [Mula menggunakan](Getting-Started-ms): `.venv`, akaun anda, menjalankan dan menguji.
|
||||
- Sembilan langkah, di bawah.
|
||||
- [Menyelesaikan masalah](Troubleshooting-ms).
|
||||
|
||||
Kod sumber dan isu: [repository](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift).
|
||||
|
||||
## Sembilan langkah
|
||||
|
||||
1. [Langkah 1: Persediaan, profil Chrome dan navigasi](Step-1-ms)
|
||||
2. [Langkah 2: Log masuk automatik](Step-2-ms)
|
||||
3. [Langkah 3: Tempah kelas Selasa seterusnya](Step-3-ms)
|
||||
4. [Langkah 4: Semak sama ada kelas sudah ditempah](Step-4-ms)
|
||||
5. [Langkah 5: Pembilang dan ringkasan](Step-5-ms)
|
||||
6. [Langkah 6: Semua kelas Selasa dan Khamis](Step-6-ms)
|
||||
7. [Langkah 7: Sahkan di halaman My Bookings](Step-7-ms)
|
||||
8. [Langkah 8: Jaminan kualiti dengan perjalanan masa](Step-8-ms)
|
||||
9. [Langkah 9: Ketahanan terhadap kegagalan rangkaian](Step-9-ms)
|
||||
@@ -0,0 +1,25 @@
|
||||
**English** · [Dansk](Home-da) · [Bahasa Melayu](Home-ms)
|
||||
|
||||
# Snack and Lift bot
|
||||
|
||||
A Selenium bot that books classes at the practice gym [Snack and Lift](https://appbrewery.github.io/gym/). It is the solution to day 49 of Udemy's *100 Days of Code*. Each page below explains what one lecture step teaches.
|
||||
|
||||
The bot logs in, books every Tuesday and Thursday 6pm class (or joins the waitlist), verifies the result on the My Bookings page and retries when the site fails a request.
|
||||
|
||||
- [Getting started](Getting-Started): the `.venv`, your account, running and testing.
|
||||
- The nine steps, below.
|
||||
- [Troubleshooting](Troubleshooting).
|
||||
|
||||
Source and issues: [repository](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift).
|
||||
|
||||
## The nine steps
|
||||
|
||||
1. [Step 1: Setup, Chrome profile and navigation](Step-1)
|
||||
2. [Step 2: Automated login](Step-2)
|
||||
3. [Step 3: Book the next Tuesday class](Step-3)
|
||||
4. [Step 4: Check if a class is already booked](Step-4)
|
||||
5. [Step 5: Counters and a summary](Step-5)
|
||||
6. [Step 6: Every Tuesday and Thursday class](Step-6)
|
||||
7. [Step 7: Verify on the My Bookings page](Step-7)
|
||||
8. [Step 8: Time travel quality assurance](Step-8)
|
||||
9. [Step 9: Resilience against network failures](Step-9)
|
||||
@@ -0,0 +1,17 @@
|
||||
[English](Step-1) · **Dansk** · [Bahasa Melayu](Step-1-ms)
|
||||
|
||||
# Trin 1: Opsætning, Chrome-profil og navigation
|
||||
|
||||
## Hvad du får ud af det
|
||||
|
||||
Du lærer at starte Chrome fra Selenium med en egen profilmappe (`chrome_profile`), så sidens data overlever mellem kørsler, og med indstillingen `detach`, så vinduet bliver åbent når scriptet slutter. Botten åbner forsiden og venter på login-knappen i stedet for at sove.
|
||||
|
||||
## Hvor det er i koden
|
||||
|
||||
`src/snack_and_lift/infrastructure/browser.py`
|
||||
|
||||
## Skærmbillede
|
||||
|
||||

|
||||
|
||||
[Forside](Home-da) · [Næste: 2](Step-2-da)
|
||||
@@ -0,0 +1,17 @@
|
||||
[English](Step-1) · [Dansk](Step-1-da) · **Bahasa Melayu**
|
||||
|
||||
# Langkah 1: Persediaan, profil Chrome dan navigasi
|
||||
|
||||
## Apa yang anda peroleh
|
||||
|
||||
Anda belajar memulakan Chrome daripada Selenium dengan folder profil sendiri (`chrome_profile`) supaya data laman kekal antara larian, dan dengan pilihan `detach` supaya tetingkap kekal terbuka apabila skrip tamat. Bot membuka halaman utama dan menunggu butang log masuk, bukannya tidur.
|
||||
|
||||
## Di mana dalam kod
|
||||
|
||||
`src/snack_and_lift/infrastructure/browser.py`
|
||||
|
||||
## Tangkapan skrin
|
||||
|
||||

|
||||
|
||||
[Utama](Home-ms) · [Seterusnya: 2](Step-2-ms)
|
||||
@@ -0,0 +1,17 @@
|
||||
**English** · [Dansk](Step-1-da) · [Bahasa Melayu](Step-1-ms)
|
||||
|
||||
# Step 1: Setup, Chrome profile and navigation
|
||||
|
||||
## What you gain
|
||||
|
||||
You learn to start Chrome from Selenium with its own profile folder (`chrome_profile`), so the site's data survives between runs, and with the `detach` option, so the window stays open when the script ends. The bot opens the home page and waits for the login button instead of sleeping.
|
||||
|
||||
## Where it is in the code
|
||||
|
||||
`src/snack_and_lift/infrastructure/browser.py`
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
[Home](Home) · [Next: 2](Step-2)
|
||||
@@ -0,0 +1,19 @@
|
||||
[English](Step-2) · **Dansk** · [Bahasa Melayu](Step-2-ms)
|
||||
|
||||
# Trin 2: Automatisk login
|
||||
|
||||
## Hvad du får ud af det
|
||||
|
||||
Du lærer eksplicitte ventetider (`WebDriverWait`) og at udfylde en formular. `login()` skriver e-mail og adgangskode fra miljøvariabler, indsender og venter til timeplanen vises. En forkert adgangskode vises via sidens fejlmeddelelse.
|
||||
|
||||
## Hvor det er i koden
|
||||
|
||||
`src/snack_and_lift/application/session.py`
|
||||
|
||||
## Skærmbillede
|
||||
|
||||

|
||||
|
||||
> Dette billede er en illustration: botten kørte mod projektets lokale testsider (`tests/e2e/fixtures`), ikke mod det rigtige gym-site. En kørsel med en rigtig konto og `--screenshot-dir` (se [Kom i gang](Getting-Started-da)) erstatter det.
|
||||
|
||||
[Forrige: 1](Step-1-da) · [Forside](Home-da) · [Næste: 3](Step-3-da)
|
||||
@@ -0,0 +1,19 @@
|
||||
[English](Step-2) · [Dansk](Step-2-da) · **Bahasa Melayu**
|
||||
|
||||
# Langkah 2: Log masuk automatik
|
||||
|
||||
## Apa yang anda peroleh
|
||||
|
||||
Anda belajar menunggu secara eksplisit (`WebDriverWait`) dan mengisi borang. `login()` menaip e-mel dan kata laluan daripada pemboleh ubah persekitaran, menghantar, dan menunggu sehingga jadual kelas dipaparkan. Kata laluan salah dilaporkan melalui mesej ralat laman.
|
||||
|
||||
## Di mana dalam kod
|
||||
|
||||
`src/snack_and_lift/application/session.py`
|
||||
|
||||
## Tangkapan skrin
|
||||
|
||||

|
||||
|
||||
> Imej ini ialah ilustrasi: bot dijalankan terhadap halaman ujian tempatan projek (`tests/e2e/fixtures`), bukan laman gim sebenar. Larian dengan akaun sebenar dan `--screenshot-dir` (lihat [Mula menggunakan](Getting-Started-ms)) menggantikannya.
|
||||
|
||||
[Sebelumnya: 1](Step-1-ms) · [Utama](Home-ms) · [Seterusnya: 3](Step-3-ms)
|
||||
@@ -0,0 +1,19 @@
|
||||
**English** · [Dansk](Step-2-da) · [Bahasa Melayu](Step-2-ms)
|
||||
|
||||
# Step 2: Automated login
|
||||
|
||||
## What you gain
|
||||
|
||||
You learn explicit waits (`WebDriverWait`) and how to fill a form. `login()` types the e-mail and password from environment variables, submits, and waits until the class schedule is shown. A wrong password is reported by the site's error message.
|
||||
|
||||
## Where it is in the code
|
||||
|
||||
`src/snack_and_lift/application/session.py`
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
> This image is an illustration: the bot ran against the project's local test pages (`tests/e2e/fixtures`), not against the live gym site. A run with a real account and `--screenshot-dir` (see [Getting started](Getting-Started)) replaces it.
|
||||
|
||||
[Previous: 1](Step-1) · [Home](Home) · [Next: 3](Step-3)
|
||||
@@ -0,0 +1,19 @@
|
||||
[English](Step-3) · **Dansk** · [Bahasa Melayu](Step-3-ms)
|
||||
|
||||
# Trin 3: Book næste tirsdagshold
|
||||
|
||||
## Hvad du får ud af det
|
||||
|
||||
Du lærer at finde et element relativt til et andet: næste tirsdags dagsgruppe findes med et XPath-opslag på forfædre, derefter 18-kortet og dets knap. `book_class()` klikker på knappen og venter til teksten ændres.
|
||||
|
||||
## Hvor det er i koden
|
||||
|
||||
`src/snack_and_lift/application/booking.py`
|
||||
|
||||
## Skærmbillede
|
||||
|
||||

|
||||
|
||||
> Dette billede er en illustration: botten kørte mod projektets lokale testsider (`tests/e2e/fixtures`), ikke mod det rigtige gym-site. En kørsel med en rigtig konto og `--screenshot-dir` (se [Kom i gang](Getting-Started-da)) erstatter det.
|
||||
|
||||
[Forrige: 2](Step-2-da) · [Forside](Home-da) · [Næste: 4](Step-4-da)
|
||||
@@ -0,0 +1,19 @@
|
||||
[English](Step-3) · [Dansk](Step-3-da) · **Bahasa Melayu**
|
||||
|
||||
# Langkah 3: Tempah kelas Selasa seterusnya
|
||||
|
||||
## Apa yang anda peroleh
|
||||
|
||||
Anda belajar mencari elemen berdasarkan elemen lain: kumpulan hari Selasa seterusnya dicari dengan carian XPath ancestor, kemudian kad pukul 6 petang dan butangnya. `book_class()` menekan butang dan menunggu teks berubah.
|
||||
|
||||
## Di mana dalam kod
|
||||
|
||||
`src/snack_and_lift/application/booking.py`
|
||||
|
||||
## Tangkapan skrin
|
||||
|
||||

|
||||
|
||||
> Imej ini ialah ilustrasi: bot dijalankan terhadap halaman ujian tempatan projek (`tests/e2e/fixtures`), bukan laman gim sebenar. Larian dengan akaun sebenar dan `--screenshot-dir` (lihat [Mula menggunakan](Getting-Started-ms)) menggantikannya.
|
||||
|
||||
[Sebelumnya: 2](Step-2-ms) · [Utama](Home-ms) · [Seterusnya: 4](Step-4-ms)
|
||||
@@ -0,0 +1,19 @@
|
||||
**English** · [Dansk](Step-3-da) · [Bahasa Melayu](Step-3-ms)
|
||||
|
||||
# Step 3: Book the next Tuesday class
|
||||
|
||||
## What you gain
|
||||
|
||||
You learn to find an element relative to another one: the day group of the next Tuesday is found with an XPath ancestor lookup, then the 6pm card and its button. `book_class()` clicks the button and waits until its text changes.
|
||||
|
||||
## Where it is in the code
|
||||
|
||||
`src/snack_and_lift/application/booking.py`
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
> This image is an illustration: the bot ran against the project's local test pages (`tests/e2e/fixtures`), not against the live gym site. A run with a real account and `--screenshot-dir` (see [Getting started](Getting-Started)) replaces it.
|
||||
|
||||
[Previous: 2](Step-2) · [Home](Home) · [Next: 4](Step-4)
|
||||
@@ -0,0 +1,19 @@
|
||||
[English](Step-4) · **Dansk** · [Bahasa Melayu](Step-4-ms)
|
||||
|
||||
# Trin 4: Tjek om et hold allerede er booket
|
||||
|
||||
## Hvad du får ud af det
|
||||
|
||||
Du lærer at læse en knaps tilstand før du handler: "Book Class", "Join Waitlist", "Booked" eller "Waitlisted". Et hold der allerede er booket klikkes ikke igen, og botten udskriver en ✓-linje om hvad den gjorde.
|
||||
|
||||
## Hvor det er i koden
|
||||
|
||||
`src/snack_and_lift/domain/rules.py`
|
||||
|
||||
## Skærmbillede
|
||||
|
||||

|
||||
|
||||
> Dette billede er en illustration: botten kørte mod projektets lokale testsider (`tests/e2e/fixtures`), ikke mod det rigtige gym-site. En kørsel med en rigtig konto og `--screenshot-dir` (se [Kom i gang](Getting-Started-da)) erstatter det.
|
||||
|
||||
[Forrige: 3](Step-3-da) · [Forside](Home-da) · [Næste: 5](Step-5-da)
|
||||
@@ -0,0 +1,19 @@
|
||||
[English](Step-4) · [Dansk](Step-4-da) · **Bahasa Melayu**
|
||||
|
||||
# Langkah 4: Semak sama ada kelas sudah ditempah
|
||||
|
||||
## Apa yang anda peroleh
|
||||
|
||||
Anda belajar membaca keadaan butang sebelum bertindak: "Book Class", "Join Waitlist", "Booked" atau "Waitlisted". Kelas yang sudah ditempah tidak ditekan semula, dan bot mencetak baris ✓ tentang apa yang dilakukannya.
|
||||
|
||||
## Di mana dalam kod
|
||||
|
||||
`src/snack_and_lift/domain/rules.py`
|
||||
|
||||
## Tangkapan skrin
|
||||
|
||||

|
||||
|
||||
> Imej ini ialah ilustrasi: bot dijalankan terhadap halaman ujian tempatan projek (`tests/e2e/fixtures`), bukan laman gim sebenar. Larian dengan akaun sebenar dan `--screenshot-dir` (lihat [Mula menggunakan](Getting-Started-ms)) menggantikannya.
|
||||
|
||||
[Sebelumnya: 3](Step-3-ms) · [Utama](Home-ms) · [Seterusnya: 5](Step-5-ms)
|
||||
@@ -0,0 +1,19 @@
|
||||
**English** · [Dansk](Step-4-da) · [Bahasa Melayu](Step-4-ms)
|
||||
|
||||
# Step 4: Check if a class is already booked
|
||||
|
||||
## What you gain
|
||||
|
||||
You learn to read the state of a button before acting: "Book Class", "Join Waitlist", "Booked" or "Waitlisted". A class that is already booked is not clicked again, and the bot prints a ✓ line that says what it did.
|
||||
|
||||
## Where it is in the code
|
||||
|
||||
`src/snack_and_lift/domain/rules.py`
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
> This image is an illustration: the bot ran against the project's local test pages (`tests/e2e/fixtures`), not against the live gym site. A run with a real account and `--screenshot-dir` (see [Getting started](Getting-Started)) replaces it.
|
||||
|
||||
[Previous: 3](Step-3) · [Home](Home) · [Next: 5](Step-5)
|
||||
@@ -0,0 +1,19 @@
|
||||
[English](Step-5) · **Dansk** · [Bahasa Melayu](Step-5-ms)
|
||||
|
||||
# Trin 5: Tællere og et resumé
|
||||
|
||||
## Hvad du får ud af det
|
||||
|
||||
Du lærer at føre tællere mens en løkke kører og at udskrive en rapport: BOOKING SUMMARY viser nye bookinger, nye ventelisteplaceringer og hold der allerede var booket eller på venteliste.
|
||||
|
||||
## Hvor det er i koden
|
||||
|
||||
`src/snack_and_lift/domain/report.py`
|
||||
|
||||
## Skærmbillede
|
||||
|
||||

|
||||
|
||||
> Dette billede er en illustration: botten kørte mod projektets lokale testsider (`tests/e2e/fixtures`), ikke mod det rigtige gym-site. En kørsel med en rigtig konto og `--screenshot-dir` (se [Kom i gang](Getting-Started-da)) erstatter det.
|
||||
|
||||
[Forrige: 4](Step-4-da) · [Forside](Home-da) · [Næste: 6](Step-6-da)
|
||||
@@ -0,0 +1,19 @@
|
||||
[English](Step-5) · [Dansk](Step-5-da) · **Bahasa Melayu**
|
||||
|
||||
# Langkah 5: Pembilang dan ringkasan
|
||||
|
||||
## Apa yang anda peroleh
|
||||
|
||||
Anda belajar menyimpan pembilang semasa gelung berjalan dan mencetak laporan: BOOKING SUMMARY menyenaraikan tempahan baharu, penyertaan senarai menunggu baharu, serta kelas yang sudah ditempah atau dalam senarai menunggu.
|
||||
|
||||
## Di mana dalam kod
|
||||
|
||||
`src/snack_and_lift/domain/report.py`
|
||||
|
||||
## Tangkapan skrin
|
||||
|
||||

|
||||
|
||||
> Imej ini ialah ilustrasi: bot dijalankan terhadap halaman ujian tempatan projek (`tests/e2e/fixtures`), bukan laman gim sebenar. Larian dengan akaun sebenar dan `--screenshot-dir` (lihat [Mula menggunakan](Getting-Started-ms)) menggantikannya.
|
||||
|
||||
[Sebelumnya: 4](Step-4-ms) · [Utama](Home-ms) · [Seterusnya: 6](Step-6-ms)
|
||||
@@ -0,0 +1,19 @@
|
||||
**English** · [Dansk](Step-5-da) · [Bahasa Melayu](Step-5-ms)
|
||||
|
||||
# Step 5: Counters and a summary
|
||||
|
||||
## What you gain
|
||||
|
||||
You learn to keep counters while a loop runs and to print a report: the BOOKING SUMMARY lists new bookings, new waitlist entries, and classes that were already booked or waitlisted.
|
||||
|
||||
## Where it is in the code
|
||||
|
||||
`src/snack_and_lift/domain/report.py`
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
> This image is an illustration: the bot ran against the project's local test pages (`tests/e2e/fixtures`), not against the live gym site. A run with a real account and `--screenshot-dir` (see [Getting started](Getting-Started)) replaces it.
|
||||
|
||||
[Previous: 4](Step-4) · [Home](Home) · [Next: 6](Step-6)
|
||||
@@ -0,0 +1,19 @@
|
||||
[English](Step-6) · **Dansk** · [Bahasa Melayu](Step-6-ms)
|
||||
|
||||
# Trin 6: Alle tirsdags- og torsdagshold
|
||||
|
||||
## Hvad du får ud af det
|
||||
|
||||
Du lærer at gennemløbe alle matchende elementer: hvert tirsdags- og torsdagshold kl. 18 håndteres, og DETAILED CLASS LIST udskriver hvert hold med dato, tid og resultat (New Booking, New Waitlist, Already Booked, Already Waitlisted).
|
||||
|
||||
## Hvor det er i koden
|
||||
|
||||
`src/snack_and_lift/application/booking.py`
|
||||
|
||||
## Skærmbillede
|
||||
|
||||

|
||||
|
||||
> Dette billede er en illustration: botten kørte mod projektets lokale testsider (`tests/e2e/fixtures`), ikke mod det rigtige gym-site. En kørsel med en rigtig konto og `--screenshot-dir` (se [Kom i gang](Getting-Started-da)) erstatter det.
|
||||
|
||||
[Forrige: 5](Step-5-da) · [Forside](Home-da) · [Næste: 7](Step-7-da)
|
||||
@@ -0,0 +1,19 @@
|
||||
[English](Step-6) · [Dansk](Step-6-da) · **Bahasa Melayu**
|
||||
|
||||
# Langkah 6: Semua kelas Selasa dan Khamis
|
||||
|
||||
## Apa yang anda peroleh
|
||||
|
||||
Anda belajar mengulang semua elemen yang sepadan: setiap kelas Selasa dan Khamis pukul 6 petang diuruskan, dan DETAILED CLASS LIST mencetak setiap kelas dengan tarikh, masa dan hasil (New Booking, New Waitlist, Already Booked, Already Waitlisted).
|
||||
|
||||
## Di mana dalam kod
|
||||
|
||||
`src/snack_and_lift/application/booking.py`
|
||||
|
||||
## Tangkapan skrin
|
||||
|
||||

|
||||
|
||||
> Imej ini ialah ilustrasi: bot dijalankan terhadap halaman ujian tempatan projek (`tests/e2e/fixtures`), bukan laman gim sebenar. Larian dengan akaun sebenar dan `--screenshot-dir` (lihat [Mula menggunakan](Getting-Started-ms)) menggantikannya.
|
||||
|
||||
[Sebelumnya: 5](Step-5-ms) · [Utama](Home-ms) · [Seterusnya: 7](Step-7-ms)
|
||||
@@ -0,0 +1,19 @@
|
||||
**English** · [Dansk](Step-6-da) · [Bahasa Melayu](Step-6-ms)
|
||||
|
||||
# Step 6: Every Tuesday and Thursday class
|
||||
|
||||
## What you gain
|
||||
|
||||
You learn to loop over all matching elements: every Tuesday and Thursday 6pm class is handled, and the DETAILED CLASS LIST prints each class with its date, time and result (New Booking, New Waitlist, Already Booked, Already Waitlisted).
|
||||
|
||||
## Where it is in the code
|
||||
|
||||
`src/snack_and_lift/application/booking.py`
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
> This image is an illustration: the bot ran against the project's local test pages (`tests/e2e/fixtures`), not against the live gym site. A run with a real account and `--screenshot-dir` (see [Getting started](Getting-Started)) replaces it.
|
||||
|
||||
[Previous: 5](Step-5) · [Home](Home) · [Next: 7](Step-7)
|
||||
@@ -0,0 +1,19 @@
|
||||
[English](Step-7) · **Dansk** · [Bahasa Melayu](Step-7-ms)
|
||||
|
||||
# Trin 7: Bekræft på siden My Bookings
|
||||
|
||||
## Hvad du får ud af det
|
||||
|
||||
Du lærer at kontrollere dit eget arbejde fra en anden kilde: `get_my_bookings()` læser siden My Bookings (bookinger og venteliste) og `verify_bookings()` sammenligner med det botten gjorde. Resultatet er SUCCESS eller MISMATCH (exitkode 3).
|
||||
|
||||
## Hvor det er i koden
|
||||
|
||||
`src/snack_and_lift/application/verification.py`
|
||||
|
||||
## Skærmbillede
|
||||
|
||||

|
||||
|
||||
> Dette billede er en illustration: botten kørte mod projektets lokale testsider (`tests/e2e/fixtures`), ikke mod det rigtige gym-site. En kørsel med en rigtig konto og `--screenshot-dir` (se [Kom i gang](Getting-Started-da)) erstatter det.
|
||||
|
||||
[Forrige: 6](Step-6-da) · [Forside](Home-da) · [Næste: 8](Step-8-da)
|
||||
@@ -0,0 +1,19 @@
|
||||
[English](Step-7) · [Dansk](Step-7-da) · **Bahasa Melayu**
|
||||
|
||||
# Langkah 7: Sahkan di halaman My Bookings
|
||||
|
||||
## Apa yang anda peroleh
|
||||
|
||||
Anda belajar menyemak kerja sendiri daripada sumber kedua: `get_my_bookings()` membaca halaman My Bookings (tempahan dan senarai menunggu) dan `verify_bookings()` membandingkannya dengan apa yang dilakukan bot. Hasilnya SUCCESS atau MISMATCH (kod keluar 3).
|
||||
|
||||
## Di mana dalam kod
|
||||
|
||||
`src/snack_and_lift/application/verification.py`
|
||||
|
||||
## Tangkapan skrin
|
||||
|
||||

|
||||
|
||||
> Imej ini ialah ilustrasi: bot dijalankan terhadap halaman ujian tempatan projek (`tests/e2e/fixtures`), bukan laman gim sebenar. Larian dengan akaun sebenar dan `--screenshot-dir` (lihat [Mula menggunakan](Getting-Started-ms)) menggantikannya.
|
||||
|
||||
[Sebelumnya: 6](Step-6-ms) · [Utama](Home-ms) · [Seterusnya: 8](Step-8-ms)
|
||||
@@ -0,0 +1,19 @@
|
||||
**English** · [Dansk](Step-7-da) · [Bahasa Melayu](Step-7-ms)
|
||||
|
||||
# Step 7: Verify on the My Bookings page
|
||||
|
||||
## What you gain
|
||||
|
||||
You learn to check your own work from a second source: `get_my_bookings()` reads the My Bookings page (bookings and waitlist entries) and `verify_bookings()` compares it with what the bot did. The result is SUCCESS or MISMATCH (exit code 3).
|
||||
|
||||
## Where it is in the code
|
||||
|
||||
`src/snack_and_lift/application/verification.py`
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
> This image is an illustration: the bot ran against the project's local test pages (`tests/e2e/fixtures`), not against the live gym site. A run with a real account and `--screenshot-dir` (see [Getting started](Getting-Started)) replaces it.
|
||||
|
||||
[Previous: 6](Step-6) · [Home](Home) · [Next: 8](Step-8)
|
||||
@@ -0,0 +1,19 @@
|
||||
[English](Step-8) · **Dansk** · [Bahasa Melayu](Step-8-ms)
|
||||
|
||||
# Trin 8: Kvalitetssikring med tidsrejse
|
||||
|
||||
## Hvad du får ud af det
|
||||
|
||||
Du lærer at datoer er den almindelige fejlkilde: sidens admin-knap "advance 3 days" flytter dens ur. Botten læser dagsetiketterne ("Today", "Tomorrow", "Thu, Aug 14") i stedet for at antage en dato, så den også virker efter et tidshop.
|
||||
|
||||
## Hvor det er i koden
|
||||
|
||||
`src/snack_and_lift/domain/day_label.py`
|
||||
|
||||
## Skærmbillede
|
||||
|
||||

|
||||
|
||||
> Dette billede er en illustration: botten kørte mod projektets lokale testsider (`tests/e2e/fixtures`), ikke mod det rigtige gym-site. En kørsel med en rigtig konto og `--screenshot-dir` (se [Kom i gang](Getting-Started-da)) erstatter det.
|
||||
|
||||
[Forrige: 7](Step-7-da) · [Forside](Home-da) · [Næste: 9](Step-9-da)
|
||||
@@ -0,0 +1,19 @@
|
||||
[English](Step-8) · [Dansk](Step-8-da) · **Bahasa Melayu**
|
||||
|
||||
# Langkah 8: Jaminan kualiti dengan perjalanan masa
|
||||
|
||||
## Apa yang anda peroleh
|
||||
|
||||
Anda belajar bahawa tarikh ialah punca pepijat biasa: kawalan admin laman "advance 3 days" menggerakkan jamnya. Bot membaca label hari ("Today", "Tomorrow", "Thu, Aug 14") dan bukannya menganggap tarikh, jadi ia berfungsi selepas lompatan masa juga.
|
||||
|
||||
## Di mana dalam kod
|
||||
|
||||
`src/snack_and_lift/domain/day_label.py`
|
||||
|
||||
## Tangkapan skrin
|
||||
|
||||

|
||||
|
||||
> Imej ini ialah ilustrasi: bot dijalankan terhadap halaman ujian tempatan projek (`tests/e2e/fixtures`), bukan laman gim sebenar. Larian dengan akaun sebenar dan `--screenshot-dir` (lihat [Mula menggunakan](Getting-Started-ms)) menggantikannya.
|
||||
|
||||
[Sebelumnya: 7](Step-7-ms) · [Utama](Home-ms) · [Seterusnya: 9](Step-9-ms)
|
||||
@@ -0,0 +1,19 @@
|
||||
**English** · [Dansk](Step-8-da) · [Bahasa Melayu](Step-8-ms)
|
||||
|
||||
# Step 8: Time travel quality assurance
|
||||
|
||||
## What you gain
|
||||
|
||||
You learn that dates are the usual bug: the site's admin control "advance 3 days" moves its clock. The bot reads the day labels ("Today", "Tomorrow", "Thu, Aug 14") instead of assuming a date, so it works after a time jump too.
|
||||
|
||||
## Where it is in the code
|
||||
|
||||
`src/snack_and_lift/domain/day_label.py`
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
> This image is an illustration: the bot ran against the project's local test pages (`tests/e2e/fixtures`), not against the live gym site. A run with a real account and `--screenshot-dir` (see [Getting started](Getting-Started)) replaces it.
|
||||
|
||||
[Previous: 7](Step-7) · [Home](Home) · [Next: 9](Step-9)
|
||||
@@ -0,0 +1,19 @@
|
||||
[English](Step-9) · **Dansk** · [Bahasa Melayu](Step-9-ms)
|
||||
|
||||
# Trin 9: Robusthed mod netværksfejl
|
||||
|
||||
## Hvad du får ud af det
|
||||
|
||||
Du lærer at prøve igen: `retry(func, retries=7, description=None)` gentager et kald der kaster en fejl, venter mellem forsøgene, logger en advarsel og kaster RetriesExhaustedError til sidst. `login()`, `book_class()` og `get_my_bookings()` er pakket ind i den, så en side der taber forespørgsler ikke stopper kørslen.
|
||||
|
||||
## Hvor det er i koden
|
||||
|
||||
`src/snack_and_lift/application/retry.py`
|
||||
|
||||
## Skærmbillede
|
||||
|
||||

|
||||
|
||||
> Dette billede er en illustration: botten kørte mod projektets lokale testsider (`tests/e2e/fixtures`), ikke mod det rigtige gym-site. En kørsel med en rigtig konto og `--screenshot-dir` (se [Kom i gang](Getting-Started-da)) erstatter det.
|
||||
|
||||
[Forrige: 8](Step-8-da) · [Forside](Home-da)
|
||||
@@ -0,0 +1,19 @@
|
||||
[English](Step-9) · [Dansk](Step-9-da) · **Bahasa Melayu**
|
||||
|
||||
# Langkah 9: Ketahanan terhadap kegagalan rangkaian
|
||||
|
||||
## Apa yang anda peroleh
|
||||
|
||||
Anda belajar mencuba semula: `retry(func, retries=7, description=None)` mengulang panggilan yang gagal, menunggu antara percubaan, mencatat amaran dan melempar RetriesExhaustedError di akhir. `login()`, `book_class()` dan `get_my_bookings()` dibalut dengannya, jadi laman yang menjatuhkan permintaan tidak menghentikan larian.
|
||||
|
||||
## Di mana dalam kod
|
||||
|
||||
`src/snack_and_lift/application/retry.py`
|
||||
|
||||
## Tangkapan skrin
|
||||
|
||||

|
||||
|
||||
> Imej ini ialah ilustrasi: bot dijalankan terhadap halaman ujian tempatan projek (`tests/e2e/fixtures`), bukan laman gim sebenar. Larian dengan akaun sebenar dan `--screenshot-dir` (lihat [Mula menggunakan](Getting-Started-ms)) menggantikannya.
|
||||
|
||||
[Sebelumnya: 8](Step-8-ms) · [Utama](Home-ms)
|
||||
@@ -0,0 +1,19 @@
|
||||
**English** · [Dansk](Step-9-da) · [Bahasa Melayu](Step-9-ms)
|
||||
|
||||
# Step 9: Resilience against network failures
|
||||
|
||||
## What you gain
|
||||
|
||||
You learn to retry: `retry(func, retries=7, description=None)` repeats a call that raises, waits between attempts, logs a warning and raises RetriesExhaustedError at the end. `login()`, `book_class()` and `get_my_bookings()` are wrapped with it, so a site that drops requests does not stop the run.
|
||||
|
||||
## Where it is in the code
|
||||
|
||||
`src/snack_and_lift/application/retry.py`
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
> This image is an illustration: the bot ran against the project's local test pages (`tests/e2e/fixtures`), not against the live gym site. A run with a real account and `--screenshot-dir` (see [Getting started](Getting-Started)) replaces it.
|
||||
|
||||
[Previous: 8](Step-8) · [Home](Home)
|
||||
@@ -0,0 +1,12 @@
|
||||
[English](Troubleshooting) · **Dansk** · [Bahasa Melayu](Troubleshooting-ms)
|
||||
|
||||
# Fejlfinding
|
||||
|
||||
| Symptom | Årsag og løsning |
|
||||
| --- | --- |
|
||||
| Exitkode 2 | `SNACK_AND_LIFT_EMAIL` eller `SNACK_AND_LIFT_PASSWORD` er ikke sat. |
|
||||
| Login fejler 7 gange, derefter exit 1 | Forkert e-mail eller adgangskode, eller kontoen findes ikke i denne `chrome_profile` (siden gemmer konti i browseren). Opret den igen med `--keep-open`. |
|
||||
| Exitkode 3 og MISMATCH | Siden My Bookings viser ikke det botten bookede. Kør igen; tjek om et hold var fyldt. |
|
||||
| Advarsler "attempt 2 of 7" | Sidens fejlrate er slået til; det er retry der arbejder (trin 9). |
|
||||
| Chrome advarer om adgangskoden | Chrome markerer svage adgangskoder som `12346`; vælg en anden. |
|
||||
| Intet vindue i Docker | Containeren er headless med vilje; brug `--screenshot-dir`. |
|
||||
@@ -0,0 +1,12 @@
|
||||
[English](Troubleshooting) · [Dansk](Troubleshooting-da) · **Bahasa Melayu**
|
||||
|
||||
# Menyelesaikan masalah
|
||||
|
||||
| Gejala | Punca dan penyelesaian |
|
||||
| --- | --- |
|
||||
| Kod keluar 2 | `SNACK_AND_LIFT_EMAIL` atau `SNACK_AND_LIFT_PASSWORD` tidak ditetapkan. |
|
||||
| Log masuk gagal 7 kali, kemudian keluar 1 | E-mel atau kata laluan salah, atau akaun tiada dalam `chrome_profile` ini (laman menyimpan akaun dalam pelayar). Daftar semula dengan `--keep-open`. |
|
||||
| Kod keluar 3 dan MISMATCH | Halaman My Bookings tidak menyenaraikan apa yang ditempah bot. Jalankan semula; semak sama ada kelas penuh. |
|
||||
| Amaran "attempt 2 of 7" | Kadar kegagalan laman dihidupkan; ini retry sedang berfungsi (langkah 9). |
|
||||
| Chrome memberi amaran tentang kata laluan | Chrome menandakan kata laluan lemah seperti `12346`; pilih yang lain. |
|
||||
| Tiada tetingkap dalam Docker | Bekas ini tanpa kepala (headless) secara sengaja; gunakan `--screenshot-dir`. |
|
||||
@@ -0,0 +1,12 @@
|
||||
**English** · [Dansk](Troubleshooting-da) · [Bahasa Melayu](Troubleshooting-ms)
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
| Symptom | Cause and fix |
|
||||
| --- | --- |
|
||||
| Exit code 2 | `SNACK_AND_LIFT_EMAIL` or `SNACK_AND_LIFT_PASSWORD` is not set. |
|
||||
| Login fails 7 times, then exit 1 | Wrong e-mail or password, or the account is not in this `chrome_profile` (the site keeps accounts in the browser). Register again with `--keep-open`. |
|
||||
| Exit code 3 and MISMATCH | The My Bookings page does not list what the bot booked. Run again; check whether a class was full. |
|
||||
| Warnings "attempt 2 of 7" | The site's failure rate is on; this is the retry at work (step 9). |
|
||||
| Chrome warns about the password | Chrome flags weak passwords such as `12346`; choose another. |
|
||||
| No window in Docker | The container is headless by design; use `--screenshot-dir`. |
|
||||
@@ -0,0 +1 @@
|
||||
[Repository](https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/049-snack-and-lift) · [Snack and Lift](https://appbrewery.github.io/gym/) · AGPL-3.0
|
||||
@@ -0,0 +1,11 @@
|
||||
**English**
|
||||
- [Home](Home) · [Getting started](Getting-Started) · [Troubleshooting](Troubleshooting)
|
||||
- [1](Step-1) · [2](Step-2) · [3](Step-3) · [4](Step-4) · [5](Step-5) · [6](Step-6) · [7](Step-7) · [8](Step-8) · [9](Step-9)
|
||||
|
||||
**Dansk**
|
||||
- [Forside](Home-da) · [Kom i gang](Getting-Started-da) · [Fejlfinding](Troubleshooting-da)
|
||||
- [1](Step-1-da) · [2](Step-2-da) · [3](Step-3-da) · [4](Step-4-da) · [5](Step-5-da) · [6](Step-6-da) · [7](Step-7-da) · [8](Step-8-da) · [9](Step-9-da)
|
||||
|
||||
**Bahasa Melayu**
|
||||
- [Utama](Home-ms) · [Mula menggunakan](Getting-Started-ms) · [Menyelesaikan masalah](Troubleshooting-ms)
|
||||
- [1](Step-1-ms) · [2](Step-2-ms) · [3](Step-3-ms) · [4](Step-4-ms) · [5](Step-5-ms) · [6](Step-6-ms) · [7](Step-7-ms) · [8](Step-8-ms) · [9](Step-9-ms)
|
||||