Skip to content

Commit bde9fb8

Browse files
bcordisclaude
andauthored
feat: link teachers to Joomla user accounts for auto-access (#1188)
* feat: link teachers to Joomla user accounts for auto-access (#1180) Add user_id column to teachers table, enabling teacher-to-user linkage. When a teacher is linked to a Joomla user, that user automatically gains access to content at locations where they teach via the multi-campus location system. Implements the previously stubbed getTeacherLocations() and userIsTeacher() methods in CwmlocationHelper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: consolidate 10.3.0 SQL update files into single migration Merge three separate dated files into one since none have been released. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: rename SQL update file to reflect latest modification date Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2619619 commit bde9fb8

File tree

9 files changed

+124
-28
lines changed

9 files changed

+124
-28
lines changed

admin/forms/teacher.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
addfieldprefix="Joomla\Component\Contact\Administrator\Field"
2828
description="JBS_TCH_SELECT_CONTACT_DESC"
2929
label="JBS_TCH_SELECT_CONTACT_LABEL"/>
30+
<field name="user_id"
31+
type="user"
32+
label="JBS_TCH_USER_ACCOUNT"
33+
description="JBS_TCH_USER_ACCOUNT_DESC"/>
3034
</fieldset>
3135

3236
<fieldset name="sidebar" label="JGLOBAL_FIELDSET_OPTIONS">

admin/language/en-GB/en-GB.com_proclaim.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2297,6 +2297,8 @@ JBS_TCH_TEACHER_IMAGE_LARGE = "Teacher Image Large"
22972297
JBS_TCH_TITLE_DESC = "Title or salutation like Prof"
22982298
JBS_TCH_TWITTER = "Twitter Feed"
22992299
JBS_TCH_TWITTER_DESC = "Enter a full URL link to the teacher's Twitter feed starting with http://"
2300+
JBS_TCH_USER_ACCOUNT = "Joomla User Account"
2301+
JBS_TCH_USER_ACCOUNT_DESC = "Link this teacher to a Joomla user account. When linked, the user automatically gains access to content at locations where they teach."
23002302
JBS_TCH_ORG_NAME = "Organization"
23012303
JBS_TCH_ORG_NAME_DESC = "The organization this teacher belongs to. Used in Schema.org worksFor. Leave blank to use the site default."
23022304
JBS_TCH_ORG_NAME_HINT = "Leave blank for site default"

admin/sql/install.mysql.utf8.sql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,7 @@ CREATE TABLE IF NOT EXISTS `#__bsms_teachers`
555555
`link3` VARCHAR(150) DEFAULT NULL,
556556
`linklabel3` VARCHAR(150) DEFAULT NULL,
557557
`contact` INT(11) DEFAULT NULL,
558+
`user_id` INT(10) UNSIGNED DEFAULT NULL,
558559
`address` MEDIUMTEXT,
559560
`social_links` TEXT DEFAULT NULL,
560561
`landing_show` INT(3) DEFAULT NULL,
@@ -571,7 +572,8 @@ CREATE TABLE IF NOT EXISTS `#__bsms_teachers`
571572
KEY `idx_access` (`access`),
572573
KEY `idx_checkout` (`checked_out`),
573574
KEY `idx_createdby` (`created_by`),
574-
KEY `idx_published_access` (`published`, `access`)
575+
KEY `idx_published_access` (`published`, `access`),
576+
KEY `idx_teacher_user` (`user_id`)
575577
) ENGINE InnoDB
576578
DEFAULT CHARSET = utf8mb4
577579
DEFAULT COLLATE = utf8mb4_unicode_ci;

admin/sql/updates/mysql/10.3.0-20260318.sql

Lines changed: 0 additions & 2 deletions
This file was deleted.

admin/sql/updates/mysql/10.3.0-20260316.sql renamed to admin/sql/updates/mysql/10.3.0-20260319.sql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,11 @@ UPDATE `#__bsms_teachers` AS t
1515
SET t.`ordering` = ranked.`new_ordering`;
1616

1717
ALTER TABLE `#__bsms_teachers` DROP COLUMN IF EXISTS `landing_ordering`;
18+
19+
-- Add organization name field to teachers for Schema.org worksFor override
20+
ALTER TABLE `#__bsms_teachers` ADD COLUMN `org_name` VARCHAR(255) DEFAULT NULL AFTER `title`;
21+
22+
-- Add Joomla user account linkage for teacher auto-access
23+
ALTER TABLE `#__bsms_teachers`
24+
ADD COLUMN `user_id` INT(10) UNSIGNED DEFAULT NULL AFTER `contact`,
25+
ADD KEY `idx_teacher_user` (`user_id`);

admin/src/Helper/CwmlocationHelper.php

Lines changed: 89 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Joomla\CMS\Component\ComponentHelper;
2020
use Joomla\CMS\Factory;
2121
use Joomla\Database\DatabaseInterface;
22+
use Joomla\Database\ParameterType;
2223
use Joomla\Database\QueryInterface;
2324

2425
/**
@@ -204,37 +205,114 @@ public static function applySecurityFilter(QueryInterface $query, string $alias,
204205
/**
205206
* Return location IDs where the user has an associated teacher record.
206207
*
208+
* Joins through both the many-to-many study_teachers table and the legacy
209+
* teacher_id column on studies to cover all assignment paths.
210+
*
207211
* @param int $userId Joomla user ID.
208212
*
209213
* @return int[] Location IDs.
210214
*
211-
* @since 10.1.0
212-
* @todo Implement once a user_id column is added to #__bsms_teachers.
213-
* When available, join: teachers.user_id = $userId
214-
* → study_teachers.teacher_id → studies.location_id.
215+
* @since 10.3.0
215216
*/
216217
public static function getTeacherLocations(int $userId): array
217218
{
218-
// Stub: teacher-to-user linkage requires a user_id field on #__bsms_teachers
219-
// which is not present in the current schema. Return empty until Phase 2.
220-
return [];
219+
if ($userId <= 0) {
220+
return [];
221+
}
222+
223+
$db = Factory::getContainer()->get(DatabaseInterface::class);
224+
225+
// Many-to-many path: teachers → study_teachers → studies
226+
$query1 = $db->getQuery(true)
227+
->select('DISTINCT ' . $db->quoteName('s.location_id'))
228+
->from($db->quoteName('#__bsms_teachers', 't'))
229+
->innerJoin(
230+
$db->quoteName('#__bsms_study_teachers', 'st')
231+
. ' ON ' . $db->quoteName('st.teacher_id') . ' = ' . $db->quoteName('t.id')
232+
)
233+
->innerJoin(
234+
$db->quoteName('#__bsms_studies', 's')
235+
. ' ON ' . $db->quoteName('s.id') . ' = ' . $db->quoteName('st.study_id')
236+
)
237+
->where($db->quoteName('t.user_id') . ' = :userId1')
238+
->where($db->quoteName('s.location_id') . ' IS NOT NULL')
239+
->where($db->quoteName('s.location_id') . ' > 0')
240+
->bind(':userId1', $userId, ParameterType::INTEGER);
241+
242+
// Legacy path: teachers → studies.teacher_id
243+
$query2 = $db->getQuery(true)
244+
->select('DISTINCT ' . $db->quoteName('s.location_id'))
245+
->from($db->quoteName('#__bsms_teachers', 't'))
246+
->innerJoin(
247+
$db->quoteName('#__bsms_studies', 's')
248+
. ' ON ' . $db->quoteName('s.teacher_id') . ' = ' . $db->quoteName('t.id')
249+
)
250+
->where($db->quoteName('t.user_id') . ' = :userId2')
251+
->where($db->quoteName('s.location_id') . ' IS NOT NULL')
252+
->where($db->quoteName('s.location_id') . ' > 0')
253+
->bind(':userId2', $userId, ParameterType::INTEGER);
254+
255+
$db->setQuery('(' . $query1 . ') UNION (' . $query2 . ')');
256+
257+
return array_map('intval', $db->loadColumn() ?: []);
221258
}
222259

223260
/**
224261
* Determine whether a user is a teacher of a specific message.
225262
*
263+
* Checks both the many-to-many study_teachers table and the legacy
264+
* teacher_id column on the study record.
265+
*
226266
* @param int $userId Joomla user ID.
227267
* @param int $messageId Message (study) ID.
228268
*
229269
* @return bool
230270
*
231-
* @since 10.1.0
232-
* @todo Implement once a user_id column is added to #__bsms_teachers.
271+
* @since 10.3.0
233272
*/
234273
public static function userIsTeacher(int $userId, int $messageId): bool
235274
{
236-
// Stub: see getTeacherLocations() note.
237-
return false;
275+
if ($userId <= 0 || $messageId <= 0) {
276+
return false;
277+
}
278+
279+
$db = Factory::getContainer()->get(DatabaseInterface::class);
280+
281+
// Check many-to-many path
282+
$query = $db->getQuery(true)
283+
->select('1')
284+
->from($db->quoteName('#__bsms_teachers', 't'))
285+
->innerJoin(
286+
$db->quoteName('#__bsms_study_teachers', 'st')
287+
. ' ON ' . $db->quoteName('st.teacher_id') . ' = ' . $db->quoteName('t.id')
288+
)
289+
->where($db->quoteName('t.user_id') . ' = :userId')
290+
->where($db->quoteName('st.study_id') . ' = :studyId')
291+
->bind(':userId', $userId, ParameterType::INTEGER)
292+
->bind(':studyId', $messageId, ParameterType::INTEGER);
293+
294+
$db->setQuery($query, 0, 1);
295+
296+
if ($db->loadResult()) {
297+
return true;
298+
}
299+
300+
// Check legacy teacher_id path
301+
$query2 = $db->getQuery(true)
302+
->select('1')
303+
->from($db->quoteName('#__bsms_teachers', 't'))
304+
->innerJoin(
305+
$db->quoteName('#__bsms_studies', 's')
306+
. ' ON ' . $db->quoteName('s.teacher_id') . ' = ' . $db->quoteName('t.id')
307+
)
308+
->where($db->quoteName('t.user_id') . ' = :userId2')
309+
->where($db->quoteName('s.id') . ' = :studyId2')
310+
->bind(':userId2', $userId, ParameterType::INTEGER)
311+
->bind(':studyId2', $messageId, ParameterType::INTEGER);
312+
313+
$db->setQuery($query2, 0, 1);
314+
315+
return (bool) $db->loadResult();
238316
}
239317

240318
/**

admin/src/Table/CwmteacherTable.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ class CwmteacherTable extends Table
119119
/** @var int|null Contact ID @since 7.0.0 */
120120
public ?int $contact = null;
121121

122+
/** @var int|null Linked Joomla user ID @since 10.3.0 */
123+
public ?int $user_id = null;
124+
122125
/** @var string|null Mailing address @since 7.0.0 */
123126
public ?string $address = null;
124127

@@ -277,7 +280,7 @@ public function bind($src, $ignore = ''): bool
277280

278281
// Cast typed int properties to prevent PHP 8.3 TypeError when form posts strings
279282
foreach ([
280-
'id', 'list_show', 'published', 'asset_id', 'access', 'contact',
283+
'id', 'list_show', 'published', 'asset_id', 'access', 'contact', 'user_id',
281284
'landing_show', 'created_by', 'modified_by', 'checked_out',
282285
] as $field) {
283286
if (isset($src[$field])) {

admin/tmpl/cwmteacher/edit.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class="form-validate" enctype="multipart/form-data">
6868
<?php echo $this->form->renderField('email'); ?>
6969
<?php echo $this->form->renderField('address'); ?>
7070
<?php echo $this->form->renderField('contact'); ?>
71+
<?php echo $this->form->renderField('user_id'); ?>
7172
<?php if ($this->form->getValue('contact')) : ?>
7273
<a href="<?php echo Route::_('index.php?option=com_contact&task=contact.edit&id=' . (int) $this->form->getValue('contact')); ?>"
7374
target="_blank" class="btn btn-sm btn-secondary mb-3">

tests/unit/Admin/Helper/CwmlocationHelperTest.php

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,35 +64,35 @@ public function testResetCacheForUser(): void
6464
}
6565

6666
// -------------------------------------------------------------------------
67-
// getTeacherLocations / userIsTeacher stubs
67+
// getTeacherLocations / userIsTeacher edge cases (no DB needed)
6868
// -------------------------------------------------------------------------
6969

7070
/**
71-
* getTeacherLocations() is stubbed and must return an empty array for any
72-
* user ID until a user_id column is added to #__bsms_teachers.
71+
* getTeacherLocations() returns empty for zero or negative user IDs
72+
* without hitting the database.
7373
*
7474
* @return void
75-
* @since 10.1.0
75+
* @since 10.3.0
7676
*/
77-
public function testGetTeacherLocationsReturnsEmptyArray(): void
77+
public function testGetTeacherLocationsReturnsEmptyForInvalidUserId(): void
7878
{
79-
$this->assertSame([], CwmlocationHelper::getTeacherLocations(1));
80-
$this->assertSame([], CwmlocationHelper::getTeacherLocations(999));
8179
$this->assertSame([], CwmlocationHelper::getTeacherLocations(0));
80+
$this->assertSame([], CwmlocationHelper::getTeacherLocations(-1));
8281
}
8382

8483
/**
85-
* userIsTeacher() is stubbed and must return false for any combination of
86-
* user ID and message ID until the teacher-user linkage is implemented.
84+
* userIsTeacher() returns false for zero or negative IDs without
85+
* hitting the database.
8786
*
8887
* @return void
89-
* @since 10.1.0
88+
* @since 10.3.0
9089
*/
91-
public function testUserIsTeacherReturnsFalse(): void
90+
public function testUserIsTeacherReturnsFalseForInvalidIds(): void
9291
{
93-
$this->assertFalse(CwmlocationHelper::userIsTeacher(1, 1));
9492
$this->assertFalse(CwmlocationHelper::userIsTeacher(0, 0));
95-
$this->assertFalse(CwmlocationHelper::userIsTeacher(999, 999));
93+
$this->assertFalse(CwmlocationHelper::userIsTeacher(-1, 1));
94+
$this->assertFalse(CwmlocationHelper::userIsTeacher(1, 0));
95+
$this->assertFalse(CwmlocationHelper::userIsTeacher(1, -1));
9696
}
9797

9898
// -------------------------------------------------------------------------

0 commit comments

Comments
 (0)