vendor and env first commit

This commit is contained in:
2025-03-28 08:52:46 +01:00
parent f8388bc81b
commit 8f26283832
10976 changed files with 1349952 additions and 2 deletions
@@ -0,0 +1,14 @@
.DS_Store
/node_modules
/vendor
/.idea
/.vscode
/.vagrant
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
.env
.phpunit.result.cache
.php_cs.cache
composer.lock
+38
View File
@@ -0,0 +1,38 @@
{
"name": "vanguardapp/announcements",
"description": "Announcements plugin for Vanguard",
"keywords": [
"laravel",
"vanguard",
"announcements"
],
"license": "MIT",
"authors": [
{
"name": "Milos Stojanovic",
"email": "stojanovic.loshmi@gmail.com",
"homepage": "https://mstojanovic.net",
"role": "Developer"
}
],
"require": {
"php": "^8.0.2|^8.1|^8.2",
"vanguardapp/plugins": "^6.0",
"spatie/laravel-query-builder": "^5.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5.10"
},
"autoload": {
"psr-4": {
"Vanguard\\Announcements\\": "src/",
"Vanguard\\Announcements\\Database\\Seeders\\": "database/seeders/",
"Vanguard\\Announcements\\Database\\Factories\\": "database/factories/"
}
},
"autoload-dev": {
"psr-4": {
"Vanguard\\Announcements\\Tests\\": "tests/"
}
}
}
@@ -0,0 +1,30 @@
<?php
namespace Vanguard\Announcements\Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Vanguard\Announcements\Announcement;
class AnnouncementFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Announcement::class;
/**
* Define the model's default state.
*/
public function definition(): array
{
return [
'title' => $this->faker->title,
'body' => $this->faker->paragraph(2),
'user_id' => function () {
return \Vanguard\User::factory()->create()->id;
},
];
}
}
@@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateAnnouncementsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('announcements', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedInteger('user_id');
$table->string('title');
$table->text('body');
$table->timestamps();
$table->foreign('user_id')
->references('id')->on('users')
->onDelete('cascade');
});
Schema::table('users', function (Blueprint $table) {
$table->timestamp('announcements_last_read_at')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('announcements_last_read_at');
});
Schema::table('announcements', function (Blueprint $table) {
$table->dropForeign('announcements_user_id_foreign');
});
Schema::dropIfExists('announcements');
}
}
@@ -0,0 +1,34 @@
<?php
namespace Vanguard\Announcements\Database\Seeders;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Seeder;
use Vanguard\Permission;
use Vanguard\Role;
class AnnouncementsDatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Model::unguard();
$permission = Permission::create([
'name' => 'announcements.manage',
'display_name' => 'Manage Announcements',
'description' => '',
'removable' => false,
]);
Role::where('name', 'Admin')
->first()
->attachPermission($permission);
Model::reguard();
}
}
@@ -0,0 +1 @@
.nav-item.announcements a.nav-icon{font-size:1.3rem}.nav-item.announcements a.nav-icon:hover .activity-badge{-webkit-transform:scale(1.1);transform:scale(1.1)}.nav-item.announcements .activity-badge{width:13px;height:13px;background:#bf5329;border:2px solid #fff;border-radius:10px;position:absolute;right:-1px;-webkit-transition:all .15s;transition:all .15s;-webkit-transform:scale(.85);transform:scale(.85)}.nav-item.announcements .navbar-item{border-bottom:1px solid #e0ebf0}.nav-item.announcements .navbar-item:last-of-type{border-bottom:0}
@@ -0,0 +1 @@
!function(n){var e={};function t(o){if(e[o])return e[o].exports;var r=e[o]={i:o,l:!1,exports:{}};return n[o].call(r.exports,r,r.exports,t),r.l=!0,r.exports}t.m=n,t.c=e,t.d=function(n,e,o){t.o(n,e)||Object.defineProperty(n,e,{configurable:!1,enumerable:!0,get:o})},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,"a",e),e},t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.p="/",t(t.s=0)}({0:function(n,e,t){t("sV/x"),n.exports=t("xZZD")},"sV/x":function(n,e){$("#announcements-icon").click(function(){$.post(read_announcements_endpoint),$(".announcements .activity-badge").remove()})},xZZD:function(n,e){}});
@@ -0,0 +1,4 @@
{
"/js/announcements.js": "/js/announcements.js?id=e58568e3bf6dd42c45cd",
"/css/announcements.css": "/css/announcements.css?id=96053a4526b9aad79d23"
}
+11091
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"cross-env": "^5.1.4",
"laravel-mix": "^2.0"
}
}
+97
View File
@@ -0,0 +1,97 @@
📢 Announcements plugin for [Vanguard - Advanced PHP Login and User Management](https://vanguardapp.io)
system.
## Installation
This plugin requires Vanguard `4.0.0` or greater.
### Installation via Composer
To install the plugin first you will need to pull it via composer
by running the following command
```
composer require vanguardapp/announcements
```
The composer will install the plugin for you as well as it's dependencies.
The next step is to register the plugin by adding the
`\Vanguard\Announcements\Announcements::class`
to the list of Vanguard plugins inside the `VanguardServiceProvider`:
```php
protected function plugins()
{
return [
//...
\Vanguard\Announcements\Announcements::class,
];
}
```
As soon as your plugin is registered, you should publish the
plugins static assets and migrations by running the following command:
```
php artisan vendor:publish --provider="Vanguard\Announcements\Announcements" --tag="public" --tag="migrations"
```
And, as the last step of the installation, you will need to
run the following commands to make all the necessary database modifications:
```
php artisan migrate
php artisan db:seed --class="AnnouncementsDatabaseSeeder"
```
At this point the plugin will be fully installed and ready to go.
### Manual Installation
If you plan to make the modifications to the plugin and customize it to
fit your needs, it's much easier if you add it to your project manually.
To do so, you will need to download the ZIP archive from GitHub
by clicking the green "Clone or download" button and then choosing
the "Download ZIP" option from the dropdown.
Once you have the ZIP file on your computer, extract it to the
`plugins/Announcements` folder (you will need to create this folder
since it probably won't be present in your Vanguard installation).
Next step is to update your main `composer.json` file located in
Vanguard's root directory and add the following object to the `repositories`
array:
```
{
"type": "path",
"url": "./plugins/Announcements"
}
```
This will tell the composer that your plugin is located in `/plugins/Announcements`
directory and that it should be installed from there.
Now, add the following to the composer's `require` section
```
"vanguardapp/announcements": "*"
```
And run `composer update`.
Composer will now install the plugin from your local directory instead
of pulling it from GitHub, which means that you will be able to make
the changes to the plugin itself and customize it to fit your needs.
The rest of the process is the same as when the plugin is installed
by directly fetching it via composer from the GitHub repository, so you
will need to do all the same steps as above, which in short involves
updating the `VanguardServiceProvider` and running the commands to
publish plugin's static assets and to update the database.
## License
This plugin is an open-source software licensed under the [MIT license](https://opensource.org/licenses/MIT).
@@ -0,0 +1,4 @@
$("#announcements-icon").click(function () {
$.post(read_announcements_endpoint);
$(".announcements .activity-badge").remove();
});
@@ -0,0 +1,31 @@
.nav-item.announcements {
a.nav-icon {
font-size: 1.3rem;
&:hover .activity-badge {
transform: scale(1.1);
}
}
.activity-badge {
width: 13px;
height: 13px;
background: #bf5329;
border: 2px solid #fff;
border-radius: 10px;
position: absolute;
right: -1px;
-webkit-transition: all 0.15s;
transition: all 0.15s;
-webkit-transform: scale(0.85);
transform: scale(0.85);
}
.navbar-item {
border-bottom: 1px solid #e0ebf0;
&:last-of-type {
border-bottom: 0;
}
}
}
@@ -0,0 +1,7 @@
<?php
return [
'created_announcement' => 'Erstellt eine Ankündigung #:id: :title',
'updated_announcement' => 'Aktualisierte Ankündigung #:id',
'deleted_announcement' => 'Gelöschte Ankündigung #:id',
];
+33
View File
@@ -0,0 +1,33 @@
{
"Announcement": "",
"Announcements": "",
"Create Announcement": "",
"Creator": "",
"Title": "",
"Created At": "",
"Action": "",
"Edit Announcement": "",
"Delete Announcement": "",
"Please Confirm": "",
"Are you sure that you want to delete this announcement?": "",
"Yes, delete it!": "",
"No announcements found.": "",
"No new announcements at the moment.": "",
"Edit": "",
"New Announcement": "",
"Create an Announcement": "",
"Update Announcement": "",
"Update": "",
"Create": "",
"What are you announcing?": "",
"Body": "",
"Describe your announcement using markdown...": "",
"E-Mail Notification": "",
"Send email notification about the announcement to all users.": "",
"Announcement Details": "",
"View All": "",
"Thanks": "",
"Announcement created successfully.": "",
"Announcement updated successfully.": "",
"Announcement deleted successfully.": ""
}
@@ -0,0 +1,7 @@
<?php
return [
'created_announcement' => 'Created an announcement #:id: :title',
'updated_announcement' => 'Updated announcement #:id',
'deleted_announcement' => 'Deleted announcement #:id',
];
+33
View File
@@ -0,0 +1,33 @@
{
"Announcement": "Annonce",
"Announcements": "Annonces",
"Create Announcement": "Créer une annonce",
"Creator": "Créateur",
"Title": "Titre",
"Created At": "Date de création",
"Action": "Action",
"Edit Announcement": "Modifier l'annonce",
"Delete Announcement": "Supprimer l'annonce",
"Please Confirm": "Confirmation",
"Are you sure that you want to delete this announcement?": "Êtes-vous bien sûr de vouloir supprimer cette annonce ?",
"Yes, delete it!": "Oui, supprimer",
"No announcements found.": "Aucune annonce à afficher",
"No new announcements at the moment.": "Aucune annonce pour le moment.",
"Edit": "Modifier",
"New Announcement": "Nouvelle annonce",
"Create an Announcement": "Créer une annonce",
"Update Announcement": "Mettre à jour l'annonce",
"Update": "Mettre à jour",
"Create": "Créer",
"What are you announcing?": "Que souhaitez-vous annoncer ?",
"Body": "Message",
"Describe your announcement using markdown...": "Vous pouvez utiliser Markdown dans le corps de votre annonce.",
"E-Mail Notification": "Notification par mail",
"Send email notification about the announcement to all users.": "Envoyer une notification par mail à tous les utilisateurs.",
"Announcement Details": "Détails de l'annonce",
"View All": "Liste complète",
"Thanks": "Merci",
"Announcement created successfully.": "L'annonce a bien été créée.",
"Announcement updated successfully.": "L'annonce a bien été mise à jour.",
"Announcement deleted successfully.": "L'annonce a bien été supprimée."
}
@@ -0,0 +1,7 @@
<?php
return [
'created_announcement' => 'Création de l\'annonce #:id: :title',
'updated_announcement' => 'Mise à jour de l\'annonce #:id',
'deleted_announcement' => 'Suppression de l\'annonce #:id',
];
+33
View File
@@ -0,0 +1,33 @@
{
"Announcement": "Objava",
"Announcements": "Objave",
"Create Announcement": "Kreiraj Objavu",
"Creator": "Autor",
"Title": "Naslov",
"Created At": "Datum Kreiranja",
"Action": "Akcija",
"Edit Announcement": "Izmeni Objavu",
"Delete Announcement": "Obriši Objavu",
"Please Confirm": "Molimo potvrdite",
"Are you sure that you want to delete this announcement?": "Da li ste sigurni da želite da obrišete ovu objavu?",
"Yes, delete it!": "Da, obriši!",
"No announcements found.": "Nema pronađenih objava.",
"No new announcements at the moment.": "Nema novih objava u ovom trenutku.",
"Edit": "Izmeni",
"New Announcement": "Nova Objava",
"Create an Announcement": "Kreiraj Objavu",
"Update Announcement": "Ažuriraj Objavu",
"Update": "Ažuriraj",
"Create": "Kreiraj",
"What are you announcing?": "Šta želite da objavite?",
"Body": "Opis",
"Describe your announcement using markdown...": "Napišite objavu koristeći markdown...",
"E-Mail Notification": "Email Obaveštenje",
"Send email notification about the announcement to all users.": "Pošalji email o ovom obaveštenju svim korisnicima.",
"Announcement Details": "Detalji Objave",
"View All": "Vidi sve",
"Thanks": "Hvala",
"Announcement created successfully.": "Objava je uspešno kreirana.",
"Announcement updated successfully.": "Objava je uspešno ažurirana.",
"Announcement deleted successfully.": "Objava je uspešno obrisana."
}
@@ -0,0 +1,7 @@
<?php
return [
'created_announcement' => 'Kreirao je objavu #:id: :title.',
'updated_announcement' => 'Ažurirao je objavu #:id.',
'deleted_announcement' => 'Obrisao je objavu #:id.',
];
@@ -0,0 +1,85 @@
@extends('layouts.app')
@section('page-title', $edit ? __('Update Announcement') : __('New Announcement'))
@section('page-heading', $edit ? __('Update Announcement') : __('New Announcement'))
@section('breadcrumbs')
<li class="breadcrumb-item">
<a href="{{ route('announcements.index') }}">@lang('Announcements')</a>
</li>
<li class="breadcrumb-item active">
{{ $edit ? __('Update') : __('Create') }}
</li>
@stop
@section('content')
@include('partials.messages')
@if ($edit)
{!! Form::open(['route' => ['announcements.update', $announcement], 'id' => 'announcement-form', 'method' => 'PUT']) !!}
@else
{!! Form::open(['route' => 'announcements.store', 'id' => 'announcement-form']) !!}
@endif
<div class="row">
<div class="col-md-6 my-4 mx-auto">
<div class="card">
<h6 class="card-header">
{{ $edit ? __('Update Announcement') : __('Create an Announcement') }}
</h6>
<div class="card-body">
<div class="form-group">
<label for="title">@lang('Title')</label>
<input type="text"
class="form-control input-solid"
id="title"
name="title"
placeholder="@lang('What are you announcing?')"
value="{{ $edit ? $announcement->title : '' }}">
</div>
<div class="form-group">
<label for="body">@lang('Body')</label>
<textarea
name="body"
class="form-control input-solid"
rows="10"
id="body"
placeholder="@lang('Describe your announcement using markdown...')"
>{{ $edit ? $announcement->body : '' }}</textarea>
</div>
@if (! $edit)
<div class="form-group mt-4">
<div class="custom-control custom-checkbox">
<input type="checkbox"
class="custom-control-input"
name="email_notification"
id="email_notification"
value="1"/>
<label class="custom-control-label font-weight-normal" for="email_notification">
<span class="d-block">@lang('E-Mail Notification')</span>
<small>@lang('Send email notification about the announcement to all users.')</small>
</label>
</div>
</div>
@endif
</div>
</div>
<div class="row">
<div class="col-md-12">
<button type="submit" class="btn btn-primary">
{{ $edit ? __('Update Announcement') : __('Create Announcement') }}
</button>
</div>
</div>
</div>
</div>
{!! Form::close() !!}
@stop
@section('scripts')
{!! JsValidator::formRequest(\Vanguard\Announcements\Http\Requests\AnnouncementRequest::class, '#announcement-form') !!}
@stop
@@ -0,0 +1,101 @@
@extends('layouts.app')
@section('page-title', __('Announcements'))
@section('page-heading', __('Announcements'))
@section('breadcrumbs')
<li class="breadcrumb-item active">
@lang('Announcements')
</li>
@stop
@section('content')
@include('partials.messages')
<div class="d-flex mb-4">
<a href="{{ route('announcements.create') }}" class="btn btn-primary btn-rounded ml-auto">
<i class="fas fa-plus mr-2"></i>
@lang('Create Announcement')
</a>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-borderless table-striped">
<thead>
<tr>
<th></th>
<th class="min-width-150">@lang('Creator')</th>
<th class="min-width-150">@lang('Title')</th>
<th class="min-width-150">@lang('Created At')</th>
<th class="text-center min-width-150">@lang('Action')</th>
</tr>
</thead>
<tbody>
@if (count($announcements))
@foreach ($announcements as $announcement)
<tr>
<td style="width: 40px;">
<a href="{{ route('users.show', $announcement->creator) }}">
<img
class="rounded-circle img-responsive"
width="40"
src="{{ $announcement->creator->present()->avatar }}"
alt="{{ $announcement->creator->present()->name }}">
</a>
</td>
<td class="align-middle">
@permission('users.manage')
<a href="{{ route('users.show', $announcement->creator) }}">
{{ $announcement->creator->username ?: __('N/A') }}
</a>
@else
<span>{{ $announcement->creator->username ?: __('N/A') }}</span>
@endpermission
</td>
<td class="align-middle">
<a href="{{ route('announcements.show', $announcement) }}">
{{ \Illuminate\Support\Str::limit($announcement->title, 50) }}
</a>
</td>
<td class="align-middle">
{{ $announcement->created_at->format(config('app.date_format')) }}
</td>
<td class="text-center align-middle">
<a href="{{ route('announcements.edit', $announcement) }}"
class="btn btn-icon edit"
title="@lang('Edit Announcement')"
data-toggle="tooltip" data-placement="top">
<i class="fas fa-edit"></i>
</a>
<a href="{{ route('announcements.destroy', $announcement) }}"
class="btn btn-icon"
title="@lang('Delete Announcement')"
data-toggle="tooltip"
data-placement="top"
data-method="DELETE"
data-confirm-title="@lang('Please Confirm')"
data-confirm-text="@lang('Are you sure that you want to delete this announcement?')"
data-confirm-delete="@lang('Yes, delete it!')">
<i class="fas fa-trash"></i>
</a>
</td>
</tr>
@endforeach
@else
<tr>
<td colspan="7"><em>@lang('No announcements found.')</em></td>
</tr>
@endif
</tbody>
</table>
</div>
</div>
</div>
{!! $announcements->render() !!}
@stop
@@ -0,0 +1,33 @@
@extends('layouts.app')
@section('page-title', __('Announcements'))
@section('page-heading', __('Announcements'))
@section('breadcrumbs')
<li class="breadcrumb-item active">
@lang('Announcements')
</li>
@stop
@section('content')
<div class="row">
<div class="col-md-6 mx-auto mb-4">
@foreach ($announcements as $announcement)
@include('announcements::partials.card')
@endforeach
@if (count($announcements) == 0)
<div class="card">
<div class="card-body">
<p class="lead text-center m-0">
@lang('No new announcements at the moment.')
</p>
</div>
</div>
@endif
{!! $announcements->render() !!}
</div>
</div>
@stop
@@ -0,0 +1,10 @@
@component('mail::message')
# {{ $announcement->title }}
{!! $announcement->body !!}
@lang('Thanks'),<br>
{{ config('app.name') }}
@endcomponent
@@ -0,0 +1,39 @@
<div class="card overflow-hidden shadow-lg">
<div class="card-body p-0">
<div class="p-4">
<h4 class="card-title mb-3">
{{ $announcement->title }}
</h4>
<div>
{{ $announcement->parsed_body }}
</div>
</div>
<div class="d-flex justify-content-between align-items-center px-4 py-3 bg-lighter">
<div class="d-flex align-items-center ">
<img
class="rounded-circle img-responsive mr-2"
width="40"
src="{{ $announcement->creator->present()->avatar }}"
alt="{{ $announcement->creator->present()->name }}">
<div>
<div class="line-height-1">
{{ $announcement->creator->present()->name }}
</div>
<div class="text-muted">
{{ $announcement->created_at->format('M d') }}
</div>
</div>
</div>
@permission('announcements.manage')
<div>
<a href="{{ route('announcements.edit', $announcement) }}"
class="btn btn-secondary btn-sm">
@lang('Edit')
</a>
</div>
@endpermission
</div>
</div>
</div>
@@ -0,0 +1,19 @@
<div class="d-flex align-items-start px-4 p-4 navbar-item">
<img src="{{ $announcement->creator->present()->avatar }}" width="50" height="50"
class="rounded-circle img-responsive mr-3">
<div class="w-100">
<div class="d-flex justify-content-between align-items-start">
<span class="font-weight-bold">
{{ $announcement->creator->present()->name }}
</span>
<span class="text-muted">
{{ $announcement->created_at->diffForHumans(null, true, true) }}
</span>
</div>
<a href="{{ route('announcements.show', $announcement) }}">
{{ $announcement->title }}
</a>
</div>
</div>
@@ -0,0 +1,38 @@
<li class="nav-item dropdown announcements d-flex align-items-center px-3" id="announcements-icon">
<a href="#"
class="text-gray-500 position-relative nav-icon"
id="announcementsDropdown"
role="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
@if (count($announcements) > 0 && $announcements->first()->wasReadBy(auth()->user()))
<i class="activity-badge"></i>
@endif
<i class="fas fa-bullhorn"></i>
</a>
<div class="dropdown-menu dropdown-menu-right position-absolute p-0 shadow-lg"
aria-labelledby="announcementsDropdown"
style="width: 380px; height: 350px; overflow-y: scroll; overflow-x: hidden;">
<div class="text-center p-4">
<h5 class="text-muted mt-2">
@lang('Announcements')
</h5>
@if (count($announcements) > 0)
<a href="{{ route('announcements.list') }}">
@lang('View All')
</a>
@endif
</div>
<div class="bg-lighter">
@if (count($announcements) > 0)
@foreach ($announcements as $announcement)
@include('announcements::partials.navbar.item')
@endforeach
@else
<p class="text-center">@lang('No new announcements at the moment.')</p>
@endif
</div>
</div>
</li>
@@ -0,0 +1,4 @@
<script>
var read_announcements_endpoint = "{{ route('announcements.read') }}";
</script>
<script src="{{ url("vendor/plugins/announcements/js/announcements.js") }}"></script>
@@ -0,0 +1,4 @@
<link media="all"
type="text/css"
rel="stylesheet"
href="{{ url(mix('/css/announcements.css', 'vendor/plugins/announcements')) }}">
@@ -0,0 +1,24 @@
@extends('layouts.app')
@section('page-title', __('Announcement') . ":" . $announcement->title)
@section('page-heading', __('Announcement Details'))
@section('breadcrumbs')
<li class="breadcrumb-item">
<a href="{{ route('announcements.list') }}">
@lang('Announcements')
</a>
</li>
<li class="breadcrumb-item active">
{{ __('Announcement Details') }}
</li>
@stop
@section('content')
<div class="row">
<div class="col-md-6 mx-auto mb-4">
@include('announcements::partials.card')
</div>
</div>
@stop
+21
View File
@@ -0,0 +1,21 @@
<?php
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application.
| Enjoy building your API!
|
*/
Route::group(['middleware' => 'auth'], function () {
Route::apiResource('/announcements', 'AnnouncementsController')
->except('show')
->names('announcements.api');
Route::get('announcements/{announcementId}', 'AnnouncementsController@show');
Route::post('announcements/read', 'ReadAnnouncementsController@index');
});
+21
View File
@@ -0,0 +1,21 @@
<?php
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application.
| Now create something great!
|
*/
Route::group(['middleware' => ['auth', 'verified']], function () {
Route::get('announcements/list', 'AnnouncementListController@index')
->name('announcements.list');
Route::post('announcements/read', 'ReadAnnouncementsController@index')
->name('announcements.read');
Route::resource('announcements', 'AnnouncementsController');
});
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace Vanguard\Announcements;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Illuminate\Support\HtmlString;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Table\TableExtension;
use Vanguard\Announcements\Database\Factories\AnnouncementFactory;
use Vanguard\User;
/**
* @property int $id
* @property string $title
* @property string $body
* @property Carbon $created_at
* @property Carbon $deleted_at
*/
class Announcement extends Model
{
use HasFactory;
protected $table = 'announcements';
protected $guarded = [];
public function creator(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
public function wasReadBy(User $user): bool
{
return $user->announcements_last_read_at < $this->created_at;
}
public function getParsedBodyAttribute(): HtmlString
{
$environment = Environment::createCommonMarkEnvironment();
$environment->addExtension(new TableExtension);
$converter = new CommonMarkConverter([
'html_input' => 'escape',
'allow_unsafe_links' => false,
], $environment);
return new HtmlString(
$converter->convertToHtml($this->attributes['body'])
);
}
/**
* {@inheritDoc}
*/
protected static function newFactory(): AnnouncementFactory
{
return new AnnouncementFactory;
}
}
+154
View File
@@ -0,0 +1,154 @@
<?php
namespace Vanguard\Announcements;
use Event;
use Route;
use Vanguard\Announcements\Events\EmailNotificationRequested;
use Vanguard\Announcements\Hooks\NavbarItemsHook;
use Vanguard\Announcements\Hooks\ScriptsHook;
use Vanguard\Announcements\Hooks\StylesHook;
use Vanguard\Announcements\Listeners\ActivityLogSubscriber;
use Vanguard\Announcements\Listeners\SendEmailNotification;
use Vanguard\Announcements\Repositories\AnnouncementsRepository;
use Vanguard\Announcements\Repositories\EloquentAnnouncements;
use Vanguard\Plugins\Plugin;
use Vanguard\Plugins\Vanguard;
use Vanguard\Support\Sidebar\Item;
class Announcements extends Plugin
{
/**
* A sidebar item for the plugin.
*/
public function sidebar(): ?Item
{
return Item::create(__('Announcements'))
->icon('fas fa-bullhorn')
->route('announcements.index')
->permissions('announcements.manage')
->active('announcements*');
}
/**
* Register plugin services.
*/
public function register(): void
{
$this->app->singleton(AnnouncementsRepository::class, EloquentAnnouncements::class);
}
/**
* Bootstrap services.
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function boot(): void
{
$this->registerViews();
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
$this->loadViewsFrom(__DIR__.'/../resources/views', 'announcements');
$this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'announcements');
$this->loadJsonTranslationsFrom(__DIR__.'/../resources/lang');
$this->publishes([
__DIR__.'/../database/migrations' => database_path('migrations'),
], 'migrations');
$this->mapRoutes();
$this->registerHooks();
$this->registerEventListeners();
$this->publishAssets();
}
/**
* Register plugin views.
*/
protected function registerViews(): void
{
$viewsPath = __DIR__.'/../resources/views';
$this->publishes([
$viewsPath => resource_path('views/vendor/plugins/announcements'),
], 'views');
$this->loadViewsFrom($viewsPath, 'announcements');
}
/**
* Map all plugin related routes.
*/
protected function mapRoutes(): void
{
$this->mapWebRoutes();
if ($this->app['config']->get('auth.expose_api')) {
$this->mapApiRoutes();
}
}
/**
* Map web plugin related routes.
*/
protected function mapWebRoutes(): void
{
Route::group([
'namespace' => 'Vanguard\Announcements\Http\Controllers\Web',
'middleware' => 'web',
], function () {
$this->loadRoutesFrom(__DIR__.'/../routes/web.php');
});
}
/**
* Map API plugin related routes.
*/
protected function mapApiRoutes(): void
{
Route::group([
'namespace' => 'Vanguard\Announcements\Http\Controllers\Api',
'middleware' => 'api',
'prefix' => 'api',
], function () {
$this->loadRoutesFrom(__DIR__.'/../routes/api.php');
});
}
/**
* Register plugin event listeners.
*/
private function registerEventListeners(): void
{
// Register activity log subscriber only if
// UserActivity plugin is installed.
if ($this->app->bound('Vanguard\UserActivity\Repositories\Activity\ActivityRepository')) {
Event::subscribe(ActivityLogSubscriber::class);
}
Event::listen(EmailNotificationRequested::class, SendEmailNotification::class);
}
/**
* Register all necessary view hooks for the plugin.
*/
private function registerHooks(): void
{
Vanguard::hook('navbar:items', NavbarItemsHook::class);
Vanguard::hook('app:styles', StylesHook::class);
Vanguard::hook('app:scripts', ScriptsHook::class);
}
/**
* Publish public assets.
*/
protected function publishAssets(): void
{
$this->publishes([
realpath(__DIR__.'/../dist') => $this->app['path.public'].'/vendor/plugins/announcements',
], 'public');
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace Vanguard\Announcements\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Vanguard\Announcements\Announcement;
class Created
{
use Dispatchable;
public function __construct(public Announcement $announcement, public $sendEmailNotification = false)
{
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace Vanguard\Announcements\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Vanguard\Announcements\Announcement;
class Deleted
{
use Dispatchable;
public function __construct(public Announcement $announcement)
{
}
}
@@ -0,0 +1,15 @@
<?php
namespace Vanguard\Announcements\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Vanguard\Announcements\Announcement;
class EmailNotificationRequested
{
use Dispatchable;
public function __construct(public Announcement $announcement)
{
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace Vanguard\Announcements\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Vanguard\Announcements\Announcement;
class Updated
{
use Dispatchable;
public function __construct(public Announcement $announcement)
{
}
}
@@ -0,0 +1,25 @@
<?php
namespace Vanguard\Announcements\Hooks;
use Illuminate\Contracts\View\View;
use Vanguard\Announcements\Repositories\AnnouncementsRepository;
use Vanguard\Plugins\Contracts\Hook;
class NavbarItemsHook implements Hook
{
public function __construct(private readonly AnnouncementsRepository $announcements)
{
}
/**
* Execute the hook action.
*/
public function handle(): View
{
$announcements = $this->announcements->latest(5);
$announcements->load('creator');
return view('announcements::partials.navbar.list', compact('announcements'));
}
}
@@ -0,0 +1,17 @@
<?php
namespace Vanguard\Announcements\Hooks;
use Illuminate\Contracts\View\View;
use Vanguard\Plugins\Contracts\Hook;
class ScriptsHook implements Hook
{
/**
* Execute the hook action.
*/
public function handle(): View
{
return view('announcements::partials.scripts');
}
}
@@ -0,0 +1,17 @@
<?php
namespace Vanguard\Announcements\Hooks;
use Illuminate\Contracts\View\View;
use Vanguard\Plugins\Contracts\Hook;
class StylesHook implements Hook
{
/**
* Execute the hook action.
*/
public function handle(): View
{
return view('announcements::partials.styles');
}
}
@@ -0,0 +1,108 @@
<?php
namespace Vanguard\Announcements\Http\Controllers\Api;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\AllowedInclude;
use Spatie\QueryBuilder\QueryBuilder;
use Vanguard\Announcements\Announcement;
use Vanguard\Announcements\Events\EmailNotificationRequested;
use Vanguard\Announcements\Http\Requests\AnnouncementRequest;
use Vanguard\Announcements\Http\Resources\AnnouncementResource;
use Vanguard\Announcements\Repositories\AnnouncementsRepository;
use Vanguard\Http\Controllers\Api\ApiController;
/**
* Class AnnouncementsController
*/
class AnnouncementsController extends ApiController
{
public function __construct(private readonly AnnouncementsRepository $announcements)
{
$this->middleware('permission:announcements.manage')->except('index', 'show');
}
/**
* Returns a paginated list of announcements.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function index(Request $request): AnonymousResourceCollection
{
$this->validate($request, ['per_page' => 'numeric|max:50']);
$announcements = QueryBuilder::for(Announcement::class)
->allowedIncludes([
AllowedInclude::relationship('user', 'creator'),
])
->allowedFilters([
AllowedFilter::partial('title'),
AllowedFilter::partial('body'),
AllowedFilter::exact('user', 'user_id'),
])
->allowedSorts('title', 'created_at')
->defaultSort('-created_at')
->paginate($request->per_page);
return AnnouncementResource::collection($announcements);
}
/**
* Stores the announcement inside the database.
*/
public function store(AnnouncementRequest $request): AnnouncementResource
{
$announcement = $this->announcements->createFor(
auth()->user(),
$request->title,
$request->body
);
if ($request->email_notification) {
EmailNotificationRequested::dispatch($announcement);
}
return new AnnouncementResource($announcement);
}
/**
* Returns a single announcement resource.
*/
public function show($announcementId): AnnouncementResource
{
$announcement = QueryBuilder::for(Announcement::where('id', $announcementId))
->allowedIncludes([
AllowedInclude::relationship('user', 'creator'),
])
->first();
return new AnnouncementResource($announcement);
}
/**
* Updates announcement details.
*/
public function update(Announcement $announcement, AnnouncementRequest $request): AnnouncementResource
{
$announcement = $this->announcements->update(
$announcement,
$request->title,
$request->body
);
return new AnnouncementResource($announcement);
}
/**
* Removes announcement from the system.
*/
public function destroy(Announcement $announcement): JsonResponse
{
$this->announcements->delete($announcement);
return $this->respondWithSuccess();
}
}
@@ -0,0 +1,23 @@
<?php
namespace Vanguard\Announcements\Http\Controllers\Api;
use Vanguard\Http\Controllers\Api\ApiController;
class ReadAnnouncementsController extends ApiController
{
/**
* Update the timestamp when announcements were last read
* by the currently authenticated user.
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
auth()->user()->forceFill([
'announcements_last_read_at' => now(),
])->save();
return $this->respondWithSuccess();
}
}
@@ -0,0 +1,25 @@
<?php
namespace Vanguard\Announcements\Http\Controllers\Web;
use Illuminate\Contracts\View\View;
use Vanguard\Announcements\Repositories\AnnouncementsRepository;
use Vanguard\Http\Controllers\Controller;
class AnnouncementListController extends Controller
{
public function __construct(private readonly AnnouncementsRepository $announcements)
{
}
/**
* Displays the plugin index page.
*/
public function index(): View
{
$announcements = $this->announcements->paginate(7);
$announcements->load('creator');
return view('announcements::list', compact('announcements'));
}
}
@@ -0,0 +1,105 @@
<?php
namespace Vanguard\Announcements\Http\Controllers\Web;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Vanguard\Announcements\Announcement;
use Vanguard\Announcements\Events\EmailNotificationRequested;
use Vanguard\Announcements\Http\Requests\AnnouncementRequest;
use Vanguard\Announcements\Repositories\AnnouncementsRepository;
use Vanguard\Http\Controllers\Controller;
/**
* Class AnnouncementsController
*/
class AnnouncementsController extends Controller
{
public function __construct(private readonly AnnouncementsRepository $announcements)
{
$this->middleware('permission:announcements.manage')->except('show');
}
/**
* Displays the plugin index page.
*/
public function index(): View
{
$announcements = $this->announcements->paginate();
$announcements->load('creator');
return view('announcements::index', compact('announcements'));
}
/**
* Shows the create announcement form.
*/
public function create(): View
{
return view('announcements::add-edit', ['edit' => false]);
}
/**
* Stores the announcement inside the database.
*/
public function store(AnnouncementRequest $request): RedirectResponse
{
$announcement = $this->announcements->createFor(
auth()->user(),
$request->title,
$request->body
);
if ($request->email_notification) {
EmailNotificationRequested::dispatch($announcement);
}
return redirect()->route('announcements.index')
->withSuccess(__('Announcement created successfully.'));
}
/**
* Renders "view announcement" page.
*/
public function show(Announcement $announcement): View
{
return view('announcements::show', compact('announcement'));
}
/**
* Renders the form for editing the announcement.
*/
public function edit(Announcement $announcement): View
{
return view('announcements::add-edit', [
'edit' => true,
'announcement' => $announcement,
]);
}
/**
* Updates announcement details.
*/
public function update(Announcement $announcement, AnnouncementRequest $request): RedirectResponse
{
$this->announcements->update(
$announcement,
$request->title,
$request->body
);
return redirect()->route('announcements.index')
->withSuccess(__('Announcement updated successfully.'));
}
/**
* Removes announcement from the system.
*/
public function destroy(Announcement $announcement): RedirectResponse
{
$this->announcements->delete($announcement);
return redirect()->route('announcements.index')
->withSuccess(__('Announcement deleted successfully.'));
}
}
@@ -0,0 +1,19 @@
<?php
namespace Vanguard\Announcements\Http\Controllers\Web;
use Vanguard\Http\Controllers\Controller;
class ReadAnnouncementsController extends Controller
{
/**
* Update the timestamp when announcements were last read
* by the currently authenticated user.
*/
public function index(): void
{
auth()->user()->forceFill([
'announcements_last_read_at' => now(),
])->save();
}
}
@@ -0,0 +1,20 @@
<?php
namespace Vanguard\Announcements\Http\Requests;
use Vanguard\Http\Requests\Request;
class AnnouncementRequest extends Request
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'title' => 'required|max:150',
'body' => 'required|max:1500',
'email_notifications' => 'boolean',
];
}
}
@@ -0,0 +1,28 @@
<?php
namespace Vanguard\Announcements\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use Vanguard\Http\Resources\UserResource;
class AnnouncementResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
*/
public function toArray($request): array
{
return [
'id' => (int) $this->id,
'user_id' => (int) $this->user_id,
'title' => $this->title,
'body' => $this->body,
'parsed_body' => (string) $this->parsed_body,
'created_at' => (string) $this->created_at,
'updated_at' => (string) $this->updated_at,
'user' => new UserResource($this->whenLoaded('creator')),
];
}
}
@@ -0,0 +1,51 @@
<?php
namespace Vanguard\Announcements\Listeners;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Str;
use Vanguard\Announcements\Events\Created;
use Vanguard\Announcements\Events\Deleted;
use Vanguard\Announcements\Events\Updated;
use Vanguard\UserActivity\Logger;
class ActivityLogSubscriber
{
public function __construct(private Logger $logger)
{
}
public function onCreate(Created $event): void
{
$this->logger->log(__('announcements::log.created_announcement', [
'id' => $event->announcement->id,
'title' => Str::limit($event->announcement->title, 50),
]));
}
public function onUpdate(Updated $event): void
{
$this->logger->log(__('announcements::log.created_announcement', [
'id' => $event->announcement->id,
]));
}
public function onDelete(Deleted $event): void
{
$this->logger->log(__('announcements::log.deleted_announcement', [
'id' => $event->announcement->id,
]));
}
/**
* Register the listeners for the subscriber.
*/
public function subscribe(Dispatcher $events): void
{
$class = self::class;
$events->listen(Created::class, "{$class}@onCreate");
$events->listen(Updated::class, "{$class}@onUpdate");
$events->listen(Deleted::class, "{$class}@onDelete");
}
}
@@ -0,0 +1,29 @@
<?php
namespace Vanguard\Announcements\Listeners;
use Mail;
use Vanguard\Announcements\Announcement;
use Vanguard\Announcements\Events\EmailNotificationRequested;
use Vanguard\Announcements\Mail\AnnouncementEmail;
use Vanguard\User;
class SendEmailNotification
{
/**
* Handle the event.
*/
public function handle(EmailNotificationRequested $event): void
{
User::chunk(200, function ($users) use ($event) {
foreach ($users as $user) {
$this->sendEmailTo($user, $event->announcement);
}
});
}
private function sendEmailTo(User $user, Announcement $announcement): void
{
Mail::to($user)->send(new AnnouncementEmail($announcement));
}
}
@@ -0,0 +1,32 @@
<?php
namespace Vanguard\Announcements\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Vanguard\Announcements\Announcement;
class AnnouncementEmail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct(public Announcement $announcement)
{
}
/**
* Build the message.
*/
public function build(): self
{
$subject = sprintf('[%s] %s', __('Announcement'), $this->announcement->title);
return $this->subject($subject)
->markdown('announcements::mail.notification');
}
}
@@ -0,0 +1,43 @@
<?php
namespace Vanguard\Announcements\Repositories;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
use Vanguard\Announcements\Announcement;
use Vanguard\User;
interface AnnouncementsRepository
{
/**
* Get latest announcements.
*
* @return Collection<Announcement>
*/
public function latest(int $count = 5): Collection;
/**
* Paginate announcements in descending order.
*/
public function paginate(int $perPage = 10): LengthAwarePaginator;
/**
* Create an announcement for user.
*/
public function createFor(User $user, string $title, string $body): Announcement;
/**
* Find announcement by ID.
*/
public function find(int $id): ?Announcement;
/**
* Update announcement.
*/
public function update(Announcement $announcement, string $title, string $body): Announcement;
/**
* Remove announcement from the system.
*/
public function delete(Announcement $announcement): bool;
}
@@ -0,0 +1,87 @@
<?php
namespace Vanguard\Announcements\Repositories;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
use Vanguard\Announcements\Announcement;
use Vanguard\Announcements\Events\Created;
use Vanguard\Announcements\Events\Deleted;
use Vanguard\Announcements\Events\Updated;
use Vanguard\User;
class EloquentAnnouncements implements AnnouncementsRepository
{
/**
* Get latest announcements.
*
* @return Collection<Announcement>
*/
public function latest(int $count = 5): Collection
{
return Announcement::latest()->take($count)->get();
}
/**
* Paginate announcements in descending order.
*/
public function paginate(int $perPage = 10): LengthAwarePaginator
{
return Announcement::latest()->paginate($perPage);
}
/**
* Create an announcement for user.
*/
public function createFor(User $user, string $title, string $body): Announcement
{
$announcement = Announcement::create([
'title' => $title,
'body' => $body,
'user_id' => $user->id,
]);
Created::dispatch($announcement);
return $announcement;
}
/**
* Find announcement by ID.
*/
public function find($id): ?Announcement
{
return Announcement::find($id);
}
/**
* Update announcement.
*/
public function update(Announcement $announcement, string $title, string $body): Announcement
{
$announcement->update([
'title' => $title,
'body' => $body,
]);
Updated::dispatch($announcement);
return $announcement;
}
/**
* Remove announcement from the system.
*
* @throws \Exception
*/
public function delete(Announcement $announcement): bool
{
if ($announcement->delete()) {
Deleted::dispatch($announcement);
return true;
}
return false;
}
}
@@ -0,0 +1,299 @@
<?php
namespace Vanguard\Announcements\Tests\Feature\Api;
use Carbon\Carbon;
use Facades\Tests\Setup\UserFactory;
use Mail;
use Tests\Feature\ApiTestCase;
use Vanguard\Announcements\Announcement;
use Vanguard\Announcements\Database\Seeders\AnnouncementsDatabaseSeeder;
use Vanguard\Announcements\Http\Resources\AnnouncementResource;
use Vanguard\Announcements\Mail\AnnouncementEmail;
class AnnouncementsTest extends ApiTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->artisan('db:seed', ['--class' => AnnouncementsDatabaseSeeder::class]);
}
/** @test */
public function guests_cannot_paginate_announcements()
{
$this->getJson('/api/announcements')->assertStatus(401);
}
/** @test */
public function any_authenticated_users_can_paginate_announcements()
{
$user = UserFactory::user()->create();
$announcements = Announcement::factory()->times(11)->create();
$response = $this->actingAs($user, self::API_GUARD)
->getJson('/api/announcements?per_page=10')
->assertOk();
$transformed = AnnouncementResource::collection($announcements->take(10))->resolve();
$this->assertEquals($response->json('data'), $transformed);
$response->assertJson([
'meta' => [
'current_page' => 1,
'from' => 1,
'to' => 10,
'last_page' => 2,
'path' => url('api/announcements'),
'total' => 11,
'per_page' => 10,
],
]);
}
/** @test */
public function paginate_announcements_with_more_records_per_page_than_allowed()
{
$this->actingAs($this->validUser(), self::API_GUARD)
->getJson('/api/announcements?per_page=140')
->assertStatus(422);
}
/** @test */
public function guests_cannot_create_announcements()
{
$this->postJson('/api/announcements', $this->validParams())
->assertUnauthorized();
$this->assertEquals(0, Announcement::count());
}
/** @test */
public function users_without_appropriate_permission_cannot_create_announcements()
{
$user = UserFactory::user()->create();
$this->actingAs($user, self::API_GUARD)
->postJson('/api/announcements', $this->validParams())
->assertForbidden();
$this->assertEquals(0, Announcement::count());
}
/** @test */
public function users_with_appropriate_permission_can_create_announcements()
{
$user = $this->validUser();
$data = $this->validParams();
$response = $this->actingAs($user, self::API_GUARD)
->postJson('/api/announcements', $data)
->assertStatus(201);
$announcement = Announcement::first();
$response->assertExactJson([
'data' => (new AnnouncementResource($announcement))->resolve(),
]);
$this->assertEquals($user->id, $announcement->user_id);
$this->assertEquals($data['title'], $announcement->title);
$this->assertEquals($data['body'], $announcement->body);
}
/** @test */
public function an_email_notification_can_be_triggered_when_an_announcement_is_created()
{
Mail::fake();
$data = $this->validParams(['email_notification' => true]);
$this->actingAs($this->validUser(), self::API_GUARD)
->postJson('/api/announcements', $data)
->assertStatus(201);
$announcement = Announcement::first();
Mail::assertQueued(AnnouncementEmail::class, function ($mail) use ($announcement) {
return $mail->announcement->id === $announcement->id;
});
}
/** @test */
public function title_field_is_required_when_creating_an_announcement()
{
$data = $this->validParams(['title' => '']);
$this->actingAs($this->validUser(), self::API_GUARD)
->postJson('/api/announcements', $data)
->assertStatus(422)
->assertJsonValidationErrors('title');
$this->assertEquals(0, Announcement::count());
}
/** @test */
public function body_field_is_required_when_creating_an_announcement()
{
$data = $this->validParams(['body' => '']);
$this->actingAs($this->validUser(), self::API_GUARD)
->postJson('/api/announcements', $data)
->assertStatus(422)
->assertJsonValidationErrors('body');
$this->assertEquals(0, Announcement::count());
}
/** @test */
public function guests_cannot_view_an_announcement()
{
$announcement = Announcement::factory()->create();
$this->getJson("/api/announcements/{$announcement->id}")
->assertUnauthorized();
}
/** @test */
public function authenticated_users_can_view_any_announcement()
{
$user = UserFactory::user()->create();
$announcement = Announcement::factory()->create();
$this->actingAs($user, self::API_GUARD)
->getJson("/api/announcements/{$announcement->id}")
->assertOk()
->assertExactJson([
'data' => (new AnnouncementResource($announcement))->resolve(),
]);
}
/** @test */
public function guests_cannot_update_an_announcement()
{
$announcement = Announcement::factory()->create();
$this->putJson("/api/announcements/{$announcement->id}", $this->validParams())
->assertUnauthorized();
}
/** @test */
public function users_without_approprite_permission_cannot_update_an_announcement()
{
$user = UserFactory::user()->create();
$announcement = Announcement::factory()->create();
$this->actingAs($user, self::API_GUARD)
->putJson("/api/announcements/{$announcement->id}", $this->validParams())
->assertForbidden();
}
/** @test */
public function users_with_approprite_permission_can_update_an_announcement()
{
$user = $this->validUser();
$announcement = Announcement::factory()->create();
$this->actingAs($user, self::API_GUARD)
->putJson("/api/announcements/{$announcement->id}", $this->validParams())
->assertOk()
->assertExactJson([
'data' => (new AnnouncementResource($announcement->fresh()))->resolve(),
]);
}
/** @test */
public function title_field_is_required_when_updating_an_announcement()
{
$data = $this->validParams(['title' => '']);
$announcement = Announcement::factory()->create();
$this->actingAs($this->validUser(), self::API_GUARD)
->putJson("/api/announcements/{$announcement->id}", $data)
->assertStatus(422)
->assertJsonValidationErrors('title');
}
/** @test */
public function body_field_is_required_when_updating_an_announcement()
{
$data = $this->validParams(['body' => '']);
$announcement = Announcement::factory()->create();
$this->actingAs($this->validUser(), self::API_GUARD)
->putJson("/api/announcements/{$announcement->id}", $data)
->assertStatus(422)
->assertJsonValidationErrors('body');
}
/** @test */
public function guests_cannot_delete_an_announcement()
{
$announcement = Announcement::factory()->create();
$this->deleteJson("/api/announcements/{$announcement->id}")
->assertUnauthorized();
}
/** @test */
public function users_without_appropriate_permission_cannot_delete_an_announcement()
{
$user = UserFactory::user()->create();
$announcement = Announcement::factory()->create();
$this->actingAs($user, self::API_GUARD)
->deleteJson("/api/announcements/{$announcement->id}")
->assertForbidden();
}
/** @test */
public function users_with_appropriate_permission_can_delete_an_announcement()
{
$announcement = Announcement::factory()->create();
$this->actingAs($this->validUser(), self::API_GUARD)
->deleteJson("/api/announcements/{$announcement->id}")
->assertOk();
$this->assertNull($announcement->fresh());
}
/** @test */
public function user_announcements_can_be_marked_as_read()
{
$user = UserFactory::user()->create([
'announcements_last_read_at' => null,
]);
Carbon::setTestNow(now());
$this->actingAs($user, self::API_GUARD)
->post('/api/announcements/read');
$this->assertEquals(
now()->format('Y-m-d H:i:s'),
$user->fresh()->announcements_last_read_at
);
}
/**
* @return mixed
*/
private function validUser()
{
return UserFactory::user()->withPermissions('announcements.manage')->create();
}
/**
* @return array
*/
private function validParams(array $overrides = [])
{
return array_merge([
'title' => 'Foo Announcement',
'body' => 'This is the announcement body.',
'email_notification' => false,
], $overrides);
}
}
@@ -0,0 +1,361 @@
<?php
namespace Vanguard\Announcements\Tests\Feature\Web;
use Carbon\Carbon;
use Facades\Tests\Setup\UserFactory;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Arr;
use Mail;
use Tests\TestCase;
use Vanguard\Announcements\Announcement;
use Vanguard\Announcements\Database\Seeders\AnnouncementsDatabaseSeeder;
use Vanguard\Announcements\Mail\AnnouncementEmail;
class AnnouncementsTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->artisan('db:seed', ['--class' => 'RolesSeeder']);
$this->artisan('db:seed', ['--class' => 'PermissionsSeeder']);
$this->artisan('db:seed', ['--class' => 'CountriesSeeder']);
$this->artisan('db:seed', ['--class' => AnnouncementsDatabaseSeeder::class]);
}
/** @test */
public function guests_cannot_view_announcement_index_page()
{
$this->get('/announcements')->assertRedirect('login');
}
/** @test */
public function users_without_appropriate_permission_cannot_view_announcement_list()
{
$user = UserFactory::user()->create();
$this->actingAs($user)->get('/announcements')->assertForbidden();
}
/** @test */
public function users_with_appropriate_permission_can_view_the_announcement_list()
{
$user = UserFactory::user()->withPermissions('announcements.manage')->create();
Announcement::factory()->create([
'user_id' => $user->id,
'title' => 'Foo Announcement',
]);
$response = $this->actingAs($user)->get('/announcements')->assertOk();
$response->assertSee('Foo Announcement');
}
/** @test */
public function guests_cannot_create_announcements()
{
$this->post('/announcements', $this->validParams())->assertRedirect('login');
$this->assertEquals(0, Announcement::count());
}
/** @test */
public function users_without_appropriate_permission_cannot_create_announcements()
{
$user = UserFactory::user()->create();
$this->actingAs($user)->post('/announcements', $this->validParams())->assertForbidden();
$this->assertEquals(0, Announcement::count());
}
/** @test */
public function users_with_appropriate_permission_can_create_announcements()
{
Mail::fake();
$user = UserFactory::user()->withPermissions('announcements.manage')->create();
$data = $this->validParams();
$this->actingAs($user)->post('/announcements', $data)
->assertRedirect('/announcements');
$announcement = Announcement::first();
$this->assertEquals($data['title'], $announcement->title);
$this->assertEquals($data['body'], $announcement->body);
Mail::assertNothingSent();
}
/** @test */
public function title_field_is_required_when_creating_an_announcement()
{
$user = UserFactory::user()->withPermissions('announcements.manage')->create();
$data = $this->validParams(['title' => '']);
$this->actingAs($user)
->from('/announcements/create')
->post('/announcements', $data)
->assertRedirect('/announcements/create')
->assertSessionHasErrors('title');
}
/** @test */
public function body_field_is_required_when_creating_an_announcement()
{
$user = UserFactory::user()->withPermissions('announcements.manage')->create();
$data = $this->validParams(['body' => '']);
$this->actingAs($user)
->from('/announcements/create')
->post('/announcements', $data)
->assertRedirect('/announcements/create')
->assertSessionHasErrors('body');
}
/** @test */
public function an_email_notification_can_be_triggered_when_an_announcement_is_created()
{
Mail::fake();
$user = UserFactory::user()->withPermissions('announcements.manage')->create();
$data = $this->validParams(['email_notification' => '1']);
$this->actingAs($user)
->from('/announcements/create')
->post('/announcements', $data)
->assertRedirect('/announcements');
$announcement = Announcement::first();
Mail::assertQueued(AnnouncementEmail::class, function ($mail) use ($announcement) {
return $mail->announcement->id === $announcement->id;
});
}
/** @test */
public function guests_cannot_view_an_announcement()
{
$announcement = Announcement::factory()->create();
$this->get('/announcements/'.$announcement->id)
->assertRedirect('/login');
}
/** @test */
public function authenticated_users_can_see_an_announcement()
{
$this->withoutExceptionHandling();
$user = UserFactory::user()->create();
$admin = UserFactory::admin()->create();
$data = Arr::except($this->validParams(['user_id' => $admin->id]), 'email_notification');
$announcement = Announcement::factory()->create($data);
$this->actingAs($user)->get('/announcements/'.$announcement->id)
->assertOk()
->assertSee($data['title']);
$this->actingAs($admin)->get('/announcements/'.$announcement->id)
->assertOk()
->assertSee($data['title']);
}
/** @test */
public function guests_cannot_edit_announcements()
{
$announcement = Announcement::factory()->create();
$this->get('/announcements/'.$announcement->id)->assertRedirect('login');
$this->put('/announcements/'.$announcement->id, $this->validParams())->assertRedirect('login');
$this->assertEquals($announcement->title, $announcement->fresh()->title);
$this->assertEquals($announcement->body, $announcement->fresh()->body);
}
/** @test */
public function users_with_appropriate_permissions_can_edit_an_announcement()
{
$user = UserFactory::user()->withPermissions('announcements.manage')->create();
$announcement = Announcement::factory()->create();
$data = $this->validParams();
$this->actingAs($user)
->from("/announcements/{$announcement->id}/edit")
->put('/announcements/'.$announcement->id, $data)
->assertRedirect('/announcements')
->assertSessionDoesntHaveErrors();
$this->assertEquals($data['title'], $announcement->fresh()->title);
$this->assertEquals($data['body'], $announcement->fresh()->body);
}
/** @test */
public function title_field_is_required_when_updating_an_announcement()
{
$user = UserFactory::user()->withPermissions('announcements.manage')->create();
$announcement = Announcement::factory()->create();
$data = $this->validParams(['title' => '']);
$this->actingAs($user)
->from("/announcements/{$announcement->id}/edit")
->put('/announcements/'.$announcement->id, $data)
->assertRedirect("/announcements/{$announcement->id}/edit")
->assertSessionHasErrors('title');
}
/** @test */
public function body_field_is_required_when_updating_an_announcement()
{
$user = UserFactory::user()->withPermissions('announcements.manage')->create();
$announcement = Announcement::factory()->create();
$data = $this->validParams(['body' => '']);
$this->actingAs($user)
->from("/announcements/{$announcement->id}/edit")
->put('/announcements/'.$announcement->id, $data)
->assertRedirect("/announcements/{$announcement->id}/edit")
->assertSessionHasErrors('body');
}
/** @test */
public function guests_cannot_delete_an_announcement()
{
$announcement = Announcement::factory()->create();
$this->delete('/announcements/'.$announcement->id)->assertRedirect('login');
$this->assertNotNull($announcement->fresh());
}
/** @test */
public function users_without_appropriate_permission_cannot_delete_an_announcement()
{
$user = UserFactory::user()->create();
$announcement = Announcement::factory()->create();
$this->actingAs($user)
->delete('/announcements/'.$announcement->id)
->assertForbidden();
$this->assertNotNull($announcement->fresh());
}
/** @test */
public function users_with_appropriate_permission_can_delete_an_announcement()
{
$user = UserFactory::user()->withPermissions('announcements.manage')->create();
$announcement = Announcement::factory()->create();
$this->actingAs($user)
->delete('/announcements/'.$announcement->id)
->assertRedirect('/announcements');
$this->assertNull($announcement->fresh());
}
/** @test */
public function guests_cannot_view_announcement_list()
{
$this->get('/announcements/list')->assertRedirect('login');
}
/** @test */
public function any_authenticated_user_can_view_announcement_list()
{
$user = UserFactory::user()->create();
$announcementA = Announcement::factory()->create();
$announcementB = Announcement::factory()->create();
$this->actingAs($user)
->get('/announcements/list')
->assertSee($announcementA->title)
->assertSee($announcementA->parsed_body)
->assertSee($announcementB->title)
->assertSee($announcementB->parsed_body);
}
/** @test */
public function authenticated_users_can_see_the_announcements_header_section()
{
$user = UserFactory::user()->create();
$data = ['title' => 'some random announcement'];
Announcement::factory()->create($data);
$this->actingAs($user)
->get('/')
->assertSee('id="announcements-icon"', false)
->assertSee('id="announcementsDropdown"', false)
->assertSee($data['title']);
}
/** @test */
public function a_red_dot_indicator_is_displayed_if_user_has_unread_announcements()
{
Announcement::factory()->create([
'created_at' => now()->subMinutes(3),
]);
$userA = UserFactory::user()->create([
'announcements_last_read_at' => now(),
]);
$userB = UserFactory::user()->create([
'announcements_last_read_at' => now()->subMinutes(5),
]);
$this->actingAs($userA)
->get('/')
->assertDontSee('activity-badge');
$this->actingAs($userB)
->get('/')
->assertSee('activity-badge');
}
/** @test */
public function user_announcements_can_be_marked_as_read()
{
$user = UserFactory::user()->create([
'announcements_last_read_at' => null,
]);
Carbon::setTestNow(now());
$this->actingAs($user)
->post('/announcements/read');
$this->assertEquals(
now()->format('Y-m-d H:i:s'),
$user->fresh()->announcements_last_read_at
);
}
private function validParams(array $overrides = [])
{
return array_merge([
'title' => 'Foo Announcement',
'body' => 'This is the announcement body.',
'email_notification' => '0',
], $overrides);
}
}
@@ -0,0 +1,20 @@
<?php
namespace Vanguard\Announcements\Tests\Unit;
use Tests\TestCase;
use Vanguard\Announcements\Announcement;
class AnnouncementTest extends TestCase
{
/** @test */
public function testParsedBody()
{
$announcement = new Announcement([
'title' => 'foo',
'body' => '# test',
]);
$this->assertEquals("<h1>test</h1>\n", (string) $announcement->parsed_body);
}
}
+10
View File
@@ -0,0 +1,10 @@
const mix = require('laravel-mix');
mix.setPublicPath('./dist/');
mix.js( __dirname + '/resources/assets/js/app.js', 'dist/js/announcements.js')
.sass( __dirname + '/resources/assets/sass/app.scss', 'dist/css/announcements.css');
if (mix.inProduction()) {
mix.version();
}