Setting Admin time availability - ruby-on-rails

I am trying to build a booking system. I have the user booking system and the admin (devise) user.
The admin will need to be able to set their time availability in the admin page which will then be available on the user form.
Currently I was thinking about putting a start_datetime and end_datetime in the admin.rb model but this doesn't account for days of the week.
My idea is to have a checkbox for each day and then a time start and time end.
Any suggestions on how I'd restrict the dates shown in the form for booking.rb to the times set by the admin.
Code is very basic at the moment but let me know if you would like anything from the app.

Ruby's Time class has lots of options for datetime validation. You could do something like
if user_submitted_time.saturday? || user_submitted_time.sunday? ...
This would work for validation. You could just black out the dates somehow in the view.
Check out the documentation here for more possibilities.
You could add a helper for your view to use that checks whether or not a date is available. Then disable the field using javascript if it is already booked.
Helper:
def is_booked(appt)
appointment = Appointment.find_by_id(appt)
if appointment.start_time?
true
end
View:
<% #appointments.each do |appt| %>
<% if appt.is_booked(appt.id) %>
<%= appt.date, class: "booked" %>
<% else %>
<%= appt.date, class: "open" %>
<% end %>
<% end %>
Then use CSS or Javascript to disable/hide the dates that are booked. All this can be refactored to be cleaner (you could use the helper to return the class in the view), but this is this should give you the general idea.

Related

Ruby on Rails trying to update a boolean in a user table while logged in as another entity

I am very novice at Ruby and Ruby on Rails.
I am trying to update a web application that has signed up volunteers that require approval before they can access full website functionality. I added a boolean field to the volunteers database model called :approved that defaults to false when a volunteer signs up, and requires a logged in administrator to update it to true.
I created an administrator class in my project that can view a list of pending volunteers from the controller:
def view_pending_volunteers
#navbar = true
#volunteers = Volunteer.where(approved: false)
end
In the administrator view I want to use checkboxes associated with volunteer, which when submitted will allow the administrator to update the :approved field in the volunteer table.
I have several issues that are not clear to accomplish this task:
In my administrator model I use has_many :volunteers with a migration that put the volunteer_id as a foreign key in the administrator database table. Is that sufficient to accomplish the task at hand, or do I need to create a join table for :approved? I can't have volunteers belong_to :administrators because they would not be able to sign up on their own, they need to be created by administrators.
I am not sure exactly how to configure the code for checkbox helpers in my administrator view. I am using form_with but I am sure my code is not correct. I would like to pass an array of parameters for each check_box associated with the :approved field for the list of pending volunteers back to the controller.
<p><b style="color:blue;font-size:24px;">
<%= form_with(model: #volunteer, local: true) do |f| %>
<% #volunteers.each do |v| %>
<li>
Volunteer: <%= x.first_name%> <%= x.last_name%>  Age: <%= x.age%>  Description: <%= x.description%> 
<%= f.check_box (#volunteers, v.approved, options = {}, checked_value = "1", unchecked_value = "0")%>
</li>
<% end %>
<%= f.submit "Approve" %>
<% end %>
</b></p>
I am not exactly sure how to handle the array of checkbox values that get returned to the administrator controller in order to update the volunteer database table. Do I need to create a hash in the view (how to do that?) and then loop through the hash to update_attribute(:approved, true) for each checked volunteer? I have created an update_pending_volunteers method in the administrator controller for the POST operation, but am unclear on the code that should be there because I am unsure the checkbox approach.
Thanks in advance for your patience with a newbie. This seems like such a simple thing to do but I am not sure of the proper approach. I have spent quite a bit of time reviewing APIs and videos and stack overflow articles but cannot seem to piece together information that will give me confidence in a particular approach to do this correctly. Again it seems like such a simple thing to accomplish but has become a source of frustration.
See the comment from Rockwell Rice. Don't create a relationship of any kind for this functionality.
Use the URL feature of form_with, not model. You're not acting on one volunteer, you're acting on many. Create a post route (ie. approve_volunteers). In the view you would create the checkboxes like this:
<%= form_with(url: approve_volunteers_path, local: true) do |f| %>
<% #volunteers.each do |v| %>
<div class="field">
<label class="checkbox">
Volunteer: <%= x.first_name%> <%= x.last_name%>  Age: <%= x.age%>  Description: <%= x.description%> 
<input type="checkbox" name="volunteers[]" value="<%= v.id %>">
</label>
</div>
<% end %>
<%= f.submit "Approve" %>
<% end %>
That should send params through the form like {"volunteers" => ["1", "3"]} and leave the rest empty. You might have to play around with those a little. Check your console for the params.
Then in your controller something like:
def approve_volunteers
volunteer_ids = []
volunteers = Volunteer.where(approved: false)
volunteers.each do |v|
if v.id.in?(params[:volunteers])
volunteer_ids << v.id
end
end
Volunteer.where('id IN (?)', volunteer_ids).update(approved: true)
end
See above.
The checkbox code provided by Sam worked perfectly. My url for the view is "update_pending_volunteers_path." I need to improve the formatting a little bit.
The code in the controller that worked to loop through the array of volunteer ids that was passed back into the controller is as below:
def view_pending_volunteers
#navbar = true
#volunteers = Volunteer.where(approved: false)
end
def update_pending_volunteers
#navbar = true
params[:volunteers].each do |value|
Volunteer.find(Integer(value)).update(approved: true)
end
redirect_to (administrator_dashboard_path)
end
The volunteers passed into the view have already been parsed to just those that have not been approved, so the volunteer_ids returned as checked are only from that list.

How should I remove this logic from my view in Rails?

I have a Rails app with events, and when an event's end date has passed I want to include an element on event#show pages indicating that the event is expired.
Right now I have this logic in my view like this:
<% if #event.end_time <= Time.now %>
<h4 class="label--warning">This event has already happened</h4>
<% end %>
I understand it's good to strip logic out of views and I don't want to hit the database from the view, so I'm wondering what the best way to do that would be for a case like this?
If you are loading the page for an #event you have already hit the database. So that code in your view will not generate an extra database query.
The logic in your view (whether or not an event is expired) can be moved to the model. But you're still going to have some code in the view. The nice thing about moving it to the model, however, is that you can easily write tests for the model class.
Model:
def is_expired?
end_time <= Time.now
end
View:
<% if #event.is_expired? %>
<h4 class="label--warning">This event has already happened</h4>
<% end %>
Instead of having conditional logic in the view you could always use view helpers.
=> app/helpers/expired_event.rb
Module ExpiredEvent
def event_expired?(event)
if event.end_time <= Time.now
return "<h4 class="label--warning">This event has already happened</h4>".html_safe
end
end
end
then in your view
<%= event_expired?(#event) %>
Something like that?

Disable upvote button in acts_as_votable gem after an up vote

Context:
I have a forum thread model which acts_as_votable.
The up vote functionality works fine
The up vote button allows the current_user to up vote the forum thread only once, which is the desired functionality. I am trying to change the css of the up vote button to a different color once the current_user had clicked on it.
<%= link_to forum_thread do %>
<h3>
<%= link_to like_forum_thread_path(forum_thread), method: :put, class: "" do %>
<span class="glyphicon glyphicon-arrow-up"></span>
<% end %>
<%= forum_thread.get_upvotes.size %>
<%= forum_thread.subject %>
</h3>
<% end %>
Problem:
One way to do this is to get a list of all the user_id's(using #forum_thread.votes_for_ids) who have up voted the forum thread and check if the current_user user id matches the list. If so, then disable the button. Is there a more efficient way to do this ?
Solution first:
#user.voted_for? #post
Its available in the documentation as well. Here
Now, lets go to the details:
You current solution does the following:
Fetch all the votes on the Post. Ideally, see to it that you get MySQL to return as fewer ActiveRecord objects as possible. Because, there is a considerable performance loss converting each MySQL row into an Active Record.
Looping through all the ActiveRecords and collecting the USER_ID in it. Looping is another performance hindrance, if you could easily avoid it.
Instead, prefer a MySQL query that returns the ROW/Data of what exactly we need, "If the current user had voted on the Post". something like (You would need to use the correct/apt database table names):
PostVote.find_by_post_id_and_user_id(post_id, user_id).
The above returns if the user had voted or not.
Alternatively, Acts_As_Votable does provide the same without you having to do the heavy lifting:
#user.voted_for? #post

Rails - Submit multiple forms on same page

Model:
Users have expenses. Expense has a status.
View:
As users add their expenses, they are shown in a list. Each expense row has a form button on the end, which is used to submit the expense (changing the status of the expense). This allows users to add expenses they have not completely filled out, and submit them when they are ready. There is no parent form on this page, just the form buttons which submit the expense to a method which changes the status, and then reloads the page.
Currently it works great, but users have asked to be able to "submit all" the expenses that are showing on the view with a single button.
Question:
What is the proper way to handle this in rails? Should I find a way to gather the array of expense id's and then submit a separate form? Is there a way to ask for a set of records present in a view with a certain status?
Thanks!
Another option, if I'm thinking about this right (big if), would be to wrap your page in a User form. Then you could have something like...
<%= form_for(#user) do |f| %>
<% #user.expenses.each do |expense| %>
<% f.fields_for expense do |e| %>
<!-- expense form -->
<% end %>
<% end >
<% end %>
This is something you could submit as a whole. I'm having trouble picturing what a single expense addition might look like, but hopefully this gets you a little further down the road.
Edit: in addition to having this User form on the page, you could have an "extra" Expense form to create an expense. When you submit a new expense, that expense appears in the list under the user form, where it can be edited or submitted, either as part of a group or individually (as part of a "group" of 1).
custom controller action:
def update_all_expense_statuses
expenses = current_user.expenses
ExpenseUpdater.new(expenses).update_expense
redirect_to :back
end
expense updater class:
class ExpenseUpdater
def initialize(expenses)
#expenses = expenses
end
def update_expense
#expenses.each do |expense|
expense.update_attributes(status: 'paid')
expense.save
end
end
end
This is just an example of one way to update all the user's expenses with a custom controller action. Just call the controller method from a link_to:
<%= link_to "Update all expenses", update_all_expense_statuses_path %>
Remember to add it to your routes. Hope this helps.
The first thing you should do is change the forms to submit remotely, ie make an ajax request. Then you're not reloading the whole page. Check out Rails' various "remote" form helpers, eg "remote_form_for".
Then, write a javascript function to submit all the forms for inputs that have changed since the page loaded. You'd probably want to add a "changed" (or similar) class to the parent form in an onchange event in each input, to facilitate this. I think this is the best way to handle the "status" thing you're asking about. Make a "Submit all" button which calls this function.
Use a form/service object http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ to encapsulate expense report

How to hide parts of the view given a user role on Rails 4

I'm trying to hide parts of my views depending on the User role.
So let's say I want only admins to be able to destroy Products. Besides the code in the controller for preventing regular users from destroying records, I would do the following in the view:
<% if current_user.admin? %>
<%= link_to 'Delete', product, method: :delete %>
<% end %>
The previous code works, but it's prone to errors of omission, which may cause regular users to see links to actions they are not allowed to execute.
Also, if I decide later on that a new role (e.g. "moderator") can delete Products, I would have to find the views that display a delete link and add the logic allowing moderators to see it.
And if there are many models that can be deleted only by admin users (e.g. Promotion, User) maitenance of all the ifs would be pretty challenging.
Is there a better way of doing it? Maybe using helpers, or something similar? I'm looking for something maybe like this:
<%= destroy_link 'Delete', product %> # Only admins can see it
<%= edit_link 'Edit', promotion %> # Again, only admins see this link
<%= show_link 'Show', comment %> # Everyone sees this one
I found these two questions that are similar to mine, but none of them answered my question:
Show and hide based on user role in rails
Ruby on Rails (3) hiding parts of the view
I strongly recommend pundit.
It allows you to create "policies" for each model. For your Product model you might have a ProductPolicy that looks something like this
class ProductPolicy < ApplicationPolicy
def delete?
user.admin?
end
end
In your view you can do something like this
<% if policy(#post).delete? %>
<%= link_to 'Delete', product, method: :delete %>
<% end %>
If later on you want to add a moderator role, just modify the policy method
class ProductPolicy < ApplicationPolicy
def delete?
user.admin? || user.moderator?
end
end
So I kind of figured a way to move the IFs out of the view. First, I override the link_to helper in my application_helper.rb:
def link_to(text, path, options={})
super(text, path, options) unless options[:admin] and !current_user.admin?
end
Then on my views I use it as:
<%= link_to 'Edit Product', product, admin: true, ... %>
This prevents regular users from seeing admin links, but for other html tags with content inside, such as divs, tables etc., an if would still be needed.
CanCan is another gem that lets you define "Abilities" per user role.
In views you can use something like if can? :delete, #post to check if the
user may delete that specific post.
Using the CanCan and Role gems, what is still needed is a way to Check The Route and see if "current_user" has permissions to access that Route based on their role(s) - then show/hide based on that.
This saves the user clicking on things and getting told they cannot see it - or us having to write per-item "if" logic specifying what roles can see what list-items (which the customer will change periodically, as roles are changed/refined) around every single link in one's menu (consider a bootstrap menu with 50+ items nested in groups with html formatting, etc), which is insane.
If we must put if-logic around each menu-item, let's use the exact same logic for every item by checking the role/permissions we already defined in the Ability file.
But in our menu-list, we have route-helpers - not "controller/method" info, so how to test the user's ability to hit the controller-action specified for the "path" in each link?
To get the controller and method (action) of a path (my examples use the 'users_path' route-helper) ...
Rails.application.routes.recognize_path(app.users_path)
=> {:controller=>"users", :action=>"index"}
Get just the controller-name
Rails.application.routes.recognize_path(app.users_path)[:controller]
=> "users"
Ability uses the Model for its breakdown, so convert from controller name to it's model (assuming default naming used) ...
Rails.application.routes.recognize_path(app.users_path)[:controller].classify
=> "User"
Get just the action-name
Rails.application.routes.recognize_path(app.users_path)[:action]
=> "index"
And since the "can?" method needs a Symbol for the action, and Constant for the model, for each menu-item we get this:
path_hash = Rails.application.routes.recognize_path(app.users_path)
model = path_hash[:controller].classify.constantize
action = path_hash[:action].to_sym
Then use our existing Abilty system to check if the current_user can access it, we have to pass the action as a symbol and the Model as a constant, so ...
<% if can? action model %>
<%= link_to "Users List", users_path %>
<% end %>
Now we can change who can see this resource and link from the Ability file, without ever messing with the menu, again. But to make this a bit cleaner, I extracted out the lookup for each menu-item with this in the app-controller:
def get_path_parts(path)
path_hash = Rails.application.routes.recognize_path(path)
model_name = path_hash[:controller].classify.constantize
action_name = path_hash[:action].to_sym
return [model_name, action_name]
end
helper_method :get_path_parts
... so I could do this in the view (I took out all the html-formatting from the links for simplicity, here):
<% path_parts = get_path_parts(users_path); if can?(path_parts[1], path_parts[0]) %>
<%= link_to "Users Listing", users_path %>
<% end %>
... and to make this not take all day typing these per-menu-item if-wraps, I used regex find/replace with capture and wildcards to wrap this around every list-item in the menu-item listing in one pass.
It's far from ideal, and I could do a lot more to make it much better, but I don't have spare-time to write the rest of this missing-piece of the Role/CanCan system. I hope this part helps someone out.

Resources