Skip to content

Commit 0d3ca08

Browse files
committed
merged with swagger branch
2 parents 3c6bf16 + 51c1c71 commit 0d3ca08

20 files changed

+686
-477
lines changed

app/V1Module/presenters/GroupsPresenter.php

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use App\Model\Entity\LocalizedGroup;
1717
use App\Model\Entity\GroupMembership;
1818
use App\Model\Entity\AssignmentSolution;
19-
use App\Model\Entity\SecurityEvent;
2019
use App\Model\Repository\Assignments;
2120
use App\Model\Repository\Groups;
2221
use App\Model\Repository\GroupExams;
@@ -194,6 +193,36 @@ public function actionDefault(
194193
$this->sendSuccessResponse($this->groupViewFactory->getGroups($groups, false));
195194
}
196195

196+
/**
197+
* Helper method that handles updating points limit and threshold to a group entity (from a request).
198+
* @param Request $req request data
199+
* @param Group $group to be updated
200+
*/
201+
private function setGroupPoints(Request $req, Group $group): void
202+
{
203+
$threshold = $req->getPost("threshold");
204+
$pointsLimit = $req->getPost("pointsLimit");
205+
if ($threshold !== null && $pointsLimit !== null) {
206+
throw new InvalidArgumentException("A group may have either a threshold or points limit, not both.");
207+
}
208+
if ($threshold !== null) {
209+
if ($threshold <= 0 || $threshold > 100) {
210+
throw new InvalidArgumentException("A threshold must be in the (0, 100] (%) range.");
211+
}
212+
$group->setThreshold($threshold / 100);
213+
} else {
214+
$group->setThreshold(null);
215+
}
216+
if ($pointsLimit !== null) {
217+
if ($pointsLimit <= 0) {
218+
throw new InvalidArgumentException("A points limit must be a positive number.");
219+
}
220+
$group->setPointsLimit($pointsLimit);
221+
} else {
222+
$group->setPointsLimit(null);
223+
}
224+
}
225+
197226
/**
198227
* Create a new group
199228
* @POST
@@ -215,10 +244,10 @@ public function actionDefault(
215244
* description="Whether the group is an exam group.")
216245
* @Param(type="post", name="localizedTexts", validation="array", required=false,
217246
* description="Localized names and descriptions")
218-
* @Param(type="post", name="hasThreshold", validation="bool",
219-
* description="True if threshold was given, false if it should be unset")
220247
* @Param(type="post", name="threshold", validation="numericint", required=false,
221248
* description="A minimum percentage of points needed to pass the course")
249+
* @Param(type="post", name="pointsLimit", validation="numericint", required=false,
250+
* description="A minimum of (absolute) points needed to pass the course")
222251
* @Param(type="post", name="noAdmin", validation="bool", required=false,
223252
* description="If true, no admin is assigned to group (current user is assigned as admin by default.")
224253
* @throws ForbiddenRequestException
@@ -249,7 +278,6 @@ public function actionAddGroup()
249278
$isPublic = filter_var($req->getPost("isPublic"), FILTER_VALIDATE_BOOLEAN);
250279
$isOrganizational = filter_var($req->getPost("isOrganizational"), FILTER_VALIDATE_BOOLEAN);
251280
$isExam = filter_var($req->getPost("isExam"), FILTER_VALIDATE_BOOLEAN);
252-
$hasThreshold = filter_var($req->getPost("hasThreshold"), FILTER_VALIDATE_BOOLEAN);
253281
$noAdmin = filter_var($req->getPost("noAdmin"), FILTER_VALIDATE_BOOLEAN);
254282

255283
if ($isOrganizational && $isExam) {
@@ -267,12 +295,8 @@ public function actionAddGroup()
267295
$detaining,
268296
$isExam,
269297
);
270-
if ($hasThreshold) {
271-
$threshold = $req->getPost("threshold") !== null
272-
? $req->getPost("threshold") / 100
273-
: $group->getThreshold();
274-
$group->setThreshold($threshold);
275-
}
298+
299+
$this->setGroupPoints($req, $group);
276300
$this->updateLocalizations($req, $group);
277301

278302
$this->groups->persist($group, false);
@@ -329,10 +353,10 @@ public function checkUpdateGroup(string $id)
329353
* required=false, description="Are students prevented from leaving the group on their own?")
330354
* @Param(type="post", name="isPublic", validation="bool",
331355
* description="Should the group be visible to all student?")
332-
* @Param(type="post", name="hasThreshold", validation="bool",
333-
* description="True if threshold was given, false if it should be unset")
334356
* @Param(type="post", name="threshold", validation="numericint", required=false,
335357
* description="A minimum percentage of points needed to pass the course")
358+
* @Param(type="post", name="pointsLimit", validation="numericint", required=false,
359+
* description="A minimum of (absolute) points needed to pass the course")
336360
* @Param(type="post", name="localizedTexts", validation="array", description="Localized names and descriptions")
337361
* @param string $id An identifier of the updated group
338362
* @throws InvalidArgumentException
@@ -343,22 +367,14 @@ public function actionUpdateGroup(string $id)
343367
$publicStats = filter_var($req->getPost("publicStats"), FILTER_VALIDATE_BOOLEAN);
344368
$detaining = filter_var($req->getPost("detaining"), FILTER_VALIDATE_BOOLEAN);
345369
$isPublic = filter_var($req->getPost("isPublic"), FILTER_VALIDATE_BOOLEAN);
346-
$hasThreshold = filter_var($req->getPost("hasThreshold"), FILTER_VALIDATE_BOOLEAN);
347370

348371
$group = $this->groups->findOrThrow($id);
349372
$group->setExternalId($req->getPost("externalId"));
350373
$group->setPublicStats($publicStats);
351374
$group->setDetaining($detaining);
352375
$group->setIsPublic($isPublic);
353376

354-
if ($hasThreshold) {
355-
$threshold = $req->getPost("threshold") !== null ? $req->getPost("threshold") / 100 : $group->getThreshold(
356-
);
357-
$group->setThreshold($threshold);
358-
} else {
359-
$group->setThreshold(null);
360-
}
361-
377+
$this->setGroupPoints($req, $group);
362378
$this->updateLocalizations($req, $group);
363379

364380
$this->groups->persist($group);

app/commands/GenerateSwagger.php

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,10 @@
22

33
namespace App\Console;
44

5-
use App\Helpers\Notifications\ReviewsEmailsSender;
6-
use App\Model\Repository\AssignmentSolutions;
7-
use App\Model\Entity\Group;
8-
use App\Model\Entity\User;
95
use Symfony\Component\Console\Command\Command;
10-
use Symfony\Component\Console\Input\InputArgument;
116
use Symfony\Component\Console\Input\InputInterface;
127
use Symfony\Component\Console\Output\OutputInterface;
13-
use DateTime;
8+
use \OpenApi\Generator;
149

1510
class GenerateSwagger extends Command
1611
{
@@ -19,18 +14,26 @@ class GenerateSwagger extends Command
1914
protected function configure()
2015
{
2116
$this->setName(self::$defaultName)->setDescription(
22-
'Generate a swagger specification file from existing code.'
17+
'Generate an OpenAPI documentation from the temporary file created by the swagger:annotate command.'
18+
. ' The temporary file is deleted afterwards.'
2319
);
2420
}
2521

2622
protected function execute(InputInterface $input, OutputInterface $output)
2723
{
2824
$path = __DIR__ . '/../V1Module/presenters/_autogenerated_annotations_temp.php';
29-
$openapi = \OpenApi\Generator::scan([$path]);
25+
26+
// check if file exists
27+
if (!file_exists($path)) {
28+
$output->writeln("Error in GenerateSwagger: Temp annotation file not found.");
29+
return Command::FAILURE;
30+
}
31+
32+
$openapi = Generator::scan([$path]);
3033

3134
$output->writeln($openapi->toYaml());
3235

33-
# delete the temp file
36+
// delete the temp file
3437
unlink($path);
3538

3639
return Command::SUCCESS;

app/commands/MetaTester.php

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,11 @@
22

33
namespace App\Console;
44

5-
use App\Helpers\Notifications\ReviewsEmailsSender;
6-
use App\Model\Repository\AssignmentSolutions;
7-
use App\Model\Entity\Group;
8-
use App\Model\Entity\User;
95
use Symfony\Component\Console\Command\Command;
106
use Symfony\Component\Console\Input\InputArgument;
117
use Symfony\Component\Console\Input\InputInterface;
128
use Symfony\Component\Console\Output\OutputInterface;
13-
use DateTime;
9+
use App\Model\View\TestView;
1410

1511
///TODO: this command is debug only, delete it
1612
class MetaTester extends Command
@@ -30,8 +26,9 @@ protected function execute(InputInterface $input, OutputInterface $output)
3026
return Command::SUCCESS;
3127
}
3228

33-
function test(string $arg) {
34-
$view = new \App\Model\View\TestView();
29+
function test(string $arg)
30+
{
31+
$view = new TestView();
3532
$view->endpoint([
3633
"id" => "0",
3734
"organizational" => false,

app/commands/SwaggerAnnotator.php

Lines changed: 51 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
namespace App\Console;
44

5-
use App\Helpers\Swagger\FileBuilder;
5+
use App\Helpers\Swagger\TempAnnotationFileBuilder;
66
use App\Helpers\Swagger\AnnotationHelper;
77
use App\V1Module\Router\MethodRoute;
88
use Nette\Routing\RouteList;
99
use Symfony\Component\Console\Command\Command;
1010
use Symfony\Component\Console\Input\InputInterface;
1111
use Symfony\Component\Console\Output\OutputInterface;
12+
use Exception;
13+
use ReflectionException;
14+
use ReflectionClass;
1215

1316
class SwaggerAnnotator extends Command
1417
{
@@ -18,59 +21,68 @@ class SwaggerAnnotator extends Command
1821

1922
protected function configure(): void
2023
{
24+
$filePath = self::$autogeneratedAnnotationFilePath;
2125
$this->setName(self::$defaultName)->setDescription(
22-
'Annotate all methods with Swagger PHP annotations.'
26+
"Extracts endpoint method annotations and puts them into a temporary file that can be used to generate"
27+
. " an OpenAPI documentation. The file is located at {$filePath}"
2328
);
2429
}
2530

2631
protected function execute(InputInterface $input, OutputInterface $output): int
2732
{
28-
# create a temporary file containing transpiled annotations usable by the external library (Swagger-PHP)
29-
$fileBuilder = new FileBuilder(self::$autogeneratedAnnotationFilePath);
30-
$fileBuilder->startClass('__Autogenerated_Annotation_Controller__', '1.0', 'ReCodEx API');
31-
32-
# get all routes of the api
33-
$routes = $this->getRoutes();
34-
foreach ($routes as $routeObj) {
35-
# extract class and method names of the endpoint
36-
$metadata = $this->extractMetadata($routeObj);
37-
$route = $this->extractRoute($routeObj);
38-
$className = self::$presenterNamespace . $metadata['class'];
39-
40-
# extract data from the existing annotations
41-
$annotationData = AnnotationHelper::extractAnnotationData($className, $metadata['method'], $route);
42-
43-
# add an empty method to the file with the transpiled annotations
44-
$fileBuilder->addAnnotatedMethod($metadata['method'], $annotationData->toSwaggerAnnotations($route));
45-
}
46-
$fileBuilder->endClass();
33+
try {
34+
// create a temporary file containing transpiled annotations usable by the external library (Swagger-PHP)
35+
$fileBuilder = new TempAnnotationFileBuilder(self::$autogeneratedAnnotationFilePath);
36+
$fileBuilder->startClass('__Autogenerated_Annotation_Controller__', '1.0', 'ReCodEx API');
37+
38+
// get all routes of the api
39+
$routes = $this->getRoutes();
40+
foreach ($routes as $routeObj) {
41+
// extract class and method names of the endpoint
42+
$metadata = $this->extractMetadata($routeObj);
43+
$route = $this->extractRoute($routeObj);
44+
$className = self::$presenterNamespace . $metadata['class'];
45+
46+
// extract data from the existing annotations
47+
$annotationData = AnnotationHelper::extractAnnotationData($className, $metadata['method'], $route);
48+
49+
// add an empty method to the file with the transpiled annotations
50+
$fileBuilder->addAnnotatedMethod($metadata['method'], $annotationData->toSwaggerAnnotations($route));
51+
}
52+
$fileBuilder->endClass();
53+
54+
return Command::SUCCESS;
55+
} catch (Exception $e) {
56+
$output->writeln("Error in SwaggerAnnotator: {$e->getMessage()}");
4757

48-
return Command::SUCCESS;
58+
return Command::FAILURE;
59+
}
4960
}
5061

5162
/**
5263
* Finds all route objects of the API
5364
* @return array Returns an array of all found route objects.
5465
*/
55-
function getRoutes(): array {
66+
private function getRoutes(): array
67+
{
5668
$router = \App\V1Module\RouterFactory::createRouter();
5769

58-
# find all route object using a queue
70+
// find all route object using a queue
5971
$queue = [$router];
6072
$routes = [];
6173
while (count($queue) != 0) {
6274
$cursor = array_shift($queue);
6375

6476
if ($cursor instanceof RouteList) {
6577
foreach ($cursor->getRouters() as $item) {
66-
# lists contain routes or nested lists
78+
// lists contain routes or nested lists
6779
if ($item instanceof RouteList) {
6880
array_push($queue, $item);
69-
}
70-
else {
71-
# the first route is special and holds no useful information for annotation
72-
if (get_parent_class($item) !== MethodRoute::class)
81+
} else {
82+
// the first route is special and holds no useful information for annotation
83+
if (get_parent_class($item) !== MethodRoute::class) {
7384
continue;
85+
}
7486

7587
$routes[] = $this->getPropertyValue($item, "route");
7688
}
@@ -85,10 +97,11 @@ function getRoutes(): array {
8597
* Extracts the route string from a route object. Replaces '<..>' in the route with '{...}'.
8698
* @param mixed $routeObj
8799
*/
88-
private function extractRoute($routeObj): string {
100+
private function extractRoute($routeObj): string
101+
{
89102
$mask = self::getPropertyValue($routeObj, "mask");
90103

91-
# sample: replaces '/users/<id>' with '/users/{id}'
104+
// sample: replaces '/users/<id>' with '/users/{id}'
92105
$mask = str_replace(["<", ">"], ["{", "}"], $mask);
93106
return "/" . $mask;
94107
}
@@ -98,14 +111,16 @@ private function extractRoute($routeObj): string {
98111
* @param mixed $routeObj The route object representing the endpoint.
99112
* @return string[] Returns a dictionary [ "class" => ..., "method" => ...]
100113
*/
101-
private function extractMetadata($routeObj) {
114+
private function extractMetadata($routeObj)
115+
{
102116
$metadata = self::getPropertyValue($routeObj, "metadata");
103117
$presenter = $metadata["presenter"]["value"];
104118
$action = $metadata["action"]["value"];
105119

106-
# if the name is empty, the method will be called 'actionDefault'
107-
if ($action === null)
120+
// if the name is empty, the method will be called 'actionDefault'
121+
if ($action === null) {
108122
$action = "default";
123+
}
109124

110125
return [
111126
"class" => $presenter . "Presenter",
@@ -122,12 +137,12 @@ private function extractMetadata($routeObj) {
122137
*/
123138
private static function getPropertyValue($object, string $propertyName): mixed
124139
{
125-
$class = new \ReflectionClass($object);
140+
$class = new ReflectionClass($object);
126141

127142
do {
128143
try {
129144
$property = $class->getProperty($propertyName);
130-
} catch (\ReflectionException $exception) {
145+
} catch (ReflectionException $exception) {
131146
$class = $class->getParentClass();
132147
$property = null;
133148
}

app/commands/runtimes/RuntimeImport.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ protected static function validate(array $obj, array $schema, array $prefix = []
153153
{
154154
foreach ($schema as $key => $desc) {
155155
// check existence
156-
$path = $prefix ? ("'" . join($prefix, "' / '") . "'") : '<root>';
156+
$path = $prefix ? ("'" . join("' / '", $prefix) . "'") : '<root>';
157157
if (!array_key_exists($key, $obj)) {
158158
throw new RuntimeException("Property '$key' is missing in $path structure.");
159159
}

app/helpers/Emails/Notifications/Reviews/reviewClosed_en.latte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
{if !$issues && !$comments}
1111
<p>
12-
There are no issues to be resolved no comments in the review.
12+
There are no issues to be resolved and no comments in the review.
1313
</p>
1414
{else}
1515
<p>

0 commit comments

Comments
 (0)