Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
96.22% |
178 / 185 |
|
71.43% |
10 / 14 |
CRAP | |
0.00% |
0 / 1 |
XMLFileTesttakers | |
96.22% |
178 / 185 |
|
71.43% |
10 / 14 |
54 | |
0.00% |
0 / 1 |
crossValidate | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
checkIfBookletsArePresent | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
5.20 | |||
checkIfIdsAreUsedInOtherFiles | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
6 | |||
reportDuplicates | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
getAllLogins | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
getAllLoginNames | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
getGroups | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
getLoginsInSameGroup | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
4.25 | |||
getLogin | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
2 | |||
collectBookletsOfGroup | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
collectBookletsPerCode | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
9 | |||
getCodesFromBookletElement | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
4.03 | |||
getCustomTexts | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
collectProfiles | |
95.35% |
41 / 43 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | /** @noinspection PhpUnhandledExceptionInspection */ |
4 | declare(strict_types=1); |
5 | |
6 | class 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 | } |