Added user submission improvements.

This commit is contained in:
Andrew Lalis 2023-02-16 14:48:01 +01:00
parent 0663296052
commit 6b7b38595e
11 changed files with 164 additions and 52 deletions

View File

@ -0,0 +1,23 @@
package nl.andrewlalis.gymboard_api.domains.api.controller;
import nl.andrewlalis.gymboard_api.domains.api.dto.SubmissionResponse;
import nl.andrewlalis.gymboard_api.domains.api.service.submission.UserSubmissionService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class UserSubmissionsController {
private final UserSubmissionService submissionService;
public UserSubmissionsController(UserSubmissionService submissionService) {
this.submissionService = submissionService;
}
@GetMapping(path = "/users/{userId}/recent-submissions")
public List<SubmissionResponse> getRecentSubmissions(@PathVariable String userId) {
return submissionService.getRecentSubmissions(userId);
}
}

View File

@ -15,7 +15,8 @@ public record SubmissionResponse(
double rawWeight, double rawWeight,
String weightUnit, String weightUnit,
double metricWeight, double metricWeight,
int reps int reps,
boolean verified
) { ) {
public SubmissionResponse(Submission submission) { public SubmissionResponse(Submission submission) {
this( this(
@ -29,7 +30,8 @@ public record SubmissionResponse(
submission.getRawWeight().doubleValue(), submission.getRawWeight().doubleValue(),
submission.getWeightUnit().name(), submission.getWeightUnit().name(),
submission.getMetricWeight().doubleValue(), submission.getMetricWeight().doubleValue(),
submission.getReps() submission.getReps(),
submission.isVerified()
); );
} }
} }

View File

@ -19,4 +19,9 @@ public enum WeightUnit {
BigDecimal metric = new BigDecimal("0.45359237"); BigDecimal metric = new BigDecimal("0.45359237");
return metric.multiply(pounds); return metric.multiply(pounds);
} }
public static BigDecimal toKilograms(BigDecimal weight, WeightUnit unit) {
if (unit == POUNDS) return toKilograms(weight);
return weight;
}
} }

View File

@ -53,9 +53,23 @@ public class Submission {
@Column(nullable = false) @Column(nullable = false)
private int reps; private int reps;
@Column(nullable = false)
private boolean verified;
public Submission() {} public Submission() {}
public Submission(String id, Gym gym, Exercise exercise, User user, LocalDateTime performedAt, String videoFileId, BigDecimal rawWeight, WeightUnit unit, BigDecimal metricWeight, int reps) { public Submission(
String id,
Gym gym,
Exercise exercise,
User user,
LocalDateTime performedAt,
String videoFileId,
BigDecimal rawWeight,
WeightUnit unit,
BigDecimal metricWeight,
int reps
) {
this.id = id; this.id = id;
this.gym = gym; this.gym = gym;
this.exercise = exercise; this.exercise = exercise;
@ -66,6 +80,7 @@ public class Submission {
this.weightUnit = unit; this.weightUnit = unit;
this.metricWeight = metricWeight; this.metricWeight = metricWeight;
this.reps = reps; this.reps = reps;
this.verified = false;
} }
public String getId() { public String getId() {
@ -111,4 +126,12 @@ public class Submission {
public int getReps() { public int getReps() {
return reps; return reps;
} }
public boolean isVerified() {
return verified;
}
public void setVerified(boolean verified) {
this.verified = verified;
}
} }

View File

@ -41,14 +41,16 @@ public class GymService {
Gym gym = gymRepository.findByCompoundId(id) Gym gym = gymRepository.findByCompoundId(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
return submissionRepository.findAll((root, query, criteriaBuilder) -> { return submissionRepository.findAll((root, query, criteriaBuilder) -> {
query.orderBy(criteriaBuilder.desc(root.get("createdAt"))); query.orderBy(
criteriaBuilder.desc(root.get("performedAt")),
criteriaBuilder.desc(root.get("createdAt"))
);
query.distinct(true); query.distinct(true);
// TODO: Filter to only verified submissions.
return PredicateBuilder.and(criteriaBuilder) return PredicateBuilder.and(criteriaBuilder)
.with(criteriaBuilder.equal(root.get("gym"), gym)) .with(criteriaBuilder.equal(root.get("gym"), gym))
.with(criteriaBuilder.isTrue(root.get("verified")))
.build(); .build();
}, PageRequest.of(0, 10)) }, PageRequest.of(0, 5))
.map(SubmissionResponse::new) .map(SubmissionResponse::new)
.toList(); .toList();
} }

View File

@ -55,9 +55,10 @@ public class LeaderboardService {
query.distinct(true); query.distinct(true);
query.orderBy(criteriaBuilder.desc(root.get("metricWeight"))); query.orderBy(criteriaBuilder.desc(root.get("metricWeight")));
PredicateBuilder pb = PredicateBuilder.and(criteriaBuilder); PredicateBuilder pb = PredicateBuilder.and(criteriaBuilder)
.with(criteriaBuilder.isTrue(root.get("verified")));
cutoffTime.ifPresent(time -> pb.with(criteriaBuilder.greaterThan(root.get("createdAt"), time))); cutoffTime.ifPresent(time -> pb.with(criteriaBuilder.greaterThan(root.get("performedAt"), time)));
optionalExercise.ifPresent(exercise -> pb.with(criteriaBuilder.equal(root.get("exercise"), exercise))); optionalExercise.ifPresent(exercise -> pb.with(criteriaBuilder.equal(root.get("exercise"), exercise)));
if (!gyms.isEmpty()) { if (!gyms.isEmpty()) {
PredicateBuilder or = PredicateBuilder.or(criteriaBuilder); PredicateBuilder or = PredicateBuilder.or(criteriaBuilder);

View File

@ -0,0 +1,44 @@
package nl.andrewlalis.gymboard_api.domains.api.service.submission;
import nl.andrewlalis.gymboard_api.domains.api.dao.submission.SubmissionRepository;
import nl.andrewlalis.gymboard_api.domains.api.dto.SubmissionResponse;
import nl.andrewlalis.gymboard_api.domains.auth.dao.UserRepository;
import nl.andrewlalis.gymboard_api.domains.auth.model.User;
import nl.andrewlalis.gymboard_api.util.PredicateBuilder;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
@Service
public class UserSubmissionService {
private final UserRepository userRepository;
private final SubmissionRepository submissionRepository;
public UserSubmissionService(UserRepository userRepository, SubmissionRepository submissionRepository) {
this.userRepository = userRepository;
this.submissionRepository = submissionRepository;
}
@Transactional(readOnly = true)
public List<SubmissionResponse> getRecentSubmissions(String userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
if (user.getPreferences().isAccountPrivate()) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
return submissionRepository.findAll((root, query, criteriaBuilder) -> {
query.orderBy(
criteriaBuilder.desc(root.get("performedAt")),
criteriaBuilder.desc(root.get("createdAt"))
);
PredicateBuilder pb = PredicateBuilder.and(criteriaBuilder)
.with(criteriaBuilder.equal(root.get("user"), user));
return pb.build();
}, PageRequest.of(0, 5)).map(SubmissionResponse::new).toList();
}
}

View File

@ -6,14 +6,16 @@ public record UserResponse(
String id, String id,
boolean activated, boolean activated,
String email, String email,
String name String name,
boolean accountPrivate
) { ) {
public UserResponse(User user) { public UserResponse(User user) {
this( this(
user.getId(), user.getId(),
user.isActivated(), user.isActivated(),
user.getEmail(), user.getEmail(),
user.getName() user.getName(),
user.getPreferences().isAccountPrivate()
); );
} }
} }

View File

@ -63,8 +63,9 @@ public class SampleSubmissionGenerator implements SampleDataGenerator {
final LocalDateTime latestSubmission = LocalDateTime.now(); final LocalDateTime latestSubmission = LocalDateTime.now();
Random random = new Random(1); Random random = new Random(1);
List<Submission> submissions = new ArrayList<>(count);
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
generateRandomSubmission( submissions.add(generateRandomSubmission(
gyms, gyms,
users, users,
exercises, exercises,
@ -72,11 +73,12 @@ public class SampleSubmissionGenerator implements SampleDataGenerator {
earliestSubmission, earliestSubmission,
latestSubmission, latestSubmission,
random random
); ));
} }
submissionRepository.saveAll(submissions);
} }
private void generateRandomSubmission( private Submission generateRandomSubmission(
List<Gym> gyms, List<Gym> gyms,
List<User> users, List<User> users,
List<Exercise> exercises, List<Exercise> exercises,
@ -94,18 +96,20 @@ public class SampleSubmissionGenerator implements SampleDataGenerator {
rawWeight = metricWeight.multiply(new BigDecimal("2.2046226218")); rawWeight = metricWeight.multiply(new BigDecimal("2.2046226218"));
} }
submissionRepository.save(new Submission( var submission = new Submission(
ulid.nextULID(), ulid.nextULID(),
randomChoice(gyms, random), randomChoice(gyms, random),
randomChoice(exercises, random), randomChoice(exercises, random),
randomChoice(users, random), randomChoice(users, random),
time, time,
randomChoice(videoIds, random), randomChoice(videoIds, random),
rawWeight, rawWeight,
weightUnit, weightUnit,
metricWeight, metricWeight,
random.nextInt(13) random.nextInt(13) + 1
)); );
submission.setVerified(true);
return submission;
} }
@Override @Override

View File

@ -10,14 +10,13 @@
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
<q-item-section side top> <q-item-section side top>
{{ submission.createdAt.setLocale($i18n.locale).toLocaleString(DateTime.DATETIME_MED) }} {{ submission.performedAt.setLocale($i18n.locale).toLocaleString(DateTime.DATETIME_MED) }}
</q-item-section> </q-item-section>
</q-item> </q-item>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ExerciseSubmission, WeightUnitUtil } from 'src/api/main/submission'; import { ExerciseSubmission, WeightUnitUtil } from 'src/api/main/submission';
import { getFileUrl } from 'src/api/cdn';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
interface Props { interface Props {

View File

@ -1,27 +1,27 @@
<template> <template>
<q-page> <q-page>
<StandardCenteredPage v-if="submission"> <StandardCenteredPage v-if="submission">
<h3> <video
{{ submission.rawWeight }}&nbsp;{{ WeightUnitUtil.toAbbreviation(submission.weightUnit) }} class="submission-video"
{{ submission.exercise.displayName }} :src="getFileUrl(submission.videoFileId)"
</h3> loop
<p>{{ submission.reps }} reps</p> controls
<p>by <router-link :to="'/users/' + submission.user.id">{{ submission.user.name }}</router-link></p> autopictureinpicture
<p>At <router-link :to="getGymRoute(submission.gym)">{{ submission.gym.displayName }}</router-link></p> preload="metadata"
<p> autoplay
{{ submission.createdAt.setLocale($i18n.locale).toLocaleString(DateTime.DATETIME_MED) }} />
</p> <h3>
<video {{ submission.rawWeight }}&nbsp;{{ WeightUnitUtil.toAbbreviation(submission.weightUnit) }}
:src="getFileUrl(submission.videoFileId)" {{ submission.exercise.displayName }}
width="600" </h3>
loop <p>{{ submission.reps }} reps</p>
controls <p>by <router-link :to="'/users/' + submission.user.id">{{ submission.user.name }}</router-link></p>
autopictureinpicture <p>At <router-link :to="getGymRoute(submission.gym)">{{ submission.gym.displayName }}</router-link></p>
preload="metadata" <p>
autoplay {{ submission.performedAt.setLocale($i18n.locale).toLocaleString(DateTime.DATETIME_MED) }}
/> </p>
</StandardCenteredPage> </StandardCenteredPage>
</q-page> </q-page>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -48,4 +48,11 @@ onMounted(async () => {
await router.push('/'); await router.push('/');
} }
}); });
</script> </script>
<style scoped>
.submission-video {
width: 100%;
max-height: 100%;
margin-top: 20px;
}
</style>