| | |
| | | <?php |
| | | namespace JVBase\managers\queue\executors; |
| | | |
| | | use JVBase\managers\CacheManager; |
| | | use JVBase\managers\queue\{Executor, Operation, Progress, Result}; |
| | | use JVBase\meta\MetaManager; |
| | | use JVBase\utility\Features; |
| | | use JVBase\managers\queue\{Executor, Operation, Progress, Result, Storage}; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | use Exception; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | { |
| | | private const HANDLED_TYPES = [ |
| | | 'content_update', |
| | | 'batch_creation', |
| | | // 'batch_creation', |
| | | ]; |
| | | |
| | | private int $userId; |
| | | private string $postType; |
| | | private array $fields = []; |
| | | private array $timelineSharedFields = []; |
| | | private array $timelineUniqueFields = []; |
| | | |
| | | public function execute(Operation $operation, Progress $progress): Result |
| | | { |
| | | $this->userId = $operation->userId; |
| | | |
| | | if (!in_array($operation->type, self::HANDLED_TYPES)) { |
| | | throw new Exception("ContentExecutor cannot handle type: {$operation->type}"); |
| | | } |
| | | |
| | | $this->userId = $operation->userId; |
| | | error_log('Executing ContentExecutor.php'); |
| | | |
| | | try { |
| | | $data = $operation->requestData; |
| | | |
| | | $result = match($operation->type) { |
| | | return match($operation->type) { |
| | | 'content_update' => $this->processContentUpdate($operation, $data, $progress), |
| | | 'batch_creation' => $this->processBatchCreation($operation, $data, $progress), |
| | | default => throw new Exception("Unknown type: {$operation->type}") |
| | | }; |
| | | |
| | | return $result; |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | '[ContentExecutor]:execute', |
| | |
| | | |
| | | if (empty($posts)) { |
| | | return new Result( |
| | | outcome: 'failed', |
| | | outcome: 'success', |
| | | result: ['message' => 'No posts to update'] |
| | | ); |
| | | } |
| | | |
| | | $results = []; |
| | | $results = [ |
| | | 'errors' => [], |
| | | 'success' => [], |
| | | 'newPosts' => [], |
| | | 'timelineParents' => [], |
| | | 'timelineStatus' => [], |
| | | 'timelineSharedFields' => [], |
| | | ]; |
| | | $errors = []; |
| | | |
| | | $updateTimelineOrder = []; |
| | | |
| | | foreach ($posts as $id => $postData) { |
| | | try { |
| | | $content = $postData['content'] ?? ''; |
| | | |
| | | // New post creation |
| | | if (str_starts_with((string)$id, 'new')) { |
| | | $newId = wp_insert_post([ |
| | | 'post_author' => $this->userId, |
| | | 'post_type' => jvbCheckBase($content), |
| | | 'post_title' => $postData['post_title'] ?? '', |
| | | 'post_status' => $postData['status'] ?? 'draft', |
| | | ]); |
| | | |
| | | if (!$newId || is_wp_error($newId)) { |
| | | $progress->failItem($id, 'Could not create post'); |
| | | continue; |
| | | } |
| | | |
| | | $this->savePostFields($newId, $postData); |
| | | $results[$id] = ['success' => true, 'new_id' => $newId]; |
| | | |
| | | if (Features::forContent($content)->has('is_timeline')) { |
| | | $this->updateTimelineLatestDate($newId); |
| | | } |
| | | |
| | | $progress->advance(1); |
| | | continue; |
| | | $content = $postData['content'] ?? false; |
| | | if (!$content) continue; |
| | | $registrar = Registrar::getInstance($content); |
| | | switch ($registrar->getType()) { |
| | | case 'post': |
| | | $results = $this->handlePost($id, $postData, $registrar, $results, $progress); |
| | | break; |
| | | case 'term': |
| | | $results = $this->handleTerm($id, $postData, $registrar, $results, $progress); |
| | | break; |
| | | case 'user': |
| | | $results = $this->handleUser($id, $postData, $registrar, $results, $progress); |
| | | break; |
| | | } |
| | | |
| | | // Existing post update |
| | | if (!$this->verifyOwnership((int)$id)) { |
| | | $progress->failItem($id, 'No permission to modify this post'); |
| | | continue; |
| | | } |
| | | // Check if this is a timeline post |
| | | $isTimeline = Features::forContent($content)->has('is_timeline'); |
| | | |
| | | if ($isTimeline) { |
| | | $post = get_post((int)$id); |
| | | $parentId = $post->post_parent; |
| | | $isParent = ($parentId === 0); |
| | | |
| | | // Track timeline reordering only if date changed |
| | | if (array_key_exists('post_date', $postData)) { |
| | | $timelineRoot = $isParent ? (int)$id : $parentId; |
| | | if (!in_array($timelineRoot, $updateTimelineOrder)) { |
| | | $updateTimelineOrder[] = $timelineRoot; |
| | | } |
| | | } |
| | | |
| | | // Update shared fields if this is the parent |
| | | if ($isParent) { |
| | | $this->initTimelineFields($content); |
| | | $sharedFieldsUpdated = array_filter($postData, function($key) { |
| | | return in_array($key, $this->timelineSharedFields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | if (!empty($sharedFieldsUpdated)) { |
| | | $this->updateSharedFields((int)$id, $sharedFieldsUpdated); |
| | | } |
| | | } |
| | | } |
| | | $this->processPostUpdate((int)$id, $postData); |
| | | |
| | | if (Features::forContent($content)->has('is_timeline') |
| | | && array_key_exists('post_date', $postData)) { |
| | | $post = get_post((int)$id); |
| | | $parentId = $post->post_parent === 0 ? (int)$id : $post->post_parent; |
| | | $this->updateTimelineLatestDate($parentId); |
| | | } |
| | | |
| | | $results[$id] = ['success' => true]; |
| | | $progress->advance(1); |
| | | |
| | | // Clear caches |
| | | CacheManager::for($content)->clear(); |
| | | if (jvbSiteUsesFeedBlock()) { |
| | | CacheManager::for('feed')->clear(); |
| | | } |
| | | |
| | | } catch (Exception $e) { |
| | | $progress->failItem($id, $e->getMessage()); |
| | | $errors[$id] = $e->getMessage(); |
| | | $results['errors'][$id] = $e->getMessage(); |
| | | } |
| | | } |
| | | if (!empty($updateTimelineOrder)) { |
| | | foreach ($updateTimelineOrder as $parentID) { |
| | | $this->reorderTimelineByDate($parentID); |
| | | error_log('Final Results: '.print_r($results, true)); |
| | | |
| | | try { |
| | | if (!empty($results['timelineSharedFields'])) { |
| | | $this->checkSharedFields($results['timelineSharedFields']); |
| | | } |
| | | if (!empty($results['timelineStatus'])) { |
| | | $this->handleTimelineStatusChange($results['timelineStatus']); |
| | | } |
| | | if (!empty($results['timelineParents'])) { |
| | | $this->maybeReorderTimelines($results['timelineParents']); |
| | | } |
| | | } catch (Exception $e) { |
| | | $results['errors'][] = $e->getMessage(); |
| | | } |
| | | |
| | | |
| | | // Send notification |
| | | if (jvbSiteHasNotifications()) { |
| | | JVB()->notification()->addNotification( |
| | | $this->userId, |
| | | 'content_update_complete', |
| | | null, |
| | | 'Content updates completed!' |
| | | ); |
| | | } |
| | | // if (jvbSiteHasNotifications()) { |
| | | // JVB()->notification()->addNotification( |
| | | // $this->userId, |
| | | // 'content_update_complete', |
| | | // null, |
| | | // 'Content updates completed!' |
| | | // ); |
| | | // } |
| | | |
| | | $outcome = 'success'; |
| | | if (!empty($errors)) { |
| | |
| | | |
| | | return new Result( |
| | | outcome: $outcome, |
| | | result: $results |
| | | result: $results, |
| | | ); |
| | | } |
| | | |
| | | private function processPostUpdate(int $postId, array $postData): void |
| | | { |
| | | $content = $postData['content'] ?? ''; |
| | | |
| | | // Handle status changes |
| | | if (isset($postData['post_status'])) { |
| | | switch ($postData['post_status']) { |
| | | case 'publish': |
| | | if (user_can($this->userId, 'manage_options') || user_can($this->userId, 'skip_moderation')) { |
| | | wp_update_post(['ID' => $postId, 'post_status' => 'publish']); |
| | | } |
| | | unset($postData['post_status']); |
| | | break; |
| | | case 'draft': |
| | | wp_update_post(['ID' => $postId, 'post_status' => 'draft']); |
| | | unset($postData['post_status']); |
| | | break; |
| | | case 'trash': |
| | | wp_trash_post($postId); |
| | | return; |
| | | case 'delete': |
| | | wp_delete_post($postId, true); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | // Save all fields via MetaManager (handles post fields too) |
| | | $this->savePostFields($postId, $postData); |
| | | } |
| | | |
| | | private function updateSharedFields(int $parentId, array $sharedFields): void |
| | | { |
| | | // Get all posts in timeline |
| | | $children = get_posts([ |
| | | 'post_type' => get_post_type($parentId), |
| | | 'post_parent' => $parentId, |
| | | 'post_status' => ['publish', 'draft'], |
| | | 'posts_per_page' => -1, |
| | | 'fields' => 'ids' |
| | | ]); |
| | | |
| | | $allPostIds = array_merge([$parentId], $children); |
| | | |
| | | // Apply shared fields to all posts |
| | | foreach ($allPostIds as $timelinePostId) { |
| | | $meta = new MetaManager($timelinePostId, 'post'); |
| | | $meta->setAll($sharedFields); |
| | | } |
| | | } |
| | | |
| | | private function reorderTimelineByDate(int $parentId): void |
| | | { |
| | | $parent = get_post($parentId); |
| | | if (!$parent) return; |
| | | |
| | | // Get all posts in this timeline (parent + children) |
| | | $children = get_posts([ |
| | | 'post_type' => get_post_type($parentId), |
| | | 'post_parent' => $parentId, |
| | | 'post_status' => ['publish', 'draft'], |
| | | 'posts_per_page' => -1, |
| | | 'orderby' => 'date', |
| | | 'order' => 'ASC' |
| | | ]); |
| | | |
| | | // Combine and sort by post_date |
| | | $allPosts = array_merge([$parent], $children); |
| | | usort($allPosts, function($a, $b) { |
| | | return strtotime($a->post_date) <=> strtotime($b->post_date); |
| | | }); |
| | | |
| | | $newParent = $allPosts[0]; |
| | | |
| | | // If parent changed, restructure |
| | | if ($newParent->ID !== $parentId) { |
| | | wp_update_post([ |
| | | 'ID' => $newParent->ID, |
| | | 'post_parent' => 0, |
| | | 'menu_order' => 0 |
| | | ]); |
| | | |
| | | wp_update_post([ |
| | | 'ID' => $parentId, |
| | | 'post_parent' => $newParent->ID |
| | | ]); |
| | | |
| | | foreach ($allPosts as $index => $post) { |
| | | if ($index === 0) continue; |
| | | |
| | | wp_update_post([ |
| | | 'ID' => $post->ID, |
| | | 'post_parent' => $newParent->ID, |
| | | 'menu_order' => $index |
| | | ]); |
| | | |
| | | $this->getOrCreateTerm($post->ID, (string)$index, 'number'); |
| | | } |
| | | } else { |
| | | // Just update menu_order |
| | | foreach ($allPosts as $index => $post) { |
| | | if ($index === 0) continue; |
| | | |
| | | wp_update_post([ |
| | | 'ID' => $post->ID, |
| | | 'menu_order' => $index |
| | | ]); |
| | | |
| | | $this->getOrCreateTerm($post->ID, (string)$index, 'number'); |
| | | } |
| | | } |
| | | |
| | | // Calculate and set timeline taxonomy (time since previous post) |
| | | $previousPost = null; |
| | | foreach ($allPosts as $index => $post) { |
| | | if ($index === 0) { |
| | | // Parent post - no timeline term (it's the baseline) |
| | | wp_set_object_terms($post->ID, [], BASE . 'timeline', false); |
| | | $previousPost = $post; |
| | | continue; |
| | | } |
| | | |
| | | $timelineTerm = $this->calculateTimelineTerm($previousPost, $post); |
| | | if ($timelineTerm) { |
| | | $this->getorCreateTerm($post->ID, $timelineTerm, 'timeline'); |
| | | } |
| | | |
| | | $previousPost = $post; |
| | | } |
| | | |
| | | $this->updateTimelineLatestDate($newParent->ID); |
| | | } |
| | | |
| | | private function updateTimelineLatestDate(int $parentId): void |
| | | { |
| | | $parent = get_post($parentId); |
| | | if (!$parent) return; |
| | | |
| | | // Get all posts in timeline |
| | | $children = get_posts([ |
| | | 'post_type' => get_post_type($parentId), |
| | | 'post_parent' => $parentId, |
| | | 'post_status' => ['publish', 'draft'], |
| | | 'posts_per_page' => -1, |
| | | 'orderby' => 'date', |
| | | 'order' => 'DESC', // Get newest first |
| | | 'fields' => 'ids' |
| | | ]); |
| | | |
| | | $allPostIds = array_merge([$parentId], $children); |
| | | |
| | | // Get all timestamps |
| | | $timestamps = array_map(function($id) { |
| | | $post = get_post($id); |
| | | return $post ? strtotime($post->post_date) : 0; |
| | | }, $allPostIds); |
| | | |
| | | $latestTimestamp = max($timestamps); |
| | | |
| | | // Store as UNIX timestamp |
| | | update_post_meta($parentId, BASE . 'latest_date', $latestTimestamp); |
| | | } |
| | | |
| | | private function calculateTimelineTerm(\WP_Post $previousPost, \WP_Post $currentPost): ?string |
| | | { |
| | | $previousDate = strtotime($previousPost->post_date); |
| | | $currentDate = strtotime($currentPost->post_date); |
| | | |
| | | if (!$previousDate || !$currentDate || $currentDate <= $previousDate) { |
| | | return null; |
| | | } |
| | | |
| | | // Calculate difference in days |
| | | $daysDiff = floor(($currentDate - $previousDate) / (60 * 60 * 24)); |
| | | |
| | | // Convert to weeks |
| | | $weeks = floor($daysDiff / 7); |
| | | |
| | | // If less than 16 weeks, use weeks |
| | | if ($weeks < 16) { |
| | | if ($weeks === 0) { |
| | | return null; // Same week, no term |
| | | } |
| | | return $weeks === 1 ? '1 Week' : $weeks . ' Weeks'; |
| | | } |
| | | |
| | | // 16+ weeks, calculate months |
| | | // Using actual month calculation rather than weeks/4 |
| | | $previousDateTime = new \DateTime($previousPost->post_date); |
| | | $currentDateTime = new \DateTime($currentPost->post_date); |
| | | $interval = $previousDateTime->diff($currentDateTime); |
| | | |
| | | $months = ($interval->y * 12) + $interval->m; |
| | | |
| | | if ($months === 0) { |
| | | // Edge case: technically less than a full month but 16+ weeks |
| | | return $weeks . ' Weeks'; |
| | | } |
| | | |
| | | return $months === 1 ? '1 Month' : $months . ' Months'; |
| | | } |
| | | |
| | | private function getOrCreateTerm(int $postID, string $termName, string $taxonomy): void |
| | | { |
| | | $taxonomy = jvbCheckBase($taxonomy); |
| | | $term = get_term_by('name', $termName, $taxonomy); |
| | | |
| | | if (!$term) { |
| | | $result = wp_insert_term($termName, $taxonomy); |
| | | if (is_wp_error($result)) { |
| | | return; |
| | | } |
| | | $termID = $result['term_id']; |
| | | } else { |
| | | $termID = $term->term_id; |
| | | } |
| | | |
| | | if ($termID) { |
| | | wp_set_object_terms($postID, [$termID], $taxonomy, false); |
| | | } |
| | | } |
| | | |
| | | private function savePostFields(int $postId, array $postData): bool |
| | | { |
| | | $content = $postData['content'] ?? ''; |
| | | $fields = jvbGetFields($content); |
| | | $fields = Registrar::getFieldsFor($content); |
| | | |
| | | $allowedFields = array_filter($postData, function ($key) use ($fields) { |
| | | return array_key_exists($key, $fields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | //Remove values that are already saved |
| | | $check = Meta::forPost($postId)->getAll(array_keys($allowedFields)); |
| | | error_log('Stored values: '.print_r($check, true)); |
| | | $allowedFields = array_filter($allowedFields, function ($key) use ($allowedFields, $check) { |
| | | return $allowedFields[$key] !== $check[$key]; |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | if (empty($allowedFields)) { |
| | | return true; |
| | | } |
| | | |
| | | $meta = new MetaManager($postId, 'post'); |
| | | return $meta->setAll($allowedFields); |
| | | return Meta::forPost($postId) |
| | | ->setAll($allowedFields); |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Batch Creation |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | private function processBatchCreation(Operation $operation, array $data, Progress $progress): Result |
| | | private function saveTermFields(int $termId, array $data): bool |
| | | { |
| | | $this->postType = BASE . $data['content']; |
| | | $content = $data['content'] ?? ''; |
| | | error_log('Saving term fields: '.print_r($data, true)); |
| | | $fields = Registrar::getFieldsFor($content); |
| | | |
| | | // Get upload results from dependency |
| | | $uploadOpId = $operation->id . '_upload'; |
| | | $images = JVB()->queue()->get($uploadOpId)?->result ?? null; |
| | | $allowedFields = array_filter($data, function ($key) use ($fields) { |
| | | return array_key_exists($key, $fields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | if (!$images) { |
| | | return new Result( |
| | | outcome: 'failed', |
| | | result: ['message' => 'No upload results found'] |
| | | ); |
| | | |
| | | //Remove values that are already saved |
| | | $check = Meta::forTerm($termId)->getAll(array_keys($allowedFields)); |
| | | error_log('Stored values: '.print_r($check, true)); |
| | | $allowedFields = array_filter($allowedFields, function ($value, $key) use ($check) { |
| | | error_log('Sent value: '.print_r($value, true)); |
| | | error_log('Stored Value: '.print_r($check[$key], true)); |
| | | return $value !== $check[$key]; |
| | | }, ARRAY_FILTER_USE_BOTH); |
| | | |
| | | if (empty($allowedFields)) { |
| | | return true; |
| | | } |
| | | |
| | | $results = []; |
| | | error_log('Allowed fields: '.print_r($allowedFields, true)); |
| | | |
| | | if ($data['mode'] === 'selection') { |
| | | $results = $this->createFromSelection($operation, $data, $images, $progress); |
| | | } else { |
| | | $results = $this->createFromDirect($operation, $data, $images, $progress); |
| | | } |
| | | |
| | | // Clear caches |
| | | CacheManager::for($data['content'])->clear(); |
| | | CacheManager::for('feed')->clear(); |
| | | |
| | | return new Result( |
| | | outcome: !empty($results) ? 'success' : 'failed', |
| | | result: $results |
| | | ); |
| | | return Meta::forTerm($termId) |
| | | ->setAll($allowedFields); |
| | | } |
| | | |
| | | private function createFromSelection(Operation $operation, array $data, array $images, Progress $progress): array |
| | | private function saveUserFields(int $userId, array $data): bool |
| | | { |
| | | $results = []; |
| | | $content = $data['content'] ?? ''; |
| | | $fields = Registrar::getFieldsFor($content); |
| | | |
| | | foreach ($images as $group => $files) { |
| | | $settings = json_decode($data['files_data'][$group] ?? '{}'); |
| | | $allowedFields = array_filter($data, function ($key) use ($fields) { |
| | | return array_key_exists($key, $fields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | if (($settings->type ?? '') === 'group') { |
| | | $postId = $this->createGroupPost($operation, $data, $files, $settings); |
| | | } else { |
| | | $postId = $this->createIndividualPosts($operation, $data, $files); |
| | | } |
| | | //Remove values that are already saved |
| | | $check = Meta::forUser($userId)->getAll(array_keys($allowedFields)); |
| | | $allowedFields = array_filter($allowedFields, function ($key) use ($allowedFields, $check) { |
| | | return $allowedFields[$key] !== $check[$key]; |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | if ($postId) { |
| | | $results = array_merge($results, (array)$postId); |
| | | } |
| | | |
| | | $progress->advance(1); |
| | | if (empty($allowedFields)) { |
| | | return true; |
| | | } |
| | | |
| | | return $results; |
| | | return Meta::forUser($userId) |
| | | ->setAll($allowedFields); |
| | | } |
| | | |
| | | private function createFromDirect(Operation $operation, array $data, array $images, Progress $progress): array |
| | | { |
| | | $results = []; |
| | | |
| | | foreach ($images as $img) { |
| | | $postId = wp_insert_post([ |
| | | 'post_type' => $this->postType, |
| | | 'post_title' => $this->generatePostTitle($data['content']), |
| | | 'post_status' => 'draft', |
| | | 'post_author' => $operation->userId, |
| | | ]); |
| | | |
| | | if ($postId && !is_wp_error($postId)) { |
| | | set_post_thumbnail($postId, $img['attachment_id']); |
| | | $results[] = $postId; |
| | | } |
| | | |
| | | $progress->advance(1); |
| | | } |
| | | |
| | | return $results; |
| | | } |
| | | |
| | | private function createGroupPost(Operation $operation, array $data, array $files, object $settings): ?int |
| | | { |
| | | $featuredIndex = $settings->metadata->featuredFile ?? 0; |
| | | $title = $settings->metadata->title ?? $this->generatePostTitle($data['content']); |
| | | |
| | | $postId = wp_insert_post([ |
| | | 'post_type' => $this->postType, |
| | | 'post_title' => $title, |
| | | 'post_status' => 'draft', |
| | | 'post_author' => $operation->userId, |
| | | ]); |
| | | |
| | | if (!$postId || is_wp_error($postId)) { |
| | | return null; |
| | | } |
| | | |
| | | // Set featured image |
| | | set_post_thumbnail($postId, $files[$featuredIndex]['attachment_id']); |
| | | |
| | | // Remaining files go to gallery |
| | | unset($files[$featuredIndex]); |
| | | if (!empty($files)) { |
| | | $meta = new MetaManager($postId, 'post'); |
| | | $ids = array_column($files, 'attachment_id'); |
| | | $meta->updateValue('gallery', implode(',', $ids)); |
| | | } |
| | | |
| | | return $postId; |
| | | } |
| | | |
| | | private function createIndividualPosts(Operation $operation, array $data, array $files): array |
| | | { |
| | | $results = []; |
| | | |
| | | foreach ($files as $img) { |
| | | $title = $this->generatePostTitle($data['content']); |
| | | $postId = wp_insert_post([ |
| | | 'post_type' => $this->postType, |
| | | 'post_title' => $title, |
| | | 'post_slug' => sanitize_title($title), |
| | | 'post_status' => 'draft', |
| | | 'post_author' => $operation->userId, |
| | | ]); |
| | | |
| | | if ($postId && !is_wp_error($postId)) { |
| | | set_post_thumbnail($postId, $img['attachment_id']); |
| | | $results[] = $postId; |
| | | } |
| | | } |
| | | |
| | | return $results; |
| | | } |
| | | |
| | | // ───────────────────────────────────────────────────────────── |
| | | // Helpers |
| | | // ───────────────────────────────────────────────────────────── |
| | | |
| | | private function initTimelineFields(string $content): void |
| | | { |
| | | $content = jvbNoBase($content); |
| | | if (!Features::forContent($content)->has('is_timeline')) { |
| | | return; |
| | | } |
| | | |
| | | $config = Features::getConfig($content); |
| | | $this->fields = $config['fields'] ?? []; |
| | | |
| | | // Shared fields (apply to all posts) |
| | | $this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) { |
| | | return !isset($field['for_all']) || $field['for_all'] === false; |
| | | })); |
| | | array_unshift($this->timelineSharedFields, 'post_thumbnail', 'post_title', 'post_status'); |
| | | |
| | | // Unique fields (per-entry) |
| | | $this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) { |
| | | return isset($field['for_all']) && $field['for_all'] === true; |
| | | })); |
| | | } |
| | | |
| | | private function verifyOwnership(int $postId): bool |
| | | { |
| | | $post = get_post($postId); |
| | | return $post && (int)$post->post_author === $this->userId; |
| | | } |
| | | |
| | | private function generatePostTitle(string $content): string |
| | | /************************************************************* |
| | | * TIMELINE HELPERS |
| | | *************************************************************/ |
| | | protected function maybeReorderTimelines(array $parentIDs):void { |
| | | foreach ($parentIDs as $parentId) { |
| | | try { |
| | | $this->maybeReorderTimeline((int)$parentId); |
| | | } catch (Exception $e) { |
| | | error_log("Timeline reorder failed for parent {$parentId}: " . $e->getMessage()); |
| | | } |
| | | } |
| | | } |
| | | protected function getTimelinePosts(int $parentID):array |
| | | { |
| | | $username = get_user_meta($this->userId, 'first_name', true); |
| | | $link = get_user_meta($this->userId, BASE . 'link', true); |
| | | $city = function_exists('jvbArtistCity') ? jvbArtistCity($link) : ''; |
| | | $parent = get_post($parentID); |
| | | if (!$parent) { |
| | | return []; |
| | | } |
| | | |
| | | return ucfirst($content) . ' by ' . ($city ? "$city artist " : '') . $username; |
| | | $children = get_children([ |
| | | 'post_parent' => $parentID, |
| | | 'posts_per_page' => -1, |
| | | 'post_status' => ['publish', 'draft'], |
| | | ]); |
| | | |
| | | |
| | | if (count($children) === 0) { |
| | | return []; |
| | | } |
| | | |
| | | $allPosts = array_merge([$parent], $children); |
| | | |
| | | // Sort by post_date |
| | | usort($allPosts, function($a, $b) { |
| | | return strtotime($a->post_date) <=> strtotime($b->post_date); |
| | | }); |
| | | |
| | | return $allPosts; |
| | | } |
| | | protected function maybeReorderTimeline(int $parentID):void |
| | | { |
| | | // clean_post_cache($parentID); |
| | | $parent = get_post($parentID); |
| | | if (!$parent) { |
| | | return; |
| | | } |
| | | |
| | | $allPosts = $this->getTimelinePosts($parentID); |
| | | if (empty($allPosts)) { |
| | | return; |
| | | } |
| | | |
| | | |
| | | |
| | | // Check if order changed |
| | | $needsReorder = false; |
| | | foreach ($allPosts as $index => $post) { |
| | | if ($index === 0 && $post->ID !== $parent->ID) { |
| | | $needsReorder = true; |
| | | break; |
| | | } |
| | | if ($index > 0 && (int)$post->menu_order !== $index) { |
| | | $needsReorder = true; |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if (!$needsReorder) { |
| | | // Just recalculate timelines without reordering |
| | | $this->recalculateTimelines($allPosts); |
| | | return; |
| | | } |
| | | |
| | | // Handle parent swap if needed |
| | | $newParent = $allPosts[0]; |
| | | if ($newParent->ID !== $parent->ID) { |
| | | $this->swapTimelineParent($parent, $newParent, $allPosts); |
| | | } else { |
| | | // Just update menu orders and timelines |
| | | foreach ($allPosts as $index => $post) { |
| | | if ($index === 0) continue; // Skip parent |
| | | |
| | | $success = jvb_update_post([ |
| | | 'ID' => $post->ID, |
| | | 'post_parent' => $newParent->ID, |
| | | 'menu_order' => $index |
| | | ]); |
| | | } |
| | | |
| | | $this->recalculateTimelines($allPosts); |
| | | } |
| | | } |
| | | |
| | | private function recalculateTimelines(array $posts): void |
| | | { |
| | | $previousPost = null; |
| | | $latestTimestamp = 0; |
| | | |
| | | |
| | | $lastKey = array_key_last($posts); |
| | | foreach ($posts as $index => $post) { |
| | | $meta = Meta::forPost($post->ID); |
| | | if ($index === 0) { |
| | | $meta->set('timeline', ''); |
| | | $previousPost = $post; |
| | | continue; // Parent has no timeline |
| | | } |
| | | |
| | | // Calculate timeline from previous post |
| | | if ($previousPost) { |
| | | $timeline = $this->calculateTimeline($previousPost, $post); |
| | | if ($timeline) { |
| | | $termId = $this->getOrCreateTerm($timeline, 'timeline'); |
| | | if ($termId) { |
| | | $success = $meta->set('timeline', $termId); |
| | | } |
| | | } |
| | | } |
| | | |
| | | if ($lastKey === $index) { |
| | | $latestTimestamp = strtotime($post->post_date); |
| | | } |
| | | $previousPost = $post; |
| | | } |
| | | |
| | | // Update parent's latest_date |
| | | if ($latestTimestamp > 0) { |
| | | $success = update_post_meta($posts[0]->ID, BASE . 'latest_date', $latestTimestamp); |
| | | } |
| | | } |
| | | |
| | | private function calculateTimeline(\WP_POST $previous, \WP_POST $current): ?string |
| | | { |
| | | $previousDate = strtotime($previous->post_date); |
| | | $currentDate = strtotime($current->post_date); |
| | | |
| | | if (!$previousDate || !$currentDate || $currentDate <= $previousDate) { |
| | | return null; |
| | | } |
| | | |
| | | $daysDiff = floor(($currentDate - $previousDate) / (60*60*24)); |
| | | $weeks = floor($daysDiff / 7); |
| | | if ($weeks === 0) { |
| | | return 'Less than 1 Week'; |
| | | } |
| | | if ($weeks < 16) { |
| | | return $weeks === 1 ? '1 Week' : $weeks . ' Weeks'; |
| | | } |
| | | |
| | | $previousDateTime = new \DateTime($previous->post_date); |
| | | $currentDateTime = new \DateTime($current->post_date); |
| | | |
| | | $interval = $previousDateTime->diff($currentDateTime); |
| | | $months = ($interval->y * 12) + $interval->m; |
| | | |
| | | if ($months === 0) { |
| | | return $weeks . ' Weeks'; |
| | | } |
| | | |
| | | return ($months === 1) ? '1 Month' : $months . ' Months'; |
| | | } |
| | | |
| | | private function swapTimelineParent(\WP_Post $oldParent, \WP_Post $newParent, array $allPosts): void |
| | | { |
| | | // Swap titles and content |
| | | $originalTitle = $oldParent->post_title; |
| | | $originalSlug = $oldParent->post_name; |
| | | $originalContent = $oldParent->post_content; |
| | | |
| | | $updateParent = jvb_update_post([ |
| | | 'ID' => $oldParent->ID, |
| | | 'post_title' => 'Treatment', |
| | | 'post_name' => sanitize_title('Treatment ' . $newParent->ID), |
| | | 'post_content' => '', |
| | | ]); |
| | | |
| | | $updateNewParent = jvb_update_post([ |
| | | 'ID' => $newParent->ID, |
| | | 'post_title' => $originalTitle, |
| | | 'post_name' => $originalSlug, |
| | | 'post_content' => $originalContent, |
| | | 'post_parent' => 0, |
| | | 'menu_order' => 0 |
| | | ]); |
| | | |
| | | // Clear timeline taxonomy from new parent |
| | | wp_set_object_terms($newParent->ID, [], BASE . 'timeline', false); |
| | | |
| | | // Update all other posts to new parent |
| | | foreach ($allPosts as $index => $post) { |
| | | if ($index === 0) continue; // Skip new parent |
| | | |
| | | $title = $post->post_title; |
| | | if (str_starts_with($title, 'Treatment #')) { |
| | | $title = 'Treatment #' . $index; |
| | | } |
| | | |
| | | $childUpdate = jvb_update_post([ |
| | | 'ID' => $post->ID, |
| | | 'post_title' => $title, |
| | | 'post_parent' => $newParent->ID, |
| | | 'menu_order' => $index, |
| | | ]); |
| | | } |
| | | |
| | | // Recalculate timelines for all posts |
| | | $this->recalculateTimelines($allPosts); |
| | | } |
| | | |
| | | private function getOrCreateTerm(string $termName, string $taxonomy): ?int |
| | | { |
| | | $taxonomy = jvbCheckBase($taxonomy); |
| | | $term = get_term_by('name', $termName, $taxonomy); |
| | | |
| | | if (!$term) { |
| | | $result = wp_insert_term($termName, $taxonomy); |
| | | if (is_wp_error($result)) { |
| | | return null; |
| | | } |
| | | return $result['term_id']; |
| | | } |
| | | |
| | | return $term->term_id; |
| | | } |
| | | |
| | | protected function checkSharedFields(array $fields): void |
| | | { |
| | | foreach ($fields as $parentID => $shared) { |
| | | $meta = Meta::forPost($parentID); |
| | | $values = $meta->getAll($shared); |
| | | |
| | | $children = get_children([ |
| | | 'post_parent' => $parentID, |
| | | 'posts_per_page' => -1, |
| | | 'fields' => 'ids', |
| | | ]); |
| | | |
| | | if (empty($children)) { |
| | | continue; |
| | | } |
| | | |
| | | foreach ($children as $child) { |
| | | Meta::forPost($child)->setAll($values); |
| | | } |
| | | } |
| | | } |
| | | |
| | | protected function handleTimelineStatusChange(array $updates):void |
| | | { |
| | | $updates = array_filter($updates, function ($status) { |
| | | return in_array($status, ['trash', 'delete', 'publish', 'draft']); |
| | | }); |
| | | |
| | | foreach ($updates as $parentID => $status) { |
| | | $children = get_children([ |
| | | 'post_parent' => $parentID, |
| | | 'posts_per_page' => -1, |
| | | 'fields' => 'ids' |
| | | ]); |
| | | if (!empty($children)) { |
| | | foreach($children as $child) { |
| | | if ($status === 'trash') { |
| | | wp_trash_post($child); |
| | | } elseif ($status === 'delete') { |
| | | wp_delete_post($child, true); |
| | | }else { |
| | | jvb_update_post([ |
| | | 'ID' => $child, |
| | | 'post_status' => $status |
| | | ]); |
| | | } |
| | | |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | protected function handlePost(string|int $ID, array $data, Registrar $registrar, array $results, Progress $progress):array |
| | | { |
| | | // New post creation |
| | | if (str_starts_with((string)$ID, 'new')) { |
| | | |
| | | $newId = wp_insert_post([ |
| | | 'post_author' => $this->userId, |
| | | 'post_type' => $registrar->getBased(), |
| | | 'post_title' => $data['post_title'] ?? apply_filters('jvbDefaultTitle', '', $registrar->getSlug()), |
| | | 'post_status' => $data['status'] ?? 'draft', |
| | | ]); |
| | | error_log('Created new post: '.print_r($newId, true)); |
| | | |
| | | if (!$newId || is_wp_error($newId)) { |
| | | $results['errors'][$ID] = 'Could not create post'; |
| | | $progress->failItem($ID, 'Could not create post'); |
| | | return $results; |
| | | } |
| | | |
| | | $results['newPosts'][$ID] = $newId; |
| | | $this->savePostFields($newId, $data); |
| | | unset($data['content']); |
| | | $results['success'][$newId] = $data; |
| | | $progress->advance(); |
| | | return $results; |
| | | } |
| | | |
| | | //Existing post update |
| | | if (!$this->verifyOwnership((int)$ID)) { |
| | | $progress->failItem($ID, 'No permission to modify this post'); |
| | | $results['errors'][$ID] = 'No permission'; |
| | | return $results; |
| | | } |
| | | |
| | | $result = $this->savePostFields((int)$ID, $data); |
| | | unset($data['content']); |
| | | if ($result) { |
| | | $results['success'][$ID] = $data; |
| | | } else { |
| | | $results['errors'][$ID] = 'Could not update post data'; |
| | | } |
| | | if ($registrar && $registrar->hasFeature('is_timeline')) { |
| | | $post = get_post((int)$ID); |
| | | $parentId = $post->post_parent > 0 ? $post->post_parent : $post->ID; |
| | | $fields = $registrar->getFields(); |
| | | $sharedFields = array_keys(array_filter($fields, function ($field) { |
| | | return !array_key_exists('for_all', $field) || !$field['for_all']; |
| | | })); |
| | | |
| | | |
| | | if (array_key_exists('timeline_gallery', $data)) { |
| | | //This should only happen if we delete an image from the gallery |
| | | $changes = explode(',', $data['timeline_gallery']); |
| | | $timelinePosts = $this->getTimelinePosts($parentId); |
| | | if (!empty($timelinePosts)) { |
| | | $posts = array_map(function($item) { return $item->ID; }, $timelinePosts); |
| | | $changed = false; |
| | | foreach ($posts as $tID) { |
| | | if (!in_array($tID, $changes)) { |
| | | $changed = true; |
| | | wp_delete_post($tID, true); |
| | | } |
| | | } |
| | | if ($changed) { |
| | | $results['timelineParents'][] = $parentId; |
| | | } |
| | | } |
| | | |
| | | } |
| | | |
| | | |
| | | if (array_key_exists('post_date', $data) && !in_array($parentId, $results['timelineParents'])) { |
| | | $results['timelineParents'][] = $parentId; |
| | | } |
| | | if ($parentId === $ID) { |
| | | if (array_key_exists('post_status', $data) && !array_key_exists($parentId, $results['timelineStatus'])) { |
| | | $results['timelineStatus'][$parentId] = $data['post_status']; |
| | | } |
| | | |
| | | if (count(array_intersect($sharedFields, array_keys($data))) > 0) { |
| | | if (!array_key_exists($parentId, $results['timelineSharedFields'])) { |
| | | $results['timelineSharedFields'][$parentId] = []; |
| | | } |
| | | $temp = array_intersect($sharedFields, array_keys($data)); |
| | | $results['timelineSharedFields'][$parentId] = array_unique(array_merge($results['timelineSharedFields'][$parentId], $temp)); |
| | | } |
| | | } |
| | | } |
| | | $progress->advance(); |
| | | return $results; |
| | | } |
| | | |
| | | |
| | | protected function handleTerm(int $ID, array $data, Registrar $registrar, array $results, Progress $progress):array |
| | | { |
| | | error_log('Handling term '.$ID.' with data: '.print_r($data, true)); |
| | | //Existing term update |
| | | if ($registrar->hasFeature('is_ownable') && (!JVB()->roles()->isOwner($this->userId, $ID) && !JVB()->roles()->isManager($this->userId, $ID))) { |
| | | error_log('Term is ownable. User does not own this term.'); |
| | | $progress->failItem($ID, 'No permission to modify this term'); |
| | | $results['errors'][$ID] = 'No permission'; |
| | | return $results; |
| | | } |
| | | |
| | | $result = $this->saveTermFields($ID, $data); |
| | | unset($data['content']); |
| | | if ($result) { |
| | | $results['success'][$ID] = $data; |
| | | } else { |
| | | $results['errors'][$ID] = 'Could not update term data'; |
| | | } |
| | | $progress->advance(); |
| | | return $results; |
| | | } |
| | | protected function handleUser(int $ID, array $data, Registrar $registrar, array $results, Progress $progress):array |
| | | { |
| | | //Existing term update |
| | | if ($ID !== $this->userId || !user_can($this->userId, 'manage_options')) { |
| | | $progress->failItem($ID, 'No permission to modify this term'); |
| | | $results['errors'][$ID] = 'No permission'; |
| | | return $results; |
| | | } |
| | | |
| | | $result = $this->saveUserFields($ID, $data); |
| | | unset($data['content']); |
| | | if ($result) { |
| | | $results['success'][$ID] = $data; |
| | | } else { |
| | | $results['errors'][$ID] = 'Could not update post data'; |
| | | } |
| | | $progress->advance(); |
| | | return $results; |
| | | } |
| | | } |