Skip to main content

UTS allocation for 2027 opens on 9 December. Plan your timetable before then.

All universities

The import script, with notes

To bring your classes in from Allocate+ (myTimetable at UTS, Sydney Timetable at USYD, MyTimetable at ANU), Smoov asks you to paste a script into your browser's console. Pasting code you haven't read into a console is a bad habit, so here it is, with notes.

What it does

  1. Reads the subjects you're enrolled in from the timetable page you have open. Allocate+ has already loaded them into the page, in an object called data.
  2. Asks that same Allocate+ site for the class times on offer in each subject, one request at a time, while you're signed in to it. It shows a line in the console for each subject as it goes, and gives up on a request after 30 seconds. If three requests in a row get no answer, or the whole thing takes more than about 3 minutes, it stops and copies nothing.
  3. Puts together a list of your subjects in every teaching period you're enrolled in, and every class time on offer: day, start time, length, room and teaching weeks. When you paste it, Smoov asks which period to plan if there's more than one.
  4. Copies that list to your clipboard and pops up a message saying how many subjects it found.

What it doesn't do

  • Send anything to Smoov, or to any site other than your uni's Allocate+. No web addresses are written into it. Every request is built from location.origin, which is the site you're already on.
  • Change your timetable. Every request it makes is a read. Nothing in it selects, swaps or drops a class.
  • Copy your name or student number. It reads your student number from the page because Allocate+'s own addresses contain it, and that's the only thing it uses it for.
  • Keep running. It runs once when you press Enter, and it's gone when you reload or close the tab.

After you paste it into Smoov

When you paste the list into Smoov and press Parse, Smoov shows you every subject it found, and you can leave out any you don't want before you import.

At that point Smoov also sends a copy of everything you pasted to its server, including subjects you then leave out: your subject codes and names, campus, and every class on offer with its day, time and room. It doesn't include your name or student number or the timetable you end up picking, and it isn't linked to you or an account. It's used only to find and fix import problems, for example when a uni changes something in Allocate+, and deleted after 12 months. The details are in the privacy policy.

The script

Shown for UTS.

This is exactly what Copy script to clipboard in Smoov's import dialog puts on your clipboard, notes included. Every uni and every teaching period gets the same script. The only thing that changes is the name its messages use for your uni's timetable system:

  • UTSmyTimetable
  • USYDSydney Timetable
  • WSUAllocate+
  • UONmyTimetable
  • MelbourneMyTimetable
  • MonashAllocate+
  • RMITmyTimetable
  • La TrobeAllocate+
  • VUMyTimetable
  • UQMy Timetable
  • UTASMyTimetable
  • ANUMyTimetable
(async function () {
  // The console's copy() only exists while the script starts, so keep hold of it now.
  var cp = typeof copy === "function" ? copy : null;
  try {
    // "data" is the object myTimetable has already loaded into the page you are on.
    if (typeof data === "undefined" || !data || !data.student || !data.student.student_enrolment) {
      alert(
        "This doesn't look like your myTimetable page with your classes on it. Open myTimetable, sign in, wait until your subjects show, then run the script again. If it still doesn't work, go back to Smoov and use the link under the paste box to tell us."
      );
      return;
    }
    // Your student number is only used to build the addresses of myTimetable's own
    // requests below. It is not put into what gets copied.
    var sc = data.student.student_code,
      e = data.student.student_enrolment,
      keys = Object.keys(e),
      ss = new URLSearchParams(location.search).get("ss") || "",
      // The first part of the page's address, e.g. /even/, /odd/ or /aplus/ (none if the page
      // is at the top of the site). Every request goes to the same site and the same part of it
      // as the page you are on.
      seg = location.pathname.split("/")[1] || "",
      root = seg && !/^(student|timetable|rest)$/.test(seg) ? "/" + seg : "",
      base = location.origin + root + "/rest/student/" + sc,
      // What gets copied: your subjects in every teaching period, with their classes. Nothing else.
      o = { smoovImport: true, version: 2, subjects: [], failed: [] },
      signedOut = false,
      maybeOut = false,
      // Stop instead of waiting for ever: after about 3 minutes in all, or after 3 requests in a
      // row that time out or drop. Requests that keep dropping usually mean the sign-in page took
      // over (it lives on another site, so the browser blocks it and reports a dropped connection).
      started = Date.now(),
      slow = false,
      streak = 0,
      drops = 0;
    if (keys.length === 0) {
      alert(
        "Your myTimetable page has no subjects on it yet. Once your enrolment shows there, run the script again. Until then you can type your subjects into Smoov."
      );
      return;
    }

    // One read from myTimetable, one at a time, giving up after 30 seconds.
    function get(path) {
      return new Promise(function (done) {
        if (Date.now() - started > 180000) return done({ budget: true });
        var x = new XMLHttpRequest();
        x.open("GET", base + path + "?ss=" + ss, true);
        x.timeout = 30000;
        x.onload = function () {
          // A sign-in page instead of data means the session has ended
          if (x.status === 401 || x.status === 403 || (x.status === 200 && /^\s*</.test(x.responseText || ""))) return done({ out: true });
          if (x.status !== 200) return done({ error: "it answered with error " + x.status });
          try {
            done({ json: JSON.parse(x.responseText) });
          } catch (err) {
            done({ error: "its answer couldn't be read" });
          }
        };
        x.ontimeout = function () {
          done({ error: "it didn't answer within 30 seconds", late: true });
        };
        x.onerror = function () {
          done({ error: "the connection dropped", dropped: true });
        };
        x.send();
      });
    }

    // Whether to stop reading after this answer.
    function stop(r) {
      if (r.out) signedOut = true;
      else if (r.budget) slow = true;
      else if (r.late || r.dropped) {
        streak++;
        if (r.dropped) drops++;
        if (streak >= 3) {
          if (drops >= 3) signedOut = maybeOut = true;
          else slow = true;
        }
      } else {
        streak = 0;
        drops = 0;
      }
      return signedOut || slow;
    }

    // One class option: its day, start time, length, room and weeks. Prerecorded classes
    // have their day and time removed, so they don't take a slot in your week.
    function option(ac) {
      var type = ac.activityType || ac.activity_type || "",
        pr = /prerecord/i.test(ac.location || "") || /recorded/i.test(type),
        ol = ac.isOnlineAct === true || !ac.day_of_week || /online/i.test(ac.location || ""),
        d = ac.activitiesDays,
        wp = ac.week_pattern,
        weeks = "",
        count;
      // The dates the class runs on as date ranges, e.g. "2/3/2027-13/4/2027, 27/4/2027-1/6/2027"
      if (d && d.length) {
        var iso = function (s) {
          var p = s.split("/");
          return p[2] + "-" + (p[1].length < 2 ? "0" : "") + p[1] + "-" + (p[0].length < 2 ? "0" : "") + p[0];
        };
        var sorted = d.slice().sort(function (a, b) {
            return iso(a) < iso(b) ? -1 : 1;
          }),
          ranges = [],
          first = sorted[0],
          prev = sorted[0];
        for (var i = 1; i < sorted.length; i++) {
          if (new Date(iso(sorted[i])) - new Date(iso(prev)) > 7 * 864e5) {
            ranges.push(first + "-" + prev);
            first = sorted[i];
          }
          prev = sorted[i];
        }
        ranges.push(sorted.length > 1 ? first + "-" + prev : first);
        weeks = ranges.join(", ");
      } else if (wp) {
        // No dates: count the teaching weeks instead ("1-6, 8-13" or a 0/1 pattern)
        var n = 0;
        if (/^[01]{5,}$/.test(wp)) n = (wp.match(/1/g) || []).length;
        else
          wp.split(",").forEach(function (part) {
            var t = part.trim(),
              m = t.match(/^(\d+)\s*-\s*(\d+)$/);
            if (m) n += +m[2] - +m[1] + 1;
            else if (/^\d+$/.test(t)) n++;
          });
        if (n > 0) count = n;
      }
      return {
        activityCode: ac.activity_code,
        day: pr ? "" : ac.day_of_week || "",
        startTime: pr ? "" : ac.start_time || "",
        duration: ac.duration || "0",
        location: ac.location || "",
        isOnline: !!(ol || pr),
        weeks: weeks,
        weekCount: count
      };
    }

    // Go through each subject you're enrolled in, in every teaching period.
    for (var k = 0; k < keys.length && !signedOut && !slow; k++) {
      var sk = keys[k],
        sub = e[sk] || {},
        code = sub.callista_code || sub.display_subject_code || sk;
      console.log("Smoov: reading subject " + (k + 1) + " of " + keys.length + " (" + code + ")");
      var subject = {
        code: code,
        name: sub.description || "",
        key: sk,
        semester: sub.semester || undefined,
        semesterDescription: sub.semester_description || undefined,
        campus: sub.campus || undefined,
        campusDescription: sub.campus_description || undefined,
        groups: []
      };

      // Some subjects come as fixed bundles of classes (UTS calls them sectioned activities).
      // If the page hasn't loaded them yet, ask myTimetable for them.
      var bundles = sub.sections && Object.keys(sub.sections).length > 0 ? sub.sections : null;
      if (!bundles && sub.section_status) {
        var r = await get("/subject/" + sk + "/sections/");
        if (stop(r)) break;
        if (r.error) o.failed.push({ code: code, key: sk, group: "bundled classes", reason: r.error });
        else if (r.json && r.json.sections && r.json.sections.length > 0) bundles = r.json.sections;
      }
      var sections = [];
      for (var b in bundles || {}) {
        var bundle = bundles[b];
        // Leave out bundles you aren't allowed to pick.
        if (bundle.student_allowed === false) continue;
        var bc = bundle.section_code || "",
          bm = bc.match(/_SC(\d+)$/),
          groups = [];
        for (var bg in bundle.activity_groups || {}) {
          var sg = bundle.activity_groups[bg];
          groups.push({
            groupCode: sg.activity_group_code,
            activityType: sg.description && sg.description !== "-" ? sg.description : sg.activityType || "Other",
            options: Object.keys(sg.activities || {}).map(function (a) {
              return option(sg.activities[a]);
            })
          });
        }
        if (groups.length > 0) sections.push({ code: bm ? "SC" + bm[1] : bc, groups: groups });
      }

      if (sections.length > 0) {
        subject.isSection = true;
        subject.sections = sections;
      } else {
        // An ordinary subject: for each kind of class (lecture, tutorial, lab...),
        // ask myTimetable's server for the times on offer.
        for (var gk in sub.groups || {}) {
          var gr = sub.groups[gk] || {};
          var res = await get("/subject/" + sk + "/group/" + gk + "/activities/");
          if (stop(res)) break;
          if (res.error || !res.json || typeof res.json !== "object") {
            // Keep going; Smoov tells you which class list didn't load
            o.failed.push({ code: code, key: sk, group: gr.activity_group_code || gk, reason: res.error || "its answer was empty" });
            continue;
          }
          var acts = Object.keys(res.json).map(function (a) {
            return res.json[a];
          });
          var firstType =
            acts.filter(function (ac) {
              return ac && (ac.activityType || ac.activity_type);
            })[0] || {};
          subject.groups.push({
            groupCode: gr.activity_group_code || gk,
            activityType: gr.description && gr.description !== "-" ? gr.description : firstType.activityType || firstType.activity_type || "Other",
            options: acts.filter(Boolean).map(option)
          });
        }
        if (signedOut || slow) break;
        if (!sub.groups) o.failed.push({ code: code, key: sk, reason: "its class list wasn't on the page" });
      }
      o.subjects.push(subject);
    }

    if (signedOut) {
      alert(
        maybeOut
          ? "myTimetable seems to have signed you out, so Smoov couldn't read your classes. Reload this page, sign in and run the script again. Running it again is safe."
          : "myTimetable has signed you out, so Smoov couldn't read your classes. Reload this page, sign in again and run the script again. Running it again is safe."
      );
      return;
    }
    if (slow) {
      alert(
        "myTimetable isn't responding, so Smoov stopped and nothing was copied. Try again in a few minutes. Running the script again is safe."
      );
      return;
    }

    // Finish: copy the list to your clipboard and say how many subjects it found.
    // This last step sends nothing anywhere.
    var text = JSON.stringify(o),
      copied = false;
    if (cp) {
      try {
        cp(text);
        copied = true;
      } catch (ce) {
        console.warn("Smoov: copy() didn't work, so the list goes in a box instead", ce);
      }
    }
    if (!copied) {
      // No copy() in this browser's console: put the list in a box to copy by hand
      var box = document.createElement("textarea");
      box.value = text;
      box.style.cssText = "position:fixed;top:0;left:0;width:100%;height:40%;z-index:2147483647";
      document.body.appendChild(box);
      box.focus();
      box.select();
    }
    var msg =
      (copied ? "Copied " : "Ready to copy: ") +
      o.subjects.length +
      (o.subjects.length === 1 ? " subject" : " subjects") +
      (copied ? ". Paste it into Smoov now." : ". Copy the text in the box at the top of the page (Ctrl+C or Cmd+C), then paste it into Smoov.");
    if (o.failed.length > 0)
      msg +=
        "\n\nSome class lists didn't load (" +
        o.failed
          .map(function (f) {
            return f.code + (f.group ? " " + f.group : "");
          })
          .join(", ") +
        "). Smoov will show which. Running the script again is safe.";
    alert(msg);
  } catch (err) {
    console.error("Smoov script failed:", err);
    alert(
      "The script stopped with an error (" + (err && err.message) + "). Reload the page and run it again. If it keeps happening, type your subjects into Smoov and use the link under the paste box to tell us."
    );
  }
})();

Check it yourself

Copy the script from Smoov's import dialog and paste it into a text editor instead of the console. Search it for :// and you won't find a web address. There's no fetch, sendBeacon or WebSocket either. Its one XMLHttpRequest goes to base, which is built from the Allocate+ site you're on, and only ever reads (GET).

If the site signs you out, the script says so in a pop-up. If a class list doesn't load, the pop-up names the subject. Running it again is safe: it only reads.

Chrome may ask you to type allow pasting before it lets you paste into the console. That's Chrome's own check for exactly this situation, not something Smoov adds.

If what Smoov copies doesn't match this page, apart from the system name listed above, don't run it, and tell us at contact@getsmoov.app.

Try it on your own week

Put in your subjects, your shifts and your hard nos. Smoov finds the timetables that fit, shows you the best few and what each one costs you.

Build my timetable