Skip to content

Commit

Permalink
Merge pull request #16174 from craftcms/feature/cms-1349-track-user-s…
Browse files Browse the repository at this point in the history
…ite-affiliations

Track user/site affiliations
  • Loading branch information
brandonkelly authored Nov 21, 2024
2 parents 1d1ea6b + 88261d1 commit 92e7a5e
Show file tree
Hide file tree
Showing 18 changed files with 507 additions and 60 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### Content Management
- “Related To”, “Not Related To”, “Author”, and relational field condition rules now allow multiple elements to be specified. ([#16121](https://github.com/craftcms/cms/discussions/16121))
- Improved the styling of inline code fragments. ([#16141](https://github.com/craftcms/cms/pull/16141))
- Added the “Affiliated Site” user condition rule. ([#16174](https://github.com/craftcms/cms/pull/16174))

### Accessibility
- Improved the accessibility of Checkboxes and Radio Buttons fields that allow custom options. ([#16080](https://github.com/craftcms/cms/pull/16080))
Expand All @@ -11,14 +12,23 @@

### Administration
- Added the “Show the ‘URL Suffix’ field” setting to Link fields. ([#15813](https://github.com/craftcms/cms/discussions/15813))
- Added the “Affiliated Site” native user field. ([#16174](https://github.com/craftcms/cms/pull/16174))

### Development
- Added support for fallback element partial templates, e.g. `_partials/entry.twig` as opposed to `_partials/entry/typeHandle.twig`. ([#16125](https://github.com/craftcms/cms/pull/16125))
- Added the `affiliatedSite` and `affiliatedSiteId` user query and GraphQL params. ([#16174](https://github.com/craftcms/cms/pull/16174))
- Added the `affiliatedSiteHandle` and `affiliatedSiteId` user GraphQL field. ([#16174](https://github.com/craftcms/cms/pull/16174))

### Extensibility
- Added `craft\base\conditions\BaseElementSelectConditionRule::allowMultiple()`.
- Added `craft\base\conditions\BaseElementSelectConditionRule::getElementIds()`.
- Added `craft\base\conditions\BaseElementSelectConditionRule::setElementIds()`.
- Added `craft\elements\User::$affiliatedSiteId`.
- Added `craft\elements\User::getAffiliatedSite()`.
- Added `craft\fields\data\LinkData::$urlSuffix`.
- Added `craft\fields\data\LinkData::getUrl()`.
- Added `craft\mail\Mailer::$siteId`.
- `craft\models\Site` now implements `craft\base\Chippable`.

### System
- Craft now keeps track of which site users registered from. When sending an email from the control panel, the current site is now set to the user’s affiliated site, if known. ([#16174](https://github.com/craftcms/cms/pull/16174))
4 changes: 4 additions & 0 deletions src/base/ApplicationTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
use craft\fieldlayoutelements\entries\EntryTitleField;
use craft\fieldlayoutelements\FullNameField;
use craft\fieldlayoutelements\TitleField;
use craft\fieldlayoutelements\users\AffiliatedSiteField;
use craft\fieldlayoutelements\users\EmailField;
use craft\fieldlayoutelements\users\FullNameField as UserFullNameField;
use craft\fieldlayoutelements\users\PhotoField;
Expand Down Expand Up @@ -1728,6 +1729,9 @@ private function _registerFieldLayoutListener(): void
$event->fields[] = UserFullNameField::class;
$event->fields[] = PhotoField::class;
$event->fields[] = EmailField::class;
if (Craft::$app->getIsMultiSite()) {
$event->fields[] = AffiliatedSiteField::class;
}
break;
}
});
Expand Down
2 changes: 1 addition & 1 deletion src/config/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
'id' => 'CraftCMS',
'name' => 'Craft CMS',
'version' => '5.5.2',
'schemaVersion' => '5.5.0.0',
'schemaVersion' => '5.6.0.0',
'minVersionRequired' => '4.5.0',
'basePath' => dirname(__DIR__), // Defines the @app alias
'runtimePath' => '@storage/runtime', // Defines the @runtime alias
Expand Down
9 changes: 6 additions & 3 deletions src/controllers/UsersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -1584,14 +1584,17 @@ public function actionSaveUser(): ?Response
$user->passwordResetRequired = (bool)$this->request->getBodyParam('passwordResetRequired', $user->passwordResetRequired);
}

// If this is public registration and it's a Pro version,
// set the default group on the user, so that any content
// based on user group condition can be validated and saved against them
if ($isPublicRegistration) {
// set the default group on the user, so that any content
// based on user group condition can be validated and saved against them
$groups = Craft::$app->getUsers()->getDefaultUserGroups($user);
if (!empty($groups)) {
$user->setGroups($groups);
}

// keep track of which site they registered from
// (do this even if it's not a multi-site install, in case it becomes one later.)
$user->affiliatedSiteId = Craft::$app->getSites()->getCurrentSite()->id;
}

// If this is Craft Pro, grab any profile content from post
Expand Down
34 changes: 31 additions & 3 deletions src/elements/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
use craft\helpers\User as UserHelper;
use craft\i18n\Formatter;
use craft\models\FieldLayout;
use craft\models\Site;
use craft\models\UserGroup;
use craft\records\User as UserRecord;
use craft\records\WebAuthn as WebAuthnRecord;
Expand Down Expand Up @@ -457,17 +458,18 @@ protected static function defineSortOptions(): array
*/
protected static function defineTableAttributes(): array
{
return array_merge(parent::defineTableAttributes(), [
return array_merge(parent::defineTableAttributes(), array_filter([
'email' => ['label' => Craft::t('app', 'Email')],
'username' => ['label' => Craft::t('app', 'Username')],
'fullName' => ['label' => Craft::t('app', 'Full Name')],
'firstName' => ['label' => Craft::t('app', 'First Name')],
'lastName' => ['label' => Craft::t('app', 'Last Name')],
'groups' => ['label' => Craft::t('app', 'Groups')],
'affiliatedSite' => Craft::$app->getIsMultiSite() ? ['label' => Craft::t('app', 'Affiliated Site')] : null,
'preferredLanguage' => ['label' => Craft::t('app', 'Preferred Language')],
'preferredLocale' => ['label' => Craft::t('app', 'Preferred Locale')],
'lastLoginDate' => ['label' => Craft::t('app', 'Last Login')],
]);
]));
}

/**
Expand Down Expand Up @@ -525,6 +527,10 @@ protected static function defineCardAttributes(): array
'label' => Craft::t('app', 'Groups'),
'placeholder' => Craft::t('app', 'Group Name'),
],
'affiliatedSite' => [
'label' => Craft::t('app', 'Affiliated Site'),
'placeholder' => Craft::t('app', 'Site Name'),
],
'preferredLanguage' => [
'label' => Craft::t('app', 'Preferred Language'),
'placeholder' => $i18n->getLocaleById('en')->getDisplayName(Craft::$app->language),
Expand Down Expand Up @@ -680,6 +686,12 @@ public static function findIdentityByAccessToken($token, $type = null): ?self
*/
public ?string $password = null;

/**
* @var int|null Affiliated site ID
* @since 5.6.0
*/
public ?int $affiliatedSiteId = null;

/**
* @var DateTime|null Last login date
*/
Expand Down Expand Up @@ -961,7 +973,7 @@ protected function defineRules(): array
]);

$rules[] = [['lastLoginDate', 'lastInvalidLoginDate', 'lockoutDate', 'lastPasswordChangeDate', 'verificationCodeIssuedDate'], DateTimeValidator::class];
$rules[] = [['invalidLoginCount', 'photoId'], 'number', 'integerOnly' => true];
$rules[] = [['invalidLoginCount', 'photoId', 'affiliatedSiteId'], 'number', 'integerOnly' => true];
$rules[] = [['username', 'email', 'unverifiedEmail', 'fullName', 'firstName', 'lastName'], 'trim', 'skipOnEmpty' => true];
$rules[] = [['email', 'unverifiedEmail'], 'email', 'enableIDN' => App::supportsIdn(), 'enableLocalIDN' => false];
$rules[] = [['email', 'username', 'fullName', 'firstName', 'lastName', 'password', 'unverifiedEmail'], 'string', 'max' => 255];
Expand Down Expand Up @@ -1514,6 +1526,21 @@ public function setFriendlyName(string $friendlyName): void
$this->_friendlyName = $friendlyName;
}

/**
* Returns the user’s affiliated site, if they have one.
*
* @return Site|null
* @since 5.6.0
*/
public function getAffiliatedSite(): ?Site
{
if ($this->affiliatedSiteId === null || !Craft::$app->getIsMultiSite()) {
return null;
}

return Craft::$app->getSites()->getSiteById($this->affiliatedSiteId, true);
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -2427,6 +2454,7 @@ public function afterSave(bool $isNew): void
$this->prepareNamesForSave();

$record->photoId = $this->photoId;
$record->affiliatedSiteId = $this->affiliatedSiteId;
$record->admin = $this->admin;
$record->username = $this->username;
$record->fullName = $this->fullName;
Expand Down
67 changes: 67 additions & 0 deletions src/elements/conditions/users/AffiliatedSiteConditionRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace craft\elements\conditions\users;

use Craft;
use craft\base\conditions\BaseMultiSelectConditionRule;
use craft\base\ElementInterface;
use craft\elements\conditions\ElementConditionRuleInterface;
use craft\elements\db\ElementQueryInterface;
use craft\elements\db\UserQuery;
use craft\elements\User;
use craft\models\Site;

/**
* Site condition rule.
*
* @author Pixel & Tonic, Inc. <[email protected]>
* @since 5.6.0
*/
class AffiliatedSiteConditionRule extends BaseMultiSelectConditionRule implements ElementConditionRuleInterface
{
/**
* @inheritdoc
*/
public function getLabel(): string
{
return Craft::t('app', 'Affiliated Site');
}

/**
* @inheritdoc
*/
public function getExclusiveQueryParams(): array
{
return ['affiliatedSite', 'affiliatedSiteId'];
}

/**
* @inheritdoc
*/
protected function options(): array
{
return array_map(fn(Site $site) => [
'label' => $site->getUiLabel(),
'value' => $site->uid,
], Craft::$app->getSites()->getAllSites());
}

/**
* @inheritdoc
*/
public function modifyQuery(ElementQueryInterface $query): void
{
$sites = Craft::$app->getSites();
/** @var UserQuery $query */
$query->affiliatedSiteId($this->paramValue(fn($uid) => $sites->getSiteByUid($uid, true)->id ?? null));
}

/**
* @inheritdoc
*/
public function matchElement(ElementInterface $element): bool
{
/** @var User $element */
return $this->matchValue($element->getAffiliatedSite()?->uid);
}
}
4 changes: 4 additions & 0 deletions src/elements/conditions/users/UserCondition.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ protected function selectableConditionRules(): array
$types[] = UsernameConditionRule::class;
}

if (Craft::$app->getIsMultiSite()) {
$types[] = AffiliatedSiteConditionRule::class;
}

return $types;
}
}
Loading

0 comments on commit 92e7a5e

Please sign in to comment.