diff --git a/api/src/main/java/com/codeforcommunity/api/authenticated/IProtectedDataProcessor.java b/api/src/main/java/com/codeforcommunity/api/authenticated/IProtectedDataProcessor.java new file mode 100644 index 00000000..9624309c --- /dev/null +++ b/api/src/main/java/com/codeforcommunity/api/authenticated/IProtectedDataProcessor.java @@ -0,0 +1,16 @@ +package com.codeforcommunity.api.authenticated; + +import com.codeforcommunity.auth.JWTData; +import com.codeforcommunity.dto.data.MetricsCountryResponse; +import com.codeforcommunity.dto.data.MetricsSchoolResponse; +import com.codeforcommunity.dto.data.MetricsTotalResponse; +import com.codeforcommunity.enums.Country; + +public interface IProtectedDataProcessor { + + MetricsTotalResponse getFixedTotalMetrics(JWTData userData); + + MetricsCountryResponse getFixedCountryMetrics(JWTData userData, Country country); + + MetricsSchoolResponse getFixedSchoolMetrics(JWTData userData, int schoolId); +} diff --git a/api/src/main/java/com/codeforcommunity/dto/data/MetricGeneric.java b/api/src/main/java/com/codeforcommunity/dto/data/MetricGeneric.java new file mode 100644 index 00000000..71544291 --- /dev/null +++ b/api/src/main/java/com/codeforcommunity/dto/data/MetricGeneric.java @@ -0,0 +1,28 @@ +package com.codeforcommunity.dto.data; + +public class MetricGeneric { + + private Integer totalBooks; + private Integer totalStudents; + + public MetricGeneric(Integer totalBooks, Integer totalStudents) { + this.totalBooks = totalBooks; + this.totalStudents = totalStudents; + } + + public Integer getTotalBooks() { + return totalBooks; + } + + public Integer getTotalStudents() { + return totalStudents; + } + + public void setTotalBooks(Integer totalBooks) { + this.totalBooks = totalBooks; + } + + public void setTotalStudents(Integer totalStudents) { + this.totalStudents = totalStudents; + } +} diff --git a/api/src/main/java/com/codeforcommunity/dto/data/MetricsCountryResponse.java b/api/src/main/java/com/codeforcommunity/dto/data/MetricsCountryResponse.java new file mode 100644 index 00000000..9acd2c53 --- /dev/null +++ b/api/src/main/java/com/codeforcommunity/dto/data/MetricsCountryResponse.java @@ -0,0 +1,95 @@ +package com.codeforcommunity.dto.data; + +public class MetricsCountryResponse { + private Integer countSchools; + private Integer countVolunteerAccounts; + private Integer countAdminAccounts; + private Float avgCountBooksPerStudent; + private Float avgCountStudentLibrariansPerSchool; + private Float percentSchoolsWithLibraries; + private Integer countStudents; + private Integer countBooks; + + public MetricsCountryResponse( + Integer countSchools, + Integer countVolunteerAccounts, + Integer countAdminAccounts, + Float avgCountBooksPerStudent, + Float avgCountStudentLibrariansPerSchool, + Float percentSchoolsWithLibraries, + Integer countStudents, + Integer countBooks) { + this.countSchools = countSchools; + this.countVolunteerAccounts = countVolunteerAccounts; + this.countAdminAccounts = countAdminAccounts; + this.avgCountBooksPerStudent = avgCountBooksPerStudent; + this.avgCountStudentLibrariansPerSchool = avgCountStudentLibrariansPerSchool; + this.percentSchoolsWithLibraries = percentSchoolsWithLibraries; + this.countStudents = countStudents; + this.countBooks = countBooks; + } + + public Integer getCountSchools() { + return countSchools; + } + + public void setCountSchools(Integer countSchools) { + this.countSchools = countSchools; + } + + public Integer getCountVolunteerAccounts() { + return countVolunteerAccounts; + } + + public void setCountVolunteerAccounts(Integer countVolunteerAccounts) { + this.countVolunteerAccounts = countVolunteerAccounts; + } + + public Integer getCountAdminAccounts() { + return countAdminAccounts; + } + + public void setCountAdminAccounts(Integer countAdminAccounts) { + this.countAdminAccounts = countAdminAccounts; + } + + public Float getAvgCountBooksPerStudent() { + return avgCountBooksPerStudent; + } + + public void setAvgCountBooksPerStudent(Float avgCountBooksPerStudent) { + this.avgCountBooksPerStudent = avgCountBooksPerStudent; + } + + public Float getAvgCountStudentLibrariansPerSchool() { + return avgCountStudentLibrariansPerSchool; + } + + public void setAvgCountStudentLibrariansPerSchool(Float avgCountStudentLibrariansPerSchool) { + this.avgCountStudentLibrariansPerSchool = avgCountStudentLibrariansPerSchool; + } + + public Float getPercentSchoolsWithLibraries() { + return percentSchoolsWithLibraries; + } + + public void setPercentSchoolsWithLibraries(Float percentSchoolsWithLibraries) { + this.percentSchoolsWithLibraries = percentSchoolsWithLibraries; + } + + public Integer getCountBooks() { + return countBooks; + } + + public void setCountBooks(Integer countBooks) { + this.countBooks = countBooks; + } + + public Integer getCountStudents() { + return countStudents; + } + + public void setCountStudents(Integer countStudents) { + this.countStudents = countStudents; + } +} diff --git a/api/src/main/java/com/codeforcommunity/dto/data/MetricsSchoolResponse.java b/api/src/main/java/com/codeforcommunity/dto/data/MetricsSchoolResponse.java new file mode 100644 index 00000000..5e1956bc --- /dev/null +++ b/api/src/main/java/com/codeforcommunity/dto/data/MetricsSchoolResponse.java @@ -0,0 +1,62 @@ +package com.codeforcommunity.dto.data; + +public class MetricsSchoolResponse { + private Float countBooksPerStudent; + private Integer countStudents; + private Integer countStudentLibrarians; + private Integer netBooksInOut; + private Integer countBooks; + + public MetricsSchoolResponse( + Float countBooksPerStudent, + Integer countStudents, + Integer countStudentLibrarians, + Integer netBooksInOut, + Integer countBooks) { + this.countBooksPerStudent = countBooksPerStudent; + this.countStudents = countStudents; + this.countStudentLibrarians = countStudentLibrarians; + this.netBooksInOut = netBooksInOut; + this.countBooks = countBooks; + } + + public Float getCountBooksPerStudent() { + return countBooksPerStudent; + } + + public void setCountBooksPerStudent(Float countBooksPerStudent) { + this.countBooksPerStudent = countBooksPerStudent; + } + + public Integer getCountStudents() { + return countStudents; + } + + public void setCountStudents(Integer countStudents) { + this.countStudents = countStudents; + } + + public Integer getCountStudentLibrarians() { + return countStudentLibrarians; + } + + public void setCountStudentLibrarians(Integer countStudentLibrarians) { + this.countStudentLibrarians = countStudentLibrarians; + } + + public Integer getNetBooksInOut() { + return netBooksInOut; + } + + public void setNetBooksInOut(Integer netBooksInOut) { + this.netBooksInOut = netBooksInOut; + } + + public Integer getCountBooks() { + return countBooks; + } + + public void setCountBooks(Integer countBooks) { + this.countBooks = countBooks; + } +} diff --git a/api/src/main/java/com/codeforcommunity/dto/data/MetricsTotalResponse.java b/api/src/main/java/com/codeforcommunity/dto/data/MetricsTotalResponse.java new file mode 100644 index 00000000..f91bbe12 --- /dev/null +++ b/api/src/main/java/com/codeforcommunity/dto/data/MetricsTotalResponse.java @@ -0,0 +1,35 @@ +package com.codeforcommunity.dto.data; + +public class MetricsTotalResponse { + private Integer countSchools; + private Integer countBooks; + private Integer countStudents; + + public MetricsTotalResponse(Integer countSchools, Integer countBooks, Integer countStudents) { + this.countSchools = countSchools; + this.countBooks = countBooks; + this.countStudents = countStudents; + } + + public Integer getCountSchools() { + return countSchools; + } + + public void setCountSchools(Integer countSchools) { + this.countSchools = countSchools; + } + + public Integer getCountBooks() { + return countBooks; + } + + public void setCountBooks(Integer countBooks) { + this.countBooks = countBooks; + } + + public Integer getCountStudents() { return countStudents; } + + public void setCountStudents(Integer countStudents) { + this.countStudents = countStudents; + } +} diff --git a/api/src/main/java/com/codeforcommunity/dto/school/School.java b/api/src/main/java/com/codeforcommunity/dto/school/School.java index 67b2f747..45e1a135 100644 --- a/api/src/main/java/com/codeforcommunity/dto/school/School.java +++ b/api/src/main/java/com/codeforcommunity/dto/school/School.java @@ -2,7 +2,6 @@ import com.codeforcommunity.enums.Country; import com.codeforcommunity.enums.LibraryStatus; -import java.util.ArrayList; import java.util.List; public class School { diff --git a/api/src/main/java/com/codeforcommunity/rest/ApiRouter.java b/api/src/main/java/com/codeforcommunity/rest/ApiRouter.java index 4da8321a..e9c10ecc 100644 --- a/api/src/main/java/com/codeforcommunity/rest/ApiRouter.java +++ b/api/src/main/java/com/codeforcommunity/rest/ApiRouter.java @@ -1,12 +1,14 @@ package com.codeforcommunity.rest; import com.codeforcommunity.api.authenticated.IProtectedCountryProcessor; +import com.codeforcommunity.api.authenticated.IProtectedDataProcessor; import com.codeforcommunity.api.authenticated.IProtectedSchoolProcessor; import com.codeforcommunity.api.authenticated.IProtectedUserProcessor; import com.codeforcommunity.api.unauthenticated.IAuthProcessor; import com.codeforcommunity.auth.JWTAuthorizer; import com.codeforcommunity.rest.subrouter.CommonRouter; import com.codeforcommunity.rest.subrouter.authenticated.ProtectedCountryRouter; +import com.codeforcommunity.rest.subrouter.authenticated.ProtectedDataRouter; import com.codeforcommunity.rest.subrouter.authenticated.ProtectedSchoolRouter; import com.codeforcommunity.rest.subrouter.authenticated.ProtectedUserRouter; import com.codeforcommunity.rest.subrouter.unauthenticated.AuthRouter; @@ -20,18 +22,21 @@ public class ApiRouter implements IRouter { private final ProtectedUserRouter protectedUserRouter; private final ProtectedCountryRouter protectedCountryRouter; private final ProtectedSchoolRouter protectedSchoolRouter; + private final ProtectedDataRouter protectedDataRouter; public ApiRouter( JWTAuthorizer jwtAuthorizer, IAuthProcessor authProcessor, IProtectedUserProcessor protectedUserProcessor, IProtectedCountryProcessor protectedCountryProcessor, - IProtectedSchoolProcessor protectedSchoolProcessor) { + IProtectedSchoolProcessor protectedSchoolProcessor, + IProtectedDataProcessor protectedDataProcessor) { this.commonRouter = new CommonRouter(jwtAuthorizer); this.authRouter = new AuthRouter(authProcessor); this.protectedUserRouter = new ProtectedUserRouter(protectedUserProcessor); this.protectedCountryRouter = new ProtectedCountryRouter(protectedCountryProcessor); this.protectedSchoolRouter = new ProtectedSchoolRouter(protectedSchoolProcessor); + this.protectedDataRouter = new ProtectedDataRouter(protectedDataProcessor); } /** Initialize a router and register all route handlers on it. */ @@ -57,6 +62,7 @@ private Router defineProtectedRoutes(Vertx vertx) { protectedSubRouter.mountSubRouter("/user", protectedUserRouter.initializeRouter(vertx)); protectedSubRouter.mountSubRouter("/countries", protectedCountryRouter.initializeRouter(vertx)); protectedSubRouter.mountSubRouter("/schools", protectedSchoolRouter.initializeRouter(vertx)); + protectedSubRouter.mountSubRouter("/data", protectedDataRouter.initializeRouter(vertx)); return protectedSubRouter; } diff --git a/api/src/main/java/com/codeforcommunity/rest/FailureHandler.java b/api/src/main/java/com/codeforcommunity/rest/FailureHandler.java index ac121198..cbf67c6a 100644 --- a/api/src/main/java/com/codeforcommunity/rest/FailureHandler.java +++ b/api/src/main/java/com/codeforcommunity/rest/FailureHandler.java @@ -48,12 +48,12 @@ public void handleMissingParameter(RoutingContext ctx, MissingParameterException } public void handleNoReportFound(RoutingContext ctx, NoReportFoundException e) { - String message = String.format("Report not found for school with id %d", e.getSchoolId()); + String message = String.format("Report not found for school with ID %d", e.getSchoolId()); end(ctx, message, 404); } public void handleNoReportByIdFound(RoutingContext ctx, NoReportByIdFoundException e) { - String message = String.format("Report not found for report with id %d", e.getReportId()); + String message = String.format("Report not found for report with ID %d", e.getReportId()); end(ctx, message, 404); } @@ -64,7 +64,8 @@ public void handleAdminOnlyRoute(RoutingContext ctx) { public void handleEmailAlreadyInUse(RoutingContext ctx, EmailAlreadyInUseException exception) { String message = - String.format("Error creating new user, given email %s already used", exception.getEmail()); + String.format( + "Error creating new user, given email '%s' already used", exception.getEmail()); end(ctx, message, 409); } @@ -72,7 +73,7 @@ public void handleEmailAlreadyInUse(RoutingContext ctx, EmailAlreadyInUseExcepti public void handleSchoolAlreadyExists(RoutingContext ctx, SchoolAlreadyExistsException e) { String message = String.format( - "School '%s' already exists in '%s'", e.getSchoolName(), e.getSchoolCountry()); + "School '%s' already exists in country '%s'", e.getSchoolName(), e.getSchoolCountry()); end(ctx, message, 409); } @@ -175,7 +176,7 @@ public void handleMalformedParameter(RoutingContext ctx, MalformedParameterExcep } public void handleUnknownCountry(RoutingContext ctx, UnknownCountryException exception) { - String message = String.format("Unknown country given: %s", exception.getUnknownCountry()); + String message = String.format("No country found with name %s", exception.getUnknownCountry()); end(ctx, message, 400); } @@ -183,7 +184,7 @@ public void handleUnknownSchoolContact( RoutingContext ctx, SchoolContactDoesNotExistException exception) { String message = String.format( - "Unknown contact with id '%d' given for school with id '%d'", + "Unknown contact with ID %d given for school with ID %d", exception.getContactId(), exception.getSchoolId()); end(ctx, message, 400); } @@ -194,23 +195,24 @@ public void handleBadImageRequest(RoutingContext ctx) { } public void handleS3FailedUpload(RoutingContext ctx, String exceptionMessage) { - String message = "The given file could not be uploaded to AWS S3: " + exceptionMessage; + String message = + "The given file could not be uploaded to AWS S3 with error: " + exceptionMessage; end(ctx, message, 502); } public void handleSchoolDoesNotExist(RoutingContext ctx, SchoolDoesNotExistException e) { - String message = String.format("No school found with given id: %d", e.getSchoolId()); + String message = String.format("No school found with ID %d", e.getSchoolId()); end(ctx, message, 400); } public void handleBookLogDoesNotExist(RoutingContext ctx, BookLogDoesNotExistException e) { - String message = String.format("No book log found with given id: %d", e.getBookId()); + String message = String.format("No book log found with ID %d", e.getBookId()); end(ctx, message, 400); } public void handleCsvSerializer(RoutingContext ctx, CsvSerializerException e) { String message = - String.format("Report with id: %d was unable to be converted to CSV", e.getReportId()); + String.format("Report with ID %d was unable to be converted to CSV", e.getReportId()); end(ctx, message, 500); } diff --git a/api/src/main/java/com/codeforcommunity/rest/subrouter/authenticated/ProtectedDataRouter.java b/api/src/main/java/com/codeforcommunity/rest/subrouter/authenticated/ProtectedDataRouter.java new file mode 100644 index 00000000..123a34a8 --- /dev/null +++ b/api/src/main/java/com/codeforcommunity/rest/subrouter/authenticated/ProtectedDataRouter.java @@ -0,0 +1,78 @@ +package com.codeforcommunity.rest.subrouter.authenticated; + +import static com.codeforcommunity.rest.ApiRouter.end; + +import com.codeforcommunity.api.authenticated.IProtectedDataProcessor; +import com.codeforcommunity.auth.JWTData; +import com.codeforcommunity.dto.data.MetricsCountryResponse; +import com.codeforcommunity.dto.data.MetricsSchoolResponse; +import com.codeforcommunity.dto.data.MetricsTotalResponse; +import com.codeforcommunity.enums.Country; +import com.codeforcommunity.rest.IRouter; +import com.codeforcommunity.rest.RestFunctions; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Route; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; + +public class ProtectedDataRouter implements IRouter { + + private final IProtectedDataProcessor processor; + + public ProtectedDataRouter(IProtectedDataProcessor processor) { + this.processor = processor; + } + + @Override + public Router initializeRouter(Vertx vertx) { + Router router = Router.router(vertx); + + registerGetFixedTotalMetrics(router); + registerGetFixedCountryMetrics(router); + registerGetFixedSchoolMetrics(router); + + return router; + } + + private void registerGetFixedTotalMetrics(Router router) { + Route getTotalMetricRoute = router.get("/total"); + getTotalMetricRoute.handler(this::handleGetFixedTotalMetrics); + } + + private void registerGetFixedCountryMetrics(Router router) { + Route getCountryMetricsRoute = router.get("/country/:country"); + getCountryMetricsRoute.handler(this::handleGetFixedCountryMetrics); + } + + private void registerGetFixedSchoolMetrics(Router router) { + Route getSchoolMetricsRoute = router.get("/school/:school_id"); + getSchoolMetricsRoute.handler(this::handleGetFixedSchoolMetrics); + } + + private void handleGetFixedTotalMetrics(RoutingContext ctx) { + JWTData userData = ctx.get("jwt_data"); + + MetricsTotalResponse response = processor.getFixedTotalMetrics(userData); + + end(ctx.response(), 200, JsonObject.mapFrom(response).toString()); + } + + private void handleGetFixedCountryMetrics(RoutingContext ctx) { + JWTData userData = ctx.get("jwt_data"); + + Country country = RestFunctions.getCountryFromString(ctx.pathParam("country")); + MetricsCountryResponse response = processor.getFixedCountryMetrics(userData, country); + + end(ctx.response(), 200, JsonObject.mapFrom(response).toString()); + } + + private void handleGetFixedSchoolMetrics(RoutingContext ctx) { + JWTData userData = ctx.get("jwt_data"); + + int schoolId = RestFunctions.getPathParamAsInt(ctx, "school_id"); + MetricsSchoolResponse response = processor.getFixedSchoolMetrics(userData, schoolId); + + end(ctx.response(), 200, JsonObject.mapFrom(response).toString()); + } +} diff --git a/service/src/main/java/com/codeforcommunity/ServiceMain.java b/service/src/main/java/com/codeforcommunity/ServiceMain.java index c917679c..acf094e2 100644 --- a/service/src/main/java/com/codeforcommunity/ServiceMain.java +++ b/service/src/main/java/com/codeforcommunity/ServiceMain.java @@ -1,6 +1,7 @@ package com.codeforcommunity; import com.codeforcommunity.api.authenticated.IProtectedCountryProcessor; +import com.codeforcommunity.api.authenticated.IProtectedDataProcessor; import com.codeforcommunity.api.authenticated.IProtectedSchoolProcessor; import com.codeforcommunity.api.authenticated.IProtectedUserProcessor; import com.codeforcommunity.api.unauthenticated.IAuthProcessor; @@ -9,6 +10,7 @@ import com.codeforcommunity.auth.JWTHandler; import com.codeforcommunity.logger.SLogger; import com.codeforcommunity.processor.authenticated.ProtectedCountryProcessorImpl; +import com.codeforcommunity.processor.authenticated.ProtectedDataProcessorImpl; import com.codeforcommunity.processor.authenticated.ProtectedSchoolProcessorImpl; import com.codeforcommunity.processor.authenticated.ProtectedUserProcessorImpl; import com.codeforcommunity.processor.unauthenticated.AuthProcessorImpl; @@ -89,11 +91,17 @@ private void initializeServer() { IProtectedUserProcessor protectedUserProc = new ProtectedUserProcessorImpl(this.db, emailer); IProtectedCountryProcessor protectedCountryProc = new ProtectedCountryProcessorImpl(this.db); IProtectedSchoolProcessor protectedSchoolProc = new ProtectedSchoolProcessorImpl(this.db); + IProtectedDataProcessor protectedDataProc = new ProtectedDataProcessorImpl(this.db); // Create the API router and start the HTTP server ApiRouter router = new ApiRouter( - jwtAuthorizer, authProc, protectedUserProc, protectedCountryProc, protectedSchoolProc); + jwtAuthorizer, + authProc, + protectedUserProc, + protectedCountryProc, + protectedSchoolProc, + protectedDataProc); startApiServer(router, vertx); } diff --git a/service/src/main/java/com/codeforcommunity/dataaccess/SchoolDatabaseOperations.java b/service/src/main/java/com/codeforcommunity/dataaccess/SchoolDatabaseOperations.java new file mode 100644 index 00000000..04cb4629 --- /dev/null +++ b/service/src/main/java/com/codeforcommunity/dataaccess/SchoolDatabaseOperations.java @@ -0,0 +1,69 @@ +package com.codeforcommunity.dataaccess; + +import static org.jooq.generated.Tables.SCHOOLS; +import static org.jooq.generated.Tables.SCHOOL_CONTACTS; +import static org.jooq.generated.Tables.SCHOOL_REPORTS_WITHOUT_LIBRARIES; +import static org.jooq.generated.Tables.SCHOOL_REPORTS_WITH_LIBRARIES; + +import com.codeforcommunity.dto.report.ReportGeneric; +import com.codeforcommunity.dto.report.ReportWithLibrary; +import com.codeforcommunity.dto.report.ReportWithoutLibrary; +import com.codeforcommunity.dto.school.SchoolContact; +import com.codeforcommunity.enums.LibraryStatus; +import java.util.List; +import org.jooq.DSLContext; +import org.jooq.generated.tables.records.SchoolsRecord; + +public class SchoolDatabaseOperations { + + private final DSLContext db; + + public SchoolDatabaseOperations(DSLContext db) { + this.db = db; + } + + public SchoolsRecord getSchool(int schoolId) { + return db.selectFrom(SCHOOLS) + .where(SCHOOLS.ID.eq(schoolId)) + .and(SCHOOLS.DELETED_AT.isNull()) + .fetchOne(); + } + + public List getSchoolContacts(int schoolId) { + return db.selectFrom(SCHOOL_CONTACTS) + .where(SCHOOL_CONTACTS.DELETED_AT.isNull()) + .and(SCHOOL_CONTACTS.SCHOOL_ID.eq(schoolId)) + .fetchInto(SchoolContact.class); + } + + public ReportGeneric getMostRecentReport(int schoolId) throws IllegalArgumentException { + SchoolsRecord school = this.getSchool(schoolId); + if (school == null) { + throw new IllegalArgumentException( + String.format("School with ID %d does not exist", schoolId)); + } + + ReportGeneric report = null; + LibraryStatus libraryStatus = school.getLibraryStatus(); + + if (libraryStatus == LibraryStatus.EXISTS) { + report = + db.selectFrom(SCHOOL_REPORTS_WITH_LIBRARIES) + .where(SCHOOL_REPORTS_WITH_LIBRARIES.DELETED_AT.isNull()) + .and(SCHOOL_REPORTS_WITH_LIBRARIES.SCHOOL_ID.eq(schoolId)) + .orderBy(SCHOOL_REPORTS_WITH_LIBRARIES.ID.desc()) + .limit(1) + .fetchOneInto(ReportWithLibrary.class); + } else if (libraryStatus == LibraryStatus.DOES_NOT_EXIST) { + report = + db.selectFrom(SCHOOL_REPORTS_WITHOUT_LIBRARIES) + .where(SCHOOL_REPORTS_WITHOUT_LIBRARIES.DELETED_AT.isNull()) + .and(SCHOOL_REPORTS_WITHOUT_LIBRARIES.SCHOOL_ID.eq(schoolId)) + .orderBy(SCHOOL_REPORTS_WITHOUT_LIBRARIES.ID.desc()) + .limit(1) + .fetchOneInto(ReportWithoutLibrary.class); + } + + return report; + } +} diff --git a/service/src/main/java/com/codeforcommunity/processor/authenticated/ProtectedDataProcessorImpl.java b/service/src/main/java/com/codeforcommunity/processor/authenticated/ProtectedDataProcessorImpl.java new file mode 100644 index 00000000..2a21a49e --- /dev/null +++ b/service/src/main/java/com/codeforcommunity/processor/authenticated/ProtectedDataProcessorImpl.java @@ -0,0 +1,257 @@ +package com.codeforcommunity.processor.authenticated; + +import static org.jooq.generated.Tables.SCHOOLS; +import static org.jooq.generated.Tables.USERS; + +import com.codeforcommunity.api.authenticated.IProtectedDataProcessor; +import com.codeforcommunity.auth.JWTData; +import com.codeforcommunity.dataaccess.SchoolDatabaseOperations; +import com.codeforcommunity.dto.data.MetricGeneric; +import com.codeforcommunity.dto.data.MetricsCountryResponse; +import com.codeforcommunity.dto.data.MetricsSchoolResponse; +import com.codeforcommunity.dto.data.MetricsTotalResponse; +import com.codeforcommunity.dto.report.ReportGeneric; +import com.codeforcommunity.dto.report.ReportWithLibrary; +import com.codeforcommunity.dto.school.SchoolSummary; +import com.codeforcommunity.enums.Country; +import com.codeforcommunity.enums.LibraryStatus; +import com.codeforcommunity.enums.PrivilegeLevel; +import com.codeforcommunity.exceptions.SchoolDoesNotExistException; +import com.codeforcommunity.logger.SLogger; +import java.util.ArrayList; +import java.util.List; +import org.jooq.DSLContext; + +public class ProtectedDataProcessorImpl implements IProtectedDataProcessor { + + private final SLogger logger = new SLogger(ProtectedDataProcessorImpl.class); + private final SchoolDatabaseOperations schoolDatabaseOperations; + private final DSLContext db; + + public ProtectedDataProcessorImpl(DSLContext db) { + this.schoolDatabaseOperations = new SchoolDatabaseOperations(db); + this.db = db; + } + + @Override + public MetricsTotalResponse getFixedTotalMetrics(JWTData userData) { + int countSchools = + db.fetchCount( + db.selectFrom(SCHOOLS) + .where(SCHOOLS.HIDDEN.isFalse()) + .and(SCHOOLS.DELETED_AT.isNull())); + + List schoolIds = + db.selectFrom(SCHOOLS) + .where(SCHOOLS.HIDDEN.isFalse()) + .and(SCHOOLS.DELETED_AT.isNull()) + .fetch(SCHOOLS.ID); + + MetricGeneric metricGeneric = getGenericMetrics(schoolIds); + + return new MetricsTotalResponse(countSchools, metricGeneric.getTotalBooks(), + metricGeneric.getTotalStudents()); + } + + @Override + public MetricsCountryResponse getFixedCountryMetrics(JWTData userData, Country country) { + int countSchools = + db.fetchCount( + db.selectFrom(SCHOOLS) + .where(SCHOOLS.HIDDEN.isFalse()) + .and(SCHOOLS.DELETED_AT.isNull()) + .and(SCHOOLS.COUNTRY.eq(country))); + + int countVolunteerAccounts = + db.fetchCount( + db.selectFrom(USERS) + .where(USERS.DELETED_AT.isNull()) + .and(USERS.COUNTRY.eq(country)) + .and(USERS.PRIVILEGE_LEVEL.eq(PrivilegeLevel.STANDARD))); + + int countAdminAccounts = + db.fetchCount( + db.selectFrom(USERS) + .where(USERS.DELETED_AT.isNull()) + .and(USERS.COUNTRY.eq(country)) + .and(USERS.PRIVILEGE_LEVEL.eq(PrivilegeLevel.ADMIN))); + + List schoolReports = this.getCountryReports(country); + + List schoolIds = + db.selectFrom(SCHOOLS) + .where(SCHOOLS.HIDDEN.isFalse()) + .and(SCHOOLS.DELETED_AT.isNull()) + .and(SCHOOLS.COUNTRY.eq(country)) + .fetch(SCHOOLS.ID); + + Float avgCountBooksPerStudent = this.getCountryBooksPerStudentAverage(country, schoolReports); + Float avgCountStudentLibrariansPerSchool = + this.getCountryStudentLibrariansPerSchoolAverage(country, schoolReports); + + int countSchoolsWithLibrary = + db.fetchCount( + db.selectFrom(SCHOOLS) + .where(SCHOOLS.HIDDEN.isFalse()) + .and(SCHOOLS.DELETED_AT.isNull()) + .and(SCHOOLS.COUNTRY.eq(country)) + .and(SCHOOLS.LIBRARY_STATUS.eq(LibraryStatus.EXISTS))); + + float percentSchoolsWithLibraries = + (countSchools > 0) ? ((float) countSchoolsWithLibrary / (float) countSchools) * 100 : 0; + + MetricGeneric metricGeneric = getGenericMetrics(schoolIds); + + return new MetricsCountryResponse( + countSchools, + countVolunteerAccounts, + countAdminAccounts, + avgCountBooksPerStudent, + avgCountStudentLibrariansPerSchool, + percentSchoolsWithLibraries, + metricGeneric.getTotalStudents(), + metricGeneric.getTotalBooks()); + } + + @Override + public MetricsSchoolResponse getFixedSchoolMetrics(JWTData userData, int schoolId) { + ReportGeneric report; + + try { + report = schoolDatabaseOperations.getMostRecentReport(schoolId); + } catch (IllegalArgumentException e) { + throw new SchoolDoesNotExistException(schoolId); + } + + Integer countBooks = report.getNumberOfBooks(); + Integer countStudents = report.getNumberOfChildren(); + + Float countBooksPerStudent = + (countBooks != null && countStudents != null) + ? ((float) countBooks / (float) countStudents) + : null; + + Integer countStudentLibrarians = + (report instanceof ReportWithLibrary) + ? ((ReportWithLibrary) report).getNumberOfStudentLibrarians() + : null; + + Integer netBooksInOut = null; // TODO + + return new MetricsSchoolResponse( + countBooksPerStudent, countStudents, countStudentLibrarians, netBooksInOut, countBooks); + } + + private List getCountryReports(Country country) { + // Get all schools for this country that are not deleted or hidden + List schoolIds = + db.selectFrom(SCHOOLS) + .where(SCHOOLS.DELETED_AT.isNull()) + .and(SCHOOLS.HIDDEN.isFalse()) + .and(SCHOOLS.COUNTRY.eq(country)) + .fetch(SCHOOLS.ID); + + return getReports(schoolIds); + } + + private List getReports(List schoolIds) { + List reports = new ArrayList(); + + for (int schoolId : schoolIds) { + // For each school, get the most recent report + ReportGeneric report = schoolDatabaseOperations.getMostRecentReport(schoolId); + + if (report == null) { + logger.info( + String.format( + "No report found for school with ID `%d`", + schoolId)); + continue; + } + + reports.add(report); + } + + return reports; + } + + private Float getCountryBooksPerStudentAverage( + Country country, List schoolReports) { + List schoolAveragesBooksPerStudent = new ArrayList(); + + for (ReportGeneric report : schoolReports) { + // For each report, calculate books per student + Integer countBooks = report.getNumberOfBooks(); + Integer countStudents = report.getNumberOfChildren(); + + if (countBooks == null || countStudents == null) { + logger.info( + String.format( + "School report with ID `%d` missing count books or count students", + report.getId())); + continue; + } + + float schoolAvg = ((float) countBooks) / ((float) countStudents); + schoolAveragesBooksPerStudent.add(schoolAvg); + } + + if (schoolAveragesBooksPerStudent.isEmpty()) { + return null; + } + + return (float) schoolAveragesBooksPerStudent.stream().mapToDouble(d -> d).average().orElse(0.0); + } + + // gets total books and students from a list of schools + private MetricGeneric getGenericMetrics(List schoolIds) { + Integer totalBooks = 0; + Integer totalStudents = 0; + + for (Integer schoolId : schoolIds) { + totalBooks += schoolDatabaseOperations.getMostRecentReport(schoolId).getNumberOfBooks(); + totalStudents += schoolDatabaseOperations.getMostRecentReport(schoolId).getNumberOfChildren(); + } + return new MetricGeneric(totalBooks, totalStudents); + } + + private Float getCountryStudentLibrariansPerSchoolAverage( + Country country, List schoolReports) { + int totalCountStudentLibrarians = 0; + int totalCountSchoolsWithLibraries = 0; // TODO: SHOULD THIS BE ALL SCHOOLS + + for (ReportGeneric report : schoolReports) { + // For each report, get count student librarians + if (report.getLibraryStatus() != LibraryStatus.EXISTS + || !(report instanceof ReportWithLibrary)) { + // Skip reports with no libraries + + logger.info( + String.format( + "Skipping school report with ID `%d` since it has no library", report.getId())); + continue; + } + + ReportWithLibrary reportWithLibrary = (ReportWithLibrary) report; + + Integer numStudentLibrarians = reportWithLibrary.getNumberOfStudentLibrarians(); + if (numStudentLibrarians == null) { + logger.info( + String.format( + "Skipping school report with ID `%d` since it has a `null` student librarian count", + report.getId())); + continue; + } + + // Otherwise, increment count of schools and add the number of librarians + totalCountStudentLibrarians += numStudentLibrarians; + totalCountSchoolsWithLibraries++; + } + + if (totalCountSchoolsWithLibraries == 0) { + return null; + } + + return (float) totalCountStudentLibrarians / (float) totalCountSchoolsWithLibraries; + } +} diff --git a/service/src/main/java/com/codeforcommunity/processor/authenticated/ProtectedSchoolProcessorImpl.java b/service/src/main/java/com/codeforcommunity/processor/authenticated/ProtectedSchoolProcessorImpl.java index d14d9db2..05299d00 100644 --- a/service/src/main/java/com/codeforcommunity/processor/authenticated/ProtectedSchoolProcessorImpl.java +++ b/service/src/main/java/com/codeforcommunity/processor/authenticated/ProtectedSchoolProcessorImpl.java @@ -8,6 +8,7 @@ import com.codeforcommunity.api.authenticated.IProtectedSchoolProcessor; import com.codeforcommunity.auth.JWTData; +import com.codeforcommunity.dataaccess.SchoolDatabaseOperations; import com.codeforcommunity.dto.CsvSerializer; import com.codeforcommunity.dto.report.ReportGeneric; import com.codeforcommunity.dto.report.ReportGenericListResponse; @@ -54,9 +55,11 @@ public class ProtectedSchoolProcessorImpl implements IProtectedSchoolProcessor { private final SLogger logger = new SLogger(ProtectedSchoolProcessorImpl.class); + private final SchoolDatabaseOperations schoolDatabaseOperations; private final DSLContext db; public ProtectedSchoolProcessorImpl(DSLContext db) { + this.schoolDatabaseOperations = new SchoolDatabaseOperations(db); this.db = db; } @@ -149,7 +152,7 @@ public School createSchool(JWTData userData, UpsertSchoolRequest upsertSchoolReq school.store(); Integer schoolId = school.getId(); - List contacts = this.queryForSchoolContacts(schoolId); + List contacts = schoolDatabaseOperations.getSchoolContacts(schoolId); return new School( school.getId(), @@ -170,7 +173,7 @@ public School createSchool(JWTData userData, UpsertSchoolRequest upsertSchoolReq @Override public SchoolContactListResponse getAllSchoolContacts(JWTData userData, int schoolId) { - SchoolsRecord school = this.queryForSchool(schoolId); + SchoolsRecord school = schoolDatabaseOperations.getSchool(schoolId); if (school == null) { throw new SchoolDoesNotExistException(schoolId); } @@ -186,7 +189,7 @@ public SchoolContactListResponse getAllSchoolContacts(JWTData userData, int scho @Override public SchoolContact getSchoolContact(JWTData userData, int schoolId, int contactId) { - SchoolsRecord school = this.queryForSchool(schoolId); + SchoolsRecord school = schoolDatabaseOperations.getSchool(schoolId); if (school == null) { throw new SchoolDoesNotExistException(schoolId); } @@ -208,7 +211,7 @@ public SchoolContact getSchoolContact(JWTData userData, int schoolId, int contac @Override public SchoolContact createSchoolContact( JWTData userData, int schoolId, UpsertSchoolContactRequest upsertSchoolContactRequest) { - SchoolsRecord school = this.queryForSchool(schoolId); + SchoolsRecord school = schoolDatabaseOperations.getSchool(schoolId); if (school == null) { throw new SchoolDoesNotExistException(schoolId); } @@ -326,7 +329,7 @@ public void deleteSchoolContact(JWTData userData, int schoolId, int contactId) { @Override public void updateSchool( JWTData userData, int schoolId, UpsertSchoolRequest upsertSchoolRequest) { - SchoolsRecord school = this.queryForSchool(schoolId); + SchoolsRecord school = schoolDatabaseOperations.getSchool(schoolId); if (school == null) { throw new SchoolDoesNotExistException(schoolId); } @@ -359,7 +362,7 @@ public void deleteSchool(JWTData userData, int schoolId) { throw new AdminOnlyRouteException(); } - SchoolsRecord school = this.queryForSchool(schoolId); + SchoolsRecord school = schoolDatabaseOperations.getSchool(schoolId); if (school == null) { throw new SchoolDoesNotExistException(schoolId); } @@ -370,7 +373,7 @@ public void deleteSchool(JWTData userData, int schoolId) { @Override public void hideSchool(JWTData userData, int schoolId) { - SchoolsRecord school = this.queryForSchool(schoolId); + SchoolsRecord school = schoolDatabaseOperations.getSchool(schoolId); if (school == null) { throw new SchoolDoesNotExistException(schoolId); } @@ -380,7 +383,7 @@ public void hideSchool(JWTData userData, int schoolId) { @Override public void unHideSchool(JWTData userData, int schoolId) { - SchoolsRecord school = this.queryForSchool(schoolId); + SchoolsRecord school = schoolDatabaseOperations.getSchool(schoolId); if (school == null) { throw new SchoolDoesNotExistException(schoolId); } @@ -392,7 +395,7 @@ public void unHideSchool(JWTData userData, int schoolId) { @Override public ReportWithLibrary createReportWithLibrary( JWTData userData, int schoolId, UpsertReportWithLibrary req) { - SchoolsRecord school = this.queryForSchool(schoolId); + SchoolsRecord school = schoolDatabaseOperations.getSchool(schoolId); if (school == null) { throw new SchoolDoesNotExistException(schoolId); } @@ -459,7 +462,7 @@ public ReportWithLibrary createReportWithLibrary( @Override public void updateReportWithLibrary( JWTData userData, int schoolId, int reportId, UpsertReportWithLibrary req) { - SchoolsRecord school = this.queryForSchool(schoolId); + SchoolsRecord school = schoolDatabaseOperations.getSchool(schoolId); if (school == null) { throw new SchoolDoesNotExistException(schoolId); } @@ -505,30 +508,12 @@ public void updateReportWithLibrary( @Override public ReportGeneric getMostRecentReport(JWTData userData, int schoolId) { - SchoolsRecord school = this.queryForSchool(schoolId); - if (school == null) { - throw new SchoolDoesNotExistException(schoolId); - } - - ReportGeneric report = null; - LibraryStatus libraryStatus = school.getLibraryStatus(); + ReportGeneric report; - if (libraryStatus == LibraryStatus.EXISTS) { - report = - db.selectFrom(SCHOOL_REPORTS_WITH_LIBRARIES) - .where(SCHOOL_REPORTS_WITH_LIBRARIES.DELETED_AT.isNull()) - .and(SCHOOL_REPORTS_WITH_LIBRARIES.SCHOOL_ID.eq(schoolId)) - .orderBy(SCHOOL_REPORTS_WITH_LIBRARIES.ID.desc()) - .limit(1) - .fetchOneInto(ReportWithLibrary.class); - } else if (libraryStatus == LibraryStatus.DOES_NOT_EXIST) { - report = - db.selectFrom(SCHOOL_REPORTS_WITHOUT_LIBRARIES) - .where(SCHOOL_REPORTS_WITHOUT_LIBRARIES.DELETED_AT.isNull()) - .and(SCHOOL_REPORTS_WITHOUT_LIBRARIES.SCHOOL_ID.eq(schoolId)) - .orderBy(SCHOOL_REPORTS_WITHOUT_LIBRARIES.ID.desc()) - .limit(1) - .fetchOneInto(ReportWithoutLibrary.class); + try { + report = schoolDatabaseOperations.getMostRecentReport(schoolId); + } catch (IllegalArgumentException e) { + throw new SchoolDoesNotExistException(schoolId); } if (report == null) { @@ -542,7 +527,7 @@ public ReportGeneric getMostRecentReport(JWTData userData, int schoolId) { @Override public ReportWithoutLibrary createReportWithoutLibrary( JWTData userData, int schoolId, UpsertReportWithoutLibrary req) { - SchoolsRecord school = this.queryForSchool(schoolId); + SchoolsRecord school = schoolDatabaseOperations.getSchool(schoolId); if (school == null) { throw new SchoolDoesNotExistException(schoolId); } @@ -592,7 +577,7 @@ public ReportWithoutLibrary createReportWithoutLibrary( public void updateReportWithoutLibrary( JWTData userData, int schoolId, int reportId, UpsertReportWithoutLibrary req) { - SchoolsRecord school = this.queryForSchool(schoolId); + SchoolsRecord school = schoolDatabaseOperations.getSchool(schoolId); if (school == null) { throw new SchoolDoesNotExistException(schoolId); } @@ -634,7 +619,7 @@ public ReportGenericListResponse getPaginatedReports(JWTData userData, int schoo throw new MalformedParameterException("p"); } - SchoolsRecord school = this.queryForSchool(schoolId); + SchoolsRecord school = schoolDatabaseOperations.getSchool(schoolId); if (school == null) { throw new SchoolDoesNotExistException(schoolId); } @@ -684,7 +669,7 @@ public BookLog createBookLog(JWTData userData, int schoolId, UpsertBookLogReques throw new AdminOnlyRouteException(); } - SchoolsRecord school = this.queryForSchool(schoolId); + SchoolsRecord school = schoolDatabaseOperations.getSchool(schoolId); if (school == null) { throw new SchoolDoesNotExistException(schoolId); } @@ -709,7 +694,7 @@ public BookLog updateBookLog( if (!userData.isAdmin()) { throw new AdminOnlyRouteException(); } - SchoolsRecord school = this.queryForSchool(schoolId); + SchoolsRecord school = schoolDatabaseOperations.getSchool(schoolId); if (school == null) { throw new SchoolDoesNotExistException(schoolId); } @@ -741,7 +726,7 @@ public void deleteBookLog(JWTData userData, int schoolId, int bookId) { if (!userData.isAdmin()) { throw new AdminOnlyRouteException(); } - SchoolsRecord school = this.queryForSchool(schoolId); + SchoolsRecord school = schoolDatabaseOperations.getSchool(schoolId); if (school == null) { throw new SchoolDoesNotExistException(schoolId); } @@ -758,7 +743,7 @@ public void deleteBookLog(JWTData userData, int schoolId, int bookId) { @Override public BookLogListResponse getBookLog(JWTData userData, int schoolId) { - SchoolsRecord school = this.queryForSchool(schoolId); + SchoolsRecord school = schoolDatabaseOperations.getSchool(schoolId); if (school == null) { throw new SchoolDoesNotExistException(schoolId); } @@ -797,18 +782,4 @@ public String getReportAsCsv(JWTData userData, int reportId, boolean hasLibrary) return builder.toString(); } - - private SchoolsRecord queryForSchool(int schoolId) { - return db.selectFrom(SCHOOLS) - .where(SCHOOLS.ID.eq(schoolId)) - .and(SCHOOLS.DELETED_AT.isNull()) - .fetchOne(); - } - - private List queryForSchoolContacts(int schoolId) { - return db.selectFrom(SCHOOL_CONTACTS) - .where(SCHOOL_CONTACTS.DELETED_AT.isNull()) - .and(SCHOOL_CONTACTS.SCHOOL_ID.eq(schoolId)) - .fetchInto(SchoolContact.class); - } }