Updated

app/models / signature.rb

F
782 lines of codes
104 methods
8.9 complexity/method
139 churn
929.64 complexity
278 duplications
require 'active_support/core_ext/digest/uuid' require 'postcode_sanitizer' require 'ipaddr' class Signature < ActiveRecord::Base
  1. Signature assumes too much for instance variable '@just_validated'
  2. Signature has no descriptive comment
  3. Signature has 7 constants
  4. Signature has at least 104 methods
include PerishableTokenGenerator include GeoipLookup has_perishable_token has_perishable_token called: 'signed_token' has_perishable_token called: 'unsubscribe_token' ISO8601_TIMESTAMP = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\z/ PENDING_STATE = 'pending' FRAUDULENT_STATE = 'fraudulent' VALIDATED_STATE = 'validated' INVALIDATED_STATE = 'invalidated' STATES = [ PENDING_STATE, FRAUDULENT_STATE, VALIDATED_STATE, INVALIDATED_STATE ] TIMESTAMPS = { 'government_response' => :government_response_email_at, 'debate_scheduled' => :debate_scheduled_email_at, 'debate_outcome' => :debate_outcome_email_at, 'petition_email' => :petition_email_at } belongs_to :petition belongs_to :invalidation validates :state, inclusion: { in: STATES } validates :name, presence: true, length: { maximum: 255 } validates :email, presence: true, email: { allow_blank: true }, on: :create validates :location_code, presence: true validates :postcode, presence: true, postcode: true, if: :united_kingdom? validates :postcode, length: { maximum: 255 }, allow_blank: true validates :uk_citizenship, acceptance: true, unless: :persisted?, allow_nil: false validates :constituency_id, length: { maximum: 255 } attr_readonly :sponsor, :creator before_create if: :email? do self.uuid = generate_uuid self.canonical_email = Domain.normalize(email) if find_duplicate raise ActiveRecord::RecordNotUnique, "Signature is not unique: #{name}, #{email}, #{postcode}" end if find_similar raise ActiveRecord::RecordNotUnique, "Signature is not unique: #{name}, #{email}, #{postcode}" end end before_destroy do !creator? end after_destroy do if validated? now = Time.current ConstituencyPetitionJournal.invalidate_signature_for(self, now) CountryPetitionJournal.invalidate_signature_for(self, now) petition.decrement_signature_count!(now) end end class << self def batch(id = 0, limit: 1000)
  1. Similar code found in 2 nodes Locations: 0 1
where(arel_table[:id].gt(id)).order(id: :asc).limit(limit) end def by_most_recent order(created_at: :desc) end def column_name_for(timestamp) TIMESTAMPS.fetch(timestamp) rescue KeyError => e
  1. Signature#column_name_for has the variable name 'e'
raise ArgumentError, "Unknown petition email timestamp: #{timestamp.inspect}" end def destroy!(signature_ids)
  1. Signature has missing safe method 'destroy!'
signatures = find(signature_ids) transaction do signatures.each do |signature| signature.destroy! end end end def duplicate(id, email) where(arel_table[:id].not_eq(id).and(arel_table[:email].eq(email))) end def duplicate_emails
  1. Signature::duplicate_emails has a flog score of 28
unscoped.from(validated.select(:uuid).group(:uuid).having(arel_table[Arel.star].count.gt(1))).count end def pending_rate (Rational(pending.count, total.count) * 100).to_d(2) end def similar(id, email) where(canonical_email: email).where.not(id: id) end def for_domain(domain) where("SUBSTRING(email FROM POSITION('@' IN email) + 1) = ?", domain[1..-1]) end def for_email(email) where("(REGEXP_REPLACE(LEFT(email, POSITION('@' IN email) - 1), '\\.|\\+.+', '', 'g') || SUBSTRING(email FROM POSITION('@' IN email)) = ?)", normalize_email(email)) end def for_invalidating where(state: [PENDING_STATE, VALIDATED_STATE]) end def for_ip(ip) where("inet(ip_address) <<= inet(?)", ip) end def for_name(name) where(arel_table[:name].lower.eq(name.downcase)) end def for_petition(id) where(petition_id: id) end def for_postcode(postcode) where(postcode: PostcodeSanitizer.call(postcode)) end def for_sector(postcode) where("LEFT(postcode, -3) = ?", PostcodeSanitizer.call(postcode)[0..-4]) end def for_timestamp(timestamp, since:)
  1. Identical code found in 2 nodes Locations: 0 1
column = arel_table[column_name_for(timestamp)] where(column.eq(nil).or(column.lt(since))) end def fraudulent where(state: FRAUDULENT_STATE) end def fraudulent_domains where(state: FRAUDULENT_STATE). select("SUBSTRING(email FROM POSITION('@' IN email) + 1) AS domain"). group("SUBSTRING(email FROM POSITION('@' IN email) + 1)"). order("COUNT(*) DESC"). count(:all) end def invalidate!(signature_ids, now = Time.current, invalidation_id = nil)
  1. Signature has missing safe method 'invalidate!'
signatures = find(signature_ids) transaction do signatures.each do |signature| signature.invalidate!(now, invalidation_id) end end end def invalidated where(state: INVALIDATED_STATE) end def missing_constituency_id(since: nil) if since uk.validated(since: since).where(constituency_id: nil) else uk.validated.where(constituency_id: nil) end end def need_emailing_for(timestamp, since:) validated.subscribed.for_timestamp(timestamp, since: since) end def pending where(state: PENDING_STATE) end def total where(state: [PENDING_STATE, VALIDATED_STATE]) end def petition_ids_signed_since(timestamp) validated(since: timestamp).distinct.pluck(:petition_id) end def search(query, options = {})
  1. Signature::search has a flog score of 78
  2. Signature#search has approx 20 statements
query = query.to_s state = options[:state] window = options[:window] page = [options[:page].to_i, 1].max scope = preload(:petition).by_most_recent if state.in?(STATES) scope = scope.where(state: state) end if window.present? if window =~ ISO8601_TIMESTAMP starts_at = window.in_time_zone.at_beginning_of_hour
  1. Identical code found in 2 nodes Locations: 0 1
ends_at = starts_at.advance(hours: 1) scope = scope.where(created_at: starts_at..ends_at)
  1. Signature#search calls 'scope.where(created_at: starts_at..ends_at)' 2 times Locations: 0 1
elsif window =~ /\A\d+\z/ starts_at = window.to_i.seconds.ago ends_at = Time.current scope = scope.where(created_at: starts_at..ends_at)
  1. Signature#search calls 'scope.where(created_at: starts_at..ends_at)' 2 times Locations: 0 1
end
  1. Identical code found in 2 nodes Locations: 0 1
end if ip_search?(query)
  1. Identical code found in 2 nodes Locations: 0 1
scope = scope.for_ip(query) elsif domain_search?(query) scope = scope.for_domain(query) elsif email_search?(query) scope = scope.for_email(query) elsif petition_search?(query) scope = scope.for_petition(query) elsif postcode_search?(query) scope = scope.for_postcode(query) elsif sector_search?(query) scope = scope.for_sector(query) else scope = scope.for_name(query) end scope.paginate(page: page, per_page: 50) end def creator where(arel_table[:creator].eq(true)) end def sponsors where(arel_table[:sponsor].eq(true)) end def subscribed where(notify_by_email: true) end def fraudulent_domains(since: 1.hour.ago, limit: 20)
  1. Signature::fraudulent_domains has a flog score of 39
  2. Signature takes parameters ['limit', 'since'] to 4 methods Locations: 0 1 2 3
select("SUBSTRING(email FROM POSITION('@' IN email) + 1) AS domain"). where(arel_table[:created_at].gt(since)). where(state: FRAUDULENT_STATE). group("SUBSTRING(email FROM POSITION('@' IN email) + 1)"). order("COUNT(*) DESC"). limit(limit). count(:all) end def fraudulent_ips(since: 1.hour.ago, limit: 20)
  1. Signature::fraudulent_ips has a flog score of 29
  2. Signature takes parameters ['limit', 'since'] to 4 methods Locations: 0 1 2 3
select(:ip_address). where(arel_table[:created_at].gt(since)). where(state: FRAUDULENT_STATE). group(:ip_address). order("COUNT(*) DESC"). limit(limit). count(:all) end def trending_domains(since: 1.hour.ago, limit: 20)
  1. Signature::trending_domains has a flog score of 37
  2. Signature takes parameters ['limit', 'since'] to 4 methods Locations: 0 1 2 3
select("SUBSTRING(email FROM POSITION('@' IN email) + 1) AS domain"). where(arel_table[:validated_at].gt(since)). where(arel_table[:invalidated_at].eq(nil)). group("SUBSTRING(email FROM POSITION('@' IN email) + 1)"). order("COUNT(*) DESC"). limit(limit). count(:all) end def trending_ips(since: 1.hour.ago, limit: 20)
  1. Signature::trending_ips has a flog score of 37
  2. Signature takes parameters ['limit', 'since'] to 4 methods Locations: 0 1 2 3
select(:ip_address). where(arel_table[:validated_at].gt(since)). where(arel_table[:invalidated_at].eq(nil)). group(:ip_address). order("COUNT(*) DESC"). limit(limit). count(:all) end def trending_domains_by_petition(window, threshold = 5)
  1. Signature::trending_domains_by_petition has a flog score of 34
trending_domains = Hash.new { |h, k| h[k] = {} }
  1. Signature#trending_domains_by_petition has the variable name 'h'
  2. Signature#trending_domains_by_petition has the variable name 'k'
domain = "SUBSTRING(email FROM POSITION('@' IN email) + 1)" where(validated_at: window) .group(:petition_id, domain) .having(count_star.gteq(threshold)) .order(:petition_id, count_star.desc) .pluck(:petition_id, domain, count_star.to_sql) .each_with_object(trending_domains) do |(petition_id, domain, count), hash| hash[petition_id][domain] = count end end def trending_ips_by_petition(window, threshold = 5, ignored_domains = [])
  1. Signature::trending_ips_by_petition has a flog score of 38
  2. Signature#trending_ips_by_petition has approx 7 statements
trending_ips = Hash.new { |h, k| h[k] = {} }
  1. Signature#trending_ips_by_petition has the variable name 'h'
  2. Signature#trending_ips_by_petition has the variable name 'k'
domain_not_in = "SUBSTRING(email FROM POSITION('@' IN email) + 1) NOT IN (?)" scope = where(validated_at: window) unless ignored_domains.empty? scope = scope.where(domain_not_in, ignored_domains) end scope .group(:petition_id, :ip_address) .having(count_star.gteq(threshold)) .order(:petition_id, count_star.desc) .pluck(:petition_id, :ip_address, count_star.to_sql) .each_with_object(trending_ips) do |(petition_id, ip_address, count), hash| hash[petition_id][ip_address] = count end end def uk where(location_code: "GB") end def unarchived where(archived_at: nil) end def subscribe!(signature_ids)
  1. Identical code found in 2 nodes Locations: 0 1
  2. Signature has missing safe method 'subscribe!'
signatures = find(signature_ids) transaction do signatures.each do |signature| signature.update!(notify_by_email: true) end end end def unsubscribe!(signature_ids)
  1. Identical code found in 2 nodes Locations: 0 1
  2. Signature has missing safe method 'unsubscribe!'
  3. Signature#unsubscribe! has approx 6 statements
signatures = find(signature_ids) transaction do signatures.each do |signature| if signature.creator? raise RuntimeError, "Can't unsubscribe the creator signature" elsif signature.pending? raise RuntimeError, "Can't unsubscribe a pending signature" else signature.update!(notify_by_email: false) end end end end def validate!(signature_ids, now = Time.current, force: false, request: nil)
  1. Signature#validate! has boolean parameter 'force'
  2. Signature#validate! has 4 parameters
  3. Signature has missing safe method 'validate!'
signatures = find(signature_ids) transaction do signatures.each do |signature| signature.validate!(now, force: force, request: request) end end end def validated(since: nil, upto: nil) scope = where(state: VALIDATED_STATE) scope = scope.where(validated_at.gt(since)) if since scope = scope.where(validated_at.lteq(upto)) if upto scope end def validated_count(timestamp, upto)
  1. Signature takes parameters ['timestamp', 'upto'] to 3 methods Locations: 0 1 2
validated(since: timestamp, upto: upto).pluck(count_star.to_sql).first end def validated_count_by_location_code(timestamp, upto)
  1. Signature takes parameters ['timestamp', 'upto'] to 3 methods Locations: 0 1 2
validated(since: timestamp, upto: upto).group(:location_code).pluck(:location_code, count_star.to_sql) end def validated_count_by_constituency_id(timestamp, upto)
  1. Signature takes parameters ['timestamp', 'upto'] to 3 methods Locations: 0 1 2
validated(since: timestamp, upto: upto).group(:constituency_id).pluck(:constituency_id, count_star.to_sql) end def validated?(id) where(id: id).where(validated_at.not_eq(nil)).exists? end private def ip_search?(query) IPAddr.new(query) rescue IPAddr::InvalidAddressError => e
  1. Signature#ip_search? has the variable name 'e'
false end def domain_search?(query) query.starts_with?('@') end def email_search?(query) query.include?('@') end def petition_search?(query) query =~ /\A\d+\z/ end def postcode_search?(query) PostcodeSanitizer.call(query) =~ PostcodeValidator::PATTERN end def sector_search?(query) PostcodeSanitizer.call(query) =~ /\A[A-Z]{1,2}[0-9][0-9A-Z]?XXX\z/ end def validated_at arel_table[:validated_at] end def count_star arel_table[Arel.star].count end def max_validated_at arel_table[:validated_at].maximum.to_sql end def normalize_email(email) "#{normalize_user(email)}@#{normalize_domain(email)}" end def normalize_user(email) email.split("@").first.split("+").first.tr(".", "").downcase end def normalize_domain(email) email.split("@").last.downcase end end attr_accessor :uk_citizenship
  1. Signature#uk_citizenship is a writable attribute
def find_duplicate
  1. Similar code found in 2 nodes Locations: 0 1
return nil unless petition signatures = petition.signatures.duplicate(id, email) return signatures.first if signatures.many?
  1. Signature#find_duplicate calls 'signatures.first' 2 times Locations: 0 1
if signature = signatures.first
  1. Signature#find_duplicate calls 'signatures.first' 2 times Locations: 0 1
if sanitized_name == signature.sanitized_name signature elsif postcode != signature.postcode signature end end end def find_duplicate! find_duplicate || find_similar || (raise ActiveRecord::RecordNotFound, "Signature not found: #{name}, #{email}, #{postcode}") end def find_similar
  1. Similar code found in 2 nodes Locations: 0 1
return nil unless petition signatures = petition.signatures.similar(id, canonical_email) return signatures.first if signatures.many?
  1. Signature#find_similar calls 'signatures.first' 2 times Locations: 0 1
if signature = signatures.first
  1. Signature#find_similar calls 'signatures.first' 2 times Locations: 0 1
if sanitized_name == signature.sanitized_name signature elsif postcode != signature.postcode signature end end end def name=(value) super(value.to_s.strip) end def email=(value) super(value.to_s.strip.downcase) end def postcode=(value) super(PostcodeSanitizer.call(value)) end def sanitized_name name.to_s.parameterize end def pending? state == PENDING_STATE end def fraudulent? state == FRAUDULENT_STATE end def validated? state == VALIDATED_STATE end def invalidated? state == INVALIDATED_STATE end def subscribed? validated? && !unsubscribed? end def unsubscribed? notify_by_email == false end def fraudulent!(now = Time.current) retry_lock do if pending? update_columns(state: FRAUDULENT_STATE, updated_at: now) end end end def validate!(now = Time.current, force: false, request: nil)
  1. Signature#validate! has a flog score of 44
  2. Signature#validate! has boolean parameter 'force'
  3. Signature has missing safe method 'validate!'
  4. Signature#validate! has approx 16 statements
update_signature_counts = false new_constituency_id = nil unless constituency_id? if united_kingdom? && postcode? new_constituency_id = constituency.try(:external_id) end end retry_lock do if force || pending?
  1. Signature#validate! is controlled by argument 'force'
update_signature_counts = true petition.validate_creator!(now) unless creator? attributes = { number: petition.signature_count + 1, state: VALIDATED_STATE, validated_at: now, invalidation_id: nil, invalidated_at: nil, updated_at: now } if request attributes[:validated_ip] = request.remote_ip end if new_constituency_id attributes[:constituency_id] = new_constituency_id end unless signed_token? attributes[:signed_token] = Authlogic::Random.friendly_token end update_columns(attributes) end end if update_signature_counts @just_validated = true end if inline_updates? && update_signature_counts last_signed_at = petition.last_signed_at petition.increment_signature_count!(now) ConstituencyPetitionJournal.increment_signature_counts_for(petition, last_signed_at) CountryPetitionJournal.increment_signature_counts_for(petition, last_signed_at) end end def just_validated? defined?(@just_validated) ? @just_validated : false end def validated_before?(timestamp) validated? && validated_at < timestamp end def reload(*) super.tap { @just_validated = false } end def invalidate!(now = Time.current, invalidation_id = nil)
  1. Signature has missing safe method 'invalidate!'
  2. Signature#invalidate! has approx 7 statements
update_signature_counts = false retry_lock do if validated? update_signature_counts = true end update_columns( state: INVALIDATED_STATE, notify_by_email: false, invalidation_id: invalidation_id, invalidated_at: now, updated_at: now ) end if update_signature_counts ConstituencyPetitionJournal.invalidate_signature_for(self, now) CountryPetitionJournal.invalidate_signature_for(self, now) petition.decrement_signature_count!(now) end end def mark_seen_signed_confirmation_page!
  1. Signature has missing safe method 'mark_seen_signed_confirmation_page!'
update seen_signed_confirmation_page: true end def save(*args) super rescue ActiveRecord::RecordNotUnique => e
  1. Signature#save has the variable name 'e'
if creator? errors.add(:name, :already_signed, name: name, email: email) and return false else raise e end end def unsubscribe!(token)
  1. Identical code found in 2 nodes Locations: 0 1
  2. Signature has missing safe method 'unsubscribe!'
if unsubscribed? errors.add(:base, "Already Unsubscribed") elsif unsubscribe_token != token
  1. Signature#unsubscribe! is controlled by argument 'token'
errors.add(:base, "Invalid Unsubscribe Token") else update(notify_by_email: false) end end def already_unsubscribed? errors[:base].include?("Already Unsubscribed") end def invalid_unsubscribe_token? errors[:base].include?("Invalid Unsubscribe Token") end def constituency if constituency_id? @constituency ||= Constituency.find_by_external_id(constituency_id) elsif united_kingdom? @constituency ||= Constituency.find_by_postcode(postcode) end end def signed_token super || generate_and_save_signed_token end def get_email_sent_at_for(timestamp) self[column_name_for(timestamp)] end def set_email_sent_at_for(timestamp, to: Time.current) update_column(column_name_for(timestamp), to) end def account Mail::Address.new(email).local rescue Mail::Field::ParseError nil end def domain Mail::Address.new(email).domain rescue Mail::Field::ParseError nil end def rate(window = 5.minutes) period = Range.new(created_at - window, created_at) petition.signatures.where(ip_address: ip_address, created_at: period).count end def update_uuid update_column(:uuid, generate_uuid) end def update_canonical_email update_column(:canonical_email, Domain.normalize(email)) end def number super || petition.signature_count + 1 end def email_threshold_reached? email_count >= 5 end def united_kingdom? location_code == 'GB' end alias_method :uk?, :united_kingdom? def update_all(updates) self.class.unscoped.where(id: id).update_all(updates) end def location if postcode? "#{formatted_postcode}, #{location_code}" else location_code end end def form_duration form_requested_at? ? created_at - form_requested_at : 0 end def form_token_reused? self.class.where(form_token: form_token).count > 1 end private def formatted_postcode if united_kingdom? postcode.gsub(/\A([A-Z0-9]+?)([A-Z0-9]{3})\z/, "\\1 \\2") else postcode end end def inline_updates?
  1. Signature#inline_updates? doesn't depend on instance state (maybe move it to another class?)
ENV["INLINE_UPDATES"] == "true" end def generate_uuid Digest::UUID.uuid_v5(Digest::UUID::URL_NAMESPACE, "mailto:#{email}") end def generate_and_save_signed_token token = Authlogic::Random.friendly_token retry_lock do if signed_token? token = read_attribute(:signed_token) else update_column(:signed_token, token) end end token end def column_name_for(timestamp) self.class.column_name_for(timestamp) end def retry_lock
  1. Identical code found in 2 nodes Locations: 0 1
  2. Signature#retry_lock has approx 8 statements
retried = false begin with_lock { yield } rescue PG::InFailedSqlTransaction => e
  1. Signature#retry_lock has the variable name 'e'
if retried raise e else retried = true self.class.connection.clear_cache! retry end end end end