vendor and env first commit
This commit is contained in:
@@ -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
@@ -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;
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
+50
@@ -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');
|
||||
}
|
||||
}
|
||||
+34
@@ -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
File diff suppressed because it is too large
Load Diff
+16
@@ -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
@@ -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',
|
||||
];
|
||||
@@ -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',
|
||||
];
|
||||
@@ -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',
|
||||
];
|
||||
@@ -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>
|
||||
+19
@@ -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>
|
||||
+38
@@ -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
@@ -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
@@ -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');
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
+108
@@ -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();
|
||||
}
|
||||
}
|
||||
+23
@@ -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();
|
||||
}
|
||||
}
|
||||
+25
@@ -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'));
|
||||
}
|
||||
}
|
||||
+105
@@ -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.'));
|
||||
}
|
||||
}
|
||||
+19
@@ -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');
|
||||
}
|
||||
}
|
||||
+43
@@ -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
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user