Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/silverscouts #295

Merged
merged 25 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4b058e0
Add new ehemalige group
TheWalkingLeek May 16, 2023
f18e09a
Add new root group and silverscouts group
TheWalkingLeek May 22, 2023
c1052f1
Add migration to insert new root group
TheWalkingLeek May 22, 2023
f20e304
Adjust specs and fixtures
TheWalkingLeek May 23, 2023
95cb8aa
Update README
TheWalkingLeek May 23, 2023
424e8fa
Add silverscouts region group
TheWalkingLeek Sep 8, 2023
364e8fd
Add hostname for groups
TheWalkingLeek Aug 24, 2023
e330a6f
Add comment
daniel-illi Sep 18, 2023
112d8b4
Add spec for logo lookup by hostname
daniel-illi Sep 18, 2023
b43c000
Migration to add alumni processing timestamps to Group
daniel-illi Sep 18, 2023
5d42a48
Add custom contents for alumni invitation mails
daniel-illi Sep 23, 2023
91fa5df
Add AlumniMailer
daniel-illi Sep 23, 2023
9646a82
Add finder class to look up applicable groups
daniel-illi Sep 23, 2023
eb67766
Add domain class to check conditions and send invitation mails
daniel-illi Sep 23, 2023
4d0a146
Add classes to send invitations/reminders for deleted roles
daniel-illi Sep 23, 2023
ac2b462
Add background job for invitations/reminders
daniel-illi Sep 23, 2023
7d006ce
Patch Group::Silverscouts to fix constant lookup
daniel-illi Sep 23, 2023
ed95fc0
Make date range configurable
daniel-illi Sep 25, 2023
4abd824
Add copyright comments
daniel-illi Sep 25, 2023
1d2aca8
Remove encoding comments
TheWalkingLeek Oct 23, 2023
f01af45
Do not nest group constant in more modules
TheWalkingLeek Oct 23, 2023
c62493d
Adjust specs for new root group
TheWalkingLeek Oct 23, 2023
548ce49
Adjust specs
TheWalkingLeek Oct 23, 2023
0484f41
Use array of ids as argument since worker cant serialize ActiveRecord…
TheWalkingLeek Oct 24, 2023
83a84a4
Adjust spec for new feature gate
TheWalkingLeek Nov 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

* Das Anwesenheiten-Tab bei Kursen im Status "Qualifikationen erfasst" und "Abgeschlossen" wird neu mit einem Ausrufezeichen markiert, wenn die Anwesenheiten noch gespeichert werden müssen. Merci @ewangler! (hitobito/hitobito_pbs#262)
* Adressverwalter\*innen auf der Abteilungsebene können neu auch für Zugriffsanfragen ausgewählt werden. Merci @philobr! (hitobito/hitobito_pbs#261)
* Die Gruppenstruktur wurde so angepasst, dass neu Silverscouts und Silverscouts Regionen unter der "Root" Ebene erstellt werden können (hitobito_pbs#271)
* Neu kann der Hostname auf PBS und SiSc Ebene gesetzt werden (hitobito_pbs#272)

## Version 1.28

Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ This hitobito wagon defines the organization hierarchy with groups and roles of
# Pfadi Organization Hierarchy

<!-- roles:start -->
* Root
* Root
* Admin: 2FA [:layer_and_below_full]
* Bund
* Bund
* Mitarbeiter*in GS: 2FA [:layer_and_below_full, :contact_data, :admin]
Expand Down Expand Up @@ -256,6 +259,14 @@ This hitobito wagon defines the organization hierarchy with groups and roles of
* Internes Gremium
* Leitung: [:group_and_below_full]
* Mitglied: [:group_read]
* Silverscouts
* Silverscouts
* Leitung: [:group_read, :contact_data]
* Mitglied: []
* Global
* Ehemalige
* Mitglied: []
* Leitung: [:group_full]

(Output of rake app:hitobito:roles)
<!-- roles:end -->
2 changes: 1 addition & 1 deletion app/abilities/pbs/event/constraints.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def in_same_layer_or_below_with_excluded_abteilungen_for_courses
def in_same_layer_or_course_in_below_abteilung
in_same_layer ||
(course_in_abteilung? &&
permission_in_layers?(course_group_ids_above_abteilung - [Group.root.id]))
permission_in_layers?(course_group_ids_above_abteilung - [Group.bund.id]))
end

private
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/censuses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ class CensusesController < CrudController
decorates :group

def create
super(location: census_bund_group_path(Group.root))
super(location: census_bund_group_path(group))
end

private

def group
@group ||= Group.root
@group ||= Group.bund
end

end
20 changes: 20 additions & 0 deletions app/controllers/concerns/hostnamed_groups.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

# Copyright (c) 2023, Pfadibewegung Schweiz. This file is part of
# hitobito_pbs and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito_pbs.

module HostnamedGroups
extend ActiveSupport::Concern

included do
prepend_before_action :determine_group_by_hostname
end

# Initialize @group by matching the current request hostname.
# This is used in LayoutHelper#header_logo to show a specific group logo depending on the hostname
def determine_group_by_hostname
@group ||= Group.where(hostname: request.host).first
end
end
32 changes: 32 additions & 0 deletions app/domain/alumni/applicable_groups.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

# Copyright (c) 2023, Pfadibewegung Schweiz. This file is part of
# hitobito_pbs and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito_pbs.

module Alumni
class ApplicableGroups
attr_reader :role

def initialize(role)
@role = role
end

def silverscout_group_ids
Group::SilverscoutsRegion.
without_deleted.
where.not(self_registration_role_type: nil).
pluck(:id)
end

def ex_members_group_ids
ancestor_layers = role.group.layer_group.self_and_ancestors
Group::Ehemalige.
without_deleted.
where(layer_group_id: ancestor_layers).
where.not(self_registration_role_type: nil).
pluck(:id)
end
end
end
89 changes: 89 additions & 0 deletions app/domain/alumni/invitation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# frozen_string_literal: true

# Copyright (c) 2023, Pfadibewegung Schweiz. This file is part of
# hitobito_pbs and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito_pbs.

module Alumni
class Invitation
AGE_GROUPS = [
Group::Biber, Group::Woelfe, Group::Pfadi, Group::Pio, Group::Pta
].freeze

ALUMNI_ROLES = [Group::Ehemalige::Mitglied, Group::Ehemalige::Leitung].freeze

SILVERSCOUT_GROUPS = [Group::Silverscouts, Group::SilverscoutsRegion].freeze

attr_reader :role, :type, :alumni_groups

def initialize(role, type, alumni_groups = ApplicableGroups.new(role))
@role = role
@type = type
@alumni_groups = alumni_groups
raise "Unknown type: #{type}" unless AlumniMailer.respond_to?(type)
end

def process
set_timestamp
send_invitation if conditions_met?
end

def conditions_met?
feature_enabled? &&
no_active_role_in_layer? &&
old_enough_if_in_age_group? &&
applicable_role? &&
person_has_main_email? &&
person_has_no_alumni_role?
end

def feature_enabled?
FeatureGate.enabled?('alumni.invitation')
end

def no_active_role_in_layer?
!Role.
joins(:group).
where(person_id: role.person_id, group: { layer_group_id: role.group.layer_group_id }).
exists?
end

def old_enough_if_in_age_group?
return true unless AGE_GROUPS.include?(role.group.class)

role.person.birthday.present? && role.person.birthday <= 16.years.ago.to_date
end

def applicable_role?
ALUMNI_ROLES.exclude?(role.class) &&
SILVERSCOUT_GROUPS.exclude?(role.group.class) &&
!role.group.is_a?(Group::Root)
end

def person_has_main_email?
role.person.email.present?
end

def person_has_no_alumni_role?
role.person.roles.none? do |role|
ALUMNI_ROLES.include?(role.class) ||
SILVERSCOUT_GROUPS.include?(role.group.class) ||
role.group.is_a?(Group::Root)
end
end

private

def set_timestamp
timestamp_attr = "alumni_#{type}_processed_at"
role.update!(timestamp_attr => Time.zone.now)
end

def send_invitation
AlumniMailer.send(type, role.person, alumni_groups.ex_members_group_ids.presence,
alumni_groups.silverscout_group_ids.presence).
deliver_later
end
end
end
39 changes: 39 additions & 0 deletions app/domain/alumni/invitations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

# Copyright (c) 2023, Pfadibewegung Schweiz. This file is part of
# hitobito_pbs and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito_pbs.

module Alumni
class Invitations
class_attribute :type, default: :invitation

def process
relevant_roles.each { |role| Alumni::Invitation.new(role, type).process }
end

def relevant_roles
Role.
with_deleted.
where(deleted_at: time_range, alumni_invitation_processed_at: nil).
includes(:person, :group)
end

def time_range
from = parse_duration(:alumni, :invitation, :role_deleted_after_ago)
to = parse_duration(:alumni, :invitation, :role_deleted_before_ago)
from.ago..to.ago
end

private

def parse_duration(*settings_path)
iso8601duration = Settings.dig(*settings_path)
ActiveSupport::Duration.parse(iso8601duration)
rescue ActiveSupport::Duration::ISO8601Parser::ParsingError, ArgumentError
raise "Value #{iso8601duration.inspect} at Settings.#{settings_path.join('')} " +
'is not a valid ISO8601 duration'
end
end
end
16 changes: 16 additions & 0 deletions app/domain/alumni/reminders.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

# Copyright (c) 2023, Pfadibewegung Schweiz. This file is part of
# hitobito_pbs and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito_pbs.

module Alumni
class Reminders < Invitations
self.type = :reminder

def time_range
..parse_duration(:alumni, :reminder, :role_deleted_before_ago).ago
end
end
end
15 changes: 15 additions & 0 deletions app/jobs/alumni_invitations_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

# Copyright (c) 2023, Pfadibewegung Schweiz. This file is part of
# hitobito_pbs and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito_pbs.

class AlumniInvitationsJob < RecurringJob
run_every 1.day

def perform
Alumni::Invitations.new.process
Alumni::Reminders.new.process
end
end
70 changes: 70 additions & 0 deletions app/mailers/alumni_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen_string_literal: true

# Copyright (c) 2023, Pfadibewegung Schweiz. This file is part of
# hitobito_pbs and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito_pbs.

class AlumniMailer < ApplicationMailer

include ActionView::Helpers::TagHelper
include ActionView::Context
include Rails.application.routes.url_helpers

CONTENT_INVITATION_WITH_REGIONAL_GROUPS = 'alumni_invitation_with_regional_alumni_groups'
CONTENT_INVITATION_WITHOUT_REGIONAL_GROUPS = 'alumni_invitation_without_regional_alumni_groups'

CONTENT_REMINDER_WITH_REGIONAL_GROUPS = 'alumni_reminder_with_regional_alumni_groups'
CONTENT_REMINDER_WITHOUT_REGIONAL_GROUPS = 'alumni_reminder_without_regional_alumni_groups'

def invitation(person, ex_members_group_ids, silverscout_group_ids)
@person = person
@ex_members_groups = Group.where(id: ex_members_group_ids)
@silverscout_groups = Group.where(id: silverscout_group_ids)

key = if @ex_members_groups.present?
CONTENT_INVITATION_WITH_REGIONAL_GROUPS
else
CONTENT_INVITATION_WITHOUT_REGIONAL_GROUPS
end

custom_content_mail(@person.email, key, values_for_placeholders(key))
end

def reminder(person, ex_members_group_ids, silverscout_group_ids)
@person = person
@ex_members_groups = Group.where(id: ex_members_group_ids)
@silverscout_groups = Group.where(id: silverscout_group_ids)

key = if @ex_members_groups.present?
CONTENT_REMINDER_WITH_REGIONAL_GROUPS
else
CONTENT_REMINDER_WITHOUT_REGIONAL_GROUPS
end

custom_content_mail(@person.email, key, values_for_placeholders(key))
end

private

# Placeholder {person-name}
def placeholder_person_name
@person.full_name
end

# Placeholder {SiScRegion-Links}
def placeholder_si_sc_region_links
format_helper.group_selfinscription_links(@silverscout_groups, &:name)
end

# Placeholder {AlumniGroup-Links}
def placeholder_alumni_group_links
format_helper.group_selfinscription_links(@ex_members_groups) do |group|
"#{group.parent.name}: #{group.name}"
end
end

def format_helper
@format_helper ||= AlumniMailer::FormatHelper.new(default_url_options)
end
end
37 changes: 37 additions & 0 deletions app/mailers/alumni_mailer/format_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

# Copyright (c) 2023, Pfadibewegung Schweiz. This file is part of
# hitobito_pbs and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/hitobito/hitobito_pbs.

class AlumniMailer::FormatHelper
include ActionView::Helpers::TagHelper
include ActionView::Helpers::UrlHelper
include ActionView::Context
include Rails.application.routes.url_helpers

def initialize(default_url_options)
@default_url_options = default_url_options # used for path helpers
end

def group_selfinscription_links(groups, &block)
content_tag(:ul) do
groups.each_with_object([]) do |group, links|
links << content_tag(:li) { group_selfinscription_link(group, &block) }
end.join.html_safe
end
end

def group_selfinscription_link(group, &block)
url = group_self_registration_url(group_id: group, target: '_blank')
label = block.call(group)
link_to(label, url).html_safe
end

private

# used for path helpers
def controller; end

end
3 changes: 2 additions & 1 deletion app/models/group/abteilung.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ class Group::Abteilung < Group
Group::Elternrat,
Group::AbteilungsGremium,
Group::InternesAbteilungsGremium,
Group::ErziehungsberechtigtenGremium
Group::ErziehungsberechtigtenGremium,
Group::Ehemalige

has_many :member_counts # rubocop:disable Rails/HasManyOrHasOneDependent since groups are only soft-deleted
has_many :geolocations, as: :geolocatable, dependent: :destroy
Expand Down