first commit
这个提交包含在:
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2019-2023 Clark Winkelmann
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Catch The Fish
|
||||
|
||||
[](https://github.com/Xiaoxiaobai5724/catch-the-fish/blob/master/LICENSE.md) [](https://packagist.org/packages/xxb5724/catch-the-fish) [](https://packagist.org/packages/xxb5724/catch-the-fish)
|
||||
|
||||
[](https://floxum.com/extension/xxb5724/catch-the-fish)
|
||||
|
||||
A [Flarum](https://flarum.org) 2.0 extension. This extension is a fun experiment that adds a minigame to Flarum.
|
||||
|
||||
Once enabled, admins or mods can create fish catching rounds.
|
||||
|
||||
The fishes will appear on the forum and users compete to catch as many as they can.
|
||||
|
||||
It includes many customization options. You can let users change the fish names or choose the placement of the fish themselves.
|
||||
|
||||
All aspects of the game are controlled via Flarum permissions.
|
||||
|
||||
The extension comes with a starting pack of (public domain image) fish that you can include when creating a new round.
|
||||
|
||||
## Installation
|
||||
|
||||
Installing with Composer:
|
||||
|
||||
```sh
|
||||
composer require xxb5724/catch-the-fish
|
||||
```
|
||||
|
||||
> **Migrating from clarkwinkelmann/catch-the-fish**
|
||||
> This extension was transferred to XXB and was previously published as `clarkwinkelmann/catch-the-fish`. The XXB line begins at **2.0.0**, continuing the version history (the previous package reached 1.1.4).
|
||||
> For forum admins, migration is a one-line change — swap the package, keep your settings and stored SEO data:
|
||||
```sh
|
||||
composer remove clarkwinkelmann/catch-the-fish
|
||||
composer require xxb5724/catch-the-fish
|
||||
php flarum cache:clear
|
||||
```
|
||||
|
||||
## Upgrade
|
||||
|
||||
```sh
|
||||
composer update xxb5724/catch-the-fish
|
||||
php flarum cache:clear
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
You will find the settings for this extension in 3 different places:
|
||||
|
||||
- Extension settings page: global day-based, minute-based and animation settings, as well as probabilities for automatic fish placement and discussion tags whitelist
|
||||
- Flarum permissions: all access related settings
|
||||
- Front end page (not in admin): Round and fish configuration
|
||||
|
||||
## New improvements
|
||||
|
||||
The 'Fish Basket' feature from clarkwinkelmann/catch-the-fish wasn’t compatible with mobile, but now it is. Mobile users can choose where to store their fish after catching them. When you go back to the bottom of the homepage, there’s a 'Fish Basket.' Open any discussion or post, select a fish from the basket, and then click anywhere to place it.
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "xxb5724/catch-the-fish",
|
||||
"description": "Watch your users catch as many fishes as they can.",
|
||||
"keywords": [
|
||||
"extension",
|
||||
"flarum",
|
||||
"minigame",
|
||||
"april-fools"
|
||||
],
|
||||
"type": "flarum-extension",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Clark Winkelmann",
|
||||
"email": "clark.winkelmann@gmail.com",
|
||||
"homepage": "https://clarkwinkelmann.com/"
|
||||
},
|
||||
{
|
||||
"name": "Xiaoxiaobai5724",
|
||||
"email": "odoorbell@163.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://git.parlz.com/Xiaoxiaobai5724/catch-the-fish/issues",
|
||||
"source": "https://git.parlz.com/Xiaoxiaobai5724/catch-the-fish"
|
||||
},
|
||||
"require": {
|
||||
"flarum/core": "^2.0",
|
||||
"ext-json": "*"
|
||||
},
|
||||
"extra": {
|
||||
"flarum-extension": {
|
||||
"title": "XXB Catch the fish",
|
||||
"category": "feature",
|
||||
"icon": {
|
||||
"name": "fas fa-fish",
|
||||
"backgroundColor": "#00BFFF",
|
||||
"color": "#fff"
|
||||
},
|
||||
"optional-dependencies": [
|
||||
"flarum/suspend",
|
||||
"flarum/tags"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"XXB\\CatchTheFish\\": "src/",
|
||||
"Tobscure\\JsonApi\\": "src/Tobscure/JsonApi/"
|
||||
}
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish;
|
||||
|
||||
use XXB\CatchTheFish\Controllers;
|
||||
use XXB\CatchTheFish\Api\Resource\FishResource;
|
||||
use XXB\CatchTheFish\Api\Resource\RoundResource;
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\User;
|
||||
|
||||
return [
|
||||
(new Extend\Frontend('forum'))
|
||||
->css(__DIR__ . '/resources/less/forum.less')
|
||||
->js(__DIR__ . '/js/dist/forum.js')
|
||||
->route('/catch-the-fish', 'catch-the-fish-ranking')
|
||||
->route('/catch-the-fish/rounds', 'catch-the-fish-rounds')
|
||||
->route('/catch-the-fish/rounds/{id}', 'catch-the-fish-round'),
|
||||
(new Extend\Frontend('admin'))
|
||||
->css(__DIR__ . '/resources/less/admin.less')
|
||||
->js(__DIR__ . '/js/dist/admin.js'),
|
||||
|
||||
(new Extend\Routes('api'))
|
||||
->get('/catch-the-fish/rounds', 'catchthefish.api.rounds.index', Controllers\RoundIndexController::class)
|
||||
->post('/catch-the-fish/rounds', 'catchthefish.api.rounds.store', Controllers\RoundStoreController::class)
|
||||
->get('/catch-the-fish/rounds/{id:[0-9]+}', 'catchthefish.api.rounds.show', Controllers\RoundShowController::class)
|
||||
->patch('/catch-the-fish/rounds/{id:[0-9]+}', 'catchthefish.api.rounds.update', Controllers\RoundUpdateController::class)
|
||||
->delete('/catch-the-fish/rounds/{id:[0-9]+}', 'catchthefish.api.rounds.delete', Controllers\RoundDeleteController::class)
|
||||
->get('/catch-the-fish/rounds/{id:[0-9]+}/fishes', 'catchthefish.api.fishes.index', Controllers\FishIndexController::class)
|
||||
->post('/catch-the-fish/rounds/{id:[0-9]+}/fishes', 'catchthefish.api.fishes.store', Controllers\FishStoreController::class)
|
||||
->post('/catch-the-fish/rounds/{id:[0-9]+}/fishes-from-images', 'catchthefish.api.fishes.store-image', Controllers\FishImageBulkController::class)
|
||||
->patch('/catch-the-fish/fishes/{id:[0-9]+}', 'catchthefish.api.fishes.update', Controllers\FishUpdateController::class)
|
||||
->delete('/catch-the-fish/fishes/{id:[0-9]+}', 'catchthefish.api.fishes.delete', Controllers\FishDeleteController::class)
|
||||
->post('/catch-the-fish/fishes/{id:[0-9]+}/catch', 'catchthefish.api.fishes.catch', Controllers\FishCatchController::class)
|
||||
->post('/catch-the-fish/fishes/{id:[0-9]+}/place', 'catchthefish.api.fishes.place', Controllers\FishPlaceController::class)
|
||||
->post('/catch-the-fish/fishes/{id:[0-9]+}/image', 'catchthefish.api.fishes.image', Controllers\FishImageController::class)
|
||||
->get('/catch-the-fish/rounds/{id:[0-9]+}/rankings', 'catchthefish.api.rankings.index', Controllers\RankingIndexController::class),
|
||||
|
||||
(new Extend\Locales(__DIR__ . '/resources/locale')),
|
||||
|
||||
new Extend\ApiResource(RoundResource::class),
|
||||
new Extend\ApiResource(FishResource::class),
|
||||
(new Extend\ApiResource(Resource\ForumResource::class))
|
||||
->fields(fn() => [
|
||||
Schema\Boolean::make('catchTheFishCanModerate')
|
||||
->get(fn(object $forum, Context $context) => $context->getActor()->isAdmin() || $context->getActor()->can('catchthefish.moderate')),
|
||||
Schema\Boolean::make('catchTheFishCanSeeRankingsPage')
|
||||
->get(fn(object $forum, Context $context) => $context->getActor()->isAdmin() || $context->getActor()->can('catchthefish.list-rankings')),
|
||||
Schema\Boolean::make('catchTheFishAlertRound')
|
||||
->get(fn(object $forum, Context $context) => resolve(SettingsRepositoryInterface::class)->get('catch-the-fish.alertRound') !== '0'),
|
||||
Schema\Boolean::make('catchTheFishAnimateFlip')
|
||||
->get(fn(object $forum, Context $context) => resolve(SettingsRepositoryInterface::class)->get('catch-the-fish.animateFlip') !== '0'),
|
||||
Schema\Relationship\ToMany::make('catchTheFishActiveRounds')
|
||||
->type('catchthefish-rounds')
|
||||
->includable()
|
||||
->get(fn(object $forum, Context $context) => ($context->getActor()->isAdmin() || $context->getActor()->can('catchthefish.visible')) ? Round::activeRound()->get()->all() : []),
|
||||
])
|
||||
->endpoint(Endpoint\Show::class, fn(Endpoint\Show $endpoint) => $endpoint->addDefaultInclude(['catchTheFishActiveRounds'])),
|
||||
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||
->fields(fn() => [
|
||||
Schema\Relationship\ToMany::make('catchTheFishFishes')
|
||||
->type('catchthefish-fishes')
|
||||
->includable()
|
||||
->get(fn(Discussion $discussion, Context $context) => ($context->getActor()->isAdmin() || $context->getActor()->can('catchthefish.visible')) ? $discussion->catchTheFishFishes()->activeFish()->get()->all() : []),
|
||||
])
|
||||
->endpoint([Endpoint\Index::class, Endpoint\Show::class], fn(Endpoint\Index|Endpoint\Show $endpoint) => $endpoint->addDefaultInclude(['catchTheFishFishes'])),
|
||||
(new Extend\ApiResource(Resource\PostResource::class))
|
||||
->fields(fn() => [
|
||||
Schema\Relationship\ToMany::make('catchTheFishFishes')
|
||||
->type('catchthefish-fishes')
|
||||
->includable()
|
||||
->get(fn(Post $post, Context $context) => ($context->getActor()->isAdmin() || $context->getActor()->can('catchthefish.visible')) ? $post->catchTheFishFishes()->activeFish()->get()->all() : []),
|
||||
])
|
||||
->endpoint([Endpoint\Index::class, Endpoint\Show::class], fn(Endpoint\Index|Endpoint\Show $endpoint) => $endpoint->addDefaultInclude(['catchTheFishFishes'])),
|
||||
(new Extend\ApiResource(Resource\UserResource::class))
|
||||
->fields(fn() => [
|
||||
Schema\Relationship\ToMany::make('catchTheFishFishes')
|
||||
->type('catchthefish-fishes')
|
||||
->includable()
|
||||
->get(fn(User $user, Context $context) => ($context->getActor()->isAdmin() || $context->getActor()->can('catchthefish.visible')) ? $user->catchTheFishFishes()->activeFish()->get()->all() : []),
|
||||
Schema\Relationship\ToMany::make('catchTheFishBasket')
|
||||
->type('catchthefish-fishes')
|
||||
->includable()
|
||||
->get(fn(User $user, Context $context) => $context->getActor()->id === $user->id ? $user->catchTheFishBasket()->get()->all() : []),
|
||||
])
|
||||
->endpoint([Endpoint\Index::class, Endpoint\Show::class], fn(Endpoint\Index|Endpoint\Show $endpoint) => $endpoint->addDefaultInclude(['catchTheFishFishes', 'catchTheFishBasket'])),
|
||||
|
||||
(new Extend\Model(Discussion::class))
|
||||
->relationship('catchTheFishFishes', function ($model) {
|
||||
return (new ConfigureFishesRelationship('discussion_id_placement'))($model);
|
||||
}),
|
||||
(new Extend\Model(Post::class))
|
||||
->relationship('catchTheFishFishes', function ($model) {
|
||||
return (new ConfigureFishesRelationship('post_id_placement'))($model);
|
||||
}),
|
||||
(new Extend\Model(User::class))
|
||||
->relationship('catchTheFishFishes', function ($model) {
|
||||
return (new ConfigureFishesRelationship('user_id_placement'))($model);
|
||||
})
|
||||
->relationship('catchTheFishBasket', function ($model) {
|
||||
return (new ConfigureBasketRelationship())($model);
|
||||
}),
|
||||
|
||||
(new Extend\Policy())
|
||||
->modelPolicy(Fish::class, Access\FishPolicy::class)
|
||||
->modelPolicy(Round::class, Access\RoundPolicy::class),
|
||||
|
||||
(new Extend\ServiceProvider())
|
||||
->register(Providers\StorageServiceProvider::class),
|
||||
];
|
||||
第三方依赖
+2
文件差异因一行或多行过长而隐藏
第三方依赖
+1
文件差异因一行或多行过长而隐藏
第三方依赖
+2
文件差异因一行或多行过长而隐藏
第三方依赖
+1
文件差异因一行或多行过长而隐藏
@@ -0,0 +1,193 @@
|
||||
import { extend, override } from 'flarum/common/extend';
|
||||
import app from 'flarum/forum/app';
|
||||
import CommentPost from 'flarum/forum/components/CommentPost';
|
||||
import UserCard from 'flarum/forum/components/UserCard';
|
||||
import DiscussionHero from 'flarum/forum/components/DiscussionHero';
|
||||
import DropArea from './components/DropArea';
|
||||
import MovingFish from './components/MovingFish';
|
||||
function fishIdFromEvent(event) {
|
||||
if (!event.dataTransfer) {
|
||||
return null;
|
||||
}
|
||||
const data = event.dataTransfer.getData("text/plain");
|
||||
const match = /^fish:([0-9]+)$/.exec(data);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
function selectedFishId() {
|
||||
return app.draggedFishId || app.catchTheFishSelectedFishId || null;
|
||||
}
|
||||
function placeSelectedFish(model, modelProperty) {
|
||||
const fishId = selectedFishId();
|
||||
if (!fishId) {
|
||||
return;
|
||||
}
|
||||
const fish = app.store.getById('catchthefish-fishes', fishId);
|
||||
if (!fish) {
|
||||
alert(app.translator.trans('xxb-catch-the-fish.forum.drop-area.missing-from-store'));
|
||||
return;
|
||||
}
|
||||
const placement = {};
|
||||
placement[modelProperty + '_id'] = model.id();
|
||||
app.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/catch-the-fish/fishes/' + fish.id() + '/place',
|
||||
body: {
|
||||
placement
|
||||
}
|
||||
}).then(result => {
|
||||
app.store.pushPayload(result);
|
||||
app.draggedFishId = null;
|
||||
app.catchTheFishSelectedFishId = null;
|
||||
app.store.find('users', app.session.user.id()).then(() => {
|
||||
m.redraw();
|
||||
});
|
||||
});
|
||||
}
|
||||
function movingFishContent(dragover, model) {
|
||||
const content = [];
|
||||
if (dragover || app.catchTheFishSelectedFishId) {
|
||||
content.push(m(DropArea));
|
||||
}
|
||||
const fishes = model.catchTheFishFishes();
|
||||
if (fishes) {
|
||||
fishes.forEach(fish => {
|
||||
if (!fish.canSee()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove fish from relationship
|
||||
content.push(m(MovingFish, {
|
||||
fish,
|
||||
oncatch: () => {
|
||||
model.pushData({
|
||||
relationships: {
|
||||
catchTheFishFishes: {
|
||||
data: model.data.relationships.catchTheFishFishes.data.filter(f => f.id !== fish.id())
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
return content;
|
||||
}
|
||||
function addDropAttrs(attrs, modelProperty) {
|
||||
attrs.ondrop = event => {
|
||||
this.fishDragOver = false;
|
||||
const fishId = app.draggedFishId || fishIdFromEvent(event);
|
||||
if (fishId) {
|
||||
event.preventDefault();
|
||||
const fish = app.store.getById('catchthefish-fishes', fishId);
|
||||
if (fish) {
|
||||
const placement = {};
|
||||
placement[modelProperty + '_id'] = this.attrs[modelProperty].id();
|
||||
app.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/catch-the-fish/fishes/' + fish.id() + '/place',
|
||||
body: {
|
||||
placement
|
||||
}
|
||||
}).then(result => {
|
||||
app.store.pushPayload(result);
|
||||
|
||||
// Refresh basket by reloading user
|
||||
app.store.find('users', app.session.user.id()).then(() => {
|
||||
m.redraw();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
alert(app.translator.trans('xxb-catch-the-fish.forum.drop-area.missing-from-store'));
|
||||
}
|
||||
}
|
||||
};
|
||||
attrs.onclick = event => {
|
||||
if (app.catchTheFishSelectedFishId) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
placeSelectedFish(this.attrs[modelProperty], modelProperty);
|
||||
}
|
||||
};
|
||||
attrs.ondragover = event => {
|
||||
if (app.draggedFishId || fishIdFromEvent(event)) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
if (this.fishDragOver) {
|
||||
event.redraw = false;
|
||||
} else {
|
||||
this.fishDragOver = true;
|
||||
m.redraw();
|
||||
}
|
||||
} else {
|
||||
// In order to still support drag and drop across windows, we will accept any text drops
|
||||
// This is necessary because browsers don't give access to the drop value until the actual drop
|
||||
// But if we don't call preventDefault() here the drop can't happen in the first place
|
||||
if (event.dataTransfer && event.dataTransfer.types.includes('text/plain')) {
|
||||
event.preventDefault();
|
||||
}
|
||||
event.redraw = false;
|
||||
}
|
||||
};
|
||||
attrs.ondragenter = event => {
|
||||
event.preventDefault();
|
||||
event.redraw = false;
|
||||
};
|
||||
attrs.ondragleave = event => {
|
||||
if (this.fishDragOver) {
|
||||
this.fishDragOver = false;
|
||||
m.redraw();
|
||||
} else {
|
||||
event.redraw = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
function addAreaToComponent(component, viewName, modelProperty) {
|
||||
if (viewName === 'content') {
|
||||
override(component, viewName, function (original) {
|
||||
return original().concat(movingFishContent(this.fishDragOver, this.attrs[modelProperty]));
|
||||
});
|
||||
} else {
|
||||
extend(component, viewName, function (items) {
|
||||
items.add('catchthefish-fish-and-drop', movingFishContent(this.fishDragOver, this.attrs[modelProperty]));
|
||||
});
|
||||
}
|
||||
extend(component, 'oninit', function () {
|
||||
this.fishDragOver = false;
|
||||
|
||||
// Add a condition to the post tree retainer
|
||||
if (this.subtree) {
|
||||
this.subtree.check(() => this.fishDragOver);
|
||||
}
|
||||
});
|
||||
if (viewName === 'content') {
|
||||
// CommentPost has an attrs() method we can extend
|
||||
extend(component, 'elementAttrs', function (attrs) {
|
||||
addDropAttrs.bind(this)(attrs, modelProperty);
|
||||
});
|
||||
} else {
|
||||
// For other elements we manually add attrs to the vdom of the view
|
||||
extend(component, 'view', function (vdom) {
|
||||
vdom.attrs = vdom.attrs || {};
|
||||
addDropAttrs.bind(this)(vdom.attrs, modelProperty);
|
||||
});
|
||||
}
|
||||
}
|
||||
export default function () {
|
||||
addAreaToComponent(CommentPost.prototype, 'content', 'post');
|
||||
addAreaToComponent(UserCard.prototype, 'infoItems', 'user');
|
||||
addAreaToComponent(DiscussionHero.prototype, 'items', 'discussion');
|
||||
extend(UserCard.prototype, 'oninit', function () {
|
||||
const user = this.attrs.user;
|
||||
if (user && user.exists && !user.data.relationships?.catchTheFishFishes && !user.catchTheFishReloading) {
|
||||
user.catchTheFishReloading = true;
|
||||
app.store.find('users', user.id()).then(() => {
|
||||
m.redraw();
|
||||
}).finally(() => {
|
||||
user.catchTheFishReloading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
document.addEventListener('dragend', () => {
|
||||
app.draggedFishId = null;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import Basket from './components/Basket';
|
||||
export default function () {
|
||||
let container = document.querySelector('.catchthefish-basket-root');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'catchthefish-basket-root';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
m.mount(container, Basket);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { extend } from 'flarum/common/extend';
|
||||
import app from 'flarum/forum/app';
|
||||
import IndexSidebar from 'flarum/forum/components/IndexSidebar';
|
||||
import LinkButton from 'flarum/common/components/LinkButton';
|
||||
const translationPrefix = 'xxb-catch-the-fish.forum.nav.';
|
||||
export default function () {
|
||||
extend(IndexSidebar.prototype, 'navItems', function (items) {
|
||||
if (app.forum.catchTheFishCanSeeRankingsPage()) {
|
||||
items.add('catchthefish-rankings', LinkButton.component({
|
||||
icon: 'fas fa-fish',
|
||||
href: app.route('catchTheFishRankings')
|
||||
}, app.translator.trans(translationPrefix + 'rankings')));
|
||||
}
|
||||
if (app.forum.catchTheFishCanModerate()) {
|
||||
items.add('catchthefish-settings', LinkButton.component({
|
||||
icon: 'fas fa-fish',
|
||||
href: app.route('catchTheFishRounds')
|
||||
}, app.translator.trans(translationPrefix + 'settings')));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import app from 'flarum/forum/app';
|
||||
import FishImage from './FishImage';
|
||||
const translationPrefix = 'xxb-catch-the-fish.forum.basket.';
|
||||
export default class Basket {
|
||||
view() {
|
||||
if (!app.session || !app.session.user) {
|
||||
return m('div');
|
||||
}
|
||||
const basket = app.session.user.catchTheFishBasket();
|
||||
if (!basket) {
|
||||
return m('div');
|
||||
}
|
||||
const fishesThatCanBePlaced = basket.filter(fish => fish.canPlace());
|
||||
if (fishesThatCanBePlaced.length === 0) {
|
||||
return m('div');
|
||||
}
|
||||
return m('.catchthefish-basket', [m('.catchthefish-basket-title', app.translator.trans(translationPrefix + 'title')), m('p', app.translator.trans(translationPrefix + 'drag-help')), fishesThatCanBePlaced.map(fish => m('.catchthefish-basket-entry', [m('.catchthefish-basket-fish', {
|
||||
className: app.catchTheFishSelectedFishId === fish.id() ? 'active' : '',
|
||||
draggable: true,
|
||||
onclick() {
|
||||
app.catchTheFishSelectedFishId = app.catchTheFishSelectedFishId === fish.id() ? null : fish.id();
|
||||
},
|
||||
ondragstart(event) {
|
||||
if (!event.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Used for internal drag and drop animation
|
||||
app.draggedFishId = fish.id();
|
||||
|
||||
// Used for cross-window drag and drop
|
||||
// Chrome doesn't allow reading this value in ondragover so we can't show a drop area in that situation
|
||||
// (we would have to show a drop area for all text drops, which is too broad)
|
||||
event.dataTransfer.setData('text/plain', 'fish:' + fish.id());
|
||||
}
|
||||
}, m(FishImage, {
|
||||
fish
|
||||
})), m('.catchthefish-basket-time', app.translator.trans(translationPrefix + 'time', {
|
||||
time: dayjs(fish.placeUntil()).fromNow()
|
||||
}))]))]);
|
||||
}
|
||||
}
|
||||
flarum.reg.add('xxb-catch-the-fish', 'forum/components/Basket', Basket);
|
||||
@@ -0,0 +1,111 @@
|
||||
import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
|
||||
import app from 'flarum/forum/app';
|
||||
import Modal from 'flarum/common/components/Modal';
|
||||
import Button from 'flarum/common/components/Button';
|
||||
import FishImage from '../components/FishImage';
|
||||
import User from '../components/User';
|
||||
const translationPrefix = 'xxb-catch-the-fish.forum.caught-fish-modal.';
|
||||
export default class CaughtFishModal extends Modal {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
_defineProperty(this, "newName", void 0);
|
||||
_defineProperty(this, "dirty", false);
|
||||
_defineProperty(this, "loading", false);
|
||||
}
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
this.newName = this.attrs.fish.name();
|
||||
}
|
||||
className() {
|
||||
return 'Modal--small catchthefish-catch-modal';
|
||||
}
|
||||
title() {
|
||||
return app.translator.trans(translationPrefix + 'title');
|
||||
}
|
||||
saveNameAndPlacement(randomPlacement) {
|
||||
if (randomPlacement === void 0) {
|
||||
randomPlacement = false;
|
||||
}
|
||||
const body = {};
|
||||
if (this.dirty) {
|
||||
body.name = this.newName;
|
||||
}
|
||||
if (randomPlacement) {
|
||||
body.placement = 'random';
|
||||
}
|
||||
if (body) {
|
||||
this.loading = true;
|
||||
app.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/catch-the-fish/fishes/' + this.attrs.fish.id() + '/place',
|
||||
body
|
||||
}).then(result => {
|
||||
app.store.pushPayload(result);
|
||||
this.hide();
|
||||
if (this.attrs.fish.canPlace() && !randomPlacement) {
|
||||
// Refresh basket by reloading user
|
||||
app.store.find('users', app.session.user.id()).then(() => {
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
throw err;
|
||||
});
|
||||
} else {
|
||||
this.hide();
|
||||
if (this.attrs.fish.canPlace() && !randomPlacement) {
|
||||
// Refresh basket by reloading user
|
||||
app.store.find('users', app.session.user.id()).then(() => {
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
content() {
|
||||
var _myRanking;
|
||||
const fish = this.attrs.fish;
|
||||
const namedBy = fish.namedBy();
|
||||
const placedBy = fish.placedBy();
|
||||
const round = fish.round();
|
||||
const myRanking = round && round.myRanking ? round.myRanking() : null;
|
||||
const catchCount = round && typeof round.my_catch_count === 'function' ? round.my_catch_count() : (myRanking ? (typeof myRanking.catch_count === 'function' ? myRanking.catch_count() : (myRanking.data && myRanking.data.attributes && myRanking.data.attributes.catch_count) || myRanking.catch_count || 0) : 0);
|
||||
return m('.Modal-body', [m('h3', '"' + fish.name() + '"'), m(FishImage, {
|
||||
fish
|
||||
}), namedBy ? m('p', [app.translator.trans(translationPrefix + 'named-by'), ' ', m(User, {
|
||||
user: namedBy
|
||||
})]) : null, placedBy ? m('p', [app.translator.trans(translationPrefix + 'placed-by'), ' ', m(User, {
|
||||
user: placedBy
|
||||
})]) : null, m('p', app.translator.trans(translationPrefix + 'congratulation', {
|
||||
catch_count: catchCount
|
||||
})), fish.canName() ? m('.Form-group', [m('p', app.translator.trans(translationPrefix + 'name-help')), m('label', app.translator.trans(translationPrefix + 'name')), m('input.FormControl', {
|
||||
value: this.newName,
|
||||
oninput: event => {
|
||||
this.newName = event.target.value;
|
||||
this.dirty = true;
|
||||
}
|
||||
})]) : null, fish.canPlace() ? m('p', app.translator.trans(translationPrefix + 'placement-help')) : null, m('.Form-group', Button.component({
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'button',
|
||||
loading: this.loading,
|
||||
onclick: () => {
|
||||
this.saveNameAndPlacement();
|
||||
}
|
||||
}, app.translator.trans(translationPrefix + (this.dirty ? fish.canPlace() ? 'submit-name-place-later' : 'submit-name' : fish.canPlace() ? 'submit-place-later' : 'submit-continue')))), fish.canPlace() ? m('.Form-group', Button.component({
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'button',
|
||||
loading: this.loading,
|
||||
onclick: () => {
|
||||
this.saveNameAndPlacement(true);
|
||||
}
|
||||
}, app.translator.trans(translationPrefix + (this.dirty ? 'submit-name-place-random' : 'submit-place-random')))) : null]);
|
||||
}
|
||||
onsubmit(event) {
|
||||
event.preventDefault();
|
||||
// Because the modal has its own form, pressing enter will submit here
|
||||
// In this case we apply the same feature as the first button
|
||||
this.saveNameAndPlacement();
|
||||
}
|
||||
}
|
||||
flarum.reg.add('xxb-catch-the-fish', 'forum/modals/CaughtFishModal', CaughtFishModal);
|
||||
@@ -0,0 +1,17 @@
|
||||
import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
|
||||
import Model from 'flarum/common/Model';
|
||||
export default class Round extends Model {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
_defineProperty(this, "name", Model.attribute('name'));
|
||||
_defineProperty(this, "starts_at", Model.attribute('starts_at'));
|
||||
_defineProperty(this, "ends_at", Model.attribute('ends_at'));
|
||||
_defineProperty(this, "include_starting_pack", Model.attribute('include_starting_pack'));
|
||||
_defineProperty(this, "my_catch_count", Model.attribute('my_catch_count'));
|
||||
_defineProperty(this, "myRanking", Model.hasOne('myRanking'));
|
||||
}
|
||||
apiEndpoint() {
|
||||
return '/catch-the-fish/rounds' + (this.exists ? '/' + this.data.id : '');
|
||||
}
|
||||
}
|
||||
flarum.reg.add('xxb-catch-the-fish', 'forum/models/Round', Round);
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
$schema->create('catchthefish_rounds', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('name');
|
||||
$table->timestamp('starts_at')->nullable()->index();
|
||||
$table->timestamp('ends_at')->nullable()->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
},
|
||||
'down' => function (Builder $schema) {
|
||||
$schema->dropIfExists('catchthefish_rounds');
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
$schema->create('catchthefish_fishes', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('round_id');
|
||||
$table->unsignedInteger('discussion_id_placement')->nullable();
|
||||
$table->unsignedInteger('post_id_placement')->nullable();
|
||||
$table->unsignedInteger('user_id_placement')->nullable();
|
||||
$table->unsignedInteger('user_id_last_catch')->nullable(); // To determine which user is allowed to edit fish
|
||||
$table->unsignedInteger('user_id_last_placement')->nullable(); // To show last user who placed fish
|
||||
$table->unsignedInteger('user_id_last_naming')->nullable(); // To show last user who renamed fish
|
||||
$table->timestamp('placement_valid_since')->nullable()->index(); // To automatically apply previously random placement when custom placement expires
|
||||
$table->timestamp('last_caught_at')->nullable()->index();
|
||||
$table->string('name');
|
||||
$table->string('image')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('round_id')->references('id')->on('catchthefish_rounds')->onDelete('cascade');
|
||||
$table->foreign('discussion_id_placement')->references('id')->on('discussions')->onDelete('set null');
|
||||
$table->foreign('post_id_placement')->references('id')->on('posts')->onDelete('set null');
|
||||
$table->foreign('user_id_placement')->references('id')->on('users')->onDelete('set null');
|
||||
$table->foreign('user_id_last_catch')->references('id')->on('users')->onDelete('set null');
|
||||
$table->foreign('user_id_last_placement')->references('id')->on('users')->onDelete('set null');
|
||||
$table->foreign('user_id_last_naming')->references('id')->on('users')->onDelete('set null');
|
||||
});
|
||||
},
|
||||
'down' => function (Builder $schema) {
|
||||
$schema->dropIfExists('catchthefish_fishes');
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
$schema->create('catchthefish_rankings', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('round_id');
|
||||
$table->unsignedInteger('user_id');
|
||||
$table->unsignedInteger('catch_count')->index();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('round_id')->references('id')->on('catchthefish_rounds')->onDelete('cascade');
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
});
|
||||
},
|
||||
'down' => function (Builder $schema) {
|
||||
$schema->dropIfExists('catchthefish_rankings');
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use XXB\CatchTheFish\Ranking;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
// Merge any duplicate ranking that might exist in the database
|
||||
$schema->getConnection()->table('catchthefish_rankings')
|
||||
->selectRaw('round_id, user_id, sum(catch_count) as catch_count_sum')
|
||||
->orderBy('round_id')
|
||||
->orderBy('user_id')
|
||||
->groupBy('round_id', 'user_id')->each(function ($duplicate) {
|
||||
$first = Ranking::query()
|
||||
->where('round_id', $duplicate->round_id)
|
||||
->where('user_id', $duplicate->user_id)
|
||||
->first();
|
||||
|
||||
$first->catch_count = $duplicate->catch_count_sum;
|
||||
$first->save();
|
||||
|
||||
Ranking::query()
|
||||
->where('round_id', $duplicate->round_id)
|
||||
->where('user_id', $duplicate->user_id)
|
||||
->where('id', '!=', $first->id)
|
||||
->delete();
|
||||
});
|
||||
|
||||
// Add missing unique constraint
|
||||
$schema->table('catchthefish_rankings', function (Blueprint $table) {
|
||||
$table->unique(['round_id', 'user_id']);
|
||||
});
|
||||
},
|
||||
'down' => function (Builder $schema) {
|
||||
// Not implemented because in Flarum you can't revert just one migration
|
||||
// And it causes an error with the foreign key indexes
|
||||
},
|
||||
];
|
||||
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 97 KiB |
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 147 KiB |
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 128 KiB |
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 170 KiB |
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 124 KiB |
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 44 KiB |
@@ -0,0 +1,29 @@
|
||||
.CTFFormControl--range {
|
||||
display: inline-block;
|
||||
width: 7em;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.xxb-catch-the-fish-Page h3 {
|
||||
max-width: 460px;
|
||||
margin: 0 -30px;
|
||||
padding: 15px 30px;
|
||||
border-top: 2px dashed var(--control-bg, #e8ecf3);
|
||||
|
||||
}
|
||||
|
||||
.xxb-catch-the-fish-Page .helpText {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.CTFTagsTable {
|
||||
min-width: 500px;
|
||||
|
||||
td, th {
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
.catchthefish-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
th, td {
|
||||
padding: 5px 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--control-bg, #eee);
|
||||
}
|
||||
|
||||
tr:nth-child(odd) td {
|
||||
background: var(--control-bg, #fafafa);
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 40px;
|
||||
}
|
||||
|
||||
.Avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.catchthefish-basket {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
width: 220px;
|
||||
background: var(--body-bg);
|
||||
border: 1px solid var(--control-bg, #ddd);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.18);
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.catchthefish-basket-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.catchthefish-basket-fish {
|
||||
cursor: move;
|
||||
max-width: 150px;
|
||||
margin: 8px auto;
|
||||
padding: 6px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 10px;
|
||||
touch-action: manipulation;
|
||||
|
||||
&.active {
|
||||
border-color: var(--primary-color);
|
||||
background: var(--control-bg, #f0f0f0);
|
||||
}
|
||||
}
|
||||
|
||||
.catchthefish-basket-entry {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.catchthefish-drop-area {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border: 4px dashed #dedede;
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #dedede;
|
||||
font-weight: bold;
|
||||
font-size: 2em;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.catchthefish-moving-fish {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 50%;
|
||||
width: 140px;
|
||||
opacity: 0.9;
|
||||
animation: 14s swim linear infinite;
|
||||
cursor: pointer;
|
||||
|
||||
&.catchthefish-animate-flip .catchthefish-image {
|
||||
animation: 14s flip linear infinite;
|
||||
}
|
||||
|
||||
&.catchthefish-animate-reverse {
|
||||
animation-name: swim-reverse;
|
||||
|
||||
&.catchthefish-animate-flip .catchthefish-image {
|
||||
animation-name: flip-reverse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.UserCard,
|
||||
.DiscussionHero {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.catchthefish-name {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.catchthefish-image {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.catchthefish-basket {
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
width: auto;
|
||||
max-height: 42vh;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.catchthefish-basket p {
|
||||
margin: 4px 0 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.catchthefish-basket-entry {
|
||||
display: inline-block;
|
||||
width: 92px;
|
||||
vertical-align: top;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.catchthefish-basket-fish {
|
||||
max-width: 76px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.catchthefish-basket-time {
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.catchthefish-drop-area {
|
||||
font-size: 1.1em;
|
||||
border-width: 3px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
.catchthefish-no-image {
|
||||
background: black;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.catchthefish-catch-modal {
|
||||
text-align: center;
|
||||
|
||||
.catchthefish-image {
|
||||
display: block;
|
||||
width: 80%;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
.Avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes swim {
|
||||
0% {
|
||||
transform: translateX(-200px);
|
||||
}
|
||||
48% {
|
||||
transform: translateX(200px);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(200px);
|
||||
}
|
||||
98% {
|
||||
transform: translateX(-200px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-200px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes swim-reverse {
|
||||
0% {
|
||||
transform: translateX(200px);
|
||||
}
|
||||
48% {
|
||||
transform: translateX(-200px);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-200px);
|
||||
}
|
||||
98% {
|
||||
transform: translateX(200px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(200px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flip {
|
||||
0% {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
48% {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
50% {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
98% {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
100% {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flip-reverse {
|
||||
0% {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
48% {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
50% {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
98% {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
100% {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
xxb-catch-the-fish:
|
||||
forum:
|
||||
moderate:
|
||||
add: Add
|
||||
edit: Edit
|
||||
delete: Delete
|
||||
fishes: Fishes
|
||||
rounds: Rounds
|
||||
|
||||
nav:
|
||||
rankings: Catch The Fish Rankings
|
||||
settings: Catch The Fish Settings
|
||||
|
||||
new-round-modal:
|
||||
title: New Round
|
||||
|
||||
new-fish-modal:
|
||||
title: New Fish
|
||||
|
||||
edit-round-modal:
|
||||
title: Edit Round "{name}"
|
||||
|
||||
edit-fish-modal:
|
||||
title: Edit Fish "{name}"
|
||||
|
||||
caught-fish-modal:
|
||||
title: You caught a fish !
|
||||
named-by: Named by
|
||||
placed-by: Placed by
|
||||
congratulation: Congratulation, you caught a fish ! You have caught {catch_count} fishes.
|
||||
name: Fish name
|
||||
name-help: You can rename the fish if you want. The name must respect the forum guidelines.
|
||||
placement-help: You can hide the fish where you want on the forum in recent discussions and active user profiles. You cannot catch a fish you placed yourself.
|
||||
submit-name-place-later: Save name and choose hiding place
|
||||
submit-name: Save name
|
||||
submit-place-later: Yep I'll choose a hiding place
|
||||
submit-continue: Continue
|
||||
submit-name-place-random: Save name and let the fish hide itself
|
||||
submit-place-random: Let the fish hide itself
|
||||
|
||||
edit-round:
|
||||
name: Name
|
||||
name-help: The public name for this round, will be shown in banner and in ranking
|
||||
starts-at: Start time
|
||||
ends-at: End time
|
||||
starting-pack: Starting Pack
|
||||
starting-pack-help: Will include a few fishes to get you started in no time
|
||||
create: Create round
|
||||
save: Save round
|
||||
delete: Delete round
|
||||
delete-confirmation: Delete round "{name}" ?
|
||||
|
||||
edit-fish:
|
||||
name: Name
|
||||
name-help: The current name of the fish. Can be edited by players if they have the permission to rename fishes
|
||||
create: Create fish
|
||||
save: Save fish
|
||||
delete: Delete fish
|
||||
delete-confirmation: Delete fish "{name}" ?
|
||||
|
||||
moving-fish:
|
||||
login: Login to catch fishes
|
||||
|
||||
fish-image:
|
||||
alt: Image of fish "{name}"
|
||||
missing: Missing fish image
|
||||
|
||||
round-alert:
|
||||
message: Round "{name}", catch as many fishes as you can until {time}
|
||||
rankings: See rankings
|
||||
|
||||
basket:
|
||||
title: Fish Basket
|
||||
drag-help: Drag and drop the fish on a discussion, post or user profile
|
||||
time: "Place until: {time}"
|
||||
|
||||
table-round:
|
||||
loading: Loading...
|
||||
title: Rounds
|
||||
new: New round
|
||||
name: Name
|
||||
start: From
|
||||
end: Until
|
||||
actions: Actions
|
||||
edit: Edit
|
||||
fishes: Fishes
|
||||
|
||||
table-fish:
|
||||
loading: Loading...
|
||||
title: Fishes for round {name}
|
||||
new: New fish
|
||||
image: Image
|
||||
name: Name
|
||||
user-name: Last named by
|
||||
user-place: Last placed by
|
||||
placement: Current location
|
||||
actions: Actions
|
||||
edit: Edit
|
||||
no-user-name: Not renamed
|
||||
no-user-place: Random
|
||||
upload: Upload new image
|
||||
new-from-image: Create new fishes via bulk image import
|
||||
|
||||
table-ranking:
|
||||
loading: Loading...
|
||||
title: Ranking of round {name}
|
||||
rank: Name
|
||||
count: Fishes caught
|
||||
user: User
|
||||
|
||||
page-ranking:
|
||||
title: Rankings
|
||||
permission: You do not have permission to see the rankings
|
||||
nothing: No active rounds
|
||||
|
||||
drop-area:
|
||||
message: You can drop the fish here
|
||||
missing-from-store: Could not find the fish in the data store
|
||||
|
||||
admin:
|
||||
settings:
|
||||
user: Users
|
||||
user-age: How long since a user must have been active for a fish to be placed on it (days)
|
||||
user-probability: Probability to be placed on a user profile (%)
|
||||
discussion: Discussions
|
||||
discussion-age: How old discussions can be for a fish to be placed on it (days)
|
||||
discussion-tags: Tag whitelist
|
||||
discussion-tags-unavailable: Setting not available because the Tags extension is disabled.
|
||||
discussion-tags-help: >
|
||||
If you don't select any tag, any discussion that matches the time settings above can be chosen.
|
||||
If any number of tags are selected, a discussion needs to have at least one of the selected tags to be chosen.
|
||||
The probability is the chance to be chosen after all previous choices have been tried.
|
||||
The last tag in the list will always act as fallback, so make sure that tag has at least one valid discussion inside.
|
||||
tags-header:
|
||||
tag: Tag
|
||||
probability: Probability
|
||||
tags-control:
|
||||
choose: Choose a tag
|
||||
add: Add to whitelist
|
||||
down: Move down
|
||||
up: Move up
|
||||
delete: Remove from whitelist
|
||||
tags-fallback:
|
||||
last-line: Fallback
|
||||
single-line: At least 2 tags required for probability
|
||||
post: Posts
|
||||
post-age: How old posts can be for a fish to be placed on it (days)
|
||||
post-probability: Probability to be placed on a post rather than the discussion (%)
|
||||
general: General
|
||||
time-to-place: Time allowed to place fish before it is randomly placed (minutes)
|
||||
alert-round: Show an alert with the round name and duration on the homepage
|
||||
animate-flip: Flip fish images based on direction
|
||||
|
||||
permissions:
|
||||
visible: See the fishes
|
||||
list-rankings: Access the rankings page
|
||||
participate: Participate in the rounds and catch fishes
|
||||
choose-place: Choose fish placement after catching
|
||||
choose-name: Choose fish name after catching
|
||||
moderate: Moderate rounds and fishes
|
||||
|
||||
api:
|
||||
default-fish-name: "Fish #{number}"
|
||||
wrong-catch-placement: There is no fish here. Maybe you weren't quick enough and someone else got it first
|
||||
too-many-placement-models: Only one placement is possible at a time
|
||||
invalid-discussion-id: Discussion doesn't seem to exist, maybe it was recently deleted
|
||||
invalid-post-id: Post doesn't seem to exist, maybe it was recently deleted
|
||||
invalid-user-id: User doesn't seem to exist, maybe it was recently deleted
|
||||
model-deleted: Can't place fish on a deleted discussion or post
|
||||
model-private: Can't place fish on a private discussion or post
|
||||
tag-not-allowed: You can't place fish under this tag
|
||||
inactive-discussion: Can't place fish on discussion inactive for more than {days} days
|
||||
inactive-post: Can't place fish on post created more than {days} days ago
|
||||
inactive-user: Can't place fish on user inactive for more than {days} days
|
||||
non-comment-post: Can only place fishes on comment posts
|
||||
user-suspended: Can't place fish on a suspended user
|
||||
random-error: Could not generate a random fish placement
|
||||
cannot-catch-own-fish: You cannot catch a fish you placed yourself
|
||||
cannot-catch-hold-fish: The previous catcher needs to release the fish before you can catch it
|
||||
fish-update-wrong-user: You can no longer edit this fish. Somebody else has probably caught it
|
||||
fish-update-expired: Too late, you can no longer edit this fish. It has automatically been placed somewhere in the meantime
|
||||
round-not-started: This round has not started
|
||||
round-finished: This round is finished
|
||||
@@ -0,0 +1,183 @@
|
||||
xxb-catch-the-fish:
|
||||
forum:
|
||||
moderate:
|
||||
add: 添加
|
||||
edit: 编辑
|
||||
delete: 删除
|
||||
fishes: 鱼
|
||||
rounds: 轮次
|
||||
|
||||
nav:
|
||||
rankings: 捕鱼排行榜
|
||||
settings: 捕鱼设置
|
||||
|
||||
new-round-modal:
|
||||
title: 新建轮次
|
||||
|
||||
new-fish-modal:
|
||||
title: 新建鱼
|
||||
|
||||
edit-round-modal:
|
||||
title: 编辑轮次「{name}」
|
||||
|
||||
edit-fish-modal:
|
||||
title: 编辑鱼「{name}」
|
||||
|
||||
caught-fish-modal:
|
||||
title: 你抓到了一条鱼!
|
||||
named-by: 命名者
|
||||
placed-by: 放置者
|
||||
congratulation: 恭喜,你抓到了一条鱼!你已经抓到了 {catch_count} 条鱼。
|
||||
name: 鱼的名称
|
||||
name-help: 如果你愿意,可以重新命名这条鱼。但请注意名称必须遵守规范。
|
||||
placement-help: 你可以把鱼藏在论坛近期的讨论或活跃用户资料页中的任意位置。但是你不能抓自己放置的鱼。
|
||||
submit-name-place-later: 保存名称并选择藏鱼位置
|
||||
submit-name: 保存名称
|
||||
submit-place-later: 好的,我稍后选择藏鱼位置
|
||||
submit-continue: 继续
|
||||
submit-name-place-random: 保存名称并让鱼自己藏起来
|
||||
submit-place-random: 让鱼自己藏起来
|
||||
|
||||
edit-round:
|
||||
name: 名称
|
||||
name-help: 此轮次的公开名称,会显示在横幅和排行榜中。
|
||||
starts-at: 开始时间
|
||||
ends-at: 结束时间
|
||||
starting-pack: 初始鱼包
|
||||
starting-pack-help: 会包含几条鱼,方便你快速开始。。
|
||||
create: 创建轮次
|
||||
save: 保存轮次
|
||||
delete: 删除轮次
|
||||
delete-confirmation: 确定要删除轮次「{name}」吗?
|
||||
|
||||
edit-fish:
|
||||
name: 名称
|
||||
name-help: 当前鱼的名称。如果用户拥有重命名权限,也可以编辑它。
|
||||
create: 创建鱼
|
||||
save: 保存鱼
|
||||
delete: 删除鱼
|
||||
delete-confirmation: 确定要删除鱼「{name}」吗?
|
||||
|
||||
moving-fish:
|
||||
login: 登录后即可抓鱼
|
||||
|
||||
fish-image:
|
||||
alt: 鱼「{name}」的图片
|
||||
missing: 缺少鱼的图片
|
||||
|
||||
round-alert:
|
||||
message: 轮次「{name}」正在进行,请在 {time} 到前尽可能的多抓鱼。
|
||||
rankings: 查看排行榜
|
||||
|
||||
basket:
|
||||
title: 鱼篓
|
||||
drag-help: 将鱼拖放到讨论、帖子或用户资料页上。
|
||||
time: "放置截止时间:{time}"
|
||||
|
||||
table-round:
|
||||
loading: 正在加载...
|
||||
title: 轮次
|
||||
new: 新建轮次
|
||||
name: 名称
|
||||
start: 开始
|
||||
end: 结束
|
||||
actions: 操作
|
||||
edit: 编辑
|
||||
fishes: 鱼
|
||||
|
||||
table-fish:
|
||||
loading: 正在加载...
|
||||
title: 轮次 {name} 中的鱼
|
||||
new: 新建鱼
|
||||
image: 图片
|
||||
name: 名称
|
||||
user-name: 最后命名者
|
||||
user-place: 最后放置者
|
||||
placement: 当前位置
|
||||
actions: 操作
|
||||
edit: 编辑
|
||||
no-user-name: 未重命名
|
||||
no-user-place: 随机
|
||||
upload: 上传新图片
|
||||
new-from-image: 通过批量图片导入创建新鱼
|
||||
|
||||
table-ranking:
|
||||
loading: 正在加载...
|
||||
title: 轮次 {name} 的排行榜
|
||||
rank: 名次
|
||||
count: 抓到的鱼数
|
||||
user: 用户
|
||||
|
||||
page-ranking:
|
||||
title: 排行榜
|
||||
permission: 你没有权限查看排行榜
|
||||
nothing: 当前没有进行中的轮次
|
||||
|
||||
drop-area:
|
||||
message: 你可以把鱼放在这里
|
||||
missing-from-store: 无法在数据存储中找到这条鱼
|
||||
|
||||
admin:
|
||||
settings:
|
||||
user: 用户
|
||||
user-age: 用户最近活跃时间在多少天内,鱼才可以被放置到其资料页上
|
||||
user-probability: 放置到用户资料页的概率(%)
|
||||
discussion: 讨论
|
||||
discussion-age: 讨论创建后多少天内,鱼才可以被放置到其中
|
||||
discussion-tags: 标签白名单
|
||||
discussion-tags-unavailable: 标签扩展已禁用或未安装,因此此设置不可用。
|
||||
discussion-tags-help: >
|
||||
如果不选择任何标签,任何符合上方时间设置的讨论都可能被选中。
|
||||
如果选择了一个或多个标签,讨论至少需要包含其中一个被选中的标签才可能被选中。
|
||||
概率表示在前面的选择全部尝试之后,该标签被选中的机会。
|
||||
列表中的最后一个标签会始终作为兜底选项,因此请确保该标签下至少有一个有效讨论。
|
||||
tags-header:
|
||||
tag: 标签
|
||||
probability: 概率
|
||||
tags-control:
|
||||
choose: 选择标签
|
||||
add: 添加到白名单
|
||||
down: 下移
|
||||
up: 上移
|
||||
delete: 从白名单移除
|
||||
tags-fallback:
|
||||
last-line: 兜底
|
||||
single-line: 至少需要 2 个标签才能设置概率
|
||||
post: 帖子
|
||||
post-age: 帖子创建后多少天内,鱼才可以被放置到其中
|
||||
post-probability: 相比讨论,放置到帖子上的概率(%)
|
||||
general: 通用
|
||||
time-to-place: 抓到鱼后允许手动放置的时间(分钟)
|
||||
alert-round: 在首页显示包含轮次名称和持续时间的提示
|
||||
animate-flip: 根据移动方向翻转鱼的图片
|
||||
|
||||
permissions:
|
||||
visible: 查看鱼
|
||||
list-rankings: 访问排行榜页面
|
||||
participate: 参与轮次并抓鱼
|
||||
choose-place: 抓到鱼后选择放置位置
|
||||
choose-name: 抓到鱼后选择鱼的名称
|
||||
moderate: 管理轮次和鱼
|
||||
|
||||
api:
|
||||
default-fish-name: "鱼 #{number}"
|
||||
wrong-catch-placement: 这里没有鱼。也许你慢了一步,已经被别人先抓走了。
|
||||
too-many-placement-models: 一次只能选择一个放置位置。
|
||||
invalid-discussion-id: 讨论似乎不存在,可能最近已被删除。
|
||||
invalid-post-id: 帖子似乎不存在,可能最近已被删除。
|
||||
invalid-user-id: 用户似乎不存在,可能最近已被删除。
|
||||
model-deleted: 不能把鱼放在已删除的讨论或帖子上。
|
||||
model-private: 不能把鱼放在私密讨论或帖子上。
|
||||
tag-not-allowed: 你不能把鱼放在这个标签下。
|
||||
inactive-discussion: 不能把鱼放在超过 {days} 天未活跃的讨论中。
|
||||
inactive-post: 不能把鱼放在创建超过 {days} 天的帖子上。
|
||||
inactive-user: 不能把鱼放在超过 {days} 天未活跃的用户资料页上。
|
||||
non-comment-post: 只能把鱼放在评论帖子上。
|
||||
user-suspended: 不能把鱼放在被封禁的用户资料页上。
|
||||
random-error: 无法随机生成鱼的放置位置。
|
||||
cannot-catch-own-fish: 你不能抓自己放置的鱼。
|
||||
cannot-catch-hold-fish: 上一个抓到鱼的人需要先放开这条鱼,你才能抓它。
|
||||
fish-update-wrong-user: 你不能再编辑这条鱼了。可能已经被其他人抓走了。
|
||||
fish-update-expired: 太晚了,你不能再编辑这条鱼了。它已经自动被放置到其他地方了。
|
||||
round-not-started: 此轮次尚未开始。
|
||||
round-finished: 此轮次已经结束。
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Access;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use XXB\CatchTheFish\Fish;
|
||||
use Flarum\Foundation\ValidationException;
|
||||
use Flarum\Locale\Translator;
|
||||
use Flarum\User\Access\AbstractPolicy;
|
||||
use Flarum\User\User;
|
||||
|
||||
class FishPolicy extends AbstractPolicy
|
||||
{
|
||||
const TRANSLATION_PREFIX = 'xxb-catch-the-fish.api.';
|
||||
|
||||
public function create(User $actor)
|
||||
{
|
||||
return $actor->isAdmin() || $actor->can('catchthefish.moderate');
|
||||
}
|
||||
|
||||
public function update(User $actor, Fish $fish)
|
||||
{
|
||||
return $this->create($actor);
|
||||
}
|
||||
|
||||
public function delete(User $actor, Fish $fish)
|
||||
{
|
||||
return $this->update($actor, $fish);
|
||||
}
|
||||
|
||||
public function see(User $actor, Fish $fish)
|
||||
{
|
||||
return $actor->isAdmin() || $actor->can('catchthefish.visible');
|
||||
}
|
||||
|
||||
public function catch(User $actor, Fish $fish)
|
||||
{
|
||||
if ($actor->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($actor->id === $fish->user_id_last_placement) {
|
||||
throw new ValidationException([
|
||||
'placement' => resolve(Translator::class)->trans(self::TRANSLATION_PREFIX . 'cannot-catch-own-fish'),
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$fish->placement_valid_since || $fish->placement_valid_since->gt(Carbon::now())) {
|
||||
throw new ValidationException([
|
||||
'placement' => resolve(Translator::class)->trans(self::TRANSLATION_PREFIX . 'cannot-catch-hold-fish'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $actor->can('participate', $fish->round);
|
||||
}
|
||||
|
||||
protected function updateCatched(User $actor, Fish $fish)
|
||||
{
|
||||
if (!$fish->user_id_last_catch || $fish->user_id_last_catch !== $actor->id) {
|
||||
throw new ValidationException([
|
||||
'placement' => resolve(Translator::class)->trans(self::TRANSLATION_PREFIX . 'fish-update-wrong-user'),
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$fish->placement_valid_since || $fish->placement_valid_since->lt(Carbon::now())) {
|
||||
throw new ValidationException([
|
||||
'placement' => resolve(Translator::class)->trans(self::TRANSLATION_PREFIX . 'fish-update-expired'),
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function name(User $actor, Fish $fish)
|
||||
{
|
||||
return $actor->isAdmin() || ($this->updateCatched($actor, $fish) && $actor->can('catchthefish.choose-name'));
|
||||
}
|
||||
|
||||
public function place(User $actor, Fish $fish)
|
||||
{
|
||||
return $actor->isAdmin() || ($this->updateCatched($actor, $fish) && $actor->can('catchthefish.choose-place'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Access;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use XXB\CatchTheFish\Round;
|
||||
use Flarum\Foundation\ValidationException;
|
||||
use Flarum\Locale\Translator;
|
||||
use Flarum\User\Access\AbstractPolicy;
|
||||
use Flarum\User\User;
|
||||
|
||||
class RoundPolicy extends AbstractPolicy
|
||||
{
|
||||
const TRANSLATION_PREFIX = 'xxb-catch-the-fish.api.';
|
||||
|
||||
public function list(User $actor)
|
||||
{
|
||||
return $actor->isAdmin() || $actor->can('catchthefish.moderate');
|
||||
}
|
||||
|
||||
public function create(User $actor)
|
||||
{
|
||||
return $this->list($actor);
|
||||
}
|
||||
|
||||
public function createFish(User $actor, Round $round)
|
||||
{
|
||||
return $this->create($actor);
|
||||
}
|
||||
|
||||
public function update(User $actor, Round $round)
|
||||
{
|
||||
return $this->create($actor);
|
||||
}
|
||||
|
||||
public function delete(User $actor, Round $round)
|
||||
{
|
||||
return $this->update($actor, $round);
|
||||
}
|
||||
|
||||
public function participate(User $actor, Round $round)
|
||||
{
|
||||
if ($actor->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$now = Carbon::now();
|
||||
|
||||
if ($round->starts_at && $round->starts_at->gt($now)) {
|
||||
throw new ValidationException([
|
||||
'placement' => resolve(Translator::class)->trans(self::TRANSLATION_PREFIX . 'round-not-started'),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($round->ends_at && $round->ends_at->lt($now)) {
|
||||
throw new ValidationException([
|
||||
'placement' => resolve(Translator::class)->trans(self::TRANSLATION_PREFIX . 'round-finished'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $actor->can('catchthefish.participate');
|
||||
}
|
||||
|
||||
public function listFishes(User $actor, Round $round)
|
||||
{
|
||||
return $this->list($actor);
|
||||
}
|
||||
|
||||
public function listRankings(User $actor, Round $round)
|
||||
{
|
||||
return $actor->isAdmin() || $actor->can('catchthefish.list-rankings');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Api\Resource;
|
||||
|
||||
use DateTimeInterface;
|
||||
use XXB\CatchTheFish\Fish;
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Resource\AbstractDatabaseResource;
|
||||
use Flarum\Api\Schema;
|
||||
use Flarum\Foundation\ValidationException;
|
||||
|
||||
class FishResource extends AbstractDatabaseResource
|
||||
{
|
||||
public function type(): string
|
||||
{
|
||||
return 'catchthefish-fishes';
|
||||
}
|
||||
|
||||
public function model(): string
|
||||
{
|
||||
return Fish::class;
|
||||
}
|
||||
|
||||
public function endpoints(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
return [
|
||||
Schema\Str::make('name'),
|
||||
Schema\Str::make('image_url')
|
||||
->get(fn(Fish $fish, Context $context) => $fish->image_url),
|
||||
Schema\Arr::make('placement')
|
||||
->get(function (Fish $fish, Context $context) {
|
||||
if (!$this->actorCan($context, 'catch', $fish) && !$this->actorCan($context, 'catchthefish.moderate')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'discussion_id' => $fish->discussion_id_placement,
|
||||
'post_id' => $fish->post_id_placement,
|
||||
'user_id' => $fish->user_id_placement,
|
||||
];
|
||||
}),
|
||||
Schema\Boolean::make('canSee')
|
||||
->get(fn(Fish $fish, Context $context) => $this->actorCan($context, 'see', $fish)),
|
||||
Schema\Boolean::make('canCatch')
|
||||
->get(fn(Fish $fish, Context $context) => $this->actorCan($context, 'catch', $fish)),
|
||||
Schema\Boolean::make('canName')
|
||||
->get(fn(Fish $fish, Context $context) => $this->actorCan($context, 'name', $fish)),
|
||||
Schema\Boolean::make('canPlace')
|
||||
->get(fn(Fish $fish, Context $context) => $this->actorCan($context, 'place', $fish)),
|
||||
Schema\Str::make('placeUntil')
|
||||
->get(fn(Fish $fish, Context $context) => $this->actorCan($context, 'place', $fish) ? $this->formatDate($fish->placement_valid_since) : null),
|
||||
];
|
||||
}
|
||||
|
||||
protected function actorCan(Context $context, string $ability, $arguments = []): bool
|
||||
{
|
||||
$actor = $context->getActor();
|
||||
|
||||
if ($actor->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
return $actor->can($ability, $arguments);
|
||||
} catch (ValidationException $exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function formatDate($date): ?string
|
||||
{
|
||||
return $date instanceof DateTimeInterface ? $date->format(DateTimeInterface::ATOM) : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Api\Resource;
|
||||
|
||||
use DateTimeInterface;
|
||||
use XXB\CatchTheFish\Round;
|
||||
use Flarum\Api\Context;
|
||||
use Flarum\Api\Endpoint;
|
||||
use Flarum\Api\Resource\AbstractDatabaseResource;
|
||||
use Flarum\Api\Schema;
|
||||
|
||||
class RoundResource extends AbstractDatabaseResource
|
||||
{
|
||||
public function type(): string
|
||||
{
|
||||
return 'catchthefish-rounds';
|
||||
}
|
||||
|
||||
public function model(): string
|
||||
{
|
||||
return Round::class;
|
||||
}
|
||||
|
||||
public function endpoints(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
return [
|
||||
Schema\Str::make('name'),
|
||||
Schema\Str::make('starts_at')
|
||||
->get(fn(Round $round, Context $context) => $this->formatDate($round->starts_at)),
|
||||
Schema\Str::make('ends_at')
|
||||
->get(fn(Round $round, Context $context) => $this->formatDate($round->ends_at)),
|
||||
Schema\Integer::make('my_catch_count')
|
||||
->get(function (Round $round, Context $context) {
|
||||
$ranking = $round->userRanking($context->getActor());
|
||||
|
||||
return $ranking ? $ranking->catch_count : 0;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected function formatDate($date): ?string
|
||||
{
|
||||
return $date instanceof DateTimeInterface ? $date->format(DateTimeInterface::ATOM) : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ConfigureBasketRelationship
|
||||
{
|
||||
public function __invoke(AbstractModel $model)
|
||||
{
|
||||
return $model->hasMany(Fish::class, 'user_id_last_catch')
|
||||
->whereHas('round', function (Builder $query) {
|
||||
$query->activeRound();
|
||||
})
|
||||
->where('placement_valid_since', '>', Carbon::now())
|
||||
->orderBy('name');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ConfigureFishesRelationship
|
||||
{
|
||||
protected $foreignKey;
|
||||
|
||||
public function __construct(string $foreignKey)
|
||||
{
|
||||
$this->foreignKey = $foreignKey;
|
||||
}
|
||||
|
||||
public function __invoke(AbstractModel $model)
|
||||
{
|
||||
return $model->hasMany(Fish::class, $this->foreignKey)
|
||||
->whereHas('round', function (Builder $query) {
|
||||
$query->activeRound();
|
||||
})
|
||||
->activeFish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
abstract class AbstractCreateController extends AbstractShowController
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use Laminas\Diactoros\Response\EmptyResponse;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
abstract class AbstractDeleteController implements RequestHandlerInterface
|
||||
{
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->delete($request);
|
||||
|
||||
return new EmptyResponse(204);
|
||||
}
|
||||
|
||||
abstract protected function delete(ServerRequestInterface $request);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use Tobscure\JsonApi\Collection;
|
||||
use Tobscure\JsonApi\SerializerInterface;
|
||||
|
||||
abstract class AbstractListController extends AbstractSerializeController
|
||||
{
|
||||
protected function createElement($data, SerializerInterface $serializer)
|
||||
{
|
||||
return new Collection($data, $serializer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Laminas\Diactoros\Response\JsonResponse;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
use Tobscure\JsonApi\Parameters;
|
||||
use Tobscure\JsonApi\SerializerInterface;
|
||||
|
||||
abstract class AbstractSerializeController implements RequestHandlerInterface
|
||||
{
|
||||
public $serializer;
|
||||
public $include = [];
|
||||
public $optionalInclude = [];
|
||||
public $maxLimit = 50;
|
||||
public $limit = 20;
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$document = new Document();
|
||||
$data = $this->data($request, $document);
|
||||
$serializer = resolve($this->serializer);
|
||||
|
||||
if (method_exists($serializer, 'setRequest')) {
|
||||
$serializer->setRequest($request);
|
||||
}
|
||||
|
||||
$element = $this->createElement($data, $serializer)
|
||||
->with($this->extractInclude($request))
|
||||
->fields($this->extractFields($request));
|
||||
|
||||
$document->setData($element);
|
||||
|
||||
return new JsonResponse($document);
|
||||
}
|
||||
|
||||
abstract protected function data(ServerRequestInterface $request, Document $document);
|
||||
|
||||
abstract protected function createElement($data, SerializerInterface $serializer);
|
||||
|
||||
protected function extractInclude(ServerRequestInterface $request)
|
||||
{
|
||||
$available = array_merge($this->include, $this->optionalInclude);
|
||||
|
||||
return $this->buildParameters($request)->getInclude($available) ?: $this->include;
|
||||
}
|
||||
|
||||
protected function extractFields(ServerRequestInterface $request)
|
||||
{
|
||||
return $this->buildParameters($request)->getFields();
|
||||
}
|
||||
|
||||
protected function extractOffset(ServerRequestInterface $request)
|
||||
{
|
||||
return (int) $this->buildParameters($request)->getOffset($this->extractLimit($request)) ?: 0;
|
||||
}
|
||||
|
||||
protected function extractLimit(ServerRequestInterface $request)
|
||||
{
|
||||
return (int) $this->buildParameters($request)->getLimit($this->maxLimit) ?: $this->limit;
|
||||
}
|
||||
|
||||
protected function buildParameters(ServerRequestInterface $request): Parameters
|
||||
{
|
||||
return new Parameters($request->getQueryParams());
|
||||
}
|
||||
|
||||
protected function input(ServerRequestInterface $request, string $key, $default = null)
|
||||
{
|
||||
return Arr::get($request->getParsedBody(), $key, $default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use Tobscure\JsonApi\Resource;
|
||||
use Tobscure\JsonApi\SerializerInterface;
|
||||
|
||||
abstract class AbstractShowController extends AbstractSerializeController
|
||||
{
|
||||
protected function createElement($data, SerializerInterface $serializer)
|
||||
{
|
||||
return new Resource($data, $serializer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use XXB\CatchTheFish\Repositories\FishRepository;
|
||||
use XXB\CatchTheFish\Repositories\PlacementRepository;
|
||||
use XXB\CatchTheFish\Serializers\FishSerializer;
|
||||
use XXB\CatchTheFish\Controllers\AbstractShowController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class FishCatchController extends AbstractShowController
|
||||
{
|
||||
public $serializer = FishSerializer::class;
|
||||
|
||||
public $include = [
|
||||
'round.myRanking',
|
||||
];
|
||||
|
||||
protected $fishes;
|
||||
protected $placement;
|
||||
|
||||
public function __construct(FishRepository $fishes, PlacementRepository $placement)
|
||||
{
|
||||
$this->fishes = $fishes;
|
||||
$this->placement = $placement;
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
|
||||
$fish = $this->fishes->findOrFail($id);
|
||||
|
||||
return $this->placement->catch(RequestUtil::getActor($request), $fish, $request->getParsedBody());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use XXB\CatchTheFish\Repositories\FishRepository;
|
||||
use XXB\CatchTheFish\Controllers\AbstractDeleteController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class FishDeleteController extends AbstractDeleteController
|
||||
{
|
||||
protected $fishes;
|
||||
|
||||
public function __construct(FishRepository $fishes)
|
||||
{
|
||||
$this->fishes = $fishes;
|
||||
}
|
||||
|
||||
protected function delete(ServerRequestInterface $request)
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
|
||||
$fish = $this->fishes->findOrFail($id);
|
||||
|
||||
RequestUtil::getActor($request)->assertCan('delete', $fish);
|
||||
|
||||
$this->fishes->delete($fish);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use XXB\CatchTheFish\Repositories\FishRepository;
|
||||
use XXB\CatchTheFish\Repositories\RoundRepository;
|
||||
use XXB\CatchTheFish\Serializers\FishSerializer;
|
||||
use XXB\CatchTheFish\Controllers\AbstractListController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class FishImageBulkController extends AbstractListController
|
||||
{
|
||||
public $serializer = FishSerializer::class;
|
||||
|
||||
protected $rounds;
|
||||
protected $fishes;
|
||||
|
||||
public function __construct(RoundRepository $rounds, FishRepository $fishes)
|
||||
{
|
||||
$this->rounds = $rounds;
|
||||
$this->fishes = $fishes;
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$roundId = Arr::get($request->getQueryParams(), 'id');
|
||||
|
||||
$round = $this->rounds->findOrFail($roundId);
|
||||
|
||||
RequestUtil::getActor($request)->assertCan('createFish', $round);
|
||||
|
||||
$files = array_filter($request->getUploadedFiles(), function (string $key) {
|
||||
return Str::startsWith($key, 'image');
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
|
||||
return $this->fishes->bulkImageImport($round, $files);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use XXB\CatchTheFish\Repositories\FishRepository;
|
||||
use XXB\CatchTheFish\Serializers\FishSerializer;
|
||||
use XXB\CatchTheFish\Controllers\AbstractShowController;
|
||||
use Flarum\Foundation\ValidationException;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class FishImageController extends AbstractShowController
|
||||
{
|
||||
public $serializer = FishSerializer::class;
|
||||
|
||||
protected $fishes;
|
||||
|
||||
public function __construct(FishRepository $fishes)
|
||||
{
|
||||
$this->fishes = $fishes;
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
|
||||
$fish = $this->fishes->findOrFail($id);
|
||||
|
||||
RequestUtil::getActor($request)->assertCan('update', $fish);
|
||||
|
||||
$file = Arr::get($request->getUploadedFiles(), 'image');
|
||||
|
||||
if (!$file) {
|
||||
throw new ValidationException([
|
||||
'image' => 'No image file uploaded',
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->fishes->updateImage($fish, $file);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use XXB\CatchTheFish\Repositories\FishRepository;
|
||||
use XXB\CatchTheFish\Repositories\RoundRepository;
|
||||
use XXB\CatchTheFish\Serializers\FishSerializer;
|
||||
use XXB\CatchTheFish\Controllers\AbstractListController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class FishIndexController extends AbstractListController
|
||||
{
|
||||
public $serializer = FishSerializer::class;
|
||||
|
||||
public $include = [
|
||||
'lastUserPlacement',
|
||||
'lastUserNaming',
|
||||
'placement',
|
||||
'placement.discussion',
|
||||
];
|
||||
|
||||
protected $rounds;
|
||||
protected $fishes;
|
||||
|
||||
public function __construct(RoundRepository $rounds, FishRepository $fishes)
|
||||
{
|
||||
$this->rounds = $rounds;
|
||||
$this->fishes = $fishes;
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$roundId = Arr::get($request->getQueryParams(), 'id');
|
||||
|
||||
$round = $this->rounds->findOrFail($roundId);
|
||||
|
||||
RequestUtil::getActor($request)->assertCan('listFishes', $round);
|
||||
|
||||
return $this->fishes->all($round, $this->extractLimit($request), $this->extractOffset($request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use XXB\CatchTheFish\Repositories\FishRepository;
|
||||
use XXB\CatchTheFish\Repositories\PlacementRepository;
|
||||
use XXB\CatchTheFish\Serializers\FishSerializer;
|
||||
use XXB\CatchTheFish\Controllers\AbstractShowController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class FishPlaceController extends AbstractShowController
|
||||
{
|
||||
public $serializer = FishSerializer::class;
|
||||
|
||||
protected $fishes;
|
||||
protected $placement;
|
||||
|
||||
public function __construct(FishRepository $fishes, PlacementRepository $placement)
|
||||
{
|
||||
$this->fishes = $fishes;
|
||||
$this->placement = $placement;
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
|
||||
$fish = $this->fishes->findOrFail($id);
|
||||
|
||||
return $this->placement->place(RequestUtil::getActor($request), $fish, $request->getParsedBody());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use XXB\CatchTheFish\Repositories\FishRepository;
|
||||
use XXB\CatchTheFish\Repositories\RoundRepository;
|
||||
use XXB\CatchTheFish\Serializers\FishSerializer;
|
||||
use XXB\CatchTheFish\Controllers\AbstractCreateController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class FishStoreController extends AbstractCreateController
|
||||
{
|
||||
public $serializer = FishSerializer::class;
|
||||
|
||||
protected $rounds;
|
||||
protected $fishes;
|
||||
|
||||
public function __construct(RoundRepository $rounds, FishRepository $fishes)
|
||||
{
|
||||
$this->rounds = $rounds;
|
||||
$this->fishes = $fishes;
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$roundId = Arr::get($request->getQueryParams(), 'id');
|
||||
|
||||
$round = $this->rounds->findOrFail($roundId);
|
||||
|
||||
RequestUtil::getActor($request)->assertCan('createFish', $round);
|
||||
|
||||
$attributes = Arr::get($request->getParsedBody(), 'data.attributes', []);
|
||||
|
||||
return $this->fishes->store($round, $attributes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use XXB\CatchTheFish\Repositories\FishRepository;
|
||||
use XXB\CatchTheFish\Serializers\FishSerializer;
|
||||
use XXB\CatchTheFish\Controllers\AbstractShowController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class FishUpdateController extends AbstractShowController
|
||||
{
|
||||
public $serializer = FishSerializer::class;
|
||||
|
||||
protected $fishes;
|
||||
|
||||
public function __construct(FishRepository $fishes)
|
||||
{
|
||||
$this->fishes = $fishes;
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
|
||||
$fish = $this->fishes->findOrFail($id);
|
||||
|
||||
RequestUtil::getActor($request)->assertCan('update', $fish);
|
||||
|
||||
$attributes = Arr::get($request->getParsedBody(), 'data.attributes', []);
|
||||
|
||||
return $this->fishes->update($fish, $attributes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use XXB\CatchTheFish\Repositories\RankingRepository;
|
||||
use XXB\CatchTheFish\Repositories\RoundRepository;
|
||||
use XXB\CatchTheFish\Serializers\RankingSerializer;
|
||||
use XXB\CatchTheFish\Controllers\AbstractListController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class RankingIndexController extends AbstractListController
|
||||
{
|
||||
public $serializer = RankingSerializer::class;
|
||||
|
||||
public $include = [
|
||||
'user',
|
||||
];
|
||||
|
||||
protected $rounds;
|
||||
protected $rankings;
|
||||
|
||||
public function __construct(RoundRepository $rounds, RankingRepository $rankings)
|
||||
{
|
||||
$this->rounds = $rounds;
|
||||
$this->rankings = $rankings;
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$roundId = Arr::get($request->getQueryParams(), 'id');
|
||||
|
||||
$round = $this->rounds->findOrFail($roundId);
|
||||
|
||||
RequestUtil::getActor($request)->assertCan('listRankings', $round);
|
||||
|
||||
return $this->rankings->all($round);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use XXB\CatchTheFish\Repositories\RoundRepository;
|
||||
use XXB\CatchTheFish\Controllers\AbstractDeleteController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class RoundDeleteController extends AbstractDeleteController
|
||||
{
|
||||
protected $rounds;
|
||||
|
||||
public function __construct(RoundRepository $rounds)
|
||||
{
|
||||
$this->rounds = $rounds;
|
||||
}
|
||||
|
||||
protected function delete(ServerRequestInterface $request)
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
|
||||
$round = $this->rounds->findOrFail($id);
|
||||
|
||||
RequestUtil::getActor($request)->assertCan('delete', $round);
|
||||
|
||||
$this->rounds->delete($round);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use XXB\CatchTheFish\Repositories\RoundRepository;
|
||||
use XXB\CatchTheFish\Round;
|
||||
use XXB\CatchTheFish\Serializers\RoundSerializer;
|
||||
use XXB\CatchTheFish\Controllers\AbstractListController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class RoundIndexController extends AbstractListController
|
||||
{
|
||||
public $serializer = RoundSerializer::class;
|
||||
|
||||
protected $rounds;
|
||||
|
||||
public function __construct(RoundRepository $rounds)
|
||||
{
|
||||
$this->rounds = $rounds;
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
if (!$actor->can('list', Round::class)) {
|
||||
$actor->assertCan('catchthefish.visible');
|
||||
|
||||
return $this->rounds->allActive();
|
||||
}
|
||||
|
||||
$actor->assertCan('list', Round::class);
|
||||
|
||||
return $this->rounds->all($this->extractLimit($request), $this->extractOffset($request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use XXB\CatchTheFish\Repositories\RoundRepository;
|
||||
use XXB\CatchTheFish\Serializers\RoundSerializer;
|
||||
use XXB\CatchTheFish\Controllers\AbstractShowController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class RoundShowController extends AbstractShowController
|
||||
{
|
||||
public $serializer = RoundSerializer::class;
|
||||
|
||||
protected $rounds;
|
||||
|
||||
public function __construct(RoundRepository $rounds)
|
||||
{
|
||||
$this->rounds = $rounds;
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
|
||||
$round = $this->rounds->findOrFail($id);
|
||||
|
||||
RequestUtil::getActor($request)->assertCan('view', $round);
|
||||
|
||||
return $round;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use XXB\CatchTheFish\Repositories\RoundRepository;
|
||||
use XXB\CatchTheFish\Round;
|
||||
use XXB\CatchTheFish\Serializers\RoundSerializer;
|
||||
use XXB\CatchTheFish\Controllers\AbstractCreateController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class RoundStoreController extends AbstractCreateController
|
||||
{
|
||||
public $serializer = RoundSerializer::class;
|
||||
|
||||
protected $rounds;
|
||||
|
||||
public function __construct(RoundRepository $rounds)
|
||||
{
|
||||
$this->rounds = $rounds;
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
RequestUtil::getActor($request)->assertCan('create', Round::class);
|
||||
|
||||
$attributes = Arr::get($request->getParsedBody(), 'data.attributes', []);
|
||||
|
||||
return $this->rounds->store($attributes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Controllers;
|
||||
|
||||
use XXB\CatchTheFish\Repositories\RoundRepository;
|
||||
use XXB\CatchTheFish\Serializers\RoundSerializer;
|
||||
use XXB\CatchTheFish\Controllers\AbstractShowController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Document;
|
||||
|
||||
class RoundUpdateController extends AbstractShowController
|
||||
{
|
||||
public $serializer = RoundSerializer::class;
|
||||
|
||||
protected $rounds;
|
||||
|
||||
public function __construct(RoundRepository $rounds)
|
||||
{
|
||||
$this->rounds = $rounds;
|
||||
}
|
||||
|
||||
protected function data(ServerRequestInterface $request, Document $document)
|
||||
{
|
||||
$id = Arr::get($request->getQueryParams(), 'id');
|
||||
|
||||
$round = $this->rounds->findOrFail($id);
|
||||
|
||||
RequestUtil::getActor($request)->assertCan('update', $round);
|
||||
|
||||
$attributes = Arr::get($request->getParsedBody(), 'data.attributes', []);
|
||||
|
||||
return $this->rounds->update($round, $attributes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Extenders;
|
||||
|
||||
use Flarum\Extend\ApiController;
|
||||
use Flarum\Extend\ExtenderInterface;
|
||||
use Flarum\Extension\Extension;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
/**
|
||||
* Custom extender that allows calling ApiController::addIncludes on multiple controller classes at once
|
||||
*/
|
||||
class ApiControllerIncludes implements ExtenderInterface
|
||||
{
|
||||
protected $controllerClasses = [];
|
||||
protected $addIncludes = [];
|
||||
|
||||
public function __construct(array $controllerClasses)
|
||||
{
|
||||
$this->controllerClasses = $controllerClasses;
|
||||
}
|
||||
|
||||
public function addInclude($name): self
|
||||
{
|
||||
$this->addIncludes[] = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function extend(Container $container, Extension $extension = null)
|
||||
{
|
||||
foreach ($this->controllerClasses as $controllerClass) {
|
||||
$extender = new ApiController($controllerClass);
|
||||
|
||||
foreach ($this->addIncludes as $addInclude) {
|
||||
$extender->addInclude($addInclude);
|
||||
}
|
||||
|
||||
$extender->extend($container, $extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\UrlGenerator;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Fish extends AbstractModel
|
||||
{
|
||||
protected $table = 'catchthefish_fishes';
|
||||
|
||||
protected $casts = [
|
||||
'placement_valid_since' => 'datetime',
|
||||
'last_caught_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
public function round(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Round::class);
|
||||
}
|
||||
|
||||
public function placementDiscussion(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Discussion::class, 'discussion_id_placement');
|
||||
}
|
||||
|
||||
public function placementPost(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Post::class, 'post_id_placement');
|
||||
}
|
||||
|
||||
public function placementUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id_placement');
|
||||
}
|
||||
|
||||
public function lastUserCatch(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id_last_catch');
|
||||
}
|
||||
|
||||
public function lastUserPlacement(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id_last_placement');
|
||||
}
|
||||
|
||||
public function lastUserNaming(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id_last_naming');
|
||||
}
|
||||
|
||||
public function getImageUrlAttribute(): ?string
|
||||
{
|
||||
if (!$this->image) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (strpos($this->image, '://') === false) {
|
||||
$generator = resolve(UrlGenerator::class);
|
||||
|
||||
return $generator->to('forum')->path('assets/catch-the-fish/' . $this->image);
|
||||
}
|
||||
|
||||
return $this->image;
|
||||
}
|
||||
|
||||
public function scopeActiveFish(Builder $query): Builder
|
||||
{
|
||||
return $query->where('placement_valid_since', '<', Carbon::now());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish;
|
||||
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
|
||||
class ForumAttributes
|
||||
{
|
||||
protected $settings;
|
||||
|
||||
public function __construct(SettingsRepositoryInterface $settings)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
public function __invoke($serializer): array
|
||||
{
|
||||
return [
|
||||
'catchTheFishCanModerate' => $serializer->getActor()->can('catchthefish.moderate'),
|
||||
'catchTheFishCanSeeRankingsPage' => $serializer->getActor()->can('catchthefish.list-rankings'),
|
||||
'catchTheFishAlertRound' => $this->settings->get('catch-the-fish.alertRound') !== '0',
|
||||
'catchTheFishAnimateFlip' => $this->settings->get('catch-the-fish.animateFlip') !== '0',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish;
|
||||
|
||||
use XXB\CatchTheFish\Repositories\RoundRepository;
|
||||
use Flarum\Api\Controller\ShowForumController;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class LoadRoundsRelationship
|
||||
{
|
||||
protected $repository;
|
||||
|
||||
public function __construct(RoundRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
public function __invoke(ShowForumController $controller, &$data, ServerRequestInterface $request)
|
||||
{
|
||||
if (RequestUtil::getActor($request)->can('catchthefish.visible')) {
|
||||
$data['catchTheFishActiveRounds'] = $this->repository->allActive();
|
||||
} else {
|
||||
$data['catchTheFishActiveRounds'] = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Providers;
|
||||
|
||||
use XXB\CatchTheFish\Repositories\FishImageUploader;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Foundation\Paths;
|
||||
use League\Flysystem\Filesystem;
|
||||
use League\Flysystem\Local\LocalFilesystemAdapter;
|
||||
|
||||
class StorageServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
public function register()
|
||||
{
|
||||
$this->container->bind('catchthefish-assets', function () {
|
||||
return new Filesystem(new LocalFilesystemAdapter($this->container->make(Paths::class)->public . '/assets/catch-the-fish'));
|
||||
});
|
||||
|
||||
$this->container->when(FishImageUploader::class)
|
||||
->needs(Filesystem::class)
|
||||
->give('catchthefish-assets');
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish;
|
||||
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Ranking extends AbstractModel
|
||||
{
|
||||
protected $table = 'catchthefish_rankings';
|
||||
|
||||
protected $casts = [
|
||||
'catch_count' => 'int',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'round_id',
|
||||
'user_id',
|
||||
'catch_count',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
public function round(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Round::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Repositories;
|
||||
|
||||
use XXB\CatchTheFish\Fish;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Constraint;
|
||||
use Intervention\Image\Image;
|
||||
use League\Flysystem\Filesystem;
|
||||
|
||||
class FishImageUploader
|
||||
{
|
||||
protected $assets;
|
||||
|
||||
public function __construct(Filesystem $assets)
|
||||
{
|
||||
$this->assets = $assets;
|
||||
}
|
||||
|
||||
public function upload(Fish $fish, Image $image)
|
||||
{
|
||||
if (extension_loaded('exif')) {
|
||||
if (method_exists($image, 'orientate')) {
|
||||
$image->orientate();
|
||||
} elseif (method_exists($image, 'orient')) {
|
||||
$image = $image->orient();
|
||||
}
|
||||
}
|
||||
|
||||
if (method_exists($image, 'scaleDown')) {
|
||||
$image = $image->scaleDown(width: 300, height: 200);
|
||||
} else {
|
||||
$image = $image->resize(300, 200, function (Constraint $size) {
|
||||
$size->aspectRatio();
|
||||
$size->upsize();
|
||||
});
|
||||
}
|
||||
|
||||
$encodedImage = method_exists($image, 'toPng') ? (string) $image->toPng() : (string) $image->encode('png');
|
||||
|
||||
$imagePath = Str::random() . '.png';
|
||||
|
||||
$this->remove($fish);
|
||||
$fish->image = $imagePath;
|
||||
|
||||
$this->assets->write($imagePath, $encodedImage);
|
||||
}
|
||||
|
||||
public function remove(Fish $fish)
|
||||
{
|
||||
$imagePath = $fish->image;
|
||||
|
||||
if ($imagePath && $this->assets->fileExists($imagePath)) {
|
||||
$this->assets->delete($imagePath);
|
||||
}
|
||||
|
||||
$fish->image = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Repositories;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use XXB\CatchTheFish\Fish;
|
||||
use XXB\CatchTheFish\Round;
|
||||
use XXB\CatchTheFish\Validators\FishImageValidator;
|
||||
use XXB\CatchTheFish\Validators\FishValidator;
|
||||
use Flarum\Foundation\Paths;
|
||||
use Flarum\Locale\Translator;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Intervention\Image\Image;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Psr\Http\Message\UploadedFileInterface;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class FishRepository
|
||||
{
|
||||
const BASE_IMAGES = [
|
||||
'pixabay-30828-640.png',
|
||||
'pixabay-30837-640.png',
|
||||
'pixabay-33712-640.png',
|
||||
'pixabay-36828-640.png',
|
||||
'pixabay-294469-640.png',
|
||||
'pixabay-1331813-640.png',
|
||||
];
|
||||
|
||||
protected $paths;
|
||||
protected $validator;
|
||||
protected $imageValidator;
|
||||
protected $uploader;
|
||||
protected $translator;
|
||||
|
||||
public function __construct(Paths $paths, FishValidator $validator, FishImageValidator $imageValidator, FishImageUploader $uploader, Translator $translator)
|
||||
{
|
||||
$this->paths = $paths;
|
||||
$this->validator = $validator;
|
||||
$this->imageValidator = $imageValidator;
|
||||
$this->uploader = $uploader;
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
public function all(Round $round, $limit = null, $offset = 0)
|
||||
{
|
||||
$query = $round->fishes()->getQuery();
|
||||
|
||||
if ($limit !== null) {
|
||||
$query->limit($limit)->offset($offset);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
public function findOrFail($id): Fish
|
||||
{
|
||||
return Fish::query()->where('id', $id)->firstOrFail();
|
||||
}
|
||||
|
||||
public function store(Round $round, array $attributes): Fish
|
||||
{
|
||||
$this->validator->assertValid($attributes);
|
||||
|
||||
$fish = new Fish($attributes);
|
||||
$fish->round()->associate($round);
|
||||
Placement::random()->assign($fish);
|
||||
$fish->placement_valid_since = Carbon::now();
|
||||
$fish->save();
|
||||
|
||||
return $fish;
|
||||
}
|
||||
|
||||
public function update(Fish $fish, array $attributes): Fish
|
||||
{
|
||||
$this->validator->assertValid($attributes);
|
||||
|
||||
if (Arr::has($attributes, 'name')) {
|
||||
$fish->user_id_last_naming = null;
|
||||
}
|
||||
|
||||
$fish->fill($attributes);
|
||||
$fish->save();
|
||||
|
||||
return $fish;
|
||||
}
|
||||
|
||||
public function updateImage(Fish $fish, UploadedFileInterface $file): Fish
|
||||
{
|
||||
$tmpFile = tempnam($this->paths->storage . '/tmp', 'catch-the-fish');
|
||||
$file->moveTo($tmpFile);
|
||||
|
||||
try {
|
||||
$file = new UploadedFile(
|
||||
$tmpFile,
|
||||
$file->getClientFilename(),
|
||||
$file->getClientMediaType(),
|
||||
$file->getError(),
|
||||
true
|
||||
);
|
||||
|
||||
$this->imageValidator->assertValid(['image' => $file]);
|
||||
|
||||
$image = $this->readImage($tmpFile);
|
||||
|
||||
$this->uploader->upload($fish, $image);
|
||||
|
||||
$fish->save();
|
||||
} finally {
|
||||
@unlink($tmpFile);
|
||||
}
|
||||
|
||||
return $fish;
|
||||
}
|
||||
|
||||
public function bulkImageImport(Round $round, array $files): array
|
||||
{
|
||||
$filesToUnlink = [];
|
||||
$originalNames = [];
|
||||
|
||||
try {
|
||||
return collect($files)->map(function (UploadedFileInterface $file, $index) use (&$filesToUnlink, &$originalNames) {
|
||||
$tmpFile = tempnam($this->paths->storage . '/tmp', 'catch-the-fish');
|
||||
$file->moveTo($tmpFile);
|
||||
$filesToUnlink[] = $tmpFile;
|
||||
|
||||
$originalNames[$index] = $file->getClientFilename();
|
||||
|
||||
$file = new UploadedFile(
|
||||
$tmpFile,
|
||||
$file->getClientFilename(),
|
||||
$file->getClientMediaType(),
|
||||
$file->getError(),
|
||||
true
|
||||
);
|
||||
|
||||
$this->imageValidator->assertValid(['image' => $file]);
|
||||
|
||||
return $this->readImage($tmpFile);
|
||||
})->map(function (Image $image, $index) use ($originalNames, $round) {
|
||||
$fish = new Fish();
|
||||
|
||||
$this->uploader->upload($fish, $image);
|
||||
|
||||
$fish->name = explode('.', $originalNames[$index])[0];
|
||||
$fish->round()->associate($round);
|
||||
Placement::random()->assign($fish);
|
||||
$fish->placement_valid_since = Carbon::now();
|
||||
$fish->save();
|
||||
|
||||
return $fish;
|
||||
})->all();
|
||||
} finally {
|
||||
foreach ($filesToUnlink as $tmpFile) {
|
||||
@unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(Fish $fish): void
|
||||
{
|
||||
$this->uploader->remove($fish);
|
||||
|
||||
$fish->delete();
|
||||
}
|
||||
|
||||
public function storeDefaultData(Round $round): void
|
||||
{
|
||||
$now = Carbon::now();
|
||||
|
||||
foreach (self::BASE_IMAGES as $index => $originalImagePath) {
|
||||
$fish = new Fish();
|
||||
|
||||
$image = $this->readImage(__DIR__ . '/../../resources/images/' . $originalImagePath);
|
||||
$this->uploader->upload($fish, $image);
|
||||
|
||||
$fish->round_id = $round->id;
|
||||
$fish->name = $this->translator->trans('xxb-catch-the-fish.api.default-fish-name', [
|
||||
'{number}' => $index + 1,
|
||||
]);
|
||||
Placement::random()->assign($fish);
|
||||
$fish->placement_valid_since = $now;
|
||||
$fish->save();
|
||||
}
|
||||
}
|
||||
|
||||
protected function imageManager(): ImageManager
|
||||
{
|
||||
return resolve(ImageManager::class);
|
||||
}
|
||||
|
||||
protected function readImage(string $path): Image
|
||||
{
|
||||
$manager = $this->imageManager();
|
||||
|
||||
if (method_exists($manager, 'read')) {
|
||||
return $manager->read($path);
|
||||
}
|
||||
|
||||
return $manager->make($path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Repositories;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use XXB\CatchTheFish\Fish;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Extension\ExtensionManager;
|
||||
use Flarum\Foundation\ValidationException;
|
||||
use Flarum\Locale\Translator;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class Placement
|
||||
{
|
||||
public $discussionId;
|
||||
public $postId;
|
||||
public $userId;
|
||||
|
||||
const TRANSLATION_PREFIX = 'xxb-catch-the-fish.api.';
|
||||
|
||||
protected static function intSettingWithDefault(string $key, int $default): int
|
||||
{
|
||||
$value = resolve(SettingsRepositoryInterface::class)->get("catch-the-fish.$key") ?? '';
|
||||
|
||||
if ($value === '') {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
protected static function settingDiscussionAgeDays(): int
|
||||
{
|
||||
return self::intSettingWithDefault('discussionAgeDays', 14);
|
||||
}
|
||||
|
||||
protected static function settingPostAgeDays(): int
|
||||
{
|
||||
return self::intSettingWithDefault('postAgeDays', 14);
|
||||
}
|
||||
|
||||
protected static function settingPostProbability(): int
|
||||
{
|
||||
return self::intSettingWithDefault('postProbability', 50);
|
||||
}
|
||||
|
||||
protected static function settingUserAgeDays()
|
||||
{
|
||||
return resolve(SettingsRepositoryInterface::class)->get('catch-the-fish.userAgeDays', 14);
|
||||
}
|
||||
|
||||
protected static function settingUserProbability(): int
|
||||
{
|
||||
return self::intSettingWithDefault('userProbability', 33);
|
||||
}
|
||||
|
||||
protected static function settingDiscussionTags()
|
||||
{
|
||||
return json_decode(resolve(SettingsRepositoryInterface::class)->get('catch-the-fish.discussionTags') ?: '[]', true);
|
||||
}
|
||||
|
||||
public function assertValid(): void
|
||||
{
|
||||
$translator = resolve(Translator::class);
|
||||
|
||||
if (!is_null($this->discussionId) + !is_null($this->postId) + !is_null($this->userId) !== 1) {
|
||||
throw new ValidationException([
|
||||
'placement' => $translator->trans(self::TRANSLATION_PREFIX . 'too-many-placement-models'),
|
||||
]);
|
||||
}
|
||||
|
||||
$model = null;
|
||||
|
||||
if (!is_null($this->discussionId)) {
|
||||
if (!($model = Discussion::query()->where('id', $this->discussionId)->first())) {
|
||||
throw new ValidationException([
|
||||
'placement' => $translator->trans(self::TRANSLATION_PREFIX . 'invalid-discussion-id'),
|
||||
]);
|
||||
}
|
||||
} elseif (!is_null($this->postId)) {
|
||||
if (!($model = Post::query()->where('id', $this->postId)->first())) {
|
||||
throw new ValidationException([
|
||||
'placement' => $translator->trans(self::TRANSLATION_PREFIX . 'invalid-post-id'),
|
||||
]);
|
||||
}
|
||||
} elseif (!is_null($this->userId)) {
|
||||
if (!($model = User::query()->where('id', $this->userId)->first())) {
|
||||
throw new ValidationException([
|
||||
'placement' => $translator->trans(self::TRANSLATION_PREFIX . 'invalid-user-id'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($model instanceof Discussion || $model instanceof Post) {
|
||||
if (!is_null($model->hidden_at)) {
|
||||
throw new ValidationException([
|
||||
'placement' => $translator->trans(self::TRANSLATION_PREFIX . 'model-deleted'),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($model->is_private) {
|
||||
throw new ValidationException([
|
||||
'placement' => $translator->trans(self::TRANSLATION_PREFIX . 'model-private'),
|
||||
]);
|
||||
}
|
||||
|
||||
$extensions = resolve(ExtensionManager::class);
|
||||
|
||||
$tagSettings = self::settingDiscussionTags();
|
||||
|
||||
if ($extensions->isEnabled('flarum-tags') && is_array($tagSettings) && count($tagSettings)) {
|
||||
if ($model instanceof Post) {
|
||||
$discussion = $model->discussion;
|
||||
} else {
|
||||
$discussion = $model;
|
||||
}
|
||||
|
||||
if (!$discussion->tags()->whereIn('id', Arr::pluck($tagSettings, 'tagId'))->exists()) {
|
||||
throw new ValidationException([
|
||||
'placement' => $translator->trans(self::TRANSLATION_PREFIX . 'tag-not-allowed'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($model instanceof Discussion) {
|
||||
if (is_null($model->last_posted_at) || $model->last_posted_at->lt(Carbon::now()->subDays(self::settingDiscussionAgeDays()))) {
|
||||
throw new ValidationException([
|
||||
'placement' => $translator->trans(self::TRANSLATION_PREFIX . 'inactive-discussion', [
|
||||
'{days}' => self::settingDiscussionAgeDays(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
} elseif ($model instanceof Post) {
|
||||
if ($model->created_at->lt(Carbon::now()->subDays(self::settingPostAgeDays()))) {
|
||||
throw new ValidationException([
|
||||
'placement' => $translator->trans(self::TRANSLATION_PREFIX . 'inactive-post', [
|
||||
'{days}' => self::settingPostAgeDays(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($model->type !== 'comment') {
|
||||
throw new ValidationException([
|
||||
'placement' => $translator->trans(self::TRANSLATION_PREFIX . 'non-comment-post'),
|
||||
]);
|
||||
}
|
||||
} elseif ($model instanceof User) {
|
||||
if (is_null($model->last_seen_at) || $model->last_seen_at->lt(Carbon::now()->subDays(self::settingUserAgeDays()))) {
|
||||
throw new ValidationException([
|
||||
'placement' => $translator->trans(self::TRANSLATION_PREFIX . 'inactive-user', [
|
||||
'{days}' => self::settingUserAgeDays(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
$extensions = resolve(ExtensionManager::class);
|
||||
|
||||
if ($extensions->isEnabled('flarum-suspend') && !is_null($model->suspended_until) && $model->suspended_until->gt(Carbon::now())) {
|
||||
throw new ValidationException([
|
||||
'placement' => $translator->trans(self::TRANSLATION_PREFIX . 'user-suspended'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static function randomUser(): ?User
|
||||
{
|
||||
$query = User::query()
|
||||
->where('last_seen_at', '>', Carbon::now()->subDays(self::settingUserAgeDays()))
|
||||
->whereHas('posts');
|
||||
|
||||
$extensions = resolve(ExtensionManager::class);
|
||||
|
||||
if ($extensions->isEnabled('flarum-suspend')) {
|
||||
$query->whereNull('suspended_until');
|
||||
}
|
||||
|
||||
$count = $query->count();
|
||||
|
||||
if ($count === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $query->offset(random_int(0, $count - 1))->limit(1)->first();
|
||||
}
|
||||
|
||||
protected static function randomDiscussion($tagId = null): Discussion
|
||||
{
|
||||
$query = Discussion::query()
|
||||
->where('is_private', false)
|
||||
->whereNull('hidden_at')
|
||||
->where('last_posted_at', '>', Carbon::now()->subDays(self::settingDiscussionAgeDays()));
|
||||
|
||||
if ($tagId) {
|
||||
$query->whereHas('tags', function (Builder $query) use ($tagId) {
|
||||
$query->where('id', $tagId);
|
||||
});
|
||||
}
|
||||
|
||||
$count = $query->count();
|
||||
|
||||
if ($count === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $query->offset(random_int(0, $count - 1))->limit(1)->first();
|
||||
}
|
||||
|
||||
protected static function randomPost(Discussion $discussion): ?Post
|
||||
{
|
||||
$query = $discussion
|
||||
->comments()
|
||||
->where('created_at', '>', Carbon::now()->subDays(self::settingPostAgeDays()));
|
||||
|
||||
$count = $query->count();
|
||||
|
||||
if ($count === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $query->offset(random_int(0, $count - 1))->limit(1)->first();
|
||||
}
|
||||
|
||||
public static function random(): self
|
||||
{
|
||||
$placement = new self();
|
||||
|
||||
if (random_int(0, 99) < self::settingUserProbability()) {
|
||||
$user = self::randomUser();
|
||||
|
||||
if ($user) {
|
||||
$placement->userId = $user->id;
|
||||
|
||||
return $placement;
|
||||
}
|
||||
}
|
||||
|
||||
$extensions = resolve(ExtensionManager::class);
|
||||
|
||||
$tagSettings = self::settingDiscussionTags();
|
||||
|
||||
$discussion = null;
|
||||
|
||||
if ($extensions->isEnabled('flarum-tags') && is_array($tagSettings) && count($tagSettings)) {
|
||||
foreach ($tagSettings as $index => $tagSetting) {
|
||||
|
||||
if ($index !== count($tagSettings) - 1 && random_int(0, 99) >= Arr::get($tagSetting, 'probability')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$discussion = self::randomDiscussion(Arr::get($tagSetting, 'tagId'));
|
||||
|
||||
if ($discussion) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$discussion = self::randomDiscussion();
|
||||
}
|
||||
|
||||
if (!$discussion) {
|
||||
$translator = resolve(Translator::class);
|
||||
|
||||
throw new ValidationException([
|
||||
'placement' => $translator->trans(self::TRANSLATION_PREFIX . 'random-error'),
|
||||
]);
|
||||
}
|
||||
|
||||
if (random_int(0, 99) < self::settingPostProbability()) {
|
||||
$post = self::randomPost($discussion);
|
||||
|
||||
if ($post) {
|
||||
$placement->postId = $post->id;
|
||||
|
||||
return $placement;
|
||||
}
|
||||
}
|
||||
|
||||
$placement->discussionId = $discussion->id;
|
||||
|
||||
return $placement;
|
||||
}
|
||||
|
||||
public function assign(Fish $fish)
|
||||
{
|
||||
$fish->discussion_id_placement = $this->discussionId;
|
||||
$fish->post_id_placement = $this->postId;
|
||||
$fish->user_id_placement = $this->userId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Repositories;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use XXB\CatchTheFish\Fish;
|
||||
use XXB\CatchTheFish\Ranking;
|
||||
use XXB\CatchTheFish\Validators\FishValidator;
|
||||
use Flarum\Foundation\ValidationException;
|
||||
use Flarum\Locale\Translator;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class PlacementRepository
|
||||
{
|
||||
protected $settings;
|
||||
protected $validator;
|
||||
protected $translator;
|
||||
|
||||
public function __construct(SettingsRepositoryInterface $settings, FishValidator $validator, Translator $translator)
|
||||
{
|
||||
$this->settings = $settings;
|
||||
$this->validator = $validator;
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
protected function assertFishIsAtPlacement(Fish $fish, array $placement): void
|
||||
{
|
||||
foreach ([
|
||||
'discussion_id',
|
||||
'post_id',
|
||||
'user_id',
|
||||
] as $key) {
|
||||
$value = $fish->{$key . '_placement'};
|
||||
if ($value !== null && $value === Arr::get($placement, $key)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ValidationException([
|
||||
'placement' => $this->translator->trans('xxb-catch-the-fish.api.wrong-catch-placement'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function catch(User $actor, Fish $fish, array $placement): Fish
|
||||
{
|
||||
$actor->assertCan('catch', $fish);
|
||||
|
||||
$this->assertFishIsAtPlacement($fish, $placement);
|
||||
|
||||
$fish->user_id_last_placement = null;
|
||||
$fish->last_caught_at = Carbon::now();
|
||||
$fish->lastUserCatch()->associate($actor);
|
||||
Placement::random()->assign($fish);
|
||||
|
||||
$placementValidSince = Carbon::now();
|
||||
|
||||
if ($actor->can('catchthefish.choose-place') || $actor->can('catchthefish.choose-name')) {
|
||||
$placementValidSince->addMinutes((int) $this->settings->get('catch-the-fish.autoPlacedAfterMinutes', 5));
|
||||
}
|
||||
|
||||
$fish->placement_valid_since = $placementValidSince;
|
||||
|
||||
$fish->save();
|
||||
|
||||
$ranking = Ranking::firstOrCreate(
|
||||
[
|
||||
'round_id' => $fish->round->id,
|
||||
'user_id' => $actor->id,
|
||||
],
|
||||
[
|
||||
'catch_count' => 0,
|
||||
]
|
||||
);
|
||||
|
||||
$ranking->increment('catch_count');
|
||||
|
||||
return $fish;
|
||||
}
|
||||
|
||||
public function place(User $actor, Fish $fish, array $attributes): Fish
|
||||
{
|
||||
$fishBeforeUpdate = clone $fish;
|
||||
|
||||
if (Arr::has($attributes, 'placement')) {
|
||||
$actor->assertCan('place', $fishBeforeUpdate);
|
||||
|
||||
if (Arr::get($attributes, 'placement') !== 'random') {
|
||||
$placement = new Placement();
|
||||
$placement->discussionId = Arr::get($attributes, 'placement.discussion_id');
|
||||
$placement->postId = Arr::get($attributes, 'placement.post_id');
|
||||
$placement->userId = Arr::get($attributes, 'placement.user_id');
|
||||
|
||||
$placement->assertValid();
|
||||
|
||||
$placement->assign($fish);
|
||||
$fish->lastUserPlacement()->associate($actor);
|
||||
}
|
||||
|
||||
$fish->placement_valid_since = Carbon::now();
|
||||
}
|
||||
|
||||
if (Arr::has($attributes, 'name')) {
|
||||
$actor->assertCan('name', $fishBeforeUpdate);
|
||||
|
||||
$this->validator->assertValid($attributes);
|
||||
|
||||
$fish->name = Arr::get($attributes, 'name');
|
||||
$fish->lastUserNaming()->associate($actor);
|
||||
|
||||
if (!$actor->can('place', $fishBeforeUpdate)) {
|
||||
$fish->placement_valid_since = Carbon::now();
|
||||
}
|
||||
}
|
||||
|
||||
if ($fish->isDirty()) {
|
||||
$fish->save();
|
||||
}
|
||||
|
||||
return $fish;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Repositories;
|
||||
|
||||
use XXB\CatchTheFish\Round;
|
||||
|
||||
class RankingRepository
|
||||
{
|
||||
public function all(Round $round)
|
||||
{
|
||||
return $round->rankings()
|
||||
->orderBy('catch_count', 'desc')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Repositories;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use XXB\CatchTheFish\Round;
|
||||
use XXB\CatchTheFish\Validators\RoundValidator;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class RoundRepository
|
||||
{
|
||||
protected $validator;
|
||||
protected $fishRepository;
|
||||
protected $uploader;
|
||||
|
||||
public function __construct(RoundValidator $validator, FishRepository $fishRepository, FishImageUploader $uploader)
|
||||
{
|
||||
$this->validator = $validator;
|
||||
$this->fishRepository = $fishRepository;
|
||||
$this->uploader = $uploader;
|
||||
}
|
||||
|
||||
public function all($limit = null, $offset = 0)
|
||||
{
|
||||
$query = Round::query();
|
||||
|
||||
if ($limit !== null) {
|
||||
$query->limit($limit)->offset($offset);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
public function allActive(): Collection
|
||||
{
|
||||
return Round::activeRound()->get();
|
||||
}
|
||||
|
||||
public function findOrFail($id): Round
|
||||
{
|
||||
return Round::query()->where('id', $id)->firstOrFail();
|
||||
}
|
||||
|
||||
protected function parseAttributes(array $attributes): array
|
||||
{
|
||||
$return = Arr::only($attributes, 'name');
|
||||
|
||||
if (Arr::has($attributes, 'starts_at')) {
|
||||
$return['starts_at'] = Carbon::parse($attributes['starts_at']);
|
||||
}
|
||||
|
||||
if (Arr::has($attributes, 'ends_at')) {
|
||||
$return['ends_at'] = Carbon::parse($attributes['ends_at']);
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
public function store(array $attributes): Round
|
||||
{
|
||||
$this->validator->assertValid($attributes);
|
||||
|
||||
$round = new Round($this->parseAttributes($attributes));
|
||||
$round->save();
|
||||
|
||||
if (Arr::get($attributes, 'include_starting_pack')) {
|
||||
$this->fishRepository->storeDefaultData($round);
|
||||
}
|
||||
|
||||
return $round;
|
||||
}
|
||||
|
||||
public function update(Round $round, array $attributes): Round
|
||||
{
|
||||
$this->validator->assertValid($attributes);
|
||||
|
||||
$round->fill($this->parseAttributes($attributes));
|
||||
$round->save();
|
||||
|
||||
return $round;
|
||||
}
|
||||
|
||||
public function delete(Round $round): void
|
||||
{
|
||||
foreach ($round->fishes as $fish) {
|
||||
$this->uploader->remove($fish);
|
||||
}
|
||||
|
||||
$round->delete();
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Database\AbstractModel;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Round extends AbstractModel
|
||||
{
|
||||
protected $table = 'catchthefish_rounds';
|
||||
|
||||
protected $casts = [
|
||||
'starts_at' => 'datetime',
|
||||
'ends_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'starts_at',
|
||||
'ends_at',
|
||||
];
|
||||
|
||||
public $timestamps = true;
|
||||
|
||||
public function fishes(): HasMany
|
||||
{
|
||||
return $this->hasMany(Fish::class);
|
||||
}
|
||||
|
||||
public function rankings(): HasMany
|
||||
{
|
||||
return $this->hasMany(Ranking::class);
|
||||
}
|
||||
|
||||
public function userRanking(User $user): ?Ranking
|
||||
{
|
||||
return $this->rankings()->where('user_id', $user->id)->first();
|
||||
}
|
||||
|
||||
public function scopeActiveRound(Builder $query): Builder
|
||||
{
|
||||
$now = Carbon::now();
|
||||
|
||||
return $query->where('ends_at', '>', $now)
|
||||
->where(function (Builder $query) use ($now) {
|
||||
$query->whereNull('starts_at')
|
||||
->orWhere('starts_at', '<', $now);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Serializers;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Tobscure\JsonApi\Relationship;
|
||||
use Tobscure\JsonApi\Resource;
|
||||
use Tobscure\JsonApi\SerializerInterface;
|
||||
|
||||
abstract class AbstractSerializer implements SerializerInterface
|
||||
{
|
||||
protected $type;
|
||||
protected $actor;
|
||||
protected $request;
|
||||
|
||||
public function setRequest(ServerRequestInterface $request): void
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->actor = RequestUtil::getActor($request);
|
||||
}
|
||||
|
||||
public function getType($model): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getId($model): ?string
|
||||
{
|
||||
return $model && isset($model->id) ? (string) $model->id : null;
|
||||
}
|
||||
|
||||
public function getAttributes($model, array $fields = null): array
|
||||
{
|
||||
return $this->getDefaultAttributes($model);
|
||||
}
|
||||
|
||||
public function getLinks($model): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getMeta($model): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getRelationship($model, $name): ?Relationship
|
||||
{
|
||||
if (!method_exists($this, $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->$name($model);
|
||||
}
|
||||
|
||||
protected function getDefaultAttributes($model): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function formatDate($date): ?string
|
||||
{
|
||||
return $date instanceof DateTimeInterface ? $date->format(DateTimeInterface::ATOM) : null;
|
||||
}
|
||||
|
||||
protected function resolveSerializer($serializer, ...$arguments)
|
||||
{
|
||||
return $this->resolveSerializerClass($serializer);
|
||||
}
|
||||
|
||||
protected function resolveSerializerClass(string $serializer)
|
||||
{
|
||||
$instance = resolve($serializer);
|
||||
|
||||
if ($this->request && method_exists($instance, 'setRequest')) {
|
||||
$instance->setRequest($this->request);
|
||||
}
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
protected function hasOne($model, string $serializer, ?string $relationship = null): ?Relationship
|
||||
{
|
||||
$relationship = $relationship ?: lcfirst(class_basename($serializer));
|
||||
$data = $model->{$relationship};
|
||||
|
||||
if (!$data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Relationship(new Resource($data, $this->resolveSerializerClass($serializer)));
|
||||
}
|
||||
|
||||
protected function buildRelationship($model, string $serializer, string $relationship): ?Relationship
|
||||
{
|
||||
return $this->hasOne($model, $serializer, $relationship);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Serializers;
|
||||
|
||||
class BasicDiscussionSerializer extends AbstractSerializer
|
||||
{
|
||||
protected $type = 'discussions';
|
||||
|
||||
protected function getDefaultAttributes($discussion): array
|
||||
{
|
||||
return [
|
||||
'title' => $discussion->title,
|
||||
'slug' => method_exists($discussion, 'slug') ? $discussion->slug() : (string) $discussion->id,
|
||||
'commentCount' => $discussion->comment_count,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Serializers;
|
||||
|
||||
class BasicUserSerializer extends UserSerializer
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Serializers;
|
||||
|
||||
use XXB\CatchTheFish\Fish;
|
||||
use Flarum\Foundation\ValidationException;
|
||||
use Tobscure\JsonApi\Relationship;
|
||||
use Tobscure\JsonApi\Resource;
|
||||
|
||||
class FishSerializer extends AbstractSerializer
|
||||
{
|
||||
protected $type = 'catchthefish-fishes';
|
||||
|
||||
protected function actorCan($ability, $arguments = [])
|
||||
{
|
||||
try {
|
||||
return $this->actor->can($ability, $arguments);
|
||||
} catch (ValidationException $exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getDefaultAttributes($fish): array
|
||||
{
|
||||
$canPlace = $this->actorCan('place', $fish);
|
||||
|
||||
return [
|
||||
'name' => $fish->name,
|
||||
'image_url' => $fish->image_url,
|
||||
'placement' => $this->actorCan('catch', $fish) || $this->actorCan('catchthefish.moderate') ? [
|
||||
'discussion_id' => $fish->discussion_id_placement,
|
||||
'post_id' => $fish->post_id_placement,
|
||||
'user_id' => $fish->user_id_placement,
|
||||
] : null,
|
||||
'canSee' => $this->actorCan('see', $fish),
|
||||
'canCatch' => $this->actorCan('catch', $fish),
|
||||
'canName' => $this->actorCan('name', $fish),
|
||||
'canPlace' => $canPlace,
|
||||
'placeUntil' => $canPlace ? $this->formatDate($fish->placement_valid_since) : null,
|
||||
];
|
||||
}
|
||||
|
||||
public function round($fish): ?Relationship
|
||||
{
|
||||
return $this->buildRelationship($fish, RoundSerializer::class, 'round');
|
||||
}
|
||||
|
||||
public function lastUserPlacement($fish): ?Relationship
|
||||
{
|
||||
return $this->buildRelationship($fish, UserSerializer::class, 'lastUserPlacement');
|
||||
}
|
||||
|
||||
public function lastUserNaming($fish): ?Relationship
|
||||
{
|
||||
return $this->buildRelationship($fish, UserSerializer::class, 'lastUserNaming');
|
||||
}
|
||||
|
||||
protected function placementRelationship($data, string $serializer): ?Relationship
|
||||
{
|
||||
if (!$data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$serializer = $this->resolveSerializerClass($serializer);
|
||||
|
||||
$element = new Resource($data, $serializer);
|
||||
|
||||
return new Relationship($element);
|
||||
}
|
||||
|
||||
public function placement(Fish $fish): ?Relationship
|
||||
{
|
||||
if ($fish->discussion_id_placement) {
|
||||
return $this->placementRelationship($fish->placementDiscussion()->whereVisibleTo($this->actor)->first(), BasicDiscussionSerializer::class);
|
||||
}
|
||||
|
||||
if ($fish->post_id_placement) {
|
||||
return $this->placementRelationship($fish->placementPost()->whereVisibleTo($this->actor)->first(), PostSerializer::class);
|
||||
}
|
||||
|
||||
if ($fish->user_id_placement) {
|
||||
return $this->placementRelationship($fish->placementUser()->whereVisibleTo($this->actor)->first(), BasicUserSerializer::class);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Serializers;
|
||||
|
||||
use Tobscure\JsonApi\Relationship;
|
||||
|
||||
class PostSerializer extends AbstractSerializer
|
||||
{
|
||||
protected $type = 'posts';
|
||||
|
||||
protected function getDefaultAttributes($post): array
|
||||
{
|
||||
return [
|
||||
'number' => $post->number,
|
||||
'createdAt' => $this->formatDate($post->created_at),
|
||||
];
|
||||
}
|
||||
|
||||
public function discussion($post): ?Relationship
|
||||
{
|
||||
return $this->hasOne($post, BasicDiscussionSerializer::class, 'discussion');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Serializers;
|
||||
|
||||
use XXB\CatchTheFish\Ranking;
|
||||
use Tobscure\JsonApi\Relationship;
|
||||
|
||||
class RankingSerializer extends AbstractSerializer
|
||||
{
|
||||
protected $type = 'catchthefish-rankings';
|
||||
|
||||
protected function getDefaultAttributes($ranking): array
|
||||
{
|
||||
return [
|
||||
'catch_count' => $ranking->catch_count,
|
||||
];
|
||||
}
|
||||
|
||||
public function user(Ranking $ranking): ?Relationship
|
||||
{
|
||||
return $this->hasOne($ranking, UserSerializer::class, 'user');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Serializers;
|
||||
|
||||
use XXB\CatchTheFish\Ranking;
|
||||
use XXB\CatchTheFish\Round;
|
||||
use Tobscure\JsonApi\Relationship;
|
||||
use Tobscure\JsonApi\Resource;
|
||||
|
||||
class RoundSerializer extends AbstractSerializer
|
||||
{
|
||||
protected $type = 'catchthefish-rounds';
|
||||
|
||||
protected function getDefaultAttributes($round): array
|
||||
{
|
||||
$ranking = $round->userRanking($this->actor);
|
||||
|
||||
return [
|
||||
'name' => $round->name,
|
||||
'starts_at' => $this->formatDate($round->starts_at),
|
||||
'ends_at' => $this->formatDate($round->ends_at),
|
||||
'my_catch_count' => $ranking ? $ranking->catch_count : 0,
|
||||
];
|
||||
}
|
||||
|
||||
public function myRanking(Round $round): ?Relationship
|
||||
{
|
||||
$data = $round->userRanking($this->actor);
|
||||
|
||||
if (!$data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Relationship(new Resource($data, $this->resolveSerializer(RankingSerializer::class, $round, $data)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Serializers;
|
||||
|
||||
class UserSerializer extends AbstractSerializer
|
||||
{
|
||||
protected $type = 'users';
|
||||
|
||||
protected function getDefaultAttributes($user): array
|
||||
{
|
||||
return [
|
||||
'username' => $user->username,
|
||||
'displayName' => $user->display_name ?: $user->username,
|
||||
'avatarUrl' => $user->avatar_url,
|
||||
'slug' => method_exists($user, 'slug') ? $user->slug() : (string) $user->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of JSON-API.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobscure\JsonApi;
|
||||
|
||||
use LogicException;
|
||||
|
||||
abstract class AbstractSerializer implements SerializerInterface
|
||||
{
|
||||
/**
|
||||
* The type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getType($model)
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getId($model)
|
||||
{
|
||||
return $model->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAttributes($model, array $fields = null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLinks($model)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMeta($model)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public function getRelationship($model, $name)
|
||||
{
|
||||
$method = $this->getRelationshipMethodName($name);
|
||||
|
||||
if (method_exists($this, $method)) {
|
||||
$relationship = $this->$method($model);
|
||||
|
||||
if ($relationship !== null && ! ($relationship instanceof Relationship)) {
|
||||
throw new LogicException('Relationship method must return null or an instance of Tobscure\JsonApi\Relationship');
|
||||
}
|
||||
|
||||
return $relationship;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the serializer method name for the given relationship.
|
||||
*
|
||||
* snake_case and kebab-case are converted into camelCase.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getRelationshipMethodName($name)
|
||||
{
|
||||
if (stripos($name, '-')) {
|
||||
$name = lcfirst(implode('', array_map('ucfirst', explode('-', $name))));
|
||||
}
|
||||
|
||||
if (stripos($name, '_')) {
|
||||
$name = lcfirst(implode('', array_map('ucfirst', explode('_', $name))));
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of JSON-API.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobscure\JsonApi;
|
||||
|
||||
class Collection implements ElementInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $resources = [];
|
||||
|
||||
/**
|
||||
* Create a new collection instance.
|
||||
*
|
||||
* @param mixed $data
|
||||
* @param \Tobscure\JsonApi\SerializerInterface $serializer
|
||||
*/
|
||||
public function __construct($data, SerializerInterface $serializer)
|
||||
{
|
||||
$this->resources = $this->buildResources($data, $serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an array of raw data to Resource objects.
|
||||
*
|
||||
* @param mixed $data
|
||||
* @param SerializerInterface $serializer
|
||||
*
|
||||
* @return \Tobscure\JsonApi\Resource[]
|
||||
*/
|
||||
protected function buildResources($data, SerializerInterface $serializer)
|
||||
{
|
||||
$resources = [];
|
||||
|
||||
foreach ($data as $resource) {
|
||||
if (! ($resource instanceof Resource)) {
|
||||
$resource = new Resource($resource, $serializer);
|
||||
}
|
||||
|
||||
$resources[] = $resource;
|
||||
}
|
||||
|
||||
return $resources;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getResources()
|
||||
{
|
||||
return $this->resources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the resources array.
|
||||
*
|
||||
* @param array $resources
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setResources($resources)
|
||||
{
|
||||
$this->resources = $resources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a relationship to be included for all resources.
|
||||
*
|
||||
* @param string|array $relationships
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function with($relationships)
|
||||
{
|
||||
foreach ($this->resources as $resource) {
|
||||
$resource->with($relationships);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a relationship to be identified for all resources.
|
||||
*
|
||||
* @param string|array $relationships
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function identify($relationships)
|
||||
{
|
||||
foreach ($this->resources as $resource) {
|
||||
$resource->identify($relationships);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a restricted set of fields.
|
||||
*
|
||||
* @param array|null $fields
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function fields($fields)
|
||||
{
|
||||
foreach ($this->resources as $resource) {
|
||||
$resource->fields($fields);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
return array_map(function (Resource $resource) {
|
||||
return $resource->toArray();
|
||||
}, $this->resources);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toIdentifier()
|
||||
{
|
||||
return array_map(function (Resource $resource) {
|
||||
return $resource->toIdentifier();
|
||||
}, $this->resources);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of JSON-API.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobscure\JsonApi;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
class Document implements JsonSerializable
|
||||
{
|
||||
use LinksTrait;
|
||||
use MetaTrait;
|
||||
|
||||
/**
|
||||
* The included array.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $included = [];
|
||||
|
||||
/**
|
||||
* The errors array.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $errors;
|
||||
|
||||
/**
|
||||
* The jsonapi array.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $jsonapi;
|
||||
|
||||
/**
|
||||
* The data object.
|
||||
*
|
||||
* @var ElementInterface
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* @param ElementInterface $data
|
||||
*/
|
||||
public function __construct(ElementInterface $data = null)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get included resources.
|
||||
*
|
||||
* @param \Tobscure\JsonApi\ElementInterface $element
|
||||
* @param bool $includeParent
|
||||
*
|
||||
* @return \Tobscure\JsonApi\Resource[]
|
||||
*/
|
||||
protected function getIncluded(ElementInterface $element, $includeParent = false)
|
||||
{
|
||||
$included = [];
|
||||
|
||||
foreach ($element->getResources() as $resource) {
|
||||
if ($resource->isIdentifier()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($includeParent) {
|
||||
$included = $this->mergeResource($included, $resource);
|
||||
} else {
|
||||
$type = $resource->getType();
|
||||
$id = $resource->getId();
|
||||
}
|
||||
|
||||
foreach ($resource->getUnfilteredRelationships() as $relationship) {
|
||||
$includedElement = $relationship->getData();
|
||||
|
||||
if (! $includedElement instanceof ElementInterface) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($this->getIncluded($includedElement, true) as $child) {
|
||||
// If this resource is the same as the top-level "data"
|
||||
// resource, then we don't want it to show up again in the
|
||||
// "included" array.
|
||||
if (! $includeParent && $child->getType() === $type && $child->getId() === $id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$included = $this->mergeResource($included, $child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$flattened = [];
|
||||
|
||||
array_walk_recursive($included, function ($a) use (&$flattened) {
|
||||
$flattened[] = $a;
|
||||
});
|
||||
|
||||
return $flattened;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Tobscure\JsonApi\Resource[] $resources
|
||||
* @param \Tobscure\JsonApi\Resource $newResource
|
||||
*
|
||||
* @return \Tobscure\JsonApi\Resource[]
|
||||
*/
|
||||
protected function mergeResource(array $resources, Resource $newResource)
|
||||
{
|
||||
$type = $newResource->getType();
|
||||
$id = $newResource->getId();
|
||||
|
||||
if (isset($resources[$type][$id])) {
|
||||
$resources[$type][$id]->merge($newResource);
|
||||
} else {
|
||||
$resources[$type][$id] = $newResource;
|
||||
}
|
||||
|
||||
return $resources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the data object.
|
||||
*
|
||||
* @param \Tobscure\JsonApi\ElementInterface $element
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setData(ElementInterface $element)
|
||||
{
|
||||
$this->data = $element;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the errors array.
|
||||
*
|
||||
* @param array $errors
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setErrors($errors)
|
||||
{
|
||||
$this->errors = $errors;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the jsonapi array.
|
||||
*
|
||||
* @param array $jsonapi
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setJsonapi($jsonapi)
|
||||
{
|
||||
$this->jsonapi = $jsonapi;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map everything to arrays.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
$document = [];
|
||||
|
||||
if (! empty($this->links)) {
|
||||
$document['links'] = $this->links;
|
||||
}
|
||||
|
||||
if (! empty($this->data)) {
|
||||
$document['data'] = $this->data->toArray();
|
||||
|
||||
$resources = $this->getIncluded($this->data);
|
||||
|
||||
if (count($resources)) {
|
||||
$document['included'] = array_map(function (Resource $resource) {
|
||||
return $resource->toArray();
|
||||
}, $resources);
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($this->meta)) {
|
||||
$document['meta'] = $this->meta;
|
||||
}
|
||||
|
||||
if (! empty($this->errors)) {
|
||||
$document['errors'] = $this->errors;
|
||||
}
|
||||
|
||||
if (! empty($this->jsonapi)) {
|
||||
$document['jsonapi'] = $this->jsonapi;
|
||||
}
|
||||
|
||||
return $document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map to string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return json_encode($this->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize for JSON usage.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of JSON-API.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobscure\JsonApi;
|
||||
|
||||
interface ElementInterface
|
||||
{
|
||||
/**
|
||||
* Get the resources array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getResources();
|
||||
|
||||
/**
|
||||
* Map to a "resource object" array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray();
|
||||
|
||||
/**
|
||||
* Map to a "resource object identifier" array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toIdentifier();
|
||||
|
||||
/**
|
||||
* Request a relationship to be included.
|
||||
*
|
||||
* @param string|array $relationships
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function with($relationships);
|
||||
|
||||
/**
|
||||
* Request a restricted set of fields.
|
||||
*
|
||||
* @param array|null $fields
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function fields($fields);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of JSON-API.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobscure\JsonApi;
|
||||
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface;
|
||||
|
||||
class ErrorHandler
|
||||
{
|
||||
/**
|
||||
* Stores the valid handlers.
|
||||
*
|
||||
* @var \Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface[]
|
||||
*/
|
||||
private $handlers = [];
|
||||
|
||||
/**
|
||||
* Handle the exception provided.
|
||||
*
|
||||
* @param Exception $e
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*
|
||||
* @return \Tobscure\JsonApi\Exception\Handler\ResponseBag
|
||||
*/
|
||||
public function handle(Exception $e)
|
||||
{
|
||||
foreach ($this->handlers as $handler) {
|
||||
if ($handler->manages($e)) {
|
||||
return $handler->handle($e);
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException('Exception handler for '.get_class($e).' not found.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new exception handler.
|
||||
*
|
||||
* @param \Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface $handler
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function registerHandler(ExceptionHandlerInterface $handler)
|
||||
{
|
||||
$this->handlers[] = $handler;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of JSON-API.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobscure\JsonApi\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
class InvalidParameterException extends Exception
|
||||
{
|
||||
/**
|
||||
* @var string The parameter that caused this exception.
|
||||
*/
|
||||
private $invalidParameter;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param string $invalidParameter The parameter that caused this exception.
|
||||
*/
|
||||
public function __construct($message = '', $code = 0, $previous = null, $invalidParameter = '')
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
|
||||
$this->invalidParameter = $invalidParameter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string The parameter that caused this exception.
|
||||
*/
|
||||
public function getInvalidParameter()
|
||||
{
|
||||
return $this->invalidParameter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of JSON-API.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobscure\JsonApi;
|
||||
|
||||
trait LinksTrait
|
||||
{
|
||||
/**
|
||||
* The links array.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $links;
|
||||
|
||||
/**
|
||||
* Get the links.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLinks()
|
||||
{
|
||||
return $this->links;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the links.
|
||||
*
|
||||
* @param array $links
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setLinks(array $links)
|
||||
{
|
||||
$this->links = $links;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a link.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addLink($key, $value)
|
||||
{
|
||||
$this->links[$key] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add pagination links (first, prev, next, and last).
|
||||
*
|
||||
* @param string $url The base URL for pagination links.
|
||||
* @param array $queryParams The query params provided in the request.
|
||||
* @param int $offset The current offset.
|
||||
* @param int $limit The current limit.
|
||||
* @param int|null $total The total number of results, or null if unknown.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addPaginationLinks($url, array $queryParams, $offset, $limit, $total = null)
|
||||
{
|
||||
if (isset($queryParams['page']['number'])) {
|
||||
$offset = floor($offset / $limit) * $limit;
|
||||
}
|
||||
|
||||
$this->addPaginationLink('first', $url, $queryParams, 0, $limit);
|
||||
|
||||
if ($offset > 0) {
|
||||
$this->addPaginationLink('prev', $url, $queryParams, max(0, $offset - $limit), $limit);
|
||||
}
|
||||
|
||||
if ($total === null || $offset + $limit < $total) {
|
||||
$this->addPaginationLink('next', $url, $queryParams, $offset + $limit, $limit);
|
||||
}
|
||||
|
||||
if ($total) {
|
||||
$this->addPaginationLink('last', $url, $queryParams, floor(($total - 1) / $limit) * $limit, $limit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a pagination link.
|
||||
*
|
||||
* @param string $name The name of the link.
|
||||
* @param string $url The base URL for pagination links.
|
||||
* @param array $queryParams The query params provided in the request.
|
||||
* @param int $offset The offset to link to.
|
||||
* @param int $limit The current limit.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addPaginationLink($name, $url, array $queryParams, $offset, $limit)
|
||||
{
|
||||
if (! isset($queryParams['page']) || ! is_array($queryParams['page'])) {
|
||||
$queryParams['page'] = [];
|
||||
}
|
||||
|
||||
$page = &$queryParams['page'];
|
||||
|
||||
if (isset($page['number'])) {
|
||||
$page['number'] = floor($offset / $limit) + 1;
|
||||
|
||||
if ($page['number'] <= 1) {
|
||||
unset($page['number']);
|
||||
}
|
||||
} else {
|
||||
$page['offset'] = $offset;
|
||||
|
||||
if ($page['offset'] <= 0) {
|
||||
unset($page['offset']);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($page['limit'])) {
|
||||
$page['limit'] = $limit;
|
||||
}
|
||||
|
||||
$queryString = http_build_query($queryParams);
|
||||
|
||||
$this->addLink($name, $url.($queryString ? '?'.$queryString : ''));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of JSON-API.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobscure\JsonApi;
|
||||
|
||||
trait MetaTrait
|
||||
{
|
||||
/**
|
||||
* The meta data array.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $meta;
|
||||
|
||||
/**
|
||||
* Get the meta.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getMeta()
|
||||
{
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the meta data array.
|
||||
*
|
||||
* @param array $meta
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setMeta(array $meta)
|
||||
{
|
||||
$this->meta = $meta;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta data.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addMeta($key, $value)
|
||||
{
|
||||
$this->meta[$key] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of JSON-API.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobscure\JsonApi;
|
||||
|
||||
use Tobscure\JsonApi\Exception\InvalidParameterException;
|
||||
|
||||
class Parameters
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $input;
|
||||
|
||||
/**
|
||||
* @param array $input
|
||||
*/
|
||||
public function __construct(array $input)
|
||||
{
|
||||
$this->input = $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the includes.
|
||||
*
|
||||
* @param array $available
|
||||
*
|
||||
* @throws \Tobscure\JsonApi\Exception\InvalidParameterException
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getInclude(array $available = [])
|
||||
{
|
||||
if ($include = $this->getInput('include')) {
|
||||
$relationships = explode(',', $include);
|
||||
|
||||
$invalid = array_diff($relationships, $available);
|
||||
|
||||
if (count($invalid)) {
|
||||
throw new InvalidParameterException(
|
||||
'Invalid includes ['.implode(',', $invalid).']',
|
||||
1,
|
||||
null,
|
||||
'include'
|
||||
);
|
||||
}
|
||||
|
||||
return $relationships;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get number of offset.
|
||||
*
|
||||
* @param int|null $perPage
|
||||
*
|
||||
* @throws \Tobscure\JsonApi\Exception\InvalidParameterException
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getOffset($perPage = null)
|
||||
{
|
||||
if ($perPage && ($offset = $this->getOffsetFromNumber($perPage))) {
|
||||
return $offset;
|
||||
}
|
||||
|
||||
$offset = (int) $this->getPage('offset');
|
||||
|
||||
if ($offset < 0) {
|
||||
throw new InvalidParameterException('page[offset] must be >=0', 2, null, 'page[offset]');
|
||||
}
|
||||
|
||||
return $offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the offset based on the page[number] parameter.
|
||||
*
|
||||
* @param int $perPage
|
||||
*
|
||||
* @throws \Tobscure\JsonApi\Exception\InvalidParameterException
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getOffsetFromNumber($perPage)
|
||||
{
|
||||
$page = (int) $this->getPage('number');
|
||||
|
||||
if ($page <= 1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ($page - 1) * $perPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the limit.
|
||||
*
|
||||
* @param int|null $max
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getLimit($max = null)
|
||||
{
|
||||
$limit = $this->getPage('limit') ?: $this->getPage('size') ?: null;
|
||||
|
||||
if ($limit && $max) {
|
||||
$limit = min($max, $limit);
|
||||
}
|
||||
|
||||
return $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sort.
|
||||
*
|
||||
* @param array $available
|
||||
*
|
||||
* @throws \Tobscure\JsonApi\Exception\InvalidParameterException
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSort(array $available = [])
|
||||
{
|
||||
$sort = [];
|
||||
|
||||
if ($input = $this->getInput('sort')) {
|
||||
$fields = explode(',', $input);
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if (substr($field, 0, 1) === '-') {
|
||||
$field = substr($field, 1);
|
||||
$order = 'desc';
|
||||
} else {
|
||||
$order = 'asc';
|
||||
}
|
||||
|
||||
$sort[$field] = $order;
|
||||
}
|
||||
|
||||
$invalid = array_diff(array_keys($sort), $available);
|
||||
|
||||
if (count($invalid)) {
|
||||
throw new InvalidParameterException(
|
||||
'Invalid sort fields ['.implode(',', $invalid).']',
|
||||
3,
|
||||
null,
|
||||
'sort'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $sort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fields requested for inclusion.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFields()
|
||||
{
|
||||
$fields = $this->getInput('fields');
|
||||
|
||||
if (! is_array($fields)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(function ($fields) {
|
||||
return explode(',', $fields);
|
||||
}, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a filter item.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFilter()
|
||||
{
|
||||
return $this->getInput('filter');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an input item.
|
||||
*
|
||||
* @param string $key
|
||||
* @param null $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getInput($key, $default = null)
|
||||
{
|
||||
return isset($this->input[$key]) ? $this->input[$key] : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the page.
|
||||
*
|
||||
* @param string $key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getPage($key)
|
||||
{
|
||||
$page = $this->getInput('page');
|
||||
|
||||
return isset($page[$key]) ? $page[$key] : '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of JSON-API.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobscure\JsonApi;
|
||||
|
||||
class Relationship
|
||||
{
|
||||
use LinksTrait;
|
||||
use MetaTrait;
|
||||
|
||||
/**
|
||||
* The data object.
|
||||
*
|
||||
* @var \Tobscure\JsonApi\ElementInterface|null
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* Create a new relationship.
|
||||
*
|
||||
* @param \Tobscure\JsonApi\ElementInterface|null $data
|
||||
*/
|
||||
public function __construct(ElementInterface $data = null)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data object.
|
||||
*
|
||||
* @return \Tobscure\JsonApi\ElementInterface|null
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the data object.
|
||||
*
|
||||
* @param \Tobscure\JsonApi\ElementInterface|null $data
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setData($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map everything to an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
$array = [];
|
||||
|
||||
if (! empty($this->data)) {
|
||||
$array['data'] = $this->data->toIdentifier();
|
||||
}
|
||||
|
||||
if (! empty($this->meta)) {
|
||||
$array['meta'] = $this->meta;
|
||||
}
|
||||
|
||||
if (! empty($this->links)) {
|
||||
$array['links'] = $this->links;
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of JSON-API.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobscure\JsonApi;
|
||||
|
||||
class Resource implements ElementInterface
|
||||
{
|
||||
use LinksTrait;
|
||||
use MetaTrait;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* @var \Tobscure\JsonApi\SerializerInterface
|
||||
*/
|
||||
protected $serializer;
|
||||
|
||||
/**
|
||||
* A list of relationships to include.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $includes = [];
|
||||
|
||||
/**
|
||||
* A list of fields to restrict to.
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
protected $fields;
|
||||
|
||||
/**
|
||||
* An array of Resources that should be merged into this one.
|
||||
*
|
||||
* @var \Tobscure\JsonApi\Resource[]
|
||||
*/
|
||||
protected $merged = [];
|
||||
|
||||
/**
|
||||
* @var \Tobscure\JsonApi\Relationship[]
|
||||
*/
|
||||
private $relationships;
|
||||
|
||||
/**
|
||||
* @param mixed $data
|
||||
* @param \Tobscure\JsonApi\SerializerInterface $serializer
|
||||
*/
|
||||
public function __construct($data, SerializerInterface $serializer)
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->serializer = $serializer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getResources()
|
||||
{
|
||||
return [$this];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
$array = $this->toIdentifier();
|
||||
|
||||
if (! $this->isIdentifier()) {
|
||||
$attributes = $this->getAttributes();
|
||||
if ($attributes) {
|
||||
$array['attributes'] = $attributes;
|
||||
}
|
||||
}
|
||||
|
||||
$relationships = $this->getRelationshipsAsArray();
|
||||
|
||||
if (count($relationships)) {
|
||||
$array['relationships'] = $relationships;
|
||||
}
|
||||
|
||||
$links = [];
|
||||
if (! empty($this->links)) {
|
||||
$links = $this->links;
|
||||
}
|
||||
$serializerLinks = $this->serializer->getLinks($this->data);
|
||||
if (! empty($serializerLinks)) {
|
||||
$links = array_merge($serializerLinks, $links);
|
||||
}
|
||||
if (! empty($links)) {
|
||||
$array['links'] = $links;
|
||||
}
|
||||
|
||||
$meta = [];
|
||||
if (! empty($this->meta)) {
|
||||
$meta = $this->meta;
|
||||
}
|
||||
$serializerMeta = $this->serializer->getMeta($this->data);
|
||||
if (! empty($serializerMeta)) {
|
||||
$meta = array_merge($serializerMeta, $meta);
|
||||
}
|
||||
if (! empty($meta)) {
|
||||
$array['meta'] = $meta;
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether or not this resource is an identifier (i.e. does it have
|
||||
* any data attached?).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isIdentifier()
|
||||
{
|
||||
return ! is_object($this->data) && ! is_array($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toIdentifier()
|
||||
{
|
||||
if (! $this->data) {
|
||||
return;
|
||||
}
|
||||
|
||||
$array = [
|
||||
'type' => $this->getType(),
|
||||
'id' => $this->getId()
|
||||
];
|
||||
|
||||
if (! empty($this->meta)) {
|
||||
$array['meta'] = $this->meta;
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->serializer->getType($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource ID.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
if (! is_object($this->data) && ! is_array($this->data)) {
|
||||
return (string) $this->data;
|
||||
}
|
||||
|
||||
return (string) $this->serializer->getId($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource attributes.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAttributes()
|
||||
{
|
||||
$attributes = (array) $this->serializer->getAttributes($this->data, $this->getOwnFields());
|
||||
|
||||
$attributes = $this->filterFields($attributes);
|
||||
|
||||
$attributes = $this->mergeAttributes($attributes);
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the requested fields for this resource type.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
protected function getOwnFields()
|
||||
{
|
||||
$type = $this->getType();
|
||||
|
||||
if (isset($this->fields[$type])) {
|
||||
return $this->fields[$type];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the given fields array (attributes or relationships) according
|
||||
* to the requested fieldset.
|
||||
*
|
||||
* @param array $fields
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function filterFields(array $fields)
|
||||
{
|
||||
if ($requested = $this->getOwnFields()) {
|
||||
$fields = array_intersect_key($fields, array_flip($requested));
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the attributes of merged resources into an array of attributes.
|
||||
*
|
||||
* @param array $attributes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function mergeAttributes(array $attributes)
|
||||
{
|
||||
foreach ($this->merged as $resource) {
|
||||
$attributes = array_replace_recursive($attributes, $resource->getAttributes());
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource relationships.
|
||||
*
|
||||
* @return \Tobscure\JsonApi\Relationship[]
|
||||
*/
|
||||
public function getRelationships()
|
||||
{
|
||||
$relationships = $this->buildRelationships();
|
||||
|
||||
return $this->filterFields($relationships);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource relationships without considering requested ones.
|
||||
*
|
||||
* @return \Tobscure\JsonApi\Relationship[]
|
||||
*/
|
||||
public function getUnfilteredRelationships()
|
||||
{
|
||||
return $this->buildRelationships();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource relationships as an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getRelationshipsAsArray()
|
||||
{
|
||||
$relationships = $this->getRelationships();
|
||||
|
||||
$relationships = $this->convertRelationshipsToArray($relationships);
|
||||
|
||||
return $this->mergeRelationships($relationships);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of built relationships.
|
||||
*
|
||||
* @return \Tobscure\JsonApi\Relationship[]
|
||||
*/
|
||||
protected function buildRelationships()
|
||||
{
|
||||
if (isset($this->relationships)) {
|
||||
return $this->relationships;
|
||||
}
|
||||
|
||||
$paths = Util::parseRelationshipPaths($this->includes);
|
||||
|
||||
$relationships = [];
|
||||
|
||||
foreach ($paths as $name => $nested) {
|
||||
$relationship = $this->serializer->getRelationship($this->data, $name);
|
||||
|
||||
if ($relationship) {
|
||||
$relationshipData = $relationship->getData();
|
||||
if ($relationshipData instanceof ElementInterface) {
|
||||
$relationshipData->with($nested)->fields($this->fields);
|
||||
}
|
||||
|
||||
$relationships[$name] = $relationship;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->relationships = $relationships;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the relationships of merged resources into an array of
|
||||
* relationships.
|
||||
*
|
||||
* @param array $relationships
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function mergeRelationships(array $relationships)
|
||||
{
|
||||
foreach ($this->merged as $resource) {
|
||||
$relationships = array_replace_recursive($relationships, $resource->getRelationshipsAsArray());
|
||||
}
|
||||
|
||||
return $relationships;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given array of Relationship objects into an array.
|
||||
*
|
||||
* @param \Tobscure\JsonApi\Relationship[] $relationships
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function convertRelationshipsToArray(array $relationships)
|
||||
{
|
||||
return array_map(function (Relationship $relationship) {
|
||||
return $relationship->toArray();
|
||||
}, $relationships);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a resource into this one.
|
||||
*
|
||||
* @param \Tobscure\JsonApi\Resource $resource
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function merge(Resource $resource)
|
||||
{
|
||||
$this->merged[] = $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function with($relationships)
|
||||
{
|
||||
$this->includes = array_unique(array_merge($this->includes, (array) $relationships));
|
||||
|
||||
$this->relationships = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function fields($fields)
|
||||
{
|
||||
$this->fields = $fields;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getData()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $data
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setData($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Tobscure\JsonApi\SerializerInterface
|
||||
*/
|
||||
public function getSerializer()
|
||||
{
|
||||
return $this->serializer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Tobscure\JsonApi\SerializerInterface $serializer
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setSerializer(SerializerInterface $serializer)
|
||||
{
|
||||
$this->serializer = $serializer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of JSON-API.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobscure\JsonApi;
|
||||
|
||||
interface SerializerInterface
|
||||
{
|
||||
/**
|
||||
* Get the type.
|
||||
*
|
||||
* @param mixed $model
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getType($model);
|
||||
|
||||
/**
|
||||
* Get the id.
|
||||
*
|
||||
* @param mixed $model
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId($model);
|
||||
|
||||
/**
|
||||
* Get the attributes array.
|
||||
*
|
||||
* @param mixed $model
|
||||
* @param array|null $fields
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAttributes($model, array $fields = null);
|
||||
|
||||
/**
|
||||
* Get the links array.
|
||||
*
|
||||
* @param mixed $model
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLinks($model);
|
||||
|
||||
/**
|
||||
* Get the meta.
|
||||
*
|
||||
* @param mixed $model
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getMeta($model);
|
||||
|
||||
/**
|
||||
* Get a relationship.
|
||||
*
|
||||
* @param mixed $model
|
||||
* @param string $name
|
||||
*
|
||||
* @return \Tobscure\JsonApi\Relationship|null
|
||||
*/
|
||||
public function getRelationship($model, $name);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of JSON-API.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Tobscure\JsonApi;
|
||||
|
||||
class Util
|
||||
{
|
||||
/**
|
||||
* Parse relationship paths.
|
||||
*
|
||||
* Given a flat array of relationship paths like:
|
||||
*
|
||||
* ['user', 'user.employer', 'user.employer.country', 'comments']
|
||||
*
|
||||
* create a nested array of relationship paths one-level deep that can
|
||||
* be passed on to other serializers:
|
||||
*
|
||||
* ['user' => ['employer', 'employer.country'], 'comments' => []]
|
||||
*
|
||||
* @param array $paths
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function parseRelationshipPaths(array $paths)
|
||||
{
|
||||
$tree = [];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
list($primary, $nested) = array_pad(explode('.', $path, 2), 2, null);
|
||||
|
||||
if (! isset($tree[$primary])) {
|
||||
$tree[$primary] = [];
|
||||
}
|
||||
|
||||
if ($nested) {
|
||||
$tree[$primary][] = $nested;
|
||||
}
|
||||
}
|
||||
|
||||
return $tree;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Validators;
|
||||
|
||||
use Flarum\Foundation\AbstractValidator;
|
||||
|
||||
class FishImageValidator extends AbstractValidator
|
||||
{
|
||||
protected array $rules = [
|
||||
'image' => 'required|mimes:jpeg,png,bmp,gif|max:2048',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Validators;
|
||||
|
||||
use Flarum\Foundation\AbstractValidator;
|
||||
|
||||
class FishValidator extends AbstractValidator
|
||||
{
|
||||
protected array $rules = [
|
||||
'name' => 'required|string|min:3',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace XXB\CatchTheFish\Validators;
|
||||
|
||||
use Flarum\Foundation\AbstractValidator;
|
||||
|
||||
class RoundValidator extends AbstractValidator
|
||||
{
|
||||
protected array $rules = [
|
||||
'name' => 'required|string',
|
||||
'starts_at' => 'nullable|date',
|
||||
'ends_at' => 'required|date',
|
||||
];
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户