app/models/user.rb
class UniqueUsernameValidator < ActiveModel::Validator
def validate(record)
if User.find_by(username: record.username) && record.openid_identifier.nil?
record.errors[:base] << 'That username is already taken. If this is your username, you can simply log in to this site.'
end
end
end
# Overwrites authlogic username regex to allow one character usernames
Authlogic::Regex::LOGIN = /\A[A-Za-z\d_\-]*\z/
class User < ActiveRecord::Base
extend Utils
include Statistics
extend RawStats
self.table_name = 'rusers'
alias_attribute :name, :username
module Status
VALUES = [
NORMAL = 1, # Usage: Status::NORMAL
BANNED = 0, # Usage: Status::BANNED
MODERATED = 5 # Usage: Status::MODERATED
].freeze
end
module Frequency
VALUES = [
DAILY = 0,
WEEKLY = 1
].freeze
end
attr_readonly :username
acts_as_authentic do |c|
c.crypto_provider = Authlogic::CryptoProviders::Sha512
c.validates_format_of_login_field_options = { with: Authlogic::Regex::LOGIN, message: I18n.t('error_messages.login_invalid', default: "can only consist of alphabets, numbers, underscore '_', and hyphen '-'.") }
end
has_attached_file :photo,
styles: { thumb: '200x200#', medium: '500x500#', large: '800x800#' },
url: '/public/system/profile/photos/:id/:style/:basename.:extension',
path: ':rails_root/public/system/public/system/profile/photos/:id/:style/:filename'
do_not_validate_attachment_file_type :photo_file_name
# validates_attachment_content_type :photo_file_name, :content_type => %w(image/jpeg image/jpg image/png)
has_many :images, foreign_key: :uid
has_many :node, foreign_key: 'uid', dependent: :destroy
has_many :csvfiles, foreign_key: :uid
has_many :node_selections, foreign_key: :user_id
has_many :revision, foreign_key: 'uid', dependent: :destroy
has_many :user_tags, foreign_key: 'uid', dependent: :destroy
has_many :active_relationships, class_name: 'Relationship', foreign_key: 'follower_id', dependent: :destroy
has_many :passive_relationships, class_name: 'Relationship', foreign_key: 'followed_id', dependent: :destroy
has_many :following_users, through: :active_relationships, source: :followed
has_many :followers, through: :passive_relationships, source: :follower
has_many :likes
has_many :answer_selections, foreign_key: :user_id
has_many :revisions, through: :node
has_many :comments, foreign_key: :uid
validates_with UniqueUsernameValidator, on: :create
before_save :set_token
scope :past_week, -> { where("created_at > ?", 7.days.ago) }
scope :past_month, -> { where("created_at > ?", 1.month.ago) }
def is_new_contributor?
Node.where(uid: id).size === 1 && Node.where(uid: id).first.created_at > 1.month.ago
end
def new_contributor
return "<a href='/tag/first-time-poster' class='badge badge-success font-italic'>new contributor</a>".html_safe if is_new_contributor?
end
def set_token
self.token = SecureRandom.uuid if token.nil?
end
def nodes
node
end
def notes
Node.where(uid: uid)
.where(type: 'note')
.order('created DESC')
end
def coauthored_notes
coauthored_tag = "with:" + name.downcase
Node.where(status: 1, type: "note")
.includes(:revision, :tag)
.references(:term_data, :node_revisions)
.where('term_data.name = ? OR term_data.parent = ?', coauthored_tag.to_s, coauthored_tag.to_s)
end
def generate_reset_key
key = [*'a'..'z'].sample(20).join
update_attribute(:reset_key, key)
key
end
def uid
id
end
def title
username
end
def path
"/profile/#{username}"
end
def lat
get_value_of_power_tag('lat')
end
def lon
get_value_of_power_tag('lon')
end
def zoom
get_value_of_power_tag('zoom')
end
# we can revise/improve this for m2m later...
def has_role(some_role)
role == some_role
end
def admin?
role == 'admin'
end
def moderator?
role == 'moderator'
end
def can_moderate?
admin? || moderator?
end
def basic_user?
can_moderate? ? false : true
end
def is_coauthor?(node)
id == node.author.id || node.has_tag("with:#{username}")
end
def tags(limit = 10)
Tag.where('name in (?)', tagnames).limit(limit)
end
def normal_tags(limit = false)
tags(limit).select { |tag| !tag.name.include?(':') }
end
def tagnames(limit = 20)
tagnames = []
Node.includes(:tag).order('nid DESC').where(type: 'note', status: 1, uid: id).limit(limit).each do |node|
tagnames += node.tags.collect(&:name)
end
tagnames.uniq
end
def has_tag(tagname)
user_tags.collect(&:value).include?(tagname)
end
# power tags have "key:value" format, and should be searched with a "key:*" wildcard
def has_power_tag(key)
user_tags.where('value LIKE ?', key + ':%').exists?
end
def get_value_of_power_tag(key)
tname = user_tags.where('value LIKE ?', key + ':%')
tvalue = tname.first.name.partition(':').last if tname.present?
tvalue
end
def blurred?
has_power_tag("location") && get_value_of_power_tag("location") == 'blurred'
end
def get_last_value_of_power_tag(key)
tname = user_tags.where('value LIKE ?', key + ':%')
tvalue = tname.last.name.partition(':').last
tvalue
end
def subscriptions(type = :tag)
if type == :tag
TagSelection.where(user_id: uid,
following: true)
end
end
def following(tagname)
tids = Tag.where(name: tagname).collect(&:tid)
!TagSelection.where(following: true, tid: tids, user_id: uid).empty?
end
def add_to_lists(lists)
lists.each do |list|
WelcomeMailer.add_to_list(self, list).deliver_later
end
end
def barnstars
NodeTag.includes(:node, :tag)
.references(:term_data)
.where('type = ? AND term_data.name LIKE ? AND node.uid = ?', 'note', 'barnstar:%', uid)
end
def photo_path(size = :medium)
photo.url(size)
end
def first_time_poster
notes.where(status: 1).size.zero?
end
def first_time_commenter
Comment.where(status: 1, uid: uid).size.zero?
end
def follow(other_user)
active_relationships.create(followed_id: other_user.id)
end
def unfollow(other_user)
active_relationships.where(followed_id: other_user.id).first.destroy
end
def following?(other_user)
following_users.include?(other_user)
end
def profile_image(size = :thumb)
if photo_file_name
photo_path(size)
else
"https://www.gravatar.com/avatar/#{OpenSSL::Digest::MD5.hexdigest(email)}"
end
end
def questions
Node.questions.where(status: 1, uid: id)
end
def content_followed_in_period(start_time, end_time,
order_by = 'node_revisions.timestamp DESC', node_type = 'note', include_revisions = false)
tagnames = TagSelection.where(following: true, user_id: uid)
node_ids = []
tagnames.each do |tagname|
node_ids += NodeTag.where(tid: tagname.tid).collect(&:nid)
end
range = "(created >= #{start_time.to_i} AND created <= #{end_time.to_i})"
range += " OR (timestamp >= #{start_time.to_i} AND timestamp <= #{end_time.to_i})" if include_revisions
Node.where(nid: node_ids)
.includes(:revision, :tag)
.references(:node_revision)
.where('node.status = 1')
.where(type: node_type)
.where(range)
.order(order_by)
.distinct
end
def unmoderated_in_period(start_time, end_time)
range = "(created >= #{start_time.to_i} AND created <= #{end_time.to_i})"
Node.where('node.status = 4')
.where(type: 'note')
.where(range)
.order('created DESC')
.distinct
end
def moderate
self.status = Status::MODERATED
save
# user is logged out next time they access current_user in a controller; see application controller
self
end
def unmoderate
self.status = Status::NORMAL
save
self
end
def ban
decrease_likes_banned
self.status = Status::BANNED
save
# user is logged out next time they access current_user in a controller; see application controller
self
end
def unban
increase_likes_unbanned
self.status = Status::NORMAL
save
self
end
def self.send_browser_notification(users_ids, notification)
users_ids.each do |uid|
if UserTag.where(value: 'notifications:all', uid: uid).any?
ActionCable.server.broadcast "users:notification:#{uid}", notification: notification
end
end
end
def banned?
status == Status::BANNED
end
def note_count
Node.where(status: 1, uid: uid, type: 'note').size
end
def node_count
Node.where(status: 1, uid: uid).size + Revision.where(uid: uid).size
end
def liked_notes
Node.includes(:node_selections)
.references(:node_selections)
.where("type = 'note' AND \
node_selections.liking = ? \
AND node_selections.user_id = ? \
AND node.status = 1", true, id)
.order('node_selections.nid DESC')
end
def liked_pages
nids = NodeSelection.where(user_id: uid, liking: true)
.collect(&:nid)
Node.where(nid: nids)
.where(type: 'page')
.order('nid DESC')
end
def send_digest_email
if has_tag('digest:daily')
@nodes = content_followed_in_period(1.day.ago, Time.current)
@frequency = Frequency::DAILY
else
@nodes = content_followed_in_period(1.week.ago, Time.current)
@frequency = Frequency::WEEKLY
end
if @nodes.size.positive?
SubscriptionMailer.send_digest(id, @nodes, @frequency).deliver_later
end
end
def send_digest_email_spam
if has_tag('digest:weekly:spam')
@frequency_digest = Frequency::WEEKLY
@nodes_unmoderated = unmoderated_in_period(1.week.ago, Time.current)
elsif has_tag('digest:daily:spam')
@frequency_digest = Frequency::DAILY
@nodes_unmoderated = unmoderated_in_period(1.day.ago, Time.current)
end
if @nodes_unmoderated.size.positive?
AdminMailer.send_digest_spam(@nodes_unmoderated, @frequency_digest).deliver_later
end
end
def tag_counts
tags = {}
Node.order('nid DESC').where(type: 'note', status: 1, uid: id).limit(20).each do |node|
node.tags.each do |tag|
if tags[tag.name]
tags[tag.name] += 1
else
tags[tag.name] = 1
end
end
end
tags
end
def generate_token
user_id_and_time = { id: id, timestamp: Time.now }
User.encrypt(user_id_and_time)
end
class << self
def search(query)
query = query.tr('@', ' ') # @ is a special char in full text search in MYSQL, and cannot be escaped; https://github.com/publiclab/plots2/issues/8344
query += '*' unless query.empty?
User.where('MATCH(bio, username) AGAINST(? IN BOOLEAN MODE)', query)
end
def search_by_username(query)
query = query.tr('@', ' ') # @ is a special char in full text search in MYSQL, and cannot be escaped; https://github.com/publiclab/plots2/issues/8344
query += '*' unless query.empty?
User.where('MATCH(username) AGAINST(? IN BOOLEAN MODE)', query)
end
def validate_token(token)
begin
decrypted_data = User.decrypt(token)
rescue ActiveSupport::MessageVerifier::InvalidSignature
return 0
end
if (Time.now - decrypted_data[:timestamp]) / 1.hour > 24.0
return 0
else
return decrypted_data[:id]
end
end
def find_by_username_case_insensitive(username)
User.where('lower(username) = ?', username.downcase).first
end
# all users who've posted a node, comment, or answer in the given period
def contributor_count_for(start_time, end_time)
notes = Node.where(type: 'note', status: 1, created: start_time.to_i..end_time.to_i).pluck(:uid)
questions = Node.questions.where(status: 1, created: start_time.to_i..end_time.to_i).pluck(:uid)
comments = Comment.where(timestamp: start_time.to_i..end_time.to_i).pluck(:uid)
revisions = Revision.where(status: 1, timestamp: start_time.to_i..end_time.to_i).pluck(:uid)
contributors = (notes + questions + comments + revisions).compact.uniq.size
contributors
end
def create_with_omniauth(auth)
random_chars = [*'A'..'Z', *'a'..'z', *0..9].sample(2).join
email_prefix = auth["info"]["email"].tr('.', '_').split('@')[0]
email_prefix = auth["info"]["email"].tr('.', '_').split('@')[0] + random_chars until User.where(username: email_prefix).empty?
provider = { "facebook" => 1, "github" => 2, "google_oauth2" => 3, "twitter" => 4 }
create! do |user|
generated_password = SecureRandom.urlsafe_base64
user.username = email_prefix
user.email = auth["info"]["email"]
user.password = generated_password
user.status = Status::NORMAL
user.password_confirmation = generated_password
user.password_checker = provider[auth["provider"]]
user.save!
end
end
def count_all_time_contributor
notes = Node.where(type: 'note', status: 1).pluck(:uid)
questions = Node.questions.where(status: 1).pluck(:uid)
comments = Comment.pluck(:uid)
revisions = Revision.where(status: 1).pluck(:uid)
(notes + questions + comments + revisions).compact.uniq.size
end
def watching_location(nwlat, selat, nwlng, selng)
raise("Must be a float") unless (nwlat.is_a? Float) && (nwlng.is_a? Float) && (selat.is_a? Float) && (selng.is_a? Float)
tids = Tag.where("SUBSTRING_INDEX(term_data.name,':',1) = ? \
AND SUBSTRING_INDEX(SUBSTRING_INDEX(term_data.name, ':', 2),':',-1)+0 <= ? \
AND SUBSTRING_INDEX(SUBSTRING_INDEX(term_data.name, ':', 3),':',-1)+0 <= ? \
AND SUBSTRING_INDEX(SUBSTRING_INDEX(term_data.name, ':', 4),':',-1)+0 <= ? \
AND SUBSTRING_INDEX(term_data.name, ':', -1) <= ?", 'subscribed', nwlat, nwlng, selat, selng).collect(&:tid).uniq || []
uids = TagSelection.where('tag_selections.tid IN (?)', tids).collect(&:user_id).uniq || []
User.where("id IN (?)", uids).order(:id)
end
end
def recent_locations(limit = 5)
recent_nodes = nodes.includes(:tag)
.references(:term_data)
.where('term_data.name LIKE ?', 'lat:%')
.joins("INNER JOIN term_data AS lon_tag ON lon_tag.name LIKE 'lat:%'")
.order(created: :desc)
.limit(limit)
end
def latest_location
recent_locations.last
end
def self.recently_active_users(limit = 15, order = 'last_updated DESC')
Rails.cache.fetch('users/active', expires_in: 1.hour) do
User.select('rusers.username, rusers.status, rusers.id, MAX(node_revisions.timestamp) AS last_updated')
.joins("INNER JOIN `node_revisions` ON `node_revisions`.`uid` = `rusers`.`id` ")
.where("node_revisions.status = 1")
.where("rusers.status = 1")
.group('rusers.id')
.order(order)
.limit(limit)
end
end
def drafts
Node.where(uid: uid)
.where(status: 3, type: 'note')
.order('created DESC')
end
def notes_for_tags(tagnames)
Node.includes(:node_tag, :tag)
.where('term_data.name IN (?)', tagnames)
.references(:term_data, :node_tag)
.where(type: 'note')
.order('node.nid DESC')
.where(uid: uid)
end
private
def decrease_likes_banned
node_selections.each do |selection|
selection.node.cached_likes -= 1
selection.node.save!
end
end
def increase_likes_unbanned
node_selections.each do |selection|
selection.node.cached_likes += 1
selection.node.save!
end
end
def map_openid_registration(registration)
self.email = registration['email'] if email.blank?
self.username = registration['nickname'] if username.blank?
end
end