dev/core#2308 do not require fields if activity_id is present
[civicrm-core.git] / CRM / Utils / Check / Component / Case.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17 class CRM_Utils_Check_Component_Case extends CRM_Utils_Check_Component {
18
19 const DOCTOR_WHEN = 'https://github.com/civicrm/org.civicrm.doctorwhen';
20
21 /**
22 * @var CRM_Case_XMLRepository
23 */
24 protected $xmlRepo;
25
26 /**
27 * @var string[]
28 */
29 protected $caseTypeNames;
30
31 /**
32 * Class constructor.
33 */
34 public function __construct() {
35 $this->caseTypeNames = CRM_Case_PseudoConstant::caseType('name');
36 $this->xmlRepo = CRM_Case_XMLRepository::singleton();
37 }
38
39 /**
40 * @inheritDoc
41 */
42 public function isEnabled() {
43 return CRM_Case_BAO_Case::enabled();
44 }
45
46 /**
47 * Check that the case-type names don't rely on double-munging.
48 *
49 * @return CRM_Utils_Check_Message[]
50 * An empty array, or a list of warnings
51 */
52 public function checkCaseTypeNameConsistency() {
53 $messages = [];
54
55 foreach ($this->caseTypeNames as $caseTypeName) {
56 $normalFile = $this->xmlRepo->findXmlFile($caseTypeName);
57 $mungedFile = $this->xmlRepo->findXmlFile(CRM_Case_XMLProcessor::mungeCaseType($caseTypeName));
58
59 if ($normalFile && $mungedFile && $normalFile == $mungedFile) {
60 // ok
61 }
62 elseif ($normalFile && $mungedFile) {
63 $messages[] = new CRM_Utils_Check_Message(
64 __FUNCTION__ . $caseTypeName,
65 ts('Case type "%1" has duplicate XML files ("%2" and "%3")', [
66 1 => $caseTypeName,
67 2 => $normalFile,
68 3 => $mungedFile,
69 ]) .
70 '<br /><a href="' . CRM_Utils_System::getWikiBaseURL() . __FUNCTION__ . '">' .
71 ts('Read more about this warning') .
72 '</a>',
73 ts('CiviCase'),
74 \Psr\Log\LogLevel::WARNING,
75 'fa-puzzle-piece'
76 );
77 }
78 elseif ($normalFile && !$mungedFile) {
79 // ok
80 }
81 elseif (!$normalFile && $mungedFile) {
82 $messages[] = new CRM_Utils_Check_Message(
83 __FUNCTION__ . $caseTypeName,
84 ts('Case type "%1" corresponds to XML file ("%2") The XML file should be named "%3".', [
85 1 => $caseTypeName,
86 2 => $mungedFile,
87 3 => "{$caseTypeName}.xml",
88 ]) .
89 '<br /><a href="' . CRM_Utils_System::getWikiBaseURL() . __FUNCTION__ . '">' .
90 ts('Read more about this warning') .
91 '</a>',
92 ts('CiviCase'),
93 \Psr\Log\LogLevel::WARNING,
94 'fa-puzzle-piece'
95 );
96 }
97 elseif (!$normalFile && !$mungedFile) {
98 // ok -- probably a new or DB-based CaseType
99 }
100 }
101
102 return $messages;
103 }
104
105 /**
106 * Check that the timestamp columns are populated. (CRM-20958)
107 *
108 * @return CRM_Utils_Check_Message[]
109 * An empty array, or a list of warnings
110 */
111 public function checkNullTimestamps() {
112 $messages = [];
113
114 $nullCount = 0;
115 $nullCount += CRM_Utils_SQL_Select::from('civicrm_activity')
116 ->where('created_date IS NULL OR modified_date IS NULL')
117 ->select('COUNT(*)')
118 ->execute()
119 ->fetchValue();
120 $nullCount += CRM_Utils_SQL_Select::from('civicrm_case')
121 ->where('created_date IS NULL OR modified_date IS NULL')
122 ->select('COUNT(*)')
123 ->execute()
124 ->fetchValue();
125
126 if ($nullCount > 0) {
127 $messages[] = new CRM_Utils_Check_Message(
128 __FUNCTION__,
129 '<p>' .
130 ts('The tables "<em>civicrm_activity</em>" and "<em>civicrm_case</em>" were updated to support two new fields, "<em>created_date</em>" and "<em>modified_date</em>". For historical data, these fields may appear blank. (%1 records have NULL timestamps.)', [
131 1 => $nullCount,
132 ]) .
133 '</p><p>' .
134 ts('At time of writing, this is not a problem. However, future extensions and improvements could rely on these fields, so it may be useful to back-fill them.') .
135 '</p><p>' .
136 ts('For further discussion, please visit %1', [
137 1 => sprintf('<a href="%s" target="_blank">%s</a>', self::DOCTOR_WHEN, self::DOCTOR_WHEN),
138 ]) .
139 '</p>',
140 ts('Timestamps for Activities and Cases'),
141 \Psr\Log\LogLevel::NOTICE,
142 'fa-clock-o'
143 );
144 }
145
146 return $messages;
147 }
148
149 /**
150 * Check that the relationship types aren't going to cause problems.
151 *
152 * @return CRM_Utils_Check_Message[]
153 * An empty array, or a list of warnings
154 */
155 public function checkRelationshipTypeProblems() {
156 $messages = [];
157
158 /**
159 * There's no use-case to have two different relationship types
160 * with the same machine name, and it will cause problems because the
161 * system might match up the wrong type when comparing to xml.
162 * A single bi-directional one CAN and probably does have the same
163 * name_a_b and name_b_a and that's ok.
164 */
165
166 $dao = CRM_Core_DAO::executeQuery("SELECT rt1.*, rt2.id AS id2, rt2.name_a_b AS nameab2, rt2.name_b_a AS nameba2 FROM civicrm_relationship_type rt1 INNER JOIN civicrm_relationship_type rt2 ON (rt1.name_a_b = rt2.name_a_b OR rt1.name_a_b = rt2.name_b_a) WHERE rt1.id <> rt2.id");
167 while ($dao->fetch()) {
168 $messages[] = new CRM_Utils_Check_Message(
169 __FUNCTION__ . $dao->id . "dupe1",
170 ts("Relationship type <em>%1</em> has the same internal machine name as another type.
171 <table>
172 <tr><th>ID</th><th>name_a_b</th><th>name_b_a</th></tr>
173 <tr><td>%2</td><td>%3</td><td>%4</td></tr>
174 <tr><td>%5</td><td>%6</td><td>%7</td></tr>
175 </table>", [
176 1 => htmlspecialchars($dao->label_a_b),
177 2 => $dao->id,
178 3 => htmlspecialchars($dao->name_a_b),
179 4 => htmlspecialchars($dao->name_b_a),
180 5 => $dao->id2,
181 6 => htmlspecialchars($dao->nameab2),
182 7 => htmlspecialchars($dao->nameba2),
183 ]) .
184 '<br /><a href="' . CRM_Utils_System::docURL2('user/case-management/what-you-need-to-know#relationship-type-internal-name-duplicates', TRUE) . '">' .
185 ts('Read more about this warning') .
186 '</a>',
187 ts('Relationship Type Internal Name Duplicates'),
188 \Psr\Log\LogLevel::ERROR,
189 'fa-exchange'
190 );
191 }
192
193 // Ditto for labels
194 $dao = CRM_Core_DAO::executeQuery("SELECT rt1.*, rt2.id AS id2, rt2.label_a_b AS labelab2, rt2.label_b_a AS labelba2 FROM civicrm_relationship_type rt1 INNER JOIN civicrm_relationship_type rt2 ON (rt1.label_a_b = rt2.label_a_b OR rt1.label_a_b = rt2.label_b_a) WHERE rt1.id <> rt2.id");
195 while ($dao->fetch()) {
196 $messages[] = new CRM_Utils_Check_Message(
197 __FUNCTION__ . $dao->id . "dupe2",
198 ts("Relationship type <em>%1</em> has the same display label as another type.
199 <table>
200 <tr><th>ID</th><th>label_a_b</th><th>label_b_a</th></tr>
201 <tr><td>%2</td><td>%3</td><td>%4</td></tr>
202 <tr><td>%5</td><td>%6</td><td>%7</td></tr>
203 </table>", [
204 1 => htmlspecialchars($dao->label_a_b),
205 2 => $dao->id,
206 3 => htmlspecialchars($dao->label_a_b),
207 4 => htmlspecialchars($dao->label_b_a),
208 5 => $dao->id2,
209 6 => htmlspecialchars($dao->labelab2),
210 7 => htmlspecialchars($dao->labelba2),
211 ]) .
212 '<br /><a href="' . CRM_Utils_System::docURL2('user/case-management/what-you-need-to-know#relationship-type-display-label-duplicates', TRUE) . '">' .
213 ts('Read more about this warning') .
214 '</a>',
215 ts('Relationship Type Display Label Duplicates'),
216 \Psr\Log\LogLevel::ERROR,
217 'fa-exchange'
218 );
219 }
220
221 /**
222 * If the name of one type matches the label of another type, there may
223 * also be problems. This can happen if for example you initially set
224 * it up and then keep changing your mind adding and deleting and renaming
225 * a couple times in a certain order.
226 */
227 $dao = CRM_Core_DAO::executeQuery("SELECT rt1.*, rt2.id AS id2, rt2.name_a_b AS nameab2, rt2.name_b_a AS nameba2, rt2.label_a_b AS labelab2, rt2.label_b_a AS labelba2 FROM civicrm_relationship_type rt1 INNER JOIN civicrm_relationship_type rt2 ON (rt1.name_a_b = rt2.label_a_b OR rt1.name_b_a = rt2.label_a_b OR rt1.name_a_b = rt2.label_b_a OR rt1.name_b_a = rt2.label_b_a) WHERE rt1.id <> rt2.id");
228 // No point displaying the same matching id twice, which can happen with
229 // the query.
230 $ids = [];
231 while ($dao->fetch()) {
232 if (isset($ids[$dao->id2])) {
233 continue;
234 }
235 $ids[$dao->id] = $dao->id;
236 $messages[] = new CRM_Utils_Check_Message(
237 __FUNCTION__ . $dao->id . "dupe3",
238 ts("Relationship type <em>%1</em> has an internal machine name that is the same as the display label as another type.
239 <table>
240 <tr><th>ID</th><th>name_a_b</th><th>name_b_a</th><th>label_a_b</th><th>label_b_a</th></tr>
241 <tr><td>%2</td><td>%3</td><td>%4</td><td>%5</td><td>%6</td></tr>
242 <tr><td>%7</td><td>%8</td><td>%9</td><td>%10</td><td>%11</td></tr>
243 </table>", [
244 1 => htmlspecialchars($dao->label_a_b),
245 2 => $dao->id,
246 3 => htmlspecialchars($dao->name_a_b),
247 4 => htmlspecialchars($dao->name_b_a),
248 5 => htmlspecialchars($dao->label_a_b),
249 6 => htmlspecialchars($dao->label_b_a),
250 7 => $dao->id2,
251 8 => htmlspecialchars($dao->nameab2),
252 9 => htmlspecialchars($dao->nameab2),
253 10 => htmlspecialchars($dao->labelab2),
254 11 => htmlspecialchars($dao->labelba2),
255 ]) .
256 '<br /><a href="' . CRM_Utils_System::docURL2('user/case-management/what-you-need-to-know#relationship-type-cross-duplication', TRUE) . '">' .
257 ts('Read more about this warning') .
258 '</a>',
259 ts('Relationship Type Cross-Duplication'),
260 \Psr\Log\LogLevel::WARNING,
261 'fa-exchange'
262 );
263 }
264
265 /**
266 * Check that ones that appear to be unidirectional don't have the same
267 * machine name for both a_b and b_a. This can happen for example if you
268 * forget to fill in the b_a label when creating, then go back and edit.
269 */
270 $dao = CRM_Core_DAO::executeQuery("SELECT rt1.* FROM civicrm_relationship_type rt1 WHERE rt1.name_a_b = rt1.name_b_a AND rt1.label_a_b <> rt1.label_b_a");
271 while ($dao->fetch()) {
272 $messages[] = new CRM_Utils_Check_Message(
273 __FUNCTION__ . $dao->id . "ambiguousname",
274 ts("Relationship type <em>%1</em> appears to be unidirectional, but has the same internal machine name for both sides.
275 <table>
276 <tr><th>ID</th><th>name_a_b</th><th>name_b_a</th><th>label_a_b</th><th>label_b_a</th></tr>
277 <tr><td>%2</td><td>%3</td><td>%4</td><td>%5</td><td>%6</td></tr>
278 </table>", [
279 1 => htmlspecialchars($dao->label_a_b),
280 2 => $dao->id,
281 3 => htmlspecialchars($dao->name_a_b),
282 4 => htmlspecialchars($dao->name_b_a),
283 5 => htmlspecialchars($dao->label_a_b),
284 6 => htmlspecialchars($dao->label_b_a),
285 ]) .
286 '<br /><a href="' . CRM_Utils_System::docURL2('user/case-management/what-you-need-to-know#relationship-type-ambiguity', TRUE) . '">' .
287 ts('Read more about this warning') .
288 '</a>',
289 ts('Relationship Type Ambiguity'),
290 \Psr\Log\LogLevel::WARNING,
291 'fa-exchange'
292 );
293 }
294
295 /**
296 * Check that ones that appear to be unidirectional don't have the same
297 * label for both a_b and b_a. This can happen for example if you
298 * created it as unidirectional, then edited it later trying to make it
299 * bidirectional.
300 */
301 $dao = CRM_Core_DAO::executeQuery("SELECT rt1.* FROM civicrm_relationship_type rt1 WHERE rt1.label_a_b = rt1.label_b_a AND rt1.name_a_b <> rt1.name_b_a");
302 while ($dao->fetch()) {
303 $messages[] = new CRM_Utils_Check_Message(
304 __FUNCTION__ . $dao->id . "ambiguouslabel",
305 ts("Relationship type <em>%1</em> appears to be unidirectional internally, but has the same display label for both sides. Possibly you created it initially as unidirectional and then made it bidirectional later.
306 <table>
307 <tr><th>ID</th><th>name_a_b</th><th>name_b_a</th><th>label_a_b</th><th>label_b_a</th></tr>
308 <tr><td>%2</td><td>%3</td><td>%4</td><td>%5</td><td>%6</td></tr>
309 </table>", [
310 1 => htmlspecialchars($dao->label_a_b),
311 2 => $dao->id,
312 3 => htmlspecialchars($dao->name_a_b),
313 4 => htmlspecialchars($dao->name_b_a),
314 5 => htmlspecialchars($dao->label_a_b),
315 6 => htmlspecialchars($dao->label_b_a),
316 ]) .
317 '<br /><a href="' . CRM_Utils_System::docURL2('user/case-management/what-you-need-to-know#relationship-type-ambiguity', TRUE) . '">' .
318 ts('Read more about this warning') .
319 '</a>',
320 ts('Relationship Type Ambiguity'),
321 \Psr\Log\LogLevel::WARNING,
322 'fa-exchange'
323 );
324 }
325
326 /**
327 * Check for missing roles listed in the xml but not defined as
328 * relationship types.
329 */
330
331 // Don't use database since might be in xml files.
332 $caseTypes = civicrm_api3('CaseType', 'get', [
333 'options' => ['limit' => 0],
334 ])['values'];
335 // Don't use pseudoconstant since want all and also name and label.
336 $relationshipTypes = civicrm_api3('RelationshipType', 'get', [
337 'options' => ['limit' => 0],
338 ])['values'];
339 $allConfigured = array_column($relationshipTypes, 'id', 'name_a_b')
340 + array_column($relationshipTypes, 'id', 'name_b_a')
341 + array_column($relationshipTypes, 'id', 'label_a_b')
342 + array_column($relationshipTypes, 'id', 'label_b_a');
343 $missing = [];
344 foreach ($caseTypes as $caseType) {
345 foreach ($caseType['definition']['caseRoles'] ?? [] as $role) {
346 if (!isset($allConfigured[$role['name']])) {
347 $missing[$role['name']] = $role['name'];
348 }
349 }
350 }
351 if (!empty($missing)) {
352 $tableRows = [];
353 foreach ($relationshipTypes as $relationshipType) {
354 $tableRows[] = ts('<tr><td>%1</td><td>%2</td><td>%3</td><td>%4</td><td>%5</td></tr>', [
355 1 => $relationshipType['id'],
356 2 => htmlspecialchars($relationshipType['name_a_b']),
357 3 => htmlspecialchars($relationshipType['name_b_a']),
358 4 => htmlspecialchars($relationshipType['label_a_b']),
359 5 => htmlspecialchars($relationshipType['label_b_a']),
360 ]);
361 }
362 $messages[] = new CRM_Utils_Check_Message(
363 __FUNCTION__ . "missingroles",
364 ts("<p>The following roles listed in your case type definitions do not match any relationship type defined in the system: <em>%1</em>.</p>"
365 . "<p>This might be because of a mismatch if you are using external xml files to manage case types. If using xml files, then use either the name_a_b or name_b_a value from the following table. (Out of the box you would use name_b_a, which lists them on the case from the client perspective.) If you are not using xml files, you can edit your case types at Administer - CiviCase - Case Types.</p>"
366 . "<table>
367 <tr><th>ID</th><th>name_a_b</th><th>name_b_a</th><th>label_a_b</th><th>label_b_a</th></tr>"
368 . implode("\n", $tableRows)
369 . "</table>", [
370 1 => htmlspecialchars(implode(', ', $missing)),
371 ]) .
372 '<br /><a href="' . CRM_Utils_System::docURL2('user/case-management/what-you-need-to-know#missing-roles', TRUE) . '">' .
373 ts('Read more about this warning') .
374 '</a>',
375 ts('Missing Roles'),
376 \Psr\Log\LogLevel::ERROR,
377 'fa-exclamation'
378 );
379 }
380
381 return $messages;
382 }
383
384 /**
385 * Check any xml definitions stored as external files to see if they
386 * have label as the role and where the label is different from the name.
387 * We don't have to think about edge cases because there are already
388 * status checks above for those.
389 *
390 * @return CRM_Utils_Check_Message[]
391 * An empty array, or a list of warnings
392 */
393 public function checkExternalXmlFileRoleNames() {
394 $messages = [];
395
396 // Get config for relationship types
397 $relationship_types = civicrm_api3('RelationshipType', 'get', [
398 'options' => ['limit' => 0],
399 ])['values'];
400 // keyed on name, with id as the value, e.g. 'Case Coordinator is' => 10
401 $names_a_b = array_column($relationship_types, 'id', 'name_a_b');
402 $names_b_a = array_column($relationship_types, 'id', 'name_b_a');
403 $labels_a_b = array_column($relationship_types, 'id', 'label_a_b');
404 $labels_b_a = array_column($relationship_types, 'id', 'label_b_a');
405
406 $dao = CRM_Core_DAO::executeQuery("SELECT id FROM civicrm_case_type WHERE definition IS NULL OR definition=''");
407 while ($dao->fetch()) {
408 $case_type = civicrm_api3('CaseType', 'get', [
409 'id' => $dao->id,
410 ])['values'][$dao->id];
411 if (empty($case_type['definition'])) {
412 $messages[] = new CRM_Utils_Check_Message(
413 __FUNCTION__ . "missingcasetypedefinition",
414 '<p>' . ts('Unable to locate xml file for Case Type "<em>%1</em>".',
415 [
416 1 => htmlspecialchars(empty($case_type['title']) ? $dao->id : $case_type['title']),
417 ]) . '</p>',
418 ts('Missing Case Type Definition'),
419 \Psr\Log\LogLevel::ERROR,
420 'fa-exclamation'
421 );
422 continue;
423 }
424
425 if (empty($case_type['definition']['caseRoles'])) {
426 $messages[] = new CRM_Utils_Check_Message(
427 __FUNCTION__ . "missingcaseroles",
428 '<p>' . ts('CaseRoles seems to be missing in the xml file for Case Type "<em>%1</em>".',
429 [
430 1 => htmlspecialchars(empty($case_type['title']) ? $dao->id : $case_type['title']),
431 ]) . '</p>',
432 ts('Missing Case Roles'),
433 \Psr\Log\LogLevel::ERROR,
434 'fa-exclamation'
435 );
436 continue;
437 }
438
439 // Loop thru each role in the xml.
440 foreach ($case_type['definition']['caseRoles'] as $role) {
441 $name_to_suggest = NULL;
442 $xml_name = $role['name'];
443 if (isset($names_a_b[$xml_name]) || isset($names_b_a[$xml_name])) {
444 // It matches a name, so either name and label are the same or it's
445 // an edge case already dealt with by core status checks, so do
446 // nothing.
447 continue;
448 }
449 elseif (isset($labels_b_a[$xml_name])) {
450 // $labels_b_a[$xml_name] gives us the id, so then look up name_b_a
451 // from the original relationship_types array which is keyed on id.
452 // We do b_a first because it's the more standard one, although it
453 // will only make a difference in edge cases which we leave to the
454 // other checks.
455 $name_to_suggest = $relationship_types[$labels_b_a[$xml_name]]['name_b_a'];
456 }
457 elseif (isset($labels_a_b[$xml_name])) {
458 $name_to_suggest = $relationship_types[$labels_a_b[$xml_name]]['name_a_b'];
459 }
460
461 // If it didn't match any name or label then that's weird.
462 if (empty($name_to_suggest)) {
463 $messages[] = new CRM_Utils_Check_Message(
464 __FUNCTION__ . "invalidcaserole",
465 '<p>' . ts('CaseRole "<em>%1</em>" in the xml file for Case Type "<em>%2</em>" doesn\'t seem to match any existing relationship type.',
466 [
467 1 => htmlspecialchars($xml_name),
468 2 => htmlspecialchars(empty($case_type['title']) ? $dao->id : $case_type['title']),
469 ]) . '</p>',
470 ts('Invalid Case Role'),
471 \Psr\Log\LogLevel::ERROR,
472 'fa-exclamation'
473 );
474 }
475 else {
476 $messages[] = new CRM_Utils_Check_Message(
477 __FUNCTION__ . "suggestedchange",
478 '<p>' . ts('Please edit the XML file for case type "<em>%2</em>" so that the case role label "<em>%1</em>" is changed to its corresponding name "<em>%3</em>". Using label is deprecated as of version 5.20.',
479 [
480 1 => htmlspecialchars($xml_name),
481 2 => htmlspecialchars(empty($case_type['title']) ? $dao->id : $case_type['title']),
482 3 => htmlspecialchars($name_to_suggest),
483 ]) . '</p>',
484 ts('Case Role using display label instead of internal machine name'),
485 \Psr\Log\LogLevel::WARNING,
486 'fa-code'
487 );
488 }
489 }
490 }
491 return $messages;
492 }
493
494 }