Skip to content

Commit

Permalink
Merge pull request #256 from ArmaForces/nearest-mission-modlist
Browse files Browse the repository at this point in the history
Show nearest mission modlist on the modlist list page
  • Loading branch information
veteran29 authored Sep 26, 2022
2 parents 7754513 + c33092b commit dac5d5d
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 56 deletions.
2 changes: 1 addition & 1 deletion src/Controller/HomeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function __construct(
public function indexAction(): Response
{
try {
$nearestMission = $this->missionClient->getNearestMission();
$nearestMission = $this->missionClient->getNextUpcomingMission();
} catch (\Exception $ex) {
$this->logger->warning('Could not fetch nearest mission', ['ex' => $ex]);
$nearestMission = null;
Expand Down
20 changes: 19 additions & 1 deletion src/Controller/ModListPublicController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
use App\Repository\Mod\ModRepository;
use App\Repository\ModList\ModListRepository;
use App\Security\Enum\PermissionsEnum;
use App\Service\Mission\MissionClient;
use Psr\Log\LoggerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\HeaderUtils;
Expand All @@ -20,8 +22,10 @@
class ModListPublicController extends AbstractController
{
public function __construct(
private LoggerInterface $logger,
private ModRepository $modRepository,
private ModListRepository $modListRepository
private ModListRepository $modListRepository,
private MissionClient $missionClient,
) {
}

Expand All @@ -35,8 +39,22 @@ public function selectAction(): Response
'name' => 'ASC',
]);

try {
$nextMission = $this->missionClient->getCurrentMission();
} catch (\Exception $ex) {
$this->logger->warning('Could not fetch next upcoming', ['ex' => $ex]);
$nextMission = null;
}

$nextMissionModList = null;
if ($nextMission) {
$nextMissionModList = $this->modListRepository->findOneBy(['name' => $nextMission->getModlist()]);
}

return $this->render('mod_list_public/select.html.twig', [
'modLists' => $modLists,
'nextMission' => $nextMission,
'nextMissionModList' => $nextMissionModList,
]);
}

Expand Down
7 changes: 7 additions & 0 deletions src/Service/Mission/Dto/MissionDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public function __construct(
private \DateTimeImmutable $date,
private \DateTimeImmutable $closeDate,
private string $description,
private string $modlistName,
private int $freeSlots,
private int $allSlots,
private string $state,
Expand All @@ -30,6 +31,7 @@ public static function fromArray(array $array): self
\DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s', substr($array['date'], 0, 19), $timezone),
\DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s', substr($array['closeDate'], 0, 19), $timezone),
$array['description'],
$array['modlistName'],
$array['freeSlots'],
$array['allSlots'],
$array['state'],
Expand Down Expand Up @@ -62,6 +64,11 @@ public function getDescription(): string
return $this->description;
}

public function getModlist(): string
{
return $this->modlistName;
}

public function getFreeSlots(): int
{
return $this->freeSlots;
Expand Down
25 changes: 24 additions & 1 deletion src/Service/Mission/MissionClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,36 @@ public function getMissions(bool $includeArchive = true, int $ttl = 600): \Gener
}
}

public function getNearestMission(): ?MissionDto
public function getNextUpcomingMission(): ?MissionDto
{
$upcomingMissions = $this->getUpcomingMissions();

return $upcomingMissions ? array_pop($upcomingMissions) : null;
}

public function getCurrentMission(): ?MissionDto
{
$missions = $this->getMissions();

$now = new \DateTimeImmutable();
foreach ($missions as $mission) {
$startTime = $mission->getDate()->sub(new \DateInterval('PT2H'));
$endTime = $mission->getDate()->add(new \DateInterval('PT3H'));

// missions are sorted by date,
// so if we've reached mission that ended before $now there's no point in checking next missions
if ($endTime < $now) {
break;
}

if ($startTime < $now && $endTime > $now) {
return $mission;
}
}

return null;
}

public function getArchivedMissions(): array
{
/** @var MissionDto[] $allMissions */
Expand Down
26 changes: 15 additions & 11 deletions templates/_partial/_page_header.html.twig
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
<div class="row my-3">
<div class="col-12">
<h2>
{% block page_title %}{% endblock %}
</h2>
<h6>
{% block page_subtitle %}{% endblock %}
</h6>
</div>
<div class="col-12 mt-2 text-right">
{% block page_actions %}{% endblock %}
</div>
{% block page_title_row %}
<div class="col-12">
<h2>
{% block page_title %}{% endblock %}
</h2>
<h6>
{% block page_subtitle %}{% endblock %}
</h6>
</div>
{% endblock %}
{% block page_actions_row %}
<div class="col-12 mt-2 text-right">
{% block page_actions %}{% endblock %}
</div>
{% endblock %}
</div>
110 changes: 68 additions & 42 deletions templates/mod_list_public/select.html.twig
Original file line number Diff line number Diff line change
@@ -1,49 +1,75 @@
{% extends 'card.html.twig' %}
{% extends 'container.html.twig' %}

{% block content %}
{% embed '_partial/_page_header.html.twig' %}
{% block page_title %}
{{ 'Mod lists'|trans }}
{% endblock %}
{% endembed %}
{% if nextMission %}
<div class="container card bg-dark mt-4">
{% embed '_partial/_page_header.html.twig' %}
{% block page_title %}
{{ 'Today\'s mission'|trans }}: {{ nextMission.title }}
{% endblock %}
{% block page_subtitle %}

{% embed '_partial/_table.html.twig' %}
{% import '_macro/common_macro.html.twig' as commonMacro %}
{% import '_macro/table_macro.html.twig' as tableMacro %}
{{ 'Download mod list'|trans }}:
{% if nextMissionModList %}
<a href="{{ path('app_mod_list_public_customize', { name: nextMissionModList.name }) }}">
{{ nextMissionModList.name }}
<i class="fas fa-download" ></i>
</a>
{% else %}
{{ nextMission.modlist }}
{% endif %}
{% endblock %}

{% block table_head %}
<tr>
<th scope="col">#</th>
<th scope="col"></th>
<th scope="col" class="w-100">{{ 'Mod list name'|trans }}</th>
<th scope="col">{{ 'Last updated at'|trans }}</th>
<th></th>
</tr>
{% endblock %}
{% block table_body %}
{# @var modList \App\Entity\ModList\ModList #}
{% for modList in modLists %}
<tr data-row-action-url="{{ path('app_mod_list_public_customize', { name: modList.name }) }}">
<th scope="row">{{ loop.index }}</th>
<td>
{% if modList.approved %}
{{ tableMacro.icon('fas fa-check', 'Mod list approved'|trans) }}
{% endif %}
</td>
<td>
{{ modList.name }}
{% block page_actions_row %}{% endblock %}
{% endembed %}
</div>
{% endif %}

{# @var dlc \App\Entity\Dlc\Dlc #}
{% for dlc in modList.dlcs %}
{{ commonMacro.dlc_icon(dlc) }}
{% endfor %}
</td>
<td>{{ tableMacro.format_date(modList.lastUpdatedAt) }}</td>
<td class="text-right">
{{ tableMacro.row_action(path('app_mod_list_public_customize', { name: modList.name }), 'fas fa-download', 'Download mod list'|trans) }}
</td>
<div class="container card bg-dark mt-4">
{% embed '_partial/_page_header.html.twig' %}
{% block page_title %}
{{ 'Mod lists'|trans }}
{% endblock %}
{% endembed %}

{% embed '_partial/_table.html.twig' %}
{% import '_macro/common_macro.html.twig' as commonMacro %}
{% import '_macro/table_macro.html.twig' as tableMacro %}

{% block table_head %}
<tr>
<th scope="col">#</th>
<th scope="col"></th>
<th scope="col" class="w-100">{{ 'Mod list name'|trans }}</th>
<th scope="col">{{ 'Last updated at'|trans }}</th>
<th></th>
</tr>
{% endfor %}
{% endblock %}
{% endembed %}
{% endblock %}
{% block table_body %}
{# @var modList \App\Entity\ModList\ModList #}
{% for modList in modLists %}
<tr data-row-action-url="{{ path('app_mod_list_public_customize', { name: modList.name }) }}">
<th scope="row">{{ loop.index }}</th>
<td>
{% if modList.approved %}
{{ tableMacro.icon('fas fa-check', 'Mod list approved'|trans) }}
{% endif %}
</td>
<td>
{{ modList.name }}

{# @var dlc \App\Entity\Dlc\Dlc #}
{% for dlc in modList.dlcs %}
{{ commonMacro.dlc_icon(dlc) }}
{% endfor %}
</td>
<td>{{ tableMacro.format_date(modList.lastUpdatedAt) }}</td>
<td class="text-right">
{{ tableMacro.row_action(path('app_mod_list_public_customize', { name: modList.name }), 'fas fa-download', 'Download mod list'|trans) }}
</td>
</tr>
{% endfor %}
{% endblock %}
{% endembed %}
</div>
{% endblock %}
130 changes: 130 additions & 0 deletions tests/unit/Service/MissionClientTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php

declare(strict_types=1);

namespace App\Tests\Unit\Service;

use App\Service\Mission\Enum\MissionStateEnum;
use App\Service\Mission\MissionClient;
use App\Service\Mission\MissionStore;
use PHPUnit\Framework\TestCase;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;

/**
* @internal
* @covers \App\Service\Mission\MissionClient
*/
final class MissionClientTest extends TestCase
{
/**
* @test
* @dataProvider provideCurrentMissionsData
*/
public function getCurrentMission(array $missionData, ?string $expectedTitle): void
{
$mockHttpClient = $this->mockHttpClient($missionData);
$mockStore = $this->getMockBuilder(MissionStore::class)
->disableOriginalConstructor()
->getMock()
;

$client = new MissionClient($mockHttpClient, $mockStore, 'https://2137.xd');

$currentMission = $client->getCurrentMission();

if ($expectedTitle) {
static::assertSame($expectedTitle, $currentMission->getTitle());
} else {
static::assertNull($currentMission);
}
}

public function provideCurrentMissionsData(): array
{
$now = new \DateTimeImmutable();
$dateFormat = 'Y-m-d\TH:i:s';

return
[
'current mission available' => [[
[
'id' => 1,
'title' => 'Mission in future 1',
'date' => $now->add(new \DateInterval('P2D'))->format($dateFormat),
'closeDate' => $now->add(new \DateInterval('P1D'))->format($dateFormat),
'description' => '',
'modlistName' => '',
'image' => '',
'freeSlots' => 0,
'allSlots' => 0,
'state' => MissionStateEnum::OPEN,
],
[
'id' => 2,
'title' => 'Current mission',
'date' => $now->add(new \DateInterval('PT1H'))->format($dateFormat),
'closeDate' => $now->sub(new \DateInterval('P1D'))->format($dateFormat),
'description' => '',
'modlistName' => '',
'image' => '',
'freeSlots' => 0,
'allSlots' => 0,
'state' => MissionStateEnum::OPEN,
],
[
'id' => 3,
'title' => 'Old mission 1',
'date' => $now->sub(new \DateInterval('P2D'))->format($dateFormat),
'closeDate' => $now->sub(new \DateInterval('P3D'))->format($dateFormat),
'description' => '',
'modlistName' => '',
'image' => '',
'freeSlots' => 0,
'allSlots' => 0,
'state' => MissionStateEnum::ARCHIVED,
],
], 'Current mission'],
'no current mission' => [[
[
'id' => 1,
'title' => 'Mission in future 1',
'date' => $now->add(new \DateInterval('P2D'))->format($dateFormat),
'closeDate' => $now->add(new \DateInterval('P1D'))->format($dateFormat),
'description' => '',
'modlistName' => '',
'image' => '',
'freeSlots' => 0,
'allSlots' => 0,
'state' => MissionStateEnum::OPEN,
],
[
'id' => 2,
'title' => 'Old mission 1',
'date' => $now->sub(new \DateInterval('P2D'))->format($dateFormat),
'closeDate' => $now->sub(new \DateInterval('P3D'))->format($dateFormat),
'description' => '',
'modlistName' => '',
'image' => '',
'freeSlots' => 0,
'allSlots' => 0,
'state' => MissionStateEnum::ARCHIVED,
],
[], // empty element will explode the deserialization process, so this basically makes sure that we're not iterating more than we should ;)
], null],
'empty data' => [[], null],
];
}

private function mockHttpClient(array $responsePayload): HttpClientInterface
{
$response = $this->createMock(ResponseInterface::class);
$response->method('getStatusCode')->willReturn(200);
$response->method('getContent')->willReturn(json_encode($responsePayload, JSON_THROW_ON_ERROR));

$httpClient = $this->createMock(HttpClientInterface::class);
$httpClient->method('request')->willReturn($response);

return $httpClient;
}
}
Loading

0 comments on commit dac5d5d

Please sign in to comment.