Can the merit gem recalculate reputation and re-evaluate badges and ranks? - ruby-on-rails-3

I would like to add a system of points, ranks and badges in my rails app.
The merit gem looks cool, but I want to make sure how it works before using it.
The way you define badges
# app/models/merit/badge_rules.rb
grant_on 'comments#vote', :badge => 'relevant-commenter', :to => :user do |comment|
comment.votes.count == 5
end
and points
# app/models/merit/point_rules.rb
score 10, :to => :post_creator, :on => 'comments#create' do |comment|
comment.title.present?
end
suggest that the action is done as a hook, just after the action (comments#vote or comments#create) in those cases. I havn't looked how the calculation/attribution of badges and points work yet so I am not sure.
As my app will evolve over time, I would like to be able to change the point and badges rules and re-evaluate them. Eg: Say I first decide to grant 10 points on account activation. I would like to be able to change it to 20 and then all activated profiles are re-evaluated and get a +10 point increase. Same thing for badges.
Does this gem support that ?

It is possible to recompute reputation in an app with merit gem. Merit keeps the history of which actions triggered which point/badge granting in Merit::Action model, which is mapped to merit_actions table.
Following script should work (do a back up first, as I didn't do this in production yet):
# 1. Reset all badges/points granting
Merit::BadgesSash.delete_all
Merit::Score::Point.delete_all
# 1.1 Optionally reset activity log (badges/points granted/removed until now)
Merit::ActivityLog.delete_all
# 2. Mark all `merit_actions` as unprocessed
Merit::Action.all.map{|a| a.update_attribute :processed, false }
# 3. Recompute reputation rules
Merit::Action.check_unprocessed
Merit::RankRules.new.check_rank_rules
Notes on merit internals ("General merit workflow" wiki page).

Related

Clockwork with a multi-tenant setup

My app uses postgres schemas to implement multi-tenancy. So for a table like users, each tenant actually has it's own users table -
public.users (default)
foo.users
bar.users
...
As a side note, it implements multi-tenancy using the apartment gem
The clockwork gem allows you to read events from a database table, with the use of it's sync_database_events method. For example, it can read events from a scheduled_jobs table/model:
sync_database_events model: ScheduledJob, every: 1.minute do |model_instance|
rake model_instance.name
end
By default this reads from public.scheduled_jobs, but each one of my schemas will have it's own scheduled_jobs to read from.
Is there a convenient way to have it loop through all my tenants?
I'm doing a very similar thing with the difference that I'm using Apartment and MySQL not Postgres. Apartment is very, very powerful but it feels poorly documented at best. Here was how I approached this:
if Rails.env.development?
every(1.day, '3am.job -- Daily Job', :at => '03:00') do |job|
User.delay.crawl
Search.delay.crawl
end
elsif Rails.env.production?
every(1.day, '3am.job -- Daily Job', :at => '03:00') do |job|
SiteApi.usernames.each do |username|
Apartment::Tenant.switch!(username)
User.delay.crawl
Search.delay.crawl
end
end
end
The magic happens in my Rails.env.production? block. SiteApi is an API that talks to my master database of users and gets back an array of usernames. It then calls Apartment::Tenant.switch!(username) to switch the tenancy context to the right user. It then calls the functions that I need to run on each tenant.
You should note that I am also making use of the Apartment Sidekiq extension for delay.
I suspect this conceptual leap is what you're looking for.

Rails, gem with functionality in between enum and many to many

i have im my rails app model which has few options (no more than 10 i think).
Something like Product - Category, where product can be part of 1 or many categories.
But i think i have too few categories to engage fully fledged many-to-many construct.
Moreover the list of categroies is predefined and will almost never change.
I think from sql side this could look like string field categories with such content:"Fruits|Vegetables|..."
Maybe someone know preexisting gem for such functionality, or maybe it is no real advantage doing so and i should choose standart many-to-many ?
I checked acts-as-taggable-on plugin, but it is i think fits not very well for this task.
Enum gems like enumerize i think fit just best, but they are allow only single single value to be choosen.
Currently came out with following combination:
This gem:
https://github.com/pboling/flag_shih_tzu
In model:
class Product < ActiveRecord::Base
KINDS = { 1 => :fruit, 2 => :vegetable }
include FlagShihTzu
attr_accessible *KINDS.values
as_flags KINDS
Then in view (haml):
=form_for [#product] do |f|
-Product::KINDS.each do |k, v|
=f.check_box v
=f.label v
UPDATE:
Yet another gem adressing this problem: https://github.com/joelmoss/bitmask_attributes

An efficient way to track user login dates and IPs history

I'm trying to track user login history for stat purposes but its not clear to me what the best way to go about it would be. I could have a separate table that records users and their login stats with a date, but that table could get REALLY big. I could track some historic fields in the User model/object itself in a parse-able field and just update it (them) with some delimited string format. e.g. split on :, get the last one, if an included date code isn't today, add an item (date+count) otherwise increment, then save it back. At least with this second approach it would be easy to remove old items (e.g. only keep 30 days of daily logins, or IPs), as a separate table would require a task to delete old records.
I'm a big fan of instant changes. Tasks are useful, but can complicate things for maintenance reasons.
Anyone have any suggestions? I don't have an external data caching solution up or anything yet. Any pointers are also welcome! (I've been hunting for similar questions and answers)
Thanks!
If you have the :trackable module, I found this the easiest way. In the User model (or whichever model you're authenticating)
def update_tracked_fields!(request)
old_signin = self.last_sign_in_at
super
if self.last_sign_in_at != old_signin
Audit.create :user => self, :action => "login", :ip => self.last_sign_in_ip
end
end
(Inspired by https://github.com/plataformatec/devise/wiki/How-To:-Turn-off-trackable-for-admin-users)
There is a nice way to do that through Devise.
Warden sets up a hook called after_set_user that runs after setting a user. So, supposed you have a model Login containing an ip field, a logged_in_at field and user_id field, you can only create the record using this fields.
Warden::Manager.after_set_user :except => :fetch do |record, warden, options|
Login.create!(:ip => warden.request.ip, :logged_in_at => Time.now, :user_id => record.id)
end
Building upon #user208769's answer, the core Devise::Models::Trackable#update_tracked_fields! method now calls a helper method named update_tracked_fields prior to saving. That means you can use ActiveRecord::Dirty helpers to make it a little simpler:
def update_tracked_fields(request)
super
if last_sign_in_at_changed?
Audit.create(user: self, action: 'login', ip: last_sign_in_ip)
end
end
This can be simplified even further (and be more reliable given validations) if audits is a relationship on your model:
def update_tracked_fields(request)
super
audits.build(action: 'login', ip: last_sign_in_ip) if last_sign_in_at_changed?
end
Devise supports tracking the last signed in date and the last signed in ip address with it's :trackable module. By adding this module to your user model, and then also adding the correct fields to your database, which are:
:sign_in_count, :type => Integer, :default => 0
:current_sign_in_at, :type => Time
:last_sign_in_at, :type => Time
:current_sign_in_ip, :type => String
:last_sign_in_ip, :type => String
You could then override the Devise::SessionsController and it's create action to then save the :last_sign_in_at and :last_sign_in_ip to a separate table in a before_create callback. You should then be able to keep them as long you would like.
Here's an example (scribd_analytics)
create_table 'page_views' do |t|
t.column 'user_id', :integer
t.column 'request_url', :string, :limit => 200
t.column 'session', :string, :limit => 32
t.column 'ip_address', :string, :limit => 16
t.column 'referer', :string, :limit => 200
t.column 'user_agent', :string, :limit => 200
t.column 'created_at', :timestamp
end
Add a whole bunch of indexes, depending on queries
Create a PageView on every request
We used a hand-built SQL query to take out the ActiveRecord overhead on
this
Might try MySQL's 'insert delayed´
Analytics queries are usually hand-coded SQL
Use 'explain select´ to make sure MySQL isusing the indexes you expect
Scales pretty well
BUT analytics queries expensive, can clog upmain DB server
Our solution:
use two DB servers in a master/slave setup
move all the analytics queries to the slave
http://www.scribd.com/doc/49575/Scaling-Rails-Presentation-From-Scribd-Launch
Another option to check is Gattica with Google Analytics
I hate answering my own questions, especially given that you both gave helpful answers. I think answering my question with the approach I took might help others, in combination with your answers.
I've been playing with the Impressionist Gem (the only useful page view Gem since the abandoned RailStat) with good results so far. After setting up the basic migration, I found that the expected usage follows Rail's MVC design very closely. If you add "impressionist" to a Controller, it will go looking for the Model when logging the page view to the database. You can modify this behaviour or just call impressionist yourself in your Controller (or anywhere really) if you're like me and happen to be testing it out on a Controller that doesn't have a Model.
Anyways, I got it working with Devise to track successful logins by overriding the Devise::SessionsController and just calling the impressionist method for the #current_member: (don't forget to check if it's nil! on failed login)
class TestSessionController < Devise::SessionsController
def create
if not #current_member.nil?
impressionist(#current_member)
end
super
end
end
Adding it to other site parts later for some limited analytics is easy to do. The only other thing I had to do was update my routes to use the new TestSessionController for the Devise login route:
post 'login' => 'test_session#create', :as => :member_session
Devise works like normal without having to modify Devise in anyway, and my impressionist DB table is indexed and logging logins. I'll just need a rake task later to trim it weekly or so.
Now I just need to work out how to chart daily logins without having to write a bunch of looping, dirty queries...
There is also 'paper_trail' gem, that allows to track model changes.

How to user defined friendly URLs in Rails 3?

Now i have something like this
http://myapp.com/pages/1
http://myapp.com/pages/2
http://myapp.com/pages/3
http://myapp.com/pages/4
And each page belong to one user
What i need is to each user to set it's own custom name for the page.
I was thinking of using the friendly_id gem http://norman.github.com/friendly_id/
but I don't find any method to directly edit the slug to set a custom friendly url
how should i proceed?
FriendlyID is a great gem.
It shouldn't be hard to implement user defined page URL.
Create table pages with user_id and link
class User < ActiveRecord::Base
has_many :pages
class Page < ActiveRecord::Base
belongs_to :user
has_friendly_id :link # link is name of the column whose value will be replaced by slugged value
On the page#new you add an input for the link attribute.
Alternatively, you could set friendly_id on title or something else with :use_slug => true option. This way FriendlyID will take the title and modify it so it doesn't have and restricted characters. It will use it's own table to store slugs. Use cached_slug to increase performanse.
Updated
To give users a choice whether they wan't to set a custom link, you could do this:
Set friendly_id on the link field without slugs..
Make a virtual attribute permalink so you could show it in your forms.
In the before_filter, check whether the permalink is set.
If it is, write it to the link field.
If it's not, write title to the link field.
FriendlyID uses babosa gem to generate slugs. If you decide to use it as well, this is how your filter could look like:
protected
def generate_link
#you might need to use .nil? instead
self.link = self.permalink.empty? ? make_slug(self.title) : make_slug(self.permalink)
end
def make_slug(value)
value.to_slug.normalize.to_s #you could as well use ph6py's way
end
Adding to_param method to one of the models should help:
def to_param
"#{id}-#{call_to_method_that_returns_custom_name.parameterize}"
end
Hope this is what you are looking for :)
I am not using the friendly_url gem and am not sure whether my way is efficient. But it works fine for me.
I have a model called Node with id and friendly url field called url_title.
My routes.rb file:
resource 'nodes/:url_title', :to => 'Nodes#view'
nodes_controller.rb
class NodesController <ActiveController
def view
#node = Node.find_by_url_title(:params(url_title))
end
end
And use the #node variable to populate your view.
Now, whenever I type www.example.com/nodes/awesome-title , it takes me to the proper page. One argument against this can be need to create an index on a non-primary field. But I think that might be required for better performance even in the friendly_url gem. Also, the non-primary field url_title needs to be unique. Again, this might be required even for correct working for friendly_url .
Feel free to correct me if I am wrong in these assumptions.
There are a variety of ways, you can achieve this-
1) using Stringex
2) sluggable-finder
3) friendly_id
A complete step by step methodology with reasons for each to be used can be found out here. Happy reading!

How do I implement aasm in Rails 3 for what I want it to do?

I am a Rails n00b and have been advised that in order for me to keep track of the status of my user's accounts (i.e. paid, unpaid (and therefore disabled), free trial, etc.) I should use an 'AASM' gem.
So I found one that seems to be the most popular: https://github.com/rubyist/aasm But the instructions are pretty vague.
I have a Users model and a Plan model. User's model manages everything you might expect (username, password, first name, etc.). Plan model manages the subscription plan that users should be assigned to (with the restrictions).
So I am trying to figure out how to use the AASM gem to do what I want to do, but no clue where to start.
Do I create a new model ? Then do I setup a relationship between my User model and the model for AASM ? How do I setup a relationship? As in, a user 'has_many' states ? That doesn't seem to make much sense to me.
Any guidance would be really appreciated.
Thanks.
Edit: If anyone else is confused by AASMs like myself, here is a nice explanation of their function in Rails by the fine folks at Envy Labs: http://blog.envylabs.com/2009/08/the-rails-state-machine/
Edit2: How does this look:
include AASM
aasm_column :current_state
aasm_state :paid
aasm_state :free_trial
aasm_state :disabled #this is for accounts that have exceed free trial and have not paid
#aasm_state :free_acct
aasm_event :pay do
transitions :to => :paid, :from => [:free_trial, :disabled]
transitions :to => :disabled, :from => [:free_trial, :paid]
end
given it thought and this is what came out:
you're right about not making the states in the Plan, dunno what I was thinking. Either do it in the User model, or make an Account model, which belongs_to :user. Then, in your account try these (it's all about logics, so if you want more states, just mak'em up):
aasm_initial_state :free
aasm_state :free
aasm_state :paid
aasm_state :disabled
aasm_event :pay do
transitions :to => :paid, :from => [:free, :disabled]
end
aasm_event :disable do
transitions :to => :disabled, :from => [:free,:paid]
end
So when you create a new user, build an account for it too. Don't worry about the initial state at account creation, it will automatically be set to "free" when an account is created. Or, if it sounds easier, in the User model, as you suggested.