diff --git a/docker-compose.yml b/docker-compose.yml index 571f24926..7ff09e651 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,8 @@ services: POSTGRES_PASSWORD: myradio volumes: - db-data:/var/lib/postgresql/data + ports: + - 55432:5432 memcached: image: memcached:alpine diff --git a/schema/patches/16.sql b/schema/patches/16.sql new file mode 100644 index 000000000..0780762d5 --- /dev/null +++ b/schema/patches/16.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE public.officer + ADD COLUMN num_places INT DEFAULT 1; + +COMMIT; diff --git a/src/Classes/MyRadioTwig.php b/src/Classes/MyRadioTwig.php index 705332f9d..2fb0ced46 100644 --- a/src/Classes/MyRadioTwig.php +++ b/src/Classes/MyRadioTwig.php @@ -98,7 +98,7 @@ public function __construct() * @param string $name The name of the variable * @param mixed $value The value of the variable - literally any valid type * - * @return \MyRadioTwig This for chaining + * @return MyRadioTwig This for chaining */ public function addVariable($name, $value) { diff --git a/src/Classes/ServiceAPI/MyRadio_Officer.php b/src/Classes/ServiceAPI/MyRadio_Officer.php index 3df9f7d83..db02a822f 100644 --- a/src/Classes/ServiceAPI/MyRadio_Officer.php +++ b/src/Classes/ServiceAPI/MyRadio_Officer.php @@ -82,6 +82,12 @@ class MyRadio_Officer extends ServiceAPI */ private $permissions; + /** + * How many people can have this officership - should be 1 for most. + * @var int + */ + private $num_places; + protected function __construct($id) { $result = self::$db->fetchOne( @@ -101,6 +107,7 @@ protected function __construct($id) $this->description = $result['descr']; $this->status = $result['status']; $this->type = $result['type']; + $this->num_places = (int) $result['num_places']; //Get the officer's permissions $this->updatePermissions(); @@ -110,23 +117,24 @@ protected function __construct($id) /** * Create a new Officer position. * - * @param string $name The position name, e.g. "Station Cat" - * @param string $descr A description of the position "official feline" - * @param string $alias Email alias (may be NULL) e.g. station.cat - * @param int $ordering Weighting when appearing in lists e.g. 0 - * @param MyRadio_Team $team The Team the Officer is part of - * @param char $type 'm'ember, 'o'fficer, 'a'ssistant head, 'h'ead + * @param string $name The position name, e.g. "Station Cat" + * @param string $descr A description of the position "official feline" + * @param string $alias Email alias (may be NULL) e.g. station.cat + * @param int $ordering Weighting when appearing in lists e.g. 0 + * @param MyRadio_Team $team The Team the Officer is part of + * @param char $type 'm'ember, 'o'fficer, 'a'ssistant head, 'h'ead + * @param int $num_places How many people can have this officership (default 1) * * @return MyRadio_Officer The new Officer position */ - public static function createOfficer($name, $descr, $alias, $ordering, MyRadio_Team $team, $type = 'o') + public static function createOfficer($name, $descr, $alias, $ordering, MyRadio_Team $team, $type = 'o', $num_places = 1) { return self::getInstance( self::$db->fetchColumn( 'INSERT INTO public.officer - (officer_name, officer_alias, teamid, ordering, descr, type) - VALUES ($1, $2, $3, $4, $5, $6) RETURNING officerid', - [$name, $alias, $team->getID(), $ordering, $descr, $type] + (officer_name, officer_alias, teamid, ordering, descr, type, num_places) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING officerid', + [$name, $alias, $team->getID(), $ordering, $descr, $type, $num_places] )[0] ); } @@ -543,6 +551,22 @@ private function considerEmailingNewOfficer(MyRadio_User $member) ); } + public function getNumPlaces() + { + return $this->num_places; + } + + public function setNumPlaces(int $val) + { + self::$db->query(' + UPDATE public.officer + SET num_places = $1 + WHERE officerid = $2 + ', [$val, $this->getID()]); + $this->num_places = $val; + $this->updateCacheObject(); + } + /** * Returns all the officer's active permission flags. * @@ -659,6 +683,16 @@ public static function getForm() 'required' => false, ] ) + )->addField( + new MyRadioFormField( + 'num_places', + MyRadioFormField::TYPE_NUMBER, + [ + 'label' => 'Number of Places', + 'explanation' => 'How many people can have this officership at a time.', + 'value' => 1, + ] + ) )->addField( new MyRadioFormField( 'status', @@ -759,6 +793,7 @@ public function getEditForm() 'team' => $this->getTeam()->getID(), 'type' => $this->getType(), 'status' => $this->getStatus(), + 'num_places' => $this->getNumPlaces(), 'permissions.permission' => array_map( function ($perm) { return $perm['value']; @@ -828,6 +863,7 @@ public function toDataSource($mixins = []) 'description' => $this->getDescription(), 'status' => $this->getStatus(), 'type' => $this->getType(), + 'num_places' => $this->getNumPlaces(), ]; $this->addMixins($data, $mixins, $mixin_funcs); diff --git a/src/Classes/ServiceAPI/Profile.php b/src/Classes/ServiceAPI/Profile.php index 6357b5912..6fdf2edb9 100644 --- a/src/Classes/ServiceAPI/Profile.php +++ b/src/Classes/ServiceAPI/Profile.php @@ -135,7 +135,7 @@ public static function getOfficers() if (self::$officers === false) { self::wakeup(); self::$officers = self::$db->fetchAll( - 'SELECT team.team_name AS team, officer.type, officer.officer_name AS officership, + 'SELECT team.team_name AS team, officer.type, officer.officer_name AS officership, officer.num_places, fname || \' \' || sname AS name, member.memberid, officer.officerid FROM team LEFT JOIN officer ON team.teamid = officer.teamid AND officer.status = \'c\' @@ -145,8 +145,43 @@ public static function getOfficers() WHERE team.status = \'c\' AND officer.type != \'m\' ORDER BY team.ordering, officer.ordering, sname' ); + // Insert into the list duplicate entries for vacant positions + // For example, if ASM has num_positions=2, and only one is filled, + // we need to insert another ASM entry with null name/memberid + + // keyed by officerid + $num_filled_positions = []; + // dto - avoid iterating multiple times + $num_avail_positions = []; + $last_index_of_officer = []; + foreach (self::$officers as $i => $off) { + $num_avail_positions[$off['officerid']] = (int)$off['num_places']; + if ($off['memberid'] !== null) { + $num_filled_positions[$off['officerid']]++; + } + $last_index_of_officer[$off['officerid']] = $i; + } + + $offset = 0; + foreach ($num_avail_positions as $officerid => $avail) { + if ($num_filled_positions[$officerid] < $avail && $avail > 1) { + $vacancies = ($avail ?? 1) - $num_filled_positions[$officerid]; + if ($num_filled_positions[$officerid] == 0) { + $vacancies--; + } + for ($i = 0; $i < $vacancies; $i++) { + $vacancy = self::$officers[$last_index_of_officer[$officerid]+$offset]; + $vacancy['memberid'] = null; + $vacancy['name'] = null; + array_splice(self::$officers, $last_index_of_officer[$officerid]+$offset+1, 0, [$vacancy]); + $offset++; + } + } + } + self::$cache->set('MyRadioProfile_officers', self::$officers); } + // var_dump(self::$officers); return self::$officers; } diff --git a/src/Controllers/Profile/editOfficer.php b/src/Controllers/Profile/editOfficer.php index f26de5356..6071bf7f6 100644 --- a/src/Controllers/Profile/editOfficer.php +++ b/src/Controllers/Profile/editOfficer.php @@ -17,7 +17,8 @@ $data['alias'], $data['ordering'], $data['team'], - $data['type'] + $data['type'], + (int) $data['num_places'] ); } else { //submit edit @@ -31,7 +32,8 @@ ->setOrdering($data['ordering']) ->setTeam($data['team']) ->setType($data['type']) - ->setStatus($data['status']); + ->setStatus($data['status']) + ->setNumPlaces((int) $data['num_places']); // remove empty permissions values $data['permissions'] = array_filter($data['permissions']['permission']) ?: []; diff --git a/src/Public/js/myradio.profile.listOfficers.js b/src/Public/js/myradio.profile.listOfficers.js index 66e54f181..cb198b8bd 100644 --- a/src/Public/js/myradio.profile.listOfficers.js +++ b/src/Public/js/myradio.profile.listOfficers.js @@ -12,6 +12,10 @@ $(".twig-datatable").dataTable({ { "sTitle": "Officership", }, + // numPlaces + { + bVisible: false + }, //name { "sTitle": "Name"