I have the following model with subscriptions for classes
create_table "subscriptions", force: true do |t|
t.integer "user_id"
t.integer "course_id"
t.datetime "date_subscription_start"
t.datetime "date_subscription_end"
t.string "subscription_type"
t.datetime "created_at"
t.datetime "updated_at"
end
I want to make it so users are able to subscribe to the same class again if they, for some reason, want to retake it. My logic is that it should only be possible to sign up for a class if the date_subscription_end date of the class / user combination has already passed at the time of the new creation. If at the time of creation a Subscription with the same user_id and course_id exists that has a subscription_end date in the future it should be rejected because it means the user is still taking that course.
So something like this:
validates :user_id,
uniqueness: {
scope: :course_id,
message: "User is already subscribed to this course"
},
unless: Proc.new { |a|
Subscription.where(user_id: a.user_id, course_id: a.course_id) &&
Subscription.where('date_subscription_end < ?', Time.now)
}
Basically rails has to go through the subscriptions table, find all subscriptions with the same primary keys and check their date_subscription_end attributes. I feel like I'm close but missing something fundamental.
Thanks!
Jean
Edit: For some reason the "Hello!" greeting at the top of the post gets removed
First the Proc looks incorrect, as far as I can see the Proc returns a truthy value, as soon as the user had a subscription and some user (not necessarily a.user_id) subscribed any course (not necessarily a.course_id)
Second the unless needs to be an if and be part of the options hash for the uniqueness validation.
So the following code should work:
validates :user_id,
uniqueness: {
scope: :course_id,
message: "User is already subscribed to this course",
if: Proc.new { |a|
Subscription.where(user_id: a.usr_id, course_id: a.course_id)
.where('date_subscription_end >= ?', Time.now).exists?
}
}
This way you hit the database only once and your code doesn't need to instantiate all matching subscriptions like it does if you just run .where(…)
UPDATE:
the condition for the date_subscription_end should be: 'date_subscription_end >= ?', Time.now
Related
I'm using PostgreSQL with Rails in API mode and I have a row called expired with data type Date and I want to make a trigger that identifies if the expired date is equal to the current date and change my is_expired column from false to true, but I don't know where to start.
I've read a bit of Rails documentation or some libraries like hairtrigger and it seems a bit confusing.
this is my table:
class CreateRifs < ActiveRecord::Migration[7.0]
def change
create_table :rifs do |t|
t.string :name
t.datetime :rif_date
t.string :award_with_sign
t.string :award_no_sign
t.string :plate
t.integer :year
t.float :price
t.integer :numbers
t.integer :with_signs
t.date :expired
t.boolean :is_expired, default: false
t.timestamps
end
end
end
Do you have a specific reason to use a database column for this? Because you could easily write this method on the model:
def expired? # This is actually an override, see next paragraph
expired <= Date.today
end
Or alternatively, if the expired column only gets populated with a past date after it actually has expired (and doesn't, e.g., represent a future expiry), don't write a method at all. Rails automatically provides you with a predicate for every column: a column? method that returns true if column is populated.
You don't need a trigger for this, maybe a scope to only get expired vs not expired records.
class Rif < ApplicationRecord
scope :expired, -> { where('expired < NOW()') }
end
You can then use the scope later on
expired_rifs = Rif.expired
I've got a portfolio model with following fields:
name: string (required)
status: string (required) one of: draft, active, funded
One of the requirement is that a newly created portfolio should have a status draft. I could set a default value inside of migration something like:
create_table :portfolios do |t|
t.string :name, null: false
t.string :status, null: false, default: 'draft'
t.timestamps
end
But I don't think it will easy to maintain. Of course I could set this status inside create method like:
Portfolio.create!(
name: params[:name],
status: 'draft'
)
Is there a better way to create such record? maybe some method inside of model?
class Portfolio < ApplicationRecord
after_initialize do
self.name = "draft"
end
end
I think it's better to do it using after_initialize because this callback will guarantee that the default value will be there from the very beginning of the life cycle of the object
Portfolio.new.name
#shoudl give you draft
I'm building a basic waiting list functionality for a project of mine.
I've got a model called Subscribers which has 2 columns: "points" and "created_at". Created_at is obviously generated when the record is created and points is an integer that starts from 1.
When users sign up they receive a code they can share on social media.
Every time a new user signs up through a referral code, the referrer gets 1 point.
I need a function to sort subscribers by points AND time so that, given a specific subscriber, I know how many people he has ahead and behind in the list.
The difficulty is that I'm not just counting the users with more points, but also the users who have signed up AFTER the specific user I'm querying. What I need to avoid is that users with the same points of a specific user, but who have registered much later, end up being ahead.
WaitingList model
class WaitingList < ActiveRecord::Base
# Database Schema
# t.string "name"
# t.string "uuid"
# t.integer "user_id"
# t.string "status", default: "active"
belongs_to :user
has_many :subscribers
validates :user, :presence => true
validates_presence_of :uuid, :name
end
Subscriber Model
class Subscriber < ActiveRecord::Base
# Database Schema
# t.string :email
# t.string :name
# t.integer :waiting_list_id
# t.string :code
# t.boolean :referred, default: false
# t.integer :referral_id
# t.integer "points", default: 1
belongs_to :waiting_list
validates :waiting_list, :presence => true
validates_presence_of :email
end
Any idea how to achieve this?
You can order the subscribers by their maximum points.
Subscriber ordered by their maximum points and ascending created_at.
#subscribers = Subscriber.group('id').order("max(points) desc, created_at")
Subscriber ordered by their maximum points and descending created_at.
#subscribers = Subscriber.group('id').order("max(points) desc, created_at desc")
I probably didn't understand STI or whatever inheritance of rails :)
I need to implement a design where I have a form; on upload I submit, but if the user is not logged in, she needs to login via openid (only access supported). Thus this implies a redirect, and the best thing I could come up with so far is to temporarily save the data, and on successful login actually create the real object. I wanted to have two separate tables for the objects; the Site object is the one to be created, the Sitetmp is the temporary store, and it has an additional field called nonce; (on successful login, the nonce will be compared, and if ok, the Sitetmp instance deleted and a new Site one created)
The DB:
#schema.rb
create_table "sites", force: true do |t|
t.string "name"
t.date "date"
t.text "description"
t.float "lat"
t.float "lon"
t.date "updated"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "sitetmps", force: true do |t|
t.string "nonce"
t.string "name"
t.date "date"
t.text "description"
end
The Subclass:
#app/models/sitetmp.rb
class Sitetmp < Site
attr_accessor :nonce
end
The Superclass:
#app/models/site.rb
class Site < ActiveRecord::Base
validates :name, uniqueness: true, :presence => true,
:length => { :minimum => 5 }
validates :date, :presence => true
has_many :images, :inverse_of => :site
end
It seemed to be all set, but when I actually want to access the temporary object on successful login, it tells me
SQLite3::SQLException: no such column: sites.nonce: SELECT "sites".* FROM "sites" WHERE "sites"."nonce" = '059253928646750523787961570357' LIMIT 1
The code in the controller causing this is:
if nonce
#tmp = Sitetmp.find_by nonce: nonce
Clearly I am trying to access the Sitetmp instance by its nonce attribute, but rails is resolving to actually access the Site class - which doesn't have a nonce attribute.
What am I doing wrong? How do I correctly find the Sitetmp object by nonce in order to create a valid Site object from it?
As I understand it, ActiveRecord::Base has a table_name method. If you inherit from ActiveRecord::Base, the subclass (Site), through some rails magic, has the table_name set to the underscored lowercased version of itself.
Then inheriting from the Site class, Sitetmp does not get the advantage of having the table_name set.
You may be able to fix this by setting this:
#app/models/sitetmp.rb
class Sitetmp < Site
# Set custom table name
table_name "sitetmp"
attr_accessor :nonce
end
I don't know if this would have an impact on the table_name of the Site class, so you may just have to create a standalone Sitetmp model to work from.
Alternatively, you could load the values into the session - see here for more
I'm trying to figure out the best way to build my model. Each user can have many balances, but I would like to enforce one balance of each currency per user. The application controls the record generation, so perhaps this is overkill. However, the question perplexed me, so I thought I'd ask the community.
If it makes sense to do, what would be the best way to build this?
My migration thus far:
class CreateBalances < ActiveRecord::Migration
def change
create_table :balances do |t|
t.decimal :amount
t.integer :currency, default: 0, null: false # this will be an enum in the model
t.references :user, index: true
t.timestamps
end
end
end
TL;DR: one of each :currency per :user
Try this in your Balance model:
validates :currency, :uniqueness => true, :scope => user_id
What I think this says (and I could be wrong, so please take this with a grain of salt) is, "Make sure that this type of currency exists only once for this user." Failing that, you could also try a custom validation:
validates :unique_currency_balance_per_user
def unique_currency_balance_per_user
Balance.where('id != ?', id)
.where(:currency => currency, :user_id => user_id)
.present?
end
There are also likely to be database-level constraints, but I am not yet aware of these.