# COMP1010 EXAM NOTES, Ngoc Yen Nhi Pham (z5573974)
## Python (W1-3)
Introduction: https://www.w3schools.com/python/python_intro.asp
### Input, Output & Data Types

#### Syntax
 - Indentation: Spaces at beginning of code line
   - Most common use = 4 spaces
 - Comment = Explain code, readable, prevent execution when testing code
   - `#` for a line, `"""` for multiple comments
 - Statements = programming instructions


#### Variable
- Python has no command for declaring variable, it is created when we assign value to it (x = 5, y = "John")
- Case sensitive, can use single/double quote
- Rules for variables: https://www.w3schools.com/python/python_variables_names.asp
- Don't need to be declared with a particular type, it can change type after set

```
type() = Check which type this variable is
Casting = change its type
`int()`, `float()`, `str()` to change type
```

- Global variable = variable created outside of a function, can be used both inside/outside functions
- Local variable = only used inside that function
- To create global v inside a function, use `global`

- Output = `print()`
 - Print without new line = `print(..., end=" ")`
> print() = output variables
>
> If variable are different, use comma instead of +

#### Data Types

| Type | Name | Example |
| ---- | ---- | ---- |
| Text | `str` | x = "Hi" |
| Numeric | `int`, `float`, `complex` | x = 20, 20.0, 20j |
| Sequence | `list`, `tuple`, `range` | x = [a, b, c], x = (a, b, c), x = range(6) |
| Mapping | `dict` | x = {"name" : "A", "age" : "5"} |
| Set | `set`, `frozenset` | x = {a, b, c}, x = fronzenset({a, b, c}) |
| Boolean | `bool` | x = True |
| Binary | `bytes`, `bytearray`, `memoryview` | ... |
| None | `NoneType` | x = None |

#### IF-ELSE-ELIF
1. If + If
    - Use when conditions are checked separately
    - Multiple blocks run if multiple condition is true
    - 2 ifs are independent
2. If + Else
    - Use when there're only 2 possible paths
        - If condition true => if run
        - If condition false => else run
    - Only one block runs
3. If + Elif
    - Use when there're multiple related conditions
    - Only 1 block runs
    - Check from top to bottom, if a condition is true => ignore the rest of elif

### Loops
#### WHILE
- Execute statements when **condition is true**
- Require relevant variable to be ready

| Statement | What they do |
| -------- | -------- |
| `break` | stop the loop even if condition is true |
| `continue` | stop the current iteration, continue with the next |
| `else` | run another code once the condition is no longer true |

#### FOR
- Execute set of statements **once each item in a list, dictionary...**
- Iterating over a **sequence** (of a list, tuple, dictionary, set or string)
- Don't require index variable to set before

| Statement | What they do |
| -------- | -------- |
| `break` | stop the loop before looped through all items |
| `continue` | stop the current iteration, continue with the next |
| `range()` | loop through code with a specific time |
| `else` | execute when loop finished, but NOT execute when `break` |
| `pass` | if loop has no content, `pass` would avoid error |

##### Nested Loops
- A loop inside another loop
- Inner loop with executed 1 time for each iteration of outer loop

```
adj = ["red", "big", "tasty"]
fruits = ["apple", "banana", "cherry"]

for x in adj:
  for y in fruits:
    print(x, y)
```

### Collections: Lists, Dictionaries, Data Structure
There're 4 collection data types:

| Collection | What they do |
| --- | --- |
| `list` | ordered & changeable, allow duplicate members |
| `tuple` | unordered & unchangeable, allow duplicate |
| `set` | unordered, unchangeable, unindex, no duplicate |
| `dictionary` | ordered & changeable, no duplicate |

If we want to check whether data structure function properly: `assert 1 == 1`
- Example in the Find House lecture (week 3):
```
assert find_houses(get_houses(), 3, 1, ["garage"]) == ["The house address"]
```

#### **LISTS**
**In Data Structure, List:**
1. Store multiple values/items
2. Preserve order of items
3. Store collections of records, often as a list of dictionaries
---
- `my_list = [item1, item2, item3]`
- Store multiple items in order
- Use [] to create a list
- Access list item by position/index
- Use when:
    - Many items
    - Loop through items
    - Order matters & duplicable

#### **DICTIONARY**
**In Data Structure, Dictionary:**
1. Group related data together using key-value pair
2. Map unique keys to values for quick lookup
---
- `my_dict = {"key1": value1, "key2": value 2}`
- Store labelled data using key-value pair
- Use {} to create dictionary
- Access by key
- Use when:
    - 1 thing have many properties
    - Label/name for each value
    - Lookup value using key

#### **DICTIONARIES INSIDE LIST**
```
my_list = [
    {
        "key1": value1,
        "key2": value2,
    },
    {
        "key3": value3,
        "key4": value4,
    }
]
```
- Use when:
    - Have many things
    - Each thing have same type of information
- How to access:
    - `my_list[0]` = first dictionary
    - `my_list[0]["key1"]` = value of "key1" in the first dictionary
- How to loop:
```
for item in my_list:
    print(item["key1"])
```

#### **LISTS INSIDE DICTIONARY**
```
my_dict = {
    "key1": value1,
    "key2": [item1, item2, item3]
}
```
- Use when: One thing have a property that contains many items
- How to access:
```
my_dict["key2"]
# the whole list

my_dict["key2"][0]
# first item inside that list
```
- How to loop:
```
for item in my_dict["key2"]:
    print(item)
```
#### **DICTIONARIES INSIDE DICTIONARIES**
```
my_dict = {
    "key1": {
        "subkey1": value1,
        "subkey2": value2
    }
}
```
- Use when: One thing has a property that also contains smaller properties
- How to access:
```
my_dict["key1"]
# inner dictionary

my_dict["key1"]["subkey1"]
# value inside inner dictionary
```

#### **Nested access**
`data[0]["key"]`
Read left to right:
- `data[0]` = get first item from list
- `key` = then get value from that dictionary

`data["key"][0]`
- `data["key"]` = get the list stored under key
- `[0]` = get the first item from that list


### Functions
- Only runs when it is called
- Return data as a result
- Helps avoiding code repetition

**CREATE FUNCTION**
```
def my_function():
    print("Hi")
```
**CALL FUNCTION**
```
my_function()
```
| Function | What they do |
| -------- | -------- |
| Argument | Value sent to function when it is called |
| Parameter | Variable listed inside the () in function |
Example 1:
```
def my_function(name): # name is a parameter
  print("Hello", name)

my_function("Emil") # "Emil" is an argument
```
Example 2:
```
Function must be called with correct number of args

def my_function(fname, lname):
  print(fname + " " + lname)

my_function("Emil", "Refsnes")
```

**2 Ways to assign args**
1. Keyword args: key = value
    - my_function(fname = "John", lname = "...")
    - Position does not matter in this case
    - If we only want keyword arg = `def my_function(*, name)`
2. Positional args
    - Must be in correct order
    - If we only want position arg = `def my_function(name, /)`
3. Mixing
    - my_function("dog", name = "Buddy", age = "5")

**Passing Different Data Types**
- Can send any data type as argument to a function (string, number, list, dictionary...)
- Example:
```
def my_function(fruits):
  for fruit in fruits:
    print(fruit)

my_fruits = ["apple", "banana", "cherry"]
my_function(my_fruits)
```
**Returning Different Data Types**
- Can return any data types
```
def my_function():
  return ["apple", "banana", "cherry"]

fruits = my_function()
print(fruits[0])
print(fruits[1])
print(fruits[2])
```

---

## Web Browsers & Servers (W4-8)

### HTML
- Hyper Text Markup Language
- Role:
    - Structure of webpage, organize and give meaning to the page
    - Tell browser what content is
    - Add headings, paragraph, img...
- Attributes = additional information about element
    - Specified in start tag
    - name="value"

### HTML FORM

**FORM ATTRIBUTE**
| Attribute | What it does |
| ---- | ---- |
| `action="/home_page"` | perform action when submit form |
| `target="_blank/_self/_parent/_top"` | where to display response |
| `method="get/post"` | method used when submit form data |
| `autocomplete="on/off"` | if on -> complete based on entered value |

**FORM ELEMENT**
| Element | What it does |
| ---- | ---- |
| `<input>` | display depending on type attribute |
| `<label>` | `for` is label's id |
| `multiple` | allow multiple selection |
| `<textarea>` | multi-line input field |
| `<button>` | clickable button |
| `<output>` | result of a calculation |



### CSS
- Cascading Style Sheets
- Role:
    - Style HTML elements
    - Colours, fonts, spacing, borders,...
    - Readable and visually appealing
    - Improve usability by making important elements easier to see

### Flask: Intro, pyhtml, routes
**DISPLAY PAGE ROUTE:**
```
from flask import Flask, request, redirect
import pyhtml as p

app = Flask(__name__)

@app.get("/somewhere")
def home():
    data = get_data()
    return str(
        p.html(
            p.head(
                p.title("Home")
            ),
            p.body(
                show data here
            )
        )
    )

if __name__ == "__main__":
    app.run(port=5001)
```

**REUSABLE NAV BAR:**
def navbar_html():
    return p.nav(
        p.a(href="/")("Home"),
        p.a(href="/add_player")("Add Player"),
        p.a(href="/results")("Results"),
    )

**REUSABLE HEAD:**
def head_html(title):
    return p.head(
        p.title(title),
        p.link(rel="stylesheet", href="/static/style.css"),
    )

**DYNAMIC ROUTE:**
@app.get("/player/<player_id>")
def player_page(player_id):
    data = get_data()
    player = get_player_by_id(data, player_id)

    return str(
        p.html(
            head_html(player["name"]),
            p.body(
                navbar_html(),
                p.h1(player["name"]),
                p.p(f"Elo: {player['elo']}")
            )
        )
    )

**GET FORM ROUTE:**
```
@app.get("/add_player")
def add_player_form():
    return str(
        p.html(
            head_html("Add Player"),
            p.body(
                navbar_html(),
                p.form(method="POST", action="/add_player")(
                    p.label(for_="id")("Player ID:"),
                    p.input(type="text", id="id", name="id"),

                    p.label(for_="name")("Name:"),
                    p.input(type="text", id="name", name="name"),

                    p.button(type="submit")("Submit")
                )
            )
        )
    )
```

"""
IMPORTANT!!
method="POST"
action="/add_player"
name="id"
name="name" (use later in request.form.get())
"""

"""
p.form(method="POST", action="/upload")(
    p.textarea(name="timeline_json")(),
    p.button(type="submit")("Validate")
)
---
timeline_json = request.form.get("timeline_json")
"""

**FORM SUBMIT ROUTE:**
```
@app.post("/form")
    def submit_form():
    data = get_data()

    value = request.form.get("input_name")
    example:
        player_id = request.form.get("id")
    name = request.form.get("name")
    faculty = request.form.get("faculty")

    save_data(data)
    return redirect("/")
```

**Note:**
```
data = get_data()      # load current data
# change the data
save_data(data)        # save updated data
return redirect("/")   # redirect after POST
```

### JSON
- JSON = JavaScript Object Notation
- Text format used to store & exchange structured data
- Common use for files, web apps, APIs, saved data app
- `json.load(file)`: JSON file to Python data
- `json.loads(json_string)`: JSON string to Python
- `json.dump(data, file)`: Python to Json file
- `json.dumps()` vs `json.dump` = python data -> json string vs python data -> json file
- `get_data()` returns [] if loading fails > not crash when the file does not exist
- `save_data(file)` should be called after adding or clearing data
- When to call save_data? - When changing data

#### Serialisation
- Converting data from a program format to format that can be saved/ sent somewhere
- loading data = deserialisation, "r" = read mode
- saving data = serialisation, "w" = write mode
- in data.py -> import json

**IN DATA.PY:**
"""
**LOAD JSON INTO PYTHON:**
import json

def get_data():
    try:
        with open("tournament.json", "r") as file:
            return json.load(file)
    except:
        return []

**SAVE PYTHON DATA TO JSON:**
def save_data(data):
    with open("tournament.json", "w") as file:
        json.dump(data, file, indent=4)

"""
**IN SERVER.PY:**

"""
from data import get_data, save_data
"""


**CLEAR ROUTE:**
@app.get("/clear")
def clear_data():
    save_data([])
    return redirect("/")


**Pros of JSON:**
- easy to read and write
- good for nested structured data
- useful for saving data between app restarts

**Cons of JSON:**
- not ideal for very large data
- no built-in password/security protection
- can become messy if structure is inconsistent
- the whole file often needs to be loaded and saved again

**When to use:**
- Use JSON when the app needs to save structured data permanently.
- Example: tasks, users, quiz scores, bookings, products.

**When not to use:**
- highly sensitive data without proper security.
- many users are editing the same data at the same time.
- the app needs a large database.

### Cookies
- stored in the user's browser
- useful for remembering simple preferences
- can be changed by the user, so do not trust sensitive data

**WHEN TO USE COOKIES**
- Remember theme preference, language preference, or simple non-sensitive choices.

**WHEN NOT TO USE COOKIES**
- Do not store passwords, private information, or important server data directly in cookies.


"""
from flask import Flask, session

app = Flask(__name__)
app.secret_key = "top secret"
"""

**STORE DATA:**
"""
@app.get("/remember")
def remember():
    session["name"] = "name"
    return "Saved"
"""
**READ DATA:**
@app.get("/hello")
def hello():
    name = session.get("name", "Guest")
    return f"Hello {name}"

**REMOVE DATA:**
@app.get("/forget")
def forget():
    session.pop("name", None)
    return "Forgotten"



#### Session:
- stores data for a user's visit/login state
- needs app.secret_key `app.secret.key = "top secret"`
- STORE = `session["timeline"] = timeline`
- READ = `timeline = session["timeline"]`
- CHECK = `if "timeline" in session:...`
- CLEAR = `session.clear()`
- FORCE FLASK TO UPDATE SESSION AFTER CHANGING NESTED DATA: `session.modified = True`
