witness-org / witness

Your fitness witness!
1 stars 0 forks source link

9: Create New Exercise - [merged] #76

Closed raffaelfoidl closed 2 years ago

raffaelfoidl commented 2 years ago

In GitLab by @11712616 on Oct 15, 2021, 20:50

Merges 9-create-new-exercise -> develop

Closes #9

raffaelfoidl commented 2 years ago

In GitLab by @11712616 on Oct 15, 2021, 20:51

Commented on client/lib/widgets/exercises/editing/exercise_create.dart line 8

Should I move this file to the model folder?

raffaelfoidl commented 2 years ago

In GitLab by @11712616 on Oct 15, 2021, 20:52

Commented on client/lib/widgets/exercises/editing/create_exercise_form.dart line 169

Should this error message be localized somehow?

raffaelfoidl commented 2 years ago

In GitLab by @11712616 on Oct 15, 2021, 20:53

requested review from @11775820

raffaelfoidl commented 2 years ago

In GitLab by @11712616 on Oct 15, 2021, 22:32

added 1 commit

Compare with previous version

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 15:32

Commented on server/src/main/resources/application-setup.yml line 45

Superfluous space before Place.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 15:32

Commented on client/lib/widgets/exercises/exercises_screen.dart line 100

As I cannot comment on images, I'm writing here.
Did you check if the license of dumbbell.png allows use for non-commercial as well as commercial projects without attribution or license/copyright notice? If not, please do so or find a dumbbell image that satisfies these criteria.

Furthermore, can't we now remove the flutter_logo.png file as well as the other resolutions in the directories 2.0x and 3.0x?

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 15:32

Commented on client/lib/extensions/enum_extensions.dart line 79

Couldn't the whole method body of toUpperCaseString replaced to something like this?

String toUpperCaseString() {
  return toUiString().toUpperCase();
}

This way you do not have to change string literals in two places and overall simply avoid duplication. Overhead induced by two method calls should be negligible.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 15:32

Commented on client/lib/localization/app_en.arb line 73

Save instead of save?

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 15:32

Commented on client/lib/localization/app_en.arb line 113

logging types instead of loggin types?

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 15:32

Commented on client/lib/models/training_programs/exercise_set.dart line 3

This class stems from myself when we didn't have a domain model yet. Which concept does this class correspond to in our domain model? Is my assumption that this class is not yet "really" used because it will be needed only for training programs correct?

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 15:32

Commented on client/lib/providers/exercise_provider.dart line 31

_auth is not unused anymore, the linter suppression can and should be removed

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 15:32

Commented on client/lib/providers/exercise_provider.dart line 39

Why make it nullable? In my opinion, the consumer should not have the responsibility of checking whether the list is null or empty. This is unnecessary downstream complexity. We can simply return an empty list otherwise.

Apart from that, this nullable list does not make much sense because the type of _exercises is Map<MuscleGroup, List<Exercise>>. Therefore, the list of exercises can never be null, making the ? redundant. Please remove it.

Even when setting _exercises in fetchExercisesByMuscleGroup, what you are getting is a List<Exercise>, so it cannot be null either. The reason you still have to return a Future<List<Exercise>?> is a mistake that is explained in the comment below (or above, whatever GitLab is doing).

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 15:32

Commented on client/lib/providers/exercise_provider.dart line 53

Since freaking GitLab seems to be failing all the time with comments that are longer than 3 characters and is not able to display a diff...this comment addresses fetchExercisesByMuscleGroup in exercise_provider.

This method is written a bit confusingly. First of all, the fact that you are returning a List<Exercise>? wrapped in a Future instead of List<Exercise> wrapped in a Future stems from the fact that you are never returning anything. In dart, if no value is explicitly returned using the return statement, null is returned.

Since you do not return anything in the nested future, i.e. within the then(...), you always return null. You would need to add return fetchedExercises; below the log statement Received ... in order for the signature Future<List<Exercise>> fetchExercisesByMuscleGroup(...) to be valid and correct.

Nevertheless, another problem with this method is the usage auf .then and having a non-void return type at all. fetchExercisesByMuscleGroup is a method that, on the UI side, is used with a FutureBuilder and a Consumer (see exercises_by_muscle_group_screen.dart). That means: Calling the method "starts" the future, FutureBuilder wraps a widget around its value and the nested Consumer<ExerciseProvider allows to access the provider state (i.e. the data fetched by the future). This usage is important because it avoids making separate HTTP requests (because the provider invokes the service) for each widget tree build lifecycle.

In other words, the function's return value is never used (as you can also see by the way it is invoked: await Provider.of<ExerciseProvider>(context, listen: false).fetchExercisesByMuscleGroup(group); in exercises_by_muscle_group_screen.dart).


Above paragraphs explained why the method does not need a return value. The following sentences aim to elaborate when the use Future(...).then(..) syntax vs. async/await.

The second "problem" I mentioned above was the usage of then. In this case, it is not necessarily wrong, but just no leveraging much more easily readable syntax - which would make the method body more concise and better comprehensible.

If you are in a method that is already declared async (or is "allowed to be declared async), then I highly encourage the use of async/await for readability and maintenance reasons. It is completely equivalent to then, the compiler/preprocessor simply allows us to have it look a bit nicer.

More generally, please always use the async/await syntax if possible. There is only one (but "hard") exception to this: If you are in a synchronous (i.e. non-async) method that must stay synchronous (e.g. a UI build method, especially if it uses a BuildContext object; corresponding linter rule). I have needed to do this, for example, in the _deleteTrainingProgram method where I access the build context across an async gap. The method needs to be synchronous, but is also waiting for the completion of a Future. Therefore, I was "forced" to use the then syntax.

However, there is no such contra-indication in this method (fetchExercisesByMuscleGroup), which is a "plain" (i.e. decoupled from UI logic) async method. Therefore, I strongly recommend rewriting it to async/await. This also makes the return type more clear (async methods automatically wrap the returned value in a Future) - a "mistake" as the one described above (with the accidental always null return value) would be spotted more easily. My approach to rewrite it would be like this:

Future<void> fetchExercisesByMuscleGroup(final MuscleGroup group) async {
  _logger.i('Fetching exercises that affect muscle group "${group.toUiString()}"');

  final fetchedExercises = await _exerciseService.getExercisesByMuscleGroup(group, await _auth?.getToken());
  _exercises[group] = fetchedExercises;
  notifyListeners();

  _logger.i('Received ${fetchedExercises.length} exercises');
}

Another note: In you current proposal, you call notifyListeners() before you change the internal state (_exercises[group] = fetchedExercises). We should aspire to avoid this because that instructs clients to redraw their widget screen based on not-yet-updated provider data. So, please, always change the internal state before notifying potential listeners.

raffaelfoidl commented 2 years ago

In GitLab by @11712616 on Oct 16, 2021, 17:33

approved this merge request

raffaelfoidl commented 2 years ago

In GitLab by @11712616 on Oct 16, 2021, 17:33

unapproved this merge request

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:05

Commented on client/lib/services/exercise_service.dart line 21

This and other occurrences of Uri constructions as well as query parameter appending will be adjusted after merging #49.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:05

Commented on client/lib/services/exercise_service.dart line 26

Suggestion:

final responseList = json.decode(response.body) as List<dynamic>;
return responseList.map((final dynamic e) => Exercise.fromJson(e as Map<String, dynamic>)).toList();

toList() is faster and, because generic types are inferred easier to read imo. see also List.from is slower than toList on github for reasons.

The alternative presented above is, in my opinion easier to read. The performance is possibly not very relevant for us, but maybe a factor in favor of this one. In any case, please rename responseMap to responseList.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:05

Commented on client/lib/widgets/exercises/details/exercise_detail_screen.dart line 17

VoidCallback is just a type alias for void Function(). As far as I know, there are unfortunately not many more useful callback aliases for different types of Functions we could use.

If you would like to stay with VoidCallback here, then please also replace the occurrences of void Function() in training_log_screen andapp_drawerbyVoidCallback` to stay consistent.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:05

Commented on client/lib/widgets/exercises/editing/create_exercise_form.dart line 13

Please use auto-format (CTRL+ALT+L) which also tidies up the imports. Maybe dartfmt doesn't complain in the CI build, but imports like this are still inconsistent with the rest and auto-format would fix it.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:05

Commented on client/lib/widgets/exercises/editing/create_exercise_form.dart line 53

The way you are using bangs (!) (more officially, the null-check operator) is, unfortunately, not very beneficial for null-safety. If you have a look hat how I have been using the null-check operator, you will notice that I only employ it when I have an assertion that the respective variable is not null. For example, I have many constructs like var1 == null ? doSomethingThatCanHandleNull(var1) : doTheThingThatDoesNotCareAboutNull(var1!).

These bangs should only be used when you can absolutely be certain that the variable is NOT null (i.e. via null-checks). For local variables, a preceding (if (x != null)) is sufficient due to dart's type promotion. For global variables, however, this does not work (which is why I often declare methods taking non-nullable parameters which are just banged versions of the nullable global variable -- so that I do not have the complexity of handling null in those methods).

What you are doing here, i.e. putting bangs after the variables without having any assertion (from yourself, the docs, ...), is essentially turning off darts sound null-safety. This completely removes all the advantages we get with compiler-guaranteed null-safety and re-introduces nullpointer exception during runtime. You can compare it to using dynamic to turn off static type checks by the compiler. So, please don't do it like this. The same goes for late variables - bangs and late are not to be used as ways to turn off the annoying compiler errors; they must not be used unless we have asserted them not to bear the risk of runtime NPEs.

The documentation of onSaved tells you in which cases value could be null. You can circumvent this with the initialValue attribute of TextFormField (read docs please what's applicable, initialValue is ok in this case since you dont have it hooked up to a Controller).

(non-) Type promotion: https://dart.dev/tools/non-promotion-reasons
sound Null-Safety in general: https://dart.dev/null-safety/understanding-null-safety

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:05

Commented on client/lib/widgets/exercises/editing/create_exercise_form.dart line 54

As per the style guide in the wiki, please add , after the last list element and after the closing bracket ] and then re-auto-format.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:05

Commented on client/lib/widgets/exercises/editing/create_exercise_form.dart line 61

,],),) and then auto-format. Furthemore, insert a comma , after the final argument because decoration constructs a new widget (i.e. .... _formInput.description = value,), then auto-format. The same goes for various places in the files touched by this PR, I will not comment on every instance. Please try to stick to the style guide described in the wiki.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:05

Commented on client/lib/widgets/exercises/editing/create_exercise_form.dart line 67

Please use more descriptive lambda variable names than v.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:06

Commented on client/lib/widgets/exercises/editing/create_exercise_form.dart line 70

here it should be more or less ok I think because all muscle groups are in the map. one needs to re-check if muscle groups are added, though

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:06

Commented on client/lib/widgets/exercises/editing/create_exercise_form.dart line 86

same as with muscle groups, I think this bang is ok. but please choose a bettername than v

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:06

Commented on client/lib/widgets/exercises/editing/create_exercise_form.dart line 117

I think a full-width button would look a bit nicer:

child: SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  onPressed: () => _submitForm(context),
                  child: Text(uiStrings.createExerciseScreen_body_save_buttonText),
               ),
)

image

image

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:06

Commented on client/lib/widgets/exercises/editing/create_exercise_form.dart line 152

See comments regarding null safety. See login_card.submit for an example on how to assert that _formKey.currentState is not null. (make the if disjunctive with _formKey.currentState == null being the first operand - due to short-circuit semantics, the bang in the second operator and within the if body will then be ok). However, I'd prefer inverted if to reduce nesting:

if (_formkey.currentState == null || !_formKey.currentState!.validater()) {
  return;
}
...save(); ....
raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:06

Commented on client/lib/widgets/exercises/editing/create_exercise_form.dart line 201

NOTE (2):
Note number 2 is on top because it should have occurred to me right at the beginning and not only after making screenshots and writing everything out below...

I don't quite understand why you are doing the navigation like this at all. I mean, why are you passing a navigation callback to the detail screen with the newly introduced back button just in order to go back to the "main exercise screen" (by muscle group) after going back from the newly created exercise?

From what I have tested, you would achieve exactly the same thing if you replaced Navigator.of(context).pushNamed(ExerciseDetailScreen.routeName, ...) by pushNamedReplacement. Then, the create_exercise_form is popped off the screen stack (making the "by muscle group" screen the top screen again), after whcih the new exercise detail screen is pushed onto the stack. So when you go back, you are back at the "by muscle group screen" - in my eyes exactly the same behaviour as we have here right now, just without any additional changes. So, in my opinion, you don't need the changes in the routing and the new backButtonNavigation field as well as leading: BackButton() since the default behaviour should suffice (and also no list in the arguments when navigating to the exercise detail screen).

Am I missing something? Ff my comment here should be correct, please also read the notes below.


The navigation with the backButtonNavigation only works by coincidence here. You are asking for a VoidCallback, which would have the form () => Navigator.of(context).pushReplacementNamed(ExerciseScreen.routeName). What you are passing, however, is the return value of pushReplacementNamed, which is of type Future.

In app_routing, you then try to convert it with castOrNull, which will always be null because a Future cannot be cast to a void Function(). Here a picture providing evidence:

image

So why does the navigation still work as intended? I honestly don't want to dig too deep, but I think it's got something to do with the fact that the pushReplacement returns Future<void in this case which is almost the same as void. However, as the screenshot shows, there is definitely something fishy here.

The correct way to invoke would be like this (please also note the formatting):

      provider.postUserExercise(exerciseCreate).then(
          (final value) => Navigator.of(context).pushNamed(
                ExerciseDetailScreen.routeName,
                arguments: [
                  value,
                  () => Navigator.of(context).pushReplacementNamed(ExercisesScreen.routeName),
                ],
              ),
          onError: (final Object error) => _showError(error.toString()));

Which works as intended with the routing: image

NOTE (1)
However, I would also like to suggest another way of doing it. Since you already called it backButtonNavigation, I would like to reduce the complexity of routing by simply passing a route to nevigate to upon pressing the back button instead of the callback. I.e.

ExerciseDetailScreen:
final String? _backButtonTargetRoute
...
BackButton(onPressed: _backButtonTargetRoute != null ? () => Navigator.of(context).pushReplacement(_backButtonTargetRoute!) : null

app_routing:
return ExerciseDetailScreen(args?[0].castOrNull<Exercise>(), args?[1].castOrNull<String?>());

create_exercise_form:
arguments: [value, ExercisesScreen.routeName]

In my opinion, this approach would decrease the overall complexity since the nasty stuff is hidden in exercise_detail_screen. If you don't like this, in any case, you need to resolve the error in the routing parameter (add () => in order to actually pass a callback).

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:06

Commented on client/lib/widgets/exercises/editing/create_exercise_form.dart line 218

that's how I like my !s :)

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:06

Commented on client/lib/widgets/exercises/editing/create_exercise_form.dart line 169

Yes, please. In general, I like dynamic error messages like this. However, that doesn't really translate well to other languages. Since the number of actual error messages (the possible combinations) that can be produced by this method is rather limited, I'd propose storing exactly those error messages in the .arb file and - based on which condition is actually present - return the correct one.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:07

Commented on client/lib/widgets/exercises/editing/edit_exercise_screen.dart line 36

That's an example of what I was taking about before in create_exercise_form regarding (my interpretation of) proper utiliztation of dart's nullable types.

We know that _exercise is not null when we are building the "real" exercise screen in using _buildEditScreen. So why should we not alleviate ourselves from the hassle of supporting or handling the Exercise? type in that method? What you are doing here is saying Hey, this a method that can handle null values gracefully, but then you just turn off the compiler checks with the bang !, effectively making the code nun-null-afe and inviting runtime NPEs because the method does not handle null gracefully.

Therefore, please change the _buildEditScreen as follows (note the changed formatting, I inserted a , after the child Text according to the style guide - but it was already incorrectly formatted by myself, my bad):

  Widget _buildEditScreen(final Exercise exercise) {
    return Center(
      child: Text('Editing Exercise "${exercise.name}"'),
    );
  }

Then, we are allowed to change this line to _exercise == null ? _buildCreateScreen() : _buildEditScreen(_exercise!) because we know __exericse_exercise to be non-null.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:07

Commented on client/lib/widgets/exercises/editing/exercise_create.dart line 8

Yes, absolutely, please.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:07

Commented on client/lib/widgets/exercises/exercises_by_muscle_group_screen.dart line 56

If you have removed this UI element entirely (and not moved to somewhere, I couldn't find it), then please also remove the localized string exerciseByMuscleGroupScreen_createNew_tooltip in the arb file.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:07

Commented on client/lib/widgets/exercises/exercises_by_muscle_group_screen.dart line 137

As already elaborated above, I do not think this change to a list argument is required.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:07

Commented on client/lib/widgets/app_routing.dart line 55

As already explained somewhere above, I don't think this change is required.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:07

Commented on server/src/main/java/com/witness/server/dto/UserExerciseDto.java line 10

Maybe remove this blank line? ExerciseDto doesn't have it either.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:08

Commented on server/src/main/java/com/witness/server/entity/Exercise.java line 64

Superfluous blank line?

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:08

Commented on server/src/main/java/com/witness/server/entity/Exercise.java line 72

var

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:08

Commented on server/src/main/java/com/witness/server/entity/User.java line 89

var

And why remove height from equals condition?

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:08

Commented on server/src/main/java/com/witness/server/entity/UserExercise.java line 29

Is there a reason why the @Builder annotation is not placed at the class level? And what's the reasoning behind the specified builder method name, i.e. why does the name generated by Lombok by default not suffice? Does MapStruct need it like this?

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:08

Commented on server/src/main/java/com/witness/server/enumeration/LoggingType.java line 3

For consistency reasons, please modify Sex such that each lement is on a separate line.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:08

Commented on server/src/main/java/com/witness/server/exception/InvalidRequestException.java line 9

This empty line is not present in the other derived Exception classes. Please either everywhere or nowhere.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:08

Commented on server/src/main/java/com/witness/server/mapper/ExerciseMapper.java line 8

ArrayList is an unused import, please remove

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:08

Commented on server/src/main/java/com/witness/server/mapper/ExerciseMapper.java line 18

Why are manually specifying target and source for properties that are mapped automatically? Doesn't that kinda defeat the purpose of the mapper? Now we have boilerplate @Mapping annotations instead of boilerplate A.setX(B.getX).

Those properties will be mapped anyways since they have the same names. I highly suggest that we only explicitly specify mappings that cannot be automatically inferred (e.g. when DTO and entity property have different names). That way we have less to read and less mental load when comprehending a mapper definition.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:09

Commented on server/src/main/java/com/witness/server/repository/ExerciseRepository.java line 12

I have some thougts here.

  1. We have to talk about queries in repositories. Not to sound rude, but I am anything but a fan of the formatting of the queries here. From a readbility point of view, I am of the opinion that uppercasing SQL keywords (SELECT, ...) is beneficial.
  2. Furtheremore, I highly, highly suggest using Java 15's text block feature. This allows us to define multline-strings as literals (the left hanging indent does not cound towards the string), which does not improve readbility of queries immensely, but also allows as to structure the query such that humans can read it. The problem with string concatenation using + here is that it is not a constant expression anymore, therefore making the Spring Data/JPL integration basically useless (as soon as it cannot match variable names anymore etc.). These problems are all solved with text blocks.
  3. I have attached a version of this very same repository in the expander below as a proposal. Please, when using this feature, make use of the auto-format feature to achieve consistent hanging left indents. We may discuss whether we want to format SQL strings a bit differently - the attached version is how I like it quite well. It is a bit verbose, but clearly separates the different parts of the query, which is my main concern with such queries.
Expand/Fold `ExerciseRepository` format proposal ```java package com.witness.server.repository; import com.witness.server.entity.Exercise; import com.witness.server.entity.User; import com.witness.server.enumeration.MuscleGroup; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository public interface ExerciseRepository extends JpaRepository { @Query(""" SELECT e FROM Exercise e LEFT OUTER JOIN UserExercise u ON e.id = u.id WHERE (u.createdBy = :user OR u.createdBy is null) AND :muscleGroup MEMBER OF e.muscleGroups """) List findAllForUser(User user, MuscleGroup muscleGroup); @Query(""" SELECT e FROM Exercise e JOIN UserExercise u ON e.id = u.id WHERE u.createdBy = :user """) List findAllByUser(User user); @Query(""" SELECT CASE WHEN COUNT(e) > 0 THEN TRUE ELSE FALSE END FROM Exercise e LEFT OUTER JOIN UserExercise u ON e.id = u.id WHERE e.name = :name AND u.createdBy IS NULL """) boolean existsByName(String name); } ```
  1. At a later point, we will have to think about whether the queries could be formulated more concise (easier to understand) or if we want to design it differently. For example, one could debate whether existsByName could simply return the count (a long) and the service checks count > 0. This would get rid of proprietary or hibernate-specific CASE WHEN END statements and, I claim, a bit easier to understand. For this MR right now, it's also okay as you have proposed.

  2. As we have rather complex queries here, I would love to see repository tests. I want to emphasize that I don't expect repository tests for "standard" operations or query methods generated by Spring (e.g. findByFirebaseIdEquals in UserRepository). However, what we have here are rather complicated queries. Especially considering we might change them in the future (making them easier etc.), I would like to have repository tests verifying their correct behaviour - tests that assert that correct elements are part of the result set and other elementes are not part of the result set. It doesn't have to be too much effort, the base cases should be covered. In the expander below you find a sketch of how I would like those tests to look like. Te @DataJpaTest annotation makes that the unit tests are executed on an in-memory database (which is desired, in the best case, only integration tests should rellay create a database).

Expand/Fold `ExerciseRepositoryTest` format proposal _server/src/test/java/com/witness/server/unit/repository/ExerciseRepositoryTest.java_ ```java package com.witness.server.unit.repository; import static org.assertj.core.api.Assertions.assertThat; import com.witness.server.entity.Exercise; import com.witness.server.repository.ExerciseRepository; import com.witness.server.repository.UserRepository; import com.witness.server.unit.BaseUnitTest; import com.witness.server.util.JsonFileSource; import com.witness.server.util.JsonFileSources; import java.util.List; import lombok.Data; import lombok.NoArgsConstructor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; @DataJpaTest class ExerciseRepositoryTest extends BaseUnitTest { private static final String DATA_ROOT = "data/unit/repository/exercise-repository-test/"; @Autowired ExerciseRepository exerciseRepository; @Autowired UserRepository userRepository; @BeforeEach public void setup() { exerciseRepository.deleteAll(); userRepository.deleteAll(); } @ParameterizedTest @JsonFileSources(unwrapArrays = true, parameters = { @JsonFileSource(value = DATA_ROOT + "existsByName.json", type = ExistsByNameTestSpecification[].class) }) void existsByName_givenRepositoryContent_returnsValueBasedOnName(ExistsByNameTestSpecification specification) { exerciseRepository.saveAllAndFlush(specification.exercises); assertThat(exerciseRepository.existsByName(specification.queryName)).isEqualTo(specification.expectedResult); } @Data @NoArgsConstructor static class ExistsByNameTestSpecification { private List exercises; private String queryName; private boolean expectedResult; } } ``` _server/src/test/resources/data/unit/repository/exercise-repository-test/existsByName.json_: ```java [ { "exercises": [], "queryName": "nonExistingName", "expectedResult": false }, { "exercises": [ { "name": "existingName1", "muscleGroups": [ "CHEST" ], "loggingTypes": [ "REPS" ] }, { "name": "existingName2", "muscleGroups": [ "SHOULDERS" ], "loggingTypes": [ "TIME" ] } ], "queryName": "existingName1", "expectedResult": true }, { "exercises": [ { "name": "existingName", "muscleGroups": [ "CHEST" ], "loggingTypes": [ "REPS" ] } ], "queryName": "nonExistingName", "expectedResult": false } ] ```
raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:09

Commented on server/src/main/java/com/witness/server/repository/UserExerciseRepository.java line 8

That's an example of a repository method that does not need a test. However, please remove the blank line.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:16

Commented on client/lib/widgets/exercises/exercises_by_muscle_group_screen.dart line 67

Nested Text element => comma after closing parenthesis of Text and then re-format to make it look like this (rule 4):

return Center(
  child: Text('Could not fetch exercises: ' + snapshot.error.toString()),
);
raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:16

Commented on client/lib/widgets/exercises/exercises_screen.dart line 24

Now it looks like this (which is definitely not nice):
image

Please wrap the Listview.builder with an Expanded().

I'd even prefer having the Listview.builder wrapped in a ScrollBar, which in turn is wrapped in an Expanded:

    return Expanded(
      child: Scrollbar(
        isAlwaysShown: true,
        child: ListView.builder(
          scrollDirection: Axis.vertical,
          shrinkWrap: true,
          itemCount: MuscleGroup.values.length,
          itemBuilder: (final _, final index) {
            final muscleGroup = MuscleGroup.values[index];
            return Column(
              children: [
                _ExerciseOverviewItem(muscleGroup),
                const Divider(),
              ],
            );
          },
        ),
      ),
    );

This yields a scrollbar (if resolution/item count warrants it):
image

And no scrollbar if resolution/item doesn't warrant it, but the "waves" are at the bottom (i.e. listview fills vertical space):
image

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:23

Commented on server/src/main/java/com/witness/server/service/impl/ExerciseServiceImpl.java line 28

Would you like to keep ExerciseRepository exerciseRepository on the previous line? There is enough space and it would be more consistent with other classes.

raffaelfoidl commented 2 years ago

In GitLab by @11775820 on Oct 16, 2021, 21:23

Commented on server/src/main/java/com/witness/server/service/impl/ExerciseServiceImpl.java line 51

var