Ground Zero

Goal

Build ~/projects/first-site/index.html with the file manager and a text editor. Capture its absolute path, the file:// URL the browser shows when you double-click it, and how many clicks it cost — you will want that number in Unit 2. ---

Type: interactive-form Unit: 1 — One file, one address

Goal

Build ~/projects/first-site/index.html with the file manager and a text editor. Capture its absolute path, the file:// URL the browser shows when you double-click it, and how many clicks it cost — you will want that number in Unit 2.


How this works

You do this on your own machine. What you hand in is five named fields in starter/submission.txt. Open that file and fill each field as you finish the step that produces it.

FieldWhat goes in it
platformExactly one of macOS, Linux, WSL
absolute_pathThe full path of your index.html, starting with /
file_urlThe exact text in the browser’s address bar after you double-click the file
mouse_actionsactions=<n> apps=<n> — distinct clicks/drags/typed names, and how many apps
ls_la_lineThe single ls -la line for your index.html, copied as-is

Your tasks

  1. Name home. Mac: /Users/<you>. Linux and WSL: /home/<you>. Write your platform into platform.
  2. Make two folders in the file manager, inside home: projects, then first-site. Not on the Desktop. Type both names.
  3. Make the file. In a text editor, type one <h1> line, save it into first-site as index.html. Lowercase i. Watch that the editor does not add .txt.
  4. Write the full path into absolute_path. From / to index.html. No ~, no C:\.
  5. Double-click the file. Copy the address bar into file_url. Copy it; do not rebuild it from memory.
  6. One command: ls -la ~/projects/first-site. Copy the index.html row into ls_la_line.
  7. Count the cost. actions=<n> apps=<n> into mouse_actions.

On WSL

Put the project on the Linux side: /home/<you>/projects/first-site/index.html. A path under /mnt/c fails. Count every app you had to use to get a file manager onto that side.

What the scaffolding is for

mouse_actions is the number Unit 2 will beat. Count honestly.

file_url is the first of three addresses this file will have. It works on your machine only.

Expected output

platform: Linux
absolute_path: /home/you/projects/first-site/index.html
file_url: file:///home/you/projects/first-site/index.html
mouse_actions: actions=<your count> apps=<your count>
ls_la_line: -rw-r--r--    1 you  you    12 Jul 29 04:25 index.html

A Mac submission uses /Users/you/…. An extra character after the permissions (a . on Fedora) is fine — leave it in.

Rules

See rubric.md for how this is scored.

Steps

Your files

Download these onto your machine and fill them in. The brief says which, and what “done” looks like.

submission.txt Download

                    # Ground Zero — submission
#
# Fill in the five values to the RIGHT of each colon. Do not rename the fields,
# do not reorder them, do not delete any of them.
#
# Lines starting with # are ignored. Only the FIRST colon on a line separates the
# field name from its value, so colons inside a value are fine — file:/// and the
# 04:25 in a timestamp both survive.
#
# Rules that the tests enforce:
#   - absolute_path must start with "/" and must NOT use ~, a C:\ drive letter,
#     or backslashes. It must end with /projects/first-site/index.html
#   - file_url is what the browser's address bar ACTUALLY said. Copy it. It begins
#     with file:// — an http:// URL from a tutorial is not evidence.
#   - ls_la_line is ONE line, copied character for character. Keep the runs of
#     spaces. Do not copy the "total" line or the directory's own row.
#   - Do not copy a leading $ or % — that is the machine's prompt, not your output.


# One of: macOS  |  Linux  |  WSL
platform:

# The full absolute path of your index.html, starting with /
# macOS:        /Users/<you>/projects/first-site/index.html
# Linux, WSL:   /home/<you>/projects/first-site/index.html
absolute_path:

# The exact text in the browser address bar after you double-clicked index.html
file_url:

# actions = every distinct click, drag, and typed name it took
# apps    = how many separate applications you switched between
mouse_actions: actions= apps=

# The single `ls -la` line for index.html, copied exactly
# (run: ls -la ~/projects/first-site  — then copy only the index.html row)
ls_la_line:

                  

On your machine, run the tests in this project’s tests/ folder. Each one prints PASS or a FAIL: line that names what is wrong.

How it is graded
CriterionWhat earns itWeight
The file exists where it was asked to exist, written as a full path

absolute_path begins with /, ends with the exact string /projects/first-site/index.html, and contains no ~, no backslash, no drive letter, and no . or .. segment. Its home prefix agrees with the declared platform: /Users/ for macOS, /home/ for Linux and for WSL. A WSL submission under /mnt/c scores 0 on this criterion regardless of how well-formed the path is, because Microsoft's own guidance is to "store your files in the WSL file system if you are working in a Linux command line". A path under Desktop, Documents, or any location other than <home>/projects/first-site scores 0. Filename case matters: Index.html is not index.html.

Full credit (30): absolute, platform-correct home prefix, exact tail, lowercase index.html. Partial (15): correct location but abbreviated or malformed — a leading ~, a trailing slash, or a path with a .. segment left unresolved. Zero: relative path, Windows-style path, /mnt/c, or the wrong directory.

30
The URL is the one the browser actually showed

file_url begins with file:// and ends with the value of absolute_path, so the URL and the path name the same file. On macOS and Linux the two must line up exactly: file:// + absolute_path. On WSL the URL may carry additional prefix material before the Linux path, and that is accepted — copied exactly, not cleaned up.

Full credit (25): a file:// URL whose tail is the submitted path. Partial (12): a file:// URL that is well-formed but names a different file than absolute_path (different folder, different case, .txt still attached) — evidence the URL was read, but of the wrong thing. Zero: an http:// or https:// URL, a localhost URL, a bare filesystem path pasted into the URL field, or a URL identical to absolute_path with no scheme.

25
The ls -la line is a real listing of the submitted file

ls_la_line is one row of long-listing output, transcribed intact and describing a regular file:

  • The first field is 10 or 11 characters. Character 1 is -, not d — a d means a directory was listed instead of the file. An eleventh character is accepted; on Fedora and RHEL a trailing . is an SELinux security-context marker, and plain Ubuntu prints ten characters and no dot.
  • Characters 2–10 are three rwx triples: owner, group, other.
  • The link count is a whole number and the size is a whole number greater than zero. A size of 0 means an empty file was saved and no HTML was typed.
  • The final field is exactly index.html.

Full credit (25): all of the above, spacing preserved. Partial (12): a genuine listing line that has been reformatted — spaces collapsed, trailing dot stripped, fields reordered — but is still recognisably this file's row. Zero: the directory's own row, the total line, the whole listing pasted, a line for a different file, or a line invented rather than copied.

25
Honest counts and real captures (craft)

Everything in the submission is copied, not reconstructed, and every field is filled with a value rather than a placeholder.

  • mouse_actions reads actions=<n> apps=<n> with both numbers greater than zero, and the numbers are plausible against the work described: creating two folders, creating and naming one file, and opening it in a browser cannot be done in one action or in one application. A submission claiming apps=1 for a task that required a file manager, an editor, and a browser is not honest.
  • No template placeholder survives: no <you>, no <your count>, no empty value after a colon.
  • No prompt character has been copied into a value. A leading $ or % means the machine's prompt was selected along with the text — the $ "is a command prompt. It is not meant to be typed in."
  • Field names are unaltered and every one of the five lines is present.
  • platform is one of the three permitted words, not a version string or a marketing name.

Full credit (20): all five fields present, no placeholders, no prompt characters, counts present and plausible. Partial (10): one field left as a placeholder, or a count given as a single number rather than the actions=<n> apps=<n> pair. Zero: fields renamed or missing, values obviously fabricated, or mouse_actions left blank.

20
Total100
Test cases and grader source
Test Checks Expected Weight
fields_and_home All five fields present and non-empty; platform is one of the three; absolute_path is space-free, ends with /projects/first-site/index.html, and its home prefix matches the declared platform (/Users/ for macOS, /home/ for Linux and WSL, never /mnt/); mouse_actions parses as actions=<n> apps=<n> with both ≥ 1. PASS 30
url_matches_path file_url begins file:// and ends with absolute_path; on macOS and Linux it must equal file:// + absolute_path exactly. Catches a URL that points at a different file than the one submitted. PASS 25
ls_la_line ls_la_line is a single long-listing row for a regular file: 10-or-11-character mode string starting -, valid rwx triples, numeric link count, positive numeric size, at least eight fields, and a final field of exactly index.html. PASS 20
adversarial_shortcuts Adversarial. Catches the four plausible shortcuts: (a) a relative path such as projects/first-site/index.html submitted as if it were absolute; (b) a ~-abbreviated path such as ~/projects/first-site/index.html; (c) a Windows drive-letter or backslash path such as C:\Users\you\projects\first-site\index.html; and (d) a file_url that is actually an http:// URL copied from a tutorial — including http://localhost:8000/, http://127.0.0.1:8000/, and any bare path pasted where a URL belongs. PASS 25

tests/test_1_fields_and_home.py

                        #!/usr/bin/env python3
"""Test 1 of 4 — weight 30.

Checks that the submission is complete and internally consistent: all five fields
present and non-empty, `platform` is one of the three supported words, the
`absolute_path` ends where the project asked it to end and carries the home prefix
that matches the declared platform, and `mouse_actions` parses as the required
`actions=<n> apps=<n>` pair.

Expected output: PASS
Usage: python3 test_1_fields_and_home.py [path/to/submission.txt]
"""

import os
import re
import sys

TAIL = "/projects/first-site/index.html"
REQUIRED = ["platform", "absolute_path", "file_url", "mouse_actions", "ls_la_line"]
PLATFORMS = {"macos": "macOS", "linux": "Linux", "wsl": "WSL"}
PLACEHOLDERS = ("<you>", "<your count>", "<n>", "<integer>")


def default_submission():
    here = os.path.dirname(os.path.abspath(__file__))
    return os.path.normpath(os.path.join(here, os.pardir, "starter", "submission.txt"))


def load(path):
    fields = {}
    try:
        handle = open(path, encoding="utf-8")
    except OSError as exc:
        fail("cannot read the submission file at %s (%s)" % (path, exc.__class__.__name__))
    with handle:
        for raw in handle:
            line = raw.rstrip("\n").rstrip("\r")
            if not line.strip() or line.lstrip().startswith("#"):
                continue
            if ":" not in line:
                continue
            key, _, value = line.partition(":")
            key = key.strip()
            if not re.fullmatch(r"[a-z_]+", key):
                continue
            fields[key] = value.strip()
    return fields


def fail(message):
    print("FAIL: " + message)
    raise SystemExit(1)


def main():
    path = sys.argv[1] if len(sys.argv) > 1 else default_submission()
    fields = load(path)

    for name in REQUIRED:
        if name not in fields:
            fail("field '%s' is missing from the submission; all five field names must stay exactly "
                 "as given in starter/submission.txt" % name)
        if not fields[name]:
            fail("field '%s' is empty; every field needs a value you read off your own screen" % name)
        for token in PLACEHOLDERS:
            if token in fields[name]:
                fail("field '%s' still contains the template placeholder '%s'" % (name, token))
        if fields[name][0] in "$%":
            fail("field '%s' starts with '%s' — that is the shell prompt, not your output; the '$' is "
                 "a command prompt and is not meant to be typed in" % (name, fields[name][0]))

    key = fields["platform"].strip().lower()
    if key not in PLATFORMS:
        fail("platform is '%s'; it must be exactly one of macOS, Linux, WSL" % fields["platform"])
    platform = PLATFORMS[key]

    path_value = fields["absolute_path"]
    if " " in path_value or "\t" in path_value:
        fail("absolute_path contains a space; name the folders 'projects' and 'first-site' with no "
             "spaces so the path can be typed as one argument later")
    if not path_value.endswith(TAIL):
        fail("absolute_path is '%s'; it must end with the exact string '%s'" % (path_value, TAIL))

    if platform == "macOS":
        if not path_value.startswith("/Users/"):
            fail("platform is macOS but absolute_path does not start with '/Users/'; macOS home "
                 "directories live under /Users")
    else:
        if path_value.startswith("/mnt/"):
            fail("platform is %s but absolute_path is under /mnt/, which is the Windows side seen "
                 "from Linux; Microsoft recommends storing your files in the WSL file system, so this "
                 "belongs at /home/<you>%s" % (platform, TAIL))
        if not path_value.startswith("/home/"):
            fail("platform is %s but absolute_path does not start with '/home/'; Linux and WSL home "
                 "directories live under /home" % platform)

    match = re.fullmatch(r"actions\s*=\s*(\d+)\s+apps\s*=\s*(\d+)", fields["mouse_actions"])
    if match is None:
        fail("mouse_actions is '%s'; it must read exactly 'actions=<n> apps=<n>' with two whole "
             "numbers" % fields["mouse_actions"])
    actions, apps = int(match.group(1)), int(match.group(2))
    if actions < 1:
        fail("mouse_actions reports actions=0; creating two folders and a file takes at least one "
             "action")
    if apps < 1:
        fail("mouse_actions reports apps=0; you used at least one application")

    print("PASS")


if __name__ == "__main__":
    main()

                      

tests/test_2_url_matches_path.py

                        #!/usr/bin/env python3
"""Test 2 of 4 — weight 25.

Checks that `file_url` and `absolute_path` name the SAME file. The URL must begin
with the file:// scheme and end with the submitted absolute path. On macOS and
Linux the two must line up exactly — file:// followed by the path. On WSL the URL
is allowed to carry extra prefix material before the Linux path, because the
browser and the file sit on opposite sides of the Windows/Linux seam; only the
scheme and the tail are required there.

Percent-encoding in the address bar is decoded before comparison.

Expected output: PASS
Usage: python3 test_2_url_matches_path.py [path/to/submission.txt]
"""

import os
import re
import sys
from urllib.parse import unquote

REQUIRED = ["platform", "absolute_path", "file_url"]


def default_submission():
    here = os.path.dirname(os.path.abspath(__file__))
    return os.path.normpath(os.path.join(here, os.pardir, "starter", "submission.txt"))


def fail(message):
    print("FAIL: " + message)
    raise SystemExit(1)


def load(path):
    fields = {}
    try:
        handle = open(path, encoding="utf-8")
    except OSError as exc:
        fail("cannot read the submission file at %s (%s)" % (path, exc.__class__.__name__))
    with handle:
        for raw in handle:
            line = raw.rstrip("\n").rstrip("\r")
            if not line.strip() or line.lstrip().startswith("#"):
                continue
            if ":" not in line:
                continue
            key, _, value = line.partition(":")
            key = key.strip()
            if not re.fullmatch(r"[a-z_]+", key):
                continue
            fields[key] = value.strip()
    return fields


def main():
    path = sys.argv[1] if len(sys.argv) > 1 else default_submission()
    fields = load(path)

    for name in REQUIRED:
        if not fields.get(name):
            fail("field '%s' is missing or empty; this test needs platform, absolute_path and "
                 "file_url" % name)

    platform = fields["platform"].strip().lower()
    if platform not in ("macos", "linux", "wsl"):
        fail("platform is '%s'; it must be exactly one of macOS, Linux, WSL" % fields["platform"])

    raw_url = fields["file_url"]
    url = unquote(raw_url)
    target = fields["absolute_path"]

    if not url.startswith("file://"):
        fail("file_url is '%s'; the address bar after double-clicking a file on disk begins with "
             "'file://'" % raw_url)

    if url.rstrip("/") == "file:":
        fail("file_url is just the scheme with no path; copy the whole address bar")

    if not url.endswith(target):
        fail("file_url does not end with absolute_path — the URL and the path name different files. "
             "url='%s' path='%s'" % (raw_url, target))

    if platform in ("macos", "linux"):
        expected = "file://" + target
        if url != expected:
            fail("on %s the address bar shows the scheme followed directly by the absolute path; "
                 "expected '%s' but got '%s'" % (fields["platform"], expected, raw_url))

    print("PASS")


if __name__ == "__main__":
    main()

                      

tests/test_3_ls_la_line.py

                        #!/usr/bin/env python3
"""Test 3 of 4 — weight 20.

Checks that `ls_la_line` is one row of long-listing output describing a REGULAR
FILE named index.html: a 10- or 11-character mode string beginning with '-', three
valid rwx triples, a whole-number link count, a whole-number size greater than
zero, at least eight whitespace-separated fields, and a final field of exactly
'index.html'.

An eleventh character after the ten permission characters is accepted: on Fedora
and RHEL a trailing '.' is an SELinux security-context marker, and plain Ubuntu
prints ten characters and no dot.

Expected output: PASS
Usage: python3 test_3_ls_la_line.py [path/to/submission.txt]
"""

import os
import re
import sys

TRIPLES = re.compile(r"[-r][-w][-xsS][-r][-w][-xsS][-r][-w][-xtT]")


def default_submission():
    here = os.path.dirname(os.path.abspath(__file__))
    return os.path.normpath(os.path.join(here, os.pardir, "starter", "submission.txt"))


def fail(message):
    print("FAIL: " + message)
    raise SystemExit(1)


def load(path):
    fields = {}
    try:
        handle = open(path, encoding="utf-8")
    except OSError as exc:
        fail("cannot read the submission file at %s (%s)" % (path, exc.__class__.__name__))
    with handle:
        for raw in handle:
            line = raw.rstrip("\n").rstrip("\r")
            if not line.strip() or line.lstrip().startswith("#"):
                continue
            if ":" not in line:
                continue
            key, _, value = line.partition(":")
            key = key.strip()
            if not re.fullmatch(r"[a-z_]+", key):
                continue
            fields[key] = value.strip()
    return fields


def main():
    path = sys.argv[1] if len(sys.argv) > 1 else default_submission()
    fields = load(path)

    line = fields.get("ls_la_line", "")
    if not line:
        fail("field 'ls_la_line' is missing or empty; run 'ls -la ~/projects/first-site' and copy the "
             "index.html row")

    if line[0] in "$%":
        fail("ls_la_line starts with '%s' — that is the shell prompt, not the listing; the '$' is a "
             "command prompt and is not meant to be typed in" % line[0])

    if line.startswith("ls ") or line.startswith("ls -"):
        fail("ls_la_line contains the command you typed, not the line it printed; copy the output row "
             "for index.html")

    if line.lower().startswith("total"):
        fail("ls_la_line is the 'total' summary line, not a file's row; copy the row that ends in "
             "index.html")

    parts = line.split()
    if len(parts) < 8:
        fail("ls_la_line has only %d whitespace-separated fields; a long listing row has at least "
             "eight: mode, link count, owner, group, size, and the modification time and name"
             % len(parts))

    mode = parts[0]
    if len(mode) not in (10, 11):
        fail("the first field of ls_la_line is '%s' (%d characters); a mode string is 10 characters, "
             "or 11 when the system appends a security-context marker" % (mode, len(mode)))

    if mode[0] == "d":
        fail("the type character of ls_la_line is 'd', so you copied a DIRECTORY's row; index.html is "
             "a regular file and its row starts with '-'")
    if mode[0] != "-":
        fail("the type character of ls_la_line is '%s'; a regular file's row starts with '-'" % mode[0])

    if TRIPLES.fullmatch(mode[1:10]) is None:
        fail("characters 2-10 of the mode string are '%s', which is not three valid rwx triples; copy "
             "the line without editing it" % mode[1:10])

    if not parts[1].isdigit():
        fail("the second field of ls_la_line is '%s'; that position is the link count and must be a "
             "whole number" % parts[1])

    name = parts[-1]
    if name == "Index.html":
        fail("ls_la_line ends in 'Index.html' with a capital I; the project asks for 'index.html'. "
             "On Linux and on the WSL Linux filesystem those are two different files")
    if name != "index.html":
        fail("ls_la_line ends in '%s'; it must be the row for 'index.html'" % name)

    if not parts[4].isdigit():
        fail("the fifth field of ls_la_line is '%s'; that position is the size in bytes and must be a "
             "whole number — the fields run mode, link count, owner, group, size" % parts[4])

    if int(parts[4]) <= 0:
        fail("ls_la_line reports a size of 0 bytes, so index.html was saved empty; type a line of "
             "HTML into it and save again")

    print("PASS")


if __name__ == "__main__":
    main()

                      

tests/test_4_adversarial_shortcuts.py

                        #!/usr/bin/env python3
"""Test 4 of 4 — weight 25. ADVERSARIAL.

This test exists to catch four specific shortcuts, each of which produces a
submission that LOOKS finished while proving nothing about the learner's machine:

  (a) a RELATIVE path submitted as if it were absolute — for example
      'projects/first-site/index.html' or './first-site/index.html'. A path is
      absolute only when its first character is '/'.
  (b) a '~'-ABBREVIATED path — '~/projects/first-site/index.html'. '~' is an
      abbreviation the shell expands to $HOME; this field wants what it expands to.
  (c) a WINDOWS-STYLE path — 'C:\\Users\\you\\projects\\first-site\\index.html', a
      drive letter, or any backslash used as a separator.
  (d) a file_url that is actually an http:// URL copied from a tutorial, including
      'http://localhost:8000/' and 'http://127.0.0.1:8000/', or a bare filesystem
      path pasted where a URL belongs. Double-clicking a file on disk does not
      produce an http:// URL — that comes later in the course, from a server the
      learner has not started yet.

Expected output: PASS
Usage: python3 test_4_adversarial_shortcuts.py [path/to/submission.txt]
"""

import os
import re
import sys

DRIVE_LETTER = re.compile(r"^[A-Za-z]:")


def default_submission():
    here = os.path.dirname(os.path.abspath(__file__))
    return os.path.normpath(os.path.join(here, os.pardir, "starter", "submission.txt"))


def fail(message):
    print("FAIL: " + message)
    raise SystemExit(1)


def load(path):
    fields = {}
    try:
        handle = open(path, encoding="utf-8")
    except OSError as exc:
        fail("cannot read the submission file at %s (%s)" % (path, exc.__class__.__name__))
    with handle:
        for raw in handle:
            line = raw.rstrip("\n").rstrip("\r")
            if not line.strip() or line.lstrip().startswith("#"):
                continue
            if ":" not in line:
                continue
            key, _, value = line.partition(":")
            key = key.strip()
            if not re.fullmatch(r"[a-z_]+", key):
                continue
            fields[key] = value.strip()
    return fields


def check_path(value):
    if value.startswith("~"):
        fail("absolute_path is '%s', which is a '~'-abbreviated path. '~' is shorthand your shell "
             "expands to your home directory; this field wants the expanded form — /Users/<you>/... "
             "or /home/<you>/..." % value)

    if DRIVE_LETTER.match(value):
        fail("absolute_path is '%s', which is a Windows drive-letter path. This course works in the "
             "POSIX filesystem: on WSL your project belongs at /home/<you>/projects/first-site/"
             "index.html, not on a lettered drive" % value)

    if "\\" in value:
        fail("absolute_path is '%s' and uses backslashes as separators. POSIX paths separate their "
             "steps with '/'" % value)

    if not value.startswith("/"):
        fail("absolute_path is '%s', which is a RELATIVE path — it has no leading '/', so it is "
             "measured from wherever you happen to be standing rather than from the root of the "
             "machine. An absolute path begins with '/'" % value)

    segments = value.split("/")
    if "." in segments or ".." in segments:
        fail("absolute_path is '%s' and still contains a '.' or '..' segment. Those are relative "
             "movements; resolve them and submit the plain absolute path" % value)

    if "//" in value:
        fail("absolute_path is '%s' and contains an empty path segment ('//')" % value)


def check_url(value, submitted_path):
    lowered = value.lower()

    if value == submitted_path:
        fail("file_url is identical to absolute_path — a filesystem path with no scheme is not a URL. "
             "The address bar shows a scheme first")

    if lowered.startswith("http://") or lowered.startswith("https://"):
        fail("file_url is '%s', which is an http URL. Double-clicking a file on disk opens it through "
             "the file:// scheme; an http:// address means a server answered, and you have not "
             "started one. Copy what YOUR address bar said" % value)

    for authority in ("://localhost", "://127.0.0.1", "://0.0.0.0"):
        if authority in lowered:
            fail("file_url points at '%s', which is a running web server, not a file opened from "
                 "disk. This project has no server in it" % authority.lstrip(":/"))

    if not lowered.startswith("file://"):
        for token in ("localhost", "127.0.0.1", "0.0.0.0", ":8000"):
            if token in lowered:
                fail("file_url is '%s', which names a web server rather than a file on disk. Nothing "
                     "in this project starts a server; copy what YOUR address bar said" % value)
        if value.startswith("~") or value.startswith("/") or DRIVE_LETTER.match(value):
            fail("file_url is '%s', which is a filesystem path rather than a URL. The address bar "
                 "shows a scheme first — 'file://' followed by the path" % value)
        fail("file_url is '%s' and does not begin with the file:// scheme; that is not the address "
             "bar's text" % value)


def main():
    path = sys.argv[1] if len(sys.argv) > 1 else default_submission()
    fields = load(path)

    submitted_path = fields.get("absolute_path", "")
    url = fields.get("file_url", "")

    if not submitted_path:
        fail("field 'absolute_path' is missing or empty")
    if not url:
        fail("field 'file_url' is missing or empty")

    check_path(submitted_path)
    check_url(url, submitted_path)

    print("PASS")


if __name__ == "__main__":
    main()

                      

Erase saved progress?

This erases all quiz scores, reading progress, project checklists, and your name on the certificate. It cannot be undone, and it affects only this course in this browser.