Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/docker-base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
org.opencontainers.image.vendor=BMLT
org.opencontainers.image.created={{date 'YYYY-MM-DDTHH:mm:ssZ'}}
org.opencontainers.image.version=${{ matrix.php_version }}
org.opencontainers.image.revision=${{ github.sha }}
php.version=${{ matrix.php_version }}

- name: Build and push Base
Expand Down
41 changes: 41 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,47 @@ You should now be able to set breakpoints, launch this debug configuration, and

This works exactly as described in the [vitest documentation](https://v0.vitest.dev/guide/debugging.html). Set any breakpoints, launch a new JavaScript Debug Terminal, and run `npm run test`.

## Developing with the TypeScript Client

When developing Admin API features alongside the TypeScript client, you can use [npm link](https://docs.npmjs.com/cli/v11/commands/npm-link/) to work with both repositories locally without publishing to npm.

### Prerequisites
Clone the BMLT TypeScript client repository:
```bash
git clone https://github.com/bmlt-enabled/bmlt-server-typescript-client.git
```

### One-time Setup
1. Link the TypeScript client globally:
```bash
cd /path/to/bmlt-server-typescript-client
npm link
```

2. Link the client in the BMLT server frontend:
```bash
cd /path/to/bmlt-server/src
npm link bmlt-server-typescript-client
```

### Development Workflow
After making API changes, regenerate the TypeScript client:

1. Generate updated OpenAPI documentation:
```bash
cd /path/to/bmlt-server
make generate-api-json
```

2. Regenerate the TypeScript client:
```bash
cd /path/to/bmlt-server-typescript-client
rm openapi.json
make generate
```

This allows you to develop the API without changing your configs or imports.

## Some useful `make` commands

- `make help` Describe all of the make commands.
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ zip: $(ZIP_FILE) ## Builds zip file

.PHONY: docker
docker: zip ## Builds Docker Image
docker build --pull --build-arg PHP_VERSION=$(BASE_IMAGE_TAG) -f docker/$(DOCKERFILE) . -t $(IMAGE):$(TAG)
docker build --pull --build-arg PHP_VERSION=$(BASE_IMAGE_TAG) --label "org.opencontainers.image.revision=$(COMMIT)" --label "org.opencontainers.image.created=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')" -f docker/$(DOCKERFILE) . -t $(IMAGE):$(TAG)

.PHONY: docker-push
docker-push: ## Pushes docker image to Dockerhub
Expand Down Expand Up @@ -169,7 +169,7 @@ phpstan: ## PHP Larastan Code Analysis

.PHONY: docker-publish-base
docker-publish-base: ## Builds Base Docker Image
docker buildx build --platform linux/amd64,linux/arm64/v8 -f docker/Dockerfile-base docker/ -t $(BASE_IMAGE):$(BASE_IMAGE_TAG) --push
docker buildx build --platform linux/amd64,linux/arm64/v8 --label "org.opencontainers.image.revision=$(COMMIT)" --label "org.opencontainers.image.created=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')" -f docker/Dockerfile-base docker/ -t $(BASE_IMAGE):$(BASE_IMAGE_TAG) --push

.PHONY: mysql
mysql: ## Runs mysql cli in mysql container
Expand Down
18 changes: 7 additions & 11 deletions src/app/Console/Commands/ImportRootServers.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,8 @@ private function importRootServersList(RootServerRepositoryInterface $rootServer
{
try {
$url = $this->option('list-url');
$response = $this->httpGet($url);
$externalRootServers = collect($response)
->map(function ($rootServer) {
try {
return new ExternalRootServer($rootServer);
} catch (InvalidObjectException) {
return null;
}
})
->reject(fn($e) => is_null($e));
$response = $this->httpGet($url, true);
$externalRootServers = collect($response)->map(fn ($rs) => new ExternalRootServer($rs));
$rootServerRepository->import($externalRootServers);
} catch (\Exception $e) {
$this->error($e->getMessage());
Expand Down Expand Up @@ -261,13 +253,17 @@ private function analyzeTables(): void
}
}

private function httpGet(string $url): array
private function httpGet(string $url, bool $shouldLogResponse = false): array
{
sleep(self::$requestDelaySeconds);

$headers = ['User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0 +aggregator'];
$response = Http::withHeaders($headers)->retry(3, self::$retryDelaySeconds * 1000)->get($url);

if ($shouldLogResponse) {
$this->info("Response from $url: $response");
}

if (!$response->ok()) {
throw new \Exception("Got bad status code {$response->status()} from $url");
}
Expand Down
20 changes: 19 additions & 1 deletion src/app/Http/Controllers/Admin/MeetingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public function index(Request $request)
weekdaysInclude: $days,
servicesInclude: $serviceBodyIds,
searchString: $searchString,
returnGroups: true,
);

return MeetingResource::collection($meetings);
Expand Down Expand Up @@ -189,6 +190,13 @@ private function validateInputs(Request $request, $skipVenueTypeLocationValidati
'worldId' => 'nullable|string|max:30',
'name' => 'required|string|max:128',
'timeZone' => ['nullable', 'string', 'max:40', new IANATimeZone],
'membersOfGroup' => 'sometimes|nullable|array',
'membersOfGroup.*.id_bigint' => 'nullable|int|exists:comdef_meetings_main,id_bigint',
'membersOfGroup.*.day' => 'required|int|between:0,6',
'membersOfGroup.*.startTime' => 'required|date_format:H:i',
'membersOfGroup.*.duration' => 'required|date_format:H:i',
'membersOfGroup.*.formatIds' => 'present|array',
'membersOfGroup.*.formatIds.*' => ['int', 'exists:comdef_formats,shared_id_bigint', Rule::notIn([$this->getVirtualFormatId(), $this->getTemporarilyClosedFormatId(), $this->getHybridFormatId()])],
], $this->getDataFieldValidators($skipVenueTypeLocationValidation))
));
}
Expand Down Expand Up @@ -272,7 +280,17 @@ private function buildValuesArray(Collection $validated): array
'worldid_mixed' => $validated['worldId'] ?? null,
'meeting_name' => $validated['name'],
];

$values['membersOfGroup'] = [];
foreach ($validated['membersOfGroup'] ?? [] as $member) {
$member['venueType'] = $validated['venueType'];
$values['membersOfGroup'][] = [
'id_bigint' => $member['id_bigint'] ?? null,
'weekday_tinyint' => $member['day'],
'start_time' => \DateTime::createFromFormat('H:i', $member['startTime'])->format('H:i:s'),
'duration_time' => \DateTime::createFromFormat('H:i', $member['duration'])->format('H:i:s'),
'formats' => $this->buildFormatsString(collect($member)),
];
}
$customFields = $this->getCustomFields();

return collect($values)
Expand Down
5 changes: 4 additions & 1 deletion src/app/Http/Controllers/Admin/Swagger/MeetingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@
* @OA\Property(property="bus_lines", type="string", example="string"),
* @OA\Property(property="train_lines", type="string", example="string"),
* @OA\Property(property="comments", type="string", example="string"),
* @OA\Property(property="membersOfGroup",type="array",
* @OA\Items(type="object", example={"day": "0", "startTime":"19:00", "duration": "01:30", "formats": "[]"}),
* ),
* @OA\Property(property="customFields", type="object", example={"key1": "value1", "key2": "value2"},
* @OA\AdditionalProperties(type="string")
* @OA\AdditionalProperties(type="string"),
* ),
* ),
* @OA\Schema(schema="Meeting", required={"id", "serviceBodyId", "formatIds", "venueType", "temporarilyVirtual", "day", "startTime", "duration", "timeZone", "latitude", "longitude", "published", "email", "worldId", "name"},
Expand Down
7 changes: 7 additions & 0 deletions src/app/Http/Controllers/Query/SwitcherController.php
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,12 @@ private function getSearchResults(Request $request, ?string $dataFormat = null):
} else {
$published = false;
}
$returnGroups = $request->input('return_groups', '0');
if ($returnGroups == '1') {
$returnGroups = true;
} else {
$returnGroups = false;
}

$sortKeys = $request->input('sort_keys');
$sortKeys = empty($sortKeys) ? null : explode(',', $sortKeys);
Expand Down Expand Up @@ -353,6 +359,7 @@ private function getSearchResults(Request $request, ?string $dataFormat = null):
sortKeys: $sortKeys,
pageSize: $pageSize,
pageNum: $pageNum,
returnGroups: $returnGroups,
);

// This code to calculate the formats fields is really inefficient, but necessary because
Expand Down
20 changes: 20 additions & 0 deletions src/app/Http/Resources/Admin/MeetingResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,24 @@ public function toArray($request)
->reject(fn ($id) => !self::$formatsById->has($id))
->sort();

$membersOfGroup = [];
foreach ($this->groupMembers->toResourceCollection(MeetingResource::class) as $member) {
$memberFormatIds = empty($member->formats) ? collect([]) : collect(explode(',', $member->formats))
->map(fn ($id) => intval($id))
->reject(fn ($id) => !self::$formatsById->has($id))
->sort();
$membersOfGroup[] = [
'id_bigint' => $member->id_bigint,
'day' => $member->weekday_tinyint,
'startTime' => is_null($member->start_time) ? null : (\DateTime::createFromFormat('H:i:s', $member->start_time) ?: \DateTime::createFromFormat('H:i', $member->start_time))->format('H:i'),
'duration' => is_null($member->duration_time) ? null : (\DateTime::createFromFormat('H:i:s', $member->duration_time) ?: \DateTime::createFromFormat('H:i', $member->duration_time))->format('H:i'),
'formats' => $memberFormatIds,
];
usort($membersOfGroup, fn($a, $b) => $a['day'] <=> $b['day'] ?: $a['startTime'] <=> $b['startTime']);
};
if (!empty($membersOfGroup)) {
$membersOfGroup = ['membersOfGroup' => $membersOfGroup];
}
return array_merge(
[
'id' => $this->id_bigint,
Expand All @@ -79,7 +97,9 @@ public function toArray($request)
'email' => $this->email_contact ?: null,
'worldId' => $this->worldid_mixed ?: null,
'name' => $meetingData->get('meeting_name') ?: null,
'is_group' => $this->is_group ?? 0,
],
$membersOfGroup,
self::$dataTemplates
->reject(fn ($t, $_) => self::$customFields->contains($t->key))
->mapWithKeys(fn ($t, $_) => [$t->key => $meetingData->get($t->key) ?: null])
Expand Down
22 changes: 21 additions & 1 deletion src/app/Http/Resources/Query/MeetingResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,20 @@ public function toArray($request)

$meeting[$meetingDataTemplate->key] = $meetingData->get($meetingDataTemplate->key, '') ?? '';
}

// Could be expensive. Maybe we want to control when this is done....
if ($this->getIsGroup() && (!self::$hasDataFieldKeys || self::$dataFieldKeys->has('membersOfGroup'))) {
$meeting['membersOfGroup'] = [];
foreach ($this->groupMembers->toResourceCollection(MeetingResource::class) as $member) {
$meeting['membersOfGroup'][] = [
'id_bigint' => $member->getIdBigint(),
'weekday_tinyint' => $member->getWeekdayTinyint(),
'start_time' => $member->getStartTime(),
'duration_time' => $member->getDurationTime(),
'formats' => $member->getFormats(),
];
}
usort($meeting['membersOfGroup'], fn($a, $b) => $a['weekday_tinyint'] <=> $b['weekday_tinyint'] ?: $a['start_time'] <=> $b['start_time']);
}
return $meeting;
}

Expand Down Expand Up @@ -248,6 +261,13 @@ private function getLangEnum()
$this->lang_enum ?? ''
);
}
private function getIsGroup()
{
return $this->when(
!self::$hasDataFieldKeys || self::$dataFieldKeys->has('is_group'),
$this->is_group ?? 0
);
}
private function getLongitude()
{
return $this->when(
Expand Down
1 change: 1 addition & 0 deletions src/app/Interfaces/MeetingRepositoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public function getSearchResults(
array $sortKeys = null,
int $pageSize = null,
int $pageNum = null,
bool $returnGroups = false,
): Collection;
public function getFieldKeys(): Collection;
public function getFieldValues(string $fieldName, array $specificFormats = [], bool $allFormats = false): Collection;
Expand Down
31 changes: 26 additions & 5 deletions src/app/Models/Meeting.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class Meeting extends Model
'latitude',
'published',
'email_contact',
'is_group',
'group_id'
];

public static $mainFields = [
Expand Down Expand Up @@ -81,7 +83,22 @@ public function longData()
{
return $this->hasMany(MeetingLongData::class, 'meetingid_bigint');
}

public function group()
{
return $this->belongsTo(Meeting::class, 'group_id', 'id_bigint');
}
public function groupMembers()
{
return $this->hasMany(Meeting::class, 'group_id');
}
public function groupData()
{
return $this->hasMany(MeetingData::class, 'meetingid_bigint', 'group_id');
}
public function groupLongData()
{
return $this->hasMany(MeetingLongData::class, 'meetingid_bigint', 'group_id');
}
private ?string $calculatedFormatKeys = null;
private function setCalculatedFormatKeys(string $formatKeyStrings)
{
Expand All @@ -106,12 +123,16 @@ public function getCalculatedFormatSharedIds(): string

public function calculateFormatsFields(Collection $formatsById)
{
if (is_null($this->formats) || $this->formats == '') {
$formatIds = [];
if (!is_null($this->formats) && !$this->formats == '') {
$formatIds = explode(',', $this->formats);
}
if (!is_null($this->group) && !is_null($this->group->formats) && !$this->group->formats == '') {
$formatIds = array_merge($formatIds, explode(',', $this->group->formats));
}
if (count($formatIds) === 0) {
return;
}

$formatIds = explode(',', $this->formats);

$calculatedFormats = [];
foreach ($formatIds as $formatId) {
$format = $formatsById->get(intval($formatId));
Expand Down
Loading
Loading