Merge pull request 'TIST-32: Reviews model and publish as pending' (#35) from TIST-32 into develop

Reviewed-on: #35
Reviewed-by: greendavid004 <davidkatrinka1995@gmail.com>
This commit is contained in:
steve_dekart 2025-07-06 09:55:55 +02:00
commit e2fe1a4173
10 changed files with 305 additions and 111 deletions

View File

@ -50,6 +50,12 @@ class AdminDeleteController extends Abstract\AdminBaseController
'field_display' => 'ingredient_name', 'field_display' => 'ingredient_name',
'back_to' => 'admin:ing-cat-rel', 'back_to' => 'admin:ing-cat-rel',
], ],
[
'url_name' => 'recipe-reviews',
'model' => 'Lycoreco\Apps\Recipes\Models\ReviewsModel',
'field_display' => 'field_title',
'back_to' => 'admin:review',
],
]; ];
protected $template_name = APPS_PATH . '/Admin/Templates/delete.php'; protected $template_name = APPS_PATH . '/Admin/Templates/delete.php';

View File

@ -0,0 +1,44 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
use Lycoreco\Apps\Recipes\Models\ReviewsModel;
class AdminReviewControler extends Abstract\AdminSingleController
{
protected $model_сlass_name = "Lycoreco\Apps\Recipes\Models\ReviewsModel";
protected $field_title = 'field_title';
protected $verbose_name = 'review';
protected $object_router_name = 'admin:review';
protected $fields = array(
[
'model_field' => 'title',
'input_type' => 'text',
'input_attrs' => ['required']
],
[
'model_field' => 'rating',
'input_type' => 'number',
'input_attrs' => ['required']
],
[
'model_field' => 'content',
'input_type' => 'textarea',
'input_attrs' => ['required']
],
[
'model_field' => 'status',
'input_type' => 'select',
'input_attrs' => ['required'],
'input_values' => ReviewsModel::STATUS
],
[
'model_field' => 'created_at',
'input_type' => 'text',
'dynamic_save' => false,
'input_label' => 'Created at',
'input_attrs' => ['disabled']
]
);
}

View File

@ -0,0 +1,17 @@
<?php
namespace Lycoreco\Apps\Admin\Controllers;
class AdminReviewListController extends Abstract\AdminListController
{
protected $model_сlass_name = "Lycoreco\Apps\Recipes\Models\ReviewsModel";
protected $table_fields = array(
'Title' => 'field_title',
'Rating' => 'field_rating',
'Status ' => 'get_status()',
'Creaated at' => 'field_created_at',
);
protected $single_router_name = 'admin:review';
protected $verbose_name = "review";
protected $verbose_name_multiply = "reviews";
}

View File

@ -26,6 +26,11 @@ $sidebar_links = [
'name' => 'Ingredients', 'name' => 'Ingredients',
'icon' => 'fa-solid fa-carrot', 'icon' => 'fa-solid fa-carrot',
'router_name' => 'admin:ingredient-list' 'router_name' => 'admin:ingredient-list'
],
[
'name' => 'Reviews',
'icon' => 'fa-solid fa-comment',
'router_name' => 'admin:review-list'
] ]
]; ];
?> ?>

View File

@ -12,6 +12,7 @@ $admin_urls = [
new Path('/admin/recipes',new Controllers\AdminRecipeListController(), 'recipe-list'), new Path('/admin/recipes',new Controllers\AdminRecipeListController(), 'recipe-list'),
new Path('/admin/categories',new Controllers\AdminCategoryListController(), 'category-list'), new Path('/admin/categories',new Controllers\AdminCategoryListController(), 'category-list'),
new Path('/admin/ingredients',new Controllers\IngredientListController(), 'ingredient-list'), new Path('/admin/ingredients',new Controllers\IngredientListController(), 'ingredient-list'),
new Path('/admin/reviews', new Controllers\AdminReviewListController(), 'review-list'),
////// Single object /////// ////// Single object ///////
// User // User
@ -35,6 +36,9 @@ $admin_urls = [
new Path('/admin/ingredient/[:int]', new Controllers\IngredientController(), 'ingredient'), new Path('/admin/ingredient/[:int]', new Controllers\IngredientController(), 'ingredient'),
new Path('/admin/ingredient/new', new Controllers\IngredientController(true), 'ingredient-new'), new Path('/admin/ingredient/new', new Controllers\IngredientController(true), 'ingredient-new'),
// Reviews
new Path('/admin/review/[:int]', new Controllers\AdminReviewControler(), 'review'),
// Recipe ingedient relation // Recipe ingedient relation
new Path('/admin/recipe/[:int]/ingredients', new Controllers\IngredientRecipeRelListController(), 'ing-cat-rel-list'), new Path('/admin/recipe/[:int]/ingredients', new Controllers\IngredientRecipeRelListController(), 'ing-cat-rel-list'),
new Path('/admin/recipe/ingredient/[:int]', new Controllers\IngredientRecipeRelController(), 'ing-cat-rel'), new Path('/admin/recipe/ingredient/[:int]', new Controllers\IngredientRecipeRelController(), 'ing-cat-rel'),

View File

@ -5,6 +5,7 @@ namespace Lycoreco\Apps\Index\Controllers;
use Lycoreco\Apps\Recipes\Models\CategoryModel; use Lycoreco\Apps\Recipes\Models\CategoryModel;
use Lycoreco\Apps\Recipes\Models\RecipeModel; use Lycoreco\Apps\Recipes\Models\RecipeModel;
use Lycoreco\Apps\Recipes\Models\RecipeUserMenu; use Lycoreco\Apps\Recipes\Models\RecipeUserMenu;
use Lycoreco\Apps\Recipes\Models\ReviewsModel;
use Lycoreco\Includes\BaseController; use Lycoreco\Includes\BaseController;
require_once(INCLUDES_PATH . '/Const/recipes.php'); require_once(INCLUDES_PATH . '/Const/recipes.php');
@ -29,6 +30,16 @@ class HomepageController extends BaseController
$dayNumber = date("w"); $dayNumber = date("w");
$dayofweek = DAYS_OF_WEEK[$dayNumber]; $dayofweek = DAYS_OF_WEEK[$dayNumber];
$context['reviews'] = ReviewsModel::filter(array(
[
'name' => 'obj.status',
'type' => '=',
'value' => 'publish'
]),
['-obj.created_at'],
6
);
if(CURRENT_USER) { if(CURRENT_USER) {
$context['usermenu_recipe_prefetch'] = RecipeUserMenu::get_prefetch_recipes(CURRENT_USER, $dayofweek); $context['usermenu_recipe_prefetch'] = RecipeUserMenu::get_prefetch_recipes(CURRENT_USER, $dayofweek);
} }

View File

@ -86,104 +86,23 @@ the_header(
<div class="recent-reviews"> <div class="recent-reviews">
<h2 class="title">Recent User Reviews</h2> <h2 class="title">Recent User Reviews</h2>
<div class="reviews-grid"> <div class="reviews-grid">
<a href="#" class="recent-review hover-anim"> <?php foreach($context['reviews'] as $review): ?>
<a href="<?php the_permalink('recipes:single', [$review->field_recipe_id]) ?>" class="recent-review hover-anim">
<div class="review-title"> <div class="review-title">
<div class="review-img"> <div class="review-img">
<img src="media/recipe1.jpeg" alt="reviewed recipe"> <img src="<?= $review->get_recipe_image() ?>" alt="reviewed recipe">
</div> </div>
<h3 class="review-title-text">Just Like Mom's</h3> <h3 class="review-title-text"><?= $review->recipe_title ?></h3>
</div> </div>
<div class="review-text"> <div class="review-text">
<p>I made this spaghetti last night and it was absolutely delicious! The sauce was rich and <p><?= $review->get_excerpt() ?></p>
flavorful—just the right balance of garlic, herbs, and tomato. I added a pinch of chili
flakes for a little kick, and it turned out perfect...</p>
</div> </div>
<div class="review-meta meta"> <div class="review-meta meta">
<p class="review-author"><i class="fa-solid fa-user"></i> GreenDavid004</p> <p class="review-author"><i class="fa-solid fa-user"></i> <?= $review->author_username ?></p>
<p class="review-date"><i class="fa-solid fa-calendar-days"></i> 2023-10-01</p> <p class="review-date"><i class="fa-solid fa-calendar-days"></i> <?= $review->get_date() ?></p>
</div>
</a>
<a href="#" class="recent-review hover-anim">
<div class="review-title">
<div class="review-img">
<img src="media/recipe1.jpeg" alt="reviewed recipe">
</div>
<h3 class="review-title-text">Just Like Mom's</h3>
</div>
<div class="review-text">
<p>I made this spaghetti last night and it was absolutely delicious! The sauce was rich and
flavorful—just the right balance of garlic, herbs, and tomato. I added a pinch of chili
flakes for a little kick, and it turned out perfect...</p>
</div>
<div class="review-meta meta">
<p class="review-author"><i class="fa-solid fa-user"></i> GreenDavid004</p>
<p class="review-date"><i class="fa-solid fa-calendar-days"></i> 2023-10-01</p>
</div>
</a><a href="#" class="recent-review hover-anim">
<div class="review-title">
<div class="review-img">
<img src="media/recipe1.jpeg" alt="reviewed recipe">
</div>
<h3 class="review-title-text">Just Like Mom's</h3>
</div>
<div class="review-text">
<p>I made this spaghetti last night and it was absolutely delicious! The sauce was rich and
flavorful—just the right balance of garlic, herbs, and tomato. I added a pinch of chili
flakes for a little kick, and it turned out perfect...</p>
</div>
<div class="review-meta meta">
<p class="review-author"><i class="fa-solid fa-user"></i> GreenDavid004</p>
<p class="review-date"><i class="fa-solid fa-calendar-days"></i> 2023-10-01</p>
</div>
</a><a href="#" class="recent-review hover-anim">
<div class="review-title">
<div class="review-img">
<img src="media/recipe1.jpeg" alt="reviewed recipe">
</div>
<h3 class="review-title-text">Just Like Mom's</h3>
</div>
<div class="review-text">
<p>I made this spaghetti last night and it was absolutely delicious! The sauce was rich and
flavorful—just the right balance of garlic, herbs, and tomato. I added a pinch of chili
flakes for a little kick, and it turned out perfect...</p>
</div>
<div class="review-meta meta">
<p class="review-author"><i class="fa-solid fa-user"></i> GreenDavid004</p>
<p class="review-date"><i class="fa-solid fa-calendar-days"></i> 2023-10-01</p>
</div>
</a><a href="#" class="recent-review hover-anim">
<div class="review-title">
<div class="review-img">
<img src="media/recipe1.jpeg" alt="reviewed recipe">
</div>
<h3 class="review-title-text">Just Like Mom's</h3>
</div>
<div class="review-text">
<p>I made this spaghetti last night and it was absolutely delicious! The sauce was rich and
flavorful—just the right balance of garlic, herbs, and tomato. I added a pinch of chili
flakes for a little kick, and it turned out perfect...</p>
</div>
<div class="review-meta meta">
<p class="review-author"><i class="fa-solid fa-user"></i> GreenDavid004</p>
<p class="review-date"><i class="fa-solid fa-calendar-days"></i> 2023-10-01</p>
</div>
</a><a href="#" class="recent-review hover-anim">
<div class="review-title">
<div class="review-img">
<img src="media/recipe1.jpeg" alt="reviewed recipe">
</div>
<h3 class="review-title-text">Just Like Mom's</h3>
</div>
<div class="review-text">
<p>I made this spaghetti last night and it was absolutely delicious! The sauce was rich and
flavorful—just the right balance of garlic, herbs, and tomato. I added a pinch of chili
flakes for a little kick, and it turned out perfect...</p>
</div>
<div class="review-meta meta">
<p class="review-author"><i class="fa-solid fa-user"></i> GreenDavid004</p>
<p class="review-date"><i class="fa-solid fa-calendar-days"></i> 2023-10-01</p>
</div> </div>
</a> </a>
<?php endforeach; ?>
</div> </div>
</div> </div>
<?php if(CURRENT_USER): ?> <?php if(CURRENT_USER): ?>

View File

@ -2,16 +2,51 @@
namespace Lycoreco\Apps\Recipes\Controllers; namespace Lycoreco\Apps\Recipes\Controllers;
use Exception;
use Lycoreco\Apps\Recipes\Models\IngredientInRecipeModel; use Lycoreco\Apps\Recipes\Models\IngredientInRecipeModel;
use Lycoreco\Apps\Recipes\Models\RecipeModel; use Lycoreco\Apps\Recipes\Models\RecipeModel;
use Lycoreco\Apps\Recipes\Models\ReviewsModel;
use Lycoreco\Apps\Users\Models\UserModel; use Lycoreco\Apps\Users\Models\UserModel;
use Lycoreco\Includes\BaseController; use Lycoreco\Includes\BaseController;
use Lycoreco\Includes\Model\ValidationError;
use Lycoreco\Includes\Routing\HttpExceptions; use Lycoreco\Includes\Routing\HttpExceptions;
class SingleRecipeController extends BaseController class SingleRecipeController extends BaseController
{ {
protected $template_name = APPS_PATH . '/Recipes/Templates/single.php'; protected $template_name = APPS_PATH . '/Recipes/Templates/single.php';
protected function post()
{
if(!CURRENT_USER) {
$this->context['reviews_error'] = 'You are not authorized';
return;
}
$rating = $_POST['rating-select'] ?? null;
$title = $_POST['review-title'] ?? null;
$content = $_POST['review-body-input'] ?? null;
$recipe = $this->get_model();
$review = new ReviewsModel();
$review->field_title = $title;
$review->field_content = $content;
$review->field_rating = $rating;
$review->field_status = 'pending';
$review->field_user_id = CURRENT_USER->get_id();
$review->field_recipe_id = $recipe->get_id();
try {
$review->save();
$this->context['display_review_form'] = false;
}
catch(ValidationError $ex) {
$this->context['reviews_error'] = $ex->getMessage();
}
catch(Exception $ex) {
$this->context['reviews_error'] = 'Unexpected error';
}
}
protected function get_model() protected function get_model()
{ {
if (isset($this->__model)) if (isset($this->__model))
@ -39,6 +74,7 @@ class SingleRecipeController extends BaseController
$context['recipe'] = $recipe; $context['recipe'] = $recipe;
$context['display_review_form'] = true;
$context['author'] = UserModel::get(array( $context['author'] = UserModel::get(array(
[ [
@ -56,6 +92,42 @@ class SingleRecipeController extends BaseController
] ]
)); ));
$context['reviews'] = ReviewsModel::filter(array(
[
'name' => 'obj.recipe_id',
'type' => '=',
'value' => $recipe->get_id()
],
[
'name' => 'obj.status',
'type' => '=',
'value' => 'publish'
],
));
$context['reviews_average'] = '0.0';
if(!empty($context['reviews'])) {
$sum = array_sum(array_column($context['reviews'], 'field_rating'));
$count = count($context['reviews']);
$context['reviews_average'] = number_format($sum / $count, 1);
}
if(CURRENT_USER) {
$review_exists = ReviewsModel::count(array(
[
'name' => 'obj.recipe_id',
'type' => '=',
'value' => $recipe->get_id()
],
[
'name' => 'obj.user_id',
'type' => '=',
'value' => CURRENT_USER->get_id()
]
));
if($review_exists > 0)
$context['display_review_form'] = false;
}
return $context; return $context;
} }
} }

View File

@ -0,0 +1,121 @@
<?php
namespace Lycoreco\Apps\Recipes\Models;
use DateTime;
use Lycoreco\Includes\Model\BaseModel;
class ReviewsModel extends BaseModel
{
const STATUS = [['publish', 'Publish'], ['pending', 'Pending']];
public $field_title;
public $field_content;
public $field_rating;
public $field_status;
public $field_recipe_id;
public $field_user_id;
public $field_created_at;
public $author_username;
public $recipe_title;
public $recipe_image;
static protected $search_fields = ['obj.title'];
static protected $table_name = 'recipe_reviews';
static protected $table_fields = [
'id' => 'int',
'title' => 'string',
'content' => 'string',
'rating' => 'int',
'status' => 'string',
'recipe_id' => 'int',
'user_id' => 'int',
'created_at' => 'DateTime',
];
static protected $additional_fields = array(
[
'field' => [
'us.username AS author_username'
],
'join_table' => 'users us ON us.id = obj.user_id'
],
[
'field' => [
're.title AS recipe_title',
're.image_url AS recipe_image'
],
'join_table' => 'recipes re ON re.id = obj.recipe_id'
]
);
public static function init_table()
{
$result = db_query('CREATE TABLE ' . static::$table_name . ' (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
rating INT NOT NULL,
status VARCHAR(20) NOT NULL CHECK (status IN (\'publish\', \'pending\')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
recipe_id INT NOT NULL,
user_id INT NOT NULL,
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);');
return $result;
}
public function get_html_content(): string
{
return nl2br(trim($this->field_content));
}
public function get_status()
{
return ucfirst($this->field_status);
}
public function get_date()
{
$date = new DateTime($this->field_created_at);
return $date->format('d.m.Y');
}
public function get_excerpt()
{
$max_length = 100;
$excerpt = mb_substr($this->field_content, 0, $max_length);
if(mb_strlen($this->field_content) > $max_length)
$excerpt .= '...';
return $excerpt;
}
public function get_recipe_image()
{
$recipe = new RecipeModel();
$recipe->field_image_url = $this->recipe_image;
return $recipe->get_image_url();
}
public function valid() {
$errors = array();
if(empty($this->field_title))
$errors[] = 'Title field is empty';
if(empty($this->field_content))
$errors[] = 'Content field is empty';
$this->field_rating = (int)$this->field_rating;
if($this->field_rating < 1 || $this->field_rating > 5) {
$errors[] = 'Rating must be between 1 and 5';
}
if (empty($errors))
return true;
return $errors;
}
}

View File

@ -130,45 +130,40 @@ the_header(
</div> </div>
<div class="single-recipe-reviews"> <div class="single-recipe-reviews">
<h2 class="title">Reviews</h2> <h2 class="title">Reviews</h2>
<h3 class="rating">Average Rating: 4.5 <i class="fa-regular fa-star"></i></h3> <h3 class="rating">Average Rating: <?= $context['reviews_average'] ?> <i class="fa-regular fa-star"></i></h3>
<div class="reviews"> <div class="reviews">
<div class="review-list"> <div class="review-list">
<h4 class="subtitle">All Reviews</h4> <h4 class="subtitle">All Reviews</h4>
<?php if(empty($context['reviews'])): ?>
<div class="nothing">Nothing to show</div>
<?php endif; ?>
<?php foreach ($context['reviews'] as $review): ?>
<div class="review"> <div class="review">
<span class="review-title"> <i class="fa-solid fa-star"></i> 5 Divine disc of <span class="review-title"><?= $review->field_rating ?> <i class="fa-solid fa-star"></i>&nbsp;&nbsp;<?= $review->field_title ?></span>
deliciousness!</span>
<div class="review-body"> <div class="review-body">
Blessings of Aqua-sama upon you, fellow foodies! As a proud and very sane member of the Axis <?= $review->get_html_content() ?>
Cult, I recently partook in a holy culinary experience that rivaled even the sacred waters
of the Blue Lake: a homemade pepperoni and mushroom pizza!
The moment I laid eyes on that glorious golden crust, I knew—Aqua-sama herself must have
guided my ovens temperature dial! The cheese was melted with such heavenly grace,
stretching with every bite like the ribbons of our goddesss divine garments. The pepperoni?
Spicy circles of salvation! And the mushrooms—earthy little blessings sent straight from the
soil Aqua purifies daily!
The crust had the perfect balance of crispy edge and fluffy soul. It crackled like the
laughter of a cultist pranking a stubborn Eris follower (teehee~). Each bite sang praises in
my mouth: “Axis! Axis! AXIS!!
Would I recommend this pizza? Of course! Id even offer it as tribute at the next festival
in Aquas honor. Try it yourself and bask in the savory enlightenment. And remember: if it
tastes weird, just pour holy water on it. Or beer. Aqua would approve either way.
Rating: 5 out of 5 Axis Blessings!(Any lower would be heresy.)
</div> </div>
<div class="single-review-meta"> <div class="single-review-meta">
<div class="review-meta__user meta"> <div class="review-meta__user meta">
<i class="fa-regular fa-user"></i> GreenDavid004 <i class="fa-regular fa-user"></i> <?= $review->author_username ?>
</div> </div>
<div class="review-meta__date meta"><?= date("d.m.Y"); ?></div> <div class="review-meta__date meta"><?= $review->get_date() ?></div>
</div> </div>
</div> </div>
<?php endforeach; ?>
</div> </div>
<?php if($context['display_review_form']): ?>
<div class="review-form"> <div class="review-form">
<h4 class="subtitle">Your Review</h4> <h4 class="subtitle">Your Review</h4>
<?php if(isset($context['reviews_error'])) {
the_alert($context['reviews_error'], 'warning');
} ?>
<form id="review-form" class="review-form__form" method="post" action="#"> <form id="review-form" class="review-form__form" method="post" action="#">
<div class="rating-selection"> <div class="rating-selection">
<label for="rating-select">Choose rating: </label> <label for="rating-select">Choose rating: </label>
<select name="rating-select" id="rating select"> <select name="rating-select" id="rating select">
<option value="0">Rating</option> <option>Rating</option>
<option value="1">1 Star</option> <option value="1">1 Star</option>
<option value="2">2 Star </option> <option value="2">2 Star </option>
<option value="3">3 Star</option> <option value="3">3 Star</option>
@ -184,8 +179,8 @@ the_header(
<button type="submit" class="btn btn-primary hover-anim">Submit</button> <button type="submit" class="btn btn-primary hover-anim">Submit</button>
</form> </form>
</div> </div>
<?php endif; ?>
</div> </div>
</div> </div>
</div> </div>