# COMP1010 Exam Notes

---

## 1. Displaying data from a list of dicts using `pyhtml`

When looping over a list of dicts (eg. player data) to build HTML, you build a
plain Python list and pass it to a `pyhtml` element. Do **not** wrap list items
in extra `p.p()` calls unless you actually want a paragraph per item.

```python
# Example: display all players on the home page
stuff_to_display = []
for player in data:
    stuff_to_display.append(p.a(href=f"/player/{player["id"]}")(p.h4(player["name"])))
    stuff_to_display.append(p.p(f"Elo: {player["elo"]}"))

return str(p.html(p.body(p.main(p.p(stuff_to_display)))))
```

The same pattern works inside a helper function to keep things tidy:

```python
def past_games_html():
    items = []
    for game in player["past_games"]:
        items.append(p.p(f"Against '{game["opponent"]}' -> {game["result"]}"))
    return items
```

---

## 2. Getting form data with `request.form`

There are two ways to get form data:

```python
name = request.form["name"]           # raises KeyError if field is missing
name = request.form.get("name")       # returns None if field is missing
name = request.form.get("name", "")   # returns "" if field is missing
```

Use `request.form["name"]` when you are certain the field will always be
present (eg. a required input). Use `request.form.get("name", "")` when the
field might be missing or optional — it is the safer choice.

The string in the brackets must match the `name=` attribute of your HTML
`<input>` or `<select>`:

```python
# HTML form field:
p.input(type="text", name="player_name", id="player_name")

# Flask POST route:
@app.post("/add_player")
def add_player_submit():
    id      = request.form.get("id", "")
    name    = request.form.get("name", "")
    faculty = request.form.get("faculty", "")
    add_player(get_data(), id, name, faculty)
    return redirect(f"/player/{id}")
```

---

## 3. Flask: `GET` vs `POST`

| Method | Use for | Decorator |
|---|---|---|
| `GET` | Loading/displaying a page | `@app.get("/route")` |
| `POST` | Submitting a form / changing data | `@app.post("/route")` |

A form page typically needs **two** routes with the same URL: one GET to show
the form, one POST to handle the submission:

```python
@app.get("/add_player")       # shows the form
def add_player_form():
    return str(p.html(...))

@app.post("/add_player")      # handles the submit
def add_player_submit():
    name = request.form.get("name", "")
    # ... do something ...
    return redirect("/")
```

URL parameters (eg. player ID) go in the GET route:

```python
@app.get("/player/<player_id>")
def player_page(player_id: str):
    player = get_player_by_id(get_data(), player_id)
    ...
```

---

## 3b. Form inputs in pyhtml

Common form input types and when to use them.

**Text input** — free-text answer:
```python
p.label(for_="name")("Name: "),
p.input(type="text", id="name", name="name", required=True),
```

**Number input** — numeric only (add `step="0.1"` for decimals):
```python
p.label(for_="age")("Age: "),
p.input(type="number", id="age", name="age", min="0", max="120", required=True),
```

**Date input** — picks a calendar date:
```python
p.label(for_="date")("Date: "),
p.input(type="date", id="date", name="date", required=True),
```

**Dropdown (`p.select` with `p.option`)** — pick exactly one from a fixed list of options. Use this when the values are known in advance:
```python
p.label(for_="type")("Type: "),
p.select(id="type", name="type")(
    p.option(value="lost")("lost"),
    p.option(value="found")("found"),
),
```

The `value=` attribute is what gets submitted; the text inside `p.option(...)("text")` is what the user sees. Building options dynamically from a list:
```python
options = []
for cat in CATEGORIES:
    options.append(p.option(value=cat)(cat))

p.select(id="category", name="category")(options)
```

**Radio buttons** — pick exactly one from a small set, shown all at once (good for 2-4 options):
```python
p.label(for_="type_lost")("Lost: "),
p.input(type="radio", id="type_lost", name="type", value="lost"),
p.label(for_="type_found")("Found: "),
p.input(type="radio", id="type_found", name="type", value="found"),
```

Important: all radios in the same group must share the **same `name`** but have **different `id`s** and **different `value`s**. Only the selected one is submitted.

**Checkbox** — boolean yes/no, or multi-select if grouped:
```python
p.label(for_="urgent")("Mark as urgent: "),
p.input(type="checkbox", id="urgent", name="urgent", value="yes"),
```

A checkbox is **only sent in the form data if it was checked**. Read it with:
```python
is_urgent = request.form.get("urgent") == "yes"   # True if ticked
```

**Textarea** — multi-line free text (descriptions, comments, reviews). Use this instead of `type="text"` when you need more than a single line:
```python
p.label(for_="description")("Description: "),
p.textarea(id="description", name="description", rows="4", cols="40", required=True),
```

`rows` and `cols` set the visible size. Read it like any other field: `request.form.get("description")`. **Important:** `p.textarea` is its own element, NOT `p.input(type="textarea")` — that's not a valid HTML type.

**Submit button** — always at the end:
```python
p.input(type="submit", value="Add item", formaction="/add"),
```

### Quick reference: which input type when?

| Use case | Best input |
|---|---|
| Pick one from many known values | `p.select` with `p.option` |
| Pick one from 2-4 visible values | radio buttons |
| Yes/no toggle | checkbox |
| Free text (name, comment) | `type="text"` |
| Multi-line free text | `p.textarea` |
| Whole number (age, rating) | `type="number"` |
| Decimal (price, weight) | `type="number"` + `step="0.1"` |
| Calendar date | `type="date"` |

---

## 4. Sessions and cookies

**Cookies** are small pieces of data stored in the browser. Flask manages them
for you via the `session` object. Import it with:

```python
from flask import session
```

Set a value:

```python
session["username"] = "magnus"
```

Read a value — both ways work:

```python
username = session["username"]          # raises KeyError if not set
username = session.get("username")      # returns None if not set
username = session.get("username", "")  # returns "" if not set
```

Prefer `session.get()` when you are not sure the key exists (eg. checking if
someone is logged in). Use `session["key"]` only when you are certain it is
there.

Delete a value (eg. on logout):

```python
session.pop("username", None)   # removes key, does nothing if missing
```

Flask requires a secret key to sign the session cookie:

```python
app.secret_key = "some secret string"
```

---

## 5. `jsonhelpers` — `create_if_not_exist` vs `write_json_file`

| Function | Behaviour |
|---|---|
| `create_if_not_exist(filename, empty_data)` | Creates the file with `empty_data` **only if it does not exist yet**. Does nothing if the file already exists. |
| `write_json_file(filename, data)` | **Always** writes to the file. Creates it if missing, overwrites it if it exists. |
| `read_json_file(filename)` | Reads and returns the content of a JSON file. |

Typical usage pattern:

```python
create_if_not_exist("tournament.json", [])   # make sure file exists on startup
data = read_json_file("tournament.json")     # load current data
# ... modify data ...
write_json_file("tournament.json", data)     # save it back
```

### Defensive `get_data` with `try/except`

When the exam spec says *"if the file does not exist or cannot be read, return an empty list"*, `create_if_not_exist` only handles the "does not exist" case. To also handle a **corrupted or unreadable** file, wrap the read in `try/except`:

```python
def get_data():
    try:
        return read_json_file("reports.json")
    except:
        return []
```

This catches any error (missing file, bad JSON, permission issue) and returns `[]` so the server doesn't crash.

---

## 6. The `round()` function

`round(number)` rounds to the nearest integer. Pass a second argument to
control decimal places. **Always assign the result back**, otherwise it is
thrown away.

```python
# Wrong — does nothing:
round(player["elo"])

# Correct — round to nearest integer:
player["elo"] = round(player["elo"])          # 2826.7 -> 2827

# Round to 1 decimal place:
value = round(3.14159, 1)                     # -> 3.1

# Round to 2 decimal places:
value = round(3.14159, 2)                     # -> 3.14

# Round to nearest 5:
value = round(value / 5) * 5                  # 13 -> 15, 12 -> 10

# Round in one step during a calculation:
player["elo"] = round(player["elo"] + elo_change)
```

---

## 7. Common list methods

```python
my_list = [1, 2, 3]

my_list.append(4)         # add to end               -> [1, 2, 3, 4]
my_list.pop()             # remove & return last item -> [1, 2, 3]
my_list.pop(0)            # remove & return at index 0
my_list.remove(2)         # remove first occurrence of value 2
my_list.insert(0, 99)     # insert 99 at index 0
my_list.sort()            # sort in place (ascending)
my_list.reverse()         # reverse in place

len(my_list)              # number of items
3 in my_list              # True if 3 is in the list
my_list[0]                # first item
my_list[-1]               # last item
my_list[1:3]              # slice: items at index 1 and 2
```

Looping patterns:

```python
for item in my_list:                    # basic loop
    print(item)

for i, item in enumerate(my_list):     # loop with index
    print(i, item)
```

Integer division with `//` — useful when you need a whole number result
(eg. calculating an index, splitting items evenly):

```python
10 // 3     # -> 3   (rounds down, no decimals)
10 / 3      # -> 3.333...  (normal division, gives float)
10 % 3      # -> 1   (remainder / modulo)

# Example: split players into groups of 2
group = index // 2
```

**String slicing** — slicing works on strings exactly like lists. The full syntax is
`[start:stop:step]`. The most useful trick is reversing with `[::-1]`:

```python
word = "racecar"
word[0]       # -> "r"          first character
word[-1]      # -> "r"          last character
word[1:4]     # -> "ace"        characters at index 1, 2, 3
word[:3]      # -> "rac"        first 3 characters
word[::2]     # -> "rcea"       every second character
word[::-1]    # -> "racecar"    entire string reversed (step = -1 = go backwards)
```

`[::-1]` means: start at the end, stop at the beginning, step backwards one
character at a time. This is the standard Python way to reverse any string or list.

---

## 8. Common dict methods

```python
player = {"id": "magnus", "elo": 2831, "faculty": "science"}

player["elo"]                   # access value by key
player["elo"] = 2850            # set / update value
player.get("elo")               # same but returns None if key missing
player.get("elo", 0)            # returns 0 if key missing

player.keys()                   # all keys
player.values()                 # all values
player.items()                  # all (key, value) pairs as tuples

"elo" in player                 # True if key exists
del player["faculty"]           # delete a key
```

Looping patterns:

```python
for key in player:
    print(key)

for key, value in player.items():
    print(key, value)
```

Searching a list of dicts (eg. find player by id):

```python
for player in data:
    if player["id"] == "magnus":
        return player
```

---

## 9. General keep-in-mind

**Do not remove elements from a list while looping over it.** Python will skip
items because the indices shift as you delete. Instead, collect the items to
remove first, then delete them after the loop:

```python
# Wrong — skips elements:
for player in data:
    if player["elo"] < 1000:
        data.remove(player)

# Correct — collect first, then delete:
to_remove = []
for player in data:
    if player["elo"] < 1000:
        to_remove.append(player)

for player in to_remove:
    data.remove(player)

# Alternative — build a new list instead:
data = [player for player in data if player["elo"] >= 1000]
```

---

## 10. CSS quick-start snippet

Add this to your `<head>` in Python:

```python
p.link(rel="stylesheet", href="/static/style.css")
```

### Inline styles (quick fixes without touching style.css)

For one-off styling on a single element, you can pass a `style=` attribute
directly to a pyhtml element. Handy in the exam if you just need to tweak
something small and don't want to edit the stylesheet:

```python
p.p(style="font-size: 10px;")("small footer text"),
p.h1(style="color: red;")("Warning!"),
p.div(style="text-align: center; margin-top: 20px;")(...),
```

Multiple properties go in the same string, separated by semicolons. For
anything reusable or applied to many elements, use `style.css` instead.

Put this in `static/style.css`:

```css
body {
    background-color: #9ecbff;
}

h1, p {
    color: rgb(8, 0, 72);
}

main {
    background-color: #9efffa;
    margin: -10px;
    padding: 10px 10px 10px 20px;
    text-align: center;
}

input[type="submit"] {
    color: black;
    background-color: #9ecbff;
    padding: 8px;
    border-radius: 2px;
    border-color: #9efffa;
}

input[type="submit"]:hover {
    color: rgb(0, 0, 0);
    background-color: #9efffa;
    border-color: #9ecbff;
}

input, select {
    margin: 10px;
}
```

### Common pitfall: centering a `<ul>` with bullets

When you set `text-align: center` on a container that holds a `<ul>`, the **list items** get centered but the **bullets** stay anchored to the far left of the page. This looks broken.

**Why:** `<ul>` is a block element that takes the full width of its container; `text-align: center` only centers text inside it, not the `<ul>` itself. The bullets sit at the left edge of the `<ul>`'s padding, regardless of where the text appears.

**Fix:** shrink the `<ul>` to its content width with `display: inline-block`, then keep its inner text left-aligned:

```css
ul {
    display: inline-block;   /* makes ul shrink to content width */
    text-align: left;        /* text inside ul reads naturally */
}
```

Now the parent's `text-align: center` will centre the `<ul>` like a word, and bullets sit right next to their items.

---

## 10b. CSS grid layout template (advanced)

Use this when you want a structured page layout with header, content, sidebar,
and footer — all arranged using CSS Grid.

### pyhtml structure

```python
# Use divs with class names that match your CSS grid areas.
# Content is just shown as comments here — fill in actual pyhtml elements.

p.div(class_="container")(
    p.div(class_="header")(
        p.div(class_="title")(
            # p.h1("Page Title")
        ),
        p.div(class_="login")(
            # p.p("Hi, username") or login info
        ),
    ),
    p.div(class_="content")(
        # main content, eg list of movies/players
    ),
    p.div(class_="menu")(
        # sidebar: forms, filters, actions
    ),
    p.div(class_="footer")(
        # p.p("@UNSW practice exam")
    ),
)
```

### style.css — animated diagonal stripes background + grid

```css
/* ── Animated blue stripes background ── */
body {
    background: linear-gradient(-45deg, #0066cc, #1a8fff, #0055aa, #33aaff);
    background-size: 400% 400%;
    animation: gradientShift 8s ease infinite;
    min-height: 100vh;
    margin: 0;
}

@keyframes gradientShift {
    0%   { background-position: 0% 50%; }
    50%  { background-position: 100% 50%; }
    100% { background-position: 0% 50%; }
}

body::before {
    content: "";
    position: fixed;
    inset: 0;
    background: repeating-linear-gradient(
        45deg,
        transparent                0px, transparent                24px,
        rgba(255,255,255,0.08)    24px, rgba(255,255,255,0.08)    25px,
        transparent               25px, transparent               55px,
        rgba(255,255,255,0.16)    55px, rgba(255,255,255,0.16)    57px,
        transparent               57px, transparent               85px,
        rgba(255,255,255,0.06)    85px, rgba(255,255,255,0.06)    86px,
        transparent               86px, transparent              100px
    );
    animation: moveLines 9s linear infinite;
    z-index: -1;
    pointer-events: none;
}

@keyframes moveLines {
    from { background-position: 0 0; }
    to   { background-position: 141px 141px; }
}

/* ── Grid container ── */
.container {
    display: grid;
    grid-template-areas:
        "header  header"
        "content menu"
        "footer  footer";
    grid-template-columns: 1fr 1fr;
    gap: 3px;
    background-color: rgba(255, 255, 255, 0.1);
    padding: 5px;
    border: 3px solid rgba(255, 255, 255, 0.4);
}

.container div {
    background-color: rgba(255, 255, 255, 0.2);
    padding: 10px;
}

/* ── Header row (split into title + login) ── */
.header {
    grid-area: header;
    display: grid;
    grid-template-areas: "title login";
    grid-template-columns: 1fr 1fr;
}

.header div.title {
    grid-area: title;
    text-align: left;
    background-color: transparent;
}

.header div.login {
    grid-area: login;
    text-align: right;
    background-color: transparent;
}

/* ── Content area (main list) ── */
.container div.content {
    grid-area: content;
    text-align: center;
}

/* ── Menu / form sidebar ── */
.container div.menu {
    grid-area: menu;
    text-align: center;
}

.container div.menu input {
    margin: 10px;
}

.container div.menu input[type="submit"] {
    background-color: greenyellow;
    padding: 5px 15px;
    border-radius: 2px;
    margin: 10px;
}

.container div.menu input[type="submit"]:hover {
    background-color: rgb(0, 170, 255);
}

/* ── Footer ── */
.container div.footer {
    grid-area: footer;
    text-align: center;
    padding: 50px;
}
```

### How CSS Grid areas work

```
grid-template-areas:
    "header  header"    <- header spans both columns
    "content menu"      <- content left, menu/form right
    "footer  footer";   <- footer spans both columns
```

Each string is a row. Each word is a column. Repeat a name to make it span
multiple columns. Then assign each div to its area with `grid-area: header;` etc.

To change the column split (eg. 2/3 content, 1/3 menu):
```css
grid-template-columns: 2fr 1fr;   /* content gets twice the space */
```

These are the standard usability principles you should reference by name in exam answers.
Source: Nielsen Norman Group (nngroup.com/articles/ten-usability-heuristics)

---

### #1 Visibility of System Status
Always keep the user informed about what is happening, with timely feedback.

**In your app:** After submitting a form, redirect to the updated page so users can see their change took effect. Show the current score after adding a review. A page that just goes blank gives no feedback.

---

### #2 Match Between the System and the Real World
Use words and concepts familiar to the user, not internal technical jargon. Information should appear in a natural, logical order.

**In your app:** Label a field "Your name" not "reviewer_id". Use "Add review" not "POST /add_review". Show a score out of 10, not a raw float like 7.428571.

---

### #3 User Control and Freedom
Users make mistakes — give them a clear way to undo or go back without having to go through a long process.

**In your app:** A "Reset tournament" or "Clear" button with a confirmation step. A cancel button on forms. Easy navigation back to the home page from any page.

---

### #4 Consistency and Standards
Users should not have to wonder whether different words or actions mean the same thing. Follow conventions — internal (within your app) and external (what users expect from other websites).

**In your app:** Same navbar on every page. Same button style throughout. Using "Submit" for all form buttons, not "Submit" on one and "Go" on another. This is why a shared `navbar_html()` function is good practice.

---

### #5 Error Prevention
The best design prevents errors from happening in the first place, rather than just handling them after.

**In your app:** Use `required=True` on mandatory fields. Use a `<select>` dropdown for genre instead of a free-text field (prevents typos). Use `type="number"` with `min="1" max="10"` for ratings so users cannot enter invalid values.

---

### #6 Recognition Rather than Recall
Minimise what users have to remember. Options, actions, and information should be visible or easy to find, not hidden in memory.

**In your app:** Show all movies in a dropdown when adding a review (recognition) rather than asking users to type a movie ID from memory (recall). Show the current score on the movie page so users do not have to remember it.

---

### #7 Flexibility and Efficiency of Use
The design should work well for both beginners and experienced users. Shortcuts and accelerators help power users without confusing newcomers.

**In your app:** Not heavily tested at this level, but a relevant example is linking a player's name directly to their page from the home page (shortcut) rather than requiring navigation through a separate search.

---

### #8 Aesthetic and Minimalist Design
Do not show information that is irrelevant or rarely needed. Every extra element competes with the important content.

**In your app:** Do not display raw internal IDs to users. On the home page, show just the key info (title, score, review count) rather than the full list of every review. Keep forms clean — only ask for what you actually need.

---

### #9 Help Users Recognize, Diagnose, and Recover from Errors
Error messages should be in plain language, explain what went wrong, and suggest a fix. No technical jargon or error codes.

**In your app:** If a movie ID is not found, say "Movie not found — go back to the home page" not just a Python crash page. If a form field is invalid, highlight it and explain what is expected.

---

### #10 Help and Documentation
Ideally the interface is self-explanatory, but when documentation is needed it should be easy to search, focused on the user's task, and list concrete steps.

**In your app:** Placeholder text in inputs (eg. `placeholder="eg. inception"`) acts as inline help. A short description on the Add Movie page explaining what an ID is would be good documentation.

---

### Quick exam cheat-sheet: map your features to heuristics

| Feature you built | Heuristic to cite |
|---|---|
| Redirect after form submit | #1 Visibility of System Status |
| Human-readable labels, not variable names | #2 Match with Real World |
| Navbar on every page / Cancel buttons | #3 User Control and Freedom |
| Shared `navbar_html()` function | #4 Consistency and Standards |
| `required=True`, dropdowns, number inputs | #5 Error Prevention |
| Movie dropdown when adding review | #6 Recognition Rather than Recall |
| Direct links from home page to movie page | #7 Flexibility and Efficiency |
| Only show key info on home page | #8 Aesthetic and Minimalist Design |
| Friendly "not found" message | #9 Help Users Recover from Errors |
| Placeholder text in form fields | #10 Help and Documentation |

---

### Practical checklist (implementation reminders)

- Same navbar on every page.
- Consistent color scheme throughout.
- Use `type="number"`, `type="email"`, `type="date"` where appropriate. For decimal inputs add `step="0.1"` (or `step="0.01"` etc.) — without it the browser only accepts whole numbers: `p.input(type="number", name="price", step="0.1")`.
- Add `required=True` to mandatory fields.
- Add `placeholder=` hints to text inputs.
- Pair every `p.label(for_="x")` with `p.input(id="x")`.
- Use `p.select` for fixed-choice fields.
- Validate input in your route and show a friendly error if invalid.
- Redirect to the updated page after any form submission.

---

## 12. Copying lists and dicts (aliasing trap)

When you write `list_b = list_a`, you are **not** making a copy. Both names
point to the exact same object in memory. Changing one changes the other:

```python
list_a = ["sunflower", "rose", "tulip"]
list_b = list_a
list_a.remove("rose")

print(list_a)   # ['sunflower', 'tulip']
print(list_b)   # ['sunflower', 'tulip']  <- also changed!
```

To get a real independent copy, use one of these:

```python
list_b = list_a.copy()    # preferred, most readable
list_b = list_a[:]        # slicing trick
list_b = list(list_a)     # constructor

dict_b = dict_a.copy()    # same for dicts
```

For nested structures (eg. a list of dicts), use `deepcopy` — otherwise the
inner dicts are still shared:

```python
from copy import deepcopy
data_copy = deepcopy(data)
```

**Mutable vs immutable — does this affect strings too?**

| Type | Mutable? | Two names stay in sync? |
|---|---|---|
| `list` | Yes | Yes, changes affect both names |
| `dict` | Yes | Yes, changes affect both names |
| `str` | No | No, "changing" a string creates a new object |
| `int` | No | No, same reason as strings |

Strings are safe because you can never change them in place — any operation
produces a brand new string, so the original name is unaffected:

```python
str_a = "Hello"
str_b = str_a
str_a = str_a + " World"   # creates a NEW string

print(str_a)   # "Hello World"
print(str_b)   # "Hello"  <- untouched
```

Quick check — use `is` to see if two names point to the same object:

```python
list_b = list_a         # same object
print(list_a is list_b) # True

list_b = list_a.copy()  # different object
print(list_a is list_b) # False
```

---

## 13. Key definitions

| Term | Meaning |
|---|---|
| **Module** | A single `.py` file containing Python code (functions, variables, classes). |
| **Package** | A folder of modules, usually with an `__init__.py`, treated as one importable unit. |
| **Library** | A collection of packages/modules you install (eg. `colorama`, `flask`, `jestspectation`). |
| **`import`** | Loads a module or library so you can use it. |
| **`from x import *`** | Imports everything from module `x` into the current namespace. |
| **Tuple** | An immutable ordered collection: `(1, 2, 3)`. Cannot be changed after creation. Watch out for accidental tuples from a trailing comma: `x = {"a": 1},` makes a tuple, not a dict! |
| **List** | A mutable ordered collection: `[1, 2, 3]`. Can be changed after creation. |
| **Dict** | A mutable key-value store: `{"key": "value"}`. |
| **Mutation** | Changing an object in place (eg. `list.append()`). Functions that mutate their arguments do not need to return anything. |
| **`deepcopy`** | Creates a fully independent copy of a list/dict including all nested objects. Use when you do not want changes to affect the original. |
| **`None`** | Python's "nothing" value. Functions that do not explicitly `return` anything return `None`. Check with `if x is None`. |
| **f-string** | String with embedded expressions: `f"Hello {name}, your elo is {elo}"`. |
| **`uv`** | A Python package manager used in this course. Use `uv sync` to install dependencies and `uv run file.py` to run scripts. |
| **`pyproject.toml`** | Config file that tells `uv` which packages the project needs. Must be present for `uv sync` to work. |
