diff --git a/app/Http/Controllers/CalendarEventsController.php b/app/Http/Controllers/CalendarEventsController.php index 762fece246..7926dae3e1 100644 --- a/app/Http/Controllers/CalendarEventsController.php +++ b/app/Http/Controllers/CalendarEventsController.php @@ -106,7 +106,16 @@ public function exportCalendar($events) $ical[] = 'PRODID:-//Restarters//NONSGML Events Calendar/EN'; // loop over events + $me = auth()->user(); + foreach ($events as $event) { + // We need to filter by approval status. If the event is not approved, we can only see it if we are + // an admin, network coordinator, or the host of the event. + + if (!User::userCanSeeEvent($me, $event)) { + continue; + } + if (! is_null($event->event_start_utc) ) { $ical[] = 'BEGIN:VEVENT'; @@ -119,7 +128,16 @@ public function exportCalendar($events) $ical[] = 'DESCRIPTION:'.url('/party/view').'/'.$event->idevents; $ical[] = "LOCATION:{$event->location}"; $ical[] = 'URL:'.url('/party/view').'/'.$event->idevents; - $ical[] = 'STATUS:CONFIRMED'; + + if ($event->cancelled) { + $ical[] = 'STATUS:CANCELLED'; + } else if ($event->approved && $event->theGroup->approved) { + // Events are only confirmed once the event and the group are approved. + $ical[] = 'STATUS:CONFIRMED'; + } else { + $ical[] = 'STATUS:TENTATIVE'; + } + $ical[] = 'END:VEVENT'; } } diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php index 3383d026a1..9006b809d0 100644 --- a/app/Http/Controllers/ExportController.php +++ b/app/Http/Controllers/ExportController.php @@ -11,6 +11,7 @@ use App\Helpers\SearchHelper; use App\Party; use App\Search; +use App\User; use App\UserGroups; use Auth; use Carbon\Carbon; @@ -18,6 +19,7 @@ use DB; use Illuminate\Http\Request; use Response; +use Illuminate\Database\Eloquent\Collection; class ExportController extends Controller { @@ -71,6 +73,8 @@ public function devices(Request $request, $idevents = NULL, $idgroups = NULL) $filename .= '.csv'; $file = fopen(base_path() . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR . $filename, 'w+'); + $me = auth()->user(); + // Do not include model column if ($host == 'therestartproject.org') { $columns = [ @@ -90,32 +94,35 @@ public function devices(Request $request, $idevents = NULL, $idgroups = NULL) fputcsv($file, $columns); foreach ($all_devices as $device) { - $wasteImpact = 0; - $co2Diverted = 0; - - if ($device->isFixed()) { - if ($device->deviceCategory->powered) { - $wasteImpact = $device->eWasteDiverted(); - $co2Diverted = $device->eCo2Diverted($eEmissionRatio, $displacementFactor); - } else { - $wasteImpact = $device->uWasteDiverted(); - $co2Diverted = $device->uCo2Diverted($uEmissionratio, $displacementFactor); + set_time_limit(60); + if (User::userCanSeeEvent($me, $event)) { + $wasteImpact = 0; + $co2Diverted = 0; + + if ($device->isFixed()) { + if ($device->deviceCategory->powered) { + $wasteImpact = $device->eWasteDiverted(); + $co2Diverted = $device->eCo2Diverted($eEmissionRatio, $displacementFactor); + } else { + $wasteImpact = $device->uWasteDiverted(); + $co2Diverted = $device->uCo2Diverted($uEmissionratio, $displacementFactor); + } } - } - fputcsv($file, [ - $device->item_type, - $device->deviceCategory->name, - $device->brand, - $device->problem, - $device->getRepairStatus(), - $device->getSpareParts(), - $device->deviceEvent->getEventName(), - $device->deviceEvent->theGroup->name, - $device->deviceEvent->getFormattedLocalStart('Y-m-d'), - $wasteImpact, - $co2Diverted - ]); + fputcsv($file, [ + $device->item_type, + $device->deviceCategory->name, + $device->brand, + $device->problem, + $device->getRepairStatus(), + $device->getSpareParts(), + $device->deviceEvent->getEventName(), + $device->deviceEvent->theGroup->name, + $device->deviceEvent->getFormattedLocalStart('Y-m-d'), + $wasteImpact, + $co2Diverted + ]); + } } } else { $columns = [ @@ -134,35 +141,44 @@ public function devices(Request $request, $idevents = NULL, $idgroups = NULL) ]; fputcsv($file, $columns); + $party = null; foreach ($all_devices as $device) { - $wasteImpact = 0; - $co2Diverted = 0; - - if ($device->isFixed()) { - if ($device->deviceCategory->powered) { - $wasteImpact = $device->eWasteDiverted(); - $co2Diverted = $device->eCo2Diverted($eEmissionRatio, $displacementFactor); - } else { - $wasteImpact = $device->uWasteDiverted(); - $co2Diverted = $device->uCo2Diverted($uEmissionratio, $displacementFactor); + set_time_limit(60); + $party = !$party || $party->idevents != $device->event ? Party::findOrFail($device->event) : $party; + + if (User::userCanSeeEvent($me, $party)) { + $wasteImpact = 0; + $co2Diverted = 0; + + if ($device->isFixed()) + { + if ($device->deviceCategory->powered) + { + $wasteImpact = $device->eWasteDiverted(); + $co2Diverted = $device->eCo2Diverted($eEmissionRatio, $displacementFactor); + } else + { + $wasteImpact = $device->uWasteDiverted(); + $co2Diverted = $device->uCo2Diverted($uEmissionratio, $displacementFactor); + } } - } - fputcsv($file, [ - $device->item_type, - $device->deviceCategory->name, - $device->brand, - $device->model, - $device->problem, - $device->getRepairStatus(), - $device->getSpareParts(), - $device->deviceEvent->getEventName(), - $device->deviceEvent->theGroup->name, - $device->deviceEvent->getFormattedLocalStart('Y-m-d'), - $wasteImpact, - $co2Diverted, - ]); + fputcsv($file, [ + $device->item_type, + $device->deviceCategory->name, + $device->brand, + $device->model, + $device->problem, + $device->getRepairStatus(), + $device->getSpareParts(), + $device->deviceEvent->getEventName(), + $device->deviceEvent->theGroup->name, + $device->deviceEvent->getFormattedLocalStart('Y-m-d'), + $wasteImpact, + $co2Diverted, + ]); + } } } @@ -205,7 +221,7 @@ public function parties(Request $request) }); $k = implode(' ', $key); }); - $headers = array_merge(['Date', 'Venue', 'Group'], $statsKeys); + $headers = array_merge(['Date', 'Venue', 'Group', 'Approved'], $statsKeys); // Send these to getEventStats() to speed things up a bit. $eEmissionRatio = \App\Helpers\LcaStats::getEmissionRatioPowered(); @@ -223,6 +239,7 @@ public function parties(Request $request) $party->getFormattedLocalStart(), $party->getEventName(), $party->theGroup && $party->theGroup->name ? $party->theGroup->name : '?', + $party->approved ? 'true' : 'false', ]; $PartyArray[$i] += $stats; } @@ -260,8 +277,14 @@ public function getTimeVolunteered(Request $request, $search = null, $export = f $all_group_tags = GroupTags::all(); //Get all applicable groups - if (Fixometer::hasRole($user, 'Administrator')) { + if (Fixometer::hasRole($user, 'Administrator')) + { $all_groups = Group::all(); + } else if (Fixometer::hasRole($user, 'NetworkCoordinator')) { + $all_groups = new Collection(); + foreach ($user->networks as $network) { + $all_groups->merge($network->groups); + } } elseif (Fixometer::hasRole($user, 'Host')) { $host_groups = UserGroups::where('user', $user->id)->where('role', 3)->pluck('group')->toArray(); $all_groups = Group::whereIn('groups.idgroups', $host_groups); diff --git a/app/Http/Resources/Party.php b/app/Http/Resources/Party.php index a34600bebd..00f8e2c012 100644 --- a/app/Http/Resources/Party.php +++ b/app/Http/Resources/Party.php @@ -26,6 +26,16 @@ class Party extends JsonResource * format="int64", * example=1 * ) + * @OA\Property( + * property="approved", + * title="approved", + * description="Whether this event has been approved.", + * format="boolean", + * example="false" + * ) + */ + + /** * @OA\Property( * property="start", * title="start", @@ -228,6 +238,7 @@ public function toArray($request) // We return information which can be public, and we rename fields to look more consistent. return [ 'id' => $this->idevents, + 'approved' => $this->approved ? true : false, 'start' => $this->event_start_utc, 'end' => $this->event_end_utc, 'timezone' => $this->timezone, diff --git a/app/Http/Resources/PartySummary.php b/app/Http/Resources/PartySummary.php index a3ea782a60..403f8a5347 100644 --- a/app/Http/Resources/PartySummary.php +++ b/app/Http/Resources/PartySummary.php @@ -26,6 +26,16 @@ class PartySummary extends JsonResource * format="int64", * example=1 * ) + * @OA\Property( + * property="approved", + * title="approved", + * description="Whether this event has been approved.", + * format="boolean", + * example="false" + * ) + */ + + /** * @OA\Property( * property="start", * title="start", @@ -110,6 +120,7 @@ public function toArray($request) // peculiarity which I can't get to the bottom of. So pull it from the resource. return [ 'id' => $this->idevents, + 'approved' => $this->approved ? true : false, 'start' => $this->event_start_utc, 'end' => $this->event_end_utc, 'timezone' => $this->timezone, diff --git a/app/Party.php b/app/Party.php index 346139ae02..7b995d951c 100644 --- a/app/Party.php +++ b/app/Party.php @@ -155,7 +155,7 @@ public function ofTheseGroups($groups = 'admin', $only_past = false, $devices = *, `e`.`venue` AS `venue`, `e`.`link` AS `link`, `e`.`location` as `location`, `g`.`name` AS group_name, - UNIX_TIMESTAMP(e.`event_start_utc`) ) AS `event_timestamp` + UNIX_TIMESTAMP(e.`event_start_utc`) AS `event_timestamp` FROM `'.$this->table.'` AS `e` INNER JOIN `groups` as `g` ON `e`.`group` = `g`.`idgroups` diff --git a/app/Search.php b/app/Search.php index 6ffcac4a55..174b239e17 100644 --- a/app/Search.php +++ b/app/Search.php @@ -3,6 +3,7 @@ namespace App; use App\Device; +use App\Helpers\Fixometer; use DB; use Illuminate\Database\Eloquent\Model; @@ -41,7 +42,15 @@ public function parties($list = [], $groups = [], $from = null, $to = null, $gro $eventsQuery->orderBy('events.event_start_utc', 'desc'); // We need to explicitly select what we want to return otherwise gtag.group might overwrite events.group. - return $eventsQuery->select(['events.*', 'gtag.group_tag'])->get(); + $events = $eventsQuery->select(['events.*', 'gtag.group_tag'])->get(); + + $me = auth()->user(); + + $events = $events->filter(function ($event) use ($me) { + return User::userCanSeeEvent($me, $event); + }); + + return $events; } public function deviceStatusCount($parties) diff --git a/app/User.php b/app/User.php index 7f62755124..2506930f35 100644 --- a/app/User.php +++ b/app/User.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use App\Events\UserDeleted; use App\Events\UserUpdated; +use App\Helpers\Fixometer; use App\Network; use App\UserGroups; use App\UsersPermissions; @@ -582,4 +583,23 @@ public function preferredLocale() // to users (not admins). return $this->language; } + + public static function userCanSeeEvent($user, $event) { + // We need to filter based on approved visibility: + // - where the group is approved, this event is visible + // - where the group is not approved, this event is visible to network coordinators or group hosts. + $amHost = $user && $user->hasRole('Host'); + $admin = $user && $user->hasRole('Administrator'); + + $group = Group::find($event->group); + + if (($event->approved && $group->approved) || + $admin || + ($user && $user->isCoordinatorForGroup($group)) || + ($amHost && $user && Fixometer::userIsHostOfGroup($group->idgroups, $user->id))) { + return true; + } + + return false; + } } diff --git a/public/icons/map_marker_ico.svg b/public/icons/map_marker_ico.svg deleted file mode 100644 index 6a2f7c08c8..0000000000 --- a/public/icons/map_marker_ico.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resources/views/layouts/navbar.blade.php b/resources/views/layouts/navbar.blade.php index 6891f4eff2..d38def1bc2 100644 --- a/resources/views/layouts/navbar.blade.php +++ b/resources/views/layouts/navbar.blade.php @@ -17,10 +17,12 @@ diff --git a/tests/Feature/Calendar/CalendarTest.php b/tests/Feature/Calendar/CalendarTest.php index 6aaf00be0e..11486972f6 100644 --- a/tests/Feature/Calendar/CalendarTest.php +++ b/tests/Feature/Calendar/CalendarTest.php @@ -35,6 +35,8 @@ protected function setUp(): void ]); $group->addVolunteer($host); $group->makeMemberAHost($host); + $group->approved = true; + $group->save(); $this->group = $group; $group2 = Group::factory()->create([ @@ -75,6 +77,7 @@ public function testByUser() { $this->expectOutputRegex('/VEVENT/'); $this->expectOutputString($this->start); $this->expectOutputString($this->end); + $this->expectOutputString('CONFIRMED'); // Invalid hash. $this->expectException(\Exception::class); @@ -118,4 +121,41 @@ public function testAll() { $this->expectException(NotFoundHttpException::class); $response = $this->get('/calendar/all-events/' . env('CALENDAR_HASH') . '1'); } + + public function testCancelled() { + $this->event->cancelled = 1; + $this->event->save(); + $response = $this->get('/calendar/user/' . $this->host->calendar_hash); + $response->assertStatus(200); + $this->expectOutputRegex('/CANCELLED/'); + } + + public function testEventNotApproved() { + $this->event->approved = false; + $this->event->save(); + $response = $this->get('/calendar/user/' . $this->host->calendar_hash); + $response->assertStatus(200); + $this->expectOutputRegex('/TENTATIVE/'); + } + + public function testGroupNotApproved() { + $this->group->approved = false; + $this->group->save(); + $response = $this->get('/calendar/user/' . $this->host->calendar_hash); + $response->assertStatus(200); + $this->expectOutputRegex('/TENTATIVE/'); + } + + public function testEventNotVisible() { + $host = User::factory()->create([ + 'latitude' => 50.64, + 'longitude' => 5.58, + 'location' => 'London', + 'calendar_hash' => \Str::random(15) + ]); + $this->actingAs($host); + $response = $this->get('/calendar/user/' . $host->calendar_hash); + $response->assertStatus(200); + $this->assertStringNotContainsString('/VEVENT/', $response->getContent()); + } } \ No newline at end of file diff --git a/tests/Feature/Events/APIv2EventTest.php b/tests/Feature/Events/APIv2EventTest.php index e49f767537..ffd20b15c5 100644 --- a/tests/Feature/Events/APIv2EventTest.php +++ b/tests/Feature/Events/APIv2EventTest.php @@ -71,6 +71,7 @@ public function testGetEventsForGroup() { $json = json_decode($response->getContent(), true); self::assertEquals(1, count($json)); self::assertEquals($id1, $json[0]['id']); + self::assertFalse($json[0]['approved']); } diff --git a/tests/Feature/Events/ExportTest.php b/tests/Feature/Events/ExportTest.php index 5f11b61e81..cca647c679 100644 --- a/tests/Feature/Events/ExportTest.php +++ b/tests/Feature/Events/ExportTest.php @@ -8,34 +8,75 @@ use App\Helpers\RepairNetworkService; use App\Network; use App\Party; +use App\Role; use App\User; +use App\UserGroups; use DB; use Tests\TestCase; class ExportTest extends TestCase { - public function testExport() + /** + * @dataProvider roleProvider + */ + public function testExport($role) { $network = Network::factory()->create(); - $host = User::factory()->administrator()->create(); - $this->actingAs($host); + $admin = User::factory()->administrator()->create(); - // Create two groups. + switch ($role) { + case 'Administrator': $user = User::factory()->administrator()->create(); break; + case 'NetworkCoordinator': $user = User::factory()->networkCoordinator()->create(); break; + case 'Host': $user = User::factory()->host()->create(); break; + } + + if ($role == 'NetworkCoordinator') { + $network->addCoordinator($user); + } + + $this->actingAs($admin); + + // Create three groups, two approved and one not. $group1 = Group::factory()->create([ 'name' => 'test1' ]); $this->networkService = new RepairNetworkService(); - $this->networkService->addGroupToNetwork($host, $group1, $network); - $group1->addVolunteer($host); - $group1->makeMemberAHost($host); + $this->networkService->addGroupToNetwork($admin, $group1, $network); + + if ($role == 'Host') { + $group1->addVolunteer($user); + $group1->makeMemberAHost($user); + } + + $group1->approved = true; + $group1->save(); $group2 = Group::factory()->create([ 'name' => 'test2' ]); - $this->networkService->addGroupToNetwork($host, $group2, $network); - $group2->addVolunteer($host); - $group2->makeMemberAHost($host); + $this->networkService->addGroupToNetwork($admin, $group2, $network); + if ($role == 'Host') { + $group2->addVolunteer($user); + $group2->makeMemberAHost($user); + } + + $group2->approved = true; + $group2->save(); + + $group3 = Group::factory()->create([ + 'name' => 'test3' + ]); + $this->networkService->addGroupToNetwork($admin, $group3, $network); + if ($role == 'Host') { + $group3->addVolunteer($user); + $group3->makeMemberAHost($user); + } + + $group3->approved = false; + $group3->save(); + + $this->actingAs($user); // Create an event on each and approve it. $idevents1 = $this->createEvent($group1->idgroups, '2000-01-02'); @@ -50,6 +91,11 @@ public function testExport() $event2->approved = true; $event2->save(); + $idevents3 = $this->createEvent($group3->idgroups, '2000-01-01'); + $event3 = Party::find($idevents3); + $event3->approved = true; + $event3->save(); + // Add a device for the events. $device = Device::factory()->fixed()->create([ 'category' => 111, @@ -61,8 +107,13 @@ public function testExport() 'category_creation' => 111, 'event' => $idevents2, ]); + $device = Device::factory()->fixed()->create([ + 'category' => 111, + 'category_creation' => 111, + 'event' => $idevents3, + ]); // Export parties. - $response = $this->get("/export/parties?fltr=dummy&parties[0]=$idevents1&parties[1]=$idevents2&from-date=&to-date="); + $response = $this->get("/export/parties?fltr=dummy&parties[0]=$idevents1&parties[1]=$idevents2&parties[2]=$idevents3&from-date=&to-date="); // Bit hacky, but grab the file that was created. Can't find a way to do this in Laravel easily, though it's // probably possible using mocking. @@ -72,10 +123,25 @@ public function testExport() $fh = fopen($filename, 'r'); fgetcsv($fh); $row2 = fgetcsv($fh); + self::assertEquals('true', e($row2[3])); self::assertEquals($group1->name, $row2[2]); $row3 = fgetcsv($fh); + self::assertEquals('true', e($row3[3])); self::assertEquals($group2->name, $row3[2]); + // Should return the third event as it's for an unapproved group but we're a host. + $row4 = fgetcsv($fh); + self::assertEquals('true', e($row4[3])); + self::assertEquals($group3->name, $row4[2]); + + if ($role == 'Host') { + // Now remove us as a host of the third group so that it's no longer included in exports. + $userGroupAssociation = UserGroups::where('user', $user->id) + ->where('group', $group3->idgroups)->first(); + $userGroupAssociation->role = Role::RESTARTER; + $userGroupAssociation->save(); + } + // Export devices. $response = $this->get("/export/devices"); $header = $response->headers->get('content-disposition'); @@ -87,6 +153,13 @@ public function testExport() self::assertEquals(e($event1->getEventName()), e($row2[7])); $row3 = fgetcsv($fh); self::assertEquals(e($event2->getEventName()), e($row3[7])); + $row4 = fgetcsv($fh); + + if ($role == 'Host') { + self::assertFalse($row4); + } else { + self::assertEquals(e($event3->getEventName()), e($row4[7])); + } // Export devices for a particular event. $response = $this->get("/export/devices/event/$idevents1"); @@ -97,7 +170,6 @@ public function testExport() $row2 = fgetcsv($fh); self::assertEquals(e($event1->getEventName()), e($row2[7])); $row3 = fgetcsv($fh); - self::assertFalse($row3); $response = $this->get("/export/devices/event/$idevents2"); $header = $response->headers->get('content-disposition'); @@ -106,8 +178,19 @@ public function testExport() fgetcsv($fh); $row2 = fgetcsv($fh); self::assertEquals(e($event2->getEventName()), e($row2[7])); - $row3 = fgetcsv($fh); - self::assertFalse($row3); + + $response = $this->get("/export/devices/event/$idevents3"); + $header = $response->headers->get('content-disposition'); + $filename = public_path() . '/' . substr($header, strpos($header, 'filename=') + 9); + $fh = fopen($filename, 'r'); + fgetcsv($fh); + $row2 = fgetcsv($fh); + + if ($role == 'Host') { + self::assertFalse($row2); + } else { + self::assertEquals(e($event3->getEventName()), e($row2[7])); + } // Export devices for a particular group. $response = $this->get("/export/devices/group/{$group1->idgroups}"); @@ -117,8 +200,6 @@ public function testExport() fgetcsv($fh); $row2 = fgetcsv($fh); self::assertEquals(e($event1->getEventName()), e($row2[7])); - $row3 = fgetcsv($fh); - self::assertFalse($row3); $response = $this->get("/export/devices/group/{$group2->idgroups}"); $header = $response->headers->get('content-disposition'); @@ -127,9 +208,19 @@ public function testExport() fgetcsv($fh); $row2 = fgetcsv($fh); self::assertEquals(e($event2->getEventName()), e($row2[7])); - $row3 = fgetcsv($fh); - self::assertFalse($row3); + $response = $this->get("/export/devices/group/{$group3->idgroups}"); + $header = $response->headers->get('content-disposition'); + $filename = public_path() . '/' . substr($header, strpos($header, 'filename=') + 9); + $fh = fopen($filename, 'r'); + fgetcsv($fh); + $row2 = fgetcsv($fh); + + if ($role == 'Host') { + self::assertFalse($row2); + } else { + self::assertEquals(e($event3->getEventName()), e($row2[7])); + } // Export time volunteered - first as a web page. $response = $this->get("/reporting/time-volunteered?a"); @@ -144,4 +235,12 @@ public function testExport() $row2 = fgetcsv($fh); $this->assertEquals('Hours Volunteered', $row2[0]); } + + public function roleProvider() { + return [ + [ 'Administrator' ], + [ 'NetworkCoordinator' ], + [ 'Host' ], + ]; + } } diff --git a/tests/Feature/Fixometer/BasicTest.php b/tests/Feature/Fixometer/BasicTest.php index cc973dfef1..312dc3862c 100644 --- a/tests/Feature/Fixometer/BasicTest.php +++ b/tests/Feature/Fixometer/BasicTest.php @@ -7,6 +7,7 @@ use App\Group; use App\Helpers\RepairNetworkService; use App\Party; +use App\Role; use App\User; use DB; use Hash; @@ -83,7 +84,7 @@ public function testPageLoads() } public function testExport() { - $this->loginAsTestUser(); + $this->loginAsTestUser(Role::ADMINISTRATOR); DB::statement('SET foreign_key_checks=0'); Category::truncate();