change posts to articles

This commit is contained in:
2023-04-26 10:57:27 +10:00
parent c6d318bbc3
commit 3ee97468f9
23 changed files with 416 additions and 486 deletions

View File

@@ -13,13 +13,13 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use LogicException; use LogicException;
class PostConductor extends Conductor class ArticleConductor extends Conductor
{ {
/** /**
* The Model Class * The Model Class
* @var string * @var string
*/ */
protected $class = '\App\Models\Post'; protected $class = '\App\Models\Article';
/** /**
* The default sorting field * The default sorting field
@@ -44,7 +44,7 @@ class PostConductor extends Conductor
public function scope(Builder $builder) public function scope(Builder $builder)
{ {
$user = auth()->user(); $user = auth()->user();
if ($user === null || $user->hasPermission('admin/posts') === false) { if ($user === null || $user->hasPermission('admin/articles') === false) {
$builder $builder
->where('publish_at', '<=', now()); ->where('publish_at', '<=', now());
} }
@@ -60,7 +60,7 @@ class PostConductor extends Conductor
{ {
if (Carbon::parse($model->publish_at)->isFuture() === true) { if (Carbon::parse($model->publish_at)->isFuture() === true) {
$user = auth()->user(); $user = auth()->user();
if ($user === null || $user->hasPermission('admin/posts') === false) { if ($user === null || $user->hasPermission('admin/articles') === false) {
return false; return false;
} }
} }
@@ -76,7 +76,7 @@ class PostConductor extends Conductor
public static function creatable() public static function creatable()
{ {
$user = auth()->user(); $user = auth()->user();
return ($user !== null && $user->hasPermission('admin/posts') === true); return ($user !== null && $user->hasPermission('admin/articles') === true);
} }
/** /**
@@ -88,7 +88,7 @@ class PostConductor extends Conductor
public static function updatable(Model $model) public static function updatable(Model $model)
{ {
$user = auth()->user(); $user = auth()->user();
return ($user !== null && $user->hasPermission('admin/posts') === true); return ($user !== null && $user->hasPermission('admin/articles') === true);
} }
/** /**
@@ -100,7 +100,7 @@ class PostConductor extends Conductor
public static function destroyable(Model $model) public static function destroyable(Model $model)
{ {
$user = auth()->user(); $user = auth()->user();
return ($user !== null && $user->hasPermission('admin/posts') === true); return ($user !== null && $user->hasPermission('admin/articles') === true);
} }
/** /**

View File

@@ -3,11 +3,11 @@
namespace App\Http\Controllers\Api; namespace App\Http\Controllers\Api;
use App\Conductors\MediaConductor; use App\Conductors\MediaConductor;
use App\Conductors\PostConductor; use App\Conductors\ArticleConductor;
use App\Enum\HttpResponseCodes; use App\Enum\HttpResponseCodes;
use App\Http\Requests\PostRequest; use App\Http\Requests\ArticleRequest;
use App\Models\Media; use App\Models\Media;
use App\Models\Post; use App\Models\Article;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Carbon\Exceptions\InvalidFormatException; use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Container\BindingResolutionException;
@@ -15,7 +15,7 @@ use Illuminate\Database\Eloquent\InvalidCastException;
use Illuminate\Database\Eloquent\MassAssignmentException; use Illuminate\Database\Eloquent\MassAssignmentException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class PostController extends ApiController class ArticleController extends ApiController
{ {
/** /**
* ApplicationController constructor. * ApplicationController constructor.
@@ -38,12 +38,13 @@ class PostController extends ApiController
*/ */
public function index(Request $request) public function index(Request $request)
{ {
list($collection, $total) = PostConductor::request($request); list($collection, $total) = ArticleConductor::request($request);
return $this->respondAsResource( return $this->respondAsResource(
$collection, $collection,
['isCollection' => true, ['isCollection' => true,
'appendData' => ['total' => $total]] 'appendData' => ['total' => $total]
]
); );
} }
@@ -51,13 +52,13 @@ class PostController extends ApiController
* Display the specified resource. * Display the specified resource.
* *
* @param \Illuminate\Http\Request $request The endpoint request. * @param \Illuminate\Http\Request $request The endpoint request.
* @param \App\Models\Post $post The post model. * @param \App\Models\Article $article The article model.
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function show(Request $request, Post $post) public function show(Request $request, Article $article)
{ {
if (PostConductor::viewable($post) === true) { if (ArticleConductor::viewable($article) === true) {
return $this->respondAsResource(PostConductor::model($request, $post)); return $this->respondAsResource(ArticleConductor::model($request, $article));
} }
return $this->respondForbidden(); return $this->respondForbidden();
@@ -66,15 +67,15 @@ class PostController extends ApiController
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param \App\Http\Requests\PostRequest $request The user request. * @param \App\Http\Requests\ArticleRequest $request The user request.
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function store(PostRequest $request) public function store(ArticleRequest $request)
{ {
if (PostConductor::creatable() === true) { if (ArticleConductor::creatable() === true) {
$post = Post::create($request->all()); $article = Article::create($request->all());
return $this->respondAsResource( return $this->respondAsResource(
PostConductor::model($request, $post), ArticleConductor::model($request, $article),
['respondCode' => HttpResponseCodes::HTTP_CREATED] ['respondCode' => HttpResponseCodes::HTTP_CREATED]
); );
} else { } else {
@@ -85,15 +86,15 @@ class PostController extends ApiController
/** /**
* Update the specified resource in storage. * Update the specified resource in storage.
* *
* @param \App\Http\Requests\PostRequest $request The post update request. * @param \App\Http\Requests\ArticleRequest $request The article update request.
* @param \App\Models\Post $post The specified post. * @param \App\Models\Article $article The specified article.
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function update(PostRequest $request, Post $post) public function update(ArticleRequest $request, Article $article)
{ {
if (PostConductor::updatable($post) === true) { if (ArticleConductor::updatable($article) === true) {
$post->update($request->all()); $article->update($request->all());
return $this->respondAsResource(PostConductor::model($request, $post)); return $this->respondAsResource(ArticleConductor::model($request, $article));
} }
return $this->respondForbidden(); return $this->respondForbidden();
@@ -102,13 +103,13 @@ class PostController extends ApiController
/** /**
* Remove the specified resource from storage. * Remove the specified resource from storage.
* *
* @param \App\Models\Post $post The specified post. * @param \App\Models\Article $article The specified article.
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function destroy(Post $post) public function destroy(Article $article)
{ {
if (PostConductor::destroyable($post) === true) { if (ArticleConductor::destroyable($article) === true) {
$post->delete(); $article->delete();
return $this->respondNoContent(); return $this->respondNoContent();
} else { } else {
return $this->respondForbidden(); return $this->respondForbidden();
@@ -117,18 +118,18 @@ class PostController extends ApiController
/** /**
* Get a list of attachments related to this model. * Get a list of attachments related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The post model. * @param Article $article The article model.
* @return JsonResponse Returns the post attachments. * @return JsonResponse Returns the article attachments.
* @throws InvalidFormatException * @throws InvalidFormatException
* @throws BindingResolutionException * @throws BindingResolutionException
* @throws InvalidCastException * @throws InvalidCastException
*/ */
public function getAttachments(Request $request, Post $post) public function getAttachments(Request $request, Article $article)
{ {
if (PostConductor::viewable($post) === true) { if (ArticleConductor::viewable($article) === true) {
$medium = $post->attachments->map(function ($attachment) { $medium = $article->attachments->map(function ($attachment) {
return $attachment->media; return $attachment->media;
}); });
@@ -140,18 +141,18 @@ class PostController extends ApiController
/** /**
* Store an attachment related to this model. * Store an attachment related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The post model. * @param Article $article The article model.
* @return JsonResponse The response. * @return JsonResponse The response.
* @throws BindingResolutionException * @throws BindingResolutionException
* @throws MassAssignmentException * @throws MassAssignmentException
*/ */
public function storeAttachment(Request $request, Post $post) public function storeAttachment(Request $request, Article $article)
{ {
if (PostConductor::updatable($post) === true) { if (ArticleConductor::updatable($article) === true) {
if($request->has("medium") && Media::find($request->medium)) { if ($request->has("medium") && Media::find($request->medium)) {
$post->attachments()->create(['media_id' => $request->medium]); $article->attachments()->create(['media_id' => $request->medium]);
return $this->respondCreated(); return $this->respondCreated();
} }
@@ -163,67 +164,67 @@ class PostController extends ApiController
/** /**
* Update/replace attachments related to this model. * Update/replace attachments related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The related model. * @param Article $article The related model.
* @return JsonResponse * @return JsonResponse
* @throws BindingResolutionException * @throws BindingResolutionException
* @throws MassAssignmentException * @throws MassAssignmentException
*/ */
public function updateAttachments(Request $request, Post $post) public function updateAttachments(Request $request, Article $article)
{ {
if (PostConductor::updatable($post) === true) { if (ArticleConductor::updatable($article) === true) {
$mediaIds = $request->attachments; $mediaIds = $request->attachments;
if(is_array($mediaIds) === false) { if (is_array($mediaIds) === false) {
$mediaIds = explode(',', $request->attachments); $mediaIds = explode(',', $request->attachments);
} }
$mediaIds = array_map('trim', $mediaIds); // trim each media ID $mediaIds = array_map('trim', $mediaIds); // trim each media ID
$attachments = $post->attachments; $attachments = $article->attachments;
// Delete attachments that are not in $mediaIds // Delete attachments that are not in $mediaIds
foreach ($attachments as $attachment) { foreach ($attachments as $attachment) {
if (!in_array($attachment->media_id, $mediaIds)) { if (!in_array($attachment->media_id, $mediaIds)) {
$attachment->delete(); $attachment->delete();
} }
} }
// Create new attachments for media IDs that are not already in $post->attachments() // Create new attachments for media IDs that are not already in $article->attachments()
foreach ($mediaIds as $mediaId) { foreach ($mediaIds as $mediaId) {
$found = false; $found = false;
foreach ($attachments as $attachment) { foreach ($attachments as $attachment) {
if ($attachment->media_id == $mediaId) { if ($attachment->media_id == $mediaId) {
$found = true; $found = true;
break; break;
} }
} }
if (!$found) { if (!$found) {
$post->attachments()->create(['media_id' => $mediaId]); $article->attachments()->create(['media_id' => $mediaId]);
} }
} }
return $this->respondNoContent(); return $this->respondNoContent();
} }//end if
return $this->respondForbidden(); return $this->respondForbidden();
} }
/** /**
* Delete a specific related attachment. * Delete a specific related attachment.
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The model. * @param Article $article The model.
* @param Media $medium The attachment medium. * @param Media $medium The attachment medium.
* @return JsonResponse * @return JsonResponse
* @throws BindingResolutionException * @throws BindingResolutionException
*/ */
public function deleteAttachment(Request $request, Post $post, Media $medium) public function deleteAttachment(Request $request, Article $article, Media $medium)
{ {
if (PostConductor::updatable($post) === true) { if (ArticleConductor::updatable($article) === true) {
$attachments = $post->attachments; $attachments = $article->attachments;
$deleted = false; $deleted = false;
foreach ($attachments as $attachment) { foreach ($attachments as $attachment) {
if ($attachment->media_id === $medium->id) { if ($attachment->media_id === $medium->id) {
$attachment->delete(); $attachment->delete();
@@ -231,7 +232,7 @@ class PostController extends ApiController
break; break;
} }
} }
if ($deleted) { if ($deleted) {
// Attachment was deleted successfully // Attachment was deleted successfully
return $this->respondNoContent(); return $this->respondNoContent();

View File

@@ -111,8 +111,8 @@ class EventController extends ApiController
* Get a list of attachments related to this model. * Get a list of attachments related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The post model. * @param Article $article The article model.
* @return JsonResponse Returns the post attachments. * @return JsonResponse Returns the article attachments.
* @throws InvalidFormatException * @throws InvalidFormatException
* @throws BindingResolutionException * @throws BindingResolutionException
* @throws InvalidCastException * @throws InvalidCastException
@@ -134,7 +134,7 @@ class EventController extends ApiController
* Store an attachment related to this model. * Store an attachment related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The post model. * @param Article $article The article model.
* @return JsonResponse The response. * @return JsonResponse The response.
* @throws BindingResolutionException * @throws BindingResolutionException
* @throws MassAssignmentException * @throws MassAssignmentException
@@ -157,7 +157,7 @@ class EventController extends ApiController
* Update/replace attachments related to this model. * Update/replace attachments related to this model.
* *
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The related model. * @param Article $article The related model.
* @return JsonResponse * @return JsonResponse
* @throws BindingResolutionException * @throws BindingResolutionException
* @throws MassAssignmentException * @throws MassAssignmentException
@@ -180,7 +180,7 @@ class EventController extends ApiController
} }
} }
// Create new attachments for media IDs that are not already in $post->attachments() // Create new attachments for media IDs that are not already in $article->attachments()
foreach ($mediaIds as $mediaId) { foreach ($mediaIds as $mediaId) {
$found = false; $found = false;
@@ -205,7 +205,7 @@ class EventController extends ApiController
/** /**
* Delete a specific related attachment. * Delete a specific related attachment.
* @param Request $request The user request. * @param Request $request The user request.
* @param Post $post The model. * @param Article $article The model.
* @param Media $medium The attachment medium. * @param Media $medium The attachment medium.
* @return JsonResponse * @return JsonResponse
* @throws BindingResolutionException * @throws BindingResolutionException

View File

@@ -4,7 +4,7 @@ namespace App\Http\Requests;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
class PostRequest extends BaseRequest class ArticleRequest extends BaseRequest
{ {
/** /**
* Get the validation rules that apply to POST requests. * Get the validation rules that apply to POST requests.
@@ -14,7 +14,7 @@ class PostRequest extends BaseRequest
public function postRules() public function postRules()
{ {
return [ return [
'slug' => 'required|string|min:6|unique:posts', 'slug' => 'required|string|min:6|unique:articles',
'title' => 'required|string|min:6|max:255', 'title' => 'required|string|min:6|max:255',
'publish_at' => 'required|date', 'publish_at' => 'required|date',
'user_id' => 'required|uuid|exists:users,id', 'user_id' => 'required|uuid|exists:users,id',
@@ -34,7 +34,7 @@ class PostRequest extends BaseRequest
'slug' => [ 'slug' => [
'string', 'string',
'min:6', 'min:6',
Rule::unique('posts')->ignoreModel($this->post), Rule::unique('articles')->ignoreModel($this->article),
], ],
'title' => 'string|min:6|max:255', 'title' => 'string|min:6|max:255',
'publish_at' => 'date', 'publish_at' => 'date',

View File

@@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\Relations\MorphMany;
class Post extends Model class Article extends Model
{ {
use HasFactory; use HasFactory;
use Uuids; use Uuids;
@@ -28,7 +28,7 @@ class Post extends Model
/** /**
* Get the post user * Get the article user
* *
* @return BelongsTo * @return BelongsTo
*/ */
@@ -38,7 +38,7 @@ class Post extends Model
} }
/** /**
* Get all of the post's attachments. * Get all of the article's attachments.
* *
* @return MorphMany * @return MorphMany
*/ */

View File

@@ -34,7 +34,7 @@ class Event extends Model
/** /**
* Get all of the post's attachments. * Get all of the article's attachments.
*/ */
public function attachments() public function attachments()
{ {

View File

@@ -142,7 +142,7 @@ class User extends Authenticatable implements Auditable
* Revoke permissions from the user * Revoke permissions from the user
* *
* @param string|array $permissions The permission(s) to revoke. * @param string|array $permissions The permission(s) to revoke.
* @return int * @return integer
*/ */
public function revokePermission($permissions) public function revokePermission($permissions)
{ {
@@ -170,9 +170,9 @@ class User extends Authenticatable implements Auditable
* *
* @return HasMany * @return HasMany
*/ */
public function posts() public function articles()
{ {
return $this->hasMany(Post::class); return $this->hasMany(Article::class);
} }
/** /**

View File

@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Event> * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Event>
*/ */
class PostFactory extends Factory class ArticleFactory extends Factory
{ {
/** /**
* Define the model's default state. * Define the model's default state.

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::rename('posts', 'articles');
// Update permissions to use articles instead of posts
DB::table('permissions')->select('id', 'permission')->where('permission', 'admin/posts')->update(['permission' => 'admin/articles']);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::rename('articles', 'posts');
// Update permissions to use posts instead of articles
DB::table('permissions')->select('id', 'permission')->where('permission', 'admin/articles')->update(['permission' => 'admin/posts']);
}
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -33,7 +33,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from "vue"; import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import { api, getApiResultData } from "../helpers/api"; import { api, getApiResultData } from "../helpers/api";
import { PostCollection } from "../helpers/api.types"; import { ArticleCollection } from "../helpers/api.types";
import { mediaGetVariantUrl } from "../helpers/media"; import { mediaGetVariantUrl } from "../helpers/media";
import { excerpt } from "../helpers/string"; import { excerpt } from "../helpers/string";
import SMButton from "./SMButton.vue"; import SMButton from "./SMButton.vue";
@@ -70,30 +70,31 @@ onBeforeUnmount(() => {
const handleLoad = async () => { const handleLoad = async () => {
try { try {
let postsResult = await api.get({ let articlesResult = await api.get({
url: "/posts", url: "/articles",
params: { params: {
limit: 3, limit: 3,
}, },
}); });
const postsData = getApiResultData<PostCollection>(postsResult); const articlesData =
getApiResultData<ArticleCollection>(articlesResult);
if (postsData && postsData.posts) { if (articlesData && articlesData.articles) {
const randomIndex = Math.floor( const randomIndex = Math.floor(
Math.random() * postsData.posts.length Math.random() * articlesData.articles.length
); );
heroTitle.value = postsData.posts[randomIndex].title; heroTitle.value = articlesData.articles[randomIndex].title;
heroExcerpt.value = excerpt( heroExcerpt.value = excerpt(
postsData.posts[randomIndex].content, articlesData.articles[randomIndex].content,
200 200
); );
heroImageUrl.value = mediaGetVariantUrl( heroImageUrl.value = mediaGetVariantUrl(
postsData.posts[randomIndex].hero, articlesData.articles[randomIndex].hero,
"large" "large"
); );
heroImageTitle = postsData.posts[randomIndex].hero.title; heroImageTitle = articlesData.articles[randomIndex].hero.title;
heroSlug.value = postsData.posts[randomIndex].slug; heroSlug.value = articlesData.articles[randomIndex].slug;
heroStyles.value.backgroundImage = `linear-gradient(to right, rgba(0, 0, 0, 0.7),rgba(0, 0, 0, 0.2)),url('${heroImageUrl.value}')`; heroStyles.value.backgroundImage = `linear-gradient(to right, rgba(0, 0, 0, 0.7),rgba(0, 0, 0, 0.2)),url('${heroImageUrl.value}')`;

View File

@@ -68,7 +68,7 @@ export interface Article {
attachments: Array<Media>; attachments: Array<Media>;
} }
export interface Post { export interface Article {
id: string; id: string;
title: string; title: string;
slug: string; slug: string;
@@ -80,12 +80,12 @@ export interface Post {
attachments: Array<Media>; attachments: Array<Media>;
} }
export interface PostResponse { export interface ArticleResponse {
post: Post; article: Article;
} }
export interface PostCollection { export interface ArticleCollection {
posts: Array<Post>; articles: Array<Article>;
total: number; total: number;
} }

View File

@@ -197,37 +197,37 @@ export const routes = [
component: () => import("@/views/dashboard/Dashboard.vue"), component: () => import("@/views/dashboard/Dashboard.vue"),
}, },
{ {
path: "posts", path: "articles",
children: [ children: [
{ {
path: "", path: "",
name: "dashboard-post-list", name: "dashboard-article-list",
meta: { meta: {
title: "Posts", title: "Articles",
middleware: "authenticated", middleware: "authenticated",
}, },
component: () => component: () =>
import("@/views/dashboard/PostList.vue"), import("@/views/dashboard/ArticleList.vue"),
}, },
{ {
path: "create", path: "create",
name: "dashboard-post-create", name: "dashboard-article-create",
meta: { meta: {
title: "Create Post", title: "Create Article",
middleware: "authenticated", middleware: "authenticated",
}, },
component: () => component: () =>
import("@/views/dashboard/PostEdit.vue"), import("@/views/dashboard/ArticleEdit.vue"),
}, },
{ {
path: ":id", path: ":id",
name: "dashboard-post-edit", name: "dashboard-article-edit",
meta: { meta: {
title: "Edit Post", title: "Edit Article",
middleware: "authenticated", middleware: "authenticated",
}, },
component: () => component: () =>
import("@/views/dashboard/PostEdit.vue"), import("@/views/dashboard/ArticleEdit.vue"),
}, },
], ],
}, },
@@ -258,7 +258,7 @@ export const routes = [
path: ":id", path: ":id",
name: "dashboard-event-edit", name: "dashboard-event-edit",
meta: { meta: {
title: "Event Post", title: "Event",
middleware: "authenticated", middleware: "authenticated",
}, },
component: () => component: () =>

View File

@@ -3,11 +3,11 @@
class="thumbnail" class="thumbnail"
:style="{ backgroundImage: `url('${backgroundImageUrl}')` }"></div> :style="{ backgroundImage: `url('${backgroundImageUrl}')` }"></div>
<SMContainer narrow> <SMContainer narrow>
<h1 class="title">{{ post.title }}</h1> <h1 class="title">{{ article.title }}</h1>
<div class="author">By {{ post.user.username }}</div> <div class="author">By {{ article.user.username }}</div>
<div class="date">{{ formattedDate(post.publish_at) }}</div> <div class="date">{{ formattedDate(article.publish_at) }}</div>
<SMHTML :html="post.content" class="content" /> <SMHTML :html="article.content" class="content" />
<SMAttachments :attachments="post.attachments || []" /> <SMAttachments :attachments="article.attachments || []" />
</SMContainer> </SMContainer>
</template> </template>
@@ -17,7 +17,7 @@ import { useRoute } from "vue-router";
import SMAttachments from "../components/SMAttachments.vue"; import SMAttachments from "../components/SMAttachments.vue";
import SMHTML from "../components/SMHTML.vue"; import SMHTML from "../components/SMHTML.vue";
import { api } from "../helpers/api"; import { api } from "../helpers/api";
import { Post, PostCollection, User } from "../helpers/api.types"; import { Article, ArticleCollection, User } from "../helpers/api.types";
import { SMDate } from "../helpers/datetime"; import { SMDate } from "../helpers/datetime";
import { useApplicationStore } from "../store/ApplicationStore"; import { useApplicationStore } from "../store/ApplicationStore";
import { mediaGetVariantUrl } from "../helpers/media"; import { mediaGetVariantUrl } from "../helpers/media";
@@ -25,9 +25,9 @@ import { mediaGetVariantUrl } from "../helpers/media";
const applicationStore = useApplicationStore(); const applicationStore = useApplicationStore();
/** /**
* The post data. * The article data.
*/ */
let post: Ref<Post> = ref({ let article: Ref<Article> = ref({
title: "", title: "",
user: { username: "" }, user: { username: "" },
}); });
@@ -43,9 +43,9 @@ let pageError = ref(200);
let pageLoading = ref(false); let pageLoading = ref(false);
/** /**
* Post user. * Article user.
*/ */
let postUser: User | null = null; let articleUser: User | null = null;
/** /**
* Thumbnail image URL. * Thumbnail image URL.
@@ -62,25 +62,30 @@ const handleLoad = async () => {
try { try {
if (slug.length > 0) { if (slug.length > 0) {
let result = await api.get({ let result = await api.get({
url: "/posts/", url: "/articles",
params: { params: {
slug: `=${slug}`, slug: `=${slug}`,
limit: 1, limit: 1,
}, },
}); });
const data = result.data as PostCollection; const data = result.data as ArticleCollection;
if (data && data.posts && data.total && data.total > 0) { if (data && data.articles && data.total && data.total > 0) {
post.value = data.posts[0]; article.value = data.articles[0];
post.value.publish_at = new SMDate(post.value.publish_at, { article.value.publish_at = new SMDate(
format: "ymd", article.value.publish_at,
utc: true, {
}).format("yyyy/MM/dd HH:mm:ss"); format: "ymd",
utc: true,
}
).format("yyyy/MM/dd HH:mm:ss");
backgroundImageUrl.value = mediaGetVariantUrl(post.value.hero); backgroundImageUrl.value = mediaGetVariantUrl(
applicationStore.setDynamicTitle(post.value.title); article.value.hero
);
applicationStore.setDynamicTitle(article.value.title);
} else { } else {
pageError.value = 404; pageError.value = 404;
} }
@@ -140,7 +145,7 @@ handleLoad();
} }
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {
.page-post-view .heading-image { .page-article-view .heading-image {
height: #{calc(map-get($spacing, 3) * 10)}; height: #{calc(map-get($spacing, 3) * 10)};
} }
} }

View File

@@ -16,34 +16,34 @@
/></template> /></template>
</SMInput> </SMInput>
<SMLoading v-if="pageLoading" large /> <SMLoading v-if="pageLoading" large />
<SMNoItems v-else-if="posts.length == 0" text="No Articles Found" /> <SMNoItems v-else-if="articles.length == 0" text="No Articles Found" />
<template v-else> <template v-else>
<SMPagination <SMPagination
v-if="postsTotal > postsPerPage" v-if="articlesTotal > articlesPerPage"
v-model="postsPage" v-model="articlesPage"
:total="postsTotal" :total="articlesTotal"
:per-page="postsPerPage" /> :per-page="articlesPerPage" />
<div class="posts"> <div class="articles">
<router-link <router-link
:to="{ name: 'article', params: { slug: post.slug } }" :to="{ name: 'article', params: { slug: article.slug } }"
class="article-card" class="article-card"
v-for="(post, idx) in posts" v-for="(article, idx) in articles"
:key="idx"> :key="idx">
<div <div
class="thumbnail" class="thumbnail"
:style="{ :style="{
backgroundImage: `url(${mediaGetVariantUrl( backgroundImage: `url(${mediaGetVariantUrl(
post.hero, article.hero,
'medium' 'medium'
)})`, )})`,
}"></div> }"></div>
<div class="info"> <div class="info">
{{ post.user.display_name }} - {{ article.user.display_name }} -
{{ computedDate(post.publish_at) }} {{ computedDate(article.publish_at) }}
</div> </div>
<h3 class="title">{{ post.title }}</h3> <h3 class="title">{{ article.title }}</h3>
<p class="content"> <p class="content">
{{ excerpt(post.content) }} {{ excerpt(article.content) }}
</p> </p>
</router-link> </router-link>
</div> </div>
@@ -55,7 +55,7 @@
import { Ref, ref, watch } from "vue"; import { Ref, ref, watch } from "vue";
import SMPagination from "../components/SMPagination.vue"; import SMPagination from "../components/SMPagination.vue";
import { api } from "../helpers/api"; import { api } from "../helpers/api";
import { Post, PostCollection } from "../helpers/api.types"; import { Article, ArticleCollection } from "../helpers/api.types";
import { SMDate } from "../helpers/datetime"; import { SMDate } from "../helpers/datetime";
import { mediaGetVariantUrl } from "../helpers/media"; import { mediaGetVariantUrl } from "../helpers/media";
import SMMastHead from "../components/SMMastHead.vue"; import SMMastHead from "../components/SMMastHead.vue";
@@ -67,16 +67,16 @@ import SMNoItems from "../components/SMNoItems.vue";
const message = ref(""); const message = ref("");
const pageLoading = ref(true); const pageLoading = ref(true);
const posts: Ref<Post[]> = ref([]); const articles: Ref<Article[]> = ref([]);
const postsPerPage = 24; const articlesPerPage = 24;
let postsPage = ref(1); let articlesPage = ref(1);
let postsTotal = ref(0); let articlesTotal = ref(0);
let searchInput = ref(""); let searchInput = ref("");
const handleClickSearch = () => { const handleClickSearch = () => {
postsPage.value = 1; articlesPage.value = 1;
handleLoad(); handleLoad();
}; };
@@ -86,11 +86,11 @@ const handleClickSearch = () => {
const handleLoad = () => { const handleLoad = () => {
message.value = ""; message.value = "";
pageLoading.value = true; pageLoading.value = true;
posts.value = []; articles.value = [];
let params = { let params = {
limit: postsPerPage, limit: articlesPerPage,
page: postsPage.value, page: articlesPage.value,
}; };
if (searchInput.value.length > 0) { if (searchInput.value.length > 0) {
@@ -100,16 +100,16 @@ const handleLoad = () => {
} }
api.get({ api.get({
url: "/posts", url: "/articles",
params: params, params: params,
}) })
.then((result) => { .then((result) => {
const data = result.data as PostCollection; const data = result.data as ArticleCollection;
posts.value = data.posts; articles.value = data.articles;
postsTotal.value = data.total; articlesTotal.value = data.total;
posts.value.forEach((post) => { articles.value.forEach((article) => {
post.publish_at = new SMDate(post.publish_at, { article.publish_at = new SMDate(article.publish_at, {
format: "ymd", format: "ymd",
utc: true, utc: true,
}).format("yyyy/MM/dd HH:mm:ss"); }).format("yyyy/MM/dd HH:mm:ss");
@@ -132,7 +132,7 @@ const computedDate = (date) => {
}; };
watch( watch(
() => postsPage.value, () => articlesPage.value,
() => { () => {
handleLoad(); handleLoad();
} }
@@ -143,7 +143,7 @@ handleLoad();
<style lang="scss"> <style lang="scss">
.page-blog { .page-blog {
.posts { .articles {
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 30px; gap: 30px;
@@ -188,13 +188,13 @@ handleLoad();
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.page-blog .posts { .page-blog .articles {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
.page-blog .posts { .page-blog .articles {
grid-template-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
} }
} }

View File

@@ -1,8 +1,8 @@
<template> <template>
<SMPage <SMPage
class="page-post-edit" class="page-article-edit"
:page-error="pageError" :page-error="pageError"
permission="admin/posts"> permission="admin/articles">
<template #container> <template #container>
<h1>{{ page_title }}</h1> <h1>{{ page_title }}</h1>
<SMForm <SMForm
@@ -74,7 +74,7 @@ import SMButtonRow from "../../components/SMButtonRow.vue";
import SMInput from "../../components/SMInput.vue"; import SMInput from "../../components/SMInput.vue";
import SMInputAttachments from "../../components/SMInputAttachments.vue"; import SMInputAttachments from "../../components/SMInputAttachments.vue";
import { api } from "../../helpers/api"; import { api } from "../../helpers/api";
import { PostResponse, UserCollection } from "../../helpers/api.types"; import { ArticleResponse, UserCollection } from "../../helpers/api.types";
import { SMDate } from "../../helpers/datetime"; import { SMDate } from "../../helpers/datetime";
import { Form, FormControl } from "../../helpers/form"; import { Form, FormControl } from "../../helpers/form";
import { And, DateTime, Min, Required } from "../../helpers/validate"; import { And, DateTime, Min, Required } from "../../helpers/validate";
@@ -84,7 +84,7 @@ import { useUserStore } from "../../store/UserStore";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
const page_title = route.params.id ? "Edit Post" : "Create New Post"; const page_title = route.params.id ? "Edit Article" : "Create New Article";
let pageError = ref(200); let pageError = ref(200);
const authors = ref({}); const authors = ref({});
const attachments = ref([]); const attachments = ref([]);
@@ -122,7 +122,7 @@ const updateSlug = async () => {
} }
await api.get({ await api.get({
url: "/posts", url: "/articles",
params: { params: {
slug: slug, slug: slug,
}, },
@@ -149,33 +149,33 @@ const loadData = async () => {
if (route.params.id) { if (route.params.id) {
form.loading(true); form.loading(true);
let result = await api.get({ let result = await api.get({
url: "/posts/{id}", url: "/articles/{id}",
params: { params: {
id: route.params.id, id: route.params.id,
}, },
}); });
const data = result.data as PostResponse; const data = result.data as ArticleResponse;
if (data && data.post) { if (data && data.article) {
form.controls.title.value = data.post.title; form.controls.title.value = data.article.title;
form.controls.slug.value = data.post.slug; form.controls.slug.value = data.article.slug;
form.controls.user_id.value = data.post.user.id; form.controls.user_id.value = data.article.user.id;
form.controls.content.value = data.post.content; form.controls.content.value = data.article.content;
form.controls.publish_at.value = data.post.publish_at form.controls.publish_at.value = data.article.publish_at
? new SMDate(data.post.publish_at, { ? new SMDate(data.article.publish_at, {
format: "yMd", format: "yMd",
utc: true, utc: true,
}).format("dd/MM/yyyy HH:mm") }).format("dd/MM/yyyy HH:mm")
: ""; : "";
form.controls.content.value = data.post.content; form.controls.content.value = data.article.content;
form.controls.hero.value = data.post.hero.id; form.controls.hero.value = data.article.hero.id;
attachments.value = (data.post.attachments || []).map(function ( attachments.value = (data.article.attachments || []).map(
attachment function (attachment) {
) { return attachment.id.toString();
return attachment.id.toString(); }
}); );
} else { } else {
pageError.value = 404; pageError.value = 404;
} }
@@ -201,12 +201,12 @@ const handleSubmit = async () => {
hero: form.controls.hero.value, hero: form.controls.hero.value,
}; };
let post_id = ""; let article_id = "";
if (route.params.id) { if (route.params.id) {
post_id = route.params.id as string; article_id = route.params.id as string;
await api.put({ await api.put({
url: `/posts/{id}`, url: `/articles/{id}`,
params: { params: {
id: route.params.id, id: route.params.id,
}, },
@@ -214,32 +214,32 @@ const handleSubmit = async () => {
}); });
} else { } else {
let result = await api.post({ let result = await api.post({
url: "/posts", url: "/articles",
body: data, body: data,
}); });
if (result.data) { if (result.data) {
const data = result.data as PostResponse; const data = result.data as ArticleResponse;
post_id = data.post.id; article_id = data.article.id;
} }
} }
await api.put({ await api.put({
url: `/posts/${post_id}/attachments`, url: `/articles/${article_id}/attachments`,
body: { body: {
attachments: attachments.value, attachments: attachments.value,
}, },
}); });
useToastStore().addToast({ useToastStore().addToast({
title: route.params.id ? "Post Updated" : "Post Created", title: route.params.id ? "Article Updated" : "Article Created",
content: route.params.id content: route.params.id
? "The post has been updated." ? "The article has been updated."
: "The post has been created.", : "The article has been created.",
type: "success", type: "success",
}); });
router.push({ name: "dashboard-post-list" }); router.push({ name: "dashboard-article-list" });
} catch (error) { } catch (error) {
form.apiErrors(error); form.apiErrors(error);
} }

View File

@@ -1,11 +1,11 @@
<template> <template>
<SMPage permission="admin/posts" :page-error="pageError"> <SMPage permission="admin/articles" :page-error="pageError">
<template #container> <template #container>
<SMToolbar> <SMToolbar>
<template #left> <template #left>
<SMButton <SMButton
type="primary" type="primary"
label="Create Post" label="Create Article"
:small="true" :small="true"
@click="handleCreate" /> @click="handleCreate" />
</template> </template>
@@ -31,7 +31,7 @@
<template #item-title="item"> <template #item-title="item">
<router-link <router-link
:to="{ :to="{
name: 'dashboard-post-edit', name: 'dashboard-article-edit',
params: { id: item.id }, params: { id: item.id },
}" }"
>{{ item.title }}</router-link >{{ item.title }}</router-link
@@ -64,7 +64,7 @@ import SMInput from "../../components/SMInput.vue";
import SMLoadingIcon from "../../components/SMLoadingIcon.vue"; import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
import SMToolbar from "../../components/SMToolbar.vue"; import SMToolbar from "../../components/SMToolbar.vue";
import { api } from "../../helpers/api"; import { api } from "../../helpers/api";
import { PostCollection, PostResponse } from "../../helpers/api.types"; import { ArticleCollection, ArticleResponse } from "../../helpers/api.types";
import { SMDate } from "../../helpers/datetime"; import { SMDate } from "../../helpers/datetime";
import { debounce } from "../../helpers/debounce"; import { debounce } from "../../helpers/debounce";
import { useToastStore } from "../../store/ToastStore"; import { useToastStore } from "../../store/ToastStore";
@@ -103,7 +103,7 @@ const handleClick = (item, extra: string): void => {
}; };
/** /**
* Load the post data from the server. * Load the article data from the server.
*/ */
const loadFromServer = async () => { const loadFromServer = async () => {
formLoading.value = true; formLoading.value = true;
@@ -128,17 +128,17 @@ const loadFromServer = async () => {
} }
const result = await api.get({ const result = await api.get({
url: "/posts", url: "/articles",
params: params, params: params,
}); });
const data = result.data as PostCollection; const data = result.data as ArticleCollection;
if (!data || !data.posts) { if (!data || !data.articles) {
throw new Error("The server is currently not available"); throw new Error("The server is currently not available");
} }
items.value = data.posts; items.value = data.articles;
items.value.forEach((row) => { items.value.forEach((row) => {
if (row.created_at !== "undefined") { if (row.created_at !== "undefined") {
@@ -185,15 +185,15 @@ watch(search, () => {
}); });
const handleClickRow = (item) => { const handleClickRow = (item) => {
router.push({ name: "dashboard-post-edit", params: { id: item.id } }); router.push({ name: "dashboard-article-edit", params: { id: item.id } });
}; };
const handleCreate = () => { const handleCreate = () => {
router.push({ name: "dashboard-post-create" }); router.push({ name: "dashboard-article-create" });
}; };
const handleEdit = (item) => { const handleEdit = (item) => {
router.push({ name: "dashboard-post-edit", params: { id: item.id } }); router.push({ name: "dashboard-article-edit", params: { id: item.id } });
}; };
const handleDuplicate = async (item) => { const handleDuplicate = async (item) => {
@@ -223,7 +223,7 @@ const handleDuplicate = async (item) => {
const slug = `${originalSlug}-${number}`; const slug = `${originalSlug}-${number}`;
try { try {
await api.get({ await api.get({
url: `/posts/?slug=${slug}`, url: `/articles/?slug=${slug}`,
}); });
} catch (err) { } catch (err) {
if (err.status === 404) { if (err.status === 404) {
@@ -233,7 +233,7 @@ const handleDuplicate = async (item) => {
} else { } else {
useToastStore().addToast({ useToastStore().addToast({
title: "Server error", title: "Server error",
content: "The post could not be duplicated.", content: "The article could not be duplicated.",
type: "danger", type: "danger",
}); });
return; return;
@@ -245,28 +245,28 @@ const handleDuplicate = async (item) => {
} }
const result = await api.post({ const result = await api.post({
url: "/posts", url: "/articles",
body: item, body: item,
}); });
const data = result.data as PostResponse; const data = result.data as ArticleResponse;
loadFromServer(); loadFromServer();
useToastStore().addToast({ useToastStore().addToast({
title: "Post duplicated", title: "Article duplicated",
content: "The post was duplicated successfully.", content: "The article was duplicated successfully.",
type: "success", type: "success",
}); });
router.push({ router.push({
name: "dashboard-post-edit", name: "dashboard-article-edit",
params: { id: data.post.id }, params: { id: data.article.id },
}); });
} catch (err) { } catch (err) {
useToastStore().addToast({ useToastStore().addToast({
title: "Server error", title: "Server error",
content: "The post could not be duplicated.", content: "The article could not be duplicated.",
type: "danger", type: "danger",
}); });
} }
@@ -274,24 +274,24 @@ const handleDuplicate = async (item) => {
const handleDelete = async (item) => { const handleDelete = async (item) => {
let result = await openDialog(SMDialogConfirm, { let result = await openDialog(SMDialogConfirm, {
title: "Delete Post?", title: "Delete Article?",
text: `Are you sure you want to delete the post <strong>${item.title}</strong>?`, text: `Are you sure you want to delete the article <strong>${item.title}</strong>?`,
cancel: { cancel: {
type: "secondary", type: "secondary",
label: "Cancel", label: "Cancel",
}, },
confirm: { confirm: {
type: "danger", type: "danger",
label: "Delete Post", label: "Delete Article",
}, },
}); });
if (result == true) { if (result == true) {
try { try {
await api.delete(`posts${item.id}`); await api.delete(`articles${item.id}`);
loadFromServer(); loadFromServer();
formMessage.value.message = "Post deleted successfully"; formMessage.value.message = "Article deleted successfully";
formMessage.value.type = "success"; formMessage.value.type = "success";
} catch (err) { } catch (err) {
formMessage.value.message = err.response?.data?.message; formMessage.value.message = err.response?.data?.message;

View File

@@ -1,114 +0,0 @@
<template>
<SMPage>
<SMForm v-model="form" @submit="handleSubmit">
<SMRow>
<SMInput control="title" />
</SMRow>
<SMRow>
<SMEditor
id="content"
v-model="form.content.value"
@file-accept="fileAccept"
@attachment-add="attachmentAdd" />
</SMRow>
<SMRow>
<SMButton type="submit" label="Save" />
</SMRow>
</SMForm>
</SMPage>
</template>
<script setup lang="ts">
import { reactive } from "vue";
import { useRoute } from "vue-router";
import SMButton from "../../components/SMButton.vue";
import SMForm from "../../components/SMForm.vue";
import SMInput from "../../components/SMInput.vue";
import { api } from "../../helpers/api";
import { Form, FormControl } from "../../helpers/form";
import { And, Min, Required } from "../../helpers/validate";
const route = useRoute();
let form = reactive(
Form({
title: FormControl("", And([Required(), Min(2)])),
content: FormControl("", Required()),
})
);
// const getPostById = async () => {
// try {
// if (isValidated(formData)) {
// let res = await axios.get("posts/" + route.params.id);
// formData.title.value = res.data.title;
// formData.content.value = res.data.content;
// }
// } catch (err) {
// formMessage.icon = "";
// formMessage.type = "error";
// formMessage.message = "";
// restParseErrors(formData, [formMessage, "message"], err);
// }
// };
const handleSubmit = async () => {
try {
await api.post({
url: "/posts",
body: {
title: form.title.value,
content: form.content.value,
},
});
form.message("The post has been saved", "success");
} catch (error) {
form.apiError(error);
}
};
const fileAccept = (event) => {
if (event.file.type != "image/png") {
event.preventDefault();
}
};
const createStorageKey = (file) => {
var date = new Date();
var day = date.toISOString().slice(0, 10);
var name = date.getTime() + "-" + file.name;
return ["tmp", day, name].join("/");
};
const attachmentAdd = async (event) => {
if (event.attachment.file) {
const key = createStorageKey(event.attachment.file);
var fileFormData = new FormData();
fileFormData.append("key", key);
fileFormData.append("Content-Type", event.attachment.file.type);
fileFormData.append("file", event.attachment.file);
try {
let res = await axios.post("upload", fileFormData, {
headers: {
"Content-Type": "multipart/form-data",
},
onUploadProgress: (progressEvent) =>
event.attachment.setUploadProgress(
(progressEvent.loaded * progressEvent.total) / 100
),
});
event.attachment.setAttributes({
url: res.data.url,
href: res.data.url,
});
} catch (err) {
event.preventDefault();
}
}
};
</script>

View File

@@ -7,9 +7,9 @@
<h3>My Details</h3> <h3>My Details</h3>
</router-link> </router-link>
<router-link <router-link
v-if="userStore.permissions.includes('admin/posts')" v-if="userStore.permissions.includes('admin/articles')"
:to="{ name: 'dashboard-post-list' }" :to="{ name: 'dashboard-article-list' }"
class="admin-card posts"> class="admin-card articles">
<ion-icon name="newspaper-outline" /> <ion-icon name="newspaper-outline" />
<h3>Articles</h3> <h3>Articles</h3>
</router-link> </router-link>

View File

@@ -41,7 +41,7 @@
<SMColumn <SMColumn
><SMInput ><SMInput
type="checkbox" type="checkbox"
label="Edit Posts" label="Edit Articles"
v-model="permissions.users" v-model="permissions.users"
/></SMColumn> /></SMColumn>
<SMColumn <SMColumn

View File

@@ -7,7 +7,7 @@ use App\Http\Controllers\Api\EventController;
use App\Http\Controllers\Api\LogController; use App\Http\Controllers\Api\LogController;
use App\Http\Controllers\Api\MediaController; use App\Http\Controllers\Api\MediaController;
use App\Http\Controllers\Api\OCRController; use App\Http\Controllers\Api\OCRController;
use App\Http\Controllers\Api\PostController; use App\Http\Controllers\Api\ArticleController;
use App\Http\Controllers\Api\SubscriptionController; use App\Http\Controllers\Api\SubscriptionController;
use App\Http\Controllers\Api\UserController; use App\Http\Controllers\Api\UserController;
@@ -35,8 +35,8 @@ Route::post('/users/verifyEmail', [UserController::class, 'verifyEmail']);
Route::apiResource('media', MediaController::class); Route::apiResource('media', MediaController::class);
Route::get('media/{medium}/download', [MediaController::class, 'download']); Route::get('media/{medium}/download', [MediaController::class, 'download']);
Route::apiResource('posts', PostController::class); Route::apiResource('articles', ArticleController::class);
Route::apiAttachmentResource('posts', PostController::class); Route::apiAttachmentResource('articles', ArticleController::class);
Route::apiResource('events', EventController::class); Route::apiResource('events', EventController::class);
Route::apiAttachmentResource('events', EventController::class); Route::apiAttachmentResource('events', EventController::class);

View File

@@ -0,0 +1,136 @@
<?php
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Models\User;
use App\Models\Media;
use App\Models\Article;
use Faker\Factory as FakerFactory;
class ArticlesApiTest extends TestCase
{
use RefreshDatabase;
protected $faker;
public function setUp(): void
{
parent::setUp();
$this->faker = FakerFactory::create();
}
public function testAnyUserCanViewArticle()
{
// Create an event
$article = Article::factory()->create([
'publish_at' => $this->faker->dateTimeBetween('-2 months', '-1 month'),
]);
// Create a future event
$futureArticle = Article::factory()->create([
'publish_at' => $this->faker->dateTimeBetween('+1 month', '+2 months'),
]);
// Send GET request to the /api/articles endpoint
$response = $this->getJson('/api/articles');
$response->assertStatus(200);
// Assert that the event is in the response data
$response->assertJsonCount(1, 'articles');
$response->assertJsonFragment([
'id' => $article->id,
'title' => $article->title,
'content' => $article->content,
]);
$response->assertJsonMissing([
'id' => $futureArticle->id,
'title' => $futureArticle->title,
'content' => $futureArticle->content,
]);
}
public function testAdminCanCreateUpdateDeleteArticle()
{
// Create a user with the admin/events permission
$adminUser = User::factory()->create();
$adminUser->givePermission('admin/articles');
// Create media data
$media = Media::factory()->create(['user_id' => $adminUser->id]);
// Create event data
$articleData = Article::factory()->make([
'user_id' => $adminUser->id,
'hero' => $media->id,
])->toArray();
// Test creating event
$response = $this->actingAs($adminUser)->postJson('/api/articles', $articleData);
$response->assertStatus(201);
$this->assertDatabaseHas('articles', [
'title' => $articleData['title'],
'content' => $articleData['content'],
]);
// Test viewing event
$article = Article::where('title', $articleData['title'])->first();
$response = $this->get("/api/articles/$article->id");
$response->assertStatus(200);
$response->assertJsonStructure([
'article' => [
'id',
'title',
'content',
]
]);
// Test updating event
$articleData['title'] = 'Updated Article';
$response = $this->actingAs($adminUser)->putJson("/api/articles/$article->id", $articleData);
$response->assertStatus(200);
$this->assertDatabaseHas('articles', [
'title' => 'Updated Article',
]);
// Test deleting event
$response = $this->actingAs($adminUser)->delete("/api/articles/$article->id");
$response->assertStatus(204);
$this->assertDatabaseMissing('articles', [
'title' => 'Updated Article',
]);
}
public function testNonAdminCannotCreateUpdateDeleteArticle()
{
// Create a user without admin/events permission
$user = User::factory()->create();
// Authenticate as the user
$this->actingAs($user);
// Try to create a new article
$media = Media::factory()->create(['user_id' => $user->id]);
$newArticleData = Article::factory()->make(['user_id' => $user->id, 'hero' => $media->id])->toArray();
$response = $this->postJson('/api/articles', $newArticleData);
$response->assertStatus(403);
// Try to update an event
$article = Article::factory()->create();
$updatedArticleData = [
'title' => 'Updated Event',
'content' => 'This is an updated event.',
// Add more fields as needed
];
$response = $this->putJson('/api/articles/' . $article->id, $updatedArticleData);
$response->assertStatus(403);
// Try to delete an event
$article = Article::factory()->create();
$response = $this->deleteJson('/api/articles/' . $article->id);
$response->assertStatus(403);
}
}

View File

@@ -1,134 +0,0 @@
<?php
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Models\User;
use App\Models\Media;
use App\Models\Post;
use Faker\Factory as FakerFactory;
class PostsApiTest extends TestCase
{
use RefreshDatabase;
protected $faker;
public function setUp(): void
{
parent::setUp();
$this->faker = FakerFactory::create();
}
public function testAnyUserCanViewPost()
{
// Create an event
$post = Post::factory()->create([
'publish_at' => $this->faker->dateTimeBetween('-2 months', '-1 month'),
]);
// Create a future event
$futurePost = Post::factory()->create([
'publish_at' => $this->faker->dateTimeBetween('+1 month', '+2 months'),
]);
// Send GET request to the /api/posts endpoint
$response = $this->getJson('/api/posts');
$response->assertStatus(200);
// Assert that the event is in the response data
$response->assertJsonCount(1, 'posts');
$response->assertJsonFragment([
'id' => $post->id,
'title' => $post->title,
'content' => $post->content,
]);
$response->assertJsonMissing([
'id' => $futurePost->id,
'title' => $futurePost->title,
'content' => $futurePost->content,
]);
}
public function testAdminCanCreateUpdateDeletePost()
{
// Create a user with the admin/events permission
$adminUser = User::factory()->create();
$adminUser->givePermission('admin/posts');
// Create media data
$media = Media::factory()->create(['user_id' => $adminUser->id]);
// Create event data
$postData = Post::factory()->make([
'user_id' => $adminUser->id,
'hero' => $media->id,
])->toArray();
// Test creating event
$response = $this->actingAs($adminUser)->postJson('/api/posts', $postData);
$response->assertStatus(201);
$this->assertDatabaseHas('posts', [
'title' => $postData['title'],
'content' => $postData['content'],
]);
// Test viewing event
$post = Post::where('title', $postData['title'])->first();
$response = $this->get("/api/posts/$post->id");
$response->assertStatus(200);
$response->assertJsonStructure([
'post' => [
'id',
'title',
'content',
]
]);
// Test updating event
$postData['title'] = 'Updated Post';
$response = $this->actingAs($adminUser)->putJson("/api/posts/$post->id", $postData);
$response->assertStatus(200);
$this->assertDatabaseHas('posts', [
'title' => 'Updated Post',
]);
// Test deleting event
$response = $this->actingAs($adminUser)->delete("/api/posts/$post->id");
$response->assertStatus(204);
$this->assertDatabaseMissing('posts', [
'title' => 'Updated Post',
]);
}
public function testNonAdminCannotCreateUpdateDeletePost()
{
// Create a user without admin/events permission
$user = User::factory()->create();
// Authenticate as the user
$this->actingAs($user);
// Try to create a new post
$media = Media::factory()->create(['user_id' => $user->id]);
$newPostData = Post::factory()->make(['user_id' => $user->id, 'hero' => $media->id])->toArray();
$response = $this->postJson('/api/posts', $newPostData);
$response->assertStatus(403);
// Try to update an event
$post = Post::factory()->create();
$updatedPostData = [
'title' => 'Updated Event',
'content' => 'This is an updated event.',
// Add more fields as needed
];
$response = $this->putJson('/api/posts/' . $post->id, $updatedPostData);
$response->assertStatus(403);
// Try to delete an event
$post = Post::factory()->create();
$response = $this->deleteJson('/api/posts/' . $post->id);
$response->assertStatus(403);
}
}