0

My application defines a list of user's preferred activities as an array of strings stored in a Postgres character varying array field.

The field is defined in Postgres DDL as:

preferred_activities character varying[] COLLATE pg_catalog."default" DEFAULT '{}'::character varying[]

The field is initialised by an API which receives a comma separated list of values (userActivities ) such as "CLIMB,RUN,212" with the following instruction:

@user.preferred_activities = userActivities.split(',')

This results in the expected list stored as {CLIMB,RUN,212} in Postgres field and displayed as expected in the view: CLIMB,RUN,212

As the field can also be manually edited, it also appears in user's form where it is initialised with a consistent format:

<%= f.text_field :preferred_activities, value: f.object.preferred_activities.join(',') %>

which provides the expected field value attribute "CLIMB,RUN,212" (displayed as CLIMB,RUN,212).

And then formatted before user's record update:

@user.preferred_activities = user_params[:preferred_activities].split(',')

which produces the following array: ["CLIM", "RUN", "212"]

This update should be straight forward, but for a reason I don't understand, the following SQL instruction is generated:

  User Update (0.9ms)  UPDATE "users" SET "preferred_activities" = $1, "updated_at" = $2 WHERE "users"."id" = $3  
  [["preferred_activities", "{LIM,\"RUN\"}"], ["updated_at", "2022-03-17 12:26:25.195321"], ["id", 1]]

The first letter and the last word have disappeared!

I did try other syntaxes for the instruction, which lead to the same result:

@user.preferred_activities = user_params[:preferred_activities]
@user.preferred_activities = "{#{user_params[:preferred_activities]}}"

Do you see any explanation for this behaviour and any solution to properly update this character varying array field?

User model:

class User < ApplicationRecord
  extend CsvHelper

  # Audit trail setup
  audited except: [:encrypted_password,  :reset_password_token,  :reset_password_sent_at,  :remember_created_at,
                    :sign_in_count, :current_sign_in_at, :last_sign_in_at, :current_sign_in_ip, :last_sign_in_ip,
                    :confirmation_token, :confirmed_at, :confirmation_sent_at, :unconfirmed_email,
                    :failed_attempts, :unlock_token, :locked_at]

  #validates_with EmailAddress::ActiveRecordValidator, field: :email

  # Virtual attribute for authenticating by either username or email
  # This is in addition to a real persisted field like 'username'
  attr_accessor :login

  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :recoverable, :rememberable,
            :trackable, :confirmable, :lockable, :password_archivable, :registerable,
            :omniauthable, omniauth_providers: %i[keycloakopenid]#, :secure_validatable

  before_save :email_format
  before_save :name_update

### validations
  # validates :current_playground_id, presence: true
  validates :email, :presence => true, uniqueness: {scope: :playground_id, case_sensitive: false}, length: { maximum: 100 }
  #validates_format_of :email, with: /\A(\S+)@(.+)\.(\S+)\z/
  validate :password_confirmed
  validate :password_complexity

  validates :first_name,    presence: true, length: { maximum: 100 }
  validates :last_name,     presence: true, length: { maximum: 100 }
  validates :user_name,     presence: true, uniqueness: {scope: :playground_id, case_sensitive: false}, length: { maximum: 100 }
  validates :external_directory_id,         length: { maximum: 100 }
  validates :created_by,    presence: true, length: { maximum: 30 }
  validates :updated_by,    presence: true, length: { maximum: 30 }
  #validate :member_of_Everyone_group
  validates :organisation,  presence: true
  belongs_to :organisation
  belongs_to :parent, :class_name => "Playground", :foreign_key => "playground_id"
  belongs_to :owner, :class_name => "User", :foreign_key => "owner_id"

# Relations
  has_and_belongs_to_many :groups
  has_many :groups_users

  ### Translation support
  mattr_accessor :translated_fields, default: ['description']
    has_many :translations, as: :document
  has_many :description_translations, -> { where(field_name: 'description') }, class_name: 'Translation', as: :document
  accepts_nested_attributes_for :translations, :reject_if => :all_blank, :allow_destroy => true
  accepts_nested_attributes_for :description_translations, :reject_if => :all_blank, :allow_destroy => true

### Public functions
  def activity_status
    if self.is_active
      if not self.locked_at.nil?
        "locked"
      else
        if self.confirmed_at.nil?
          "Unconfirmed"
        else
          if self.sign_in_count == 0
            "Confirmed"
          else
            "Active"
          end
        end
      end
    else
      "Inactive"
    end
  end

  def principal_group
    self.groups_users.find_by(is_principal: true).group_id
  end

  def password_complexity
    # Regexp extracted from https://stackoverflow.com/questions/19605150/regex-for-password-must-contain-at-least-eight-characters-at-least-one-number-a
    return if password.blank? || password =~ /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,70}$/

    errors.add :password, 'Complexity requirement not met. Length should be 8-70 characters and include: 1 uppercase, 1 lowercase, 1 digit and 1 special character'
  end

  def password_confirmed
    return if password == password_confirmation

    errors.add :password, 'Password and confirmation do not match'
  end

  ### full-text local search
  pg_search_scope :search_by_user_name, against: [:user_name, :name, :description],
    using: { tsearch: { prefix: true, negation: true } }

  def self.search(criteria)
    if criteria.present?
      search_by_user_name(criteria)
    else
      # No query? Return all records, sorted by hierarchy.
      order( :updated_at )
    end
  end

  # Allow user creation when a new one comes through OmniAuth
  def self.from_omniauth(auth)
    where(provider: auth.provider, email: auth.info.email).first_or_create do |user|
      user.login = auth.info.email
      user.uuid = auth.uid
      user.provider = auth.provider
      user.email = auth.info.email
      user.password = "Odq!1#{Devise.friendly_token[0, 20]}"
      user.password_confirmation = user.password
      user.first_name = auth.info.first_name
      user.last_name = auth.info.last_name
      user.name = auth.info.name   # assuming the user model has a name
      user.user_name = auth.info.email[0, 32]
      user.playground_id = 0
      user.current_playground_id = 0
      user.organisation_id = Parameter.find_by_code("ORGANISATION_ID").property.to_i || 0
      user.language = 'fr_OFS'
      user.description = 'Created through OmniAuth'
      user.active_from = Time.now
      user.active_to = Time.now + 3.years
      user.is_admin = false
      user.is_active = true
      user.owner_id = 1
      user.created_by = 'OmniAuth'
      user.created_at = Time.now
      user.updated_by = 'OmniAuth'
      user.updated_at = Time.now
      user.confirmed_at = Time.now
      # If you are using confirmable and the provider(s) you use validate emails,
      # uncomment the line below to skip the confirmation emails.
      user.skip_confirmation!
    end
  end

### private functions definitions
  private

  ### before filters

  def email_format
    self.email = email.downcase
  end

  def name_update
    self.name = "#{first_name} #{last_name}"
   end

  def self.find_for_database_authentication(warden_conditions)
    conditions = warden_conditions.dup
    if login = conditions.delete(:login)
      where(conditions.to_h).where(["lower(user_name) = :value OR lower(email) = :value", { :value => login.downcase }]).first
    elsif conditions.has_key?(:user_name) || conditions.has_key?(:email)
      where(conditions.to_h).first
    end
  end

  def member_of_Everyone_group
    errors.add(:base, :EveryoneMembershipMissing) unless group_ids.include? 0
  end

end
user1185081
  • 1,510
  • 1
  • 16
  • 40

0 Answers0