diff --git a/app/Console/Commands/AddSiteSettings.php b/app/Console/Commands/AddSiteSettings.php index b5dfe41fec..f8172c1773 100644 --- a/app/Console/Commands/AddSiteSettings.php +++ b/app/Console/Commands/AddSiteSettings.php @@ -81,6 +81,8 @@ public function handle() { $this->addSiteSetting('is_maintenance_mode', 0, '0: Site is normal, 1: Users without the Has Maintenance Access power will be redirected to the home page.'); + $this->addSiteSetting('is_queue_open', 1, '0: New queue submissions cannot be made (mods can work on the queue still), 1: Queue is submittable.'); + $this->addSiteSetting('deactivated_privacy', 0, 'Who can view the deactivated list? 0: Admin only, 1: Staff only, 2: Members only, 3: Public.'); $this->addSiteSetting('deactivated_link', 0, '0: No link to the deactivated list is displayed anywhere, 1: Link to the deactivated list is shown on the user list.'); diff --git a/app/Http/Controllers/Admin/Data/PromptController.php b/app/Http/Controllers/Admin/Data/PromptController.php index 8f40dd8362..79ad960c3d 100644 --- a/app/Http/Controllers/Admin/Data/PromptController.php +++ b/app/Http/Controllers/Admin/Data/PromptController.php @@ -179,7 +179,6 @@ public function getCreatePrompt() { return view('admin.prompts.create_edit_prompt', [ 'prompt' => new Prompt, 'categories' => ['none' => 'No category'] + PromptCategory::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), - 'limit_periods' => config('lorekeeper.extensions.limit_periods'), ]); } @@ -199,7 +198,6 @@ public function getEditPrompt($id) { return view('admin.prompts.create_edit_prompt', [ 'prompt' => $prompt, 'categories' => ['none' => 'No category'] + PromptCategory::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), - 'limit_periods' => config('lorekeeper.extensions.limit_periods'), ]); } diff --git a/app/Http/Controllers/Admin/Data/QueueController.php b/app/Http/Controllers/Admin/Data/QueueController.php new file mode 100644 index 0000000000..b4d9d1887d --- /dev/null +++ b/app/Http/Controllers/Admin/Data/QueueController.php @@ -0,0 +1,304 @@ + QueueCategory::orderBy('sort', 'DESC')->get(), + ]); + } + + /** + * Shows the create queue category page. + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getCreateQueueCategory() { + return view('admin.queues.create_edit_queue_category', [ + 'category' => new QueueCategory, + ]); + } + + /** + * Shows the edit queue category page. + * + * @param int $id + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getEditQueueCategory($id) { + $category = QueueCategory::find($id); + if (!$category) { + abort(404); + } + + return view('admin.queues.create_edit_queue_category', [ + 'category' => $category, + ]); + } + + /** + * Creates or edits a queue category. + * + * @param App\Services\QueueService $service + * @param int|null $id + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postCreateEditQueueCategory(Request $request, QueueService $service, $id = null) { + $id ? $request->validate(QueueCategory::$updateRules) : $request->validate(QueueCategory::$createRules); + $data = $request->only([ + 'name', 'description', 'image', 'remove_image', 'key', 'limit', 'limit_period', 'limit_concurrent', 'display', + ]); + if ($id && $service->updateQueueCategory(QueueCategory::find($id), $data, Auth::user())) { + flash('Category updated successfully.')->success(); + } elseif (!$id && $category = $service->createQueueCategory($data, Auth::user())) { + flash('Category created successfully.')->success(); + + return redirect()->to('admin/data/queue-categories/edit/'.$category->id); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + } + + return redirect()->back(); + } + + /** + * Gets the queue category deletion modal. + * + * @param int $id + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getDeleteQueueCategory($id) { + $category = QueueCategory::find($id); + + return view('admin.queues._delete_queue_category', [ + 'category' => $category, + ]); + } + + /** + * Deletes a queue category. + * + * @param App\Services\QueueService $service + * @param int $id + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postDeleteQueueCategory(Request $request, QueueService $service, $id) { + if ($id && $service->deleteQueueCategory(QueueCategory::find($id))) { + flash('Category deleted successfully.')->success(); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + } + + return redirect()->to('admin/data/queue-categories'); + } + + /** + * Sorts queue categories. + * + * @param App\Services\QueueService $service + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postSortQueueCategory(Request $request, QueueService $service) { + if ($service->sortQueueCategory($request->get('sort'))) { + flash('Category order updated successfully.')->success(); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + } + + return redirect()->back(); + } + + /********************************************************************************************** + + QUEUES + + **********************************************************************************************/ + + /** + * Shows the queue category index. + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getQueueIndex(Request $request) { + $query = Queue::query(); + $data = $request->only(['queue_category_id', 'name']); + if (isset($data['queue_category_id']) && $data['queue_category_id'] != 'none') { + $query->where('queue_category_id', $data['queue_category_id']); + } + if (isset($data['name'])) { + $query->where('name', 'LIKE', '%'.$data['name'].'%'); + } + + return view('admin.queues.queues', [ + 'queues' => $query->paginate(20)->appends($request->query()), + 'categories' => ['none' => 'Any Category'] + QueueCategory::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), + ]); + } + + /** + * Shows the create queue page. + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getCreateQueue() { + $types = config('lorekeeper.queue_types'); + $result = []; + foreach ($types as $type => $typeData) { + $result[$type] = $typeData['name']; + } + + return view('admin.queues.create_edit_queue', [ + 'queue' => new Queue, + 'categories' => ['none' => 'No category'] + QueueCategory::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), + 'types' => $result, + 'ranks' => ['none' => 'All Staff with Submissions Power'] + Rank::pluck('name', 'id')->toArray(), + ]); + } + + /** + * Shows the edit queue page. + * + * @param int $id + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getEditQueue($id) { + $queue = Queue::find($id); + if (!$queue) { + abort(404); + } + $types = config('lorekeeper.queue_types'); + $result = []; + foreach ($types as $type => $typeData) { + $result[$type] = $typeData['name']; + } + + return view('admin.queues.create_edit_queue', [ + 'queue' => $queue, + 'categories' => ['none' => 'No category'] + QueueCategory::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), + 'types' => $result, + 'item_limits' => $queue->configSet('consume_items') ? Item::orderBy('name')->pluck('name', 'id') : [], + 'ranks' => Rank::pluck('name', 'id')->toArray(), + ] + ($queue->service?->getEditData() ?? [])); + } + + /** + * Creates or edits a queue. + * + * @param App\Services\QueueService $service + * @param int|null $id + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postCreateEditQueue(Request $request, QueueService $service, $id = null) { + $id ? $request->validate(Queue::$updateRules) : $request->validate(Queue::$createRules); + $data = $request->only(['name', 'queue_category_id', 'staff_rank_id', 'summary', 'description', 'start_at', 'end_at', 'hide_before_start', 'hide_after_end', 'is_active', 'image', 'remove_image', 'prefix', 'hide_submissions', 'staff_only', 'form', 'queue_type', 'limit', 'limit_period', 'check_text', 'user_rewardable_type', 'user_rewardable_id', 'user_quantity', 'character_rewardable_type', 'character_rewardable_id', 'character_quantity', 'limit_concurrent', + ]); + if ($id && $service->updateQueue(Queue::find($id), $data, Auth::user())) { + flash('Queue updated successfully.')->success(); + } elseif (!$id && $queue = $service->createQueue($data, Auth::user())) { + flash('Queue created successfully.')->success(); + + return redirect()->to('admin/data/queues/edit/'.$queue->id); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + } + + return redirect()->back(); + } + + /** + * Gets the queue deletion modal. + * + * @param int $id + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getDeleteQueue($id) { + $queue = Queue::find($id); + + return view('admin.queues._delete_queue', [ + 'queue' => $queue, + ]); + } + + /** + * Deletes a queue. + * + * @param App\Services\QueueService $service + * @param int $id + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postDeleteQueue(Request $request, QueueService $service, $id) { + if ($id && $service->deleteQueue(Queue::find($id))) { + flash('Queue deleted successfully.')->success(); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + } + + return redirect()->to('admin/data/queues'); + } + + /** + * Edits a queue's type data. + * + * @param App\Services\QueueService $service + * @param int $id + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postEditType(Request $request, QueueService $service, $id) { + $data = $request->all(); + if ($service->updateType(Queue::find($id), $data)) { + flash('Queue type settings updated successfully.')->success(); + + return redirect()->back(); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + } + + return redirect()->back(); + } +} diff --git a/app/Http/Controllers/Admin/HomeController.php b/app/Http/Controllers/Admin/HomeController.php index 36e25904b0..916fadecdb 100644 --- a/app/Http/Controllers/Admin/HomeController.php +++ b/app/Http/Controllers/Admin/HomeController.php @@ -9,6 +9,8 @@ use App\Models\Character\CharacterTransfer; use App\Models\Currency\Currency; use App\Models\Gallery\GallerySubmission; +use App\Models\Queue\Queue; +use App\Models\Queue\QueueSubmission; use App\Models\Report\Report; use App\Models\Submission\Submission; use App\Models\Trade\Trade; @@ -44,6 +46,8 @@ public function getIndex() { 'galleryCurrencyAwards' => $galleryCurrencyAwards, 'gallerySubmissionCount' => GallerySubmission::collaboratorApproved()->where('status', 'Pending')->count(), 'galleryAwardCount' => GallerySubmission::requiresAward()->where('is_valued', 0)->count(), + 'queueCount' => QueueSubmission::where('status', 'Pending')->whereNotNull('queue_id')->count(), + 'queues' => Queue::query()->active()->get(), ]); } diff --git a/app/Http/Controllers/Admin/QueueSubmissionController.php b/app/Http/Controllers/Admin/QueueSubmissionController.php new file mode 100644 index 0000000000..8209afe660 --- /dev/null +++ b/app/Http/Controllers/Admin/QueueSubmissionController.php @@ -0,0 +1,141 @@ +where('status', $status ? ucfirst($status) : 'Pending')->whereNotNull('queue_id'); + $data = $request->only(['queue_category_id', 'sort']); + if (isset($data['queue_category_id']) && $data['queue_category_id'] != 'none') { + $submissions->whereHas('queue', function ($query) use ($data) { + $query->where('queue_category_id', $data['queue_category_id']); + }); + } + if (isset($data['sort'])) { + switch ($data['sort']) { + case 'newest': + $submissions->sortNewest(); + break; + case 'oldest': + $submissions->sortOldest(); + break; + } + } else { + $submissions->sortOldest(); + } + + return view('admin.queues.submission_index', [ + 'submissions' => $submissions->paginate(30)->appends($request->query()), + 'categories' => ['none' => 'Any Category'] + QueueCategory::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), + ]); + } + + /** + * Shows a specific queue's submission index page. + * + * @param string $status + * @param mixed $id + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getQueueSubmissionIndex(Request $request, $id, $status = null) { + $queue = Queue::find($id); + if ($queue->staff_rank_id && !in_array(Auth::user()->rank_id, $queue->staff_rank_id)) { + abort(404); + } + + $submissions = QueueSubmission::with('queue')->where('queue_id', $id)->where('status', $status ? ucfirst($status) : 'Pending'); + $data = $request->only(['sort']); + if (isset($data['sort'])) { + switch ($data['sort']) { + case 'newest': + $submissions->sortNewest(); + break; + case 'oldest': + $submissions->sortOldest(); + break; + } + } else { + $submissions->sortOldest(); + } + + return view('admin.queues.submission_index', [ + 'queue' => $queue, + 'submissions' => $submissions->paginate(30)->appends($request->query()), + ]); + } + + /** + * Shows the submission detail page. + * + * @param int $id + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getSubmission($id) { + $submission = QueueSubmission::whereNotNull('queue_id')->where('id', $id)->where('status', '!=', 'Draft')->first(); + $inventory = isset($submission->data['user']) ? parseAssetData($submission->data['user']) : null; + if (!$submission) { + abort(404); + } + + $queue = $submission->queue; + + return view('admin.queues.submission', [ + 'submission' => $submission, + 'queue' => $queue, + 'inventory' => $inventory, + 'itemsrow' => Item::all()->keyBy('id'), + 'page' => 'submission', + 'characters' => Character::visible(Auth::user() ?? null)->myo(0)->orderBy('slug', 'DESC')->get()->pluck('fullName', 'slug')->toArray(), + ] + ($queue->service?->getActData($queue) ?? []) + + ($submission->status == 'Pending' ? [ + 'count' => QueueSubmission::where('queue_id', $submission->queue_id)->where('status', 'Approved')->where('user_id', $submission->user_id)->count(), + ] : [])); + } + + /** + * Creates a new submission. + * + * @param App\Services\QueueSubmissionManager $service + * @param int $id + * @param string $action + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postSubmission(Request $request, QueueSubmissionManager $service, $id, $action) { + $data = $request->all(); + if ($action == 'reject' && $service->rejectSubmission($request->only(['staff_comments']) + ['id' => $id], Auth::user())) { + flash('Submission rejected successfully.')->success(); + } elseif ($action == 'cancel' && $service->cancelSubmission($request->only(['staff_comments']) + ['id' => $id], Auth::user())) { + flash('Submission canceled successfully.')->success(); + + return redirect()->to('admin/queue-submissions'); + } elseif ($action == 'approve' && $service->approveSubmission($data + ['id' => $id], Auth::user())) { + flash('Submission approved successfully.')->success(); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + } + + return redirect()->back(); + } +} diff --git a/app/Http/Controllers/Admin/Users/GrantController.php b/app/Http/Controllers/Admin/Users/GrantController.php index afcedc97aa..af01f818f2 100644 --- a/app/Http/Controllers/Admin/Users/GrantController.php +++ b/app/Http/Controllers/Admin/Users/GrantController.php @@ -9,6 +9,7 @@ use App\Models\Currency\Currency; use App\Models\Item\Item; use App\Models\Loot\LootTable; +use App\Models\Queue\QueueSubmission; use App\Models\Submission\Submission; use App\Models\Trade\Trade; use App\Models\User\User; @@ -105,18 +106,20 @@ public function getItemSearch(Request $request) { $designUpdates = CharacterDesignUpdate::whereIn('user_id', $userItems->pluck('user_id')->toArray())->whereNotNull('data')->get(); $trades = Trade::whereIn('sender_id', $userItems->pluck('user_id')->toArray())->orWhereIn('recipient_id', $userItems->pluck('user_id')->toArray())->get(); $submissions = Submission::whereIn('user_id', $userItems->pluck('user_id')->toArray())->whereNotNull('data')->get(); + $queuesubmissions = QueueSubmission::whereIn('user_id', $userItems->pluck('user_id')->toArray())->whereNotNull('data')->get(); } return view('admin.grants.item_search', [ - 'item' => $item ? $item : null, - 'items' => Item::orderBy('name')->pluck('name', 'id'), - 'userItems' => $item ? $userItems : null, - 'characterItems' => $item ? $characterItems : null, - 'users' => $item ? $users : null, - 'characters' => $item ? $characters : null, - 'designUpdates' => $item ? $designUpdates : null, - 'trades' => $item ? $trades : null, - 'submissions' => $item ? $submissions : null, + 'item' => $item ? $item : null, + 'items' => Item::orderBy('name')->pluck('name', 'id'), + 'userItems' => $item ? $userItems : null, + 'characterItems' => $item ? $characterItems : null, + 'users' => $item ? $users : null, + 'characters' => $item ? $characters : null, + 'designUpdates' => $item ? $designUpdates : null, + 'trades' => $item ? $trades : null, + 'submissions' => $item ? $submissions : null, + 'queuesubmissions' => $item ? $queuesubmissions : null, ]); } diff --git a/app/Http/Controllers/QueuesController.php b/app/Http/Controllers/QueuesController.php new file mode 100644 index 0000000000..82e2563c7e --- /dev/null +++ b/app/Http/Controllers/QueuesController.php @@ -0,0 +1,158 @@ +get('name'); + if ($name) { + $query->where('name', 'LIKE', '%'.$name.'%'); + } + + return view('queues.queue_categories', [ + 'categories' => $query->orderBy('sort', 'DESC')->paginate(20)->appends($request->query()), + ]); + } + + /** + * Shows the queues page. + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getQueues(Request $request) { + $query = Queue::splash()->active()->staffOnly(Auth::user() ?? null)->with('category'); + $data = $request->only(['queue_category_id', 'name', 'sort', 'open_queues']); + if (isset($data['queue_category_id']) && $data['queue_category_id'] != 'none') { + if ($data['queue_category_id'] == 'withoutOption') { + $query->whereNull('queue_category_id'); + } else { + $query->where('queue_category_id', $data['queue_category_id']); + } + } + if (isset($data['name'])) { + $query->where('name', 'LIKE', '%'.$data['name'].'%'); + } + + if (isset($data['open_queues'])) { + switch ($data['open_queues']) { + case 'open': + $query->open(true); + break; + case 'closed': + $query->open(false); + break; + case 'any': + default: + // Don't filter + break; + } + } + + if (isset($data['sort'])) { + switch ($data['sort']) { + case 'alpha': + $query->sortAlphabetical(); + break; + case 'alpha-reverse': + $query->sortAlphabetical(true); + break; + case 'category': + $query->sortCategory(); + break; + case 'newest': + $query->sortNewest(); + break; + case 'oldest': + $query->sortOldest(); + break; + case 'start': + $query->sortStart(); + break; + case 'start-reverse': + $query->sortStart(true); + break; + case 'end': + $query->sortEnd(); + break; + case 'end-reverse': + $query->sortEnd(true); + break; + } + } else { + $query->sortCategory(); + } + + return view('queues.queues', [ + 'queues' => $query->paginate(20)->appends($request->query()), + 'categories' => ['none' => 'Any Category'] + ['withoutOption' => 'Without Category'] + QueueCategory::display()->orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), + ]); + } + + /** + * Shows an individual queue. + * + * @param mixed $id + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getQueue(Request $request, $id) { + $queue = Queue::active()->where('id', $id)->first(); + + if (!$queue) { + abort(404); + } + + return view('queues.queue', [ + 'queue' => $queue, + ]); + } + + /** + * Shows the queue category with the given key. + * + * @param string $key + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getQueueIndexPage(Request $request, $key) { + $category = QueueCategory::where('key', $key)->first(); + if (!$category) { + abort(404); + } + + return view('queues.index_page', [ + 'category' => $category, + 'queues' => $category->queues()->active()->staffOnly(Auth::user() ?? null)->paginate(20), + ]); + } +} diff --git a/app/Http/Controllers/Users/InventoryController.php b/app/Http/Controllers/Users/InventoryController.php index 1db8870cdb..8f05c32b08 100644 --- a/app/Http/Controllers/Users/InventoryController.php +++ b/app/Http/Controllers/Users/InventoryController.php @@ -9,6 +9,7 @@ use App\Models\Character\CharacterItem; use App\Models\Item\Item; use App\Models\Item\ItemCategory; +use App\Models\Queue\QueueSubmission; use App\Models\Rarity; use App\Models\Submission\Submission; use App\Models\Trade\Trade; @@ -210,17 +211,19 @@ public function getAccountSearch(Request $request) { $designUpdates = CharacterDesignUpdate::where('user_id', $user->id)->whereNotNull('data')->get(); $trades = Trade::where('sender_id', $user->id)->orWhere('recipient_id', $user->id)->get(); $submissions = Submission::where('user_id', $user->id)->whereNotNull('data')->get(); + $queuesubmissions = QueueSubmission::where('user_id', $user->id)->whereNotNull('data')->get(); } return view('home.account_search', [ - 'item' => $item ? $item : null, - 'items' => Item::orderBy('name')->released()->pluck('name', 'id'), - 'userItems' => $item ? $userItems : null, - 'characterItems' => $item ? $characterItems : null, - 'characters' => $item ? $characters : null, - 'designUpdates' => $item ? $designUpdates : null, - 'trades' => $item ? $trades : null, - 'submissions' => $item ? $submissions : null, + 'item' => $item ? $item : null, + 'items' => Item::orderBy('name')->released()->pluck('name', 'id'), + 'userItems' => $item ? $userItems : null, + 'characterItems' => $item ? $characterItems : null, + 'characters' => $item ? $characters : null, + 'designUpdates' => $item ? $designUpdates : null, + 'trades' => $item ? $trades : null, + 'submissions' => $item ? $submissions : null, + 'queuesubmissions' => $item ? $queuesubmissions : null, ]); } diff --git a/app/Http/Controllers/Users/QueueSubmissionController.php b/app/Http/Controllers/Users/QueueSubmissionController.php new file mode 100644 index 0000000000..7ccf38a6e8 --- /dev/null +++ b/app/Http/Controllers/Users/QueueSubmissionController.php @@ -0,0 +1,306 @@ +where('user_id', Auth::user()->id)->whereNotNull('queue_id')->whereHas('queue', function ($query) { + return $query->active()->staffOnly(Auth::user()); + }); + $type = $request->get('type'); + if (!$type) { + $type = 'Pending'; + } + + $queue = Queue::active()->staffOnly(Auth::user())->find($id); + if (isset($id) && !$queue) { + abort(404); + } + if (isset($id)) { + $submissions = $submissions->where('queue_id', $id); + } + + $submissions = $submissions->where('status', ucfirst($type)); + + return view('home.queues.submissions', [ + 'submissions' => $submissions->orderBy('id', 'DESC')->paginate(20)->appends($request->query()), + 'queue' => $queue, + 'isClaims' => false, + ]); + } + + /** + * Shows the submission page. + * + * @param int $id + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getSubmission($id) { + $submission = QueueSubmission::viewable(Auth::user())->where('id', $id)->whereNotNull('queue_id')->first(); + + $inventory = isset($submission->data['user']) ? parseAssetData($submission->data['user']) : null; + if (!$submission) { + abort(404); + } + + $queue = $submission->queue; + if (!$queue || ($queue->staff_only && !Auth::user()->isStaff)) { + abort(404); + } + + return view('home.queues.submission', [ + 'submission' => $submission, + 'user' => $submission->user, + 'categories' => ItemCategory::orderBy('sort', 'DESC')->get(), + 'inventory' => $inventory, + 'itemsrow' => Item::all()->keyBy('id'), // this keeps track of consumed items and will change if the prompt's items change so let's not change it + 'queue' => $queue, + ] + ($queue->service?->getActData($queue) ?? [])); + } + + /** + * Shows the submit page. + * + * @param mixed $id + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getNewSubmission(Request $request, $id) { + $closed = !Settings::get('is_queue_open'); + $queue = Queue::active()->staffOnly(Auth::user())->find($id); + if (!$queue) { + abort(404); + } + + return view('home.queues.create_submission', [ + 'closed' => $closed, + 'queue' => $queue, + ] + ($closed ? [] : [ + 'submission' => new QueueSubmission, + 'page' => 'submission', + 'count' => QueueSubmission::where('queue_id', $queue->id)->where('status', 'Approved')->where('user_id', Auth::user()->id)->count(), + ] + ($queue->service?->getActData($queue) ?? []) + + ($queue->configSet('consume_items') ? [ + 'inventory' => isset($queue->data['items']) ? UserItem::with('item')->whereNull('deleted_at')->where('count', '>', '0')->where('user_id', Auth::user()->id)->whereIn('item_id', $queue->data['items'])->get() : UserItem::with('item')->whereNull('deleted_at')->where('count', '>', '0')->where('user_id', Auth::user()->id)->get(), + 'itemsrow' => Item::all()->keyBy('id'), // this keeps track of consumed items and will change if the prompt's items change so let's not change it + 'categories' => ItemCategory::orderBy('sort', 'DESC')->get(), + 'item_filter' => isset($queue->data['items']) ? Item::whereIn('id', $queue->data['items'])->get()->keyBy('id') : Item::orderBy('name')->released()->get()->keyBy('id'), + ] : []))); + } + + /** + * Shows the edit submission page. + * + * @param mixed $id + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getEditSubmission(Request $request, $id) { + $closed = !Settings::get('is_queue_open'); + $submission = QueueSubmission::where('id', $id)->where('status', 'Draft')->where('user_id', Auth::user()->id)->first(); + if (!$submission) { + abort(404); + } + + $queue = $submission->queue; + if (!$queue) { + abort(404); + } + + return view('home.queues.edit_submission', [ + 'closed' => $closed, + 'queue' => $queue, + ] + ($closed ? [] : [ + 'submission' => $submission, + 'count' => QueueSubmission::where('queue_id', $submission->queue_id)->where('status', 'Approved')->where('user_id', $submission->user_id)->count(), + ] + ($queue->service?->getActData($queue) ?? []) + + ($queue->configSet('consume_items') ? [ + 'inventory' => isset($queue->data['items']) ? UserItem::with('item')->whereNull('deleted_at')->where('count', '>', '0')->where('user_id', Auth::user()->id)->whereIn('item_id', $queue->data['items'])->get() : UserItem::with('item')->whereNull('deleted_at')->where('count', '>', '0')->where('user_id', Auth::user()->id)->get(), + 'itemsrow' => Item::all()->keyBy('id'), // this keeps track of consumed items and will change if the prompt's items change so let's not change it + 'categories' => ItemCategory::orderBy('sort', 'DESC')->get(), + 'item_filter' => isset($queue->data['items']) ? Item::whereIn('id', $queue->data['items'])->get()->keyBy('id') : Item::orderBy('name')->released()->get()->keyBy('id'), + 'page' => 'queue-submission', + 'selectedInventory' => isset($submission->data['user']) ? parseAssetData($submission->data['user']) : null, + ] : []))); + } + + /** + * Shows character information. + * + * @param string $slug + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getCharacterInfo($slug) { + $character = Character::visible()->where('slug', $slug)->first(); + + return view('home.queues._character', [ + 'character' => $character, + ]); + } + + /** + * Creates a new submission. + * + * @param App\Services\SubmissionManager $service + * @param mixed $draft + * @param mixed $id + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postNewSubmission(Request $request, QueueSubmissionManager $service, $id, $draft = false) { + $queue = Queue::active()->where('id', $id)->first(); + if (!$queue) { + throw new \Exception('Invalid queue selected.'); + } + + $request->validate(QueueSubmission::$createRules); + if ($submission = $service->createSubmission( + $queue, + $request->all(), + Auth::user(), + $draft + )) { + if ($submission->status == 'Draft') { + flash('Draft created successfully.')->success(); + + return redirect()->to('queue-submissions/draft/'.$submission->id); + } else { + flash('Queue submitted successfully.')->success(); + + return redirect()->to('queue-submissions/view/'.$submission->id); + } + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + + return redirect()->back()->withInput(); + } + + return redirect()->to('queue-submissions'); + } + + /** + * Edits a submission draft. + * + * @param App\Services\SubmissionManager $service + * @param mixed $id + * @param mixed $submit + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postEditSubmission(Request $request, QueueSubmissionManager $service, $id, $submit = false) { + $submission = QueueSubmission::where('id', $id)->where('status', 'Draft')->where('user_id', Auth::user()->id)->first(); + if (!$submission) { + abort(404); + } + + $request->validate(QueueSubmission::$updateRules); + if ($submit && $service->editSubmission($submission, $request->all(), Auth::user(), $submit)) { + flash('Draft submitted successfully.')->success(); + } elseif ($service->editSubmission($submission, $request->all(), Auth::user())) { + flash('Draft saved successfully.')->success(); + + return redirect()->back(); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + + return redirect()->back()->withInput(); + } + + return redirect()->to('queue-submissions/view/'.$submission->id); + } + + /** + * Deletes a submission draft. + * + * @param App\Services\SubmissionManager $service + * @param mixed $id + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postDeleteSubmission(Request $request, QueueSubmissionManager $service, $id) { + $submission = QueueSubmission::where('id', $id)->where('status', 'Draft')->where('user_id', Auth::user()->id)->first(); + if (!$submission) { + abort(404); + } + + if ($service->deleteSubmission($submission, $request->all() + ['submission_id' => $submission->id], Auth::user())) { + flash('Draft deleted successfully.')->success(); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + + return redirect()->back(); + } + + return redirect()->to('queue-submissions?type=draft'); + } + + /** + * Cancels a submission and makes it into a draft again. + * + * @param App\Services\SubmissionManager $service + * @param mixed $id + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postCancelSubmission(Request $request, QueueSubmissionManager $service, $id) { + $submission = QueueSubmission::where('id', $id)->where('status', 'Pending')->where('user_id', Auth::user()->id)->first(); + if (!$submission) { + abort(404); + } + + if ($service->cancelSubmission($submission, Auth::user())) { + flash('Submission returned to drafts successfully. If you wish to delete the draft entirely you may do so from the Edit Draft page.')->success(); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + + return redirect()->back(); + } + + return redirect()->to('queue-submissions/draft/'.$submission->id); + } +} diff --git a/app/Http/Controllers/Users/UserController.php b/app/Http/Controllers/Users/UserController.php index f599dd28e9..b571a9d90f 100644 --- a/app/Http/Controllers/Users/UserController.php +++ b/app/Http/Controllers/Users/UserController.php @@ -14,6 +14,7 @@ use App\Models\Item\Item; use App\Models\Item\ItemCategory; use App\Models\Prompt\Prompt; +use App\Models\Queue\Queue; use App\Models\Rarity; use App\Models\User\User; use App\Models\User\UserCurrency; @@ -472,4 +473,27 @@ public function getUserOwnCharacterFavorites(Request $request, $name) { 'favorites' => $this->user->characters->count() ? GallerySubmission::whereIn('id', $userFavorites)->whereIn('id', GalleryCharacter::whereIn('character_id', $userCharacters)->pluck('gallery_submission_id')->toArray())->visible(Auth::user() ?? null)->orderBy('created_at', 'DESC')->paginate(20)->appends($request->query()) : null, ]); } + + /** + * Shows a user's submissions. + * + * @param string $name + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getUserQueueSubmissions(Request $request, $name) { + $logs = $this->user->getQueueSubmissions(Auth::user() ?? null); + if ($request->get('queue_ids')) { + $logs->whereIn('queue_id', $request->get('queue_ids')); + } + if ($request->get('sort')) { + $logs->orderBy('created_at', $request->get('sort') == 'newest' ? 'DESC' : 'ASC'); + } + + return view('user.queue_logs', [ + 'user' => $this->user, + 'logs' => $logs->paginate(30)->appends($request->query()), + 'queues' => Queue::active()->pluck('name', 'id'), + ]); + } } diff --git a/app/Models/Model.php b/app/Models/Model.php index 33938e9b25..4df11e0a30 100644 --- a/app/Models/Model.php +++ b/app/Models/Model.php @@ -28,6 +28,12 @@ protected static function boot() { } $object = Prompt::find($model->prompt_id); break; + case QueueSubmission::class: + if ($model->status == 'Pending') { + return true; + } + $object = $model->queue; + break; } if (!$object) { @@ -47,6 +53,12 @@ protected static function boot() { } $object = Prompt::find($model->prompt_id); break; + case QueueSubmission::class: + if ($model->status != 'Pending' || $model->staff_comments || $model->staff_id) { + return true; + } + $object = $model->queue; + break; } if (!$object) { diff --git a/app/Models/Notification.php b/app/Models/Notification.php index 2d6c1c39de..284b6ef34c 100644 --- a/app/Models/Notification.php +++ b/app/Models/Notification.php @@ -162,4 +162,7 @@ public static function getNotificationId($type) { public const GALLERY_SUBMISSION_STAFF_COMMENTS = 513; public const GALLERY_SUBMISSION_EDITED = 514; public const GALLERY_SUBMISSION_PARTICIPANT = 515; + public const QUEUE_SUBMISSION_APPROVED = 1116; + public const QUEUE_SUBMISSION_REJECTED = 1117; + public const QUEUE_SUBMISSION_CANCELLED = 1118; } diff --git a/app/Models/Queue/Queue.php b/app/Models/Queue/Queue.php new file mode 100644 index 0000000000..a6861b744e --- /dev/null +++ b/app/Models/Queue/Queue.php @@ -0,0 +1,562 @@ + 'datetime', + 'end_at' => 'datetime', + 'data' => 'array', + 'checklist' => 'array', + 'output' => 'array', + 'staff_rank_ids' => 'array', + ]; + + /** + * Validation rules for character creation. + * + * @var array + */ + public static $createRules = [ + 'queue_category_id' => 'nullable', + 'name' => 'required|unique:queues|between:3,100', + 'prefix' => 'nullable|unique:queues|between:2,10', + 'summary' => 'nullable', + 'description' => 'nullable', + 'image' => 'mimes:png', + ]; + + /** + * Validation rules for character updating. + * + * @var array + */ + public static $updateRules = [ + 'queue_category_id' => 'nullable', + 'name' => 'required|between:3,100', + 'prefix' => 'nullable|between:2,10', + 'summary' => 'nullable', + 'description' => 'nullable', + 'image' => 'mimes:png', + ]; + + /********************************************************************************************** + + RELATIONS + + **********************************************************************************************/ + + /** + * Get the category the queue belongs to. + */ + public function category() { + return $this->belongsTo(QueueCategory::class, 'queue_category_id'); + } + + /** + * Get the submissions that belong to this queue. + * + * @param mixed $status + */ + public function submissions($status) { + if (isset($status)) { + return $this->hasMany(QueueSubmission::class, 'queue_id')->where('status', ucfirst($status)); + } else { + return $this->hasMany(QueueSubmission::class, 'queue_id'); + } + } + + /** + * Get the rewards attached to this prompt. + */ + public function rewards() { + return $this->morphMany(Reward::class, 'object', 'object_model', 'object_id'); + } + + /********************************************************************************************** + + SCOPES + + **********************************************************************************************/ + + /** + * Scope a query to only include active queues. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeActive($query) { + return $query->where('is_active', 1) + ->where(function ($query) { + $query->whereNull('start_at')->orWhere('start_at', '<', Carbon::now())->orWhere(function ($query) { + $query->where('start_at', '>=', Carbon::now())->where('hide_before_start', 0); + }); + })->where(function ($query) { + $query->whereNull('end_at')->orWhere('end_at', '>', Carbon::now())->orWhere(function ($query) { + $query->where('end_at', '<=', Carbon::now())->where('hide_after_end', 0); + }); + }); + } + + /** + * Scope a query to open or closed queues. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param bool $isOpen + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeOpen($query, $isOpen) { + if ($isOpen) { + $query->where(function ($query) { + $query->whereNull('end_at')->where('start_at', '<', Carbon::now()); + })->orWhere(function ($query) { + $query->whereNull('start_at')->where('end_at', '>', Carbon::now()); + })->orWhere(function ($query) { + $query->where('start_at', '<', Carbon::now())->where('end_at', '>', Carbon::now()); + })->orWhere(function ($query) { + $query->whereNull('end_at')->whereNull('start_at'); + }); + } else { + $query->where(function ($query) { + $query->whereNull('end_at')->where('start_at', '>', Carbon::now()); + })->orWhere(function ($query) { + $query->whereNull('start_at')->where('end_at', '<', Carbon::now()); + })->orWhere('start_at', '>', Carbon::now())->orWhere('end_at', '<', Carbon::now()); + } + } + + /** + * Scope a query to include or exclude staff-only queues. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param \App\Models\User\User $user + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeStaffOnly($query, $user) { + if ($user && $user->isStaff) { + return $query; + } + + return $query->where('staff_only', 0); + } + + /** + * Scope a query to sort queues in alphabetical order. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param bool $reverse + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeSortAlphabetical($query, $reverse = false) { + return $query->orderBy('name', $reverse ? 'DESC' : 'ASC'); + } + + /** + * Scope a query to sort queues in category order. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeSortCategory($query) { + if (QueueCategory::all()->count()) { + return $query->orderBy(QueueCategory::select('sort')->whereColumn('queues.queue_category_id', 'queue_categories.id'), 'DESC'); + } + + return $query; + } + + /** + * Scope a query to sort features by newest first. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeSortNewest($query) { + return $query->orderBy('id', 'DESC'); + } + + /** + * Scope a query to sort features oldest first. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeSortOldest($query) { + return $query->orderBy('id'); + } + + /** + * Scope a query to sort queues by start date. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param bool $reverse + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeSortStart($query, $reverse = false) { + return $query->orderBy('start_at', $reverse ? 'DESC' : 'ASC'); + } + + /** + * Scope a query to sort queues by end date. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param bool $reverse + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeSortEnd($query, $reverse = false) { + return $query->orderBy('end_at', $reverse ? 'DESC' : 'ASC'); + } + + /** + * Scope a query to sort queues by end date. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeSplash($query) { + $query->whereHas('category', function ($query) { + $query->where('key', null); + })->orWhere('queue_category_id', null); + } + + /********************************************************************************************** + + ACCESSORS + + **********************************************************************************************/ + + /** + * Displays the model's name, linked to its encyclopedia page. + * + * @return string + */ + public function getDisplayNameAttribute() { + return ''.$this->name.''; + } + + /** + * Gets the file directory containing the model's image. + * + * @return string + */ + public function getImageDirectoryAttribute() { + return 'images/data/queues'; + } + + /** + * Gets the file name of the model's image. + * + * @return string + */ + public function getImageFileNameAttribute() { + return $this->id.'-'.$this->hash.'-image.png'; + } + + /** + * Gets the path to the file directory containing the model's image. + * + * @return string + */ + public function getImagePathAttribute() { + return public_path($this->imageDirectory); + } + + /** + * Gets the URL of the model's image. + * + * @return string + */ + public function getImageUrlAttribute() { + if (!$this->has_image) { + return null; + } + + return asset($this->imageDirectory.'/'.$this->imageFileName); + } + + /** + * Gets the URL of the model's encyclopedia page. + * + * @return string + */ + public function getUrlAttribute() { + return url('queues/queues?name='.$this->name); + } + + /** + * Gets the URL of the individual queue's page, by ID. + * + * @return string + */ + public function getIdUrlAttribute() { + return url('queues/'.$this->id); + } + + /** + * Gets the queue's asset type for asset management. + * + * @return string + */ + public function getAssetTypeAttribute() { + return 'queues'; + } + + /** + * Gets the admin edit URL. + * + * @return string + */ + public function getAdminUrlAttribute() { + return url('admin/data/queues/edit/'.$this->id); + } + + /** + * Gets the power required to edit this model. + * + * @return string + */ + public function getAdminPowerAttribute() { + return 'edit_data'; + } + + /** + * Get the service associated with the associated type. + * + * @return mixed + */ + public function getServiceAttribute() { + if (!$this->queue_type) { + return null; + } + + // check class exists + if (!class_exists('App\Services\Queue\\'.str_replace(' ', '', ucwords(str_replace('_', ' ', $this->queue_type))).'Service')) { + return null; + } + + $class = 'App\Services\Queue\\'.str_replace(' ', '', ucwords(str_replace('_', ' ', $this->queue_type))).'Service'; + + return new $class; + } + + /** + * Get the config data. + * + * @return mixed + */ + public function getConfigInfoAttribute() { + return config('lorekeeper.queue_types.'.$this->queue_type); + } + + /** + * Gets the file directory containing the model's image. + * + * @return string + */ + public function getCustomImageDirectoryAttribute() { + return 'images/data/queues/images'; + } + + /** + * Gets the file name of the model's image. + * + * @param mixed $key + * + * @return string + */ + public function customImageFileName($key) { + return $this->id.'-'.$key.'.png'; + } + + /** + * Gets the path to the file directory containing the model's image. + * + * @return string + */ + public function getCustomImagePathAttribute() { + return public_path($this->customImageDirectory); + } + + /** + * Gets the URL of the model's image. + * + * @param mixed $key + * + * @return string + */ + public function customImageUrl($key) { + return asset($this->customImageDirectory.'/'.$this->CustomImageFileName($key)); + } + + /** + * Check that custom image exists. + * + * @param mixed $key + * + * @return string + */ + public function customImageExists($key) { + return file_exists($this->customImagePath.'/'.$this->CustomImageFileName($key)); + } + + /** + * Get the config data. + * + * @param mixed $key + * + * @return mixed + */ + public function configSet($key) { + if (isset($this->configInfo[$key]) && $this->configInfo[$key] == true) { + return true; + } + + return false; + } + + /** + * Retrieves any data that should be used in the holiday type on the user side. + */ + public function getItemsAttribute() { + if (!isset($this->data['items'])) { + return []; + } + + $final = []; + foreach ($this->data['items'] as $item) { + $final[] = Item::find($item); + } + + return $final; + } + + /********************************************************************************************** + + OTHER FUNCTIONS + + **********************************************************************************************/ + + /** + * Determine if the user has exceeded the submission limit for a queue. + * + * @param mixed $user + * + * @return bool + */ + public function checkSubmissionLimit($user) { + // categories supersede all. + if ($this->queue_category_id && isset($this->category->limit)) { + return $this->category->checkSubmissionLimit($user); + } + + if (isset($this->limit)) { + if ($this->submissionLogCount($user) >= $this->limit) { + return false; + } + } + + return true; + } + + /** + * Get the count of total submissions for this queue. + * + * @param mixed $user + */ + public function submissionLogCount($user) { + if ($this->limit) { + switch ($this->limit_period) { + case null: + return QueueSubmission::submitted($this->id, $user->id)->count(); + break; + case 'Hour': + return QueueSubmission::submitted($this->id, $user->id)->where('created_at', '>=', now()->startOfHour())->count(); + break; + case 'Day': + return QueueSubmission::submitted($this->id, $user->id)->where('created_at', '>=', now()->startOfDay())->count(); + break; + case 'Week': + return QueueSubmission::submitted($this->id, $user->id)->where('created_at', '>=', now()->startOfWeek())->count(); + break; + case 'BiWeekly': + return QueueSubmission::submitted($this->id, $user->id)->where('created_at', '>=', now()->subWeeks(2))->count(); + break; + case 'Month': + return QueueSubmission::submitted($this->id, $user->id)->where('created_at', '>=', now()->startOfMonth())->count(); + break; + case 'BiMonthly': + return QueueSubmission::submitted($this->id, $user->id)->where('created_at', '>=', now()->subMonths(2))->count(); + break; + case 'Quarter': + return QueueSubmission::submitted($this->id, $user->id)->where('created_at', '>=', now()->subMonths(3))->count(); + break; + case 'Year': + return QueueSubmission::submitted($this->id, $user->id)->where('created_at', '>=', now()->startOfYear())->count(); + break; + } + } + + return null; + } + + /** + * Determine if the user has exceeded the concurrent submission limit for a queue. + * + * @param mixed $user + * + * @return bool + */ + public function checkConcurrentSubmissionLimit($user) { + // categories supersede all. + if ($this->queue_category_id && isset($this->category->limit_concurrent)) { + return $this->category->checkConcurrentSubmissionLimit($user); + } + + if (isset($this->limit_concurrent)) { + if (QueueSubmission::pending($this->id, $user->id)->count() >= $this->limit_concurrent) { + return false; + } + } + + return true; + } +} diff --git a/app/Models/Queue/QueueCategory.php b/app/Models/Queue/QueueCategory.php new file mode 100644 index 0000000000..b59a5d8232 --- /dev/null +++ b/app/Models/Queue/QueueCategory.php @@ -0,0 +1,256 @@ + 'required|unique:queue_categories|between:3,100', + 'description' => 'nullable', + 'image' => 'mimes:png', + ]; + + /** + * Validation rules for updating. + * + * @var array + */ + public static $updateRules = [ + 'name' => 'required|between:3,100', + 'description' => 'nullable', + 'image' => 'mimes:png', + ]; + + /********************************************************************************************** + + RELATIONS + + **********************************************************************************************/ + + /** + * Get all this category's queues. + */ + public function queues() { + return $this->hasMany('App\Models\Queue\Queue', 'queue_category_id'); + } + + /********************************************************************************************** + + SCOPES + + **********************************************************************************************/ + + /** + * Scope a query to include categories that are on display only. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeDisplay($query) { + return $query->where('display', 1); + } + + /********************************************************************************************** + + ACCESSORS + + **********************************************************************************************/ + + /** + * Displays the model's name, linked to its encyclopedia page. + * + * @return string + */ + public function getDisplayNameAttribute() { + return ''.$this->name.''; + } + + /** + * Gets the file directory containing the model's image. + * + * @return string + */ + public function getImageDirectoryAttribute() { + return 'images/data/queue-categories'; + } + + /** + * Gets the file name of the model's image. + * + * @return string + */ + public function getCategoryImageFileNameAttribute() { + return $this->id.'-'.$this->hash.'-image.png'; + } + + /** + * Gets the path to the file directory containing the model's image. + * + * @return string + */ + public function getCategoryImagePathAttribute() { + return public_path($this->imageDirectory); + } + + /** + * Gets the URL of the model's image. + * + * @return string + */ + public function getCategoryImageUrlAttribute() { + if (!$this->has_image) { + return null; + } + + return asset($this->imageDirectory.'/'.$this->categoryImageFileName); + } + + /** + * Gets the URL of the model's encyclopedia page. + * + * @return string + */ + public function getUrlAttribute() { + if ($this->key) { + return url('queues/index/'.$this->key); + } + + return url('queues/queue-categories?name='.$this->name); + } + + /** + * Gets the URL for an encyclopedia search for queues in this category. + * + * @return string + */ + public function getSearchUrlAttribute() { + return url('queues/queues?queue_category_id='.$this->id); + } + + /** + * Gets the admin edit URL. + * + * @return string + */ + public function getAdminUrlAttribute() { + return url('admin/data/queue-categories/edit/'.$this->id); + } + + /** + * Gets the power required to edit this model. + * + * @return string + */ + public function getAdminPowerAttribute() { + return 'edit_data'; + } + + /** + * Determine if the user has exceeded the submission limit for a category. + * + * @param mixed $user + * + * @return bool + */ + public function checkSubmissionLimit($user) { + if ($this->limit) { + if ($this->logCount($user) >= $this->limit) { + return false; + } + } + + return true; + } + + /** + * Get the count of total submissions for all queues in a category. + * + * @param mixed $user + * + * @return int + */ + public function submissionLogCount($user) { + if ($this->limit) { + $final = null; + foreach ($this->queues as $q) { + switch ($this->limit_period) { + case null: + $final = $final + QueueSubmission::submitted($q->id, $user->id)->count(); + break; + case 'Hour': + $final = $final + QueueSubmission::submitted($q->id, $user->id)->where('created_at', '>=', now()->startOfHour())->count(); + break; + case 'Day': + $final = $final + QueueSubmission::submitted($q->id, $user->id)->where('created_at', '>=', now()->startOfDay())->count(); + break; + case 'Week': + $final = $final + QueueSubmission::submitted($q->id, $user->id)->where('created_at', '>=', now()->startOfWeek())->count(); + break; + case 'BiWeekly': + $final = $final + QueueSubmission::submitted($q->id, $user->id)->where('created_at', '>=', now()->subWeeks(2))->count(); + break; + case 'Month': + $final = $final + QueueSubmission::submitted($q->id, $user->id)->where('created_at', '>=', now()->startOfMonth())->count(); + break; + case 'BiMonthly': + $final = $final + QueueSubmission::submitted($q->id, $user->id)->where('created_at', '>=', now()->subMonths(2))->count(); + break; + case 'Quarter': + $final = $final + QueueSubmission::submitted($q->id, $user->id)->where('created_at', '>=', now()->subMonths(3))->count(); + break; + case 'Year': + $final = $final + QueueSubmission::submitted($q->id, $user->id)->where('created_at', '>=', now()->startOfYear())->count(); + break; + } + } + + return $final; + } + + return null; + } + + /** + * Determine if the user has exceeded the submission limit for a category. + * + * @param mixed $user + * + * @return bool + */ + public function checkConcurrentSubmissionLimit($user) { + if (isset($this->limit_concurrent)) { + $final = null; + foreach ($this->queues as $q) { + $final = $final + QueueSubmission::pending($q->id, $user->id)->count(); + } + + if ($final >= $this->limit_concurrent) { + return false; + } + } + + return true; + } +} diff --git a/app/Models/Queue/QueueSubmission.php b/app/Models/Queue/QueueSubmission.php new file mode 100644 index 0000000000..e62df64db8 --- /dev/null +++ b/app/Models/Queue/QueueSubmission.php @@ -0,0 +1,274 @@ + 'array', + ]; + + /** + * Whether the model contains timestamps to be saved and updated. + * + * @var string + */ + public $timestamps = true; + + /** + * Validation rules for submission creation. + * + * @var array + */ + public static $createRules = [ + 'url' => 'nullable|url', + ]; + + /** + * Validation rules for submission updating. + * + * @var array + */ + public static $updateRules = [ + 'url' => 'nullable|url', + ]; + + /********************************************************************************************** + + RELATIONS + + **********************************************************************************************/ + + /** + * Get the queue this submission is for. + */ + public function queue() { + return $this->belongsTo(Queue::class, 'queue_id'); + } + + /** + * Get the user who made the submission. + */ + public function user() { + return $this->belongsTo(User::class, 'user_id'); + } + + /** + * Get the staff who processed the submission. + */ + public function staff() { + return $this->belongsTo(User::class, 'staff_id'); + } + + /** + * Get the characters attached to the submission. + */ + public function characters() { + return $this->hasMany(QueueSubmissionCharacter::class, 'queue_submission_id'); + } + + /********************************************************************************************** + + SCOPES + + **********************************************************************************************/ + + /** + * Scope a query to only include pending submissions. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeActive($query) { + return $query->where('status', 'Pending'); + } + + /** + * Scope a query to only include drafted submissions. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeDrafts($query) { + return $query->where('status', 'Drafts'); + } + + /** + * Scope a query to only include viewable submissions. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param mixed|null $user + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeViewable($query, $user = null) { + $forbiddenSubmissions = $this + ->whereHas('queue', function ($q) { + $q->where('hide_submissions', 1)->whereNotNull('end_at')->where('end_at', '>', Carbon::now()); + }) + ->orWhereHas('queue', function ($q) { + $q->where('hide_submissions', 2); + }) + ->orWhere('status', '!=', 'Approved')->pluck('id')->toArray(); + + if ($user && $user->hasPower('manage_submissions')) { + return $query; + } else { + return $query->where(function ($query) use ($user, $forbiddenSubmissions) { + if ($user) { + $query->whereNotIn('id', $forbiddenSubmissions)->orWhere('user_id', $user->id); + } else { + $query->whereNotIn('id', $forbiddenSubmissions); + } + }); + } + } + + /** + * Scope a query to sort submissions oldest first. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeSortOldest($query) { + return $query->orderBy('id'); + } + + /** + * Scope a query to sort submissions by newest first. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeSortNewest($query) { + return $query->orderBy('id', 'DESC'); + } + + /********************************************************************************************** + + ACCESSORS + + **********************************************************************************************/ + + /** + * Gets the inventory of the user for selection. + * + * @param mixed $user + * + * @return array + */ + public function getInventory($user) { + return $this->data && isset($this->data['user']['user_items']) ? $this->data['user']['user_items'] : []; + } + + /** + * Gets the currencies of the given user for selection. + * + * @param User $user + * + * @return array + */ + public function getCurrencies($user) { + return $this->data && isset($this->data['user']) && isset($this->data['user']['currencies']) ? $this->data['user']['currencies'] : []; + } + + /** + * Get the viewing URL of the submission/claim. + * + * @return string + */ + public function getViewUrlAttribute() { + return url('queue-submissions/view/'.$this->id); + } + + /** + * Get the admin URL (for processing purposes) of the submission/claim. + * + * @return string + */ + public function getAdminUrlAttribute() { + return url('admin/queue-submissions/edit/'.$this->id); + } + + /** + * Scope a query to only include user's logs. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param mixed $queue + * @param mixed $user + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeSubmitted($query, $queue, $user) { + return $query->where('queue_id', $queue)->where('user_id', $user)->where('status', '=', 'Approved')->orWhere('queue_id', $queue)->where('user_id', $user)->where('status', '=', 'Pending'); + } + + /** + * Scope a query to only include user's logs. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param mixed $queue + * @param mixed $user + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopePending($query, $queue, $user) { + return $query->where('queue_id', $queue)->where('user_id', $user)->where('status', '=', 'Pending')->orWhere('queue_id', $queue)->where('user_id', $user)->where('status', '=', 'Draft'); + } + + /** + * Get the rewards for the submission/claim. + * + * @return array + */ + public function getRewardsAttribute() { + if (isset($this->data['rewards'])) { + $assets = parseAssetData($this->data['rewards']); + } else { + $assets = parseAssetData($this->data); + } + $rewards = []; + foreach ($assets as $type => $a) { + $class = getAssetModelString($type, false); + foreach ($a as $id => $asset) { + $rewards[] = (object) [ + 'rewardable_type' => $class, + 'rewardable_id' => $id, + 'quantity' => $asset['quantity'], + ]; + } + } + + return $rewards; + } +} diff --git a/app/Models/Queue/QueueSubmissionCharacter.php b/app/Models/Queue/QueueSubmissionCharacter.php new file mode 100644 index 0000000000..0b212d1011 --- /dev/null +++ b/app/Models/Queue/QueueSubmissionCharacter.php @@ -0,0 +1,78 @@ + 'array', + ]; + + /********************************************************************************************** + + RELATIONS + + **********************************************************************************************/ + + /** + * Get the submission this is attached to. + */ + public function submission() { + return $this->belongsTo(QueueSubmission::class, 'queue_submission_id'); + } + + /** + * Get the character being attached to the submission. + */ + public function character() { + return $this->belongsTo(Character::class, 'character_id'); + } + + /********************************************************************************************** + + ACCESSORS + + **********************************************************************************************/ + + /** + * Get the artist of the item's image. + * + * @return string + */ + public function getIconArtistAttribute() { + if (!isset($this->data['artist_id'])) { + return null; + } + + $user = User::find($this->data['artist_id']); + if ($user) { + return $user->displayName; + } + + return null; + } +} diff --git a/app/Models/User/User.php b/app/Models/User/User.php index 9c729c1393..ce65735cdc 100644 --- a/app/Models/User/User.php +++ b/app/Models/User/User.php @@ -16,6 +16,7 @@ use App\Models\Item\ItemLog; use App\Models\Limit\UserUnlockedLimit; use App\Models\Notification; +use App\Models\Queue\QueueSubmission; use App\Models\Rank\Rank; use App\Models\Rank\RankPower; use App\Models\Shop\ShopLog; @@ -720,4 +721,15 @@ public function getSubmissions($user = null) { public function hasBookmarked($character) { return CharacterBookmark::where('user_id', $this->id)->where('character_id', $character->id)->first(); } + + /** + * Get the user's submissions. + * + * @param mixed|null $user + * + * @return \Illuminate\Pagination\LengthAwarePaginator + */ + public function getQueueSubmissions($user = null) { + return QueueSubmission::with('user')->viewable($user ? $user : null)->where('user_id', $this->id)->orderBy('id', 'DESC'); + } } diff --git a/app/Services/CommonSubmissionManager.php b/app/Services/CommonSubmissionManager.php new file mode 100644 index 0000000000..b2f9378ca0 --- /dev/null +++ b/app/Services/CommonSubmissionManager.php @@ -0,0 +1,374 @@ +find($stackId); + if (!$stack || $stack->user_id != $user->id) { + throw new \Exception('Invalid item selected.'); + } + if (!isset($data['stack_quantity'][$stackId])) { + throw new \Exception('Invalid quantity selected.'); + } + $stack->submission_count += $data['stack_quantity'][$stackId]; + $stack->save(); + + addAsset($userAssets, $stack, $data['stack_quantity'][$stackId]); + } + } + + // Attach currencies. + if (isset($data['currency_id'])) { + foreach ($data['currency_id'] as $holderKey=>$currencyIds) { + $holder = explode('-', $holderKey); + $holderType = $holder[0]; + $holderId = $holder[1]; + + $holder = User::find($holderId); + + $currencyManager = new CurrencyManager; + foreach ($currencyIds as $key=>$currencyId) { + $currency = Currency::find($currencyId); + if (!$currency) { + throw new \Exception('Invalid currency selected.'); + } + if ($data['currency_quantity'][$holderKey][$key] < 0) { + throw new \Exception('Cannot attach a negative amount of currency.'); + } + if (!$currencyManager->debitCurrency($holder, null, null, null, $currency, $data['currency_quantity'][$holderKey][$key])) { + throw new \Exception('Invalid currency/quantity selected.'); + } + + addAsset($userAssets, $currency, $data['currency_quantity'][$holderKey][$key]); + } + } + } + + // Get a list of rewards, then create the submission itself + $promptRewards = createAssetsArray(); + $characterRewards = createAssetsArray(); + if ($submission->status == 'Pending' && isset($submission->prompt_id) && $submission->prompt_id) { + foreach ($submission->prompt->rewards as $reward) { + if ($reward->rewardable_recipient == 'User') { + addAsset($promptRewards, $reward->reward, $reward->quantity); + } elseif ($reward->rewardable_recipient == 'Character') { + addAsset($characterRewards, $reward->reward, $reward->quantity); + } + } + } + $promptRewards = mergeAssetsArrays($promptRewards, $this->processRewards($data, false)); + + return [ + 'userAssets' => $userAssets, + 'promptRewards' => $promptRewards, + 'characterRewards' => $characterRewards, + ]; + } + + /** + * Creates character attachments for a submission. + * + * @param mixed $submission the submission object + * @param mixed $data the data for creating character attachments + * @param mixed|null $defaultRewards + * @param mixed|null $service + */ + protected function createCharacterAttachments($submission, $data, $defaultRewards = null, $service = null) { + DB::beginTransaction(); + + try { + // The character identification comes in both the slug field and as character IDs + // that key the reward ID/quantity arrays. + // We'll need to match characters to the rewards for them. + // First, check if the characters are accessible to begin with. + if (isset($data['slug'])) { + $characters = Character::myo(0)->visible()->whereIn('slug', $data['slug'])->get(); + if (count($characters) != count($data['slug'])) { + throw new \Exception('One or more of the selected characters do not exist.'); + } + } else { + $characters = []; + } + + if ($service) { + // process any relevant data + if (method_exists($service, 'processCharacters')) { + if (!$characterData = $service->processCharacters($submission->queue, $data, $submission)) { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + throw new \Exception('Failed to handle submission characters.'); + } + } + } else { + // Retrieve all reward IDs for characters + $currencyIds = []; + $itemIds = []; + $tableIds = []; + if (isset($data['character_currency_id'])) { + foreach ($data['character_currency_id'] as $c) { + foreach ($c as $currencyId) { + $currencyIds[] = $currencyId; + } + } // Non-expanded character rewards + } elseif (isset($data['character_rewardable_id'])) { + $data['character_rewardable_id'] = array_map([$this, 'innerNull'], $data['character_rewardable_id']); + foreach ($data['character_rewardable_id'] as $ckey => $c) { + foreach ($c as $key => $id) { + switch ($data['character_rewardable_type'][$ckey][$key]) { + case 'Currency': $currencyIds[] = $id; + break; + case 'Item': $itemIds[] = $id; + break; + case 'LootTable': $tableIds[] = $id; + break; + } + } + } // Expanded character rewards + } + array_unique($currencyIds); + array_unique($itemIds); + array_unique($tableIds); + $currencies = Currency::whereIn('id', $currencyIds)->where('is_character_owned', 1)->get()->keyBy('id'); + $items = Item::whereIn('id', $itemIds)->get()->keyBy('id'); + $tables = LootTable::whereIn('id', $tableIds)->get()->keyBy('id'); + } + + // Attach characters + foreach ($characters as $c) { + if ($service) { + // and for a specific character + if (method_exists($service, 'processCharacterAttachments')) { + if (!$assets = $service->processCharacterAttachments($submission->queue, $data + ['character_id' => $c->id], $submission)) { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + throw new \Exception('Failed to handle submission characters.'); + } + } + + // Now we have a clean set of assets (redundant data is gone, duplicate entries are merged) + // so we can attach the character to the submission + QueueSubmissionCharacter::create([ + 'character_id' => $c->id, + 'queue_submission_id' => $submission->id, + 'data' => method_exists($service, 'finalizeCharacterAttachments') + ? $service->finalizeCharacterAttachments($submission->queue, $data + ['character_id' => $c->id], $submission, Auth::user()) + : null, + ]); + } else { + // Users might not pass in clean arrays (may contain redundant data) so we need to clean that up + $assets = $this->processRewards($data + ['character_id' => $c->id, 'currencies' => $currencies, 'items' => $items, 'tables' => $tables], true); + + if ($defaultRewards) { + $assets = mergeAssetsArrays($assets, $defaultRewards); + } + + // Now we have a clean set of assets (redundant data is gone, duplicate entries are merged) + // so we can attach the character to the submission + SubmissionCharacter::create([ + 'character_id' => $c->id, + 'submission_id' => $submission->id, + 'data' => getDataReadyAssets($assets), + ]); + } + } + + return $this->commitReturn(true); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + /** + * Removes the attachments associated with a submission. + * + * @param mixed $submission the submission object + */ + protected function removeSubmissionAttachments($submission) { + $assets = $submission->data; + // Get a list of rewards, then create the submission itself + $rewards = createAssetsArray(); + $rewards = mergeAssetsArrays($rewards, parseAssetData($assets['rewards'])); + // TODO GENERICIZE THIS FUNCTION SOMEHOW + if (isset($submission->prompt_id) && $submission->prompt_id) { + foreach ($submission->prompt->rewards as $reward) { + if ($reward->rewardable_recipient != 'User') { + continue; + } + removeAsset($rewards, $reward->reward, $reward->quantity); + } + + // Remove character default rewards + foreach ($submission->characters as $c) { + $cRewards = parseAssetData($c->data); + foreach ($submission->prompt->rewards as $reward) { + if ($reward->rewardable_recipient != 'Character') { + continue; + } + removeAsset($cRewards, $reward->reward, $reward->quantity); + } + $c->update(['data' => getDataReadyAssets($cRewards)]); + } + } + + return $rewards; + } + + /** + * Removes attachments from a submission. + * + * @param mixed $submission the submission object + */ + protected function removeAttachments($submission) { + // This occurs when a draft is edited or rejected. + + // Return all added items + $addonData = $submission->data['user']; + if (isset($addonData['user_items'])) { + foreach ($addonData['user_items'] as $userItemId => $quantity) { + $userItemRow = UserItem::find($userItemId); + if (!$userItemRow) { + throw new \Exception('Cannot return an invalid item. ('.$userItemId.')'); + } + if ($userItemRow->submission_count < $quantity) { + throw new \Exception('Cannot return more items than was held. ('.$userItemId.')'); + } + $userItemRow->submission_count -= $quantity; + $userItemRow->save(); + } + } + + // And currencies + $currencyManager = new CurrencyManager; + if (isset($addonData['currencies']) && $addonData['currencies']) { + foreach ($addonData['currencies'] as $currencyId=>$quantity) { + $currency = Currency::find($currencyId); + if (!$currency) { + throw new \Exception('Cannot return an invalid currency. ('.$currencyId.')'); + } + if (!$currencyManager->creditCurrency(null, $submission->user, null, null, $currency, $quantity)) { + throw new \Exception('Could not return currency to user. ('.$currencyId.')'); + } + } + } + } + + /************************************************************************************************************** + * + * PROTECTED FUNCTIONS + * + **************************************************************************************************************/ + + /** + * Helper function to remove all empty/zero/falsey values. + * + * @param array $value + * + * @return array + */ + protected function innerNull($value) { + return array_values(array_filter($value)); + } + + /** + * Processes reward data into a format that can be used for distribution. + * + * @param array $data + * @param bool $isCharacter + * @param bool $isStaff + * @param bool $isClaim + * + * @return array + */ + protected function processRewards($data, $isCharacter, $isStaff = false, $isClaim = false) { + if ($isCharacter) { + $assets = createAssetsArray(true); + + if (isset($data['character_currency_id'][$data['character_id']]) && isset($data['character_quantity'][$data['character_id']])) { + foreach ($data['character_currency_id'][$data['character_id']] as $key => $currency) { + if ($data['character_quantity'][$data['character_id']][$key]) { + addAsset($assets, $data['currencies'][$currency], $data['character_quantity'][$data['character_id']][$key]); + } + } + } elseif (isset($data['character_rewardable_type'][$data['character_id']]) && isset($data['character_rewardable_id'][$data['character_id']]) && isset($data['character_rewardable_quantity'][$data['character_id']])) { + $data['character_rewardable_id'] = array_map([$this, 'innerNull'], $data['character_rewardable_id']); + + foreach ($data['character_rewardable_id'][$data['character_id']] as $key => $reward) { + switch ($data['character_rewardable_type'][$data['character_id']][$key]) { + case 'Currency': if ($data['character_rewardable_quantity'][$data['character_id']][$key]) { + addAsset($assets, $data['currencies'][$reward], $data['character_rewardable_quantity'][$data['character_id']][$key]); + } break; + case 'Item': if ($data['character_rewardable_quantity'][$data['character_id']][$key]) { + addAsset($assets, $data['items'][$reward], $data['character_rewardable_quantity'][$data['character_id']][$key]); + } break; + case 'LootTable': if ($data['character_rewardable_quantity'][$data['character_id']][$key]) { + addAsset($assets, $data['tables'][$reward], $data['character_rewardable_quantity'][$data['character_id']][$key]); + } break; + } + } + } + + return $assets; + } else { + $assets = createAssetsArray(false); + // Process the additional rewards + if (isset($data['rewardable_type']) && $data['rewardable_type']) { + foreach ($data['rewardable_type'] as $key => $type) { + $reward = null; + switch ($type) { + case 'Item': + $reward = Item::find($data['rewardable_id'][$key]); + break; + case 'Currency': + $reward = Currency::find($data['rewardable_id'][$key]); + if (!$reward->is_user_owned) { + throw new \Exception('Invalid currency selected.'); + } + break; + case 'LootTable': + if (!$isStaff) { + break; + } + $reward = LootTable::find($data['rewardable_id'][$key]); + break; + case 'Raffle': + if (!$isStaff && !$isClaim) { + break; + } + $reward = Raffle::find($data['rewardable_id'][$key]); + break; + } + if (!$reward) { + continue; + } + addAsset($assets, $reward, $data['quantity'][$key]); + } + } + + return $assets; + } + } +} diff --git a/app/Services/QueueService.php b/app/Services/QueueService.php new file mode 100644 index 0000000000..b16de97a18 --- /dev/null +++ b/app/Services/QueueService.php @@ -0,0 +1,434 @@ +populateCategoryData($data); + + $image = null; + if (isset($data['image']) && $data['image']) { + $data['has_image'] = 1; + $data['hash'] = randomString(10); + $image = $data['image']; + unset($data['image']); + } else { + $data['has_image'] = 0; + } + + $category = QueueCategory::create($data); + + if ($image) { + $this->handleImage($image, $category->categoryImagePath, $category->categoryImageFileName); + } + + return $this->commitReturn($category); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + /** + * Update a category. + * + * @param QueueCategory $category + * @param array $data + * @param \App\Models\User\User $user + * + * @return bool|QueueCategory + */ + public function updateQueueCategory($category, $data, $user) { + DB::beginTransaction(); + + try { + // More specific validation + if (QueueCategory::where('name', $data['name'])->where('id', '!=', $category->id)->exists()) { + throw new \Exception('The name has already been taken.'); + } + + $data = $this->populateCategoryData($data, $category); + + $image = null; + if (isset($data['image']) && $data['image']) { + $data['has_image'] = 1; + $data['hash'] = randomString(10); + $image = $data['image']; + unset($data['image']); + } + + $category->update($data); + + if ($category) { + $this->handleImage($image, $category->categoryImagePath, $category->categoryImageFileName); + } + + return $this->commitReturn($category); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + /** + * Delete a category. + * + * @param QueueCategory $category + * + * @return bool + */ + public function deleteQueueCategory($category) { + DB::beginTransaction(); + + try { + // Check first if the category is currently in use + if (Queue::where('queue_category_id', $category->id)->exists()) { + throw new \Exception('An queue with this category exists. Please change its category first.'); + } + + if ($category->has_image) { + $this->deleteImage($category->categoryImagePath, $category->categoryImageFileName); + } + $category->delete(); + + return $this->commitReturn(true); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + /** + * Sorts category order. + * + * @param array $data + * + * @return bool + */ + public function sortQueueCategory($data) { + DB::beginTransaction(); + + try { + // explode the sort array and reverse it since the order is inverted + $sort = array_reverse(explode(',', $data)); + + foreach ($sort as $key => $s) { + QueueCategory::where('id', $s)->update(['sort' => $key]); + } + + return $this->commitReturn(true); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + /********************************************************************************************** + + QUEUES + + **********************************************************************************************/ + + /** + * Creates a new queue. + * + * @param array $data + * @param \App\Models\User\User $user + * + * @return bool|Queue + */ + public function createQueue($data, $user) { + DB::beginTransaction(); + + try { + if (isset($data['queue_category_id']) && $data['queue_category_id'] == 'none') { + $data['queue_category_id'] = null; + } + + if ((isset($data['queue_category_id']) && $data['queue_category_id']) && !QueueCategory::where('id', $data['queue_category_id'])->exists()) { + throw new \Exception('The selected queue category is invalid.'); + } + + $data = $this->populateData($data); + + $image = null; + if (isset($data['image']) && $data['image']) { + $data['has_image'] = 1; + $data['hash'] = randomString(10); + $image = $data['image']; + unset($data['image']); + } else { + $data['has_image'] = 0; + } + + if (!isset($data['hide_submissions']) && !$data['hide_submissions']) { + $data['hide_submissions'] = 0; + } + + $queue = Queue::create($data); + + if ($image) { + $this->handleImage($image, $queue->imagePath, $queue->imageFileName); + } + + return $this->commitReturn($queue); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + /** + * Updates a queue. + * + * @param Queue $queue + * @param array $data + * @param \App\Models\User\User $user + * + * @return bool|Queue + */ + public function updateQueue($queue, $data, $user) { + DB::beginTransaction(); + + try { + if (isset($data['queue_category_id']) && $data['queue_category_id'] == 'none') { + $data['queue_category_id'] = null; + } + + // More specific validation + if (Queue::where('name', $data['name'])->where('id', '!=', $queue->id)->exists()) { + throw new \Exception('The name has already been taken.'); + } + if ((isset($data['queue_category_id']) && $data['queue_category_id']) && !QueueCategory::where('id', $data['queue_category_id'])->exists()) { + throw new \Exception('The selected queue category is invalid.'); + } + if (isset($data['prefix']) && Queue::where('prefix', $data['prefix'])->where('id', '!=', $queue->id)->exists()) { + throw new \Exception('That prefix has already been taken.'); + } + + $data = $this->populateData($data, $queue); + + $image = null; + if (isset($data['image']) && $data['image']) { + $data['has_image'] = 1; + $data['hash'] = randomString(10); + $image = $data['image']; + unset($data['image']); + } + + if (!isset($data['hide_submissions']) && !$data['hide_submissions']) { + $data['hide_submissions'] = 0; + } + + // clear data if changing type + if ($queue->queue_type !== $data['queue_type']) { + $queue->data = null; + $queue->save(); + } + + $queue->update($data); + + if ($queue) { + $this->handleImage($image, $queue->imagePath, $queue->imageFileName); + } + + return $this->commitReturn($queue); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + /** + * Deletes a queue. + * + * @param Queue $queue + * + * @return bool + */ + public function deleteQueue($queue) { + DB::beginTransaction(); + + try { + // Check first if the category is currently in use + if (QueueSubmission::where('queue_id', $queue->id)->exists()) { + throw new \Exception('A submission under this queue exists. Deleting the queue will break the submission page - consider setting the queue to be not active instead.'); + } + + if ($queue->has_image) { + $this->deleteImage($queue->imagePath, $queue->imageFileName); + } + $queue->delete(); + + return $this->commitReturn(true); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + /** + * Update the queue's type data. + * + * @param array $data + * @param mixed $queue + * + * @return bool + */ + public function updateType($queue, $data) { + DB::beginTransaction(); + + try { + if (isset($data['item_id'])) { + foreach ($data['item_id'] as $item) { + if (!isset($item)) { + throw new \Exception('One of the items was not specified.'); + } + } + } + + $queue->data = $queue->service->updateData($queue, $data) + + [ + 'items' => $data['item_id'] ?? null, + ] + (isset($data['item_id']) ? [ + 'items' => $data['item_id'], + ] : []); + $queue->save(); + + return $this->commitReturn(true); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + /** + * Handle category data. + * + * @param array $data + * @param QueueCategory|null $category + * + * @return array + */ + private function populateCategoryData($data, $category = null) { + if (isset($data['description']) && $data['description']) { + $data['parsed_description'] = parse($data['description']); + } elseif (!isset($data['description']) && !$data['description']) { + $data['parsed_description'] = null; + } + + if (isset($data['remove_image'])) { + if ($category && $category->has_image && $data['remove_image']) { + $data['has_image'] = 0; + $this->deleteImage($category->categoryImagePath, $category->categoryImageFileName); + } + unset($data['remove_image']); + } + + if (!isset($data['display'])) { + $data['display'] = 0; + } + + return $data; + } + + /** + * Processes user input for creating/updating a queue. + * + * @param array $data + * @param Queue $queue + * + * @return array + */ + private function populateData($data, $queue = null) { + if (isset($data['description']) && $data['description']) { + $data['parsed_description'] = parse($data['description']); + } + + if (!isset($data['hide_before_start'])) { + $data['hide_before_start'] = 0; + } + if (!isset($data['hide_after_end'])) { + $data['hide_after_end'] = 0; + } + if (!isset($data['is_active'])) { + $data['is_active'] = 0; + } + if (!isset($data['staff_only'])) { + $data['staff_only'] = 0; + } + + if (isset($data['form']) && $data['form']) { + $data['parsed_form'] = parse($data['form']); + } + + if (isset($data['remove_image'])) { + if ($queue && $queue->has_image && $data['remove_image']) { + $data['has_image'] = 0; + $this->deleteImage($queue->imagePath, $queue->imageFileName); + } + unset($data['remove_image']); + } + + if (isset($data['check_text'])) { + foreach ($data['check_text'] as $check) { + if (!isset($check)) { + throw new \Exception('One of the checklist steps was not specified.'); + } + } + $data['checklist'] = $data['check_text']; + } + + if (isset($data['user_rewardable_type'])) { + $data['output']['users'] = encodeForDataColumn($data, false, false, 'user_'); + } else { + $data['output']['users'] = null; + } + if (isset($data['character_rewardable_type'])) { + $data['output']['characters'] = encodeForDataColumn($data, false, true, 'character_'); + } else { + $data['output']['characters'] = null; + } + + return $data; + } +} diff --git a/app/Services/QueueSubmissionManager.php b/app/Services/QueueSubmissionManager.php new file mode 100644 index 0000000000..82b892f572 --- /dev/null +++ b/app/Services/QueueSubmissionManager.php @@ -0,0 +1,610 @@ +staff_only && !$user->isStaff) { + throw new \Exception('This queue may only be submitted to by staff members.'); + } + + if (!$queue->checkConcurrentSubmissionLimit($user)) { + throw new \Exception('This queue does not permit you to submit more submissions while you have '.$queue->limit_concurrent.' of them of them pending or in draft at the same time. Please wait for your submissions to be processed before trying to submit again.'); + } + + if ($queue->limit) { + if (!$queue->checkSubmissionLimit($user)) { + throw new \Exception('You have already submitted to this queue the maximum number of times.'); + } + } + + if (isset($data['comments']) && $data['comments']) { + $data['parsed_comments'] = parse($data['comments']); + } else { + $data['parsed_comments'] = null; + } + + // Create the submission itself. + $submission = QueueSubmission::create([ + 'user_id' => $user->id, + 'status' => $isDraft ? 'Draft' : 'Pending', + 'comments' => $data['comments'], + 'parsed_comments' => $data['parsed_comments'], + 'data' => null, + 'queue_id' => $queue->id, + ]); + + // Then, re-attach everything fresh. + $assets = $this->createUserAttachments($submission, $data, $user); + $userAssets = $queue->configSet('consume_items') ? $assets['userAssets'] : []; + $queueRewards = $assets['promptRewards']; + $characterRewards = $queue->configSet('character_submit') ? $assets['characterRewards'] : []; + + // carry out the initial processes when submitting the queue's form + if ($queue->service && method_exists($queue->service, 'submit')) { + if (!$service->submit($queue, $data, $user, $submission)) { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + throw new \Exception('Failed to handle submission.'); + } + } + + $submission->update([ + 'data' => [ + 'user' => $queue->configSet('consume_items') ? + Arr::only(getDataReadyAssets($userAssets), ['user_items', 'currencies']) : [], + 'rewards' => getDataReadyAssets($queueRewards), + 'character_rewards' => getDataReadyAssets($characterRewards), + 'queue' => ($queue->service && method_exists($queue->service, 'processSubmission')) ? + $queue->service->processSubmission($queue, $data, $user, $submission) : null, + ], + ]); + + if ($queue->configSet('character_submit')) { + if (!$this->createCharacterAttachments($submission, $data, [], $service)) { + throw new \Exception('Failed to handle submission characters.'); + } + } + + return $this->commitReturn($submission); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + /** + * Edits an existing submission. + * + * @param array $data + * @param User $user + * @param mixed $submission + * @param mixed $isSubmit + * + * @return mixed + */ + public function editSubmission($submission, $data, $user, $isSubmit = false) { + DB::beginTransaction(); + + try { + // 1. check that the queue can be submitted at this time + // 2. check that the characters selected exist (are visible too) + // 3. check that the currencies selected can be attached to characters + if (!Settings::get('is_queue_open')) { + throw new \Exception('The queue is closed for submissions.'); + } + $queue = $submission->queue; + if (!$queue) { + throw new \Exception('Invalid queue selected.'); + } + + // First, return all items and currency applied. + // Also, as this is an edit, delete all attached characters to be re-applied later. + if ($queue->configSet('consume_items')) { + $this->removeAttachments($submission); + } + + if ($queue->configSet('character_submit')) { + QueueSubmissionCharacter::where('queue_submission_id', $submission->id)->delete(); + } + + if ($isSubmit) { + $submission->update(['status' => 'Pending', 'submitted_at' => Carbon::now()]); + } + + // Then, re-attach everything fresh. + $assets = $this->createUserAttachments($submission, $data, $user); + $userAssets = $queue->configSet('consume_items') ? $assets['userAssets'] : []; + $queueRewards = $assets['promptRewards']; + $characterRewards = $queue->configSet('character_submit') ? $assets['characterRewards'] : []; + if ($queue->configSet('character_submit')) { + if (!$this->createCharacterAttachments($submission, $data, null, $service)) { + throw new \Exception('Failed to handle submission characters.'); + } + } + + // carry out the initial processes when submitting the queue's form + if ($queue->service && method_exists($queue->service, 'submit')) { + if (!$service->submit($queue, $data, $user, $submission)) { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + throw new \Exception('Failed to handle submission.'); + } + } + + if (isset($data['comments']) && $data['comments']) { + $data['parsed_comments'] = parse($data['comments']); + } else { + $data['parsed_comments'] = null; + } + + // Modify submission + $submission->update([ + 'updated_at' => Carbon::now(), + 'comments' => $data['comments'], + 'parsed_comments' => $data['parsed_comments'], + 'queue_id' => $queue->id, + 'data' => [ + 'user' => $queue->configSet('consume_items') ? Arr::only(getDataReadyAssets($userAssets), ['user_items', 'currencies']) : [], + 'queue' => ($queue->service && method_exists($queue->service, 'processSubmission')) ? + $queue->service->processSubmission($queue, $data, $user, $submission) : null, + 'rewards' => getDataReadyAssets($queueRewards), + ], + ]); + + return $this->commitReturn($submission); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + /** + * Cancels a submission. + * + * @param mixed $data the submission data + * @param mixed $user the user performing the cancellation + */ + public function cancelSubmission($data, $user) { + DB::beginTransaction(); + + try { + // 1. check that the submission exists + // 2. check that the submission is pending + if (!isset($data['submission'])) { + $submission = QueueSubmission::where('status', 'Pending')->where('id', $data['id'])->first(); + } elseif ($data['submission']->status == 'Pending') { + $submission = $data['submission']; + } else { + $submission = null; + } + if (!$submission) { + throw new \Exception('Invalid submission.'); + } + + // Set staff comments + if (isset($data['staff_comments']) && $data['staff_comments']) { + $data['parsed_staff_comments'] = parse($data['staff_comments']); + } else { + $data['parsed_staff_comments'] = null; + } + + $assets = $submission->data; + if ($submission->queue->configSet('consume_items')) { + $userAssets = $assets['user']; + } + $qAssets = $assets['queue']; + // Remove queue-only rewards + $queueRewards = $this->removeSubmissionAttachments($submission); + + if ($user->id != $submission->user_id) { + // The only things we need to set are: + // 1. staff comment + // 2. staff ID + // 3. status + $submission->update([ + 'staff_comments' => $data['staff_comments'], + 'parsed_staff_comments' => $data['parsed_staff_comments'], + 'updated_at' => Carbon::now(), + 'staff_id' => $user->id, + 'status' => 'Draft', + 'data' => [ + 'user' => $submission->queue->configSet('consume_items') ? $userAssets : null, + 'queue' => $qAssets, + 'rewards' => getDataReadyAssets($queueRewards), + ], + ]); + + Notifications::create('QUEUE_SUBMISSION_CANCELLED', $submission->user, [ + 'queue_name' => $submission->queue->name, + 'staff_url' => $user->url, + 'staff_name' => $user->name, + 'submission_id' => $submission->id, + ]); + } else { + // This is when a user cancels their own submission back into draft form + $submission->update([ + 'status' => 'Draft', + 'updated_at' => Carbon::now(), + 'data' => [ + 'user' => $submission->queue->configSet('consume_items') ? $userAssets : null, + 'queue' => $qAssets, + 'rewards' => getDataReadyAssets($queueRewards), + ], + ]); + } + + return $this->commitReturn($submission); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + /** + * Rejects a submission. + * + * @param array $data + * @param User $user + * + * @return mixed + */ + public function rejectSubmission($data, $user) { + DB::beginTransaction(); + + try { + // 1. check that the submission exists + // 2. check that the submission is pending + if (!isset($data['submission'])) { + $submission = QueueSubmission::where('status', 'Pending')->where('id', $data['id'])->first(); + } elseif ($data['submission']->status == 'Pending') { + $submission = $data['submission']; + } else { + $submission = null; + } + if (!$submission) { + throw new \Exception('Invalid submission.'); + } + + $queue = $submission->queue; + + if ($queue->configSet('consume_items')) { + // Return all items and currency applied. + $this->removeAttachments($submission); + } + + if (isset($data['staff_comments']) && $data['staff_comments']) { + $data['parsed_staff_comments'] = parse($data['staff_comments']); + } else { + $data['parsed_staff_comments'] = null; + } + + // The only things we need to set are: + // 1. staff comment + // 2. staff ID + // 3. status + $submission->update([ + 'staff_comments' => $data['staff_comments'], + 'parsed_staff_comments' => $data['parsed_staff_comments'], + 'staff_id' => $user->id, + 'status' => 'Rejected', + ]); + + Notifications::create('QUEUE_SUBMISSION_REJECTED', $submission->user, [ + 'queue_name' => $submission->queue->name, + 'staff_url' => $user->url, + 'staff_name' => $user->name, + 'submission_id' => $submission->id, + ]); + + if (!$this->logAdminAction($user, 'Submission Rejected', 'Rejected submission #'.$submission->id.'')) { + throw new \Exception('Failed to log admin action.'); + } + + return $this->commitReturn($submission); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + /** + * Approves a submission. + * + * @param array $data + * @param User $user + * + * @return mixed + */ + public function approveSubmission($data, $user) { + DB::beginTransaction(); + + try { + // 1. check that the submission exists + // 2. check that the submission is pending + $submission = QueueSubmission::where('status', 'Pending')->where('id', $data['id'])->first(); + if (!$submission) { + throw new \Exception('Invalid submission.'); + } + + $queue = $submission->queue; + if ($queue->configSet('consume_items')) { + // Remove any added items, hold counts, and add logs + $addonData = $submission->data['user']; + $inventoryManager = new InventoryManager; + if (isset($addonData['user_items'])) { + $stacks = $addonData['user_items']; + foreach ($addonData['user_items'] as $userItemId => $quantity) { + $userItemRow = UserItem::find($userItemId); + if (!$userItemRow) { + throw new \Exception('Cannot return an invalid item. ('.$userItemId.')'); + } + if ($userItemRow->submission_count < $quantity) { + throw new \Exception('Cannot return more items than was held. ('.$userItemId.')'); + } + $userItemRow->submission_count -= $quantity; + $userItemRow->save(); + } + + // Workaround for user not being unset after inventory shuffling, preventing proper staff ID assignment + $staff = $user; + + foreach ($stacks as $stackId => $quantity) { + $stack = UserItem::find($stackId); + $user = User::find($submission->user_id); + if (!$inventoryManager->debitStack($user, 'Queue Submission Approved', ['data' => 'Item used in submission (#'.$submission->id.')'], $stack, $quantity)) { + throw new \Exception('Failed to create log for item stack.'); + } + } + + // Set user back to the processing staff member, now that addons have been properly processed. + $user = $staff; + } + + // Log currency removal, etc. + $currencyManager = new CurrencyManager; + if (isset($addonData['currencies']) && $addonData['currencies']) { + foreach ($addonData['currencies'] as $currencyId => $quantity) { + $currency = Currency::find($currencyId); + if (!$currencyManager->createLog( + $submission->user_id, + 'User', + null, + null, + 'Queue Submission Approved', + 'Used in submission (#'.$submission->id.')', + $currencyId, + $quantity + )) { + throw new \Exception('Failed to create currency log.'); + } + } + } + } + + // Get the updated set of rewards + $rewards = $this->processRewards($data, false, true); + + // Logging data + $queueLogType = 'Queue Rewards'; + $queueData = [ + 'data' => 'Received rewards for submission (#'.$submission->id.')', + ]; + + // Distribute user rewards + if (!$rewards = fillUserAssets($rewards, $user, $submission->user, $queueLogType, $queueData)) { + throw new \Exception('Failed to distribute rewards to user.'); + } + + if ($queue->configSet('character_submit')) { + // The character identification comes in both the slug field and as character IDs + // that key the reward ID/quantity arrays. + // We'll need to match characters to the rewards for them. + // First, check if the characters are accessible to begin with. + if (isset($data['slug'])) { + $characters = Character::myo(0)->visible()->whereIn('slug', $data['slug'])->get(); + if (count($characters) != count($data['slug'])) { + throw new \Exception('One or more of the selected characters do not exist.'); + } + } else { + $characters = []; + } + + // process any relevant data + if (method_exists($service, 'processCharacters')) { + if (!$chardata = $service->processCharacters($submission->queue, $data, $submission)) { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + throw new \Exception('Failed to handle submission characters.'); + } + } + + // We're going to remove all characters from the submission and reattach them with the updated data + $submission->characters()->delete(); + + // Distribute character rewards + foreach ($characters as $c) { + if ($queue->service && method_exists($queue->service, 'processCharacterAttachments')) { + if (!$assets = $queue->service->processCharacterAttachments($submission->queue, $data + ['character_id' => $c->id], $submission)) { + foreach ($queue->service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + throw new \Exception('Failed to handle submission characters.'); + } + } else { + // Users might not pass in clean arrays (may contain redundant data) so we need to clean that up + $assets = $this->processRewards($data + ['character_id' => $c->id, 'currencies' => $currencies, 'items' => $items, 'tables' => $tables], true); + + if (!$assets = fillCharacterAssets($assets, $user, $c, $promptLogType, $promptData, $submission->user)) { + throw new \Exception('Failed to distribute rewards to character.'); + } + } + + QueueSubmissionCharacter::create([ + 'character_id' => $c->id, + 'queue_submission_id' => $submission->id, + 'data' => $queue->service && method_exists($queue->service, 'finalizeCharacterAttachments') ? + $queue->service->finalizeCharacterAttachments($submission->queue, $data + ['character_id' => $c->id], $submission, $user) : null, + ]); + } + } + + if (isset($data['staff_comments']) && $data['staff_comments']) { + $data['parsed_staff_comments'] = parse($data['staff_comments']); + } else { + $data['parsed_staff_comments'] = null; + } + + if ($queue->service && method_exists($queue->service, 'approve')) { + if (!$service->approve($queue, $data, $user, $submission)) { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + throw new \Exception('Failed to handle submission.'); + } + } + + // Finally, set: + // 1. staff comments + // 2. staff ID + // 3. status + // 4. final rewards + $submission->update([ + 'staff_comments' => $data['staff_comments'], + 'parsed_staff_comments' => $data['parsed_staff_comments'], + 'staff_id' => $user->id, + 'status' => 'Approved', + 'data' => [ + 'user' => $queue->configSet('consume_items') ? $addonData : null, + 'queue' => ($queue->service && method_exists($queue->service, 'processApprove')) ? + $queue->service->processApprove($queue, $data, $user, $submission) : $submission->data['queue'], + 'rewards' => getDataReadyAssets($rewards), + ], // list of rewards + ]); + + Notifications::create('QUEUE_SUBMISSION_APPROVED', $submission->user, [ + 'queue_name' => $submission->queue->name, + 'staff_url' => $user->url, + 'staff_name' => $user->name, + 'submission_id' => $submission->id, + ]); + + if (!$this->logAdminAction($user, 'Submission Approved', 'Approved submission #'.$submission->id.'')) { + throw new \Exception('Failed to log admin action.'); + } + + return $this->commitReturn($submission); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + /** + * Deletes a submission. + * + * @param mixed $data the data of the submission to be deleted + * @param mixed $user the user performing the deletion + * @param mixed $submission + */ + public function deleteSubmission($submission, $data, $user) { + DB::beginTransaction(); + try { + // 1. check that the submission exists + // 2. check that the submission is a draft + if (!isset($data['submission'])) { + $submission = QueueSubmission::where('status', 'Draft')->where('id', $data['submission_id'])->first(); + } elseif ($data['submission']->status == 'Pending') { + $submission = $data['submission']; + } else { + $submission = null; + } + if (!$submission) { + throw new \Exception('Invalid submission.'); + } + if ($user->id != $submission->user_id) { + throw new \Exception('This is not your submission.'); + } + + $queue = $submission->queue; + + // carry out custom deletions + $service = $submission->queue->service; + + if (method_exists($queue->service, 'delete')) { + if (!$service->delete($queue, $data, $user, $submission)) { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + throw new \Exception('Failed to handle submission.'); + } + } + + if ($submission->queue->configSet('character_submit')) { + // Remove characters and attachments. + QueueSubmissionCharacter::where('queue_submission_id', $submission->id)->delete(); + } + + if ($submission->queue->configSet('consume_items')) { + $this->removeAttachments($submission); + } + $submission->delete(); + + return $this->commitReturn($submission); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } +} diff --git a/app/Services/SubmissionManager.php b/app/Services/SubmissionManager.php index 55292c1f99..b8c2722bfc 100644 --- a/app/Services/SubmissionManager.php +++ b/app/Services/SubmissionManager.php @@ -9,7 +9,6 @@ use App\Models\Item\Item; use App\Models\Loot\LootTable; use App\Models\Prompt\Prompt; -use App\Models\Raffle\Raffle; use App\Models\Submission\Submission; use App\Models\Submission\SubmissionCharacter; use App\Models\User\User; @@ -18,7 +17,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; -class SubmissionManager extends Service { +class SubmissionManager extends CommonSubmissionManager { /* |-------------------------------------------------------------------------- | Submission Manager @@ -271,7 +270,7 @@ public function cancelSubmission($data, $user) { $assets = $submission->data; $userAssets = $assets['user']; // Remove prompt-only rewards - $promptRewards = $this->removePromptAttachments($submission); + $promptRewards = $this->removeSubmissionAttachments($submission); if ($user->id != $submission->user_id) { // The only things we need to set are: @@ -613,329 +612,4 @@ public function deleteSubmission($data, $user) { return $this->rollbackReturn(false); } - - /************************************************************************************************************** - * - * PRIVATE FUNCTIONS - * - **************************************************************************************************************/ - - /** - * Helper function to remove all empty/zero/falsey values. - * - * @param array $value - * - * @return array - */ - private function innerNull($value) { - return array_values(array_filter($value)); - } - - /** - * Processes reward data into a format that can be used for distribution. - * - * @param array $data - * @param bool $isCharacter - * @param bool $isStaff - * @param bool $isClaim - * - * @return array - */ - private function processRewards($data, $isCharacter, $isStaff = false, $isClaim = false) { - if ($isCharacter) { - $assets = createAssetsArray(true); - - if (isset($data['character_currency_id'][$data['character_id']]) && isset($data['character_quantity'][$data['character_id']])) { - foreach ($data['character_currency_id'][$data['character_id']] as $key => $currency) { - if ($data['character_quantity'][$data['character_id']][$key]) { - addAsset($assets, $data['currencies'][$currency], $data['character_quantity'][$data['character_id']][$key]); - } - } - } elseif (isset($data['character_rewardable_type'][$data['character_id']]) && isset($data['character_rewardable_id'][$data['character_id']]) && isset($data['character_rewardable_quantity'][$data['character_id']])) { - $data['character_rewardable_id'] = array_map([$this, 'innerNull'], $data['character_rewardable_id']); - - foreach ($data['character_rewardable_id'][$data['character_id']] as $key => $reward) { - switch ($data['character_rewardable_type'][$data['character_id']][$key]) { - case 'Currency': if ($data['character_rewardable_quantity'][$data['character_id']][$key]) { - addAsset($assets, $data['currencies'][$reward], $data['character_rewardable_quantity'][$data['character_id']][$key]); - } break; - case 'Item': if ($data['character_rewardable_quantity'][$data['character_id']][$key]) { - addAsset($assets, $data['items'][$reward], $data['character_rewardable_quantity'][$data['character_id']][$key]); - } break; - case 'LootTable': if ($data['character_rewardable_quantity'][$data['character_id']][$key]) { - addAsset($assets, $data['tables'][$reward], $data['character_rewardable_quantity'][$data['character_id']][$key]); - } break; - } - } - } - - return $assets; - } else { - $assets = createAssetsArray(false); - // Process the additional rewards - if (isset($data['rewardable_type']) && $data['rewardable_type']) { - foreach ($data['rewardable_type'] as $key => $type) { - $reward = null; - switch ($type) { - case 'Item': - $reward = Item::find($data['rewardable_id'][$key]); - break; - case 'Currency': - $reward = Currency::find($data['rewardable_id'][$key]); - if (!$reward->is_user_owned) { - throw new \Exception('Invalid currency selected.'); - } - break; - case 'LootTable': - if (!$isStaff) { - break; - } - $reward = LootTable::find($data['rewardable_id'][$key]); - break; - case 'Raffle': - if (!$isStaff && !$isClaim) { - break; - } - $reward = Raffle::find($data['rewardable_id'][$key]); - break; - } - if (!$reward) { - continue; - } - addAsset($assets, $reward, $data['quantity'][$key]); - } - } - - return $assets; - } - } - - /************************************************************************************************************** - * - * ATTACHMENT FUNCTIONS - * - **************************************************************************************************************/ - - /** - * Creates user attachments for a submission. - * - * @param mixed $submission the submission object - * @param mixed $data the data for creating the attachments - * @param mixed $user the user object - */ - private function createUserAttachments($submission, $data, $user) { - $userAssets = createAssetsArray(); - - // Attach items. Technically, the user doesn't lose ownership of the item - we're just adding an additional holding field. - // We're also not going to add logs as this might add unnecessary fluff to the logs and the items still belong to the user. - if (isset($data['stack_id'])) { - foreach ($data['stack_id'] as $stackId) { - $stack = UserItem::with('item')->find($stackId); - if (!$stack || $stack->user_id != $user->id) { - throw new \Exception('Invalid item selected.'); - } - if (!isset($data['stack_quantity'][$stackId])) { - throw new \Exception('Invalid quantity selected.'); - } - $stack->submission_count += $data['stack_quantity'][$stackId]; - $stack->save(); - - addAsset($userAssets, $stack, $data['stack_quantity'][$stackId]); - } - } - - // Attach currencies. - if (isset($data['currency_id'])) { - foreach ($data['currency_id'] as $holderKey=>$currencyIds) { - $holder = explode('-', $holderKey); - $holderType = $holder[0]; - $holderId = $holder[1]; - - $holder = User::find($holderId); - - $currencyManager = new CurrencyManager; - foreach ($currencyIds as $key=>$currencyId) { - $currency = Currency::find($currencyId); - if (!$currency) { - throw new \Exception('Invalid currency selected.'); - } - if ($data['currency_quantity'][$holderKey][$key] < 0) { - throw new \Exception('Cannot attach a negative amount of currency.'); - } - if (!$currencyManager->debitCurrency($holder, null, null, null, $currency, $data['currency_quantity'][$holderKey][$key])) { - throw new \Exception('Invalid currency/quantity selected.'); - } - - addAsset($userAssets, $currency, $data['currency_quantity'][$holderKey][$key]); - } - } - } - - // Get a list of rewards, then create the submission itself - $promptRewards = createAssetsArray(); - $characterRewards = createAssetsArray(); - if ($submission->status == 'Pending' && isset($submission->prompt_id) && $submission->prompt_id) { - foreach ($submission->prompt->rewards as $reward) { - if ($reward->rewardable_recipient == 'User') { - addAsset($promptRewards, $reward->reward, $reward->quantity); - } elseif ($reward->rewardable_recipient == 'Character') { - addAsset($characterRewards, $reward->reward, $reward->quantity); - } - } - } - $promptRewards = mergeAssetsArrays($promptRewards, $this->processRewards($data, false)); - - return [ - 'userAssets' => $userAssets, - 'promptRewards' => $promptRewards, - 'characterRewards' => $characterRewards, - ]; - } - - /** - * Removes the attachments associated with a prompt from a submission. - * - * @param mixed $submission the submission object - */ - private function removePromptAttachments($submission) { - $assets = $submission->data; - // Get a list of rewards, then create the submission itself - $promptRewards = createAssetsArray(); - $promptRewards = mergeAssetsArrays($promptRewards, parseAssetData($assets['rewards'])); - if (isset($submission->prompt_id) && $submission->prompt_id) { - foreach ($submission->prompt->rewards as $reward) { - if ($reward->rewardable_recipient != 'User') { - continue; - } - removeAsset($promptRewards, $reward->reward, $reward->quantity); - } - - // Remove character default rewards - foreach ($submission->characters as $c) { - $cRewards = parseAssetData($c->data); - foreach ($submission->prompt->rewards as $reward) { - if ($reward->rewardable_recipient != 'Character') { - continue; - } - removeAsset($cRewards, $reward->reward, $reward->quantity); - } - $c->update(['data' => getDataReadyAssets($cRewards)]); - } - } - - return $promptRewards; - } - - /** - * Creates character attachments for a submission. - * - * @param mixed $submission the submission object - * @param mixed $data the data for creating character attachments - * @param mixed|null $defaultRewards - */ - private function createCharacterAttachments($submission, $data, $defaultRewards = null) { - // The character identification comes in both the slug field and as character IDs - // that key the reward ID/quantity arrays. - // We'll need to match characters to the rewards for them. - // First, check if the characters are accessible to begin with. - if (isset($data['slug'])) { - $characters = Character::myo(0)->visible()->whereIn('slug', $data['slug'])->get(); - if (count($characters) != count($data['slug'])) { - throw new \Exception('One or more of the selected characters do not exist.'); - } - } else { - $characters = []; - } - - // Retrieve all reward IDs for characters - $currencyIds = []; - $itemIds = []; - $tableIds = []; - if (isset($data['character_currency_id'])) { - foreach ($data['character_currency_id'] as $c) { - foreach ($c as $currencyId) { - $currencyIds[] = $currencyId; - } - } // Non-expanded character rewards - } elseif (isset($data['character_rewardable_id'])) { - $data['character_rewardable_id'] = array_map([$this, 'innerNull'], $data['character_rewardable_id']); - foreach ($data['character_rewardable_id'] as $ckey => $c) { - foreach ($c as $key => $id) { - switch ($data['character_rewardable_type'][$ckey][$key]) { - case 'Currency': $currencyIds[] = $id; - break; - case 'Item': $itemIds[] = $id; - break; - case 'LootTable': $tableIds[] = $id; - break; - } - } - } // Expanded character rewards - } - array_unique($currencyIds); - array_unique($itemIds); - array_unique($tableIds); - $currencies = Currency::whereIn('id', $currencyIds)->where('is_character_owned', 1)->get()->keyBy('id'); - $items = Item::whereIn('id', $itemIds)->get()->keyBy('id'); - $tables = LootTable::whereIn('id', $tableIds)->get()->keyBy('id'); - - // Attach characters - foreach ($characters as $c) { - // Users might not pass in clean arrays (may contain redundant data) so we need to clean that up - $assets = $this->processRewards($data + ['character_id' => $c->id, 'currencies' => $currencies, 'items' => $items, 'tables' => $tables], true); - - if ($defaultRewards) { - $assets = mergeAssetsArrays($assets, $defaultRewards); - } - - // Now we have a clean set of assets (redundant data is gone, duplicate entries are merged) - // so we can attach the character to the submission - SubmissionCharacter::create([ - 'character_id' => $c->id, - 'submission_id' => $submission->id, - 'data' => getDataReadyAssets($assets), - ]); - } - - return true; - } - - /** - * Removes attachments from a submission. - * - * @param mixed $submission the submission object - */ - private function removeAttachments($submission) { - // This occurs when a draft is edited or rejected. - - // Return all added items - $addonData = $submission->data['user']; - if (isset($addonData['user_items'])) { - foreach ($addonData['user_items'] as $userItemId => $quantity) { - $userItemRow = UserItem::find($userItemId); - if (!$userItemRow) { - throw new \Exception('Cannot return an invalid item. ('.$userItemId.')'); - } - if ($userItemRow->submission_count < $quantity) { - throw new \Exception('Cannot return more items than was held. ('.$userItemId.')'); - } - $userItemRow->submission_count -= $quantity; - $userItemRow->save(); - } - } - - // And currencies - $currencyManager = new CurrencyManager; - if (isset($addonData['currencies']) && $addonData['currencies']) { - foreach ($addonData['currencies'] as $currencyId=>$quantity) { - $currency = Currency::find($currencyId); - if (!$currency) { - throw new \Exception('Cannot return an invalid currency. ('.$currencyId.')'); - } - if (!$currencyManager->creditCurrency(null, $submission->user, null, null, $currency, $quantity)) { - throw new \Exception('Could not return currency to user. ('.$currencyId.')'); - } - } - } - } } diff --git a/app/Services/UserService.php b/app/Services/UserService.php index 1ebae4f634..db800b2308 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -504,6 +504,13 @@ public function ban($data, $user, $staff) { $tradeManager->rejectTrade(['trade' => $trade, 'reason' => 'User has been banned from site activity.'], $staff); } + // 6. Queues + $qsubmissionManager = new QueueSubmissionManager; + $qsubmissions = QueueSubmission::where('user_id', $user->id)->where('status', 'Pending')->get(); + foreach ($qsubmissions as $qsubmission) { + $qsubmissionManager->rejectSubmission(['submission' => $qsubmission, 'staff_comments' => 'User has been banned from site activity.'], $staff); + } + UserUpdateLog::create(['staff_id' => $staff->id, 'user_id' => $user->id, 'data' => ['is_banned' => 'Yes', 'ban_reason' => $data['ban_reason'] ?? null], 'type' => 'Ban']); $user->settings->banned_at = Carbon::now(); diff --git a/config/lorekeeper/admin_sidebar.php b/config/lorekeeper/admin_sidebar.php index e751c017cf..229404b110 100644 --- a/config/lorekeeper/admin_sidebar.php +++ b/config/lorekeeper/admin_sidebar.php @@ -103,6 +103,10 @@ 'name' => 'Trades', 'url' => 'admin/trades/incoming', ], + [ + 'name' => 'Queue Submissions', + 'url' => 'admin/queue-submissions', + ], ], ], 'Grants' => [ @@ -202,6 +206,10 @@ 'name' => 'Dynamic Limits', 'url' => 'admin/data/limits', ], + [ + 'name' => 'Queues', + 'url' => 'admin/data/queues', + ], ], ], 'Raffles' => [ diff --git a/config/lorekeeper/extensions.php b/config/lorekeeper/extensions.php index 8c484c890e..ba6ee976af 100644 --- a/config/lorekeeper/extensions.php +++ b/config/lorekeeper/extensions.php @@ -156,4 +156,10 @@ 'Quarter' => 'Quarter', // This is once every three months. 'Year' => 'Year', ], + + // queue-creator - SUPERCOOL + 'queue_creator' => [ + 'expand_in_admin_index' => 0, // 1 un-nests the queues on the admin index page and lists them as individual cards with their own counts instead + 'expand_in_user_menu' => 0, // 1 un-nests the queue submissions on the user menu page and gives each one a unique page with draft/pending/approved/rejected tabs + ], ]; diff --git a/config/lorekeeper/notifications.php b/config/lorekeeper/notifications.php index 574677c4e1..67e2e4ada5 100644 --- a/config/lorekeeper/notifications.php +++ b/config/lorekeeper/notifications.php @@ -471,4 +471,25 @@ 'message' => '{sender} has added you as a participant on a gallery submission. (View Submission)', 'url' => 'gallery/view/{submission_id}', ], + + // QUEUE_SUBMISSION_APPROVED + 1116 => [ + 'name' => 'Queue Submission Approved', + 'message' => 'Your {queue_name} submission (#{submission_id}) was approved by {staff_name}. (View Submission)', + 'url' => 'queue-submissions/view/{submission_id}', + ], + + // QUEUE_SUBMISSION_REJECTED + 1117 => [ + 'name' => 'Queue Submission Rejected', + 'message' => 'Your {queue_name} submission (#{submission_id}) was rejected by {staff_name}. (View Submission)', + 'url' => 'queue-submissions/view/{submission_id}', + ], + + // QUEUE_SUBMISSION_CANCELLED + 1118 => [ + 'name' => 'Queue Submission Cancelled', + 'message' => 'Your {queue_name} submission (#{submission_id}) was cancelled and sent back to drafts by {staff_name}. (View Submission)', + 'url' => 'queue-submissions/view/{submission_id}', + ], ]; diff --git a/config/lorekeeper/queue_types.php b/config/lorekeeper/queue_types.php new file mode 100644 index 0000000000..04011da5d1 --- /dev/null +++ b/config/lorekeeper/queue_types.php @@ -0,0 +1,19 @@ + [ + // 'name' => 'Example Queue Type', + // 'consume_items' => false, // whether items are consumed on submission + // 'character_submit' => false, // whether character add-ons are allowed + // 'image_upload' => false, // whether image uploads are allowed + // ], +]; diff --git a/database/migrations/2025_06_09_160952_add_queue_maker.php b/database/migrations/2025_06_09_160952_add_queue_maker.php new file mode 100644 index 0000000000..e68bb6a5e4 --- /dev/null +++ b/database/migrations/2025_06_09_160952_add_queue_maker.php @@ -0,0 +1,129 @@ +engine = 'InnoDB'; + $table->increments('id'); + $table->string('key', 30)->unique()->nullable()->default(null); + + $table->string('name'); + $table->text('description')->nullable()->default(null); + $table->text('parsed_description')->nullable()->default(null); + $table->integer('sort')->unsigned()->default(0); + + $table->boolean('has_image')->default(0); + $table->string('hash', 10)->nullable()->default(null); + + $table->integer('limit')->nullable()->default(null); + $table->enum('limit_period', ['Hour', 'Day', 'Week', 'Month', 'Year'])->nullable()->default(null); + $table->integer('limit_concurrent')->nullable()->default(null); + $table->boolean('display')->default(1); + }); + + Schema::create('queues', function (Blueprint $table) { + $table->engine = 'InnoDB'; + $table->increments('id'); + $table->string('name', 64); + + // The summary will be displayed on the world page, + // with a link to a page that contains the full text of the queue. + $table->string('summary', 256)->nullable()->default(null); + $table->text('description')->nullable()->default(null); + $table->text('parsed_description')->nullable()->default(null); + + // The active flag is overridden by the start_at and end_at timestamps, + // i.e. if either or both of those timestamps are set, + // it will have no effect. + $table->boolean('is_active')->default(1); + $table->timestamp('start_at')->nullable()->default(null); + $table->timestamp('end_at')->nullable()->default(null); + // When submitting a queue, the selectable list will only contain queues between + // the start/end times and active queues. + + // This hides the queue from the world queue list before + // the queue start_at time has been reached. + $table->boolean('hide_before_start')->default(0); + + // This hides the queue from the world queue list after + // the queue end_at time has been reached. + $table->boolean('hide_after_end')->default(0); + + $table->text('form')->nullable()->default(null); + $table->text('parsed_form')->nullable()->default(null); + + $table->string('queue_type')->nullable()->default(null); + $table->json('data')->nullable()->default(null); + + $table->string('hash', 10)->nullable()->default(null); + + $table->integer('queue_category_id')->unsigned()->nullable(); + $table->string('prefix', 10)->nullable(); + $table->integer('hide_submissions')->unsigned()->default(0); + $table->boolean('staff_only')->default(0); + $table->boolean('has_image')->default(0); + + // Staff rank restrictions + $table->json('staff_rank_ids')->nullable()->default(null); + + $table->json('output')->nullable()->default(null); + $table->json('checklist')->nullable()->default(null); + + $table->integer('limit')->nullable()->default(null); + $table->text('limit_period')->nullable()->default(null); + $table->boolean('limit_character')->nullable()->default(null); + $table->integer('limit_concurrent')->nullable()->default(null); + }); + + Schema::create('queue_submissions', function (Blueprint $table) { + $table->engine = 'InnoDB'; + $table->increments('id'); + $table->integer('queue_id')->unsigned()->nullable()->index(); + + $table->integer('user_id')->unsigned()->index(); + $table->integer('staff_id')->unsigned()->nullable()->default(null); + + $table->text('comments')->nullable()->default(null); + $table->text('parsed_comments')->nullable()->default(null); + + $table->text('staff_comments')->nullable()->default(null); + $table->text('parsed_staff_comments')->nullable()->default(null); + + $table->enum('status', ['Draft', 'Pending', 'Approved', 'Rejected'])->default('Draft'); + + $table->json('data')->nullable()->default(null); + + $table->timestamps(); + }); + + Schema::create('queue_submission_characters', function (Blueprint $table) { + $table->engine = 'InnoDB'; + $table->increments('id'); + $table->integer('queue_submission_id')->unsigned()->index(); + $table->integer('character_id')->unsigned()->index(); + + $table->json('data')->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + */ + public function down() { + Schema::dropIfExists('queue_categories'); + Schema::dropIfExists('queue_submissions'); + Schema::dropIfExists('queue_submission_characters'); + Schema::dropIfExists('queues'); + } +} diff --git a/database/migrations/2026_01_19_212519_update_prompt_limit_enum.php b/database/migrations/2026_01_19_212519_update_prompt_limit_enum.php new file mode 100644 index 0000000000..0fb46b6873 --- /dev/null +++ b/database/migrations/2026_01_19_212519_update_prompt_limit_enum.php @@ -0,0 +1,22 @@ +text('limit_period')->nullable()->default(null)->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void { + // enum not supported going back + } +}; diff --git a/resources/views/admin/grants/item_search.blade.php b/resources/views/admin/grants/item_search.blade.php index fed31550a3..63fa7de7e1 100644 --- a/resources/views/admin/grants/item_search.blade.php +++ b/resources/views/admin/grants/item_search.blade.php @@ -89,6 +89,11 @@ array_push($held, (Auth::user()->hasPower('manage_submissions') ? 'Submission #' . App\Models\Submission\Submission::find($submission)->id . '' : 'Submission #' . App\Models\Submission\Submission::find($submission)->id) . ' (' . $quantity . ')'); } } + if (isset($holdLocations['queuesubmission'])) { + foreach ($holdLocations['queuesubmission'] as $queuesubmission => $quantity) { + array_push($held, (Auth::user()->hasPower('manage_submissions') ? 'Queue #' . App\Models\Queue\QueueSubmission::find($queuesubmission)->id . '' : 'Queue #' . App\Models\Queue\QueueSubmission::find($queuesubmission)->id) . ' (' . $quantity . ')'); + } + } $heldString = implode(', ', $held); ?>
+ @if ($queueCount) + {{ $queueCount }} {{ $queue->name }} submission{{ $queueCount == 1 ? '' : 's' }} awaiting processing. + @else + The {{ $queue->name }} submission queue is clear. Hooray! + @endif +
++ @if ($queueCount) + {{ $queueCount }} custom submission{{ $queueCount == 1 ? '' : 's' }} awaiting processing. + @else + The custom submission queue is clear. Hooray! + @endif +
+Limit the number of times a user can submit. Leave blank to allow endless submissions.
-Set a number into number of submissions. This will be applied for all time if you leave period blank, or per time period (ex: once a month, twice a week) if selected.
-If you turn 'per character' on, then the number of submissions multiplies per character (ex: if you can submit twice a month per character and you own three characters, that's 6 submissions) HOWEVER it will not keep track of which - characters are being submitted due to conflicts arising in character cameos. A user will be able to submit those full 6 times with just one character...!
-You are about to delete the queue {{ $queue->name }}. This is not reversible. If submissions exist under this queue, you will not be able to delete it.
+Are you sure you want to delete {{ $queue->name }}?
+ +You are about to delete the category {{ $category->name }}. This is not reversible. If queues in this category exist, you will not be able to delete this category.
+Are you sure you want to delete {{ $category->name }}?
+ +This is the template that will be shown in the comment section when a user select this queue in submission.
+Queue types change how the form changes in functionality.
+Create a checklist below to force users to acknowledge all necessary steps of submitting to the queue are complete. The user will not be able to submit until they have accepted that they have completed each step.
+Each "line" represents one checkbox that will need to be ticked off.
+ +| Step | ++ |
|---|---|
| + {!! Form::text('check_text[]', $check, [ + 'class' => 'form-control', + 'placeholder' => 'Enter checklist text', + ]) !!} + | +Remove + | +
User rewards are credited on a per-user basis, character rewards are rewarded to all characters attached. Mods are able to modify the specific rewards granted at approval time.
You can add loot tables containing any kind of currencies (both user- and character-attached), but be sure to keep track of which are being distributed! Character-only currencies cannot be given to users.
', + ]) + {{-- blade-formatter-enable --}} + +| {!! Form::text('check_text[]', null, ['class' => 'form-control', 'placeholder' => 'Enter checklist text']) !!} | +Remove | +
This queue consumes items, so you have the option to select which items the user may attach to their submission when they make it.
+| Item | ++ |
|---|---|
| + {!! Form::select('item_id[]', $item_limits, $item, [ + 'class' => 'form-control pet-select selectize', + 'placeholder' => 'Select Item', + ]) !!} + | +Remove + | +
| {!! Form::select('item_id[]', $item_limits, null, ['class' => 'form-control item-select selectize', 'placeholder' => 'Select Item']) !!} | +Remove | +
These additional images are optional, and the types and numbers of these will vary. They can help you further customize the look of the game. +
+ + {!! Form::open(['url' => 'admin/data/queue/images/' . $queue->id, 'files' => true]) !!} + @include('admin.queues.types.' . $queue->queue_type . '_images', ['data' => $queue->data]) +This is a list of queue categories that will be used to classify queues on the queues page. Creating queue categories is entirely optional, but recommended if you need to sort queues for mod work division, for example. The submission approval + queue page can be sorted by queue category.
+The sorting order reflects the order in which the queue categories will be displayed on the queues page.
+ + + @if (!count($categories)) +No queue categories found.
+ @else +| + + {!! $category->displayName !!} + | ++ Edit + | +
This is a list of queues users can submit to.
+Queues can be submitted to vanilla, but are primarily designed to hook into/have additional functions with other extensions or custom code.
+ + + +No queues found.
+ @else + {!! $queues->render() !!} +This queue does not use characters.
+ @endif +These items have been removed from the submitter's inventory and will be refunded if the request is rejected or consumed if it is approved.
+| Item | +Source | +Notes | +Quantity | +
|---|---|---|---|
|
+ @if (isset($itemsrow[$itemRow['asset']->item_id]->image_url))
+ | {!! array_key_exists('data', $itemRow['asset']->data) ? ($itemRow['asset']->data['data'] ? $itemRow['asset']->data['data'] : 'N/A') : 'N/A' !!} | +{!! array_key_exists('notes', $itemRow['asset']->data) ? ($itemRow['asset']->data['notes'] ? $itemRow['asset']->data['notes'] : 'N/A') : 'N/A' !!} | +{!! $itemRow['quantity'] !!} + |
| Currency | +Quantity | +
|---|---|
| {!! $currency['asset']->name !!} | +{{ $currency['quantity'] }} | +
This queue does not consume add-ons.
+ @endif +This is a standard, basic prompt-like queue, so there really isn't anything to fill out here.
diff --git a/resources/views/home/_sidebar.blade.php b/resources/views/home/_sidebar.blade.php index e899bbd02b..d051698dd8 100644 --- a/resources/views/home/_sidebar.blade.php +++ b/resources/views/home/_sidebar.blade.php @@ -14,6 +14,14 @@ + @if (config('lorekeeper.extensions.queue_creator.expand_in_user_menu')) + @php $queues = \App\Models\Queue\Queue::query()->active()->staffOnly(Auth::user())->get(); @endphp + @foreach ($queues as $queue) + + @endforeach + @else + + @endifThis user has completed this prompt {{ $count }} time{{ $count == 1 ? '' : 's' }}.
+ @include('queues._queue_limits', ['staff' => true, 'user' => $submission->user]) + @else +You have completed this queue {{ $count }} time{{ $count == 1 ? '' : 's' }}.
+ @include('queues._queue_limits', ['staff' => false, 'user' => Auth::user()]) + @endif +This queue has no associated extra form to fill in.
+ @endif +| Reward | +Amount | +
|---|---|
| {!! $asset['asset'] ? $asset['asset']->displayName : 'Deleted Asset' !!} | +{{ $asset['quantity'] }} | +
This queue has no associated extra form to fill in.
+This queue does not use characters.
+ @endif +These items have been removed from the submitter's inventory and will be refunded if the request is rejected or consumed if it is approved.
+| Item | +Source | +Notes | +Quantity | +
|---|---|---|---|
|
+ @if (isset($itemsrow[$itemRow['asset']->item_id]->image_url))
+ | {!! array_key_exists('data', $itemRow['asset']->data) ? ($itemRow['asset']->data['data'] ? $itemRow['asset']->data['data'] : 'N/A') : 'N/A' !!} | +{!! array_key_exists('notes', $itemRow['asset']->data) ? ($itemRow['asset']->data['notes'] ? $itemRow['asset']->data['notes'] : 'N/A') : 'N/A' !!} | +{!! $itemRow['quantity'] !!} + |
| Currency | +Quantity | +
|---|---|
| {!! $currency['asset']->name !!} | +{{ $currency['quantity'] }} | +
This queue does not consume add-ons.
+This queue does not use characters.
+If your submission consumes items, attach them here. Otherwise, this section can be left blank. These items will be removed from your inventory but refunded if your submission is + rejected.
+ @if (isset($queue->data['items'])) +This queue has specific requirements for items, and has filtered your inventory out automatically.
+Applicable Items:
+This queue does not consume add-ons.
+ @endif +Before you submit, make sure you have completed the following:
+If you did not complete all of the above, your submission may be rejected. Staff are not responsible for any issues that arise from incomplete submissions.
+No submissions found.
+ @endif + +@endsection diff --git a/resources/views/home/queues/submissions_closed.blade.php b/resources/views/home/queues/submissions_closed.blade.php new file mode 100644 index 0000000000..25d8ad7f78 --- /dev/null +++ b/resources/views/home/queues/submissions_closed.blade.php @@ -0,0 +1,63 @@ +@extends('home.layout') + +@section('home-title') + New Submission +@endsection + +@section('home-content') + {!! breadcrumbs(['Queue Submissions' => 'queue-submissions', 'New Submission' => 'queue-submissions/new']) !!} + +{{ $queue->summary }}
+No further details.
+ @endif + @if ($queue->hide_submissions == 1 && isset($queue->end_at) && $queue->end_at > Carbon\Carbon::now()) +Submissions to this queue are hidden until this queue ends.
+ @elseif($queue->hide_submissions == 2) +Submissions to this queue are hidden.
+ @endif +| Reward | +Amount | +
|---|---|
| + {!! $reward->rewardable_recipient == 'User' ? '' : '' !!} + {!! $reward->reward ? $reward->reward->displayName : $reward->rewardable_type !!} + | +{{ $reward->quantity }} | +
+ Limit the number of times a user can submit. Leave blank to allow endless submissions.
+Set a number into number of submissions. This will be applied for all time if you leave period blank, or per time period (ex: once a month, twice a week) if selected.
+If you turn 'per character' on, then the number of submissions multiplies per character (ex: if you can submit twice a month per character and you own three characters, that's 6 submissions) HOWEVER it will not keep track of which + characters are being submitted due to conflicts arising in character cameos. A user will be able to submit those full 6 times with just one character...!
+