diff --git a/app/Filters/EventFilters.php b/app/Filters/EventFilters.php index 05d9478b..a0497dca 100644 --- a/app/Filters/EventFilters.php +++ b/app/Filters/EventFilters.php @@ -30,7 +30,7 @@ public function promoter(?string $value = null): Builder { if (isset($value)) { return $this->builder->whereHas('promoter', function ($q) use ($value) { - $q->where('name', '=', ucfirst($value)); + $q->where('name','like', '%'.$value.'%'); }); } else { return $this->builder; diff --git a/app/Http/Controllers/Api/BlogsController.php b/app/Http/Controllers/Api/BlogsController.php new file mode 100644 index 00000000..569def56 --- /dev/null +++ b/app/Http/Controllers/Api/BlogsController.php @@ -0,0 +1,486 @@ +filter = $filter; + + // prefix for session storage + $this->prefix = 'app.blogs.'; + + // default list variables + $this->defaultLimit = 10; + $this->defaultSort = 'created_at'; + $this->defaultSortDirection = 'desc'; + $this->defaultSortCriteria = ['blogs.created_at' => 'desc']; + + $this->limit = $this->defaultLimit; + $this->sort = $this->defaultSort; + $this->sortDirection = $this->defaultSortDirection; + + $this->hasFilter = false; + + parent::__construct(); + } + + /** + * Display a listing of the resource. + */ + public function index( + Request $request, + ListParameterSessionStore $listParamSessionStore, + ListEntityResultBuilder $listEntityResultBuilder + ): string { + // initialized listParamSessionStore with baseindex key + $listParamSessionStore->setBaseIndex('internal_blog'); + $listParamSessionStore->setKeyPrefix('internal_blog_index'); + + // set the index tab in the session + $listParamSessionStore->setIndexTab(action([BlogsController::class, 'index'])); + + // create the base query including any required joins; needs select to make sure only event entities are returned + $baseQuery = Blog::query()->select('blogs.*'); + + $listEntityResultBuilder + ->setFilter($this->filter) + ->setQueryBuilder($baseQuery) + ->setDefaultLimit($this->defaultLimit) + ->setDefaultSort($this->defaultSortCriteria); + + // get the result set from the builder + $listResultSet = $listEntityResultBuilder->listResultSetFactory(); + + // get the query builder + $query = $listResultSet->getList(); + + // get the blogs + $blogs = $query->paginate($listResultSet->getLimit()); + + return response()->json(new BlogCollection($blogs)); + } + + /** + * Display a listing of the resource. + */ + public function filter( + Request $request, + ListParameterSessionStore $listParamSessionStore, + ListEntityResultBuilder $listEntityResultBuilder + ): string { + // initialized listParamSessionStore with baseindex key + $listParamSessionStore->setBaseIndex('internal_blog'); + $listParamSessionStore->setKeyPrefix('internal_blog_index'); + + // set the index tab in the session + $listParamSessionStore->setIndexTab(action([BlogsController::class, 'index'])); + + // create the base query including any required joins; needs select to make sure only event entities are returned + $baseQuery = Blog::query()->select('blogs.*'); + + $listEntityResultBuilder + ->setFilter($this->filter) + ->setQueryBuilder($baseQuery) + ->setDefaultLimit($this->defaultLimit) + ->setDefaultSort($this->defaultSortCriteria); + + // get the result set from the builder + $listResultSet = $listEntityResultBuilder->listResultSetFactory(); + + // get the query builder + $query = $listResultSet->getList(); + + // query and paginate the blogs + /* @phpstan-ignore-next-line */ + $blogs = $query->visible($this->user)->paginate($listResultSet->getLimit()); + + // saves the updated session + $listParamSessionStore->save(); + + $this->hasFilter = $listResultSet->getFilters() != $listResultSet->getDefaultFilters() || $listResultSet->getIsEmptyFilter(); + + return view('blogs.index') + ->with(array_merge( + [ + 'limit' => $listResultSet->getLimit(), + 'sort' => $listResultSet->getSort(), + 'direction' => $listResultSet->getSortDirection(), + 'hasFilter' => $this->hasFilter, + 'filters' => $listResultSet->getFilters(), + ], + $this->getFilterOptions(), + $this->getListControlOptions() + )) + ->with(compact('blogs')) + ->render(); + } + + /** + * Show the form for creating a new resource. + */ + public function create(): View + { + $blog = new Blog(); + $blog->contentType = ContentType::find(ContentType::PLAIN_TEXT); + $blog->visibility = Visibility::find(Visibility::VISIBILITY_PUBLIC); + + return view('blogs.create', compact('blog')) + ->with($this->getFormOptions()); + } + + /** + * Store a newly created resource in storage. + * + * @internal param Request $request + */ + public function store(BlogRequest $request, Blog $blog): RedirectResponse + { + // TODO change this to use the trust_blog permission to allow html + if (auth()->id() === config('app.superuser')) { + $allow_html = 1; + } else { + $allow_html = 0; + } + + $msg = ''; + + $input = $request->all(); + + $blog = $blog->create($input); + + flash()->success('Success', 'Your blog has been created'); + + // here, notify anybody following the blog + // $this->notifyFollowing($blog); + + // add to activity log + Activity::log($blog, $this->user, Action::CREATE); + + return redirect()->route('blogs.index'); + } + + /** + * @param Blog $blog + */ + protected function notifyFollowing($blog): RedirectResponse + { + $reply_email = config('app.noreplyemail'); + $site = config('app.app_name'); + $url = config('app.url'); + + // notify users following any of the tags + $tags = $blog->tags()->get(); + $users = []; + + // notify users following any tags related to the blog + + foreach ($tags as $tag) { + foreach ($tag->followers() as $user) { + // if the user hasn't already been notified, then email them + if (!array_key_exists($user->id, $users)) { + Mail::send('emails.following-thread', ['user' => $user, 'blog' => $blog, 'object' => $tag, 'reply_email' => $reply_email, 'site' => $site, 'url' => $url], function ($m) use ($user, $blog, $tag, $reply_email, $site) { + $m->from($reply_email, $site); + + $m->to($user->email, $user->name)->subject($site.': '.$tag->name.' :: '.$blog->created_at->format('D F jS').' '.$blog->name); + }); + $users[$user->id] = $tag->name; + } + } + } + + return back(); + } + + /** + * Display the specified resource. + * + * @internal param int $id + */ + public function show(Blog $blog): JsonResponse + { + app('redirect')->setIntendedUrl(url()->current()); + + return response()->json(new BlogResource($blog)); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(Blog $blog): View + { + $this->middleware('auth'); + + return view('blogs.edit', compact('blog')) + ->with($this->getFormOptions()); + } + + /** + * Update the specified resource in storage. + */ + public function update(Blog $blog, BlogRequest $request): RedirectResponse + { + $msg = ''; + + $blog->fill($request->input())->save(); + + if (!$blog->ownedBy($this->user)) { + $this->unauthorized($request); + } + + $tagArray = $request->input('tag_list', []); + $syncArray = []; + + // check the elements in the tag list, and if any don't match, add the tag + foreach ($tagArray as $key => $tag) { + if (!Tag::find($tag)) { + $newTag = new Tag(); + $newTag->name = ucwords(strtolower($tag)); + $newTag->slug = Str::slug($tag); + $newTag->tag_type_id = 1; + $newTag->save(); + + // log adding of new tag + Activity::log($newTag, $this->user, Action::CREATE); + + $syncArray[strtolower($tag)] = $newTag->id; + + $msg .= ' Added tag '.$tag.'.'; + } else { + $syncArray[$key] = $tag; + } + } + + $blog->tags()->sync($syncArray); + $blog->entities()->sync($request->input('entity_list', [])); + + // add to activity log + Activity::log($blog, $this->user, Action::UPDATE); + + flash('Success', 'Your blog has been updated'); + + return redirect()->route('blogs.index'); + } + + /** + * Remove the specified resource from storage. + * + * @throws \Exception + * + * @internal param int $id + */ + public function destroy(Blog $blog): RedirectResponse + { + if ($this->user->cannot('destroy', $blog)) { + flash('Error', 'Your are not authorized to delete the blog.'); + + return redirect()->route('blogs.index'); + } + + // add to activity log + Activity::log($blog, $this->user, Action::DELETE); + + $blog->delete(); + + flash()->success('Success', 'Your blog has been deleted!'); + + return redirect()->route('blogs.index'); + } + + /** + * Mark user as liking the blog. + */ + public function like(int $id): RedirectResponse + { + // check if there is a logged in user + if (!$this->user) { + flash()->error('Error', 'No user is logged in.'); + + return back(); + } + + if (!$blog = Blog::find($id)) { + flash()->error('Error', 'No such blog'); + + return back(); + } + + // add the like response + $like = new Like(); + $like->object_id = $id; + $like->user()->associate($this->user); + $like->object_type = 'blog'; + $like->save(); + + // update the likes + ++$blog->likes; + $blog->save(); + + Log::info('User '.$id.' is liking '.$blog->name); + + flash()->success('Success', 'You are now liking the selected blog.'); + + return back(); + } + + /** + * Mark user as unliking the blog. + */ + public function unlike(int $id): RedirectResponse + { + // check if there is a logged in user + if (!$this->user) { + flash()->error('Error', 'No user is logged in.'); + + return back(); + } + + if (!$blog = Blog::find($id)) { + flash()->error('Error', 'No such blog'); + + return back(); + } + + // update the likes + --$blog->likes; + $blog->save(); + + // delete the like + $response = Like::where('object_id', '=', $id)->where('user_id', '=', $this->user->id)->where('object_type', '=', 'blog')->first(); + $response->delete(); + + flash()->success('Success', 'You are no longer liking the blog.'); + + return back(); + } + + /** + * Reset the rpp, sort, order. + * + * @throws \Throwable + */ + public function rppReset( + Request $request, + ListParameterSessionStore $listParamSessionStore + ): RedirectResponse { + // set the rpp, sort, direction only to default values + $keyPrefix = $request->get('key') ?? 'internal_blog_index'; + $listParamSessionStore->setBaseIndex('internal_blog'); + $listParamSessionStore->setKeyPrefix($keyPrefix); + + // clear all sort + $listParamSessionStore->clearSort(); + + return redirect()->route('blogs.index'); + } + + /** + * Reset the filtering of blogs. + */ + public function reset( + Request $request, + ListParameterSessionStore $listParamSessionStore + ): RedirectResponse { + // set filters and list controls to default values + $keyPrefix = $request->get('key') ?? 'internal_blog_index'; + $listParamSessionStore->setBaseIndex('internal_blog'); + $listParamSessionStore->setKeyPrefix($keyPrefix); + + // clear + $listParamSessionStore->clearFilter(); + $listParamSessionStore->clearSort(); + + return redirect()->route($request->get('redirect') ?? 'blogs.index'); + } + + protected function unauthorized(Request $request): RedirectResponse | Response + { + if ($request->ajax()) { + return response(['message' => 'No way.'], 403); + } + + Session::flash('flash_message', 'Not authorized'); + + return redirect('/'); + } + + protected function getListControlOptions(): array + { + return [ + 'limitOptions' => [5 => 5, 10 => 10, 25 => 25, 100 => 100, 1000 => 1000], + 'sortOptions' => ['blogs.name' => 'Name', 'blogs.created_at' => 'Created At'], + 'directionOptions' => ['asc' => 'asc', 'desc' => 'desc'], + ]; + } + + protected function getFilterOptions(): array + { + return [ + 'userOptions' => ['' => ' '] + User::orderBy('name', 'ASC')->pluck('name', 'name')->all(), + 'tagOptions' => ['' => ' '] + Tag::orderBy('name', 'ASC')->pluck('name', 'slug')->all(), + ]; + } + + protected function getFormOptions(): array + { + return [ + 'visibilityOptions' => ['' => ''] + Visibility::pluck('name', 'id')->all(), + 'tagOptions' => Tag::orderBy('name', 'ASC')->pluck('name', 'id')->all(), + 'entityOptions' => Entity::orderBy('name', 'ASC')->pluck('name', 'id')->all(), + 'menuOptions' => ['' => ''] + Menu::orderBy('name', 'ASC')->pluck('name', 'id')->all(), + 'contentTypeOptions' => ['' => ''] + ContentType::orderBy('name', 'ASC')->pluck('name', 'id')->all(), + ]; + } +} diff --git a/app/Http/Controllers/Api/EntitiesController.php b/app/Http/Controllers/Api/EntitiesController.php index a7403d8c..2f00977b 100644 --- a/app/Http/Controllers/Api/EntitiesController.php +++ b/app/Http/Controllers/Api/EntitiesController.php @@ -24,9 +24,6 @@ use App\Services\SessionStore\ListParameterSessionStore; use App\Services\StringHelper; use Carbon\Carbon; -use DOMDocument; -use DOMXPath; -use Illuminate\Contracts\View\View; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; @@ -64,7 +61,6 @@ class EntitiesController extends Controller public function __construct(EntityFilters $filter) { - // $this->middleware('auth', ['only' => ['store', 'update', 'follow']]); $this->filter = $filter; // prefix for session storage diff --git a/app/Http/Resources/BlogCollection.php b/app/Http/Resources/BlogCollection.php new file mode 100644 index 00000000..709b7f77 --- /dev/null +++ b/app/Http/Resources/BlogCollection.php @@ -0,0 +1,36 @@ + $this->currentPage(), + 'data' => $this->collection->toArray(), + 'first_page_url' => $this->url(1), + 'from' => $this->firstItem(), + 'last_page' => $this->lastPage(), + 'last_page_url' => $this->url($this->lastPage()), + 'next_page_url' => $this->nextPageUrl(), + 'path' => $this->path(), + 'per_page' => $this->perPage(), + 'prev_page_url' => $this->previousPageUrl(), + 'to' => $this->lastItem(), + 'total' => $this->total(), + ]; + } +} diff --git a/app/Http/Resources/BlogResource.php b/app/Http/Resources/BlogResource.php new file mode 100644 index 00000000..228811f9 --- /dev/null +++ b/app/Http/Resources/BlogResource.php @@ -0,0 +1,34 @@ + $this->id, + 'name' => $this->name, + 'slug' => $this->slug, + 'visibility_id' => $this->visibility_id, + 'content_type_id' => $this->content_type_id, + 'body' => $this->body, + 'menu_id' => $this->menu_id, + 'sort_order' => $this->sort_order, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at + ]; + } +} \ No newline at end of file diff --git a/public/postman/schemas/api.yml b/public/postman/schemas/api.yml index 20eeae5c..dc47e265 100644 --- a/public/postman/schemas/api.yml +++ b/public/postman/schemas/api.yml @@ -54,6 +54,76 @@ components: type: http scheme: basic schemas: + Blog: + type: object + required: + - name + properties: + id: + type: integer + readOnly: true + example: 1 + name: + type: string + maxLength: 255 + description: A name of an blog + example: Group + slug: + type: string + maxLength: 255 + description: A unique identifier name for the blog in kebab-case + example: group-slug + description: + type: string + example: Here is a short blog + description: A brief description of the blog + maxLength: 255 + visibility_id: + type: integer + readOnly: true + example: 1 + description: Relation to the visibility table that defines the visibility of the blog + content_type_id: + type: integer + readOnly: true + example: 1 + description: Relation to the content type of the blog + body: + type: string + example: Here is a short blog + description: Main body of the blog + maxLength: 65535 + menu_id: + type: integer + readOnly: true + example: 1 + description: Relation to the menu of the blog + sort_order: + type: string + example: ASC + description: Sort order of the blog + maxLength: 255 + created_at: + type: string + example: "2018-03-20T09:12:28Z" + format: date-time + description: Date and time that the entity type was created + readOnly: true + updated_at: + type: string + example: "2018-03-20T09:12:28Z" + format: date-time + description: Date and time that the entity type was last updated + readOnly: true + Blogs: + allOf: [$ref: "#/components/schemas/Pagination"] + type: object + properties: + data: + type: array + description: List of the current page of blogs + items: + "$ref": "#/components/schemas/Blog" Embeds: allOf: [$ref: "#/components/schemas/Pagination"] type: object @@ -1647,6 +1717,126 @@ components: security: - basicAuth: [] paths: + /api/blogs: + get: + tags: + - blogs + summary: Get Blogs + operationId: getBlogs + parameters: + - name: filters[name] + in: query + required: false + description: A filter query of the blog name + schema: + type: string + example: Blog Title + - name: sort + in: query + required: false + description: A column to be used to sort the query + schema: + type: string + example: name + - name: direction + in: query + required: false + description: A string indicating the sort direction of the query (asc or desc) + schema: + type: string + example: asc + responses: + "200": + description: Successful response + content: + application/json: + schema: + $ref: "#/components/schemas/Pagination" + post: + tags: + - blogs + summary: Create blog + operationId: createBlog + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + responses: + "201": + description: Successful response + content: + application/json: {} + /api/blogs/{blogId}: + get: + parameters: + - name: blogId + description: The unique identifier of the blog + in: path + required: true + schema: + description: The unique identifier of an blog + type: integer + readOnly: true + example: 1 + tags: + - blogs + summary: Get Blog + operationId: getBlogById + responses: + "200": + description: Successful response + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + put: + parameters: + - name: blogId + description: The unique identifier of the blog + in: path + required: true + schema: + description: The unique identifier of a blog + type: integer + readOnly: true + example: 1 + tags: + - blogs + summary: Update Blog + operationId: updateBlogById + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + responses: + "200": + description: Successful response + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + delete: + parameters: + - name: blogId + description: The unique identifier of the blog + in: path + required: true + schema: + description: The unique identifier of a blog + type: integer + readOnly: true + example: 1 + tags: + - blogs + summary: Delete Blog + operationId: deleteBlogById + responses: + "204": + description: Successful response + content: + application/json: {} /api/events: post: tags: diff --git a/routes/api.php b/routes/api.php index ff24e27a..748b430c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -24,11 +24,15 @@ return ['token' => $token->plainTextToken]; }); + + Route::match(['get', 'post'], 'blogs/filter', ['as' => 'blogss.filter', 'uses' => 'Api\BlogsController@filter']); + Route::get('blogs/reset', ['as' => 'links.reset', 'uses' => 'Api\BlogsController@reset']); + Route::get('blogs/rpp-reset', ['as' => 'blogs.rppReset', 'uses' => 'Api\BlogsController@rppReset']); + Route::resource('blogs', 'Api\BlogsController'); Route::get('events/{event}/embeds', ['as' => 'events.embeds', 'uses' => 'Api\EventsController@embeds']); Route::get('events/reset', ['as' => 'events.reset', 'uses' => 'Api\EventsController@reset']); - Route::get('events/reset', ['as' => 'events.reset', 'uses' => 'Api\EventsController@reset']); Route::get('events/rpp-reset', ['as' => 'events.rppReset', 'uses' => 'Api\EventsController@rppReset']); Route::resource('events', 'Api\EventsController');