Skip to content

feat: notifications for new engagements (resolves #2562) #2604

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
May 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .kube/app/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ php artisan deploy:local

flock -n -E 0 /opt/data -c "php artisan deploy:global" # run exclusively on a single instance at once

# Run data migrations that aren't included in the DB migrations
flock -n -E 0 /opt/data -c "php artisan app:migrate-settings-data" # run exclusively on a single instance at once

# Generate the robots.txt and sitemap.xml files
php artisan seo:generate

Expand Down
61 changes: 34 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,20 +179,20 @@ of how some key tasks can be carried out using Herd:

Herd supports debuging via XDebug. The article "[Activating XDebug on Visual Studio Code & Laravel Herd](https://thomashysselinckx.medium.com/activating-xdebug-on-visual-studio-code-laravel-herd-cfd0553d26e0)" can help if you are having trouble getting it setup with VS Code.

### Local development using Docker and Nix
### Local development using Docker and Nix

#### Setup Instructions
#### Setup Instructions

1. Install [Nix](https://nixos.org/download/) for your system.
2. Run `nix-shell`.
3. If you are wanting to run Docker, follow the steps for your platform:
- **Linux**: On Linux, there are added aliases `dstart` & `dstop` that will start and stop the Docker daemon, which runs using rootlesskit.
- When using rootless, ensure that it is set up and allowed to run on privileged ports. See: [Exposing Privileged Ports](https://github.com/rootless-containers/rootlesskit/blob/master/docs/port.md#exposing-privileged-ports).
- You will also want to change the socket path with the following command:
1. Install [Nix](https://nixos.org/download/) for your system.
2. Run `nix-shell`.
3. If you are wanting to run Docker, follow the steps for your platform:
- **Linux**: On Linux, there are added aliases `dstart` & `dstop` that will start and stop the Docker daemon, which runs using rootlesskit.
- When using rootless, ensure that it is set up and allowed to run on privileged ports. See: [Exposing Privileged Ports](https://github.com/rootless-containers/rootlesskit/blob/master/docs/port.md#exposing-privileged-ports).
- You will also want to change the socket path with the following command:
```sh
export DOCKER_HOST=unix:///run/user/1000/docker.sock
```
- **Other Systems**: You will need to have Docker installed and running.
```
- **Other Systems**: You will need to have Docker installed and running.

#### Entering Development Environment

Expand Down Expand Up @@ -343,34 +343,34 @@ kflushall # Run kflush dev, staging, and prod environments
| `-p` | Prompts for confirmation before executing |
| `| grep ...` | Filters output based on pattern matching |

#### Environment Setup
#### Environment Setup

If the `.env` file does not exist, the script automatically generates it using `.env.local.template` and random secrets:
- `CIPHERSWEET_KEY` (32-byte hex string)
- `DB_PASSWORD` (16-byte hex string)
- `DB_ROOT_PASSWORD` (24-byte hex string)
- `REDIS_PASSWORD` (20-byte hex string)
- `APP_KEY` (generated using `php artisan key:generate`)
- `WWWUSER` (set to current user ID)
If the `.env` file does not exist, the script automatically generates it using `.env.local.template` and random secrets:
- `CIPHERSWEET_KEY` (32-byte hex string)
- `DB_PASSWORD` (16-byte hex string)
- `DB_ROOT_PASSWORD` (24-byte hex string)
- `REDIS_PASSWORD` (20-byte hex string)
- `APP_KEY` (generated using `php artisan key:generate`)
- `WWWUSER` (set to current user ID)

Ensure `.env.local.template` is available before running the script.
Ensure `.env.local.template` is available before running the script.

#### Rootless Docker Support
#### Rootless Docker Support

For users running `dockerd-rootless`, the script provides:
- Aliases:
For users running `dockerd-rootless`, the script provides:
- Aliases:
```sh
alias dstart="dockerd-rootless&"
alias dstop="pkill dockerd"
```
- Instructions to set the correct Docker socket:
```
- Instructions to set the correct Docker socket:
```sh
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
```
- To allow privileged ports, run:
```
- To allow privileged ports, run:
```sh
echo 1 | sudo tee /proc/sys/net/ipv4/ip_unprivileged_port_start
```
```

#### Troubleshooting

Expand Down Expand Up @@ -469,6 +469,13 @@ Runs other console commands in order and should be commands that are only run on

Runs other console commands in order and should be commands that should be run on each deploying container.

### app:migrate-settings-data

#### Purpose

Runs data migrations that cannot be included in the DB migrations. Should only be run oce across a multiple deploying
container.

### notifications:remove:old

#### Purpose
Expand Down
6 changes: 6 additions & 0 deletions app/Actions/Fortify/CreateNewUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ public function create(array $input): User
}
}

if ($input['context'] === UserContext::Individual->value) {
$input['notification_settings'] = ['engagements' => '1'];
}

Validator::make(
$input,
[
Expand All @@ -63,6 +67,7 @@ public function create(array $input): User
'locale' => ['required', Rule::in(config('locales.supported'))],
'accepted_privacy_policy' => 'accepted',
'accepted_terms_of_service' => 'accepted',
'notification_settings.engagements' => 'nullable|boolean',
],
[
'accepted_privacy_policy.accepted' => __('You must agree to the privacy policy.'),
Expand All @@ -88,6 +93,7 @@ public function create(array $input): User
'extra_attributes' => $input['extra_attributes'] ?? null,
'accepted_privacy_policy_at' => now(),
'accepted_terms_of_service_at' => now(),
'notification_settings' => $input['notification_settings'] ?? null,
]);
}
}
148 changes: 148 additions & 0 deletions app/Console/Commands/MigrateSettingsData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php

namespace App\Console\Commands;

use App\Models\Organization;
use App\Models\User;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Isolatable;

class MigrateSettingsData extends Command implements Isolatable
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:migrate-settings-data
{--list : lists out available migrations}
{--from=1.6.0 : when running all migrations, indicate which version the application is being migrated from. Previous migrations will be skipped.}
{--migration= : a specific migration to run}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Migrates settings data that is not performed by database migrations; such as modifying the contents of database fields.';

protected $migrations = [
'EnableEngagementNotifications' => [
'version' => '1.7.0',
'handler' => 'enableEngagementNotificationsMigration',
'description' => 'Replaces older format of notifications_settings with only ["engagements" => "1"]. Setting the engagement notifications on be default. If the notifications_settings contains a valid engagements setting, then no changes are made.',
],
];

/**
* Execute the console command.
*/
public function handle()
{
$verbose = $this->output->isVerbose();

if ($this->options()['list']) {

$this->listMigrations($this->migrations);

return 0;
}

if ($this->options()['migration']) {
try {
$migration = $this->migrations[$this->options()['migration']];
} catch (Exception $e) {
$this->fail('Could not find migration: '.$this->options()['migration']);

Check warning on line 56 in app/Console/Commands/MigrateSettingsData.php

View check run for this annotation

Codecov / codecov/patch

app/Console/Commands/MigrateSettingsData.php#L54-L56

Added lines #L54 - L56 were not covered by tests
}

if (isset($migration)) {
$handler = $migration['handler'];
$this->$handler();

Check warning on line 61 in app/Console/Commands/MigrateSettingsData.php

View check run for this annotation

Codecov / codecov/patch

app/Console/Commands/MigrateSettingsData.php#L59-L61

Added lines #L59 - L61 were not covered by tests

return 0;

Check warning on line 63 in app/Console/Commands/MigrateSettingsData.php

View check run for this annotation

Codecov / codecov/patch

app/Console/Commands/MigrateSettingsData.php#L63

Added line #L63 was not covered by tests
}
}

$this->runMigrations($this->migrations, $this->options()['from'], $verbose);
}

public function listMigrations($migrations)
{
$definitions = '';

foreach ($migrations as $name => $migration) {
$version = $migration['version'];
$description = $migration['description'];
$definitions .= "<dt>$name</dt><dd>Version added: $version</dd><dd class=\"pb-1\">$description</dd>";

$this->line("<options=bold>$name</> (Version added: $version)");
$this->info("$description");
$this->newLine();
}
}

public function runMigrations($migrations, $from = '1.6.0', $verbose = false)
{
$from = str_starts_with($from, 'v') || str_starts_with($from, 'V') ? substr($from, 1) : $from;
$migrationRunCount = 0;

foreach ($migrations as $name => $migration) {
if (version_compare($from, $migration['version'], '<')) {
if ($verbose) {
$this->line("<fg=cyan>Run migration - $name</>");
}
$handler = $migration['handler'];
$this->$handler($verbose);
$migrationRunCount++;
} elseif ($verbose) {
$this->comment("Skipped migration - $name");

Check warning on line 99 in app/Console/Commands/MigrateSettingsData.php

View check run for this annotation

Codecov / codecov/patch

app/Console/Commands/MigrateSettingsData.php#L99

Added line #L99 was not covered by tests
}

if ($verbose) {
$this->newLine();
}
}

$this->line('<options=bold;fg=green>Completed '.$migrationRunCount.'</>');
$this->line('<options=bold;fg=yellow>Skipped '.(count($migrations) - $migrationRunCount).'</>');
}

// Migrations

public function enableEngagementNotificationsMigration($verbose = false)
{
$updatedNotificationSettings = ['engagements' => '1'];

if ($verbose) {
$this->info(' - Migrating engagement notifications for users');
}

$users = User::where('context', 'individual')->whereNull('notification_settings->engagements')
->get();

$users->each(function ($user) use ($updatedNotificationSettings) {
// @phpstan-ignore assign.propertyType
$user->notification_settings = $updatedNotificationSettings;
$user->save();
});

if ($verbose) {
$this->info(' - Migrated '.$users->count().' users');
$this->info(' - Migrating engagement notification settings for Organizations');
}

$orgs = Organization::whereNull('notification_settings->engagements')
->get();

$orgs->each(function ($organization) use ($updatedNotificationSettings) {
// @phpstan-ignore assign.propertyType
$organization->notification_settings = $updatedNotificationSettings;
$organization->save();
});

if ($verbose) {
$this->info(' - Migrated '.$orgs->count().' Organizations');
}
}
}
2 changes: 1 addition & 1 deletion app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('app:refresh-dev') // use custom command to make sure that te commands are chained
$schedule->command('app:refresh-dev') // use custom command to make sure that the commands are chained
->daily() // Run daily at midnight
->environments(['dev']) // only run for APP_ENV tagged dev
->timezone('America/Los_Angeles') // Run as PST timezone
Expand Down
17 changes: 17 additions & 0 deletions app/Enums/YesNo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace App\Enums;

enum YesNo: string
{
case Yes = '1';
case No = '0';

public static function labels(): array
{
return [
'1' => __('Yes'),
'0' => __('No'),
];
}
}
17 changes: 17 additions & 0 deletions app/Http/Controllers/EngagementController.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use App\Models\Project;
use App\Models\User;
use App\Notifications\AccessNeedsFacilitationRequested;
use App\Notifications\EngagementAdded;
use App\Notifications\JoinedEngagement;
use App\Notifications\LeftEngagement;
use App\Notifications\OrganizationAddedToEngagement;
Expand All @@ -47,6 +48,7 @@
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification as FacadesNotification;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Notification;
Expand Down Expand Up @@ -422,6 +424,21 @@ public function update(UpdateEngagementRequest $request, Engagement $engagement)
if ($engagement->fresh()->isPublishable()) {
$engagement->update(['published_at' => now()]);
flash(__('Your engagement has been published.'), 'success|'.__('Your engagement has been published.', [], 'en'));

if ($engagement->recruitment === EngagementRecruitment::OpenCall->value) {
$users = User::where('context', 'individual')->withNotificationSettings('engagements', '1')->get();
FacadesNotification::send($users, new EngagementAdded($engagement));
}

$projectable = $engagement->project->projectable;

$otherOrgs = Organization::when($projectable instanceof Organization, fn ($query) => $query->whereNot(fn ($query) => $query->where('id', $projectable->id)))
->withNotificationSettings('engagements', '1')
->get();

if ($otherOrgs->count()) {
FacadesNotification::send($otherOrgs, new EngagementAdded($engagement));
}
}
} else {
flash(__('Your engagement has been updated.'), 'success|'.__('Your engagement has been updated.', [], 'en'));
Expand Down
6 changes: 2 additions & 4 deletions app/Http/Controllers/IndividualController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use App\Enums\IndividualRole;
use App\Enums\MeetingType;
use App\Enums\ProvinceOrTerritory;
use App\Enums\YesNo;
use App\Http\Requests\DestroyIndividualRequest;
use App\Http\Requests\SaveIndividualRolesRequest;
use App\Http\Requests\UpdateIndividualCommunicationAndConsultationPreferencesRequest;
Expand Down Expand Up @@ -137,10 +138,7 @@ public function edit(Individual $individual): View
'indigenousIdentities' => Options::forModels(Identity::query()->whereJsonContains('clusters', IdentityCluster::Indigenous))->toArray(),
'languages' => Options::forArray(get_available_languages(true))->nullable(__('Choose a language…'))->toArray(),
'livedExperiences' => Options::forModels(Identity::query()->whereJsonContains('clusters', IdentityCluster::LivedExperience)->withoutGlobalScope(ReachableIdentityScope::class))->toArray(),
'yesNoOptions' => Options::forArray([
'1' => __('Yes'),
'0' => __('No'),
])->toArray(),
'yesNoOptions' => Options::forEnum(YesNo::class)->toArray(),
'communityConnectorHasLivedExperience' => Options::forEnum(CommunityConnectorHasLivedExperience::class)->toArray(),
'contactPeople' => Options::forEnum(ContactPerson::class)->toArray(),
'meetingTypes' => Options::forEnum(MeetingType::class)->toArray(),
Expand Down
8 changes: 4 additions & 4 deletions app/Http/Controllers/OrganizationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use App\Enums\ProvinceOrTerritory;
use App\Enums\StaffHaveLivedExperience;
use App\Enums\TeamRole;
use App\Enums\YesNo;
use App\Http\Requests\DestroyOrganizationRequest;
use App\Http\Requests\SaveOrganizationRolesRequest;
use App\Http\Requests\StoreOrganizationLanguagesRequest;
Expand Down Expand Up @@ -82,6 +83,8 @@ public function store(StoreOrganizationRequest $request): RedirectResponse

$data['languages'] = get_supported_locales(false);

$data['notification_settings'] = ['engagements' => '1'];

$organization = Organization::create($data);

session()->forget('type');
Expand Down Expand Up @@ -183,10 +186,7 @@ public function edit(Organization $organization): View
'indigenousIdentities' => Options::forModels(Identity::query()->whereJsonContains('clusters', IdentityCluster::Indigenous))->toArray(),
'languages' => Options::forArray(get_available_languages(true))->nullable(__('Choose a language…'))->toArray(),
'livedExperiences' => Options::forModels(Identity::query()->whereJsonContains('clusters', IdentityCluster::LivedExperience)->withoutGlobalScope(ReachableIdentityScope::class))->toArray(),
'yesNoOptions' => Options::forArray([
'1' => __('Yes'),
'0' => __('No'),
])->toArray(),
'yesNoOptions' => Options::forEnum(YesNo::class)->toArray(),
'staffHaveLivedExperience' => Options::forEnum(StaffHaveLivedExperience::class)->toArray(),
]);
}
Expand Down
Loading