Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.22% covered (success)
96.22%
178 / 185
71.43% covered (warning)
71.43%
10 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
XMLFileTesttakers
96.22% covered (success)
96.22%
178 / 185
71.43% covered (warning)
71.43%
10 / 14
54
0.00% covered (danger)
0.00%
0 / 1
 crossValidate
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 checkIfBookletsArePresent
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
5.20
 checkIfIdsAreUsedInOtherFiles
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
6
 reportDuplicates
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getAllLogins
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 getAllLoginNames
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getGroups
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 getLoginsInSameGroup
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
4.25
 getLogin
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
2
 collectBookletsOfGroup
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 collectBookletsPerCode
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
9
 getCodesFromBookletElement
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 getCustomTexts
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 collectProfiles
95.35% covered (success)
95.35%
41 / 43
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/** @noinspection PhpUnhandledExceptionInspection */
4declare(strict_types=1);
5
6class XMLFileTesttakers extends XMLFile {
7  const string type = 'Testtakers';
8  const bool canBeRelationSubject = true;
9  const bool canBeRelationObject = false;
10  protected LoginArray $logins;
11
12  public function crossValidate(WorkspaceCache $workspaceCache): void {
13    parent::crossValidate($workspaceCache);
14
15    $this->logins = $this->getAllLogins();
16    $this->contextData['testtakers'] = count($this->logins->asArray());
17
18//    $this->checkForDuplicateLogins();
19
20    foreach ($this->logins as $login) {
21      /* @var Login $login */
22      $this->checkIfBookletsArePresent($login, $workspaceCache);
23    }
24
25    $this->checkIfIdsAreUsedInOtherFiles($workspaceCache);
26  }
27
28  private function checkIfBookletsArePresent(Login $testtaker, WorkspaceCache $validator): void {
29    foreach ($testtaker->getBooklets() as $code => $booklets) {
30      foreach ($booklets as $bookletId) {
31        $booklet = $validator->getBooklet($bookletId);
32
33        if (!$booklet) {
34          $this->report('error', "Booklet `$bookletId` not found for login `{$testtaker->getName()}`");
35          continue;
36        }
37
38        if (!$booklet->isValid()) {
39          $this->report('error', "Booklet `$bookletId` has an error for login `{$testtaker->getName()}`");
40          continue;
41        }
42
43        $this->addRelation(new FileRelation($booklet->getType(), $bookletId, FileRelationshipType::hasBooklet, $booklet));
44
45      }
46    }
47  }
48
49  private function checkIfIdsAreUsedInOtherFiles(WorkspaceCache $workspaceCache): void {
50    $loginList = $this->getAllLoginNames();
51    $groupList = array_keys($this->getGroups());
52
53    $workspaceCache->addGlobalIdSource($this->getName(), 'login', $loginList);
54    $workspaceCache->addGlobalIdSource($this->getName(), 'group', $groupList);
55
56    foreach ($workspaceCache->getGlobalIds() as $workspaceId => $sources) {
57      foreach ($sources as $source => $globalIdsByType) {
58        if ($source == '/name/') {
59          continue;
60        }
61        if (($source == $this->getName()) and ($workspaceId == $workspaceCache->getId())) {
62          continue;
63        }
64
65        $this->reportDuplicates(
66          'login',
67          array_intersect($loginList, array_values($globalIdsByType['login'])),
68          $source,
69          $workspaceCache->getId(),
70          $workspaceId,
71          $sources['/name/'] ?? 'unknown'
72        );
73        $this->reportDuplicates(
74          'group',
75          array_intersect($groupList, array_values($globalIdsByType['group'])),
76          $source,
77          $workspaceCache->getId(),
78          $workspaceId,
79          $sources['/name/'] ?? 'unknown'
80        );
81      }
82    }
83  }
84
85  private function reportDuplicates(
86    string $type,
87    array $duplicates,
88    string $otherFileName,
89    int $thisWsId,
90    int $otherWsId,
91    string $workspaceName
92  ): void {
93    foreach ($duplicates as $duplicate) {
94      $location = ($thisWsId !== $otherWsId) ? "on workspace `$workspaceName" : '';
95      $location .= "in file `$otherFileName`";
96      $this->report('error', "Duplicate $type: `$duplicate` - also $location");
97    }
98  }
99
100  public function getAllLogins(): LoginArray {
101    if (!$this->isValid()) {
102      return new LoginArray();
103    }
104
105    $testTakers = [];
106
107    foreach ($this->getXml()->xpath('Group') as $groupElement) {
108      foreach ($groupElement->xpath('Login[@name]') as $loginElement) {
109        $login = $this->getLogin($groupElement, $loginElement, -1);
110        $testTakers[] = $login;
111      }
112    }
113
114    return new LoginArray(...$testTakers);
115  }
116
117  public function getAllLoginNames(): array {
118    if (!$this->isValid()) {
119      return [];
120    }
121
122    $loginNames = [];
123
124    foreach ($this->getXml()->xpath('Group/Login[@name]') as $loginElement) {
125      if (!in_array((string) $loginElement['name'], $loginNames)) {
126        $loginNames[] = (string) $loginElement['name'];
127      }
128    }
129
130    return $loginNames;
131  }
132
133  public function getGroups(): array {
134    if (!$this->isValid()) {
135      return [];
136    }
137
138    $groups = [];
139
140    foreach ($this->getXml()->xpath('Group') as $groupElement) {
141      $groups[(string) $groupElement['id']] = new Group(
142        (string) $groupElement['id'],
143        (string) $groupElement['label']
144      );
145    }
146
147    return $groups;
148  }
149
150  public function getLoginsInSameGroup(string $loginName, int $workspaceId): ?LoginArray {
151    if (!$this->isValid()) {
152      return null;
153    }
154
155    foreach ($this->getXml()->xpath("Group[Login[@name='$loginName']]") as $groupElement) {
156      $groupMembers = new LoginArray();
157
158      foreach ($groupElement->xpath("Login[@name!='$loginName'][@mode!='monitor-group'][Booklet]") as $memberElement) {
159        $groupMembers->add($this->getLogin($groupElement, $memberElement, $workspaceId));
160      }
161
162      return $groupMembers;
163    }
164
165    return null;
166  }
167
168  private function getLogin(SimpleXMLElement $groupElement, SimpleXMLElement $loginElement, int $workspaceId): Login {
169    $mode = (string) $loginElement['mode'];
170    $name = (string) $loginElement['name'];
171
172    $booklets = match ($mode) {
173      'monitor-group' => ['' => $this->collectBookletsOfGroup($workspaceId, $name)],
174      default => self::collectBookletsPerCode($loginElement)
175    };
176
177    $monitors = match ($mode) {
178      'monitor-group', 'monitor-study' => $this->collectProfiles($loginElement),
179      default => []
180    };
181
182    return new Login(
183      $name,
184      (string) $loginElement['pw'],
185      (string) $loginElement['mode'] ?? 'run-demo',
186      (string) $groupElement['id'],
187      (string) $groupElement['label'] ?? (string) $groupElement['id'],
188      $booklets,
189      $workspaceId,
190      isset($groupElement['validTo']) ? TimeStamp::fromXMLFormat((string) $groupElement['validTo']) : 0,
191      TimeStamp::fromXMLFormat((string) $groupElement['validFrom']),
192      (int) ($groupElement['validFor'] ?? 0),
193      $this->getCustomTexts(),
194      $monitors
195    );
196  }
197
198  // TODO write unit test
199  // TODO make private
200  public function collectBookletsOfGroup(int $workspaceId, string $loginName): array {
201    $members = $this->getLoginsInSameGroup($loginName, $workspaceId);
202    $booklets = [];
203
204    foreach ($members as $member) {
205      /* @var $member Login */
206
207      $codes2booklets = $member->getBooklets() ?? [];
208
209      foreach ($codes2booklets as $bookletList) {
210        foreach ($bookletList as $booklet) {
211          $booklets[] = $booklet;
212        }
213      }
214    }
215
216    return array_unique($booklets);
217  }
218
219  protected static function collectBookletsPerCode(SimpleXMLElement $loginNode): array {
220    $noCodeBooklets = [];
221    $codeBooklets = [];
222
223    foreach ($loginNode->xpath('Booklet') as $bookletElement) {
224      $bookletName = strtoupper(trim((string) $bookletElement));
225
226      if (!$bookletName) {
227        continue;
228      }
229
230      $codesOfThisBooklet = self::getCodesFromBookletElement($bookletElement);
231
232      if (count($codesOfThisBooklet) > 0) {
233        foreach ($codesOfThisBooklet as $c) {
234          if (!isset($codeBooklets[$c])) {
235            $codeBooklets[$c] = [];
236          }
237
238          if (!in_array($bookletName, $codeBooklets[$c])) {
239            $codeBooklets[$c][] = $bookletName;
240          }
241        }
242
243      } else {
244        $noCodeBooklets[] = $bookletName;
245      }
246    }
247
248    $noCodeBooklets = array_unique($noCodeBooklets);
249
250    if (count($codeBooklets) === 0) {
251      $codeBooklets = ['' => $noCodeBooklets];
252
253    } else {
254      // add all no-code-booklets to every code
255      foreach ($codeBooklets as $code => $booklets) {
256        $codeBooklets[$code] = array_unique(array_merge($codeBooklets[$code], $noCodeBooklets));
257      }
258    }
259
260    return $codeBooklets;
261  }
262
263  protected static function getCodesFromBookletElement(SimpleXMLElement $bookletElement): array {
264    if ($bookletElement->getName() !== 'Booklet') {
265      return [];
266    }
267
268    $codesString = isset($bookletElement['codes'])
269      ? trim((string) $bookletElement['codes'])
270      : '';
271
272    if (!$codesString) {
273      return [];
274    }
275
276    return array_unique(explode(' ', $codesString));
277  }
278
279  public function getCustomTexts(): stdClass {
280    $customTexts = [];
281    foreach ($this->getXml()->xpath('/Testtakers/CustomTexts/CustomText') as $customTextElement) {
282      $customTexts[(string) $customTextElement['key'] ?? ''] = (string) $customTextElement;
283    }
284    return (object) $customTexts;
285  }
286
287  /**
288   * @return array[]
289   */
290  private function collectProfiles(SimpleXMLElement $loginElem): array {
291    $profiles = array_map(
292      function(SimpleXMLElement $profileReferenceElem): SimpleXMLElement | null {
293        $id = ((string) $profileReferenceElem['id']);
294        $profileElems = $this->getXml()->xpath('//Profiles/GroupMonitor/Profile[@id="' . $id . '"]');
295        if (count($profileElems) !== 1) {
296          $this->report('error', "Profile with `$id` referenced but not provided");
297          return null;
298        }
299        return $profileElems[0];
300      },
301      $loginElem->xpath('Profile')
302    );
303    $profiles = array_filter(
304      $profiles,
305      fn (SimpleXMLElement | null $profileElem) => !!$profileElem
306    );
307    return array_map(
308      fn (SimpleXMLElement $profileElem): array => [
309        'id' => ((string) $profileElem['id']) ?? Random::string(8, false),
310        'label' =>  ((string) $profileElem['label']) ?? "",
311        'settings' => [
312          'blockColumn' => ((string) $profileElem['blockColumn']) ?? "show",
313          'unitColumn' => ((string) $profileElem['unitColumn']) ?? "show",
314          'view' => ((string) $profileElem['view']) ?? "middle",
315          'groupColumn' => ((string) $profileElem['groupColumn']) ?? "hide",
316          'bookletColumn' => ((string) $profileElem['bookletColumn']) ?? "show"
317        ],
318        'filters' => array_map(
319          fn (SimpleXMLElement $filterElem): array => [
320            'target' => ((string) $filterElem['field']) ?? "personLabel",
321            'value' => ((string) $filterElem['value']) ?? "",
322            'label' => ((string) $filterElem['label']) ?? "",
323            'type' => ((string) $filterElem['type']) ?? "equals",
324            'not' => ((bool) $profileElem['not']) ?? false
325          ],
326          $profileElem->xpath('Filter')
327        ),
328        'filtersEnabled' => [
329          'pending' => ((string) $profileElem['filterPending']) ?? "no",
330          'locked' => ((string) $profileElem['filterLocked']) ?? "no"
331        ]
332      ],
333      $profiles
334    );
335  }
336}