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

TAN-3815 - Community monitor backend #10365

Open
wants to merge 62 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
44aec87
[TAN-3815] Added hidden community monitor project
jamesspeake Feb 11, 2025
1667276
[TAN-3815] Added native_survey_method column
jamesspeake Feb 11, 2025
7924cd0
[TAN-3815] Test fix
jamesspeake Feb 11, 2025
557125b
[TAN-3815] Added DB migration for native_survey_method
jamesspeake Feb 11, 2025
dfcf6b3
[TAN-3815] Fixed tests
jamesspeake Feb 11, 2025
d1d2b20
[TAN-3815] Added model tests for community_monitor phase
jamesspeake Feb 11, 2025
bc4f5b9
[TAN-3815] Rubocop fix
jamesspeake Feb 11, 2025
45860ab
[TAN-3815] Added feature flag
jamesspeake Feb 11, 2025
f76c55e
[TAN-3815] Added native_survey_method attribute to phase serializer
jamesspeake Feb 11, 2025
1508955
[TAN-3815] Added native_survey_method attribute to public API, projec…
jamesspeake Feb 11, 2025
faa7d76
[TAN-3815] Created native_survey_method classes
jamesspeake Feb 11, 2025
0e2e0b8
[TAN-3815] Fixed feature flag for community monitor phase
jamesspeake Feb 11, 2025
bc8cd55
[TAN-3815] Added different form and allowed types for community_monit…
jamesspeake Feb 11, 2025
e36b26e
[TAN-3815] Added multilocs to community monitor project creation
jamesspeake Feb 12, 2025
0cf1592
[TAN-3815] Added comment
jamesspeake Feb 12, 2025
e519018
Merge branch 'master' into TAN-3815-community-monitor-hidden-project
jamesspeake Feb 12, 2025
7b5bc0e
Merge branch 'master' into TAN-3815-community-monitor-hidden-project
jamesspeake Feb 13, 2025
c006832
[TAN-3815] Added form_builder_config attribute to phase
jamesspeake Feb 13, 2025
a0d5aa2
[TAN-3815] Removed hidden as a visible_to
jamesspeake Feb 14, 2025
87bd42d
[TAN-3815] Added constraints to community monitor fields
jamesspeake Feb 14, 2025
37a6773
[TAN-3815] Removed private attribute
jamesspeake Feb 14, 2025
bd2abdc
[TAN-3815] Added default project_id: nil to community monitor app config
jamesspeake Feb 14, 2025
c9b0d4d
[TAN-3815] Moved hidden attribute to admin publication
jamesspeake Feb 14, 2025
92de3fe
[TAN-3815] Removed form_builder_config from API
jamesspeake Feb 14, 2025
e6c65d8
[TAN-3815] Fixed tests
jamesspeake Feb 14, 2025
ef10676
Merge branch 'master' into TAN-3815-community-monitor-hidden-project
jamesspeake Feb 14, 2025
7af6681
[TAN-3815] Changed permissions for community monitor
jamesspeake Feb 17, 2025
f65673c
[TAN-3815] Added outline tests for native_survey_method
jamesspeake Feb 17, 2025
8e88d85
[TAN-3815] Added admin publication tests and fixed other tests
jamesspeake Feb 17, 2025
2aa1064
[TAN-3815] changed check for .present?
jamesspeake Feb 17, 2025
a173464
[TAN-3815] Fixed projects spec
jamesspeake Feb 17, 2025
4c529e8
[TAN-3815] Fixed tests
jamesspeake Feb 17, 2025
de312c4
[TAN-3815] Exclude native survey responses from creating idea followers
jamesspeake Feb 17, 2025
d5746cf
[TAN-3815] Exclude community monitor survey responses from creating p…
jamesspeake Feb 18, 2025
5893bd4
[TAN-3815] Exclude hidden projects from creating followers
jamesspeake Feb 18, 2025
dd5cc6e
[TAN-3815] Rubocop fix
jamesspeake Feb 18, 2025
0220682
Merge branch 'master' into TAN-3815-community-monitor-hidden-project
jamesspeake Feb 18, 2025
f022b8c
[TAN-3815] Removed native_survey_method
jamesspeake Feb 18, 2025
c30dd87
[TAN-3815] Fixed test
jamesspeake Feb 18, 2025
7bd9ae2
[TAN-3815] Created community_monitor_project factory
jamesspeake Feb 18, 2025
fbc1248
[TAN-3815] Changed .native_survey? to pmethod.supports_survey_form?
jamesspeake Feb 18, 2025
bad1f86
[TAN-3815] Changed .native_survey? to pmethod.supports_survey_form?
jamesspeake Feb 18, 2025
266ab4a
[TAN-3815] Removed phase.native_survey? entirely
jamesspeake Feb 18, 2025
e5f18e5
[TAN-3815] Rubocop fixes
jamesspeake Feb 19, 2025
e269c3a
[TAN-3815] Fixed community_monitor phase tests
jamesspeake Feb 19, 2025
0c8783a
[TAN-3815] Changed method of hiding project to use hidden: true on pr…
jamesspeake Feb 19, 2025
528916f
[TAN-3815] Removed unneeded projects.* in projects finder
jamesspeake Feb 21, 2025
4176e35
[TAN-3815] Fixed tests and moved validation to the participation method
jamesspeake Feb 21, 2025
a2776e0
[TAN-3815] Fixed phase validation
jamesspeake Feb 21, 2025
34f0270
[TAN-3815] Changed how hidden projects works
jamesspeake Feb 21, 2025
bdc6522
[TAN-3815] Rubocop fix
jamesspeake Feb 21, 2025
94aebb1
Merge pull request #10385 from CitizenLabDotCo/TAN-3815-community-mon…
jamesspeake Feb 21, 2025
772b8fd
Merge branch 'master' into TAN-3815-community-monitor-hidden-project-2
jamesspeake Feb 21, 2025
072d08a
[TAN-3815] Added participation method tests
jamesspeake Feb 21, 2025
3b63b20
[TAN-3815] Fixed survey results to work for new participation method
jamesspeake Feb 25, 2025
3d817d9
[TAN-3815] Added tests for community monitor survey results
jamesspeake Feb 26, 2025
f650cf5
[TAN-3815] Added tests for community monitor survey update_all + adde…
jamesspeake Feb 26, 2025
33b6fac
[TAN-3815] Added tests for community monitor survey idea creation + f…
jamesspeake Feb 26, 2025
4577759
[TAN-3815] Last changes after own review
jamesspeake Feb 27, 2025
d6cd458
Merge branch 'master' into TAN-3815-community-monitor-hidden-project-2
jamesspeake Feb 27, 2025
2e28c55
[TAN-3815] Rubocop fixes
jamesspeake Feb 27, 2025
4f75390
[TAN-3815] Rerun migrations
jamesspeake Feb 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion back/app/controllers/web_api/v1/ideas_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def create
input.phase_ids = [phase.id] if phase_ids.empty?

# NOTE: Needs refactor allow_anonymous_participation? so anonymous_participation can be allow or force
if phase.native_survey? && phase.allow_anonymous_participation?
if phase.pmethod.supports_survey_form? && phase.allow_anonymous_participation?
input.anonymous = true
end
input.author ||= current_user
Expand Down
4 changes: 2 additions & 2 deletions back/app/controllers/web_api/v1/phases_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ def survey_results
end

def submission_count
count = if @phase.native_survey?
@phase.ideas.native_survey.published.count
count = if @phase.pmethod.supports_survey_form?
@phase.ideas.supports_survey.published.count
else
@phase.ideas.transitive.published.count
end
Expand Down
46 changes: 45 additions & 1 deletion back/app/controllers/web_api/v1/projects_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def index_for_areas
# Returns all non-draft projects that are visible to user, for the selected topics.
# Ordered by created_at, newest first.
def index_for_topics
projects = policy_scope(Project)
projects = policy_scope(Project).not_hidden
projects = projects
.not_draft
.with_some_topics(params[:topics])
Expand Down Expand Up @@ -317,6 +317,22 @@ def destroy_participation_data
).serializable_hash, status: :ok
end

def community_monitor
settings = AppConfiguration.instance.settings
settings.dig('community_monitor', 'enabled') || raise(ActiveRecord::RecordNotFound)

# Find the community monitor project from config or create it
project_id = settings.dig('community_monitor', 'project_id')
project = project_id.present? ? Project.find(project_id) : create_community_monitor_project(settings)

authorize project
render json: WebApi::V1::ProjectSerializer.new(
project,
params: jsonapi_serializer_params,
include: %i[current_phase]
).serializable_hash
end

private

def sidefx
Expand Down Expand Up @@ -375,6 +391,34 @@ def base_render_mini_index
include: %i[project_images current_phase]
)
end

def create_community_monitor_project(settings)
multiloc_service = MultilocService.new
project = Project.create!(
hidden: true,
title_multiloc: multiloc_service.i18n_to_multiloc('phases.community_monitor_title'),
internal_role: 'community_monitor'
)
sidefx.after_create(project, current_user) if project

Phase.create!(
title_multiloc: multiloc_service.i18n_to_multiloc('phases.community_monitor_title'),
project: project,
participation_method: 'community_monitor_survey',
commenting_enabled: false, # TODO: JS - set in the participation method defaults
reacting_enabled: false,
start_at: Time.now,
campaigns_settings: { project_phase_started: true }, # TODO: JS - Is this correct?
native_survey_title_multiloc: multiloc_service.i18n_to_multiloc('phases.community_monitor_title'),
native_survey_button_multiloc: multiloc_service.i18n_to_multiloc('phases.native_survey_button')
)

# Set the ID in the settings
settings['community_monitor']['project_id'] = project.id
AppConfiguration.instance.update!(settings: settings)

project
end
end

WebApi::V1::ProjectsController.include(AggressiveCaching::Patches::WebApi::V1::ProjectsController)
3 changes: 3 additions & 0 deletions back/app/models/idea.rb
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ class Idea < ApplicationRecord
native_survey.where(publication_status: 'draft')
}

# Equivalent to pmethod.supports_survey_form?
scope :supports_survey, -> { where(creation_phase: Phase.where(participation_method: %w[native_survey community_monitor_survey])) }

# Filters out all the ideas for which the ParticipationMethod responds truety
# to the given block. The block receives the ParticipationMethod object as an
# argument
Expand Down
1 change: 1 addition & 0 deletions back/app/models/notifications/project_phase_started.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class ProjectPhaseStarted < Notification

def self.make_notifications_on(activity)
phase = activity.item
return [] unless phase.project.published?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could have used unless phase.project.hidden? but decided that it didn't make sense for notifications to be made unless the project was live


ProjectPolicy::InverseScope.new(phase.project, User.from_follows(phase.project.followers)).resolve.map do |recipient|
new(recipient: recipient, phase: phase, project: phase.project)
Expand Down
19 changes: 8 additions & 11 deletions back/app/models/notifications/project_phase_upcoming.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,15 @@ class ProjectPhaseUpcoming < Notification

def self.make_notifications_on(activity)
phase = activity.item
return [] unless phase.project.published?

if phase.project
recipients = UserRoleService.new.moderators_for phase
recipients.ids.map do |recipient_id|
new(
recipient_id: recipient_id,
phase: phase,
project: phase.project
)
end
else
[]
recipients = UserRoleService.new.moderators_for phase
recipients.ids.map do |recipient_id|
new(
recipient_id: recipient_id,
phase: phase,
project: phase.project
)
end
end
end
Expand Down
1 change: 1 addition & 0 deletions back/app/models/permission.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Permission < ApplicationRecord
'ideation' => %w[posting_idea commenting_idea reacting_idea attending_event],
'proposals' => %w[posting_idea commenting_idea reacting_idea attending_event],
'native_survey' => %w[posting_idea attending_event],
'community_monitor_survey' => %w[posting_idea attending_event],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably don't need attending event, but all other phases have attending event and there would be some refactoring not to have it

'survey' => %w[taking_survey attending_event],
'poll' => %w[taking_poll attending_event],
'voting' => %w[voting commenting_idea attending_event],
Expand Down
19 changes: 9 additions & 10 deletions back/app/models/phase.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ class Phase < ApplicationRecord
# voting?
with_options if: :voting? do
validates :voting_method, presence: true, inclusion: { in: VOTING_METHODS }
validate :validate_voting
validates :voting_term_singular_multiloc, multiloc: { presence: false }
validates :voting_term_plural_multiloc, multiloc: { presence: false }
validates :autoshare_results_enabled, inclusion: { in: [true, false] }
Expand All @@ -182,12 +181,14 @@ class Phase < ApplicationRecord
where(start_at: date)
}

# native_survey?
with_options if: :native_survey? do
# any type of native_survey phase
with_options if: ->(phase) { phase.pmethod.supports_survey_form? } do
validates :native_survey_title_multiloc, presence: true, multiloc: { presence: true }
validates :native_survey_button_multiloc, presence: true, multiloc: { presence: true }
end

validate :validate_phase_participation_method

scope :published, lambda {
joined = includes(project: { admin_publication: :parent })
joined.where(
Expand Down Expand Up @@ -256,11 +257,6 @@ def voting?
participation_method == 'voting'
end

# Used for validations (which are hard to delegate through the participation method)
def native_survey?
participation_method == 'native_survey'
end

def pmethod
reload_participation_method if !@pmethod
@pmethod
Expand Down Expand Up @@ -371,6 +367,8 @@ def reload_participation_method
ParticipationMethod::Proposals.new(self)
when 'native_survey'
ParticipationMethod::NativeSurvey.new(self)
when 'community_monitor_survey'
ParticipationMethod::CommunityMonitorSurvey.new(self)
when 'document_annotation'
ParticipationMethod::DocumentAnnotation.new(self)
when 'survey'
Expand All @@ -390,8 +388,9 @@ def set_presentation_mode
self.presentation_mode ||= 'card'
end

def validate_voting
Factory.instance.voting_method_for(self).validate_phase
# Delegate any rules specific to a method to the participation method itself
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made this more generic so any participation method can now implement it's own validation rules

def validate_phase_participation_method
pmethod.validate_phase
end
end

Expand Down
11 changes: 9 additions & 2 deletions back/app/models/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
# followers_count :integer default(0), not null
# preview_token :string not null
# header_bg_alt_text_multiloc :jsonb
# hidden :boolean default(FALSE), not null
#
# Indexes
#
Expand Down Expand Up @@ -88,7 +89,7 @@ class Project < ApplicationRecord
after_save :reassign_moderators, if: :folder_changed?
after_commit :clear_folder_changes, if: :folder_changed?

INTERNAL_ROLES = %w[open_idea_box].freeze
INTERNAL_ROLES = %w[open_idea_box community_monitor].freeze

validates :title_multiloc, presence: true, multiloc: { presence: true }
validates :description_multiloc, multiloc: { presence: false, html: true }
Expand All @@ -97,6 +98,8 @@ class Project < ApplicationRecord
validates :internal_role, inclusion: { in: INTERNAL_ROLES, allow_nil: true }
validate :admin_publication_must_exist, unless: proc { Current.loading_tenant_template } # TODO: This should always be validated!

scope :not_hidden, -> { where(hidden: false) }

pg_search_scope :search_by_all,
against: %i[title_multiloc description_multiloc description_preview_multiloc slug],
using: { tsearch: { prefix: true } }
Expand Down Expand Up @@ -142,7 +145,7 @@ class Project < ApplicationRecord

alias project_id id

delegate :ever_published?, :never_published?, to: :admin_publication, allow_nil: true
delegate :published?, :ever_published?, :never_published?, to: :admin_publication, allow_nil: true

class << self
def search_ids_by_all_including_patches(term)
Expand Down Expand Up @@ -223,6 +226,10 @@ def refresh_preview_token
self.preview_token = self.class.generate_preview_token
end

def hidden?
hidden
end

private

def admin_publication_must_exist
Expand Down
2 changes: 1 addition & 1 deletion back/app/policies/admin_publication_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class Scope < ApplicationPolicy::Scope
def resolve
AdminPublication
.publication_types
.map { |klass| scope.where(publication: scope_for(klass)) } # scope per publication type
.map { |klass| scope.where(publication: klass == Project ? scope_for(klass).not_hidden : scope_for(klass)) } # scope per publication type
.reduce(&:or) # joining partial scopes
end
end
Expand Down
4 changes: 4 additions & 0 deletions back/app/policies/project_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ def active_moderator?
UserRoleService.new.can_moderate_project? record, user
end

def community_monitor?
active_moderator?
end

private

def update_status?
Expand Down
4 changes: 4 additions & 0 deletions back/app/serializers/web_api/v1/phase_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ class WebApi::V1::PhaseSerializer < WebApi::V1::BaseSerializer
object.custom_form_persisted?
end

attribute :supports_survey_form do |phase|
phase.pmethod.supports_survey_form?
end

belongs_to :project

has_one :user_basket, if: proc { |object, params|
Expand Down
1 change: 1 addition & 0 deletions back/app/services/project_copy_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ def yml_phases(shift_timestamps: 0, timeline_start_at: nil)
yml_phase['document_annotation_embed_url'] = phase.document_annotation_embed_url
end

# TODO: JS - Needed for community monitor?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will we ever need to copy this project?

if yml_phase['participation_method'] == 'native_survey'
yml_phase['native_survey_title_multiloc'] = phase.native_survey_title_multiloc
yml_phase['native_survey_button_multiloc'] = phase.native_survey_button_multiloc
Expand Down
4 changes: 2 additions & 2 deletions back/app/services/projects_finder_service.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class ProjectsFinderService
def initialize(projects, user = nil, params = {})
@projects = projects
@projects = projects.not_hidden
@user = user
@page_size = (params.dig(:page, :size) || 500).to_i
@page_number = (params.dig(:page, :number) || 1).to_i
Expand All @@ -22,7 +22,7 @@ def participation_possible
subquery = projects_with_active_phase(subquery)
.joins('INNER JOIN phases AS active_phases ON active_phases.project_id = projects.id')
.where.not(phases: { participation_method: 'information' })
.select('projects.*, projects.created_at AS projects_created_at, projects.id AS projects_id')
.select('projects.created_at AS projects_created_at, projects.id AS projects_id')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This turned out to be unneeded as it was double returning all the fields


# Perform the SELECT DISTINCT on the outer query and order first by the end date of the active phase,
# second by project created_at, and third by project ID.
Expand Down
8 changes: 7 additions & 1 deletion back/app/services/side_fx_idea_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,19 @@ def scrape_facebook(idea)
end

def create_followers(idea, user)
Follower.find_or_create_by(followable: idea, user: user)
return if idea.project.hidden?

Follower.find_or_create_by(followable: idea, user: user) if create_idea_follower?(idea)
Follower.find_or_create_by(followable: idea.project, user: user)
return if !idea.project.in_folder?

Follower.find_or_create_by(followable: idea.project.folder, user: user)
end

def create_idea_follower?(idea)
idea.creation_phase ? idea.creation_phase.pmethod.follow_idea_on_idea_submission? : true # Defaults for true for ideas without a creation_phase
end

def serialize_idea(frozen_idea)
serialized_idea = clean_time_attributes(frozen_idea.attributes)
serialized_idea['location_point'] = serialized_idea['location_point'].to_s
Expand Down
1 change: 1 addition & 0 deletions back/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ en:
open_idea_phase_title: Current phase
native_survey_title: Survey
native_survey_button: Take the survey
community_monitor_title: Community monitor
events:
council_meeting_title: Council Meeting
council_meeting_description: >
Expand Down
1 change: 1 addition & 0 deletions back/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@
get 'finished_or_archived', action: 'index_finished_or_archived'
get 'for_followed_item', action: 'index_for_followed_item'
get 'with_active_participatory_phase', action: 'index_with_active_participatory_phase'
get 'community_monitor', action: 'community_monitor'
end

resource :review, controller: 'project_reviews'
Expand Down
26 changes: 26 additions & 0 deletions back/config/schemas/settings.schema.json.erb
Original file line number Diff line number Diff line change
Expand Up @@ -1360,6 +1360,32 @@
}
}
}
},

"community_monitor": {
"type": "object",
"title": "Community Monitor",
"description": "Long running public survey.",
"additionalProperties": false,
"required": [
"allowed",
"enabled"
],
"properties": {
"allowed": {
"type": "boolean",
"default": false
},
"enabled": {
"type": "boolean",
"default": false
},
"project_id": {
"title": "The ID of the required hidden project",
"description": "Do not edit unless you know what you're doing. This is usually set by the system",
"type": "string"
}
}
}
},
"dependencies": {
Expand Down
5 changes: 5 additions & 0 deletions back/db/migrate/20250219104523_add_hidden_to_projects.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddHiddenToProjects < ActiveRecord::Migration[7.0]
def change
add_column :projects, :hidden, :boolean, default: false, null: false
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did not add an index here as for booleans I don't think it often gets used

end
end
Loading