Housekeeping from the code review
Adds wk-sync:purge to remove old files under var/wk-sync (purge() was never reachable), makes stored file names unique within a second, excludes widgets from service autowiring, removes unused repository methods, and adds a planner test covering re-imports with and without member groups. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
995b39e916
commit
8aebfcd28d
9 files changed
+140
-64
No files matched your search
@@ -101,7 +101,7 @@ Regeln beim Import:
|
||||
| `kbkdemail` | `wkOrganizerEmail` | E-Mail **oder** URL (Veranstaltungsart 3: Anmeldung beim Veranstalter) |
|
||||
| `kbvort`, `kbgruppe`, `kbvsart`, `kbztermin` | `location`, `wkAudience`, `wkEventType`, `wkAdditionalDates` | „geschlossener Teilnehmerkreis“ verhindert die Online-Anmeldung |
|
||||
|
||||
Das Euro-Zeichen, das W&K als „Ç“ exportiert, wird im Umfeld von Zahlen korrigiert. Aliase werden nur beim Anlegen erzeugt und bleiben danach stabil. Ein erneuter Import derselben Datei meldet alle Seminare als unverändert. Jeder Import wird protokolliert (W&K-Tools → Protokoll); die Datei liegt unter `var/wk-sync/imports/`.
|
||||
Das Euro-Zeichen, das W&K als „Ç“ exportiert, wird im Umfeld von Zahlen korrigiert. Aliase werden nur beim Anlegen erzeugt und bleiben danach stabil. Ein erneuter Import derselben Datei meldet alle Seminare als unverändert. Jeder Import wird protokolliert (W&K-Tools → Protokoll); die Datei liegt unter `var/wk-sync/imports/`. Alte Import- und Exportdateien räumt `vendor/bin/contao-console wk-sync:purge --days=90` auf (z. B. per Cronjob); die Protokolleinträge bleiben erhalten.
|
||||
|
||||
Auf der Konsole:
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ services:
|
||||
- '../src/Booking/{Availability,BookingFields}.php'
|
||||
- '../src/Inbox/{InboxItem,InboxState}.php'
|
||||
- '../src/Sync/CalendarConfig.php'
|
||||
- '../src/Widget/'
|
||||
|
||||
# Ohne Notification Center werden Anmeldungen gespeichert, aber keine Mails verschickt
|
||||
Mummert\ContaoWkSyncBundle\Booking\BookingNotifierInterface: '@Mummert\ContaoWkSyncBundle\Booking\NullNotifier'
|
||||
|
||||
@@ -59,18 +59,6 @@ class BookingRepository
|
||||
|
||||
return false === $row ? null : $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt es zu dieser E-Mail-Adresse bereits eine offene Anmeldung für die Veranstaltung?
|
||||
*/
|
||||
public function hasOpenBookingForEmail(int $eventId, string $email, int $excludeId = 0): bool
|
||||
{
|
||||
return (int) $this->connection->fetchOne(
|
||||
'SELECT COUNT(*) FROM tl_wk_booking WHERE pid = ? AND email = ? AND id != ? AND state != ?',
|
||||
[$eventId, $email, $excludeId, InboxState::Spam->value],
|
||||
) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Mummert\ContaoWkSyncBundle\Category;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
@@ -76,21 +75,6 @@ class CategoryRepository implements ResetInterface
|
||||
|
||||
return (int) $this->connection->lastInsertId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $ids
|
||||
*
|
||||
* @return list<int> IDs, die tatsächlich existieren
|
||||
*/
|
||||
public function existingIds(array $ids): array
|
||||
{
|
||||
if ([] === $ids) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(intval(...), $this->connection->fetchFirstColumn('SELECT id FROM tl_wk_category WHERE id IN (?)', [$ids], [ArrayParameterType::INTEGER]));
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->cache = null;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mummert\ContaoWkSyncBundle\Command;
|
||||
|
||||
use Mummert\ContaoWkSyncBundle\Import\SyncFileStorage;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(name: 'wk-sync:purge', description: 'Löscht Import- und Exportdateien unter var/wk-sync, die älter als die angegebene Anzahl Tage sind.')]
|
||||
class PurgeCommand extends Command
|
||||
{
|
||||
public function __construct(private readonly SyncFileStorage $storage)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('days', null, InputOption::VALUE_REQUIRED, 'Dateien behalten, die jünger sind als so viele Tage', '90');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$days = max(1, (int) $input->getOption('days'));
|
||||
$deleted = $this->storage->purge($days);
|
||||
|
||||
(new SymfonyStyle($input, $output))->success(\sprintf('%d Datei(en) gelöscht, die älter als %d Tage waren. Protokolleinträge bleiben erhalten.', $deleted, $days));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ namespace Mummert\ContaoWkSyncBundle\Import;
|
||||
use Contao\CoreBundle\Cache\CacheTagManager;
|
||||
use Contao\CoreBundle\Framework\ContaoFramework;
|
||||
use Contao\CoreBundle\Slug\Slug;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Mummert\ContaoWkSyncBundle\Sync\CalendarConfig;
|
||||
|
||||
@@ -145,16 +144,4 @@ class ImportExecutor
|
||||
fn (string $alias): bool => (int) $this->connection->fetchOne('SELECT COUNT(*) FROM tl_calendar_events WHERE alias = ?', [$alias]) > 0,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $ids
|
||||
*/
|
||||
public function eventsExist(array $ids): bool
|
||||
{
|
||||
if ([] === $ids) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (int) $this->connection->fetchOne('SELECT COUNT(*) FROM tl_calendar_events WHERE id IN (?)', [$ids], [ArrayParameterType::INTEGER]) === \count(array_unique($ids));
|
||||
}
|
||||
}
|
||||
@@ -104,6 +104,6 @@ class SyncFileStorage
|
||||
$base = preg_replace('/[^A-Za-z0-9_-]+/', '_', $base) ?? 'datei';
|
||||
$base = trim(substr($base, 0, 60), '_') ?: 'datei';
|
||||
|
||||
return date('Ymd-His').'_'.$base.'.'.$extension;
|
||||
return date('Ymd-His').'_'.bin2hex(random_bytes(3)).'_'.$base.'.'.$extension;
|
||||
}
|
||||
}
|
||||
@@ -42,25 +42,4 @@ class CalendarConfigRepository
|
||||
|
||||
return array_map(CalendarConfig::fromRow(...), $rows);
|
||||
}
|
||||
|
||||
public function findForEvent(int $eventId): CalendarConfig|null
|
||||
{
|
||||
$calendarId = $this->connection->fetchOne('SELECT pid FROM tl_calendar_events WHERE id = ?', [$eventId]);
|
||||
|
||||
return false === $calendarId ? null : $this->find((int) $calendarId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<CalendarConfig>
|
||||
*/
|
||||
public function findByBookingForm(int $formId): array
|
||||
{
|
||||
if ($formId < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $this->connection->fetchAllAssociative(self::SELECT.' WHERE c.bookingForm = ?', [$formId]);
|
||||
|
||||
return array_map(CalendarConfig::fromRow(...), $rows);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mummert\ContaoWkSyncBundle\Tests\Import;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Mummert\ContaoWkSyncBundle\Category\Category;
|
||||
use Mummert\ContaoWkSyncBundle\Category\CategoryKind;
|
||||
use Mummert\ContaoWkSyncBundle\Category\CategoryRepository;
|
||||
use Mummert\ContaoWkSyncBundle\Category\CategoryResolver;
|
||||
use Mummert\ContaoWkSyncBundle\Import\CsvTable;
|
||||
use Mummert\ContaoWkSyncBundle\Import\ImportPlanner;
|
||||
use Mummert\ContaoWkSyncBundle\Import\MemberResolver;
|
||||
use Mummert\ContaoWkSyncBundle\Import\PlannedEvent;
|
||||
use Mummert\ContaoWkSyncBundle\Import\RowMapper;
|
||||
use Mummert\ContaoWkSyncBundle\Sync\CalendarConfig;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ImportPlannerTest extends TestCase
|
||||
{
|
||||
public function testReimportWithoutMemberGroupsIsUnchanged(): void
|
||||
{
|
||||
$plan = $this->plan(existingInstructor: 0, existingSpeakers: [], memberIds: [], instructorGroup: 0, speakerGroup: 0);
|
||||
|
||||
self::assertSame(PlannedEvent::UNCHANGED, $plan->events[0]->action);
|
||||
self::assertSame([], $plan->events[0]->changes);
|
||||
}
|
||||
|
||||
public function testResolvedMembersAreComparedById(): void
|
||||
{
|
||||
$plan = $this->plan(existingInstructor: 11, existingSpeakers: [12], memberIds: ['Anna Muster' => 11, 'Bernd Beispiel' => 12], instructorGroup: 0, speakerGroup: 0);
|
||||
|
||||
self::assertSame(PlannedEvent::UNCHANGED, $plan->events[0]->action);
|
||||
}
|
||||
|
||||
public function testMemberGroupsPlanMemberCreation(): void
|
||||
{
|
||||
$plan = $this->plan(existingInstructor: 0, existingSpeakers: [], memberIds: [], instructorGroup: 4, speakerGroup: 5);
|
||||
|
||||
self::assertSame(PlannedEvent::UPDATE, $plan->events[0]->action);
|
||||
self::assertSame(['wkInstructor', 'wkSpeakers'], array_keys($plan->events[0]->changes));
|
||||
self::assertSame(['', 'Anna Muster'], $plan->events[0]->changes['wkInstructor']);
|
||||
}
|
||||
|
||||
public function testChangedTitleIsAnUpdate(): void
|
||||
{
|
||||
$plan = $this->plan(existingInstructor: 0, existingSpeakers: [], memberIds: [], instructorGroup: 0, speakerGroup: 0, existingOverrides: ['title' => 'Alter Titel']);
|
||||
|
||||
self::assertSame(PlannedEvent::UPDATE, $plan->events[0]->action);
|
||||
self::assertSame(['Alter Titel', 'Testseminar'], $plan->events[0]->changes['title']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int> $memberIds Name => Mitglieds-ID (nur diese Namen sind Mitglieder)
|
||||
* @param list<int> $existingSpeakers
|
||||
* @param array<string, mixed> $existingOverrides
|
||||
*/
|
||||
private function plan(int $existingInstructor, array $existingSpeakers, array $memberIds, int $instructorGroup, int $speakerGroup, array $existingOverrides = []): \Mummert\ContaoWkSyncBundle\Import\ImportPlan
|
||||
{
|
||||
$row = [
|
||||
'kbnr' => '0001234',
|
||||
'kbvsnr' => 'ABC-1',
|
||||
'kbtitel' => 'Testseminar',
|
||||
'kbanreise' => '01.03.2027',
|
||||
'kbabreise' => '02.03.2027',
|
||||
'kbstatus1' => '2',
|
||||
'kbsgebiet' => 'Grundschule',
|
||||
'kbkln1' => 'Anna Muster',
|
||||
'kbmref' => "\nBernd Beispiel",
|
||||
];
|
||||
|
||||
$mapped = (new RowMapper())->map($row, 2);
|
||||
$existing = [
|
||||
...$mapped->data,
|
||||
...$existingOverrides,
|
||||
'id' => 7,
|
||||
'published' => 1,
|
||||
'wkCategories' => serialize(['3']),
|
||||
'wkCategoryPrimary' => 3,
|
||||
'wkInstructor' => $existingInstructor,
|
||||
'wkSpeakers' => serialize(array_map(strval(...), $existingSpeakers)),
|
||||
];
|
||||
|
||||
$connection = $this->createStub(Connection::class);
|
||||
$connection->method('fetchAllAssociative')->willReturn([$existing]);
|
||||
|
||||
$categories = $this->createStub(CategoryRepository::class);
|
||||
$categories->method('findAll')->willReturn([new Category(3, 'Grundschule', 'grundschule', CategoryKind::School, ['Grundschule'], 16)]);
|
||||
|
||||
$members = $this->createStub(MemberResolver::class);
|
||||
$members->method('findId')->willReturnCallback(static fn (string $name): int|null => $memberIds[$name] ?? null);
|
||||
$members->method('namesByIds')->willReturnCallback(static fn (array $ids): array => array_filter(array_flip($memberIds), static fn (int $id): bool => \in_array($id, $ids, true)));
|
||||
|
||||
$planner = new ImportPlanner($connection, new RowMapper(), $categories, new CategoryResolver($categories), $members);
|
||||
$calendar = new CalendarConfig(1, 1, 'Kalender', 'Einstellungen', 0, true, 2, 8, 9, 0, 1, 31, ['TPI Moritzburg'], 1, $instructorGroup, $speakerGroup, [], 0, '', 0, []);
|
||||
|
||||
return $planner->plan(new CsvTable(array_keys($row), [$row + ['__record' => '2']], 'UTF-8'), $calendar);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user