2019-05-03 06:17:27 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2014-01-31 06:10:36 +08:00
|
|
|
class GroupsController < ApplicationController
|
2018-02-01 12:17:59 +08:00
|
|
|
requires_login only: %i[
|
2016-11-29 16:25:02 +08:00
|
|
|
set_notifications
|
|
|
|
mentionable
|
2017-08-29 00:32:08 +08:00
|
|
|
messageable
|
2018-08-01 11:08:45 +08:00
|
|
|
check_name
|
2016-12-11 23:36:15 +08:00
|
|
|
update
|
2017-06-13 16:10:14 +08:00
|
|
|
histories
|
2017-07-21 14:12:24 +08:00
|
|
|
request_membership
|
2018-03-27 16:45:21 +08:00
|
|
|
search
|
FEATURE: Improve group email settings UI (#13083)
This overhauls the user interface for the group email settings management, aiming to make it a lot easier to test the settings entered and confirm they are correct before proceeding. We do this by forcing the user to test the settings before they can be saved to the database. It also includes some quality of life improvements around setting up IMAP and SMTP for our first supported provider, GMail. This PR does not remove the old group email config, that will come in a subsequent PR. This is related to https://meta.discourse.org/t/imap-support-for-group-inboxes/160588 so read that if you would like more backstory.
### UI
Both site settings of `enable_imap` and `enable_smtp` must be true to test this. You must enable SMTP first to enable IMAP.
You can prefill the SMTP settings with GMail configuration. To proceed with saving these settings you must test them, which is handled by the EmailSettingsValidator.
If there is an issue with the configuration or credentials a meaningful error message should be shown.
IMAP settings must also be validated when IMAP is enabled, before saving.
When saving IMAP, we fetch the mailboxes for that account and populate them. This mailbox must be selected and saved for IMAP to work (the feature acts as though it is disabled until the mailbox is selected and saved):
### Database & Backend
This adds several columns to the Groups table. The purpose of this change is to make it much more explicit that SMTP/IMAP is enabled for a group, rather than relying on settings not being null. Also included is an UPDATE query to backfill these columns. These columns are automatically filled when updating the group.
For GMail, we now filter the mailboxes returned. This is so users cannot use a mailbox like Sent or Trash for syncing, which would generally be disastrous.
There is a new group endpoint for testing email settings. This may be useful in the future for other places in our UI, at which point it can be extracted to a more generic endpoint or module to be included.
2021-05-28 07:28:18 +08:00
|
|
|
new
|
|
|
|
test_email_settings
|
2016-11-29 16:25:02 +08:00
|
|
|
]
|
2023-01-09 20:20:10 +08:00
|
|
|
|
2017-08-31 12:06:56 +08:00
|
|
|
skip_before_action :preload_json, :check_xhr, only: %i[posts_feed mentions_feed]
|
2018-02-01 04:04:09 +08:00
|
|
|
skip_before_action :check_xhr, only: [:show]
|
2020-05-11 13:05:42 +08:00
|
|
|
after_action :add_noindex_header
|
2015-12-15 06:17:09 +08:00
|
|
|
|
2018-03-20 15:50:46 +08:00
|
|
|
TYPE_FILTERS = {
|
2018-03-21 16:32:08 +08:00
|
|
|
my:
|
|
|
|
Proc.new do |groups, user|
|
|
|
|
raise Discourse::NotFound unless user
|
|
|
|
Group.member_of(groups, user)
|
2018-03-20 15:50:46 +08:00
|
|
|
end,
|
2018-03-21 16:32:08 +08:00
|
|
|
owner:
|
|
|
|
Proc.new do |groups, user|
|
|
|
|
raise Discourse::NotFound unless user
|
|
|
|
Group.owner_of(groups, user)
|
2018-03-20 15:50:46 +08:00
|
|
|
end,
|
|
|
|
public: Proc.new { |groups| groups.where(public_admission: true, automatic: false) },
|
2020-01-15 18:21:58 +08:00
|
|
|
close: Proc.new { |groups| groups.where(public_admission: false, automatic: false) },
|
2018-03-20 15:50:46 +08:00
|
|
|
automatic: Proc.new { |groups| groups.where(automatic: true) },
|
2020-10-21 06:46:45 +08:00
|
|
|
non_automatic: Proc.new { |groups| groups.where(automatic: false) },
|
2018-03-20 15:50:46 +08:00
|
|
|
}
|
2020-08-25 06:55:21 +08:00
|
|
|
ADD_MEMBERS_LIMIT = 1000
|
2018-03-20 15:50:46 +08:00
|
|
|
|
2016-12-14 17:26:16 +08:00
|
|
|
def index
|
2018-04-10 09:22:01 +08:00
|
|
|
unless SiteSetting.enable_group_directory? || current_user&.staff?
|
2016-12-22 14:14:03 +08:00
|
|
|
raise Discourse::InvalidAccess.new(:enable_group_directory)
|
|
|
|
end
|
|
|
|
|
2018-03-19 16:14:50 +08:00
|
|
|
order = %w[name user_count].delete(params[:order])
|
2020-01-15 18:21:58 +08:00
|
|
|
dir = params[:asc].to_s == "true" ? "ASC" : "DESC"
|
|
|
|
sort = order ? "#{order} #{dir}" : nil
|
|
|
|
groups = Group.visible_groups(current_user, sort)
|
2018-03-20 15:50:46 +08:00
|
|
|
type_filters = TYPE_FILTERS.keys
|
|
|
|
|
2020-01-15 18:21:58 +08:00
|
|
|
if (username = params[:username]).present?
|
|
|
|
raise Discourse::NotFound unless user = User.find_by_username(username)
|
|
|
|
groups = TYPE_FILTERS[:my].call(groups.members_visible_groups(current_user, sort), user)
|
2018-03-21 16:32:08 +08:00
|
|
|
type_filters = type_filters - %i[my owner]
|
|
|
|
end
|
|
|
|
|
2020-01-15 18:21:58 +08:00
|
|
|
if (filter = params[:filter]).present?
|
|
|
|
groups = Group.search_groups(filter, groups: groups)
|
|
|
|
end
|
|
|
|
|
|
|
|
if !guardian.is_staff?
|
2017-07-04 03:26:46 +08:00
|
|
|
# hide automatic groups from all non stuff to de-clutter page
|
2022-11-02 03:05:13 +08:00
|
|
|
groups = groups.where("automatic IS FALSE OR groups.id = ?", Group::AUTO_GROUPS[:moderators])
|
2018-03-20 15:50:46 +08:00
|
|
|
type_filters.delete(:automatic)
|
2017-07-04 03:26:46 +08:00
|
|
|
end
|
|
|
|
|
2017-08-08 21:45:27 +08:00
|
|
|
if Group.preloaded_custom_field_names.present?
|
|
|
|
Group.preload_custom_fields(groups, Group.preloaded_custom_field_names)
|
|
|
|
end
|
|
|
|
|
2018-03-20 15:50:46 +08:00
|
|
|
if type = params[:type]&.to_sym
|
2020-01-15 18:21:58 +08:00
|
|
|
raise Discourse::InvalidParameters.new(:type) unless callback = TYPE_FILTERS[type]
|
2018-06-29 08:43:33 +08:00
|
|
|
groups = callback.call(groups, current_user)
|
2018-03-20 15:50:46 +08:00
|
|
|
end
|
|
|
|
|
2018-03-19 18:28:57 +08:00
|
|
|
if current_user
|
|
|
|
group_users = GroupUser.where(group: groups, user: current_user)
|
|
|
|
user_group_ids = group_users.pluck(:group_id)
|
|
|
|
owner_group_ids = group_users.where(owner: true).pluck(:group_id)
|
2018-03-21 16:32:08 +08:00
|
|
|
else
|
|
|
|
type_filters = type_filters - %i[my owner]
|
2018-03-19 18:28:57 +08:00
|
|
|
end
|
2016-12-21 20:58:51 +08:00
|
|
|
|
2020-10-21 06:46:45 +08:00
|
|
|
type_filters.delete(:non_automatic)
|
|
|
|
|
2020-01-17 06:57:34 +08:00
|
|
|
# count the total before doing pagination
|
|
|
|
total = groups.count
|
|
|
|
|
2020-01-15 18:21:58 +08:00
|
|
|
page = params[:page].to_i
|
|
|
|
page_size = MobileDetection.mobile_device?(request.user_agent) ? 15 : 36
|
2018-03-21 09:25:42 +08:00
|
|
|
groups = groups.offset(page * page_size).limit(page_size)
|
|
|
|
|
2016-12-21 20:58:51 +08:00
|
|
|
render_json_dump(
|
2018-03-19 18:28:57 +08:00
|
|
|
groups:
|
|
|
|
serialize_data(
|
|
|
|
groups,
|
|
|
|
BasicGroupSerializer,
|
|
|
|
user_group_ids: user_group_ids || [],
|
|
|
|
owner_group_ids: owner_group_ids || [],
|
|
|
|
),
|
2018-03-20 15:50:46 +08:00
|
|
|
extras: {
|
2018-03-21 16:32:08 +08:00
|
|
|
type_filters: type_filters,
|
2018-03-20 15:50:46 +08:00
|
|
|
},
|
2020-01-17 06:57:34 +08:00
|
|
|
total_rows_groups: total,
|
2018-08-20 17:08:50 +08:00
|
|
|
load_more_groups:
|
|
|
|
groups_path(page: page + 1, type: type, order: order, asc: params[:asc], filter: filter),
|
2016-12-21 20:58:51 +08:00
|
|
|
)
|
2016-12-14 17:26:16 +08:00
|
|
|
end
|
|
|
|
|
2014-01-31 06:10:36 +08:00
|
|
|
def show
|
2018-02-01 04:04:09 +08:00
|
|
|
respond_to do |format|
|
|
|
|
group = find_group(:id)
|
|
|
|
|
|
|
|
format.html do
|
|
|
|
@title = group.full_name.present? ? group.full_name.capitalize : group.name
|
2021-07-12 22:35:57 +08:00
|
|
|
@full_title = "#{@title} - #{SiteSetting.title}"
|
2018-02-01 04:04:09 +08:00
|
|
|
@description_meta =
|
|
|
|
group.bio_cooked.present? ? PrettyText.excerpt(group.bio_cooked, 300) : @title
|
|
|
|
render :show
|
|
|
|
end
|
|
|
|
|
|
|
|
format.json do
|
2018-03-29 14:57:10 +08:00
|
|
|
groups = Group.visible_groups(current_user)
|
2020-09-11 02:36:40 +08:00
|
|
|
if !guardian.is_staff?
|
2022-11-02 03:05:13 +08:00
|
|
|
groups =
|
|
|
|
groups.where("automatic IS FALSE OR groups.id = ?", Group::AUTO_GROUPS[:moderators])
|
2020-09-11 02:36:40 +08:00
|
|
|
end
|
2018-03-29 14:57:10 +08:00
|
|
|
|
|
|
|
render_json_dump(
|
|
|
|
group: serialize_data(group, GroupShowSerializer, root: nil),
|
|
|
|
extras: {
|
|
|
|
visible_group_names: groups.pluck(:name),
|
|
|
|
},
|
|
|
|
)
|
2018-02-01 04:04:09 +08:00
|
|
|
end
|
|
|
|
end
|
2014-01-31 06:10:36 +08:00
|
|
|
end
|
|
|
|
|
2018-03-27 16:45:21 +08:00
|
|
|
def new
|
|
|
|
end
|
|
|
|
|
2016-12-13 15:15:20 +08:00
|
|
|
def edit
|
|
|
|
end
|
|
|
|
|
2016-11-29 16:25:02 +08:00
|
|
|
def update
|
|
|
|
group = Group.find(params[:id])
|
2022-03-24 20:50:44 +08:00
|
|
|
guardian.ensure_can_edit!(group) if !guardian.can_admin_group?(group)
|
2016-11-29 16:25:02 +08:00
|
|
|
|
2022-03-24 20:50:44 +08:00
|
|
|
group_attributes = group_params(automatic: group.automatic)
|
|
|
|
reset_group_email_settings_if_disabled!(group, group_attributes)
|
FEATURE: Improve group email settings UI (#13083)
This overhauls the user interface for the group email settings management, aiming to make it a lot easier to test the settings entered and confirm they are correct before proceeding. We do this by forcing the user to test the settings before they can be saved to the database. It also includes some quality of life improvements around setting up IMAP and SMTP for our first supported provider, GMail. This PR does not remove the old group email config, that will come in a subsequent PR. This is related to https://meta.discourse.org/t/imap-support-for-group-inboxes/160588 so read that if you would like more backstory.
### UI
Both site settings of `enable_imap` and `enable_smtp` must be true to test this. You must enable SMTP first to enable IMAP.
You can prefill the SMTP settings with GMail configuration. To proceed with saving these settings you must test them, which is handled by the EmailSettingsValidator.
If there is an issue with the configuration or credentials a meaningful error message should be shown.
IMAP settings must also be validated when IMAP is enabled, before saving.
When saving IMAP, we fetch the mailboxes for that account and populate them. This mailbox must be selected and saved for IMAP to work (the feature acts as though it is disabled until the mailbox is selected and saved):
### Database & Backend
This adds several columns to the Groups table. The purpose of this change is to make it much more explicit that SMTP/IMAP is enabled for a group, rather than relying on settings not being null. Also included is an UPDATE query to backfill these columns. These columns are automatically filled when updating the group.
For GMail, we now filter the mailboxes returned. This is so users cannot use a mailbox like Sent or Trash for syncing, which would generally be disastrous.
There is a new group endpoint for testing email settings. This may be useful in the future for other places in our UI, at which point it can be extracted to a more generic endpoint or module to be included.
2021-05-28 07:28:18 +08:00
|
|
|
|
2021-07-15 22:23:57 +08:00
|
|
|
categories, tags = []
|
|
|
|
if !group.automatic || current_user.admin
|
2022-03-24 20:50:44 +08:00
|
|
|
notification_level, categories, tags = user_default_notifications(group, group_attributes)
|
2021-07-15 22:23:57 +08:00
|
|
|
|
|
|
|
if params[:update_existing_users].blank?
|
2021-08-31 18:41:26 +08:00
|
|
|
user_count = count_existing_users(group.group_users, notification_level, categories, tags)
|
2022-03-24 20:50:44 +08:00
|
|
|
if user_count > 0
|
|
|
|
return(
|
|
|
|
render status: 422,
|
|
|
|
json: {
|
|
|
|
user_count: user_count,
|
|
|
|
errors: [I18n.t("invalid_params", message: :update_existing_users)],
|
|
|
|
}
|
2023-01-09 20:20:10 +08:00
|
|
|
)
|
|
|
|
end
|
2021-07-15 22:23:57 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-03-24 20:50:44 +08:00
|
|
|
if group.update(group_attributes)
|
2021-07-28 19:04:04 +08:00
|
|
|
GroupActionLogger.new(current_user, group).log_change_group_settings
|
FEATURE: Improve group email settings UI (#13083)
This overhauls the user interface for the group email settings management, aiming to make it a lot easier to test the settings entered and confirm they are correct before proceeding. We do this by forcing the user to test the settings before they can be saved to the database. It also includes some quality of life improvements around setting up IMAP and SMTP for our first supported provider, GMail. This PR does not remove the old group email config, that will come in a subsequent PR. This is related to https://meta.discourse.org/t/imap-support-for-group-inboxes/160588 so read that if you would like more backstory.
### UI
Both site settings of `enable_imap` and `enable_smtp` must be true to test this. You must enable SMTP first to enable IMAP.
You can prefill the SMTP settings with GMail configuration. To proceed with saving these settings you must test them, which is handled by the EmailSettingsValidator.
If there is an issue with the configuration or credentials a meaningful error message should be shown.
IMAP settings must also be validated when IMAP is enabled, before saving.
When saving IMAP, we fetch the mailboxes for that account and populate them. This mailbox must be selected and saved for IMAP to work (the feature acts as though it is disabled until the mailbox is selected and saved):
### Database & Backend
This adds several columns to the Groups table. The purpose of this change is to make it much more explicit that SMTP/IMAP is enabled for a group, rather than relying on settings not being null. Also included is an UPDATE query to backfill these columns. These columns are automatically filled when updating the group.
For GMail, we now filter the mailboxes returned. This is so users cannot use a mailbox like Sent or Trash for syncing, which would generally be disastrous.
There is a new group endpoint for testing email settings. This may be useful in the future for other places in our UI, at which point it can be extracted to a more generic endpoint or module to be included.
2021-05-28 07:28:18 +08:00
|
|
|
group.record_email_setting_changes!(current_user)
|
|
|
|
group.expire_imap_mailbox_cache
|
2021-08-31 18:41:26 +08:00
|
|
|
if params[:update_existing_users] == "true"
|
|
|
|
update_existing_users(group.group_users, notification_level, categories, tags)
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
2022-01-04 08:14:33 +08:00
|
|
|
AdminDashboardData.clear_found_problem("group_#{group.id}_email_credentials")
|
2020-08-19 22:41:40 +08:00
|
|
|
|
2022-03-24 20:50:44 +08:00
|
|
|
# Redirect user to groups index page if they can no longer see the group
|
|
|
|
return redirect_with_client_support groups_path if !guardian.can_see?(group)
|
|
|
|
|
|
|
|
render json: success_json
|
2016-11-29 16:25:02 +08:00
|
|
|
else
|
|
|
|
render_json_error(group)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-02-07 23:44:03 +08:00
|
|
|
def posts
|
2014-02-19 05:43:02 +08:00
|
|
|
group = find_group(:group_id)
|
2019-08-14 21:30:04 +08:00
|
|
|
guardian.ensure_can_see_group_members!(group)
|
|
|
|
|
2017-11-01 04:47:47 +08:00
|
|
|
posts = group.posts_for(guardian, params.permit(:before_post_id, :category_id)).limit(20)
|
2014-02-07 23:44:03 +08:00
|
|
|
render_serialized posts.to_a, GroupPostSerializer
|
|
|
|
end
|
|
|
|
|
2016-03-19 00:19:45 +08:00
|
|
|
def posts_feed
|
|
|
|
group = find_group(:group_id)
|
2019-08-14 21:30:04 +08:00
|
|
|
guardian.ensure_can_see_group_members!(group)
|
|
|
|
|
2017-11-01 04:47:47 +08:00
|
|
|
@posts = group.posts_for(guardian, params.permit(:before_post_id, :category_id)).limit(50)
|
2016-03-19 00:19:45 +08:00
|
|
|
@title =
|
|
|
|
"#{SiteSetting.title} - #{I18n.t("rss_description.group_posts", group_name: group.name)}"
|
|
|
|
@link = Discourse.base_url
|
|
|
|
@description = I18n.t("rss_description.group_posts", group_name: group.name)
|
|
|
|
render "posts/latest", formats: [:rss]
|
|
|
|
end
|
|
|
|
|
2015-12-01 13:52:43 +08:00
|
|
|
def mentions
|
2017-12-08 05:16:53 +08:00
|
|
|
raise Discourse::NotFound unless SiteSetting.enable_mentions?
|
2015-12-01 13:52:43 +08:00
|
|
|
group = find_group(:group_id)
|
2017-11-01 04:47:47 +08:00
|
|
|
posts =
|
|
|
|
group.mentioned_posts_for(guardian, params.permit(:before_post_id, :category_id)).limit(20)
|
2015-12-01 13:52:43 +08:00
|
|
|
render_serialized posts.to_a, GroupPostSerializer
|
|
|
|
end
|
|
|
|
|
2016-03-19 00:19:45 +08:00
|
|
|
def mentions_feed
|
2017-12-08 05:16:53 +08:00
|
|
|
raise Discourse::NotFound unless SiteSetting.enable_mentions?
|
2016-03-19 00:19:45 +08:00
|
|
|
group = find_group(:group_id)
|
2017-11-01 04:47:47 +08:00
|
|
|
@posts =
|
|
|
|
group.mentioned_posts_for(guardian, params.permit(:before_post_id, :category_id)).limit(50)
|
2016-03-19 00:19:45 +08:00
|
|
|
@title =
|
|
|
|
"#{SiteSetting.title} - #{I18n.t("rss_description.group_mentions", group_name: group.name)}"
|
|
|
|
@link = Discourse.base_url
|
|
|
|
@description = I18n.t("rss_description.group_mentions", group_name: group.name)
|
|
|
|
render "posts/latest", formats: [:rss]
|
|
|
|
end
|
|
|
|
|
2014-02-07 02:06:19 +08:00
|
|
|
def members
|
2014-02-19 05:43:02 +08:00
|
|
|
group = find_group(:group_id)
|
2014-11-25 04:12:48 +08:00
|
|
|
|
2019-08-14 21:30:04 +08:00
|
|
|
guardian.ensure_can_see_group_members!(group)
|
|
|
|
|
2019-11-18 20:59:28 +08:00
|
|
|
limit = (params[:limit] || 50).to_i
|
2015-01-06 01:51:45 +08:00
|
|
|
offset = params[:offset].to_i
|
2018-06-29 08:14:50 +08:00
|
|
|
|
2019-11-18 20:59:28 +08:00
|
|
|
raise Discourse::InvalidParameters.new(:limit) if limit < 0 || limit > 1000
|
|
|
|
raise Discourse::InvalidParameters.new(:offset) if offset < 0
|
2018-06-29 08:14:50 +08:00
|
|
|
|
2020-05-15 10:10:59 +08:00
|
|
|
dir = (params[:asc] && params[:asc].present?) ? "ASC" : "DESC"
|
|
|
|
if params[:desc]
|
2021-11-12 22:52:59 +08:00
|
|
|
Discourse.deprecate(
|
|
|
|
":desc is deprecated please use :asc instead",
|
|
|
|
output_in_test: true,
|
|
|
|
drop_from: "2.9.0",
|
|
|
|
)
|
2020-05-15 10:10:59 +08:00
|
|
|
dir = (params[:desc] && params[:desc].present?) ? "DESC" : "ASC"
|
|
|
|
end
|
2022-02-09 17:43:58 +08:00
|
|
|
order = "NOT group_users.owner"
|
2016-12-07 17:28:43 +08:00
|
|
|
|
2019-03-27 19:30:59 +08:00
|
|
|
if params[:requesters]
|
|
|
|
guardian.ensure_can_edit!(group)
|
|
|
|
|
|
|
|
users = group.requesters
|
|
|
|
total = users.count
|
|
|
|
|
|
|
|
if (filter = params[:filter]).present?
|
|
|
|
filter = filter.split(",") if filter.include?(",")
|
|
|
|
|
|
|
|
if current_user&.admin
|
|
|
|
users = users.filter_by_username_or_email(filter)
|
|
|
|
else
|
|
|
|
users = users.filter_by_username(filter)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
users =
|
|
|
|
users
|
|
|
|
.select("users.*, group_requests.reason, group_requests.created_at requested_at")
|
|
|
|
.order(params[:order] == "requested_at" ? "group_requests.created_at #{dir}" : "")
|
|
|
|
.order(username_lower: dir)
|
|
|
|
.limit(limit)
|
|
|
|
.offset(offset)
|
2023-01-09 20:20:10 +08:00
|
|
|
|
2019-03-27 19:30:59 +08:00
|
|
|
return(
|
|
|
|
render json: {
|
|
|
|
members: serialize_data(users, GroupRequesterSerializer),
|
|
|
|
meta: {
|
|
|
|
total: total,
|
|
|
|
limit: limit,
|
|
|
|
offset: offset,
|
2023-01-09 20:20:10 +08:00
|
|
|
},
|
2019-03-27 19:30:59 +08:00
|
|
|
}
|
2023-01-09 20:20:10 +08:00
|
|
|
)
|
2019-03-27 19:30:59 +08:00
|
|
|
end
|
|
|
|
|
2016-12-07 17:28:43 +08:00
|
|
|
if params[:order] && %w[last_posted_at last_seen_at].include?(params[:order])
|
2016-12-22 14:55:24 +08:00
|
|
|
order = "#{params[:order]} #{dir} NULLS LAST"
|
2018-11-01 12:33:28 +08:00
|
|
|
elsif params[:order] == "added_at"
|
|
|
|
order = "group_users.created_at #{dir}"
|
2016-12-07 17:28:43 +08:00
|
|
|
end
|
|
|
|
|
2017-10-06 10:35:40 +08:00
|
|
|
users = group.users.human_users
|
|
|
|
total = users.count
|
2018-03-22 13:42:46 +08:00
|
|
|
|
2018-03-26 14:30:37 +08:00
|
|
|
if (filter = params[:filter]).present?
|
|
|
|
filter = filter.split(",") if filter.include?(",")
|
|
|
|
|
|
|
|
if current_user&.admin
|
|
|
|
users = users.filter_by_username_or_email(filter)
|
|
|
|
else
|
|
|
|
users = users.filter_by_username(filter)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-02-09 17:43:58 +08:00
|
|
|
users =
|
|
|
|
users
|
2018-12-06 19:18:52 +08:00
|
|
|
.includes(:primary_group)
|
2022-12-14 17:18:09 +08:00
|
|
|
.includes(:user_option)
|
|
|
|
.select("users.*, group_users.created_at as added_at")
|
2016-12-07 17:28:43 +08:00
|
|
|
.order(order)
|
2016-12-08 14:26:50 +08:00
|
|
|
.order(username_lower: dir)
|
2022-02-09 17:43:58 +08:00
|
|
|
|
|
|
|
members = users.limit(limit).offset(offset)
|
|
|
|
owners = users.where("group_users.owner")
|
2015-01-06 01:51:45 +08:00
|
|
|
|
|
|
|
render json: {
|
|
|
|
members: serialize_data(members, GroupUserSerializer),
|
2015-11-09 21:52:04 +08:00
|
|
|
owners: serialize_data(owners, GroupUserSerializer),
|
2015-01-06 01:51:45 +08:00
|
|
|
meta: {
|
|
|
|
total: total,
|
|
|
|
limit: limit,
|
|
|
|
offset: offset,
|
|
|
|
},
|
|
|
|
}
|
2014-02-07 02:06:19 +08:00
|
|
|
end
|
|
|
|
|
2015-01-09 07:35:52 +08:00
|
|
|
def add_members
|
2015-11-09 21:52:04 +08:00
|
|
|
group = Group.find(params[:id])
|
2021-07-22 15:11:23 +08:00
|
|
|
guardian.ensure_can_edit!(group)
|
2015-11-09 21:52:04 +08:00
|
|
|
|
2021-07-22 15:11:23 +08:00
|
|
|
users = users_from_params.to_a
|
2020-08-04 23:02:01 +08:00
|
|
|
emails = []
|
|
|
|
if params[:emails]
|
|
|
|
params[:emails]
|
|
|
|
.split(",")
|
|
|
|
.each do |email|
|
|
|
|
existing_user = User.find_by_email(email)
|
|
|
|
existing_user.present? ? users.push(existing_user) : emails.push(email)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-06-02 21:28:21 +08:00
|
|
|
guardian.ensure_can_invite_to_forum!([group]) if emails.present?
|
|
|
|
|
2020-08-04 23:02:01 +08:00
|
|
|
if users.empty? && emails.empty?
|
2021-02-09 00:57:59 +08:00
|
|
|
raise Discourse::InvalidParameters.new(I18n.t("groups.errors.usernames_or_emails_required"))
|
2021-02-04 01:13:00 +08:00
|
|
|
end
|
|
|
|
|
2020-08-25 06:55:21 +08:00
|
|
|
if users.length > ADD_MEMBERS_LIMIT
|
|
|
|
return(
|
2021-02-02 17:50:04 +08:00
|
|
|
render_json_error(I18n.t("groups.errors.adding_too_many_users", count: ADD_MEMBERS_LIMIT))
|
2020-08-25 06:55:21 +08:00
|
|
|
)
|
|
|
|
end
|
2021-02-04 01:13:00 +08:00
|
|
|
|
2020-08-21 11:38:09 +08:00
|
|
|
usernames_already_in_group = group.users.where(id: users.map(&:id)).pluck(:username)
|
2021-02-04 18:06:08 +08:00
|
|
|
if usernames_already_in_group.present? && usernames_already_in_group.length == users.length &&
|
|
|
|
emails.blank?
|
2018-03-26 14:30:37 +08:00
|
|
|
render_json_error(
|
|
|
|
I18n.t(
|
|
|
|
"groups.errors.member_already_exist",
|
2020-08-21 11:38:09 +08:00
|
|
|
username: usernames_already_in_group.sort.join(", "),
|
|
|
|
count: usernames_already_in_group.size,
|
2023-01-09 20:20:10 +08:00
|
|
|
),
|
2018-03-26 14:30:37 +08:00
|
|
|
)
|
|
|
|
else
|
2021-07-22 15:11:23 +08:00
|
|
|
notify = params[:notify_users]&.to_s == "true"
|
2020-08-04 23:02:01 +08:00
|
|
|
uniq_users = users.uniq
|
2021-07-22 15:11:23 +08:00
|
|
|
uniq_users.each { |user| add_user_to_group(group, user, notify) }
|
2015-01-09 07:35:52 +08:00
|
|
|
|
2020-08-04 23:02:01 +08:00
|
|
|
emails.each do |email|
|
2021-05-21 16:34:17 +08:00
|
|
|
begin
|
|
|
|
Invite.generate(current_user, email: email, group_ids: [group.id])
|
|
|
|
rescue RateLimiter::LimitExceeded => e
|
|
|
|
return(
|
|
|
|
render_json_error(
|
|
|
|
I18n.t(
|
|
|
|
"invite.rate_limit",
|
|
|
|
count: SiteSetting.max_invites_per_day,
|
|
|
|
time_left: e.time_left,
|
2023-01-09 20:20:10 +08:00
|
|
|
),
|
2021-05-21 16:34:17 +08:00
|
|
|
)
|
2023-01-09 20:20:10 +08:00
|
|
|
)
|
2021-05-21 16:34:17 +08:00
|
|
|
end
|
2020-08-04 23:02:01 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
render json: success_json.merge!(usernames: uniq_users.map(&:username), emails: emails)
|
2015-11-09 21:52:04 +08:00
|
|
|
end
|
2015-01-09 07:35:52 +08:00
|
|
|
end
|
|
|
|
|
2023-01-11 16:43:18 +08:00
|
|
|
def add_owners
|
|
|
|
group = Group.find_by(id: params.require(:id))
|
|
|
|
raise Discourse::NotFound unless group
|
|
|
|
|
|
|
|
return can_not_modify_automatic if group.automatic
|
|
|
|
guardian.ensure_can_edit_group!(group)
|
|
|
|
|
|
|
|
users = users_from_params
|
|
|
|
group_action_logger = GroupActionLogger.new(current_user, group)
|
|
|
|
|
|
|
|
users.each do |user|
|
|
|
|
if !group.users.include?(user)
|
|
|
|
group.add(user)
|
|
|
|
group_action_logger.log_add_user_to_group(user)
|
|
|
|
end
|
|
|
|
group.group_users.where(user_id: user.id).update_all(owner: true)
|
|
|
|
group_action_logger.log_make_user_group_owner(user)
|
|
|
|
|
|
|
|
group.notify_added_to_group(user, owner: true) if params[:notify_users].to_s == "true"
|
|
|
|
end
|
|
|
|
|
|
|
|
group.restore_user_count!
|
|
|
|
|
|
|
|
render json: success_json.merge!(usernames: users.pluck(:username))
|
|
|
|
end
|
|
|
|
|
2021-07-22 15:11:23 +08:00
|
|
|
def join
|
|
|
|
ensure_logged_in
|
|
|
|
unless current_user.staff?
|
|
|
|
RateLimiter.new(current_user, "public_group_membership", 3, 1.minute).performed!
|
|
|
|
end
|
|
|
|
|
|
|
|
group = Group.find(params[:id])
|
2021-07-22 22:48:26 +08:00
|
|
|
raise Discourse::NotFound unless group
|
2021-07-22 15:11:23 +08:00
|
|
|
raise Discourse::InvalidAccess unless group.public_admission
|
|
|
|
|
|
|
|
return if group.users.exists?(id: current_user.id)
|
|
|
|
add_user_to_group(group, current_user)
|
|
|
|
end
|
|
|
|
|
2019-03-27 19:30:59 +08:00
|
|
|
def handle_membership_request
|
|
|
|
group = Group.find_by(id: params[:id])
|
|
|
|
raise Discourse::InvalidParameters.new(:id) if group.blank?
|
|
|
|
guardian.ensure_can_edit!(group)
|
|
|
|
|
2020-05-26 21:28:03 +08:00
|
|
|
user = User.find_by(id: params[:user_id])
|
|
|
|
raise Discourse::InvalidParameters.new(:user_id) if user.blank?
|
2019-03-27 19:30:59 +08:00
|
|
|
|
2020-05-26 21:28:03 +08:00
|
|
|
ActiveRecord::Base.transaction do
|
2019-03-27 19:30:59 +08:00
|
|
|
if params[:accept]
|
2020-05-26 21:28:03 +08:00
|
|
|
group.add(user)
|
2019-03-27 19:30:59 +08:00
|
|
|
GroupActionLogger.new(current_user, group).log_add_user_to_group(user)
|
|
|
|
end
|
|
|
|
|
|
|
|
GroupRequest.where(group_id: group.id, user_id: user.id).delete_all
|
|
|
|
end
|
|
|
|
|
2020-05-26 21:28:03 +08:00
|
|
|
if params[:accept]
|
|
|
|
PostCreator.new(
|
|
|
|
current_user,
|
|
|
|
title: I18n.t("groups.request_accepted_pm.title", group_name: group.name),
|
|
|
|
raw: I18n.t("groups.request_accepted_pm.body", group_name: group.name),
|
|
|
|
archetype: Archetype.private_message,
|
|
|
|
target_usernames: user.username,
|
|
|
|
skip_validations: true,
|
|
|
|
).create!
|
|
|
|
end
|
|
|
|
|
2019-03-27 19:30:59 +08:00
|
|
|
render json: success_json
|
|
|
|
end
|
|
|
|
|
2016-11-25 16:45:15 +08:00
|
|
|
def mentionable
|
2018-06-06 09:42:09 +08:00
|
|
|
group = find_group(:group_id, ensure_can_see: false)
|
2016-11-25 16:45:15 +08:00
|
|
|
|
|
|
|
if group
|
|
|
|
render json: { mentionable: Group.mentionable(current_user).where(id: group.id).present? }
|
|
|
|
else
|
|
|
|
raise Discourse::InvalidAccess.new
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-08-29 00:32:08 +08:00
|
|
|
def messageable
|
2018-06-06 09:42:09 +08:00
|
|
|
group = find_group(:group_id, ensure_can_see: false)
|
2017-08-29 00:32:08 +08:00
|
|
|
|
|
|
|
if group
|
2021-08-30 13:08:33 +08:00
|
|
|
render json: { messageable: guardian.can_send_private_message?(group) }
|
2017-08-29 00:32:08 +08:00
|
|
|
else
|
|
|
|
raise Discourse::InvalidAccess.new
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-08-01 11:08:45 +08:00
|
|
|
def check_name
|
|
|
|
group_name = params.require(:group_name)
|
|
|
|
checker = UsernameCheckerService.new(allow_reserved_username: true)
|
|
|
|
render json: checker.check_username(group_name, nil)
|
|
|
|
end
|
|
|
|
|
2015-01-09 07:35:52 +08:00
|
|
|
def remove_member
|
2018-04-06 17:11:00 +08:00
|
|
|
group = Group.find_by(id: params[:id])
|
|
|
|
raise Discourse::NotFound unless group
|
2021-07-23 00:14:18 +08:00
|
|
|
guardian.ensure_can_edit!(group)
|
2016-12-07 12:06:56 +08:00
|
|
|
|
2018-10-12 05:27:41 +08:00
|
|
|
# Maintain backwards compatibility
|
|
|
|
params[:usernames] = params[:username] if params[:username].present?
|
|
|
|
params[:user_emails] = params[:user_email] if params[:user_email].present?
|
2015-11-09 21:52:04 +08:00
|
|
|
|
2018-10-12 05:27:41 +08:00
|
|
|
users = users_from_params
|
2020-08-04 23:02:01 +08:00
|
|
|
if users.empty?
|
|
|
|
raise Discourse::InvalidParameters.new("user_ids or usernames or user_emails must be present")
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
2016-12-07 12:06:56 +08:00
|
|
|
|
2020-07-23 04:27:43 +08:00
|
|
|
removed_users = []
|
|
|
|
skipped_users = []
|
|
|
|
|
2018-10-12 05:27:41 +08:00
|
|
|
users.each do |user|
|
2020-03-11 05:25:00 +08:00
|
|
|
if group.remove(user)
|
2020-07-23 04:27:43 +08:00
|
|
|
removed_users << user.username
|
2020-03-11 05:25:00 +08:00
|
|
|
GroupActionLogger.new(current_user, group).log_remove_user_from_group(user)
|
|
|
|
else
|
2020-07-23 04:27:43 +08:00
|
|
|
if group.users.exclude? user
|
|
|
|
skipped_users << user.username
|
|
|
|
else
|
|
|
|
raise Discourse::InvalidParameters
|
|
|
|
end
|
2020-03-11 05:25:00 +08:00
|
|
|
end
|
2015-01-09 07:35:52 +08:00
|
|
|
end
|
2018-10-12 05:27:41 +08:00
|
|
|
|
|
|
|
render json: success_json.merge!(usernames: removed_users, skipped_usernames: skipped_users)
|
2017-06-13 16:10:14 +08:00
|
|
|
end
|
|
|
|
|
2021-07-23 00:14:18 +08:00
|
|
|
def leave
|
|
|
|
ensure_logged_in
|
|
|
|
unless current_user.staff?
|
|
|
|
RateLimiter.new(current_user, "public_group_membership", 3, 1.minute).performed!
|
|
|
|
end
|
|
|
|
|
|
|
|
group = Group.find_by(id: params[:id])
|
|
|
|
raise Discourse::NotFound unless group
|
|
|
|
raise Discourse::InvalidAccess unless group.public_exit
|
|
|
|
|
|
|
|
if group.remove(current_user)
|
|
|
|
GroupActionLogger.new(current_user, group).log_remove_user_from_group(current_user)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-05-20 13:28:36 +08:00
|
|
|
MAX_NOTIFIED_OWNERS ||= 20
|
|
|
|
|
2017-06-13 16:10:14 +08:00
|
|
|
def request_membership
|
2020-08-25 07:29:18 +08:00
|
|
|
params.require(:reason)
|
2017-08-08 17:53:02 +08:00
|
|
|
|
2017-06-13 16:10:14 +08:00
|
|
|
group = find_group(:id)
|
2019-04-23 10:51:30 +08:00
|
|
|
|
|
|
|
begin
|
2020-08-25 07:29:18 +08:00
|
|
|
GroupRequest.create!(group: group, user: current_user, reason: params[:reason])
|
2021-05-20 13:28:36 +08:00
|
|
|
rescue ActiveRecord::RecordNotUnique
|
2019-04-23 10:51:30 +08:00
|
|
|
return(
|
|
|
|
render json: failed_json.merge(error: I18n.t("groups.errors.already_requested_membership")),
|
|
|
|
status: 409
|
2023-01-09 20:20:10 +08:00
|
|
|
)
|
2019-04-23 10:51:30 +08:00
|
|
|
end
|
2017-06-15 11:36:09 +08:00
|
|
|
|
|
|
|
usernames = [current_user.username].concat(
|
|
|
|
group
|
|
|
|
.users
|
|
|
|
.where("group_users.owner")
|
|
|
|
.order("users.last_seen_at DESC")
|
2021-05-20 13:28:36 +08:00
|
|
|
.limit(MAX_NOTIFIED_OWNERS)
|
2017-06-15 11:36:09 +08:00
|
|
|
.pluck("users.username"),
|
|
|
|
)
|
2017-06-13 16:10:14 +08:00
|
|
|
|
|
|
|
post =
|
|
|
|
PostCreator.new(
|
|
|
|
current_user,
|
2019-04-23 10:51:30 +08:00
|
|
|
title: I18n.t("groups.request_membership_pm.title", group_name: group.name),
|
2019-08-06 18:28:22 +08:00
|
|
|
raw: params[:reason],
|
2017-06-13 16:10:14 +08:00
|
|
|
archetype: Archetype.private_message,
|
2017-06-15 11:36:09 +08:00
|
|
|
target_usernames: usernames.join(","),
|
2020-03-24 17:12:52 +08:00
|
|
|
topic_opts: {
|
|
|
|
custom_fields: {
|
|
|
|
requested_group_id: group.id,
|
|
|
|
},
|
|
|
|
},
|
2017-06-13 16:10:14 +08:00
|
|
|
skip_validations: true,
|
|
|
|
).create!
|
|
|
|
|
|
|
|
render json: success_json.merge(relative_url: post.topic.relative_url)
|
2015-01-09 07:35:52 +08:00
|
|
|
end
|
|
|
|
|
2015-12-15 06:17:09 +08:00
|
|
|
def set_notifications
|
|
|
|
group = find_group(:id)
|
|
|
|
notification_level = params.require(:notification_level)
|
|
|
|
|
2017-04-21 03:47:25 +08:00
|
|
|
user_id = current_user.id
|
|
|
|
user_id = params[:user_id] || user_id if guardian.is_staff?
|
|
|
|
|
2015-12-15 06:17:09 +08:00
|
|
|
GroupUser
|
|
|
|
.where(group_id: group.id)
|
2017-04-21 03:47:25 +08:00
|
|
|
.where(user_id: user_id)
|
2015-12-15 06:17:09 +08:00
|
|
|
.update_all(notification_level: notification_level)
|
|
|
|
|
|
|
|
render json: success_json
|
|
|
|
end
|
|
|
|
|
2016-12-11 23:36:15 +08:00
|
|
|
def histories
|
|
|
|
group = find_group(:group_id)
|
2020-08-19 22:41:40 +08:00
|
|
|
guardian.ensure_can_edit!(group) unless guardian.can_admin_group?(group)
|
2016-12-11 23:36:15 +08:00
|
|
|
|
|
|
|
page_size = 25
|
|
|
|
offset = (params[:offset] && params[:offset].to_i) || 0
|
|
|
|
|
|
|
|
group_histories =
|
|
|
|
GroupHistory.with_filters(group, params[:filters]).limit(page_size).offset(offset * page_size)
|
|
|
|
|
|
|
|
render_json_dump(
|
|
|
|
logs: serialize_data(group_histories, BasicGroupHistorySerializer),
|
|
|
|
all_loaded: group_histories.count < page_size,
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2017-07-21 14:12:24 +08:00
|
|
|
def search
|
|
|
|
groups =
|
|
|
|
Group
|
|
|
|
.visible_groups(current_user)
|
|
|
|
.where("groups.id <> ?", Group::AUTO_GROUPS[:everyone])
|
2020-10-05 16:22:55 +08:00
|
|
|
.includes(:flair_upload)
|
2017-07-21 14:12:24 +08:00
|
|
|
.order(:name)
|
|
|
|
|
2020-01-15 18:21:58 +08:00
|
|
|
if (term = params[:term]).present?
|
2017-07-21 14:12:24 +08:00
|
|
|
groups = groups.where("name ILIKE :term OR full_name ILIKE :term", term: "%#{term}%")
|
|
|
|
end
|
|
|
|
|
|
|
|
groups = groups.where(automatic: false) if params[:ignore_automatic].to_s == "true"
|
|
|
|
|
2017-08-08 21:45:27 +08:00
|
|
|
if Group.preloaded_custom_field_names.present?
|
|
|
|
Group.preload_custom_fields(groups, Group.preloaded_custom_field_names)
|
|
|
|
end
|
|
|
|
|
2017-07-21 14:12:24 +08:00
|
|
|
render_serialized(groups, BasicGroupSerializer)
|
|
|
|
end
|
|
|
|
|
2020-08-10 22:49:05 +08:00
|
|
|
def permissions
|
|
|
|
group = find_group(:id)
|
|
|
|
category_groups =
|
|
|
|
group.category_groups.select do |category_group|
|
|
|
|
guardian.can_see_category?(category_group.category)
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
2020-08-10 22:49:05 +08:00
|
|
|
render_serialized(
|
|
|
|
category_groups.sort_by { |category_group| category_group.category.name },
|
|
|
|
CategoryGroupSerializer,
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
FEATURE: Improve group email settings UI (#13083)
This overhauls the user interface for the group email settings management, aiming to make it a lot easier to test the settings entered and confirm they are correct before proceeding. We do this by forcing the user to test the settings before they can be saved to the database. It also includes some quality of life improvements around setting up IMAP and SMTP for our first supported provider, GMail. This PR does not remove the old group email config, that will come in a subsequent PR. This is related to https://meta.discourse.org/t/imap-support-for-group-inboxes/160588 so read that if you would like more backstory.
### UI
Both site settings of `enable_imap` and `enable_smtp` must be true to test this. You must enable SMTP first to enable IMAP.
You can prefill the SMTP settings with GMail configuration. To proceed with saving these settings you must test them, which is handled by the EmailSettingsValidator.
If there is an issue with the configuration or credentials a meaningful error message should be shown.
IMAP settings must also be validated when IMAP is enabled, before saving.
When saving IMAP, we fetch the mailboxes for that account and populate them. This mailbox must be selected and saved for IMAP to work (the feature acts as though it is disabled until the mailbox is selected and saved):
### Database & Backend
This adds several columns to the Groups table. The purpose of this change is to make it much more explicit that SMTP/IMAP is enabled for a group, rather than relying on settings not being null. Also included is an UPDATE query to backfill these columns. These columns are automatically filled when updating the group.
For GMail, we now filter the mailboxes returned. This is so users cannot use a mailbox like Sent or Trash for syncing, which would generally be disastrous.
There is a new group endpoint for testing email settings. This may be useful in the future for other places in our UI, at which point it can be extracted to a more generic endpoint or module to be included.
2021-05-28 07:28:18 +08:00
|
|
|
def test_email_settings
|
|
|
|
params.require(:group_id)
|
|
|
|
params.require(:protocol)
|
|
|
|
params.require(:port)
|
|
|
|
params.require(:host)
|
|
|
|
params.require(:username)
|
|
|
|
params.require(:password)
|
|
|
|
params.require(:ssl)
|
|
|
|
|
|
|
|
group = Group.find(params[:group_id])
|
|
|
|
guardian.ensure_can_edit!(group)
|
|
|
|
|
|
|
|
RateLimiter.new(current_user, "group_test_email_settings", 5, 1.minute).performed!
|
|
|
|
|
|
|
|
settings = params.except(:group_id, :protocol)
|
|
|
|
enable_tls = settings[:ssl] == "true"
|
|
|
|
email_host = params[:host]
|
|
|
|
|
|
|
|
if !%w[smtp imap].include?(params[:protocol])
|
|
|
|
raise Discourse::InvalidParameters.new("Valid protocols to test are smtp and imap")
|
|
|
|
end
|
|
|
|
|
|
|
|
hijack do
|
|
|
|
begin
|
|
|
|
case params[:protocol]
|
|
|
|
when "smtp"
|
|
|
|
enable_starttls_auto = false
|
|
|
|
settings.delete(:ssl)
|
|
|
|
|
|
|
|
final_settings =
|
|
|
|
settings.merge(
|
|
|
|
enable_tls: enable_tls,
|
|
|
|
enable_starttls_auto: enable_starttls_auto,
|
|
|
|
).permit(:host, :port, :username, :password, :enable_tls, :enable_starttls_auto, :debug)
|
|
|
|
EmailSettingsValidator.validate_as_user(
|
|
|
|
current_user,
|
|
|
|
"smtp",
|
|
|
|
**final_settings.to_h.symbolize_keys,
|
|
|
|
)
|
|
|
|
when "imap"
|
|
|
|
final_settings =
|
|
|
|
settings.merge(ssl: enable_tls).permit(:host, :port, :username, :password, :ssl, :debug)
|
|
|
|
EmailSettingsValidator.validate_as_user(
|
|
|
|
current_user,
|
|
|
|
"imap",
|
|
|
|
**final_settings.to_h.symbolize_keys,
|
|
|
|
)
|
|
|
|
end
|
2022-08-28 00:06:56 +08:00
|
|
|
|
|
|
|
render json: success_json
|
FEATURE: Improve group email settings UI (#13083)
This overhauls the user interface for the group email settings management, aiming to make it a lot easier to test the settings entered and confirm they are correct before proceeding. We do this by forcing the user to test the settings before they can be saved to the database. It also includes some quality of life improvements around setting up IMAP and SMTP for our first supported provider, GMail. This PR does not remove the old group email config, that will come in a subsequent PR. This is related to https://meta.discourse.org/t/imap-support-for-group-inboxes/160588 so read that if you would like more backstory.
### UI
Both site settings of `enable_imap` and `enable_smtp` must be true to test this. You must enable SMTP first to enable IMAP.
You can prefill the SMTP settings with GMail configuration. To proceed with saving these settings you must test them, which is handled by the EmailSettingsValidator.
If there is an issue with the configuration or credentials a meaningful error message should be shown.
IMAP settings must also be validated when IMAP is enabled, before saving.
When saving IMAP, we fetch the mailboxes for that account and populate them. This mailbox must be selected and saved for IMAP to work (the feature acts as though it is disabled until the mailbox is selected and saved):
### Database & Backend
This adds several columns to the Groups table. The purpose of this change is to make it much more explicit that SMTP/IMAP is enabled for a group, rather than relying on settings not being null. Also included is an UPDATE query to backfill these columns. These columns are automatically filled when updating the group.
For GMail, we now filter the mailboxes returned. This is so users cannot use a mailbox like Sent or Trash for syncing, which would generally be disastrous.
There is a new group endpoint for testing email settings. This may be useful in the future for other places in our UI, at which point it can be extracted to a more generic endpoint or module to be included.
2021-05-28 07:28:18 +08:00
|
|
|
rescue *EmailSettingsExceptionHandler::EXPECTED_EXCEPTIONS, StandardError => err
|
|
|
|
render_json_error(EmailSettingsExceptionHandler.friendly_exception_message(err, email_host))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-01-11 16:43:18 +08:00
|
|
|
protected
|
|
|
|
|
|
|
|
def can_not_modify_automatic
|
|
|
|
render_json_error(I18n.t("groups.errors.can_not_modify_automatic"))
|
|
|
|
end
|
|
|
|
|
2014-02-19 05:43:02 +08:00
|
|
|
private
|
|
|
|
|
2021-07-22 15:11:23 +08:00
|
|
|
def add_user_to_group(group, user, notify = false)
|
|
|
|
group.add(user)
|
|
|
|
GroupActionLogger.new(current_user, group).log_add_user_to_group(user)
|
|
|
|
group.notify_added_to_group(user) if notify
|
|
|
|
rescue ActiveRecord::RecordNotUnique
|
|
|
|
# Under concurrency, we might attempt to insert two records quickly and hit a DB
|
|
|
|
# constraint. In this case we can safely ignore the error and act as if the user
|
|
|
|
# was added to the group.
|
|
|
|
end
|
|
|
|
|
2018-04-05 13:53:00 +08:00
|
|
|
def group_params(automatic: false)
|
2022-03-24 20:50:44 +08:00
|
|
|
attributes = %i[
|
|
|
|
bio_raw
|
|
|
|
default_notification_level
|
|
|
|
messageable_level
|
|
|
|
mentionable_level
|
|
|
|
flair_bg_color
|
|
|
|
flair_color
|
|
|
|
flair_icon
|
|
|
|
flair_upload_id
|
2023-01-09 20:20:10 +08:00
|
|
|
]
|
2018-04-05 13:53:00 +08:00
|
|
|
|
2022-03-24 20:50:44 +08:00
|
|
|
if automatic
|
|
|
|
attributes.push(:visibility_level)
|
|
|
|
else
|
|
|
|
attributes.push(
|
|
|
|
:title,
|
|
|
|
:allow_membership_requests,
|
|
|
|
:full_name,
|
|
|
|
:public_exit,
|
|
|
|
:public_admission,
|
|
|
|
:membership_request_template,
|
|
|
|
)
|
|
|
|
end
|
2018-04-05 13:53:00 +08:00
|
|
|
|
2022-03-24 20:50:44 +08:00
|
|
|
if !automatic && current_user.staff?
|
|
|
|
attributes.push(
|
|
|
|
:incoming_email,
|
|
|
|
:smtp_server,
|
|
|
|
:smtp_port,
|
|
|
|
:smtp_ssl,
|
|
|
|
:smtp_enabled,
|
|
|
|
:smtp_updated_by,
|
|
|
|
:smtp_updated_at,
|
|
|
|
:imap_server,
|
|
|
|
:imap_port,
|
|
|
|
:imap_ssl,
|
|
|
|
:imap_mailbox_name,
|
|
|
|
:imap_enabled,
|
|
|
|
:imap_updated_by,
|
|
|
|
:imap_updated_at,
|
|
|
|
:email_username,
|
|
|
|
:email_password,
|
|
|
|
:email_from_alias,
|
|
|
|
:primary_group,
|
|
|
|
:visibility_level,
|
|
|
|
:members_visibility_level,
|
|
|
|
:name,
|
|
|
|
:grant_trust_level,
|
|
|
|
:automatic_membership_email_domains,
|
|
|
|
:publish_read_state,
|
|
|
|
:allow_unknown_sender_topic_replies,
|
|
|
|
)
|
|
|
|
|
|
|
|
custom_fields = DiscoursePluginRegistry.editable_group_custom_fields
|
|
|
|
attributes << { custom_fields: custom_fields } if custom_fields.present?
|
|
|
|
end
|
2018-03-27 12:18:03 +08:00
|
|
|
|
2020-08-07 00:27:27 +08:00
|
|
|
if !automatic || current_user.admin
|
2020-08-14 05:20:23 +08:00
|
|
|
%i[muted regular tracking watching watching_first_post].each do |level|
|
2022-03-24 20:50:44 +08:00
|
|
|
attributes << { "#{level}_category_ids" => [] }
|
|
|
|
attributes << { "#{level}_tags" => [] }
|
2020-08-07 00:27:27 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-03-24 20:50:44 +08:00
|
|
|
attributes << { associated_group_ids: [] } if guardian.can_associate_groups?
|
|
|
|
attributes.concat(DiscoursePluginRegistry.group_params)
|
2021-09-06 08:18:51 +08:00
|
|
|
|
2022-03-24 20:50:44 +08:00
|
|
|
params.require(:group).permit(*attributes)
|
2016-11-29 16:25:02 +08:00
|
|
|
end
|
|
|
|
|
2018-06-05 18:56:51 +08:00
|
|
|
def find_group(param_name, ensure_can_see: true)
|
2016-11-29 16:25:02 +08:00
|
|
|
name = params.require(param_name)
|
2020-01-15 18:21:58 +08:00
|
|
|
group = Group.find_by("LOWER(name) = ?", name.downcase)
|
2020-09-08 10:52:29 +08:00
|
|
|
raise Discourse::NotFound if ensure_can_see && !guardian.can_see_group?(group)
|
2016-11-29 16:25:02 +08:00
|
|
|
group
|
|
|
|
end
|
2018-10-12 05:27:41 +08:00
|
|
|
|
|
|
|
def users_from_params
|
|
|
|
if params[:usernames].present?
|
2018-10-19 03:17:24 +08:00
|
|
|
users = User.where(username_lower: params[:usernames].split(",").map(&:downcase))
|
2018-10-12 05:27:41 +08:00
|
|
|
raise Discourse::InvalidParameters.new(:usernames) if users.blank?
|
2019-01-25 08:28:48 +08:00
|
|
|
elsif params[:user_id].present?
|
|
|
|
users = User.where(id: params[:user_id].to_i)
|
|
|
|
raise Discourse::InvalidParameters.new(:user_id) if users.blank?
|
2018-10-12 05:27:41 +08:00
|
|
|
elsif params[:user_ids].present?
|
2019-01-25 08:28:48 +08:00
|
|
|
users = User.where(id: params[:user_ids].to_s.split(","))
|
2018-10-12 05:27:41 +08:00
|
|
|
raise Discourse::InvalidParameters.new(:user_ids) if users.blank?
|
|
|
|
elsif params[:user_emails].present?
|
|
|
|
users = User.with_email(params[:user_emails].split(","))
|
|
|
|
raise Discourse::InvalidParameters.new(:user_emails) if users.blank?
|
|
|
|
else
|
2020-08-04 23:02:01 +08:00
|
|
|
users = []
|
2018-10-12 05:27:41 +08:00
|
|
|
end
|
|
|
|
users
|
|
|
|
end
|
FEATURE: Improve group email settings UI (#13083)
This overhauls the user interface for the group email settings management, aiming to make it a lot easier to test the settings entered and confirm they are correct before proceeding. We do this by forcing the user to test the settings before they can be saved to the database. It also includes some quality of life improvements around setting up IMAP and SMTP for our first supported provider, GMail. This PR does not remove the old group email config, that will come in a subsequent PR. This is related to https://meta.discourse.org/t/imap-support-for-group-inboxes/160588 so read that if you would like more backstory.
### UI
Both site settings of `enable_imap` and `enable_smtp` must be true to test this. You must enable SMTP first to enable IMAP.
You can prefill the SMTP settings with GMail configuration. To proceed with saving these settings you must test them, which is handled by the EmailSettingsValidator.
If there is an issue with the configuration or credentials a meaningful error message should be shown.
IMAP settings must also be validated when IMAP is enabled, before saving.
When saving IMAP, we fetch the mailboxes for that account and populate them. This mailbox must be selected and saved for IMAP to work (the feature acts as though it is disabled until the mailbox is selected and saved):
### Database & Backend
This adds several columns to the Groups table. The purpose of this change is to make it much more explicit that SMTP/IMAP is enabled for a group, rather than relying on settings not being null. Also included is an UPDATE query to backfill these columns. These columns are automatically filled when updating the group.
For GMail, we now filter the mailboxes returned. This is so users cannot use a mailbox like Sent or Trash for syncing, which would generally be disastrous.
There is a new group endpoint for testing email settings. This may be useful in the future for other places in our UI, at which point it can be extracted to a more generic endpoint or module to be included.
2021-05-28 07:28:18 +08:00
|
|
|
|
2022-03-24 20:50:44 +08:00
|
|
|
def reset_group_email_settings_if_disabled!(group, attributes)
|
|
|
|
should_clear_imap = group.imap_enabled && attributes[:imap_enabled] == "false"
|
|
|
|
should_clear_smtp = group.smtp_enabled && attributes[:smtp_enabled] == "false"
|
FEATURE: Improve group email settings UI (#13083)
This overhauls the user interface for the group email settings management, aiming to make it a lot easier to test the settings entered and confirm they are correct before proceeding. We do this by forcing the user to test the settings before they can be saved to the database. It also includes some quality of life improvements around setting up IMAP and SMTP for our first supported provider, GMail. This PR does not remove the old group email config, that will come in a subsequent PR. This is related to https://meta.discourse.org/t/imap-support-for-group-inboxes/160588 so read that if you would like more backstory.
### UI
Both site settings of `enable_imap` and `enable_smtp` must be true to test this. You must enable SMTP first to enable IMAP.
You can prefill the SMTP settings with GMail configuration. To proceed with saving these settings you must test them, which is handled by the EmailSettingsValidator.
If there is an issue with the configuration or credentials a meaningful error message should be shown.
IMAP settings must also be validated when IMAP is enabled, before saving.
When saving IMAP, we fetch the mailboxes for that account and populate them. This mailbox must be selected and saved for IMAP to work (the feature acts as though it is disabled until the mailbox is selected and saved):
### Database & Backend
This adds several columns to the Groups table. The purpose of this change is to make it much more explicit that SMTP/IMAP is enabled for a group, rather than relying on settings not being null. Also included is an UPDATE query to backfill these columns. These columns are automatically filled when updating the group.
For GMail, we now filter the mailboxes returned. This is so users cannot use a mailbox like Sent or Trash for syncing, which would generally be disastrous.
There is a new group endpoint for testing email settings. This may be useful in the future for other places in our UI, at which point it can be extracted to a more generic endpoint or module to be included.
2021-05-28 07:28:18 +08:00
|
|
|
|
|
|
|
if should_clear_imap || should_clear_smtp
|
2022-03-24 20:50:44 +08:00
|
|
|
attributes[:imap_server] = nil
|
|
|
|
attributes[:imap_ssl] = false
|
|
|
|
attributes[:imap_port] = nil
|
|
|
|
attributes[:imap_mailbox_name] = ""
|
FEATURE: Improve group email settings UI (#13083)
This overhauls the user interface for the group email settings management, aiming to make it a lot easier to test the settings entered and confirm they are correct before proceeding. We do this by forcing the user to test the settings before they can be saved to the database. It also includes some quality of life improvements around setting up IMAP and SMTP for our first supported provider, GMail. This PR does not remove the old group email config, that will come in a subsequent PR. This is related to https://meta.discourse.org/t/imap-support-for-group-inboxes/160588 so read that if you would like more backstory.
### UI
Both site settings of `enable_imap` and `enable_smtp` must be true to test this. You must enable SMTP first to enable IMAP.
You can prefill the SMTP settings with GMail configuration. To proceed with saving these settings you must test them, which is handled by the EmailSettingsValidator.
If there is an issue with the configuration or credentials a meaningful error message should be shown.
IMAP settings must also be validated when IMAP is enabled, before saving.
When saving IMAP, we fetch the mailboxes for that account and populate them. This mailbox must be selected and saved for IMAP to work (the feature acts as though it is disabled until the mailbox is selected and saved):
### Database & Backend
This adds several columns to the Groups table. The purpose of this change is to make it much more explicit that SMTP/IMAP is enabled for a group, rather than relying on settings not being null. Also included is an UPDATE query to backfill these columns. These columns are automatically filled when updating the group.
For GMail, we now filter the mailboxes returned. This is so users cannot use a mailbox like Sent or Trash for syncing, which would generally be disastrous.
There is a new group endpoint for testing email settings. This may be useful in the future for other places in our UI, at which point it can be extracted to a more generic endpoint or module to be included.
2021-05-28 07:28:18 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
if should_clear_smtp
|
2022-03-24 20:50:44 +08:00
|
|
|
attributes[:smtp_server] = nil
|
|
|
|
attributes[:smtp_ssl] = false
|
|
|
|
attributes[:smtp_port] = nil
|
|
|
|
attributes[:email_username] = nil
|
|
|
|
attributes[:email_password] = nil
|
FEATURE: Improve group email settings UI (#13083)
This overhauls the user interface for the group email settings management, aiming to make it a lot easier to test the settings entered and confirm they are correct before proceeding. We do this by forcing the user to test the settings before they can be saved to the database. It also includes some quality of life improvements around setting up IMAP and SMTP for our first supported provider, GMail. This PR does not remove the old group email config, that will come in a subsequent PR. This is related to https://meta.discourse.org/t/imap-support-for-group-inboxes/160588 so read that if you would like more backstory.
### UI
Both site settings of `enable_imap` and `enable_smtp` must be true to test this. You must enable SMTP first to enable IMAP.
You can prefill the SMTP settings with GMail configuration. To proceed with saving these settings you must test them, which is handled by the EmailSettingsValidator.
If there is an issue with the configuration or credentials a meaningful error message should be shown.
IMAP settings must also be validated when IMAP is enabled, before saving.
When saving IMAP, we fetch the mailboxes for that account and populate them. This mailbox must be selected and saved for IMAP to work (the feature acts as though it is disabled until the mailbox is selected and saved):
### Database & Backend
This adds several columns to the Groups table. The purpose of this change is to make it much more explicit that SMTP/IMAP is enabled for a group, rather than relying on settings not being null. Also included is an UPDATE query to backfill these columns. These columns are automatically filled when updating the group.
For GMail, we now filter the mailboxes returned. This is so users cannot use a mailbox like Sent or Trash for syncing, which would generally be disastrous.
There is a new group endpoint for testing email settings. This may be useful in the future for other places in our UI, at which point it can be extracted to a more generic endpoint or module to be included.
2021-05-28 07:28:18 +08:00
|
|
|
end
|
|
|
|
end
|
2021-07-15 22:23:57 +08:00
|
|
|
|
|
|
|
def user_default_notifications(group, params)
|
|
|
|
category_notifications =
|
|
|
|
group.group_category_notification_defaults.pluck(:category_id, :notification_level).to_h
|
|
|
|
tag_notifications =
|
|
|
|
group.group_tag_notification_defaults.pluck(:tag_id, :notification_level).to_h
|
|
|
|
categories = {}
|
|
|
|
tags = {}
|
|
|
|
|
|
|
|
NotificationLevels.all.each do |key, value|
|
|
|
|
category_ids = (params["#{key}_category_ids".to_sym] || []) - ["-1"]
|
|
|
|
|
|
|
|
category_ids.each do |category_id|
|
|
|
|
category_id = category_id.to_i
|
|
|
|
old_value = category_notifications[category_id]
|
|
|
|
|
|
|
|
metadata = { old_value: old_value, new_value: value }
|
|
|
|
|
|
|
|
if old_value.blank?
|
|
|
|
metadata[:action] = :create
|
|
|
|
elsif old_value == value
|
|
|
|
category_notifications.delete(category_id)
|
|
|
|
next
|
|
|
|
else
|
|
|
|
metadata[:action] = :update
|
|
|
|
end
|
|
|
|
|
|
|
|
categories[category_id] = metadata
|
|
|
|
end
|
|
|
|
|
|
|
|
tag_names = (params["#{key}_tags".to_sym] || []) - ["-1"]
|
|
|
|
tag_ids = Tag.where(name: tag_names).pluck(:id)
|
|
|
|
|
|
|
|
tag_ids.each do |tag_id|
|
|
|
|
old_value = tag_notifications[tag_id]
|
|
|
|
|
|
|
|
metadata = { old_value: old_value, new_value: value }
|
|
|
|
|
|
|
|
if old_value.blank?
|
|
|
|
metadata[:action] = :create
|
|
|
|
elsif old_value == value
|
|
|
|
tag_notifications.delete(tag_id)
|
|
|
|
next
|
|
|
|
else
|
|
|
|
metadata[:action] = :update
|
|
|
|
end
|
|
|
|
|
|
|
|
tags[tag_id] = metadata
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
(category_notifications.keys - categories.keys).each do |category_id|
|
|
|
|
categories[category_id] = { action: :delete, old_value: category_notifications[category_id] }
|
|
|
|
end
|
|
|
|
|
|
|
|
(tag_notifications.keys - tags.keys).each do |tag_id|
|
|
|
|
tags[tag_id] = { action: :delete, old_value: tag_notifications[tag_id] }
|
|
|
|
end
|
|
|
|
|
2021-08-31 18:41:26 +08:00
|
|
|
notification_level = nil
|
|
|
|
default_notification_level = params[:default_notification_level]&.to_i
|
|
|
|
|
|
|
|
if default_notification_level.present? &&
|
|
|
|
group.default_notification_level != default_notification_level
|
|
|
|
notification_level = {
|
|
|
|
old_value: group.default_notification_level,
|
|
|
|
new_value: default_notification_level,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
[notification_level, categories, tags]
|
2021-07-15 22:23:57 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
%i[count update].each do |action|
|
2021-08-31 18:41:26 +08:00
|
|
|
define_method("#{action}_existing_users") do |group_users, notification_level, categories, tags|
|
|
|
|
return 0 if notification_level.blank? && categories.blank? && tags.blank?
|
2021-07-15 22:23:57 +08:00
|
|
|
|
|
|
|
ids = []
|
|
|
|
|
2021-08-31 18:41:26 +08:00
|
|
|
if notification_level.present?
|
|
|
|
users = group_users.where(notification_level: notification_level[:old_value])
|
|
|
|
|
|
|
|
if action == :update
|
|
|
|
users.update_all(notification_level: notification_level[:new_value])
|
|
|
|
else
|
|
|
|
ids += users.pluck(:user_id)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-07-15 22:23:57 +08:00
|
|
|
categories.each do |category_id, data|
|
|
|
|
if data[:action] == :update || data[:action] == :delete
|
|
|
|
category_users =
|
|
|
|
CategoryUser.where(
|
|
|
|
category_id: category_id,
|
|
|
|
notification_level: data[:old_value],
|
|
|
|
user_id: group_users.select(:user_id),
|
|
|
|
)
|
|
|
|
|
|
|
|
if action == :update
|
|
|
|
category_users.delete_all
|
|
|
|
else
|
|
|
|
ids += category_users.pluck(:user_id)
|
|
|
|
end
|
|
|
|
|
|
|
|
categories.delete(category_id) if data[:action] == :delete && action == :update
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
tags.each do |tag_id, data|
|
|
|
|
if data[:action] == :update || data[:action] == :delete
|
|
|
|
tag_users =
|
|
|
|
TagUser.where(
|
|
|
|
tag_id: tag_id,
|
|
|
|
notification_level: data[:old_value],
|
|
|
|
user_id: group_users.select(:user_id),
|
|
|
|
)
|
|
|
|
|
|
|
|
if action == :update
|
|
|
|
tag_users.delete_all
|
|
|
|
else
|
|
|
|
ids += tag_users.pluck(:user_id)
|
|
|
|
end
|
|
|
|
|
|
|
|
tags.delete(tag_id) if data[:action] == :delete && action == :update
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if categories.present? || tags.present?
|
|
|
|
group_users
|
|
|
|
.select(:id, :user_id)
|
|
|
|
.find_in_batches do |batch|
|
|
|
|
user_ids = batch.pluck(:user_id)
|
2023-01-09 20:20:10 +08:00
|
|
|
|
2021-07-15 22:23:57 +08:00
|
|
|
categories.each do |category_id, data|
|
|
|
|
category_users = []
|
|
|
|
existing_users =
|
|
|
|
CategoryUser.where(category_id: category_id, user_id: user_ids).where(
|
|
|
|
"notification_level IS NOT NULL",
|
|
|
|
)
|
|
|
|
skip_user_ids = existing_users.pluck(:user_id)
|
2023-01-09 20:20:10 +08:00
|
|
|
|
2021-07-15 22:23:57 +08:00
|
|
|
batch.each do |group_user|
|
|
|
|
next if skip_user_ids.include?(group_user.user_id)
|
|
|
|
category_users << {
|
|
|
|
category_id: category_id,
|
|
|
|
user_id: group_user.user_id,
|
|
|
|
notification_level: data[:new_value],
|
|
|
|
}
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
|
|
|
|
2021-07-15 22:23:57 +08:00
|
|
|
next if category_users.blank?
|
2023-01-09 20:20:10 +08:00
|
|
|
|
2021-07-15 22:23:57 +08:00
|
|
|
if action == :update
|
|
|
|
CategoryUser.insert_all!(category_users)
|
2023-01-09 20:20:10 +08:00
|
|
|
else
|
2021-07-15 22:23:57 +08:00
|
|
|
ids += category_users.pluck(:user_id)
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
2021-07-15 22:23:57 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
tags.each do |tag_id, data|
|
|
|
|
tag_users = []
|
|
|
|
existing_users =
|
|
|
|
TagUser.where(tag_id: tag_id, user_id: user_ids).where(
|
|
|
|
"notification_level IS NOT NULL",
|
2023-01-09 20:20:10 +08:00
|
|
|
)
|
2021-07-15 22:23:57 +08:00
|
|
|
skip_user_ids = existing_users.pluck(:user_id)
|
2023-01-09 20:20:10 +08:00
|
|
|
|
2021-07-15 22:23:57 +08:00
|
|
|
batch.each do |group_user|
|
|
|
|
next if skip_user_ids.include?(group_user.user_id)
|
|
|
|
tag_users << {
|
|
|
|
tag_id: tag_id,
|
|
|
|
user_id: group_user.user_id,
|
|
|
|
notification_level: data[:new_value],
|
|
|
|
created_at: Time.now,
|
|
|
|
updated_at: Time.now,
|
2023-01-09 20:20:10 +08:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2021-07-15 22:23:57 +08:00
|
|
|
next if tag_users.blank?
|
2023-01-09 20:20:10 +08:00
|
|
|
|
2021-07-15 22:23:57 +08:00
|
|
|
if action == :update
|
|
|
|
TagUser.insert_all!(tag_users)
|
|
|
|
else
|
|
|
|
ids += tag_users.pluck(:user_id)
|
2023-01-09 20:20:10 +08:00
|
|
|
end
|
2021-07-15 22:23:57 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
ids.uniq.count
|
|
|
|
end
|
|
|
|
end
|
2014-01-31 06:10:36 +08:00
|
|
|
end
|