This is a coding kata platform backend written in Java & Spring Boot, with existing support for Java, JavaScript & Python. Support for additional languages can be added easily. The corresponding front-end repo is written in React/TypeScript, and can be found here: https://github.com/RichardSneyd/coding-kata-platform-frontend
- How to Build and Run Locally
- Authentication and Authorisation
- CompileController
- EvalController
- Problems
- Solutions
- [Problem Sets](#probleThis m-sets)
- Difficulty Settings
- Input & Output Values
- Users
- Leaderboards
- Password Reset
- Cohorts
- Scoring
- Create a PostreSQL db called
lms_db - The above db should be accessible via username:
postgres, password:bnta_db_2022 - Make sure that Java, node, and python3 are all installed on your computer (they are accessed via ProcessBuilder internally to compile/exec)
Auth is handled by Spring Security and JWT. There are 2 roles, ADMIN and USER, assigned on creation of a user record. Before you can use any of the below functionality, a login request with a username and password must be provided. If login is successful, a Jwt access_token is returned:
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJmYWtlc3R1ZGVudCIsInJvbGVzIjpbIlVTRVIiXSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2xvZ2luIiwiZXhwIjoxNjY3NTc2NTk0LCJ1c2VySWQiOjJ9.go4em7DFZ8Cp_ZEZnnDfiUKzC9I_uRqpomYteCj_USM",
"refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJmYWtlc3R1ZGVudCIsInJvbGVzIjpbIlVTRVIiXSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2xvZ2luIiwiZXhwIjoxNjY4MTc3Nzk0fQ.hbhFt0SiEsEdB4owEWgmxxc5uyEJB6h_QuzrLjp_kfE"
}
This access token must be included in the Authorization header in all subsequent API calls., prefixed by 'Bearer ', to indicate to Spring Security that this token belongs to the user in question. This process can be handled by a service, to avoid repetition for every axios/fetch call.
to use the compiler directly (not to test solutions to problems since EvalController handles that.), send a POST request to /user/compile. The Request body should include a code property as well as a lang property. Currently supported languages are:
- 'java' (Java)
- 'js' (Node)
- 'py' (Python)
This can be used with a 'run' button to allow the user to run their code with logs for debugging, as logs must be removed or commented out before submitting code to the EvalController.
Use the file-extension of the language in the lang property, as indicated above.
{
"lang": "java",
"code": "public class Main {public static void main(String[] args) {System.out.println(\"Hello from Java POST test\");}}"
}If you have provided syntactically valid code, you should get a JSON response in the format of a CompileResult object (id is null since we don't save the compiled result in the DB. We only do that when through the EvalController):
{
"id": null,
"output": "Hello BNTA",
"errors": "",
"lang": "js",
"code": "console.log('Hello BNTA')",
"compiled": true
}This is arguably the most important part of the API - where we submit attempted solutions, and evaluate them against the TestSuite of a specified Problem,
then return a EvalResult, which will either be successful, or not. User logging, such as sout or console.log, are programmatically removed before evaluation to avoid outcome corruption. POST request to: /user/eval/{problemId}, with following
JSON format:
{
"lang": "java",
"code": "public class Main {public int add(int a, int b) {return a + b;}}",
"userId": 1
}The response body will be of the EvalResult format. The code is automatically sanitised for logging before being evaluated. The result of running the test with the users logs can be accessed in the testResultsWithLogs property. This allows us to feed back to them the output from their manual debugging/testing. The test results after the logs have been automatically removed, are accessed via publicTestResults:
{
"successful": true,
"privateTestsPassed": true,
"publicTestResults": [
{
"compileResult": {
"id": null,
"output": "15",
"errors": "",
"lang": "java",
"code": "public class Main {public int solution(int a, int b) {return a + b;}public static void main(String[] args){System.out.println(new Main().solution(10, 5));}}",
"compiled": true
},
"correct": true
}
],
"testResultsWithLogs": [
{
"compileResult": {
"id": null,
"output": "user logs\n15",
"errors": "",
"lang": "java",
"code": "public class Main {public int solution(int a, int b) {System.out.println(\"user logs\"); return a + b;}public static void main(String[] args){System.out.println(new Main().solution(10, 5));}}",
"compiled": true
},
"correct": false
}
],
"problem": {
"id": 1,
"title": "add",
"description": "Create a function, addValues(a:int, b:int), which adds two integers together and returns the result.",
"difficulty": "VERY_EASY",
"testSuite": {
"publicCases": [
{
"id": 1,
"inputs": [
{
"id": 1,
"value": "10",
"dataType": "INT"
},
{
"id": 2,
"value": "5",
"dataType": "INT"
}
],
"output": {
"id": 3,
"value": "15",
"dataType": "INT"
}
}
],
"privateCases": [
{
"id": 2,
"inputs": [
{
"id": 4,
"value": "15",
"dataType": "INT"
},
{
"id": 5,
"value": "4",
"dataType": "INT"
}
],
"output": {
"id": 6,
"value": "19",
"dataType": "INT"
}
}
]
},
"startCode": {
"id": 1,
"js": "const addValues = (a, b)=> {\n\n}",
"py": "def addValues(a, b):\n\nreturn",
"java": ""
},
"tags": [
"adding",
"arithmetic"
]
}
}
if the eval was successful, a Solution record will have been added to the DB as well.
All problems are assigned a difficulty level. They can also be assigned tags. In addition to the usual GET routes, you can get them by tag or difficulty:
Get all: GET /user/problems
Get by id: GET /user/problems/{problemId}
Get by tag: GET /user/problems/tag/{tag}
Get by difficulty: GET /user/problems/difficulty/{difficulty}
Create new: POST admin/problems/
{
"title": "add",
"description": "Create a function, addValues(a:int, b:int), which adds two integers together and returns the result.",
"difficulty": "VERY_EASY",
"testSuite": {
"publicCases": [
{
"inputs": [
{
"value": "10",
"dataType": "INT"
},
{
"value": "5",
"dataType": "INT"
}
],
"output": {
"value": "15",
"dataType": "INT"
}
}
],
"privateCases": [
{
"inputs": [
{
"value": "15",
"dataType": "INT"
},
{
"value": "4",
"dataType": "INT"
}
],
"output": {
"value": "19",
"dataType": "INT"
}
}
]
}
}The Controller and Service have been written to loop through all the testCases, inputs etc., and save them to the respective repos. If you have provided the data in the proper format, you will get a Problem response, complete with the id's for all the inner objects (TestSuite, TestCase, Data...):
{
"id": 4,
"title": "add",
"description": "Create a function, addValues(a:int, b:int), which adds two integers together and returns the result.",
"difficulty": "VERY_EASY",
"testSuite": {
"publicCases": [
{
"id": 7,
"inputs": [
{
"id": 15,
"value": "10",
"dataType": "INT"
},
{
"id": 16,
"value": "5",
"dataType": "INT"
}
],
"output": {
"id": 17,
"value": "15",
"dataType": "INT"
}
}
],
"privateCases": [
{
"id": 8,
"inputs": [
{
"id": 18,
"value": "15",
"dataType": "INT"
},
{
"id": 19,
"value": "4",
"dataType": "INT"
}
],
"output": {
"id": 20,
"value": "19",
"dataType": "INT"
}
}
]
},
"startCode": null,
"tags": null
}You can retrieve all the solutions for a problem like so:
GET /user/problems/{problemId}/solutions
Problem Sets are basically groups of problems, with a title and description for the group.
Get all: GET /user/problems/sets
Get by id: GET /user/problems/sets/{id}
Make new set: POST /admin/problems/sets
Payload example:
{
"title": "Loops 101",
"description": "A group of problems to practice the fundamentals of loops",
"problems": [
{"id": 1},
{"id": 2}
]
}Possible difficulty values are:
VERY_EASY, EASY, MEDIUM, HARD, VERY_HARD.
Values can be of the following 8 types, as defined in the DATATYPE Enum:
INT, INT_ARRAY, STRING, STRING_ARRAY, FLOAT, FLOAT_ARRAY, BOOLEAN, BOOLEAN_ARRAY
Only ADMIN accounts can create, udpate or delete users. While you can create new users individually here, such as for ADMIN accounts (trainers etc), it is generally best practice to create USER accounts (students) when you create a new cohort via the CohortController, documented below. This is the fastest and most efficient approach.
New User: POST /admin/users/, with in input of the format:
{
"username": "fakestudent",
"email": "fake@phony.com",
"cohort": {
"id": 1
},
"roles": [
"USER"
],
"startDate": "2022-11-08"
}The user will be issued an email to set up their own password.
All Users: GET /admin/users
User by id: GET /user/users/{userId}
Global Leaderboard: GET /admin/users/leaderboard
Leaderboard by Cohort Id: GET user/users/leaderboard/{cohortId}
Leaderboard by Cohort Name: GET user/users/leaderboard/cohort-name/{cohortName}
These routes will return User objects:
[
{
"id": 3,
"username": "JoeBlogs",
"email": "joeblogs@hotmail.com",
"cohort": null,
"roles": [
"USER"
],
"completedProblems": [],
"score": 0,
"joinDate": "2022-11-09",
"solutions": []
},
{
"id": 1,
"username": "richard",
"email": "richard@fakeaddress.com",
"cohort": null,
"completedProblems": [],
"roles": [
"ADMIN"
],
"score": 0,
"joinDate": "2022-11-09",
"solutions": []
},
{
"id": 2,
"username": "fakestudent",
"email": "student@fakeaddress.com",
"cohort": {
"id": 1,
"name": "C7",
"startDate": "2022-11-09"
},
"completedProblems": [],
"roles": [
"USER"
],
"score": 0,
"joinDate": "2022-11-09",
"solutions": []
},
{
"id": 4,
"username": "CaptainCrisps",
"email": "captaincrisps@nowhere.com",
"cohort": null,
"completedProblems": [],
"roles": [
"USER"
],
"score": 0,
"joinDate": "2022-11-09",
"solutions": []
}
]Create: POST /admin/users
Update: PUT /admin/users
Delete: DELETE /admin/users/{userId}
Forgot password (request reset email): GET /password/forgot/{userEmail}
Reset Password (call from reset form): POST /password/reset
the reset expects a json payload of type PasswordResetInput. Provide the secret you received in the reset email (it's
a request param in the url):
{
"userId": 1,
"secret": "dfqwpeoiasdafdklnqiwenfaisdn",
"newPassword": "blablabla"
}Cohorts can only be created by ADMIN users:
POST /admin/cohorts
You can create the cohort members at the same time, as demonstarted below. The payload should follow this format:
{
"name": "C8",
"members": [
{
"username": "JoeBlogs",
"email": "joeblogs@hotmail.com"
},
{
"username": "CaptainCrisps",
"email": "captaincrisps@nowhere.com"
}
]
}If successful, you will be returned a cohort object complete with startDate and id properties. Any new members submitted will also have been processed, and emailed to set their own password:
{
"id": 2,
"name": "C8",
"startDate": "2022-11-09",
"members": [
{
"id": 3,
"username": "JoeBloggs",
"email": "richardsneyd@hotmail.com",
"roles": [
"USER"
],
"score": 0,
"joinDate": "2022-11-09",
"solutions": []
},
{
"id": 4,
"username": "CaptainCrisps",
"email": "captaincrisps@nowhere.com",
"roles": [
"USER"
],
"score": 0,
"joinDate": "2022-11-09",
"solutions": []
}
]
}All: GET user/cohorts
[
{
"id": 1,
"name": "C7",
"startDate": "2022-11-09",
"members": [
{
"id": 2,
"username": "fakestudent",
"email": "student@fakeaddress.com",
"roles": [
"USER"
],
"score": 0,
"joinDate": "2022-11-09",
"solutions": []
}
]
},
{
"id": 2,
"name": "C8",
"startDate": "2022-11-09",
"members": [
{
"id": 3,
"username": "JoeBlogs",
"email": "joeblogs@hotmail.com",
"roles": [
"USER"
],
"score": 0,
"joinDate": "2022-11-09",
"solutions": []
},
{
"id": 4,
"username": "CaptainCrisps",
"email": "captaincrisps@nowhere.com",
"roles": [
"USER"
],
"score": 0,
"joinDate": "2022-11-09",
"solutions": []
}
]
}
]By Id: GET user/cohorts/{cohortId}
By Name: GET user/cohorts/by-name/{cohortName}
Delete Cohort: DELETE user/cohorts/{cohortId}
Update cohort: PUT user/cohorts
The emphasis with the scoring system is on encouraging and rewarded perseverance, to increase practice time and engagement. Scores are awarded based on the difficulty level of the problem. Points are awarded for multiple solutions to the same problem, if they are in different languages. So, the user could submit 3 solutions: One in Java, Python and JS respectively, and receive points for each of those solutions. Points are only awarded once for each language.