Noosfero/noosfero

View on GitHub
app/models/profile.rb

Summary

Maintainability
F
6 days
Test Coverage
# A Profile is the representation and web-presence of an individual or an
# organization. Every Profile is attached to its Environment of origin,
# which by default is the one returned by Environment:default.
class Profile < ApplicationRecord
  attr_accessible :name, :identifier, :access, :nickname,
                  :custom_footer, :custom_header, :address, :zip_code, :contact_phone,
                  :image_builder, :top_image_builder, :description, :closed, :template_id, :environment, :lat,
                  :lng, :is_template, :fields_privacy, :preferred_domain_id, :category_ids,
                  :country, :city, :state, :national_region_code, :email, :contact_email,
                  :redirect_l10n, :notification_time, :redirection_after_login,
                  :custom_url_redirection, :layout_template, :email_suggestions,
                  :allow_members_to_invite, :invite_friends_only, :secret,
                  :profile_admin_mail_notification, :allow_followers, :wall_access,
                  :profile_kinds, :tag_list, :boxes_attributes, :metadata

  attr_accessor :old_region_id

  # use for internationalizable human type names in search facets
  # reimplement on subclasses
  def self.type_name
    _("Profile")
  end

  SEARCHABLE_FIELDS = {
    name: { label: _("Name"), weight: 10 },
    identifier: { label: _("Username"), weight: 5 },
    nickname: { label: _("Nickname"), weight: 2 },
  }

  SEARCH_FILTERS = {
    order: %w[more_recent],
    display: %w[compact]
  }

  CAPTCHA_REQUIREMENTS = {
    create_comment: { label: _("Create a comment"), options: Entitlement::Levels.range_options(0, 3) },
    new_contact: { label: _("Make email contact"), options: Entitlement::Levels.range_options(0, 3) },
    report_abuse: { label: _("Report an abuse"), options: Entitlement::Levels.range_options(0, 3) },
  }

  NUMBER_OF_BOXES = 4

  def self.default_search_display
    "compact"
  end

  module Roles
    def self.admin(env_id)
      find_role("admin", env_id)
    end

    def self.member(env_id)
      find_role("member", env_id)
    end

    def self.moderator(env_id)
      find_role("moderator", env_id)
    end

    def self.owner(env_id)
      find_role("owner", env_id)
    end

    def self.editor(env_id)
      find_role("editor", env_id)
    end

    def self.organization_member_roles(env_id)
      all_roles(env_id).select { |r| r.key.match(/^profile_/) unless r.key.blank? || !r.profile_id.nil? }
    end

    def self.organization_custom_roles(env_id, profile_id)
      all_roles(env_id).where("profile_id = ?", profile_id)
    end

    def self.organization_roles(env_id, profile_id)
      all_roles(env_id).where("profile_id = ?  or (key like 'profile_%' and profile_id is null)", profile_id)
    end

    def self.organization_member_and_custom_roles(env_id, profile_id)
      self.organization_member_roles(env_id) | self.organization_custom_roles(env_id, profile_id)
    end

    def self.all_roles(env_id)
      Role.where(environment_id: env_id)
    end

    def self.method_missing(m, *args, &block)
      role = find_role(m, args[0])
      return role unless role.nil?

      super
    end

    private

      def self.find_role(name, env_id)
        ::Role.find_by key: "profile_#{name}", environment_id: env_id
      end
  end

  PERMISSIONS["Profile"] = {
    "edit_profile" => N_("Edit profile"),
    "destroy_profile" => N_("Destroy profile"),
    "manage_memberships" => N_("Manage memberships"),
    "post_content" => N_("Manage/Publish content"), # changed only presentation name to keep already given permissions
    "edit_profile_design" => N_("Edit profile design"),
    "manage_products" => N_("Manage products"),
    "manage_friends" => N_("Manage friends"),
    "validate_enterprise" => N_("Validate enterprise"),
    "perform_task" => N_("Perform task"),
    "view_tasks" => N_("View tasks"),
    "moderate_comments" => N_("Moderate comments"),
    "edit_appearance" => N_("Edit appearance"),
    "view_private_content" => N_("View private content"),
    "invite_members" => N_("Invite members"),
    "send_mail_to_members" => N_("Send e-Mail to members"),
    "manage_custom_roles" => N_("Manage custom roles"),
    "manage_email_templates" => N_("Manage Email Templates"),
  }

  acts_as_accessible

  prepend SetProfileRegionFromCityState
  include Customizable
  acts_as_customizable

  include Noosfero::Plugin::HotSpot

  include HasUploadQuota

  include Entitlement::SliderHelper
  include Entitlement::ProfileJudge

  scope :memberships_of, ->person {
    distinct.select("profiles.*")
            .joins(:role_assignments)
            .where("role_assignments.accessor_type = ? AND role_assignments.accessor_id = ?", person.class.base_class.name, person.id)
  }
  # FIXME: these will work only if the subclass is already loaded
  scope :enterprises, -> {
    where((Enterprise.send(:subclasses).map(&:name) << "Enterprise").map { |klass| "profiles.type = '#{klass}'" }.join(" OR "))
  }
  scope :communities, -> {
    where((Community.send(:subclasses).map(&:name) << "Community").map { |klass| "profiles.type = '#{klass}'" }.join(" OR "))
  }
  scope :templates, ->template_id = nil {
    s = where is_template: true
    s = s.where id: template_id if template_id
    s
  }

  scope :with_templates, ->templates {
    where template_id: templates
  }
  scope :no_templates, -> { where is_template: false }

  scope :recent, ->limit = nil { order("id DESC").limit(limit) }

  # Returns a scoped object to select profiles in a given location or in a radius
  # distance from the given location center.
  # The parameter can be the `request.params` with the keys:
  # * `country`: Country code string.
  # * `state`: Second-level administrative country subdivisions.
  # * `city`: City full name for center definition, or as set by users.
  # * `lat`: The latitude to define the center of georef search.
  # * `lng`: The longitude to define the center of georef search.
  # * `distance`: Define the search radius in kilometers.
  # NOTE: This method may return an exception object, to inform filter error.
  # When chaining scopes, is hardly recommended you to add this as the last one,
  # if you can't be sure about the provided parameters.

  def self.distance_is_blank(params)
    where_code = []
    [:city, :state, :country].each do |place|
      unless params[place].blank?
        # ... So we must to find on this named location
        # TODO: convert location attrs to a table column
        where_code << "(profiles.data like '%#{place}: #{params[place]}%')"
     end
    end
    self.where where_code.join(" AND ")
  end

  def self.filter_in_a_georef_circle(params)
    unless params[:lat].blank? && params[:lng].blank?
      lat, lng = [params[:lat].to_f, params[:lng].to_f]
    end
    if !lat
      location = [params[:city], params[:state], params[:country]].compact.join(", ")
      if location.blank?
        return Exception.new (
        _("You must to provide `lat` and `lng`, or `city` and `country` to define the center of the search circle, defined by `distance`.")
      )
      end
      lat, lng = Noosfero::GeoRef.location_to_georef location
    end
    dist = params[:distance].to_f
    self.where "#{Noosfero::GeoRef.sql_dist lat, lng} <= #{dist}"
  end

  def self.by_location(params)
    params = params.with_indifferent_access
    if params[:distance].blank?
      distance_is_blank params
    else # Filter in a georef circle
      # location = Location.new(params[:lat], params[:lng], params[:city], params[:state], params[:country], params[:distance])
      self.filter_in_a_georef_circle(params)
    end
  end

  include TimeScopes

  def members(by_field = "")
    scopes = plugins.dispatch_scopes(:organization_members, self)
    scopes << Person.members_of(self, by_field)
    return scopes.first if scopes.size == 1

    ScopeTool.union *scopes
  end

  def members_by(field, value = nil)
    if value && !value.blank?
      members_like(field, value).order("profiles.name")
    else
      members.order("profiles.name")
    end
  end

  def members_like(field, value)
    members(field).where("LOWER(#{field}) LIKE ?", "%#{value.downcase}%") if value
  end

  def members_by_role(roles)
    Person.members_of(self).by_role(roles)
  end

  extend ActsAsHavingSettings::ClassMethods
  acts_as_having_settings field: :data

  store_accessor :metadata
  include MetadataScopes

  metadata_items :allow_single_file

  def settings
    data
  end

  settings_items :redirect_l10n, type: :boolean, default: false
  settings_items :description
  settings_items :fields_privacy, type: :hash, default: {}
  settings_items :email_suggestions, type: :boolean, default: false
  settings_items :profile_admin_mail_notification, type: :boolean, default: true

  settings_items :profile_kinds, type: :hash, default: {}
  after_save do |profile|
    profile.profile_kinds.each do |key, value|
      environment = profile.environment
      kind = environment.kinds.where(id: key.to_s).first
      next unless kind.present?

      value == "1" ? kind.add_profile(profile) : kind.remove_profile(profile)
    end
  end
  before_save do |profile|
    unless profile.setting_changed?(:profile_kinds)
      profile.profile_kinds = {}
    end
  end

  def kinds_style_classes
    return nil if kinds.blank?

    kinds.map(&:style_class).join(" ")
  end

  extend ActsAsHavingBoxes::ClassMethods
  acts_as_having_boxes

  acts_as_taggable

  def self.qualified_column_names
    Profile.column_names.map { |n| [Profile.table_name, n].join(".") }.join(",")
  end

  scope :visible, -> { where visible: true, secret: false }
  scope :disabled, -> { where visible: false }
  scope :enabled, -> { where enabled: true }

  scope :higher_disk_usage, -> { order("metadata->>'disk_usage' DESC NULLS LAST") }
  scope :lower_disk_usage, -> { order("metadata->>'disk_usage' ASC NULLS LAST") }

  # subclass specific
  scope :more_popular, -> {}
  scope :more_active, -> { order "profiles.activities_count DESC" }
  scope :more_recent, -> { order "profiles.created_at DESC" }

  scope :followed_by, ->person {
    distinct.select("profiles.*")
            .joins("left join profiles_circles ON profiles_circles.profile_id = profiles.id")
            .joins("left join circles ON circles.id = profiles_circles.circle_id")
            .where("circles.person_id = ?", person.id)
  }

  scope :in_circle, ->circle {
    distinct.select("profiles.*")
            .joins("left join profiles_circles ON profiles_circles.profile_id = profiles.id")
            .joins("left join circles ON circles.id = profiles_circles.circle_id")
            .where("circles.id = ?", circle.id)
  }

  settings_items :wall_access, type: :integer, default: Entitlement::Levels.levels[:users]
  settings_items :allow_followers, type: :boolean, default: true
  alias_method :allow_followers?, :allow_followers

  acts_as_trackable dependent: :destroy

  has_many :profile_activities
  has_many :action_tracker_notifications, foreign_key: "profile_id"
  has_many :tracked_notifications, -> { order "updated_at DESC" }, through: :action_tracker_notifications, source: :action_tracker
  has_many :scraps_received, -> { order "updated_at DESC" }, class_name: "Scrap", foreign_key: :receiver_id, dependent: :destroy
  belongs_to :template, class_name: "Profile", foreign_key: "template_id", optional: true

  has_many :email_templates, foreign_key: :owner_id

  has_many :profile_followers
  has_many :followers, -> { distinct }, class_name: "Person", through:  :profile_followers, source: :person

  # Although this should be a has_one relation, there are no non-silly names for
  # a foreign key on article to reference the template to which it is
  # welcome_page... =P
  belongs_to :welcome_page, class_name: "Article", dependent: :destroy, optional: true

  def welcome_page_content
    welcome_page && welcome_page.access == Entitlement::Levels.levels[:visitors] ? welcome_page.body : nil
  end

  has_many :search_terms, as: :context

  def scraps(scrap = nil)
    scrap = scrap.is_a?(Scrap) ? scrap.id : scrap
    scrap.nil? ? Scrap.all_scraps(self) : Scrap.all_scraps(self).find(scrap)
  end

  validates_length_of :description, maximum: 550, allow_nil: true

  # Valid identifiers must match this format.
  IDENTIFIER_FORMAT = /\A#{Noosfero.identifier_format}\Z/

  # These names cannot be used as identifiers for Profiles
  RESERVED_IDENTIFIERS = %w[
    admin
    system
    myprofile
    profile
    cms
    community
    test
    search
    not_found
    cat
    tag
    tags
    environment
    webmaster
    info
    root
    assets
    doc
    chat
    plugin
    site
  ]

  belongs_to :user, optional: true

  has_many :domains, as: :owner
  belongs_to :preferred_domain, class_name: "Domain", foreign_key: "preferred_domain_id", optional: true
  belongs_to :environment, optional: true

  has_many :articles, dependent: :destroy
  has_many :comments_received, class_name: "Comment", through:  :articles, source: :comments
  belongs_to :home_page, class_name: Article.name, foreign_key: "home_page_id", optional: true

  has_many :files, class_name: "UploadedFile", dependent: :destroy

  extend ActsAsHavingImage::ClassMethods
  acts_as_having_image
  acts_as_having_image field: :top_image

  has_many :tasks, dependent:  :destroy, as: "target"

  has_many :events, -> { order "start_date" }, source: "articles", class_name: "Event"

  def find_in_all_tasks(task_id)
    begin
      Task.to(self).find(task_id)
    rescue
      nil
    end
  end

  has_many :profile_categorizations, -> { where "categories_profiles.virtual = ?", false }
  has_many :categories, through: :profile_categorizations
  has_many :regions, -> { where(type: ["Region", "State", "City"]) }, through: :profile_categorizations, source: :category

  has_many :profile_categorizations_including_virtual, class_name: "ProfileCategorization"
  has_many :categories_including_virtual, through: :profile_categorizations_including_virtual, source: :category

  has_many :abuse_complaints, foreign_key: "requestor_id", dependent:  :destroy

  has_many :profile_suggestions, foreign_key: :suggestion_id, dependent: :destroy

  has_and_belongs_to_many :kinds

  scope :with_kind, ->kind { joins(:kinds).where("kinds.id = ?", kind.id) }

  def top_level_categorization
    ret = {}
    self.profile_categorizations.each do |c|
      p = c.category.top_ancestor
      ret[p] = (ret[p] || []) + [c.category]
    end
    ret
  end

  def interests
    categories.select { |item| !item.is_a?(Region) }
  end

  belongs_to :region, optional: true

  LOCATION_FIELDS = %w[address address_reference district city state country zip_code]
  metadata_items *(LOCATION_FIELDS - %w[address])

  before_save :save_old_region
  def save_old_region
    self.old_region_id = self.region_id_was || self.region_id
  end

  before_save :match_articles_access
  def match_articles_access
    if access_changed?
      articles.where("access < ?", access).update_all(access: access)
    end
  end

  before_validation :update_wall_access
  def update_wall_access
    if access > wall_access
      self.wall_access = access
    end
  end

  def location(separator = " - ")
    myregion = self.region
    if myregion
      myregion.hierarchy.reverse.first(2).map(&:name).join(separator)
    else
      full_address(separator)
    end
  end

  def full_address(separator = " - ")
    LOCATION_FIELDS.map do |item|
      (self.respond_to?(item) && !self.send(item).blank?) ? self.send(item) : nil
    end.compact.join(separator)
  end

  def city
    NationalRegion.name_or_default(metadata["city"])
  end

  def state
    NationalRegion.name_or_default(metadata["state"])
  end

  def country
    NationalRegion.name_or_default(metadata["country"])
  end

  def geolocation
    unless location.blank?
      location
    else
      if environment.location.blank?
        environment.location = "BRA"
      end
      environment.location
    end
  end

  def country_name
    CountriesHelper::Object.instance.lookup(country) if respond_to?(:country)
  end

  def pending_categorizations
    @pending_categorizations ||= []
  end

  def add_category(c)
    if new_record?
      pending_categorizations << c
    else
      ProfileCategorization.add_category_to_profile(c, self)
      self.categories
    end
    self.categories
  end

  def category_ids=(ids)
    ProfileCategorization.remove_all_for(self)
    ids.uniq.each do |item|
      add_category(Category.find(item)) unless item.to_i.zero?
    end
  end

  after_create :create_pending_categorizations
  def create_pending_categorizations
    pending_categorizations.each do |item|
      ProfileCategorization.add_category_to_profile(item, self)
    end
    pending_categorizations.clear
  end

  def top_level_articles(reload = false)
    if reload
      @top_level_articles = nil
    end
    @top_level_articles ||= Article.top_level_for(self)
  end

  def self.is_available?(identifier, environment, profile_id = nil)
    return false unless !Profile::RESERVED_IDENTIFIERS.include?(identifier) &&
                        (NOOSFERO_CONF["exclude_profile_identifier_pattern"].blank? || identifier !~ /#{NOOSFERO_CONF['exclude_profile_identifier_pattern']}/)
    return true if environment.nil?

    environment.is_identifier_available?(identifier, profile_id)
  end

  validates_presence_of :identifier, :name
  validates_length_of :nickname, maximum: 16, allow_nil: true
  validate :valid_template
  validate :valid_identifier
  validate :wall_access_value

  def valid_identifier
    errors.add(:identifier, :invalid) unless identifier =~ IDENTIFIER_FORMAT
    errors.add(:identifier, :not_available) unless Profile.is_available?(identifier, environment, id)
  end

  def valid_template
    if template_id.present? && template && !template.is_template
      errors.add(:template, _("is not a template."))
    end
  end

  def wall_access_value
    if wall_access < access
      self.errors.add(:wall_access, _("can not be less restrictive than access which is: %s.") % Entitlement::Levels.label(access, self))
    end
  end

  before_create :set_default_environment
  def set_default_environment
    if self.environment.nil?
      self.environment = Environment.default
    end
    true
  end

  # registar callback for creating boxes after the object is created.
  after_create :create_default_set_of_boxes

  # creates the initial set of boxes when the profile is created. Can be
  # overridden for each subclass to create a custom set of boxes for its
  # instances.
  def create_default_set_of_boxes
    if template
      apply_template(template, copy_articles: false)
    else
      NUMBER_OF_BOXES.times do
        self.boxes << Box.new
      end

      if self.respond_to?(:default_set_of_blocks)
        default_set_of_blocks.each_with_index do |blocks, i|
          blocks.each do |block|
            self.boxes[i].blocks << block
          end
        end
      end
    end

    true
  end

  def copy_blocks_from(profile)
    template_boxes = profile.boxes.select { |box| box.position }
    self.boxes.destroy_all
    self.boxes = template_boxes.size.times.map { Box.new }

    template_boxes.each_with_index do |box, i|
      new_box = self.boxes[i]
      new_box.position = box.position
      box.blocks.each do |block|
        new_block = block.class.new(title: block[:title])
        new_block.copy_from(block)
        new_box.blocks << new_block
        if block.mirror?
          block.add_observer(new_block)
        end
      end
    end
  end

  # this method should be overwritten to provide the correct template
  def default_template
    nil
  end

  def template_with_default
    template_without_default || default_template
  end
  alias_method :template_without_default, :template
  alias_method :template, :template_with_default

  def apply_template(template, options = { copy_articles: true })
    raise "#{template.identifier} is not a template" if !template.is_template

    self.template = template
    copy_blocks_from(template)
    copy_articles_from(template) if options[:copy_articles]
    self.apply_type_specific_template(template)

    # copy interesting attributes
    self.layout_template = template.layout_template
    self.theme = template.theme
    self.custom_footer = template[:custom_footer]
    self.custom_header = template[:custom_header]
    self.access = template.access
    self.fields_privacy = template.fields_privacy
    self.image = template.image.dup if template.image
    # flush
    self.save(validate: false)
  end

  def apply_type_specific_template(template)
  end

  xss_terminate only: [:name, :nickname, :address, :contact_phone, :description], on: :validation
  xss_terminate only: [:custom_footer, :custom_header], with: :white_list

  include SanitizeTags

  include WhiteListFilter
  filter_iframes :custom_header, :custom_footer
  def iframe_whitelist
    environment && environment.trusted_sites_for_iframe
  end

  # returns the contact email for this profile.
  #
  # Subclasses may -- and should -- override this method.
  def contact_email
    raise NotImplementedError
  end

  # This method must return a list of e-mail adresses to which notification messages must be sent.
  # The implementation in this class just delegates to +contact_email+. Subclasse may override this method.
  def notification_emails
    [contact_email]
  end

  def last_articles(limit = 10)
    self.articles.limit(limit).where(
      "advertise = ? AND published = ? AND
      ((articles.type != ? and articles.type != ? and articles.type != ?) OR
      articles.type is NULL)",
      true, true, "UploadedFile", "RssFeed", "Blog"
    ).order("articles.published_at desc, articles.id desc")
  end

  def to_liquid
    HashWithIndifferentAccess.new name: name, identifier: identifier
  end

  class << self
    # finds a profile by its identifier. This method is a shortcut to
    # +find_by_identifier+.
    #
    # Examples:
    #
    #  person = Profile['username']
    #  org = Profile.['orgname']
    def [](identifier)
      self.find_by identifier: identifier
    end
  end

  def superior_instance
    environment
  end

  # returns +false+
  def person?
    self.kind_of?(Person)
  end

  def enterprise?
    self.kind_of?(Enterprise)
  end

  def organization?
    self.kind_of?(Organization)
  end

  def community?
    self.kind_of?(Community)
  end

  # returns false.
  def is_validation_entity?
    false
  end

  def url
    @url ||= generate_url(controller: "content_viewer", action: "view_page", page: [])
  end

  def admin_url
    { profile: identifier, controller: "profile_editor", action: "index" }
  end

  def tasks_url
    { profile: identifier, controller: "tasks", action: "index", host: default_hostname }
  end

  def leave_url(reload = false)
    { profile: identifier, controller: "profile", action: "leave", reload: reload }
  end

  def join_url
    { profile: identifier, controller: "profile", action: "join" }
  end

  def join_not_logged_url
    { profile: identifier, controller: "profile", action: "join_not_logged" }
  end

  def check_membership_url
    { profile: identifier, controller: "profile", action: "check_membership" }
  end

  def add_url
    { profile: identifier, controller: "profile", action: "add" }
  end

  def check_friendship_url
    { profile: identifier, controller: "profile", action: "check_friendship" }
  end

  def public_profile_url
    generate_url(profile: identifier, controller: "profile", action: "index")
  end

  def people_suggestions_url
    generate_url(profile: identifier, controller: "friends", action: "suggest")
  end

  def communities_suggestions_url
    generate_url(profile: identifier, controller: "memberships", action: "suggest")
  end

  def generate_url(options)
    url_options.merge(options)
  end

  def url_options
    options = { host: default_hostname, profile: (own_hostname ? nil : self.identifier) }
    options.merge(Noosfero.url_options)
  end

  def top_url(scheme = "http")
    url = scheme + "://"
    url << url_options[:host]
    url << ":" << url_options[:port].to_s if url_options.key?(:port)
    url << Noosfero.root("")
    url.html_safe
  end

  private :generate_url, :url_options

  def default_hostname
    @default_hostname ||= (hostname || environment.default_hostname)
  end

  def hostname
    if preferred_domain
      return preferred_domain.name
    else
      own_hostname
    end
  end

  def own_hostname
    domain = self.domains.first
    domain ? domain.name : nil
  end

  def possible_domains
    environment.domains + domains
  end

  def article_tags
    articles.tag_counts.inject({}) do |memo, tag|
      memo[tag.name] = tag.count
      memo
    end
  end

  # Tells whether a specified profile has members or nor.
  #
  # On this class, returns <tt>false</tt> by default.
  def has_members?
    false
  end

  after_create :insert_default_article_set
  def insert_default_article_set
    if template
      self.save! if copy_articles_from template
    else
      default_set_of_articles.each do |article|
        article.profile = self
        article.advertise = false
        article.access = access
        article.save!
      end
      self.save!
    end
  end

  # Override this method in subclasses of Profile to create a default article
  # set upon creation. Note that this method will be called *only* if there is
  # no template for the type of profile (i.e. if the template was removed or in
  # the creation of the template itself).
  #
  # This method must return an array of pre-populated articles, which will be
  # associated to the profile before being saved. Example:
  #
  #   def default_set_of_articles
  #     [Blog.new(:name => 'Blog'), Gallery.new(:name => 'Gallery')]
  #   end
  #
  # By default, this method returns an empty array.
  def default_set_of_articles
    []
  end

  def copy_articles_from(other)
    return false if other.top_level_articles.empty?

    other.top_level_articles.each do |a|
      copy_article_tree a
    end
    self.articles.reload
    true
  end

  def copy_article_tree(article, parent = nil)
    return if !copy_article?(article)

    original_article = self.articles.find_by name: article.name
    if original_article
      num = 2
      new_name = original_article.name + " " + num.to_s
      while self.articles.find_by name: new_name
        num = num + 1
        new_name = original_article.name + " " + num.to_s
      end
      original_article.update!(name: new_name)
    end
    article_copy = article.copy(profile: self, parent: parent, advertise: false)
    if article.profile.home_page == article
      self.home_page = article_copy
    end
    article.children.each do |a|
      copy_article_tree a, article_copy
    end
  end

  def copy_article?(article)
    !article.is_a?(RssFeed) &&
      !(is_template && article.slug == "welcome-page")
  end

  # Adds a person as member of this Profile.
  def add_member(person, invited = false, **attributes)
    if self.has_members? && (!self.secret || invited)
      if self.closed? && members.count > 0
        AddMember.create!(person: person, organization: self) unless self.already_request_membership?(person)
      else
        self.affiliate(person, Profile::Roles.admin(environment.id), attributes) if members.count == 0
        self.affiliate(person, Profile::Roles.member(environment.id), attributes)
        plugins.dispatch(:member_added, self, person)
      end
      person.tasks.pending.of("InviteMember").select { |t| t.data[:community_id] == self.id }.each { |invite| invite.cancel }
      remove_from_suggestion_list person
    else
      raise _("%s can't have members") % self.class.name
    end
  end

  def remove_member(person)
    self.disaffiliate(person, Profile::Roles.all_roles(environment.id))
    plugins.dispatch(:member_removed, self, person)
  end

  # adds a person as administrator os this profile
  def add_admin(person)
    self.affiliate(person, Profile::Roles.admin(environment.id))
  end

  def remove_admin(person)
    self.disaffiliate(person, Profile::Roles.admin(environment.id))
  end

  def add_moderator(person)
    if self.has_members?
      self.affiliate(person, Profile::Roles.moderator(environment.id))
    else
      raise _("%s can't has moderators") % self.class.name
    end
  end

  after_save :update_category_from_region
  def update_category_from_region
    ProfileCategorization.remove_region(self)
    if region
      self.add_category(region)
    end
  end

  def accept_category?(cat)
    true
  end

  include ActionView::Helpers::TextHelper
  def short_name(chars = 40)
    if self[:nickname].blank?
      if chars
        truncate self.name, length: chars, omission: "..."
      else
        self.name
      end
    else
      self[:nickname]
    end
  end

  def custom_header
    self[:custom_header] || environment && environment.custom_header
  end

  def custom_header_expanded
    header = custom_header
    if header
      %w[name short_name].each do |att|
        if self.respond_to?(att) && header.include?("{#{att}}")
          header.gsub!("{#{att}}", self.send(att))
        end
      end
      header
    end
  end

  def custom_footer
    self[:custom_footer] || environment && environment.custom_footer
  end

  def custom_footer_expanded
    footer = custom_footer
    if footer
      %w[contact_person contact_email contact_phone location address district address_reference economic_activity city state country zip_code].each do |att|
        if self.respond_to?(att) && footer.match(/\{[^{]*#{att}\}/)
          if !self.send(att).nil? && !self.send(att).blank?
            footer = footer.gsub(/\{([^{]*)#{att}\}/, '\1' + self.send(att))
          else
            footer = footer.gsub(/\{[^}]*#{att}\}/, "")
          end
        end
      end
      footer
    end
  end

  def privacy_setting
    _("Profile accessible to %s") % Entitlement::Levels.label(access, self)
  end

  def themes
    Theme.find_by_owner(self)
  end

  def find_theme(the_id)
    themes.find { |item| item.id == the_id }
  end

  settings_items :layout_template, type: String, default: "default"

  has_many :blogs, source: "articles", class_name: "Blog"
  has_many :forums, source: "articles", class_name: "Forum"
  has_many :galleries, source: "articles", class_name: "Gallery"

  def blog
    self.has_blog? ? self.blogs.order(:id).first : nil
  end

  def has_blog?
    self.blogs.count.nonzero?
  end

  def forum
    self.has_forum? ? self.forums.order(:id).first : nil
  end

  def has_forum?
    self.forums.count.nonzero?
  end

  def gallery
    self.has_blog? ? self.galleries.order(:id).first : nil
  end

  def has_gallery?
    self.galleries.count.nonzero?
  end

  def admins
    return [] if environment.blank?

    admin_role = Profile::Roles.admin(environment.id)
    return [] if admin_role.blank?

    self.members_by_role(admin_role)
  end

  def enable_contact?
    !environment.enabled?("disable_contact_" + self.class.name.downcase)
  end

  include Noosfero::Plugin::HotSpot

  def folder_types
    types = Article.folder_types
    plugins.dispatch(:content_types).each { |type|
      if type < Folder
        types << type.name
      end
    }
    types
  end

  def folders
    articles.folders(self)
  end

  def image_galleries
    articles.galleries
  end

  def blocks_to_expire_cache
    []
  end

  def cache_keys(params = {})
    []
  end

  validate :image_valid

  def image_valid
    unless self.image.nil?
      self.image.valid?
      self.image.errors.delete(:empty) # dont validate here if exists uploaded data
      self.image.errors.each do |attr, msg|
        self.errors.add(attr, msg)
      end
    end
  end

  # FIXME: horrible workaround to circular dependency in environment.rb
  after_update do |profile|
    ProfileSweeper.new().after_update(profile)
  end

  # FIXME: horrible workaround to circular dependency in environment.rb
  after_create do |profile|
    ProfileSweeper.new().after_create(profile)
  end

  def update_header_and_footer(header, footer)
    self.custom_header = header
    self.custom_footer = footer
    self.save(validate: false)
  end

  def update_theme(theme)
    self.update_attribute(:theme, theme)
  end

  def update_layout_template(template)
    self.update_attribute(:layout_template, template)
  end

  def members_cache_key(params = {})
    page = params[:npage] || "1"
    sort = (params[:sort] == "desc") ? params[:sort] : "asc"
    cache_key + "-members-page-" + page + "-" + sort
  end

  def more_recent_label
    _("Since: ")
  end

  def recent_actions
    tracked_actions.recent
  end

  def recent_notifications
    tracked_notifications.recent
  end

  def more_active_label
    amount = recent_actions.count
    amount += recent_notifications.count if organization?
    {
      0 => _("no activity"),
      1 => _("one activity")
    }[amount] || _("%s activities") % amount
  end

  def more_popular_label
    amount = self.members_count
    {
      0 => _("no members"),
      1 => _("one member")
    }[amount] || _("%s members") % amount
  end

  include Noosfero::Gravatar

  def profile_custom_icon(gravatar_default = nil)
    image.public_filename(:icon) if image.present?
  end

  def profile_custom_image(size = :icon)
    image_path = profile_custom_icon if size == :icon
    image_path ||= image.public_filename(size) if image.present?
    image_path
  end

  def jid(options = {})
    domain = options[:domain] || environment.default_hostname
    "#{identifier}@#{domain}"
  end

  def full_jid(options = {})
    "#{jid(options)}/#{short_name}"
  end

  def is_on_homepage?(url, page = nil)
    if page
      page == self.home_page
    else
      url == "/" + self.identifier
    end
  end

  def opened_abuse_complaint
    abuse_complaints.opened.first
  end

  def disable
    self.visible = false
    self.save
  end

  def enable
    self.visible = true
    self.save
  end

  def self.identification
    name
  end

  def exclude_verbs_on_activities
    %w[]
  end

  # Customize in subclasses
  def activities
    self.profile_activities.includes(:activity).order("updated_at DESC")
  end

  def may_display_field_to?(field, user = nil)
    # display if it isn't a field that can be enabled
    return true if !self.class.fields.include?(field.to_s) &&
                   !self.active_fields.include?(field.to_s)

    self.public_fields.include?(field.to_s) ||
      (user.present? && (user == self || user.is_a_friend?(self)))
  end

  # field => privacy (e.g.: "address" => "public")
  def fields_privacy
    self.data[:fields_privacy] ||= {}
    custom_field_privacy = {}
    self.custom_field_values.includes(:custom_field).pluck("custom_fields.name", :public).to_h.map do |field, is_public|
      custom_field_privacy[field] = "public" if is_public
    end
    self.data[:fields_privacy].merge!(custom_field_privacy)

    self.data[:fields_privacy]
  end

  def custom_field_value(field_name)
    value = nil
    begin
      value = self.send(field_name)
    rescue NoMethodError
      value = self.custom_field_values.by_field(field_name).pluck(:value).first
    end
    value
  end

  def self.fields
    []
  end

  # abstract
  def active_fields
    []
  end

  def public_fields
    self.active_fields
  end

  def followed_by?(person)
    (person == self) || (person.is_member_of?(self)) || (person.in? self.followers)
  end

  def in_social_circle?(person)
    (person == self) || (person.is_member_of?(self))
  end

  validates_inclusion_of :redirection_after_login, in: Environment.login_redirection_options.keys, allow_nil: true
  def preferred_login_redirection
    redirection_after_login.blank? ? environment.redirection_after_login : redirection_after_login
  end
  settings_items :custom_url_redirection, type: String, default: nil

  def remove_from_suggestion_list(person)
    suggestion = person.suggested_profiles.find_by suggestion_id: self.id
    suggestion.disable if suggestion
  end

  def allow_invitation_from(person)
    false
  end

  def allow_post_content?(person = nil)
    person.kind_of?(Profile) && person.has_permission?("post_content", self)
  end

  def allow_post_scrap?(person = nil)
    if self.kind_of?(Person) && person.kind_of?(Person)
      self == person || self.is_a_friend?(person)
    else 
      person.kind_of?(Profile) && person.has_permission?("post_content", self)
    end
  end

  def allow_edit?(person = nil)
    person.kind_of?(Profile) && person.has_permission?("edit_profile", self)
  end

  def allow_destroy?(person = nil)
    person.kind_of?(Profile) && person.has_permission?("destroy_profile", self)
  end

  def allow_edit_design?(person = nil)
    person.kind_of?(Profile) && person.has_permission?("edit_profile_design", self)
  end

  def in_circle?(circle, follower)
    ProfileFollower.with_follower(follower).with_circle(circle).with_profile(self).present?
  end

  def available_blocks(person)
    blocks = [ArticleBlock, TagsCloudBlock, InterestTagsBlock, RecentDocumentsBlock, ProfileInfoBlock, LinkListBlock, MyNetworkBlock, FeedReaderBlock, ProfileImageBlock, LocationBlock, SlideshowBlock, ProfileSearchBlock, HighlightsBlock, MenuBlock]
    # block exclusive to profiles that have blog
    blocks << BlogArchivesBlock if self.has_blog?
    # block exclusive for environment admin
    blocks << RawHTMLBlock if person.present? && person.is_admin?(self.environment)
    blocks + plugins.dispatch(:extra_blocks, type: self.class)
  end

  def self.default_quota
    # In megabytes
    nil
  end

  def disk_usage
    self.files.sum("size")
  end

  def update_disk_usage!
    self.metadata["disk_usage"] = self.disk_usage
    self.save
  end

  def allow_single_file?
    self.metadata["allow_single_file"] == "1"
  end

  # FIXME make this test
  def boxes_with_blocks
    self.boxes.with_blocks
  end

  DEFAULT_EXPORTABLE_FIELDS = %w(id name)

  N_("id")
  N_("name")

  def exportable_fields
    plugin_extra_fields = plugins.dispatch(:extra_exportable_fields, self)
    fields = active_fields + DEFAULT_EXPORTABLE_FIELDS + plugin_extra_fields
    first_fields = %w(id name email)
    fields -= first_fields
    fields.sort!
    ordered_fields = first_fields + fields
  end

  def method_missing(method, *args, &block)
    if method.to_s =~ /^(.+)_captcha_requirement$/
      environment.send(method)
    else
      super
    end
  end

  def display_private_info_to?(person)
    person.present? && (person.is_admin? || (person == self))
  end

  private

    def super_upload_quota
      if kinds.present?
        # Returns 'unlimited' if one of the kinds has an unlimited quota.
        # Otherwise, returns the biggest quota
        quotas = kinds.map(&:upload_quota)
        (nil.in? quotas) ? nil : quotas.max
      else
        environment.quota_for(self.class)
      end
    end
end