Initial import of animal_management module

This commit is contained in:
Kelin Rescue Hub
2026-04-04 13:13:00 -04:00
commit 20adb1bd1e
65 changed files with 14004 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
<?php
namespace humhub\modules\animal_management\services;
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\AnimalMedicalVisit;
use humhub\modules\animal_management\models\AnimalProgressUpdate;
use humhub\modules\animal_management\models\AnimalStreamEntry;
use humhub\modules\space\models\Space;
use Yii;
class AnimalStreamPublisherService
{
public static function publishMedicalVisit(Animal $animal, AnimalMedicalVisit $visit): void
{
if (!self::streamTableExists()) {
return;
}
$exists = AnimalStreamEntry::find()->where([
'entry_type' => AnimalStreamEntry::TYPE_MEDICAL,
'medical_visit_id' => (int)$visit->id,
])->exists();
if ($exists) {
return;
}
self::publishEntry($animal, AnimalStreamEntry::TYPE_MEDICAL, (int)$visit->id, null);
}
public static function publishProgressUpdate(Animal $animal, AnimalProgressUpdate $update): void
{
if (!self::streamTableExists()) {
return;
}
$exists = AnimalStreamEntry::find()->where([
'entry_type' => AnimalStreamEntry::TYPE_PROGRESS,
'progress_update_id' => (int)$update->id,
])->exists();
if ($exists) {
return;
}
self::publishEntry($animal, AnimalStreamEntry::TYPE_PROGRESS, null, (int)$update->id);
}
private static function publishEntry(Animal $animal, string $entryType, ?int $medicalVisitId, ?int $progressUpdateId): void
{
try {
$space = Space::findOne(['contentcontainer_id' => (int)$animal->contentcontainer_id]);
if (!$space instanceof Space) {
return;
}
$entry = new AnimalStreamEntry();
$entry->animal_id = (int)$animal->id;
$entry->entry_type = $entryType;
$entry->medical_visit_id = $medicalVisitId;
$entry->progress_update_id = $progressUpdateId;
$entry->content->container = $space;
if (!$entry->save()) {
Yii::warning([
'message' => 'Could not save animal stream entry.',
'animal_id' => (int)$animal->id,
'entry_type' => $entryType,
'errors' => $entry->getErrors(),
], 'animal_management.stream_publish');
}
} catch (\Throwable $e) {
Yii::warning([
'message' => 'Unexpected error while publishing animal stream entry.',
'animal_id' => (int)$animal->id,
'entry_type' => $entryType,
'exception' => $e->getMessage(),
], 'animal_management.stream_publish');
}
}
private static function streamTableExists(): bool
{
return Yii::$app->db->schema->getTableSchema('rescue_animal_stream_entry', true) !== null;
}
}

View File

@@ -0,0 +1,343 @@
<?php
namespace humhub\modules\animal_management\services;
use humhub\modules\animal_management\models\Animal;
use humhub\modules\animal_management\models\AnimalGalleryItem;
use humhub\modules\animal_management\models\AnimalGalleryLink;
use humhub\modules\gallery\models\CustomGallery;
use humhub\modules\space\models\Space;
use Yii;
class GalleryIntegrationService
{
public static function canSyncForSpace(Space $space): bool
{
if (!$space->moduleManager->isEnabled('animal_management')) {
return false;
}
if (!$space->moduleManager->isEnabled('gallery')) {
return false;
}
return self::hasRequiredTables();
}
public static function syncSpaceAnimalGalleries(Space $space): void
{
if (!self::canSyncForSpace($space)) {
return;
}
$animals = Animal::find()->where(['contentcontainer_id' => (int)$space->contentcontainer_id])->all();
$synced = 0;
foreach ($animals as $animal) {
if ($animal instanceof Animal) {
if (self::ensureAnimalGallery($animal, $space) instanceof CustomGallery) {
$synced++;
}
}
}
Yii::warning([
'space_id' => (int)$space->id,
'contentcontainer_id' => (int)$space->contentcontainer_id,
'animal_count' => count($animals),
'synced_count' => $synced,
'has_link_table' => self::hasLinkTable(),
], 'animal_management.gallery_integration.sync');
}
public static function ensureAnimalGallery(Animal $animal, ?Space $space = null): ?CustomGallery
{
if (!self::hasRequiredTables()) {
return null;
}
$space = $space ?: Space::findOne(['contentcontainer_id' => (int)$animal->contentcontainer_id]);
if (!$space instanceof Space || !self::canSyncForSpace($space)) {
return null;
}
$link = self::hasLinkTable() ? AnimalGalleryLink::findOne(['animal_id' => (int)$animal->id]) : null;
$gallery = null;
if ($link instanceof AnimalGalleryLink) {
$gallery = CustomGallery::find()
->contentContainer($space)
->where(['gallery_gallery.id' => (int)$link->gallery_id])
->one();
}
if (!$gallery instanceof CustomGallery) {
$gallery = CustomGallery::find()
->contentContainer($space)
->where(['like', 'description', self::buildMarker((int)$animal->id)])
->one();
if (!$gallery instanceof CustomGallery) {
$gallery = CustomGallery::find()
->contentContainer($space)
->where(['title' => self::buildTitle($animal)])
->one();
}
}
if (!$gallery instanceof CustomGallery) {
$gallery = new CustomGallery();
$gallery->content->container = $space;
$gallery->content->visibility = (int)$space->getDefaultContentVisibility();
$gallery->title = self::buildTitle($animal);
$gallery->description = self::buildDescription($animal);
try {
if (!$gallery->save()) {
Yii::warning($gallery->getErrors(), 'animal_management.gallery_integration.gallery_create_failed');
return null;
}
} catch (\Throwable $e) {
Yii::warning($e->getMessage(), 'animal_management.gallery_integration.gallery_create_failed');
return null;
}
} else {
$newTitle = self::buildTitle($animal);
$newDescription = self::buildDescription($animal);
$dirty = false;
if ((string)$gallery->title !== $newTitle) {
$gallery->title = $newTitle;
$dirty = true;
}
if ((string)$gallery->description !== $newDescription) {
$gallery->description = $newDescription;
$dirty = true;
}
if ($dirty) {
try {
$gallery->save();
} catch (\Throwable $e) {
Yii::warning($e->getMessage(), 'animal_management.gallery_integration.gallery_update_failed');
}
}
}
if (self::hasLinkTable()) {
if (!$link instanceof AnimalGalleryLink) {
$link = new AnimalGalleryLink();
$link->animal_id = (int)$animal->id;
}
$link->gallery_id = (int)$gallery->id;
$link->contentcontainer_id = (int)$space->contentcontainer_id;
try {
if (!$link->save()) {
Yii::warning($link->getErrors(), 'animal_management.gallery_integration.link_save_failed');
}
} catch (\Throwable $e) {
Yii::warning($e->getMessage(), 'animal_management.gallery_integration.link_save_failed');
}
}
self::refreshGalleryThumb($animal, $gallery);
return $gallery;
}
public static function isAnimalBackedGallery(CustomGallery $gallery): bool
{
return self::getAnimalByGalleryId((int)$gallery->id) instanceof Animal;
}
public static function getAnimalByGalleryId(int $galleryId): ?Animal
{
if (!self::hasRequiredTables()) {
return null;
}
if (self::hasLinkTable()) {
$link = AnimalGalleryLink::findOne(['gallery_id' => $galleryId]);
if ($link instanceof AnimalGalleryLink) {
return Animal::findOne(['id' => (int)$link->animal_id]);
}
}
$gallery = CustomGallery::findOne(['id' => $galleryId]);
if (!$gallery instanceof CustomGallery) {
return null;
}
$animalId = self::extractAnimalIdFromDescription((string)$gallery->description);
if ($animalId === null) {
$animalId = self::extractAnimalIdFromTitle($gallery);
if ($animalId === null) {
return null;
}
}
return Animal::findOne(['id' => $animalId]);
}
public static function getAnimalItemsQuery(CustomGallery $gallery)
{
$animal = self::getAnimalByGalleryId((int)$gallery->id);
if (!$animal instanceof Animal) {
Yii::warning([
'gallery_id' => (int)$gallery->id,
'title' => (string)$gallery->title,
'description' => (string)$gallery->description,
], 'animal_management.gallery_integration.gallery_unmapped');
return null;
}
return AnimalGalleryItem::find()
->where(['animal_id' => (int)$animal->id])
->andWhere([
'or',
['and', ['not', ['file_path' => null]], ['!=', 'file_path', '']],
['not', ['file_id' => null]],
])
->orderBy(['id' => SORT_DESC]);
}
public static function getAnimalPreviewUrl(CustomGallery $gallery): ?string
{
$animal = self::getAnimalByGalleryId((int)$gallery->id);
if (!$animal instanceof Animal) {
return null;
}
$item = AnimalGalleryItem::find()
->where(['animal_id' => (int)$animal->id])
->orderBy(['id' => SORT_DESC])
->one();
if (!$item instanceof AnimalGalleryItem) {
return null;
}
$url = trim((string)$item->getImageUrl());
return $url !== '' ? $url : null;
}
public static function isAnimalGalleryEmpty(CustomGallery $gallery): ?bool
{
$animal = self::getAnimalByGalleryId((int)$gallery->id);
if (!$animal instanceof Animal) {
return null;
}
return !AnimalGalleryItem::find()->where(['animal_id' => (int)$animal->id])->exists();
}
private static function refreshGalleryThumb(Animal $animal, CustomGallery $gallery): void
{
$thumbFileId = AnimalGalleryItem::find()
->where(['animal_id' => (int)$animal->id])
->andWhere(['not', ['file_id' => null]])
->orderBy(['id' => SORT_DESC])
->select('file_id')
->scalar();
$thumbFileId = $thumbFileId === null ? null : (int)$thumbFileId;
if ((int)($gallery->thumb_file_id ?? 0) === (int)($thumbFileId ?? 0)) {
return;
}
$gallery->thumb_file_id = $thumbFileId;
$gallery->save(false, ['thumb_file_id']);
}
private static function hasRequiredTables(): bool
{
$schema = Yii::$app->db->schema;
return $schema->getTableSchema('rescue_animal', true) !== null
&& $schema->getTableSchema('rescue_animal_gallery_item', true) !== null
&& $schema->getTableSchema('gallery_gallery', true) !== null;
}
private static function hasLinkTable(): bool
{
return Yii::$app->db->schema->getTableSchema('rescue_animal_gallery_link', true) !== null;
}
private static function buildTitle(Animal $animal): string
{
return $animal->getDisplayName() . ' Gallery';
}
private static function buildDescription(Animal $animal): string
{
return self::buildMarker((int)$animal->id);
}
private static function buildMarker(int $animalId): string
{
return '[animal-gallery:' . $animalId . ']';
}
private static function extractAnimalIdFromDescription(string $description): ?int
{
if (!preg_match('/\[animal-gallery:(\d+)\]/', $description, $matches)) {
return null;
}
return isset($matches[1]) ? (int)$matches[1] : null;
}
private static function extractAnimalIdFromTitle(CustomGallery $gallery): ?int
{
$container = $gallery->content->container ?? null;
if (!$container instanceof Space) {
return null;
}
$title = trim((string)$gallery->title);
if ($title === '') {
return null;
}
$titleNormalized = self::normalizeTitleToken($title);
if ($titleNormalized === '') {
return null;
}
$animals = Animal::find()
->where(['contentcontainer_id' => (int)$container->contentcontainer_id])
->orderBy(['id' => SORT_ASC])
->all();
foreach ($animals as $animal) {
if (!$animal instanceof Animal) {
continue;
}
$nameNormalized = self::normalizeTitleToken((string)$animal->name);
if ($nameNormalized !== '' && ($nameNormalized === $titleNormalized || strpos($titleNormalized, $nameNormalized) !== false)) {
return (int)$animal->id;
}
$uidNormalized = self::normalizeTitleToken((string)$animal->animal_uid);
if ($uidNormalized !== '' && ($uidNormalized === $titleNormalized || strpos($titleNormalized, $uidNormalized) !== false)) {
return (int)$animal->id;
}
}
return null;
}
private static function normalizeTitleToken(string $value): string
{
$value = strtolower(trim($value));
if ($value === '') {
return '';
}
$value = preg_replace('/\bgallery\b/i', ' ', $value);
$value = preg_replace('/[^a-z0-9]+/i', ' ', (string)$value);
return trim((string)$value);
}
}