commit 8fa0070201a640fb37dcf67bbaf98c6f10810910 Author: Mevaser of Yehudah Date: Thu Dec 14 11:37:49 2023 -0600 Drupal 8 stable release 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; + } +}