Skip to content

Commit

Permalink
Merge pull request #360 from karlomikus/develop
Browse files Browse the repository at this point in the history
Minor release, bar shelf
  • Loading branch information
karlomikus authored Nov 15, 2024
2 parents f76d40f + b9298f5 commit 10c09cb
Show file tree
Hide file tree
Showing 53 changed files with 1,653 additions and 283 deletions.
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
# v4.1.0
## New
- Added bar shelf
- Each bar now has its own shelf which can be viewed by every bar member
- Currently members with Admin and Moderator roles can manage bar shelf
- Bar shelf will be initially populated with all ingredients that current bar owner has in his shelf
- Added GET `/bars/{id}/ingredients` endpoint
- Added POST `/bars/{id}/ingredients/batch-store` endpoint
- Added POST `/bars/{id}/ingredients/batch-delete` endpoint
- Added filtering by `bar_shelf` attribute to `/ingredients` and `/cocktails` endpoints
- Added sorting by `missing_bar_ingredients` attribute to `/cocktails` endpoint
- Added `total_bar_shelf_cocktails` to `/stats` endpoint
- Added bar shelf status attributes to `Ingredient` and `Cocktail` response schemas
- You can now add images to Bar resource
- Allows to upload custom bar logo, for example

## Fixes
- Fallback to "EUR" for unknown currencies in menu (fixes migration issues from v3)

## Changes
- Added validation to endpoints that manage user and bar shelf

# v4.0.4
## Fixes
- Check for shelf ingredients before adding them to the shelf
- Fix for missing directories in docker entrypoint
- Fix export id generation

# v4.0.3
## Changes
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ Contributions Welcome!

For more details, see [CONTRIBUTING.md](/CONTRIBUTING.md).

## Support and Donations

Bar Asistant is free, but maintaining any open source project takes time and resources. If you find Bar Assistant valuable and want to support its future development, consider donating.

[Donate with PayPal](https://www.paypal.com/ncp/payment/9L8T4YJZBRXAS)

## License

The Bar Assistant API is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
8 changes: 4 additions & 4 deletions app/External/Import/FromJsonSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,17 @@ public function process(
// Add images
$cocktailImages = [];
foreach ($cocktailExternal->cocktail->images as $image) {
if ($image->uri && $imageContents = file_get_contents($imageDirectoryBasePath . $image->getLocalFilePath())) {
try {
try {
if ($image->uri && $imageContents = file_get_contents($imageDirectoryBasePath . $image->getLocalFilePath())) {
$imageDTO = new ImageRequest(
image: $imageContents,
copyright: $image->copyright
);

$cocktailImages[] = $this->imageService->uploadAndSaveImages([$imageDTO], 1)[0]->id;
} catch (Throwable $e) {
Log::error('Importing image error: ' . $e->getMessage());
}
} catch (Throwable $e) {
Log::error('Importing image error: ' . $e->getMessage());
}
}

Expand Down
31 changes: 29 additions & 2 deletions app/Http/Controllers/BarController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Kami\Cocktail\Models\Bar;
use Kami\Cocktail\Models\User;
use OpenApi\Attributes as OAT;
use Kami\Cocktail\Models\Image;
use Symfony\Component\Uid\Ulid;
use Kami\Cocktail\Jobs\SetupBar;
use Illuminate\Http\JsonResponse;
Expand All @@ -35,7 +36,7 @@ public function index(Request $request): JsonResource
$bars = Bar::select('bars.*')
->join('bar_memberships', 'bar_memberships.bar_id', '=', 'bars.id')
->where('bar_memberships.user_id', $request->user()->id)
->with('createdUser', 'memberships')
->with('createdUser', 'memberships', 'images')
->get();

return BarResource::collection($bars);
Expand All @@ -62,7 +63,7 @@ public function show(Request $request, int $id): JsonResource
$bar->save();
}

$bar->load('createdUser', 'updatedUser');
$bar->load('createdUser', 'updatedUser', 'images');

return new BarResource($bar);
}
Expand Down Expand Up @@ -117,6 +118,19 @@ public function store(BarRequest $request): JsonResponse

$bar->save();

/** @var array<int> */
$images = $request->input('images', []);
if (count($images) > 0) {
try {
$imageModels = Image::findOrFail($images);
$bar->attachImages($imageModels);
} catch (\Throwable $e) {
abort(500, $e->getMessage());
}
}

$bar->load('createdUser', 'updatedUser', 'images');

Bar::updateSearchToken($bar);

$request->user()->joinBarAs($bar, UserRoleEnum::Admin);
Expand Down Expand Up @@ -180,6 +194,19 @@ public function update(int $id, BarRequest $request): JsonResource
$bar->updated_at = now();
$bar->save();

/** @var array<int> */
$images = $request->input('images', []);
if (count($images) > 0) {
try {
$imageModels = Image::findOrFail($images);
$bar->attachImages($imageModels);
} catch (\Throwable $e) {
abort(500, $e->getMessage());
}
}

$bar->load('createdUser', 'updatedUser', 'images');

return new BarResource($bar);
}

Expand Down
3 changes: 2 additions & 1 deletion app/Http/Controllers/CocktailController.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class CocktailController extends Controller
new OAT\Property(property: 'collection_id', type: 'string'),
new OAT\Property(property: 'favorites', type: 'boolean'),
new OAT\Property(property: 'on_shelf', type: 'boolean'),
new OAT\Property(property: 'bar_shelf', type: 'boolean'),
new OAT\Property(property: 'user_shelves', type: 'string'),
new OAT\Property(property: 'shelf_ingredients', type: 'string'),
new OAT\Property(property: 'is_public', type: 'boolean'),
Expand All @@ -61,7 +62,7 @@ class CocktailController extends Controller
new OAT\Property(property: 'specific_ingredients', type: 'string'),
new OAT\Property(property: 'ignore_ingredients', type: 'string'),
])),
new OAT\Parameter(name: 'sort', in: 'query', description: 'Sort by attributes. Available attributes: `name`, `created_at`, `average_rating`, `user_rating`, `abv`, `total_ingredients`, `missing_ingredients`, `favorited_at`.', schema: new OAT\Schema(type: 'string')),
new OAT\Parameter(name: 'sort', in: 'query', description: 'Sort by attributes. Available attributes: `name`, `created_at`, `average_rating`, `user_rating`, `abv`, `total_ingredients`, `missing_ingredients`, `missing_bar_ingredients`, `favorited_at`.', schema: new OAT\Schema(type: 'string')),
new OAT\Parameter(name: 'include', in: 'query', description: 'Include additional relationships. Available relations: `glass`, `method`, `user`, `navigation`, `utensils`, `createdUser`, `updatedUser`, `images`, `tags`, `ingredients.ingredient`, `ratings`.', schema: new OAT\Schema(type: 'string')),
])]
#[OAT\Response(response: 200, description: 'Successful response', content: [
Expand Down
8 changes: 4 additions & 4 deletions app/Http/Controllers/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class Controller extends BaseController
use DispatchesJobs;
use ValidatesRequests;

public function getJsonEncodingOptions(): int
{
return JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_PRESERVE_ZERO_FRACTION;
}
// public function getJsonEncodingOptions(): int
// {
// return JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_PRESERVE_ZERO_FRACTION;
// }
}
1 change: 1 addition & 0 deletions app/Http/Controllers/IngredientController.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class IngredientController extends Controller
new OAT\Property(property: 'created_user_id', type: 'integer'),
new OAT\Property(property: 'on_shopping_list', type: 'boolean'),
new OAT\Property(property: 'on_shelf', type: 'boolean'),
new OAT\Property(property: 'bar_shelf', type: 'boolean'),
new OAT\Property(property: 'strength_min', type: 'float'),
new OAT\Property(property: 'strength_max', type: 'float'),
new OAT\Property(property: 'main_ingredients', type: 'string'),
Expand Down
103 changes: 101 additions & 2 deletions app/Http/Controllers/ShelfController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@
use Throwable;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Kami\Cocktail\Models\Bar;
use Kami\Cocktail\Models\User;
use OpenApi\Attributes as OAT;
use Kami\Cocktail\OpenAPI as BAO;
use Illuminate\Support\Facades\DB;
use Kami\Cocktail\Models\Cocktail;
use Kami\Cocktail\Models\Ingredient;
use Kami\Cocktail\Models\BarIngredient;
use Kami\Cocktail\Models\UserIngredient;
use Kami\Cocktail\Models\CocktailFavorite;
use Kami\Cocktail\Models\UserShoppingList;
use Illuminate\Http\Resources\Json\JsonResource;
use Kami\Cocktail\Repository\CocktailRepository;
use Kami\Cocktail\Repository\IngredientRepository;
use Kami\Cocktail\Http\Resources\CocktailBasicResource;
use Kami\Cocktail\Http\Requests\ShelfIngredientsRequest;
use Kami\Cocktail\Http\Resources\IngredientBasicResource;

class ShelfController extends Controller
Expand Down Expand Up @@ -126,7 +129,7 @@ public function favorites(Request $request, int $id): JsonResource
#[OAT\Response(response: 204, description: 'Successful response')]
#[BAO\NotAuthorizedResponse]
#[BAO\NotFoundResponse]
public function batchStore(Request $request, int $id): Response
public function batchStore(ShelfIngredientsRequest $request, int $id): Response
{
$user = User::findOrFail($id);
if ($request->user()->id !== $user->id && $request->user()->cannot('show', $user)) {
Expand Down Expand Up @@ -175,7 +178,7 @@ public function batchStore(Request $request, int $id): Response
#[OAT\Response(response: 204, description: 'Successful response')]
#[BAO\NotAuthorizedResponse]
#[BAO\NotFoundResponse]
public function batchDelete(Request $request, int $id): Response
public function batchDelete(ShelfIngredientsRequest $request, int $id): Response
{
$user = User::findOrFail($id);
if ($request->user()->id !== $user->id && $request->user()->cannot('show', $user)) {
Expand Down Expand Up @@ -226,4 +229,100 @@ public function recommend(Request $request, IngredientRepository $ingredientRepo

return response()->json(['data' => $possibleIngredients]);
}

#[OAT\Get(path: '/bars/{id}/ingredients', tags: ['Bar Shelf'], summary: 'Show a list of bar shelf ingredients', description: 'Ingredients that bar has in it\'s shelf', parameters: [
new BAO\Parameters\DatabaseIdParameter(),
new BAO\Parameters\PageParameter(),
new BAO\Parameters\PerPageParameter(),
])]
#[OAT\Response(response: 200, description: 'Successful response', content: [
new BAO\PaginateData(BAO\Schemas\IngredientBasic::class),
])]
public function barIngredients(Request $request, int $id): JsonResource
{
$bar = Bar::findOrFail($id);
if ($request->user()->cannot('show', $bar)) {
abort(403);
}

$ingredientIds = $bar->shelfIngredients->pluck('ingredient_id');

/** @var \Illuminate\Pagination\LengthAwarePaginator<Ingredient> */
$ingredients = Ingredient::whereIn('id', $ingredientIds)->orderBy('name')->paginate($request->get('per_page', 100));

return IngredientBasicResource::collection($ingredients->withQueryString());
}

#[OAT\Post(path: '/bars/{id}/ingredients/batch-store', tags: ['Bar Shelf'], summary: 'Batch store bar ingredients to bar shelf', parameters: [
new BAO\Parameters\DatabaseIdParameter(),
], requestBody: new OAT\RequestBody(
required: true,
content: [
new OAT\JsonContent(type: 'object', properties: [
new OAT\Property(property: 'ingredients', type: 'array', items: new OAT\Items(type: 'integer')),
]),
]
))]
#[OAT\Response(response: 204, description: 'Successful response')]
#[BAO\NotAuthorizedResponse]
#[BAO\NotFoundResponse]
public function batchStoreBarIngredients(ShelfIngredientsRequest $request, int $id): Response
{
$bar = Bar::findOrFail($id);
if ($request->user()->cannot('manageShelf', $bar)) {
abort(403);
}

$ingredients = DB::table('ingredients')
->select('id')
->where('bar_id', $bar->id)
->whereIn('id', $request->post('ingredients'))
->pluck('id');

$models = [];
foreach ($ingredients as $dbIngredientId) {
$userIngredient = new BarIngredient();
$userIngredient->ingredient_id = $dbIngredientId;
$models[] = $userIngredient;
}

$bar->shelfIngredients()->saveMany($models);

return new Response(null, 204);
}

#[OAT\Post(path: '/bars/{id}/ingredients/batch-delete', tags: ['Bar Shelf'], summary: 'Delete multiple ingredients from bar shelf', parameters: [
new BAO\Parameters\DatabaseIdParameter(),
], requestBody: new OAT\RequestBody(
required: true,
content: [
new OAT\JsonContent(type: 'object', properties: [
new OAT\Property(property: 'ingredients', type: 'array', items: new OAT\Items(type: 'integer')),
]),
]
))]
#[OAT\Response(response: 204, description: 'Successful response')]
#[BAO\NotAuthorizedResponse]
#[BAO\NotFoundResponse]
public function batchDeleteBarIngredients(ShelfIngredientsRequest $request, int $id): Response
{
$bar = Bar::findOrFail($id);
if ($request->user()->cannot('manageShelf', $bar)) {
abort(403);
}

$ingredients = DB::table('ingredients')
->select('id')
->where('bar_id', $bar->id)
->whereIn('id', $request->post('ingredients'))
->pluck('id');

try {
$bar->shelfIngredients()->whereIn('ingredient_id', $ingredients)->delete();
} catch (Throwable $e) {
abort(500, $e->getMessage());
}

return new Response(null, 204);
}
}
3 changes: 3 additions & 0 deletions app/Http/Controllers/StatsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ public function index(CocktailRepository $cocktailRepo, Request $request, int $i
null,
$barMembership->use_parent_as_substitute,
)->count();
$stats['total_bar_shelf_cocktails'] = $cocktailRepo->getCocktailsByIngredients(
$bar->shelfIngredients->pluck('ingredient_id')->toArray(),
)->count();
$stats['total_shelf_ingredients'] = UserIngredient::where('bar_membership_id', $barMembership->id)->count();
$stats['most_popular_ingredients'] = $popularIngredients;
$stats['top_rated_cocktails'] = $topRatedCocktails;
Expand Down
17 changes: 15 additions & 2 deletions app/Http/Filters/CocktailQueryFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ public function __construct(CocktailRepository $cocktailRepo)
));
}
}),
AllowedFilter::callback('bar_shelf', function ($query, $value) use ($cocktailRepo, $useParentIngredientAsSubstitute) {
if ($value === true) {
$query->whereIn('cocktails.id', $cocktailRepo->getCocktailsByIngredients(
bar()->shelfIngredients->pluck('ingredient_id')->toArray(),
useParentIngredientAsSubstitute: $useParentIngredientAsSubstitute,
));
}
}),
AllowedFilter::callback('user_shelves', function ($query, $value) use ($cocktailRepo, $useParentIngredientAsSubstitute) {
if (!is_array($value)) {
$value = [$value];
Expand Down Expand Up @@ -179,6 +187,7 @@ public function __construct(CocktailRepository $cocktailRepo)
'abv',
'total_ingredients',
'missing_ingredients',
'missing_bar_ingredients',
AllowedSort::callback('favorited_at', function ($query, bool $descending) use ($barMembership) {
$direction = $descending ? 'DESC' : 'ASC';

Expand All @@ -200,14 +209,18 @@ public function __construct(CocktailRepository $cocktailRepo)
'ratings',
AllowedInclude::callback('navigation', fn ($q) => $q),
])
->selectRaw('cocktails.*, COUNT(ci.cocktail_id) AS total_ingredients, COUNT(ci.ingredient_id) - COUNT(ui.ingredient_id) AS missing_ingredients')
->selectRaw('cocktails.*, COUNT(ci.cocktail_id) AS total_ingredients, COUNT(ci.ingredient_id) - COUNT(ui.ingredient_id) AS missing_ingredients, COUNT(ci.ingredient_id) - COUNT(bi.ingredient_id) AS missing_bar_ingredients')
->leftJoin('cocktail_ingredients AS ci', 'ci.cocktail_id', '=', 'cocktails.id')
->leftJoin('cocktail_ingredient_substitutes AS cis', 'cis.cocktail_ingredient_id', '=', 'ci.id')
->leftJoin('user_ingredients AS ui', function ($query) use ($barMembership) {
$query->on('ui.ingredient_id', '=', 'ci.ingredient_id')->where('ui.bar_membership_id', $barMembership->id);
})
->leftJoin('bar_ingredients AS bi', function ($query) {
$query->on('bi.ingredient_id', '=', 'ci.ingredient_id');
})
->groupBy('cocktails.id')
->filterByBar()
->filterByBar('cocktails')
->with('bar.shelfIngredients')
->withRatings($this->request->user()->id);
}
}
7 changes: 6 additions & 1 deletion app/Http/Filters/IngredientQueryFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ public function __construct(IngredientRepository $ingredientQuery)
->where('user_ingredients.bar_membership_id', $barMembership->id);
}
}),
AllowedFilter::callback('bar_shelf', function ($query, $value) {
if ($value === true) {
$query->join('bar_ingredients', 'bar_ingredients.ingredient_id', '=', 'ingredients.id');
}
}),
AllowedFilter::callback('strength_min', function ($query, $value) {
$query->where('strength', '>=', $value);
}),
Expand Down Expand Up @@ -78,6 +83,6 @@ public function __construct(IngredientRepository $ingredientQuery)
])
->allowedIncludes(['parentIngredient', 'varieties', 'prices', 'category', 'ingredientParts', 'images'])
->withCount('cocktails')
->filterByBar();
->filterByBar('ingredients');
}
}
Loading

0 comments on commit 10c09cb

Please sign in to comment.