app/models/provider.rb
# frozen_string_literal: true
require "countries"
class Provider < ApplicationRecord
audited except: %i[
globus_uuid
system_email
group_email
technical_contact
secondary_technical_contact
service_contact
secondary_service_contact
billing_contact
secondary_billing_contact
voting_contact
billing_information
salesforce_id
password
updated
experiments
comments
logo
version
doi_quota_allowed
doi_quota_used
doi_estimate
]
# include helper module for caching infrequently changing resources
include Cacheable
# include helper module for managing associated users
include Userable
# include helper module for setting password
include Passwordable
# include helper module for authentication
include Authenticable
# include helper module for Elasticsearch
include Indexable
# include helper module for sending emails
include Mailable
include Elasticsearch::Model
has_attached_file :logo,
styles: { medium: ["500x200", :png] },
default_style: :medium,
default_url: "/images/members/default.png"
validates_attachment :logo,
content_type: {
content_type: %w[image/jpg image/jpeg image/png],
}
# define table and attribute names
# uid is used as unique identifier, mapped to id in serializer
self.table_name = "allocator"
alias_attribute :flipper_id, :symbol
alias_attribute :created_at, :created
alias_attribute :updated_at, :updated
attr_readonly :symbol
attr_reader :from_salesforce
delegate :salesforce_id, to: :consortium, prefix: true, allow_nil: true
validates_presence_of :symbol, :name, :display_name, :system_email
validates_uniqueness_of :symbol, message: "This name has already been taken"
validates_format_of :symbol,
with: /\A([A-Z]+)\Z/,
message: "should only contain capital letters"
validates_length_of :symbol, minimum: 2, maximum: 8
validates_format_of :system_email,
with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i,
message: "system_email should be an email"
validates_format_of :group_email,
with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i,
if: :group_email?,
message: "group_email should be an email"
validates_format_of :website,
with: %r{https?://\S+},
if: :website?,
message: "Website should be a url"
validates_format_of :salesforce_id,
with: /[a-zA-Z0-9]{18}/,
message: "wrong format for salesforce id",
if: :salesforce_id?
validates_inclusion_of :role_name,
in: %w[
ROLE_FOR_PROFIT_PROVIDER
ROLE_CONTRACTUAL_PROVIDER
ROLE_CONSORTIUM
ROLE_CONSORTIUM_ORGANIZATION
ROLE_ALLOCATOR
ROLE_MEMBER
ROLE_ADMIN
ROLE_DEV
],
message: "Role %s is not included in the list"
validates_inclusion_of :organization_type,
in: %w[
researchInstitution
academicInstitution
governmentAgency
nationalInstitution
professionalSociety
publisher
serviceProvider
internationalOrganization
other
],
message:
"organization type %s is not included in the list",
if: :organization_type?
validates_inclusion_of :non_profit_status,
in: %w[non-profit for-profit],
message:
"non-profit status '%s' is not included in the list"
validates_inclusion_of :focus_area,
in: %w[
naturalSciences
engineeringAndTechnology
medicalAndHealthSciences
agriculturalSciences
socialSciences
humanities
general
],
message: "focus area %s is not included in the list",
if: :focus_area?
validate :freeze_symbol, on: :update
validate :can_be_in_consortium
validate :uuid_format, if: :globus_uuid?
validates_format_of :ror_id,
with: %r{\Ahttps://ror\.org/0\w{6}\d{2}\z},
if: :ror_id?,
message: "ROR ID should be a url"
validates_format_of :twitter_handle,
with: /\A@[a-zA-Z0-9_]{1,15}\z/, if: :twitter_handle?
validates_attachment_content_type :logo, content_type: /\Aimage/
# validates :technical_contact, contact: true
# validates :billing_contact, contact: true
# validates :secondary_billing_contact, contact: true
# validates :service_contact, contact: true
# validates :voting_contact, contact: true
# validates :billing_information, billing_information: true
validates :doi_estimate, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
strip_attributes
has_many :clients, foreign_key: :allocator
has_many :dois, through: :clients
has_many :provider_prefixes, dependent: :destroy
has_many :prefixes, through: :provider_prefixes
has_many :consortium_organizations,
class_name: "Provider",
primary_key: "symbol",
foreign_key: "consortium_id",
inverse_of: :consortium
belongs_to :consortium,
class_name: "Provider",
primary_key: "symbol",
inverse_of: :consortium_organizations,
optional: true
has_many :activities, as: :auditable, dependent: :destroy
has_many :contacts
# TODO get information from contact model instead storing in provider model
# has_one :voting_contact, ->(contact) { where("'voting' in ?", Array.wrap(contact.role_name)) }
# has_one :service_contact, ->(contact) { where("'service' in ?", Array.wrap(contact.role_name)) }
# has_one :billing_contact, ->(contact) { where("'billing' in ?", Array.wrap(contact.role_name)) }
before_validation :set_region, :set_defaults
before_create { self.created = Time.zone.now.utc.iso8601 }
before_save { self.updated = Time.zone.now.utc.iso8601 }
accepts_nested_attributes_for :prefixes
# use different index for testing
if Rails.env.test?
index_name "providers-test#{ENV['TEST_ENV_NUMBER']}"
elsif ENV["ES_PREFIX"].present?
index_name "providers-#{ENV['ES_PREFIX']}"
else
index_name "providers"
end
settings index: {
analysis: {
analyzer: {
string_lowercase: {
tokenizer: "keyword", filter: %w[lowercase ascii_folding]
},
},
normalizer: {
keyword_lowercase: { type: "custom", filter: %w[lowercase] },
},
filter: {
ascii_folding: {
type: "asciifolding", preserve_original: true
},
},
},
} do
mapping dynamic: "false" do
indexes :id, type: :keyword
indexes :uid, type: :keyword, normalizer: "keyword_lowercase"
indexes :symbol, type: :keyword
indexes :globus_uuid, type: :keyword
indexes :client_ids, type: :keyword
indexes :prefix_ids, type: :keyword
indexes :contact_ids, type: :keyword
indexes :name,
type: :text,
fields: {
keyword: { type: "keyword" },
raw: {
type: "text",
"analyzer": "string_lowercase",
"fielddata": true,
},
}
indexes :display_name,
type: :text,
fields: {
keyword: { type: "keyword" },
raw: {
type: "text",
"analyzer": "string_lowercase",
"fielddata": true,
},
}
indexes :system_email,
type: :text, fields: { keyword: { type: "keyword" } }
indexes :group_email,
type: :text, fields: { keyword: { type: "keyword" } }
indexes :version, type: :integer
indexes :is_active, type: :keyword
indexes :year, type: :integer
indexes :description, type: :text
indexes :website, type: :text, fields: { keyword: { type: "keyword" } }
indexes :logo_url, type: :text
indexes :image, type: :text
indexes :region, type: :keyword
indexes :focus_area, type: :keyword
indexes :organization_type, type: :keyword
indexes :member_type, type: :keyword
indexes :non_profit_status, type: :keyword
indexes :consortium_id,
type: :text,
fields: {
keyword: { type: "keyword" },
raw: {
type: "text",
"analyzer": "string_lowercase",
"fielddata": true,
},
}
indexes :consortium_organization_ids, type: :keyword
indexes :country_code, type: :keyword
indexes :role_name, type: :keyword
indexes :cache_key, type: :keyword
indexes :joined, type: :date
indexes :twitter_handle, type: :keyword
indexes :ror_id, type: :keyword
indexes :salesforce_id, type: :keyword
indexes :billing_information,
type: :object,
properties: {
postCode: { type: :keyword },
state: { type: :text },
organization: { type: :text },
department: { type: :text },
city: { type: :text },
country: { type: :keyword },
address: { type: :text },
}
indexes :technical_contact,
type: :object,
properties: {
uid: { type: :keyword },
email: { type: :text },
given_name: { type: :text },
family_name: { type: :text },
name: { type: :text },
}
indexes :secondary_technical_contact,
type: :object,
properties: {
uid: { type: :keyword },
email: { type: :text },
given_name: { type: :text },
family_name: { type: :text },
name: { type: :text },
}
indexes :billing_contact,
type: :object,
properties: {
uid: { type: :keyword },
email: { type: :text },
given_name: { type: :text },
family_name: { type: :text },
name: { type: :text },
}
indexes :secondary_billing_contact,
type: :object,
properties: {
uid: { type: :keyword },
email: { type: :text },
given_name: { type: :text },
family_name: { type: :text },
name: { type: :text },
}
indexes :service_contact,
type: :object,
properties: {
uid: { type: :keyword },
email: { type: :text },
given_name: { type: :text },
family_name: { type: :text },
name: { type: :text },
}
indexes :secondary_service_contact,
type: :object,
properties: {
uid: { type: :keyword },
email: { type: :text },
given_name: { type: :text },
family_name: { type: :text },
name: { type: :text },
}
indexes :voting_contact,
type: :object,
properties: {
uid: { type: :keyword },
email: { type: :text },
given_name: { type: :text },
family_name: { type: :text },
name: { type: :text },
}
indexes :has_required_contacts, type: :boolean
indexes :created, type: :date
indexes :updated, type: :date
indexes :deleted_at, type: :date
indexes :cumulative_years, type: :integer, index: "false"
indexes :consortium, type: :object
indexes :consortium_organizations, type: :object
indexes :contacts, type: :object, properties: {
id: { type: :keyword },
uid: { type: :keyword, normalizer: "keyword_lowercase" },
provider_id: { type: :keyword, normalizer: "keyword_lowercase" },
consortium_id: { type: :keyword, normalizer: "keyword_lowercase" },
given_name: { type: :keyword, normalizer: "keyword_lowercase" },
family_name: { type: :keyword, normalizer: "keyword_lowercase" },
name: { type: :keyword, normalizer: "keyword_lowercase" },
email: { type: :keyword, normalizer: "keyword_lowercase" },
role_name: { type: :keyword, normalizer: "keyword_lowercase" },
created_at: { type: :date },
updated_at: { type: :date },
deleted_at: { type: :date }
}
indexes :doi_estimate, type: :integer
end
end
# also index id as workaround for finding the correct key in associations
def as_indexed_json(options = {})
{
"id" => uid,
"uid" => uid,
"name" => name,
"display_name" => display_name,
"client_ids" => options[:exclude_associations] ? nil : client_ids,
"prefix_ids" => options[:exclude_associations] ? nil : prefix_ids,
"contact_ids" => options[:exclude_associations] ? nil : contact_ids,
"symbol" => symbol,
"year" => year,
"system_email" => system_email,
"group_email" => group_email,
"is_active" => is_active,
"description" => description,
"website" => website,
"region" => region,
"country_code" => country_code,
"logo_url" => logo_url,
"focus_area" => focus_area,
"organization_type" => organization_type,
"member_type" => member_type,
"non_profit_status" => non_profit_status,
"consortium_id" => consortium_id,
"consortium_organization_ids" =>
options[:exclude_associations] ? nil : consortium_organization_ids,
"role_name" => role_name,
"password" => password,
"cache_key" => cache_key,
"joined" => joined.try(:iso8601),
"twitter_handle" => twitter_handle,
"ror_id" => ror_id,
"salesforce_id" => salesforce_id,
"globus_uuid" => globus_uuid,
"billing_information" => {
"address" => billing_address,
"organization" => billing_organization,
"department" => billing_department,
"postCode" => billing_post_code,
"state" => billing_state,
"country" => billing_country,
"city" => billing_city,
},
"technical_contact" => technical_contact,
"secondary_technical_contact" => secondary_technical_contact,
"billing_contact" => billing_contact,
"secondary_billing_contact" => secondary_billing_contact,
"service_contact" => service_contact,
"secondary_service_contact" => secondary_service_contact,
"voting_contact" => voting_contact,
"has_required_contacts" => has_required_contacts,
"created" => created.try(:iso8601),
"updated" => updated.try(:iso8601),
"deleted_at" => deleted_at.try(:iso8601),
"cumulative_years" => cumulative_years,
"consortium" => consortium.try(:as_indexed_json),
"contacts" =>
if options[:exclude_associations]
nil
else
contacts.map { |m| m.try(:as_indexed_json, exclude_associations: true) }
end,
"doi_estimate" => doi_estimate
}
end
def self.query_fields
%w[uid^10 symbol^10 name^5 system_email^5 group_email^5 _all]
end
def self.query_aggregations
{
years: {
date_histogram: {
field: "created",
interval: "year",
format: "year",
order: { _key: "desc" },
min_doc_count: 1,
},
aggs: { bucket_truncate: { bucket_sort: { size: 10 } } },
},
cumulative_years: {
terms: {
field: "cumulative_years",
size: 20,
min_doc_count: 1,
order: { _count: "asc" },
},
},
regions: { terms: { field: "region", size: 10, min_doc_count: 1 } },
member_types: {
terms: { field: "member_type", size: 10, min_doc_count: 1 },
},
organization_types: {
terms: { field: "organization_type", size: 10, min_doc_count: 1 },
},
focus_areas: {
terms: { field: "focus_area", size: 10, min_doc_count: 1 },
},
non_profit_statuses: {
terms: { field: "non_profit_status", size: 10, min_doc_count: 1 },
},
has_required_contacts: {
terms: { field: "has_required_contacts", size: 2, min_doc_count: 1 },
},
}
end
def csv
provider = {
name: name,
provider_id: symbol,
consortium_id: consortium.present? ? consortium.symbol : "",
salesforce_id: salesforce_id,
consortium_salesforce_id:
consortium.present? ? consortium.salesforce_id : "",
role_name: role_name,
is_active: is_active == "\x01",
description: description,
website: website,
region: region_human_name,
country: country_code,
logo_url: logo_url,
focus_area: focus_area,
organization_type: organization_type,
member_type: member_type_label,
system_email: system_email,
group_email: group_email,
technical_contact_email: technical_contact_email,
technical_contact_given_name: technical_contact_given_name,
technical_contact_family_name: technical_contact_family_name,
secondary_technical_contact_email: secondary_technical_contact_email,
secondary_technical_contact_given_name:
secondary_technical_contact_given_name,
secondary_technical_contact_family_name:
secondary_technical_contact_family_name,
service_contact_email: service_contact_email,
service_contact_given_name: service_contact_given_name,
service_contact_family_name: service_contact_family_name,
secondary_service_contact_email: secondary_service_contact_email,
secondary_service_contact_given_name:
secondary_service_contact_given_name,
secondary_service_contact_family_name:
secondary_service_contact_family_name,
voting_contact_email: voting_contact_email,
voting_contact_given_name: voting_contact_given_name,
voting_contact_family_name: voting_contact_family_name,
billing_contact_email: billing_contact_email,
billing_contact_given_name: billing_contact_given_name,
billing_contact_family_name: billing_contact_family_name,
secondary_billing_contact_email: secondary_billing_contact_email,
secondary_billing_contact_given_name:
secondary_billing_contact_given_name,
secondary_billing_contact_family_name:
secondary_billing_contact_family_name,
billing_address: billing_address,
billing_post_code: billing_post_code,
billing_city: billing_city,
billing_department: billing_department,
billing_organization: billing_organization,
billing_state: billing_state,
billing_country: billing_country,
twitter_handle: twitter_handle,
ror_id: ror_id,
created: created,
updated: updated,
deleted_at: deleted_at,
doi_estimate: doi_estimate,
}.values
CSV.generate { |csv| csv << provider }
end
def uid
symbol.downcase
end
def consortium_uid
consortium_id.downcase
end
def from_salesforce=(value)
@from_salesforce = (value.to_s == "true")
end
def consortium_organization_ids
if consortium_organizations.present?
consortium_organizations.pluck(:symbol).map(&:downcase)
end
end
def cache_key
"providers/#{uid}-#{updated.iso8601}"
end
def year
joined.year if joined.present?
end
def technical_contact_email
technical_contact.fetch("email", nil) if technical_contact.present?
end
def technical_contact_given_name
technical_contact.fetch("given_name", nil) if technical_contact.present?
end
def technical_contact_family_name
technical_contact.fetch("family_name", nil) if technical_contact.present?
end
def secondary_technical_contact_email
if secondary_technical_contact.present?
secondary_technical_contact.fetch("email", nil)
end
end
def secondary_technical_contact_given_name
if secondary_technical_contact.present?
secondary_technical_contact.fetch("given_name", nil)
end
end
def secondary_technical_contact_family_name
if secondary_technical_contact.present?
secondary_technical_contact.fetch("family_name", nil)
end
end
def service_contact_email
service_contact.fetch("email", nil) if service_contact.present?
end
def service_contact_given_name
service_contact.fetch("given_name", nil) if service_contact.present?
end
def service_contact_family_name
service_contact.fetch("family_name", nil) if service_contact.present?
end
def secondary_service_contact_email
if secondary_service_contact.present?
secondary_service_contact.fetch("email", nil)
end
end
def secondary_service_contact_given_name
if secondary_service_contact.present?
secondary_service_contact.fetch("given_name", nil)
end
end
def secondary_service_contact_family_name
if secondary_service_contact.present?
secondary_service_contact.fetch("family_name", nil)
end
end
def voting_contact_email
voting_contact.fetch("email", nil) if voting_contact.present?
end
def voting_contact_given_name
voting_contact.fetch("given_name", nil) if voting_contact.present?
end
def voting_contact_family_name
voting_contact.fetch("family_name", nil) if voting_contact.present?
end
def has_required_contacts
if member_type == "consortium_organization"
service_contact_email.present?
else
voting_contact_email.present? && service_contact_email.present? && billing_contact_email.present?
end
end
def billing_department
billing_information.fetch("department", nil) if billing_information.present?
end
def billing_organization
if billing_information.present?
billing_information.fetch("organization", nil)
end
end
def billing_address
billing_information.fetch("address", nil) if billing_information.present?
end
def billing_state
billing_information.fetch("state", nil) if billing_information.present?
end
def billing_city
billing_information.fetch("city", nil) if billing_information.present?
end
def billing_post_code
billing_information.fetch("post_code", nil) if billing_information.present?
end
def billing_country
billing_information.fetch("country", nil) if billing_information.present?
end
def billing_contact_email
billing_contact.fetch("email", nil) if billing_contact.present?
end
def billing_contact_given_name
billing_contact.fetch("given_name", nil) if billing_contact.present?
end
def billing_contact_family_name
billing_contact.fetch("family_name", nil) if billing_contact.present?
end
def secondary_billing_contact_email
if secondary_billing_contact.present?
secondary_billing_contact.fetch("email", nil)
end
end
def secondary_billing_contact_given_name
if secondary_billing_contact.present?
secondary_billing_contact.fetch("given_name", nil)
end
end
def secondary_billing_contact_family_name
if secondary_billing_contact.present?
secondary_billing_contact.fetch("family_name", nil)
end
end
def member_type_label
member_type_labels[role_name]
end
def member_type_labels
{
"ROLE_MEMBER" => "Member Only",
"ROLE_ALLOCATOR" => "Direct Member",
"ROLE_CONSORTIUM" => "Consortium",
"ROLE_CONSORTIUM_ORGANIZATION" => "Consortium Organization",
"ROLE_CONTRACTUAL_PROVIDER" => "Contractual Member",
"ROLE_ADMIN" => "DataCite Admin",
"ROLE_DEV" => "Developer",
"ROLE_FOR_PROFIT_PROVIDER" => "Direct Member",
}
end
def member_type
member_types[role_name]
end
def member_type=(value)
role_name = member_types.invert.fetch(value, nil)
write_attribute(:role_name, role_name) if role_name.present?
end
def member_types
{
"ROLE_MEMBER" => "member_only",
"ROLE_ALLOCATOR" => "direct_member",
"ROLE_CONSORTIUM" => "consortium",
"ROLE_CONSORTIUM_ORGANIZATION" => "consortium_organization",
"ROLE_CONTRACTUAL_PROVIDER" => "contractual_member",
"ROLE_FOR_PROFIT_PROVIDER" => "for-profit_provider",
"ROLE_DEV" => "developer",
}
end
# count years account has been active. Ignore if deleted the same year as created
def cumulative_years
if deleted_at && deleted_at.year > created_at.year
(created_at.year...deleted_at.year).to_a
elsif deleted_at
[]
else
(created_at.year..Date.today.year).to_a
end
end
# def country=(value)
# write_attribute(:country_code, value["code"]) if value.present?
# end
def country_name
ISO3166::Country[country_code].name if country_code.present?
end
def billing_country_name
ISO3166::Country[billing_country].try(:name) if billing_country.present?
end
def set_region
r = ISO3166::Country[country_code].world_region if country_code.present?
write_attribute(:region, r)
end
def regions
{ "AMER" => "Americas", "APAC" => "Asia Pacific", "EMEA" => "EMEA" }
end
def region_human_name
regions[region]
end
def logo_url
logo.url(:medium) if logo.present?
end
def password_input=(value)
write_attribute(:password, encrypt_password_sha256(value)) if value.present?
end
def client_ids
clients.where(deleted_at: nil).pluck(:symbol).map(&:downcase)
end
def prefix_ids
prefixes.pluck(:uid)
end
def contact_ids
contacts.where(deleted_at: nil).pluck(:uid)
end
def can_be_in_consortium
if consortium_id && member_type != "consortium_organization"
errors.add(
:consortium_id,
"The provider must be of member_type consortium_organization",
)
elsif consortium_id && consortium.member_type != "consortium"
errors.add(
:consortium_id,
"The consortium must be of member_type consortium",
)
end
end
def uuid_format
unless UUID.validate(globus_uuid)
errors.add(:globus_uuid, "#{globus_uuid} is not a valid UUID")
end
end
def freeze_symbol
errors.add(:symbol, "cannot be changed") if symbol_changed?
end
def user_url
ENV["VOLPINO_URL"] + "/users?provider-id=" + symbol.downcase
end
def activity_id_not_changed
if activity_id_changed? && self.persisted?
errors.add(:activity_id, "Change of activity_id not allowed!")
end
end
# attributes to be sent to elasticsearch index
def to_jsonapi
attributes = {
"symbol" => symbol,
"salesforce_id" => salesforce_id,
"consortium_salesforce_id" => consortium_salesforce_id,
"parent_organization" => consortium_id.present? ? consortium_id.upcase : nil,
"name" => name,
"website" => website,
"system_email" => system_email,
"group_email" => group_email,
"description" => description,
"region" => region,
"logo_url" => logo_url,
"non_profit_status" => non_profit_status,
"focus_area" => focus_area,
"organization_type" => organization_type,
"member_type" => member_type_label,
"is_active" => is_active == "\x01",
"billing_street" => billing_address,
"billing_organization" => billing_organization,
"billing_department" => billing_department,
"billing_postal_code" => billing_post_code,
"billing_state_code" => billing_state.present? ? billing_state.split("-").last : nil,
"billing_country_code" => billing_country,
"billing_country" => billing_country_name,
"billing_city" => billing_city,
"joined" => joined&.iso8601,
"twitter_handle" => twitter_handle,
"ror_id" => ror_id,
"created" => created.iso8601,
"updated" => updated.iso8601,
"deleted_at" => deleted_at ? deleted_at.iso8601 : nil,
"doi_estimate" => doi_estimate,
}
{
"id" => symbol.downcase, "type" => "providers", "attributes" => attributes
}
end
def self.export(query: nil)
query = query.present? ? query + " !role_name:ROLE_ADMIN !role_name:ROLE_DEV" : "!role_name:ROLE_ADMIN !role_name:ROLE_DEV"
# Loop through all providers
i = 0
page = { size: 1_000, number: 1 }
response = self.query(query, include_deleted: true, page: page)
response.records.each do |provider|
provider.send_provider_export_message(provider.to_jsonapi)
i += 1
end
total = response.results.total
total_pages = page[:size] > 0 ? (total.to_f / page[:size]).ceil : 0
# keep going for all pages
page_num = 2
while page_num <= total_pages
page = { size: 1_000, number: page_num }
response = self.query(query, include_deleted: true, page: page)
response.records.each do |provider|
provider.send_provider_export_message(provider.to_jsonapi)
i += 1
end
page_num += 1
end
"#{i} providers exported."
end
private
def set_region
r = ISO3166::Country[country_code].world_region if country_code.present?
write_attribute(:region, r)
end
def set_defaults
self.symbol = symbol.upcase if symbol.present?
self.is_active = is_active ? "\x01" : "\x00"
self.version = version.present? ? version + 1 : 0
self.role_name = "ROLE_ALLOCATOR" if role_name.blank?
self.doi_quota_used = 0 unless doi_quota_used.to_i > 0
self.doi_quota_allowed = -1 unless doi_quota_allowed.to_i > 0
self.billing_information = {} if billing_information.blank?
self.consortium_id = nil unless member_type == "consortium_organization"
self.non_profit_status = "non-profit" if non_profit_status.blank?
self.doi_estimate = doi_estimate.to_i
# custom filename for attachment as data URLs don't support filenames
if logo_content_type.present?
self.logo_file_name =
symbol.downcase + "." + logo_content_type.split("/").last
end
end
end