Added bruno API spec, and implemented most of the backend API.

This commit is contained in:
Andrew Lalis 2024-12-16 22:22:56 -05:00
parent 277af441e1
commit a52e13fe57
22 changed files with 743 additions and 31 deletions

View File

@ -6,11 +6,3 @@ CREATE TABLE user (
is_locked INTEGER NOT NULL, is_locked INTEGER NOT NULL,
is_admin INTEGER NOT NULL is_admin INTEGER NOT NULL
); );
INSERT INTO user (username, password_hash, created_at, is_locked, is_admin) VALUES (
'test',
'9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08',
1734380300,
0,
1
);

View File

@ -0,0 +1,47 @@
CREATE TABLE classroom_compliance_class (
id INTEGER PRIMARY KEY,
number INTEGER NOT NULL,
school_year TEXT NOT NULL,
user_id INTEGER NOT NULL REFERENCES user(id)
ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE TABLE classroom_compliance_student (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
class_id INTEGER NOT NULL REFERENCES classroom_compliance_class(id)
ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE TABLE classroom_compliance_desk_assignment (
id INTEGER PRIMARY KEY,
class_id INTEGER NOT NULL REFERENCES classroom_compliance_class(id)
ON UPDATE CASCADE ON DELETE CASCADE,
desk_number INTEGER NOT NULL,
student_id INTEGER REFERENCES classroom_compliance_student(id)
ON UPDATE CASCADE ON DELETE SET NULL
);
CREATE TABLE classroom_compliance_entry (
id INTEGER PRIMARY KEY,
class_id INTEGER NOT NULL REFERENCES classroom_compliance_class(id)
ON UPDATE CASCADE ON DELETE CASCADE,
class_description TEXT NOT NULL,
student_id INTEGER NOT NULL REFERENCES classroom_compliance_student(id),
date TEXT NOT NULL,
created_at INTEGER NOT NULL,
absent INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE classroom_compliance_entry_phone (
entry_id INTEGER PRIMARY KEY REFERENCES classroom_compliance_entry(id)
ON UPDATE CASCADE ON DELETE CASCADE,
compliant INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE classroom_compliance_entry_behavior (
entry_id INTEGER PRIMARY KEY REFERENCES classroom_compliance_entry(id)
ON UPDATE CASCADE ON DELETE CASCADE,
rating INTEGER NOT NULL,
comment TEXT
);

View File

@ -0,0 +1,17 @@
-- TEST DATA
-- username: test, password: test
INSERT INTO user (username, password_hash, created_at, is_locked, is_admin) VALUES (
'test',
'9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08',
1734380300,
0,
1
);
INSERT INTO user (username, password_hash, created_at, is_locked, is_admin) VALUES (
'test2',
'9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08',
1734394062,
0,
0
);

View File

@ -11,17 +11,13 @@ import data_utils;
struct User { struct User {
const ulong id; const ulong id;
const string username; const string username;
@Column("password_hash")
const string passwordHash; const string passwordHash;
@Column("created_at")
const ulong createdAt; const ulong createdAt;
@Column("is_locked")
const bool isLocked; const bool isLocked;
@Column("is_admin")
const bool isAdmin; const bool isAdmin;
} }
struct UserResponse { private struct UserResponse {
ulong id; ulong id;
string username; string username;
ulong createdAt; ulong createdAt;

View File

@ -0,0 +1,373 @@
module api_modules.classroom_compliance;
import handy_httpd;
import handy_httpd.handlers.path_handler;
import std.typecons : Nullable;
import d2sqlite3;
import db;
import data_utils;
import api_modules.auth : User, getUserOrThrow;
struct ClassroomComplianceClass {
const ulong id;
const ushort number;
const string schoolYear;
const ulong userId;
}
struct ClassroomComplianceStudent {
const ulong id;
const string name;
const ulong classId;
}
struct ClassroomComplianceDeskAssignment {
const ulong id;
const ulong classId;
const ushort deskNumber;
const ulong studentId;
}
struct ClassroomComplianceEntry {
const ulong id;
const ulong classId;
const string classDescription;
const ulong studentId;
const string date;
const ulong createdAt;
const bool absent;
}
struct ClassroomComplianceEntryPhone {
const ulong entryId;
const bool compliant;
}
struct ClassroomComplianceEntryBehavior {
const ulong entryId;
const ubyte rating;
const string comment;
}
void registerApiEndpoints(PathHandler handler) {
const ROOT_PATH = "/api/classroom-compliance";
handler.addMapping(Method.POST, ROOT_PATH ~ "/classes", &createClass);
handler.addMapping(Method.GET, ROOT_PATH ~ "/classes", &getClasses);
const CLASS_PATH = ROOT_PATH ~ "/classes/:classId:ulong";
handler.addMapping(Method.DELETE, CLASS_PATH, &deleteClass);
handler.addMapping(Method.POST, CLASS_PATH ~ "/students", &createStudent);
handler.addMapping(Method.GET, CLASS_PATH ~ "/students", &getStudents);
const STUDENT_PATH = CLASS_PATH ~ "/students/:studentId:ulong";
handler.addMapping(Method.PUT, STUDENT_PATH, &updateStudent);
handler.addMapping(Method.DELETE, STUDENT_PATH, &deleteStudent);
handler.addMapping(Method.POST, CLASS_PATH ~ "/desk-assignments", &setDeskAssignments);
handler.addMapping(Method.GET, CLASS_PATH ~ "/desk-assignments", &getDeskAssignments);
handler.addMapping(Method.DELETE, CLASS_PATH ~ "/desk-assignments", &removeAllDeskAssignments);
handler.addMapping(Method.POST, CLASS_PATH ~ "/entries", &createEntry);
}
void createClass(ref HttpRequestContext ctx) {
User user = getUserOrThrow(ctx);
struct ClassPayload {
ushort number;
string schoolYear;
}
auto payload = readJsonPayload!(ClassPayload)(ctx);
auto db = getDb();
const bool classNumberExists = canFind(
db,
"SELECT id FROM classroom_compliance_class WHERE number = ? AND school_year = ? AND user_id = ?",
payload.number,
payload.schoolYear,
user.id
);
if (classNumberExists) {
ctx.response.status = HttpStatus.BAD_REQUEST;
ctx.response.writeBodyString("There is already a class with this number, for this school year.");
return;
}
auto stmt = db.prepare("INSERT INTO classroom_compliance_class (number, school_year, user_id) VALUES (?, ?, ?)");
stmt.bindAll(payload.number, payload.schoolYear, user.id);
stmt.execute();
ulong classId = db.lastInsertRowid();
auto newClass = findOne!(ClassroomComplianceClass)(
db,
"SELECT * FROM classroom_compliance_class WHERE id = ? AND user_id = ?",
classId,
user.id
).orElseThrow();
writeJsonBody(ctx, newClass);
}
void getClasses(ref HttpRequestContext ctx) {
User user = getUserOrThrow(ctx);
auto db = getDb();
auto classes = findAll!(ClassroomComplianceClass)(
db,
"SELECT * FROM classroom_compliance_class WHERE user_id = ? ORDER BY school_year DESC, number ASC",
user.id
);
writeJsonBody(ctx, classes);
}
void deleteClass(ref HttpRequestContext ctx) {
User user = getUserOrThrow(ctx);
auto cls = getClassOrThrow(ctx, user);
auto db = getDb();
db.execute("DELETE FROM classroom_compliance_class WHERE id = ? AND user_id = ?", cls.id, user.id);
}
ClassroomComplianceClass getClassOrThrow(ref HttpRequestContext ctx, in User user) {
ulong classId = ctx.request.getPathParamAs!ulong("classId");
auto db = getDb();
return findOne!(ClassroomComplianceClass)(
db,
"SELECT * FROM classroom_compliance_class WHERE user_id = ? AND id = ?",
user.id,
classId
).orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
}
void createStudent(ref HttpRequestContext ctx) {
User user = getUserOrThrow(ctx);
auto cls = getClassOrThrow(ctx, user);
struct StudentPayload {
string name;
}
auto payload = readJsonPayload!(StudentPayload)(ctx);
auto db = getDb();
bool studentExists = canFind(
db,
"SELECT id FROM classroom_compliance_student WHERE name = ? AND class_id = ?",
payload.name,
cls.id
);
if (studentExists) {
ctx.response.status = HttpStatus.BAD_REQUEST;
ctx.response.writeBodyString("Student with that name already exists.");
return;
}
db.execute("INSERT INTO classroom_compliance_student (name, class_id) VALUES (?, ?)", payload.name, cls.id);
ulong studentId = db.lastInsertRowid();
auto student = findOne!(ClassroomComplianceStudent)(
db,
"SELECT * FROM classroom_compliance_student WHERE id = ?",
studentId
).orElseThrow();
writeJsonBody(ctx, student);
}
void getStudents(ref HttpRequestContext ctx) {
User user = getUserOrThrow(ctx);
auto cls = getClassOrThrow(ctx, user);
auto db = getDb();
auto students = findAll!(ClassroomComplianceStudent)(
db,
"SELECT * FROM classroom_compliance_student WHERE class_id = ? ORDER BY name ASC",
cls.id
);
writeJsonBody(ctx, students);
}
void updateStudent(ref HttpRequestContext ctx) {
User user = getUserOrThrow(ctx);
auto student = getStudentOrThrow(ctx, user);
struct StudentUpdatePayload {
string name;
}
auto payload = readJsonPayload!(StudentUpdatePayload)(ctx);
// If there is nothing to update, quit.
if (payload.name == student.name) return;
// Check that the new name doesn't already exist.
auto db = getDb();
bool newNameExists = canFind(
db,
"SELECT id FROM classroom_compliance_student WHERE name = ? AND class_id = ?",
payload.name,
student.classId
);
if (newNameExists) {
ctx.response.status = HttpStatus.BAD_REQUEST;
ctx.response.writeBodyString("Student with that name already exists.");
return;
}
db.execute(
"UPDATE classroom_compliance_student SET name = ? WHERE id = ?",
payload.name,
student.id
);
auto updatedStudent = findOne!(ClassroomComplianceStudent)(
db,
"SELECT * FROM classroom_compliance_student WHERE id = ?",
student.id
).orElseThrow();
writeJsonBody(ctx, updatedStudent);
}
void deleteStudent(ref HttpRequestContext ctx) {
User user = getUserOrThrow(ctx);
auto student = getStudentOrThrow(ctx, user);
auto db = getDb();
db.execute(
"DELETE FROM classroom_compliance_student WHERE id = ? AND class_id = ?",
student.id,
student.classId
);
}
ClassroomComplianceStudent getStudentOrThrow(ref HttpRequestContext ctx, in User user) {
ulong classId = ctx.request.getPathParamAs!ulong("classId");
ulong studentId = ctx.request.getPathParamAs!ulong("studentId");
auto db = getDb();
string query = "
SELECT s.*
FROM classroom_compliance_student s
LEFT JOIN classroom_compliance_class c ON s.class_id = c.id
WHERE s.id = ? AND s.class_id = ? AND c.user_id = ?
";
return findOne!(ClassroomComplianceStudent)(
db,
query,
studentId,
classId,
user.id
).orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
}
private struct DeskAssignmentPayloadEntry {
ushort deskNumber;
Nullable!ulong studentId;
}
private struct DeskAssignmentPayload {
DeskAssignmentPayloadEntry[] entries;
}
void setDeskAssignments(ref HttpRequestContext ctx) {
User user = getUserOrThrow(ctx);
auto cls = getClassOrThrow(ctx, user);
auto payload = readJsonPayload!(DeskAssignmentPayload)(ctx);
auto db = getDb();
auto validationError = validateDeskAssignments(db, payload, cls.id);
if (validationError) {
import slf4d;
warnF!"Desk assignment validation failed: %s"(validationError.value);
ctx.response.status = HttpStatus.BAD_REQUEST;
ctx.response.writeBodyString(validationError.value);
return;
}
db.begin();
try {
db.execute(
"DELETE FROM classroom_compliance_desk_assignment WHERE class_id = ?",
cls.id
);
auto stmt = db.prepare(
"INSERT INTO classroom_compliance_desk_assignment (class_id, desk_number, student_id) VALUES (?, ?, ?)"
);
foreach (entry; payload.entries) {
stmt.bindAll(cls.id, entry.deskNumber, entry.studentId);
stmt.execute();
stmt.clearBindings();
stmt.reset();
}
db.commit();
// Return the new list of desk assignments to the user.
auto newAssignments = findAll!(ClassroomComplianceDeskAssignment)(
db,
"SELECT * FROM classroom_compliance_desk_assignment WHERE class_id = ?",
cls.id
);
writeJsonBody(ctx, newAssignments);
} catch (Exception e) {
db.rollback();
}
}
Optional!string validateDeskAssignments(Database db, in DeskAssignmentPayload payload, ulong classId) {
import std.algorithm : canFind, map;
import std.array;
// Check that desks are numbered 1 .. N.
for (int n = 1; n <= payload.entries.length; n++) {
bool deskPresent = false;
foreach (entry; payload.entries) {
if (entry.deskNumber == n) {
deskPresent = true;
break;
}
}
if (!deskPresent) return Optional!string.of("Desks should be numbered from 1 to N.");
}
auto allStudents = findAll!(ClassroomComplianceStudent)(
db,
"SELECT * FROM classroom_compliance_student WHERE class_id = ?",
classId
);
auto studentIds = allStudents.map!(s => s.id).array;
// Check that if a desk is assigned to a student, that it's one from this class.
foreach (entry; payload.entries) {
if (!entry.studentId.isNull && !canFind(studentIds, entry.studentId.get)) {
return Optional!string.of("Desks cannot be assigned to students that don't belong to this class.");
}
}
// Check that each student in the class is assigned a desk.
ushort[] takenDesks;
foreach (student; allStudents) {
ushort[] assignedDesks;
foreach (entry; payload.entries) {
if (!entry.studentId.isNull && entry.studentId.get() == student.id) {
assignedDesks ~= entry.deskNumber;
}
}
if (assignedDesks.length != 1) {
if (assignedDesks.length > 1) {
return Optional!string.of("A student is assigned to more than one desk.");
} else {
return Optional!string.of("Not all students are assigned to a desk.");
}
}
if (canFind(takenDesks, assignedDesks[0])) {
return Optional!string.of("Cannot assign more than one student to the same desk.");
}
takenDesks ~= assignedDesks[0];
}
return Optional!string.empty();
}
void getDeskAssignments(ref HttpRequestContext ctx) {
User user = getUserOrThrow(ctx);
auto cls = getClassOrThrow(ctx, user);
auto db = getDb();
auto deskAssignments = findAll!(ClassroomComplianceDeskAssignment)(
db,
"
SELECT d.* FROM classroom_compliance_desk_assignment d
WHERE class_id = ?
ORDER BY desk_number ASC
",
cls.id
);
writeJsonBody(ctx, deskAssignments);
}
void removeAllDeskAssignments(ref HttpRequestContext ctx) {
User user = getUserOrThrow(ctx);
auto cls = getClassOrThrow(ctx, user);
auto db = getDb();
db.execute(
"DELETE FROM classroom_compliance_desk_assignment WHERE class_id = ?",
cls.id
);
}
void createEntry(ref HttpRequestContext ctx) {
User user = getUserOrThrow(ctx);
auto cls = getClassOrThrow(ctx, user);
}

View File

@ -5,8 +5,13 @@ import d2sqlite3;
import db; import db;
import api_modules.auth; import api_modules.auth;
static import api_modules.classroom_compliance;
void main() { void main() {
// Initialize the database on startup.
auto db = getDb();
db.close();
ServerConfig config; ServerConfig config;
config.enableWebSockets = false; config.enableWebSockets = false;
config.port = 8080; config.port = 8080;
@ -21,6 +26,7 @@ void main() {
PathHandler handler = new PathHandler(); PathHandler handler = new PathHandler();
handler.addMapping(Method.OPTIONS, "/api/**", &optionsEndpoint); handler.addMapping(Method.OPTIONS, "/api/**", &optionsEndpoint);
handler.addMapping(Method.POST, "/api/auth/login", &loginEndpoint); handler.addMapping(Method.POST, "/api/auth/login", &loginEndpoint);
api_modules.classroom_compliance.registerApiEndpoints(handler);
HttpServer server = new HttpServer(handler, config); HttpServer server = new HttpServer(handler, config);
server.start(); server.start();

View File

@ -19,6 +19,8 @@ T readJsonPayload(T)(ref HttpRequestContext ctx) {
string requestBody = ctx.request.readBodyAsString(); string requestBody = ctx.request.readBodyAsString();
return deserialize!T(requestBody); return deserialize!T(requestBody);
} catch (SerdeException e) { } catch (SerdeException e) {
import slf4d;
warnF!"Failed to read JSON payload: %s"(e.msg);
throw new HttpStatusException(HttpStatus.BAD_REQUEST); throw new HttpStatusException(HttpStatus.BAD_REQUEST);
} }
} }
@ -31,10 +33,25 @@ T readJsonPayload(T)(ref HttpRequestContext ctx) {
* data = The data to write. * data = The data to write.
*/ */
void writeJsonBody(T)(ref HttpRequestContext ctx, in T data) { void writeJsonBody(T)(ref HttpRequestContext ctx, in T data) {
import std.traits : isArray;
try { try {
static if (isArray!T) {
if (data.length == 0) {
ctx.response.writeBodyString("[]", "application/json");
return;
}
}
string jsonStr = serializeToJson(data); string jsonStr = serializeToJson(data);
ctx.response.writeBodyString(jsonStr, "application/json"); ctx.response.writeBodyString(jsonStr, "application/json");
} catch (SerdeException e) { } catch (SerdeException e) {
throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR); throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
} }
} }
ulong getUnixTimestampMillis() {
import std.datetime;
SysTime now = Clock.currTime();
SysTime unixEpoch = SysTime(DateTime(1970, 1, 1), UTC());
Duration diff = now - unixEpoch;
return diff.total!"msecs";
}

View File

@ -22,14 +22,61 @@ Database getDb() {
} }
Database db = Database("teacher-tools.db", flags); Database db = Database("teacher-tools.db", flags);
db.execute("PRAGMA foreign_keys=ON"); db.execute("PRAGMA foreign_keys=ON");
const string schema = import("schema.sql");
if (shouldInitDb) { if (shouldInitDb) {
db.run(schema); const string authSchema = import("schema/auth.sql");
const string classroomComplianceSchema = import("schema/classroom_compliance.sql");
db.run(authSchema);
db.run(classroomComplianceSchema);
const string sampleData = import("schema/sample_data.sql");
db.run(sampleData);
info("Initialized database schema."); info("Initialized database schema.");
} }
return db; return db;
} }
T[] findAll(T, Args...)(Database db, string query, Args args) {
Statement stmt = db.prepare(query);
stmt.bindAll(args);
ResultRange result = stmt.execute();
return result.map!(row => parseRow!T(row)).array;
}
Optional!T findOne(T, Args...)(Database db, string query, Args args) {
Statement stmt = db.prepare(query);
stmt.bindAll(args);
ResultRange result = stmt.execute();
if (result.empty) return Optional!T.empty;
return Optional!T.of(parseRow!T(result.front));
}
bool canFind(Args...)(Database db, string query, Args args) {
Statement stmt = db.prepare(query);
stmt.bindAll(args);
return !stmt.execute().empty;
}
private string toSnakeCase(string camelCase) {
import std.uni;
if (camelCase.length == 0) return camelCase;
auto app = appender!string;
app ~= toLower(camelCase[0]);
for (int i = 1; i < camelCase.length; i++) {
if (isUpper(camelCase[i])) {
app ~= '_';
app ~= toLower(camelCase[i]);
} else {
app ~= camelCase[i];
}
}
return app[];
}
unittest {
assert(toSnakeCase("testValue") == "test_value");
}
private string[] getColumnNames(T)() { private string[] getColumnNames(T)() {
import std.string : toLower; import std.string : toLower;
alias members = __traits(allMembers, T); alias members = __traits(allMembers, T);
@ -38,7 +85,7 @@ private string[] getColumnNames(T)() {
static if (__traits(getAttributes, __traits(getMember, T, members[i])).length > 0) { static if (__traits(getAttributes, __traits(getMember, T, members[i])).length > 0) {
columnNames[i] = toLower(__traits(getAttributes, __traits(getMember, T, members[i]))[0].name); columnNames[i] = toLower(__traits(getAttributes, __traits(getMember, T, members[i]))[0].name);
} else { } else {
columnNames[i] = toLower(members[i]); columnNames[i] = toLower(toSnakeCase(members[i]));
} }
} }
return columnNames.dup; return columnNames.dup;
@ -61,18 +108,3 @@ T parseRow(T)(Row row) {
mixin("T t = T(" ~ getArgsStr!T ~ ");"); mixin("T t = T(" ~ getArgsStr!T ~ ");");
return t; return t;
} }
T[] findAll(T, Args...)(Database db, string query, Args args) {
Statement stmt = db.prepare(query);
stmt.bindAll(args);
ResultRange result = stmt.execute();
return result.map!(row => parseRow!T(row)).array;
}
Optional!T findOne(T, Args...)(Database db, string query, Args args) {
Statement stmt = db.prepare(query);
stmt.bindAll(args);
ResultRange result = stmt.execute();
if (result.empty) return Optional!T.empty;
return Optional!T.of(parseRow!T(result.front));
}

11
bruno-api/Auth/Login.bru Normal file
View File

@ -0,0 +1,11 @@
meta {
name: Login
type: http
seq: 1
}
post {
url: {{base_url}}/auth/login
body: none
auth: inherit
}

View File

@ -0,0 +1,18 @@
meta {
name: Create Class
type: http
seq: 1
}
post {
url: {{base_url}}/classroom-compliance/classes
body: json
auth: inherit
}
body:json {
{
"number": 2,
"schoolYear": "2024-2025"
}
}

View File

@ -0,0 +1,30 @@
meta {
name: Create Compliance Entry
type: http
seq: 10
}
post {
url: {{base_url}}/classroom-compliance/classes/:classId/entries
body: json
auth: inherit
}
params:path {
classId: {{class_id}}
}
body:json {
{
"student_id": 1,
"date": "2024-12-16",
"absent": false,
"phone": {
"compliant": true
},
"behavior": {
"rating": 3,
"comment": "Good job!"
}
}
}

View File

@ -0,0 +1,21 @@
meta {
name: Create Student
type: http
seq: 4
}
post {
url: {{base_url}}/classroom-compliance/classes/:classId/students
body: json
auth: inherit
}
params:path {
classId: {{class_id}}
}
body:json {
{
"name": "John F. Kennedy"
}
}

View File

@ -0,0 +1,15 @@
meta {
name: Delete Class
type: http
seq: 3
}
delete {
url: {{base_url}}/classroom-compliance/classes/:classId
body: none
auth: inherit
}
params:path {
classId: 1
}

View File

@ -0,0 +1,16 @@
meta {
name: Delete Student
type: http
seq: 7
}
delete {
url: {{base_url}}/classroom-compliance/classes/:classId/students/:studentId
body: none
auth: inherit
}
params:path {
classId: {{class_id}}
studentId: 3
}

View File

@ -0,0 +1,11 @@
meta {
name: Get Classes
type: http
seq: 2
}
get {
url: {{base_url}}/classroom-compliance/classes
body: none
auth: inherit
}

View File

@ -0,0 +1,15 @@
meta {
name: Get Desk Assignments
type: http
seq: 8
}
get {
url: {{base_url}}/classroom-compliance/classes/:classId/desk-assignments
body: none
auth: inherit
}
params:path {
classId: {{class_id}}
}

View File

@ -0,0 +1,15 @@
meta {
name: Get Students
type: http
seq: 5
}
get {
url: {{base_url}}/classroom-compliance/classes/:classId/students
body: none
auth: inherit
}
params:path {
classId: {{class_id}}
}

View File

@ -0,0 +1,30 @@
meta {
name: Set Desk Assignments
type: http
seq: 9
}
post {
url: {{base_url}}/classroom-compliance/classes/:classId/desk-assignments
body: json
auth: inherit
}
params:path {
classId: {{class_id}}
}
body:json {
{
"entries": [
{
"deskNumber": 1,
"studentId": 1
},
{
"deskNumber": 2,
"studentId": 2
}
]
}
}

View File

@ -0,0 +1,22 @@
meta {
name: Update Student
type: http
seq: 6
}
put {
url: {{base_url}}/classroom-compliance/classes/:classId/students/:studentId
body: json
auth: inherit
}
params:path {
classId: {{class_id}}
studentId: 1
}
body:json {
{
"name": "John W. Booth"
}
}

View File

@ -0,0 +1,7 @@
meta {
name: Classroom Compliance
}
vars:pre-request {
class_id: 1
}

9
bruno-api/bruno.json Normal file
View File

@ -0,0 +1,9 @@
{
"version": "1",
"name": "Teacher-Tools",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

12
bruno-api/collection.bru Normal file
View File

@ -0,0 +1,12 @@
auth {
mode: basic
}
auth:basic {
username: test
password: test
}
vars:pre-request {
base_url: http://localhost:8080/api
}