Skip to content

Commit

Permalink
Merge pull request #271 from karlomikus/develop
Browse files Browse the repository at this point in the history
Exports, filters, fixes
  • Loading branch information
karlomikus authored Apr 7, 2024
2 parents 5e6bd12 + e2bb254 commit 55866b9
Show file tree
Hide file tree
Showing 22 changed files with 740 additions and 225 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# v3.12.0
## New
- Added `/exports` endpoints
- With this endpoint you can now manage recipe exporting for specific bars
- Added `specific_ingredients` cocktails filter
- This will show recipes that always contain specific ingredients
- Added `public_id` and `slug` to public menu cocktail response

## Fixes
- Fixed "CocktailParty" scraper

# v3.11.0
## New
- Added POST `/cocktails/{id}/copy` endpoint
Expand Down
27 changes: 5 additions & 22 deletions app/Console/Commands/BarSearchRefresh.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@
namespace Kami\Cocktail\Console\Commands;

use Illuminate\Console\Command;
use Kami\Cocktail\Models\Cocktail;
use Kami\Cocktail\Models\Ingredient;
use Illuminate\Support\Facades\Artisan;
use Kami\Cocktail\Search\SearchActionsAdapter;
use Kami\Cocktail\Jobs\RefreshSearchIndex;

class BarSearchRefresh extends Command
{
Expand All @@ -26,34 +23,20 @@ class BarSearchRefresh extends Command
*/
protected $description = 'Sync search engine index with the latest Bar Assistant data';

public function __construct(private readonly SearchActionsAdapter $searchActions)
{
parent::__construct();
}

/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$searchActions = $this->searchActions->getActions();

// Clear indexes
if ($this->option('clear')) {
$this->info('Flushing site search, cocktails and ingredients index...');
Artisan::call('scout:flush', ['model' => Cocktail::class]);
Artisan::call('scout:flush', ['model' => Ingredient::class]);
$this->info('Clearing index and syncing...');
} else {
$this->info('Syncing search index...');
}

// Update settings
$this->info('Updating search index settings...');
$searchActions->updateIndexSettings();

$this->info('Syncing cocktails and ingredients to meilisearch...');
Artisan::call('scout:import', ['model' => Cocktail::class]);
Artisan::call('scout:import', ['model' => Ingredient::class]);
RefreshSearchIndex::dispatch((bool) $this->option('clear'));

return Command::SUCCESS;
}
Expand Down
71 changes: 71 additions & 0 deletions app/Http/Controllers/ExportController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace Kami\Cocktail\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Kami\Cocktail\Models\Bar;
use Kami\Cocktail\Models\Export;
use Kami\Cocktail\Jobs\StartRecipesExport;
use Illuminate\Http\Resources\Json\JsonResource;
use Kami\Cocktail\Http\Resources\ExportResource;
use Symfony\Component\HttpFoundation\BinaryFileResponse;

class ExportController extends Controller
{
public function index(Request $request): JsonResource
{
$exports = Export::orderBy('created_at', 'desc')
->where('created_user_id', $request->user()->id)
->get();

return ExportResource::collection($exports);
}

public function store(Request $request): ExportResource
{
$type = $request->post('type', 'json');
$bar = Bar::findOrFail($request->post('bar_id'));

if ($request->user()->cannot('createExport', $bar)) {
abort(403);
}

$export = new Export();
$export->withFilename();
$export->bar_id = $bar->id;
$export->is_done = false;
$export->created_user_id = $request->user()->id;
$export->save();

StartRecipesExport::dispatch($bar->id, $type, $export);

return new ExportResource($export);
}

public function delete(Request $request, int $id): Response
{
$export = Export::findOrFail($id);

if ($request->user()->cannot('delete', $export)) {
abort(403);
}

$export->delete();

return response(null, 204);
}

public function download(Request $request, int $id): BinaryFileResponse
{
$export = Export::findOrFail($id);

if ($request->user()->cannot('download', $export)) {
abort(403);
}

return response()->download($export->getFullPath());
}
}
16 changes: 16 additions & 0 deletions app/Http/Filters/CocktailQueryFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,22 @@ public function __construct(CocktailRepository $cocktailRepo)
$query->having('missing_ingredients', (int) $value);
}
}),
AllowedFilter::callback('specific_ingredients', function ($query, $value) use ($barMembership) {
if (!is_array($value)) {
$value = [$value];
}

$query->whereIn('cocktails.id', function ($query) use ($barMembership, $value) {
$query
->select('cocktails.id')
->from('cocktails')
->where('cocktails.bar_id', $barMembership->bar_id)
->join('cocktail_ingredients', 'cocktail_ingredients.cocktail_id', '=', 'cocktails.id')
->whereIn('cocktail_ingredients.ingredient_id', $value)
->groupBy('cocktails.id')
->havingRaw('COUNT(DISTINCT cocktail_ingredients.ingredient_id) >= ?', [count($value)]);
});
}),
AllowedFilter::callback('ignore_ingredients', function ($query, $value) use ($barMembership) {
if (!is_array($value)) {
$value = [$value];
Expand Down
3 changes: 3 additions & 0 deletions app/Http/Resources/CocktailResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ public function toArray($request)
'updated_at' => $this->updated_at?->toJson(),
'method' => new CocktailMethodResource($this->whenLoaded('method')),
'abv' => $this->abv,
// 'volume' => $this->getVolume(),
// 'alcohol_units' => $this->getAlcoholUnits(),
// 'calories' => $this->getCalories(),
'created_user' => new UserBasicResource($this->whenLoaded('createdUser')),
'updated_user' => new UserBasicResource($this->whenLoaded('updatedUser')),
'access' => [
Expand Down
30 changes: 30 additions & 0 deletions app/Http/Resources/ExportResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Kami\Cocktail\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

/**
* @mixin \Kami\Cocktail\Models\Export
*/
class ExportResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array<string, mixed>
*/
public function toArray($request)
{
return [
'id' => $this->id,
'filename' => $this->filename,
'created_at' => $this->created_at,
'bar_name' => $this->bar->name,
'is_done' => (bool) $this->is_done,
];
}
}
2 changes: 2 additions & 0 deletions app/Http/Resources/MenuPublicResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public function toArray($request)
'full' => $menuCocktail->price,
'formatted' => number_format($menuCocktail->price / 100, 2),
],
'public_id' => $menuCocktail->cocktail->public_id,
'slug' => $menuCocktail->cocktail->slug,
'currency' => $menuCocktail->currency,
'name' => $menuCocktail->cocktail->name,
'short_ingredients' => $menuCocktail->cocktail->getShortIngredients(),
Expand Down
55 changes: 55 additions & 0 deletions app/Jobs/RefreshSearchIndex.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace Kami\Cocktail\Jobs;

use Illuminate\Bus\Queueable;
use Kami\Cocktail\Models\Cocktail;
use Illuminate\Support\Facades\Log;
use Kami\Cocktail\Models\Ingredient;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Kami\Cocktail\Search\SearchActionsAdapter;

class RefreshSearchIndex implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

/**
* Create a new job instance.
*/
public function __construct(private readonly bool $shouldClearIndexes = false)
{
//
}

/**
* Execute the job.
*/
public function handle(SearchActionsAdapter $searchActions): void
{
$searchActions = $searchActions->getActions();

// Clear indexes
if ($this->shouldClearIndexes) {
Log::info('Clearing search indexes');
Artisan::call('scout:flush', ['model' => Cocktail::class]);
Artisan::call('scout:flush', ['model' => Ingredient::class]);
}

// Update settings
Log::info('Updating search settings');
$searchActions->updateIndexSettings();

Log::info('Building search indexes');

Artisan::call('scout:import', ['model' => Cocktail::class]);
Artisan::call('scout:import', ['model' => Ingredient::class]);

Log::info('Search indexes updated');
}
}
35 changes: 35 additions & 0 deletions app/Jobs/StartRecipesExport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Kami\Cocktail\Jobs;

use Illuminate\Bus\Queueable;
use Kami\Cocktail\Models\Export;
use Kami\Cocktail\ExportTypeEnum;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Kami\Cocktail\External\Export\Recipes;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\Attributes\WithoutRelations;

class StartRecipesExport implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public function __construct(
private readonly int $barId,
private readonly string $type,
#[WithoutRelations]
private readonly Export $export,
) {
}

public function handle(Recipes $exporter): void
{
$type = ExportTypeEnum::tryFrom($this->type) ?? ExportTypeEnum::JSON;

$exporter->process($this->barId, $this->export->getFullPath(), $type);

$this->export->markAsDone();
}
}
34 changes: 34 additions & 0 deletions app/Models/Cocktail.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,40 @@ public function getABV(): ?float
return Utils::calculateAbv($ingredients, $this->method->dilution_percentage);
}

public function getVolume(): float
{
return (float) $this->ingredients
->filter(function ($cocktailIngredient) {
$normalizedUnits = mb_strtolower($cocktailIngredient->units, 'UTF-8');

return in_array($normalizedUnits, ['ml', 'oz', 'cl']);
})
->sum('amount');
}

public function getAlcoholUnits(): float
{
if ($this->cocktail_method_id === null) {
return 0.0;
}

return (float) number_format(($this->getVolume() * $this->getABV()) / 1000, 2);
}

public function getCalories(): int
{
if ($this->getABV() === null) {
return 0;
}

// It's important to note that the calorie content of mixed drinks can vary significantly
// depending on the type and amount of mixers used. Drinks with sugary mixers or syrups
// will generally have higher calorie counts.
$averageAlcCalories = 7;

return (int) floor($this->getVolume() * ($this->getABV() / 100) * $averageAlcCalories);
}

public function getMainIngredient(): ?CocktailIngredient
{
return $this->ingredients->first();
Expand Down
Loading

0 comments on commit 55866b9

Please sign in to comment.