From 8fa0070201a640fb37dcf67bbaf98c6f10810910 Mon Sep 17 00:00:00 2001 From: Mevaser of Yehudah Date: Thu, 14 Dec 2023 11:37:49 -0600 Subject: [PATCH] Drupal 8 stable release --- composer.json | 5 + config/install/recruiter.mail.yml | 3 + config/schema/recruiter.schema.yml | 7 + recruiter.info.yml | 6 + recruiter.install | 58 +++++ recruiter.links.menu.yml | 6 + recruiter.links.task.yml | 8 + recruiter.module | 101 ++++++++ recruiter.permissions.yml | 4 + recruiter.routing.yml | 35 +++ src/Controller/UsersInvitedController.php | 196 +++++++++++++++ src/Form/RecruiterForm.php | 276 ++++++++++++++++++++++ 12 files changed, 705 insertions(+) create mode 100644 composer.json create mode 100644 config/install/recruiter.mail.yml create mode 100644 config/schema/recruiter.schema.yml create mode 100644 recruiter.info.yml create mode 100644 recruiter.install create mode 100644 recruiter.links.menu.yml create mode 100644 recruiter.links.task.yml create mode 100644 recruiter.module create mode 100644 recruiter.permissions.yml create mode 100644 recruiter.routing.yml create mode 100644 src/Controller/UsersInvitedController.php create mode 100644 src/Form/RecruiterForm.php diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b61dfb9 --- /dev/null +++ b/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "mmucklo/email-parse": "*" + } +} diff --git a/config/install/recruiter.mail.yml b/config/install/recruiter.mail.yml new file mode 100644 index 0000000..84bdce5 --- /dev/null +++ b/config/install/recruiter.mail.yml @@ -0,0 +1,3 @@ +resend_invitation: + body: "[user:display-name],\n\nRemember that invitation?\nYou were invited to help with our website at [site:name]. We created an account for you. You may now log in by clicking this link or copying and pasting it into your browser:\n\n[user:one-time-login-url]\n\nThis link can only be used once to log in and will lead you to a page where you can set your password.\n\nAfter setting your password, you will be able to log in by going to [site:login-url] in the future using:\n\nusername: [user:name]\npassword: Your password\n\nYou can also scroll to the bottom of the website and look for 'Disciple Login'.\n\n-- Welcome to the [site:name] website team!" + subject: 'Please activate your account at [site:name]' diff --git a/config/schema/recruiter.schema.yml b/config/schema/recruiter.schema.yml new file mode 100644 index 0000000..a1a621c --- /dev/null +++ b/config/schema/recruiter.schema.yml @@ -0,0 +1,7 @@ +recruiter.mail: + type: config_object + label: 'Email settings' + mapping: + resend_invitation: + type: mail + label: 'Resend invitation mail.' \ No newline at end of file diff --git a/recruiter.info.yml b/recruiter.info.yml new file mode 100644 index 0000000..0539706 --- /dev/null +++ b/recruiter.info.yml @@ -0,0 +1,6 @@ +name: Above All - Disciple Recruiter +type: module +description: Site architects can invite coverings, covering can invite disciples to join and contribute +core: 8.x +core_version_requirement: ^8 || ^9 || ^10 +package: Above All - Twelve Tribes diff --git a/recruiter.install b/recruiter.install new file mode 100644 index 0000000..1c99c71 --- /dev/null +++ b/recruiter.install @@ -0,0 +1,58 @@ + 'Keep track of the users invited using the Delegater module', + 'fields' => [ + 'uid' => [ + 'description' => 'Primary key: {users}.uid for user.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ], + 'invitation_date' => [ + 'description' => 'The identifier of the data.', + 'type' => 'varchar_ascii', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ], + ], + 'primary key' => ['uid'], + 'foreign keys' => [ + 'uid' => ['users' => 'uid'], + ], + ]; + \Drupal::database()->schema()->createTable('mytable2', $schema['recruiter_users']); + // Drupal 9 + // $config_path = drupal_get_path('module', 'recruiter') . '/config/install/recruiter.mail.yml'; + // Drupal 10 + $extension_path_resolver = Drupal::service('extension.path.resolver'); + $config_path = $extension_path_resolver->getPath('module', 'recruiter') . '/config/install/recruiter.mail.yml'; + // end of changes + + $data = Yaml::parse(file_get_contents($config_path)); + \Drupal::configFactory()->getEditable('recruiter.mail')->setData($data)->save(TRUE); +} + +/** + * Rename the mytable2 to the correct name. + */ +function recruiter_update_8002(&$sandbox) { + \Drupal::database()->schema()->renameTable('mytable2', 'recruiter_users'); +} + +/** + * Delete the recruiter_users table. + */ +function recruiter_update_8005(&$sandbox) { + if (\Drupal::database()->schema()->tableExists('recruiter_users')) { + \Drupal::database()->schema()->dropTable('recruiter_users'); + } +} diff --git a/recruiter.links.menu.yml b/recruiter.links.menu.yml new file mode 100644 index 0000000..37a079c --- /dev/null +++ b/recruiter.links.menu.yml @@ -0,0 +1,6 @@ +admin.recruiter.link: + title: Delegater + description: Delegate to disciples to help. + route_name: recruiter.form + parent: user.admin_index + weight: 4 diff --git a/recruiter.links.task.yml b/recruiter.links.task.yml new file mode 100644 index 0000000..3bb882e --- /dev/null +++ b/recruiter.links.task.yml @@ -0,0 +1,8 @@ +recruiter.invite: + title: 'Disciple Delegater' + route_name: recruiter.form + base_route: recruiter.form +recruiter.InvitedUsersList: + title: 'Pending Users' + route_name: recruiter.invited_users_controller_list + base_route: recruiter.form diff --git a/recruiter.module b/recruiter.module new file mode 100644 index 0000000..b404596 --- /dev/null +++ b/recruiter.module @@ -0,0 +1,101 @@ + 'details', + '#title' => t('Resend invitation'), + '#open' => FALSE, + '#description' => t('Send a reminder to the users invited via the Delegate module.'), + '#group' => 'email', + ]; + $form['email_resend_invite']['resend_invitation_subject'] = [ + '#type' => 'textfield', + '#title' => t('Subject'), + '#default_value' => $mail_config->get('resend_invitation.subject'), + '#maxlength' => 180, + ]; + $form['email_resend_invite']['resend_invitation_body'] = [ + '#type' => 'textarea', + '#title' => t('Body'), + '#default_value' => $mail_config->get('resend_invitation.body'), + '#rows' => 15, + ]; + + $form['#submit'][] = 'recruiter_save_email_resend_invite'; +} + +/** + * Save the changes made to the recruiter email. + */ +function recruiter_save_email_resend_invite(&$form, $form_state) { + \Drupal::configFactory()->getEditable('recruiter.mail') + ->set('resend_invitation.body', $form_state->getValue('resend_invitation_body')) + ->set('resend_invitation.subject', $form_state->getValue('resend_invitation_subject')) + ->save(); +} + +/** + * Implements hook_mail(). + */ +function recruiter_mail($key, &$message, $params) { + $token_service = \Drupal::token(); + $language_manager = \Drupal::languageManager(); + $langcode = $message['langcode']; + $variables = ['user' => $params['account']]; + + $language = \Drupal::languageManager()->getLanguage($params['account']->getPreferredLangcode()); + $original_language = $language_manager->getConfigOverrideLanguage(); + $language_manager->setConfigOverrideLanguage($language); + $mail_config = \Drupal::config('recruiter.mail'); + + $token_options = [ + 'langcode' => $langcode, + 'callback' => 'recruiter_mail_tokens', + 'clear' => TRUE, + ]; + $message['subject'] .= PlainTextOutput::renderFromHtml($token_service->replace($mail_config->get($key . '.subject'), $variables, $token_options)); + $message['body'][] = $token_service->replace($mail_config->get($key . '.body'), $variables, $token_options); + + $language_manager->setConfigOverrideLanguage($original_language); +} + +/** + * Replace the tokens with data. + * + * @param array $replacements + * Tokens. + * @param array $data + * Data where the tokens will be replaced. + * @param array $options + * Options. + */ +function recruiter_mail_tokens(&$replacements, $data, $options = []) { + if (isset($data['user'])) { + $replacements['[user:one-time-login-url]'] = user_pass_reset_url($data['user'], $options); + $replacements['[user:cancel-url]'] = user_cancel_url($data['user'], $options); + } +} + + +function recruiter_help($route_name, RouteMatchInterface $route_match) { + switch ($route_name) { + case 'help.page.recruiter': + $helpText = <<< EOT +

Disciple Delegater

+

This custom module allows disciples to be signed up and assigned to the website team. A Site Architect can invite people to become + Coverings of community pages, and coverings can invite disciples to their communities' pages.

+

The email template can be edited here (new user created + by administrator)

+EOT; + return($helpText); + } +} diff --git a/recruiter.permissions.yml b/recruiter.permissions.yml new file mode 100644 index 0000000..829b469 --- /dev/null +++ b/recruiter.permissions.yml @@ -0,0 +1,4 @@ +administer recruiter: + title: 'Administer Disciple Delegater' + description: 'Administer the Disciple Delegater system' + restrict access: true diff --git a/recruiter.routing.yml b/recruiter.routing.yml new file mode 100644 index 0000000..8d43159 --- /dev/null +++ b/recruiter.routing.yml @@ -0,0 +1,35 @@ +recruiter.form: + path: /admin/config/people/recruiter + defaults: + _form: Drupal\recruiter\Form\RecruiterForm + _title: 'Delegate to Disciples' + requirements: + _permission: 'administer recruiter' + +recruiter.invited_users_controller_list: + path: '/admin/config/people/recruiter/list' + defaults: + _controller: '\Drupal\recruiter\Controller\UsersInvitedController::invitedUsersList' + _title: "Delegated users who haven't used their account yet." + requirements: + _permission: 'administer recruiter' + +recruiter.re_send_invitation: + path: '/admin/config/people/recruiter/re_send_invitation/{user}' + defaults: + _controller: '\Drupal\recruiter\Controller\UsersInvitedController::reSendInvitation' + _title: 'Resend Invitation' + requirements: + _permission: 'administer recruiter' + options: + parameters: + user: + type: entity:user + +recruiter.re_send_invitation_all: + path: '/admin/config/people/recruiter/re_send_invitation_all' + defaults: + _controller: '\Drupal\recruiter\Controller\UsersInvitedController::reSendInvitationAll' + _title: 'Resend Invitation All' + requirements: + _permission: 'administer recruiter' diff --git a/src/Controller/UsersInvitedController.php b/src/Controller/UsersInvitedController.php new file mode 100644 index 0000000..3f247a9 --- /dev/null +++ b/src/Controller/UsersInvitedController.php @@ -0,0 +1,196 @@ +mailManager = $mail_manager; + $this->dateFormatter = $date_formatter; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.mail'), + $container->get('date.formatter') + ); + } + + /** + * Invited Users List. + * + * @return string + * Return Hello string. + */ + public function invitedUsersList() { + $header = [ + ['data' => $this->t('UID'), 'field' => 'ufd.uid'], + ['data' => $this->t('Name'), 'field' => 'ufd.name'], + ['data' => $this->t('Email'), 'field' => 'ufd.mail'], + ['data' => $this->t('Actions'), 'field' => 'actions'], + ]; + + $query = $this->getDatabase()->select('users_field_data', 'ufd') + ->fields('ufd', ['uid', 'name', 'mail']) + ->where('ufd.pass IS NULL') + ->where( 'ufd.uid != 0') + ->where( 'ufd.status = 1') + ->extend('Drupal\Core\Database\Query\TableSortExtender') + ->extend('Drupal\Core\Database\Query\PagerSelectExtender') + ->orderByHeader($header); + $data = $query->execute(); + $rows = []; + foreach ($data as $row) { + $row = (array) $row; + + $link = Link::createFromRoute('Re-Send Invitation', 'recruiter.re_send_invitation', ['user' => $row['uid']]); + $row['actions'] = $link; + $row['name'] = Link::createFromRoute($row['name'], 'entity.user.canonical', ['user' => $row['uid']]); + + $rows[] = ['data' => $row]; + + } + $build['table_pager'][] = [ + '#type' => 'table', + '#header' => $header, + '#rows' => $rows, + ]; + $build['table_pager'][] = [ + '#type' => 'pager', + ]; + + $resend_all = Link::createFromRoute( + 'Re-Send invitation to all the pending users', + 'recruiter.re_send_invitation_all', + [], + ['attributes' => ['class' => ['button', 'button--primary', 'button--small']]] + ); + $build['resend_all'] = $resend_all->toRenderable(); + return $build; + + } + + /** + * Send a email to an specific user. + * + * @param \Drupal\user\UserInterface $user + * User instance. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * A redirect response object that may be returned by the controller. + */ + public function reSendInvitation(UserInterface $user) { + $to = $user->getEmail(); + $langcode = $user->getPreferredLangcode(); + + $result = $this->mailManager->mail('recruiter', 'resend_invitation', $to, $langcode, ['account' => $user]); + + if (!$result['result']) { + $this->messenger()->addError($this->t('There was a problem sending the email, please try again.')); + } + else { + $this->messenger()->addMessage($this->t('The message has been sent.')); + } + + return $this->redirect('recruiter.invited_users_controller_list'); + } + + /** + * Add an email to all the pending users. + * + * @todo Use the batch API or create a queue. + */ + public function reSendInvitationAll() { + $query = $this->getDatabase()->select('users_field_data', 'ufd') + ->fields('ufd', ['uid', 'name', 'mail']) + ->where('ufd.pass IS NULL') + ->where( 'ufd.uid != 0') + ->where( 'ufd.status = 1'); + + $data = $query->execute(); + + foreach ($data as $row) { + + $row = (array) $row; + $user = User::load($row['uid']); + $to = $user->getEmail(); + $langcode = $user->getPreferredLangcode(); + + $result = $this->mailManager->mail('recruiter', 'resend_invitation', $to, $langcode, ['account' => $user]); + $mail_sent = 0; + $mail_errors = 0; + if (!$result['result']) { + $mail_errors += 0; + } + else { + $mail_sent += 1; + } + + if ($mail_errors > 0) { + $message = $this->formatPlural($mail_errors, 'There was a problem sending %count email', 'There was a problem sending %count emails', ['%count' => $mail_errors]); + $this->messenger()->addError($message); + } + + if ($mail_sent > 0) { + $message = $this->formatPlural($mail_sent, '%count mail was sent', '%count mails were sent.', ['%count' => $mail_sent]); + $this->messenger()->addMessage($message); + } + } + + return $this->redirect('recruiter.invited_users_controller_list'); + } + + /** + * Get a database instance. + * + * Accord with http://drupal.stackexchange.com/a/213657/4362 is not yet + * possible to inject the database connection. + * + * The idea of this method is having a way to mock the db connection on tests. + * + * @return \Drupal\Core\Database\Connection + * Database connection. + */ + public function getDatabase() { + return \Drupal::database(); + } + +} diff --git a/src/Form/RecruiterForm.php b/src/Form/RecruiterForm.php new file mode 100644 index 0000000..7cd4050 --- /dev/null +++ b/src/Form/RecruiterForm.php @@ -0,0 +1,276 @@ +getRoles(); + if (in_array('administrator', $current_user_roles)) { + $node_storage = \Drupal::entityTypeManager()->getStorage('node'); + $query = \Drupal::entityQuery('node') + ->condition('status', 1) + // Drupal 10 addition + ->accessCheck(TRUE) + ->condition('type', 'community') + ->execute(); + $community_list = $node_storage->loadMultiple($query); + foreach ($community_list as $community) { + $community_names[]= array($community->id() => $community->title->value); + } + } else { + $user = \Drupal\user\Entity\User::load(\Drupal::currentUser()->id()); + $cxn2 = $user->get('field_connections')->referencedEntities(); + foreach ($cxn2 as $community) { + $community_names[$community->id()] = $community->title->value; + } + } + + $help = <<< EOT +
+
+
+

Workers

+

Matthew 9:38 'Ask the Lord of the harvest, therefore, to send out workers into his harvest field.'

+
+
+
+
+ +
+
+

Your community can work together to freshen your page. Under your covering, disciples can:

+
    +
  • Add photos
  • Create Events
  • Write Posts
  • Discover Who's Reading
  • +
+
+
+
+
+

Invite your team members to make an account. They'll receive and email with a link, and they'll be added to your community's team under your covering.

+

Type the first name and clan or tribe, then their email address in angle brackets.
Note: Mezimmah emails will not work.

+EOT; + if (in_array('administrator', $current_user_roles)) { + $help .= "

Since you are Site Architect, these invitees will be Coverings in role, but you must set the Covering field for each Community they cover.

"; + } + $help .= <<< EOT +
+
+ +
+
+
+
+

Need to adjust the team? Made a mistake? Questions?

+ Ask for Help +
+
+

If there's no response

+ Follow Up/Remind +
+
+
+
+EOT; + $help_close = <<< EOT +
+
+
+EOT; + + $form['text_header'] = array( + '#prefix' => '', + '#suffix' => '', + '#markup' => $help, + '#weight' => -100, + ); + $form['mails'] = array( + '#type' => 'textarea', + '#title' => $this->t('Names and email addresses'), + '#description' => $this->t('Enter one person per line, like this: Username <user@email.com>. Remember, no mezimmah addresses.') + ); + + // Create the select list + $form['connections'] = array( + '#type' => 'select', + '#title' => t('Add a community to connect to:'), + '#options' => $community_names, + '#description' => t('The requested disciple will be connected to these communities.'), + '#size' => 6, + '#required' => FALSE, + '#sort_options' => TRUE, + '#multiple' => TRUE, + '#attributes' => array( + 'id' => 'edit-select-connection', + 'style' => 'width:600px', + ) + ); + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = array( + '#prefix' => '
', + '#suffix' => '
', + '#type' => 'submit', + '#value' => $this->t('Send'), + '#button_type' => 'primary', + ); + $form['text_footer'] = array( + '#prefix' => '', + '#suffix' => '', + '#markup' => $help_close, + '#weight' => 500 + ); + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $emails = Parse::getInstance()->parse($form_state->getValue('mails')); + foreach ($emails['email_addresses'] as $email) { + if ($email['invalid']) { + $this->invalid_emails[] = "The {$email['original_address']} is not valid because: {$email['invalid_reason']}"; + } else { + $this->emails[] = $email; + } + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->connections=$form_state->getValue('connections'); + + $number_invitations_sent = $this->createUsers($this->emails,$this->connections); + + if (!empty($this->invalid_emails)) { + foreach ($this->invalid_emails as $invalid) { + $this->messenger()->addError($invalid); + } + } + + if (!empty($this->invitations_sent)) { + $this->messenger()->addMessage('The invitations were sent to the following mails:'); + foreach ($this->invitations_sent as $invitation) { + $this->messenger()->addMessage($invitation); + } + } + + $response = new TrustedRedirectResponse('/intro'); + $form_state->setResponse($response); + } + + /** + * Create new users. + * @return integer the number of sent invitations. + */ + protected function createUsers($emails, $connections) { + if (empty($emails)) { + return 0; + } + $number_invitations_sent = 0; + + foreach ($emails as $email) { + $name = (isset($email['name_parsed']) && !empty($email['name_parsed'])) ? $email['name_parsed'] : $email['local_part_parsed']; + // Check if the email hasn't been registered yet. + $query = \Drupal::entityQuery('user', 'OR'); + $query->condition('init', $email['simple_address']); + $query->condition('mail', $email['simple_address']); + $query->condition('name', $name); + $entity_ids = $query->execute(); + + if (!empty($entity_ids)) { + $this->invalid_emails[] = $this->t('The mail: @mail or the user name: @username is already in use.', ['@mail' => $email['simple_address'], '@username' => $name]); + continue; + } + + $edit = []; + $edit['name'] = $name; + $edit['mail'] = $email['simple_address']; + $edit['init'] = $email['simple_address']; + $edit['status'] = 1; + + $account = User::create($edit); + $current_user_roles = \Drupal::currentUser()->getRoles(); + if (in_array('administrator', $current_user_roles)) { + $account->addRole('elder'); + } + if (!empty($connections)) { + foreach ($connections as $connection) { + $account->field_connections[] = $connection; + } + } + $account->set("field_bio", "Lives in the Community in "); + $account->save(); + $newuserid = $account->id(); + + if (in_array('administrator', $current_user_roles)) { + // now set all covering fields of these communities to this user + if (!empty($connections)) { + foreach ($connections as $connection) { + $entitiesS = \Drupal::entityTypeManager()->getStorage('node'); + $entitiesQ = $entitiesS->getQuery() + ->condition('nid',$connection) + // Drupal 10 add this line + ->accessCheck(TRUE) + ->condition('field_covering',NULL, 'IS') + ->execute(); + $entities = $entitiesS->loadMultiple($entitiesQ); + + if (!empty($entities)) { + $entity = reset($entities); + $entity->set('field_covering', $newuserid); + $entity->save(); + } + } + } + } + + $params['account'] = $account; + $langcode = $account->getPreferredLangcode(); + // Get the custom site notification email to use as the from email address + // if it has been set. + $site_mail = \Drupal::config('system.site')->get('mail_notification'); + // If the custom site notification email has not been set, we use the site + // default for this. + if (empty($site_mail)) { + $site_mail = \Drupal::config('system.site')->get('mail'); + } + if (empty($site_mail)) { + $site_mail = ini_get('sendmail_from'); + } + \Drupal::service('plugin.manager.mail')->mail('user', 'register_admin_created', $account->getEmail(), $langcode, $params, $site_mail); + $number_invitations_sent += 1; + $this->invitations_sent[] = $this->t("@email (username: @username) ", ['@username' => $edit['name'], '@email' => $edit['mail']]); + } + return $number_invitations_sent; + } +}