The Architecture¶
Here we give a deeper dive into how things are implemented (beyond making a simple problem). We first address the question,
how are problems actually imported into the website?
If you are coming from “Adding a Simple Problem” you might be surprised at how little-to-no boilerplate is needed when making a score function. We use some import magic for both the frontend verification and backend verification.
Backend Verification: Python Importing¶
A problem is registered using the file conjecscore/app/routers/problems/problems.py. In particular, the files for the problems are found using Line 179 by iterating through each .json file in the registry file.
The register_problem takes the problem details from the registry and converts that information into a route.
Frontend Verification: Typescript Importing¶
It is a little bit more complicated for frontend. Files are found not via Typescript but via Python again in conjecscore/app/routers/problems/problems.py. The files that are found are then injected into the HTML via Jinja. In particular, the Typescript files and functions are injected into conjecscore/templates/make-problem.j2. make-problem.j2 creates a Problem object (see conjecscore/static/problem.ts at Line 65) containing the (frontend) score function, the url to post scores to, what kind of input to use (does the problem take a single number, JSON, CSV, etc.), and any information on problem variants. In addition to storing problem information in a Problem object, make-problem.j2 also creates event listeners for the particular problem (that way, the problem creator does not need to worry about this!)
In conjunction with make-problem.j2 The HTML input forms are generated via conjecscore/templates/submission-form.j2. Lastly, problem_template.j2 combines various templates including make-problem.j2, submission-form.j2, and the correct template associated with a particular problem to actually render a functional problem page.
Other systems¶
We have now discussed how a problem gets rendered to a page, the webpage also consists of a couple other major components. Namely, a problem database, a login/registration system, and a user page.
The Problem Database¶
There are two database schemas that are used in conjecscore: A User table for managing account information, and an Entry table for managing submissions made by users. Both can be found in conjecscore/app/db.py at Line 17 and Line 22, respectively.
The User schema implicitly has some fields generated by FastAPI-Users but it consists of an email as the primary key (which is not visible to other users). A password (which, obviously is not exposed). And a nickname which is displayed in various places such as the scoreboards, user profile, and the /users route.
The Entry schema is a little bit more complicated. An Entry corresponds to a submission made by a user. Since memory is limited only the best scoring entry for a particular user is kept. Hence, we enforce that account_id (the ID for the account that submitted the entry) is unique. We also store the other account information along with the entry (such as the nick name). Lastly, we keep some obvious fields: score the score the submission achieved. problem the particular problem (Collatz, Brocard, etc.) that the submission is associated with. variant if the problem has variants then there must be separate submissions for each variant.
Login and Registration¶
We have mentioned the User entry in the previous section which stores the user information. Other major files for Login and Registration include:
conjecscore/templates/login.j2andconjecscore/templates/newaccount.j2: The Jinja2 files that generate the HTML for login and register pages respectively. (The routes are served by FastAPI inconjecscore/app/main.py).conjecscore/static/login.tsandconjecscore/static/register.tsthe Typescript for the login and register page.
For the future, work will need to be done to add OAuth.
Note
Once a user is logged in they can visit their profile page at /me. Let us look at the route function signature for the /me route on Line 115:
async def me(request: Request,
user: User=Depends(current_active_user)):
The request is fairly standard among all routes. A request is always made to a route. user: User=Depends(current_active_user) checks to see if the user is actually logged in. If not user is None. Otherwise, there is a user session. (This is largely handled by the FastAPI-Users library.
The User Page: A More In Depth Look¶
Going back to /me route, we have a fairly complicated route function, but at a highlevel:
We get (from the
Entrytable) each problem submission for the logged in user.Render these entries via
conjecscore/templates/profile.j2.Get the best entries for each problem.
Compute the overall score (and also render with
profile.j2). (See below for how overall score is computed.)
What makes the function so long is computing the overall score. The function is roughly the sum of: max(0, 100 + 200 * (user_score / best_score)) for each problem. It just turns out there are some annoying issues when the user has not actually submitted a problem (or there are no submissions at all). This scoring method ensures you get 100 points for attempting a problem (0 otherwise) and can achieve a maximum of 300 points if they have the highest score on the leaderboard.
A Couple of Other Pages¶
We have covered the most complicated pages, and have just left /problems and /users. Fortunately, most of the heavy-lifting for these pages is just a simple Jinja for-loop. For /problems this loop is located in conjecscore/templates/problems.j2 which renders a little card (generated by the conjecscore/templates/problem-card.j2 template) for each problem. And for /users this loop is located in conjecscore/templates/users.j2 which just lists every user nickname.