rails achievements - ruby-on-rails

Then it came the moment to implement some sort of achievement in our web application. I had an idea more or less like the hierarcy described in this question How to implement an achievement system in RoR .
The application we are working on is a software as a service intended to be managed externally without software developers. The thing is it should be possible to create new kinds of achievements runtime by the software administrator via the web interface. The hierarcy then becomes a wall.
I have read somewhere it is possible to implement this situation through finite state machines, but at this time i don't have enough informations about that topic.
Edit: specific question
I thought about modeling an achievement class with a list of conditions to be met. This basic class Achievement would have a boolean which recursively checks all the conditions to be valid. The conditions then could be hardcoded classes. The system admin then creates new kind of achievements with combinations of the atomic conditions.
My fear is the growing number of classes for the atomic conditions. I dont want to have 30+ condition classes in the project. Any advise is really appreciated.
Edit: more details about the implementation
From SpyrosP response, it seems a good idea to build the described DSL. In some way achievements then must be saved in the database. Keeping the same example:
comments :less_than => 10
check_comments
comments :more_or_equal => 100
award_hundred_comments_badge
In order to dinamically create achievements there should be a table that stores the condition(s) to be checked:
Achievement
| id | name |
| 1 | "Houndred Comments" |
Condition
| achievement_id | expression |
| 1 | some sort of condition |

I've been interested in the same idea and was reading different stuff some time ago. Probably the best way to do such a thing is by using observers. An observer is like a standard filter(before_filter and the likes), but with some differences, like how the return value is handled and so on.
That said, if your system is really really complex, you may want to use a state machine plugin like https://github.com/pluginaweek/state_machine . However, i feel that this is too much for an Achievements functionality.
If i had to face complex Achiement scenarios, i would probably create a simple DSL that defines the behavior of an achievement. Something like :
for_achievement :hundred_comments do
before_achievement :status => Comment, :lower_than => 100
after_achievement :status => Comment, :more_or_equals => 100
end
You get the idea. This would be a way to fully describe an achievement. Your observers would then be able to use your achievement.rb class scenarios, in order to make out whether an achievement was reached. Think of it the way CanCan works. This could also be a nice way for your administrators to write simple achievement requirements through an even simpler DSL than what i presented in my example above.
Hope that helps a bit, or at least gives you some ideas :)
EDIT : Simpler DSL
A DSL can be really simple and expressive, so that people may even like to write scenarios with. Something like :
comments :less_than => 10
check_comments
comments :more_or_equal => 100
award_hundred_comments_badge
This can easily be formed to be a valid scenario of achieving 100 comments. Let's think of a scenario where the user gets a badge if he has invited exactly 10 people who are women in gender.
invites :less_than => 10, gender :female
check_invites
invites :equals => 10, gender :female
award_women_invitations_badge
Now, i think that this is very simple to write even for admins who have no clue about ruby, if you explain to them basic stuff about the DSL. But if you do not want them to go into that, you can create a form like :
Action Dropdown => [Comment, Invite, Post, ....]
Condition => [Equal, Less Than, More Than, ....]
Condition_Value => (TextBox to write value to)
CheckCondition => [Check Invitation Count, Check Messages Count, ....]

Related

Rails DRY RESTful design of API endpoints with filters

I am developming a JSON API with Rails 4.2 which looks like this:
GET api/v1/car => app/controller/api/v1/car_controller#index
GET api/v1/car/:id/installment => app/controller/api/v1/carresources/installment_controller#index
Now i would like to extend these endpoint with filters.
api/v1/carresources/installment#index:
all installments for a specific car during a specific time period
all installments for a specific car by payment type (cash, mobile money, <- mobile money provider)
api/v1/car#index:
all cars by region
all cars by dealer
all cars by loan schema
The way i implemented is the following:
app/controller/api/v1/car_controller.rb
def index
res = []
if params.key?('start_date') and parms.key?('end_date')
res = Car.index_period(params['start_date'], params['stop_date']
elsif params.key?('loanstructure')
res = Car.index_loan_strucuture(params['loan_strucuture'])
....
end
end
Which is working, but not such a nice solution to put completely semantically different logic behind these if-"graves".
It would be possible to create a new endpoint for each filter, but i have the feeling that this bloats the routing and controller structure.
I would also like to avoid these if clauses, because these is also some authorization on the users role going on -which i skipped showing - which is also done with if clauses
Is there another clever way, or should i spent an extra route for each filter, where i then need to copy the whole authorization structure.
Many thanks in advance
Many thanks in advance
OP I would suggest breaking the filtering code little separately by handling the request.query.params in a filter class.
This filter class helps in de-coupling the code by
Allowing validation of the filters which are being passed so you can have a parseFilters function which does that.
You can use the hashtable which points to
{('start_date','end_date'): "index_period(params['start_date'], params['stop_date']", etc }
this will help you to apply the filters directly from the parsefilters.
This will ensure that your code is decoupled and also can be reused in other scenarios.

Notifications or user activity log implementation

The application has certain actions that are recorded for each user and recorded in db table notifications. The table has the following structure:
user_id, notification_type, credit, timestamp
notification_type does not store the entire text of the notification but just stores a short type description.
Later when the user wants to view his notifications I use a helper method from my view to fetch the actual text.
def notification_text(type)
case type_id
when 'flagPositive'
return 'A question you flagged has been marked as correct.'
when 'qAccepted'
return 'A question you added has been accepted.'
when 'qModerated'
return 'You moderated a question.'
when 'flagReport'
return 'You moderated a flag.'
end
end
1) Is this an optimum way to do this?
2) Should I replace the type_description with integer values (say 1 -> flagPositive, 2-> qAccepted) for performance benefits?
3) Are there any best practices around the same that I should be following?
1) This highly depends on your application and requirements. What I can say is that I used this approach sometimes and faced no problems so far.
2) If you see a performance problem with the string lookup, you could do so. A general recommendation is to optimize performance only when really needed.
3) Just google for "Ruby", "Rails", "ActiveRecord" and "Enum". You'll find lots of discussions about different solutions for this kind of problem. There are similar questions on this site, e.g., Enums in Ruby or In Rails, how should I implement a Status field for a Tasks app - integer or enum?

Concurrency and Mongoid

I'm currently trying my hand at developing a simple web based game using rails and Mongoid. I've ran into some concurrency issues that i'm not sure how to solve.
The issue is i'm not sure how to atomically do a check and take an action based upon it in Mongoid.
Here is a sample of the relevant parts of the controller code to give you an idea of what i'm trying to do:
battle = current_user.battle
battle.submitted = true
battle.save
if Battle.where(opponent: current_user._id, submitted: true, resolving: false).any?
battle.update_attribute(:resolving, true)
#Resolve turn
A battle is between two users, but i only want one of the threads to run the #Resolve turn. Now unless i'm completely off both threads could check the condition one after another, but before setting resolving to true, therefore both end up running the '#Resolve turn' code.
I would much appreciate any ideas on how to solve this issue.
I am however getting an increasing feeling that doing user synchronization in this way is fairly impractical and that there's a better way altogether. So suggestions for other techniques that could accomplish the same thing would be greatly appreciated!
Sounds like you want the mongo findAndModify command which allows you to atomically retrieve and update a row.
Unfortunately mongoid doesn't appear to expose this part of the mongo api, so it looks like you'll have to drop down to the driver level for this one bit:
battle = Battle.collection.find_and_modify(query: {oppenent: current_user._id, ...},
update: {'$set' => {resolving: true})
By default the returned object does not include the modification made, but you can turn this on if you want (pass {:new => true})
The value returned is a raw hash, if my memory is correct you can do Battle.instantiate(doc) to get a Battle object back.

Is this a race condition issue in Rails 3?

Basically I have this User model which has certain attributes say 'health' and another Battle model which records all the fight between Users. Users can fight with one another and some probability will determine who wins. Both will lose health after a fight.
So in the Battle controller, 'CREATE' action I did,
#battle = Battle.attempt current_user.id, opponent.id
In the Battle model,
def self.attempt current_user.id, opponent_id
battle = Battle.new({:user_id => current_user.id, :opponent_id => opponent_id})
# all the math calculation here
...
# Update Health
...
battle.User.health = new_health
battle.User.save
battle.save
return battle
end
Back to the Battle controller, I did ...
new_user_health = current_user.health
to get the new health value after the Battle. However the value I got is the old health value (the health value before the Battle).
Has anyone face this kind of problem before ???
UPDATE
I just add
current_user.reload
before the line
new_user_health = current_user.health
and that works. Problem solved. Thanks!
It appears that you are getting current_user, then updating battle.user and then expecting current_user to automatically have the updated values. This type of thing is possible using Rails' Identity Map but there are some caveats that you'll want to read up on first.
The problem is that even though the two objects are backed by the same data in the database, you have two objects in memory. To refresh the information, you can call current_user.reload.
As a side note, this wouldn't be classified a race condition because you aren't using more than one process to modify/read the data. In this example, you are reading the data, then updating the data on a different object in memory. A race condition could happen if you were using two threads to access the same information at the same time.
Also, you should use battle.user, not battle.User like Wayne mentioned in the comments.

Rails - given an array of Users - how to get a output of just emails?

I have the following:
#users = User.all
User has several fields including email.
What I would like to be able to do is get a list of all the #users emails.
I tried:
#users.email.all but that errors w undefined
Ideas? Thanks
(by popular demand, posting as a real answer)
What I don't like about fl00r's solution is that it instantiates a new User object per record in the DB; which just doesn't scale. It's great for a table with just 10 emails in it, but once you start getting into the thousands you're going to run into problems, mostly with the memory consumption of Ruby.
One can get around this little problem by using connection.select_values on a model, and a little bit of ARel goodness:
User.connection.select_values(User.select("email").to_sql)
This will give you the straight strings of the email addresses from the database. No faffing about with user objects and will scale better than a straight User.select("email") query, but I wouldn't say it's the "best scale". There's probably better ways to do this that I am not aware of yet.
The point is: a String object will use way less memory than a User object and so you can have more of them. It's also a quicker query and doesn't go the long way about it (running the query, then mapping the values). Oh, and map would also take longer too.
If you're using Rails 2.3...
Then you'll have to construct the SQL manually, I'm sorry to say.
User.connection.select_values("SELECT email FROM users")
Just provides another example of the helpers that Rails 3 provides.
I still find the connection.select_values to be a valid way to go about this, but I recently found a default AR method that's built into Rails that will do this for you: pluck.
In your example, all that you would need to do is run:
User.pluck(:email)
The select_values approach can be faster on extremely large datasets, but that's because it doesn't typecast the returned values. E.g., boolean values will be returned how they are stored in the database (as 1's and 0's) and not as true | false.
The pluck method works with ARel, so you can daisy chain things:
User.order('created_at desc').limit(5).pluck(:email)
User.select(:email).map(&:email)
Just use:
User.select("email")
While I visit SO frequently, I only registered today. Unfortunately that means that I don't have enough of a reputation to leave comments on other people's answers.
Piggybacking on Ryan's answer above, you can extend ActiveRecord::Base to create a method that will allow you to use this throughout your code in a cleaner way.
Create a file in config/initializers (e.g., config/initializers/active_record.rb):
class ActiveRecord::Base
def self.selected_to_array
connection.select_values(self.scoped)
end
end
You can then chain this method at the end of your ARel declarations:
User.select('email').selected_to_array
User.select('email').where('id > ?', 5).limit(4).selected_to_array
Use this to get an array of all the e-mails:
#users.collect { |user| user.email }
# => ["test#example.com", "test2#example.com", ...]
Or a shorthand version:
#users.collect(&:email)
You should avoid using User.all.map(&:email) as it will create a lot of ActiveRecord objects which consume large amounts of memory, a good chunk of which will not be collected by Ruby's garbage collector. It's also CPU intensive.
If you simply want to collect only a few attributes from your database without sacrificing performance, high memory usage and cpu cycles, consider using Valium.
https://github.com/ernie/valium
Here's an example for getting all the emails from all the users in your database.
User.all[:email]
Or only for users that subscribed or whatever.
User.where(:subscribed => true)[:email].each do |email|
puts "Do something with #{email}"
end
Using User.all.map(&:email) is considered bad practice for the reasons mentioned above.

Resources