Skip to content

Commit 8c87576

Browse files
committed
RoR Code Sample
0 parents  commit 8c87576

File tree

10 files changed

+678
-0
lines changed

10 files changed

+678
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.idea

RubyOnRails/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## Deck Creation & Collaboration Tool
2+
3+
4+
About Deck Creation & Collaboration Tool: Deck Creation & Collaboration Tool is application software which automate every aspect of Presentation creation.
5+
You can search slides within your organization, request a new slide design from your team or a external team,
6+
get notified of on going developemt on it, manage different version of a slide and combine a no of slide into
7+
a power point presentation with Click of button.
8+
9+
It treats a Presentation as a project and make it reusable within the organization.
10+
11+
Details of files included:
12+
13+
Project Controller : Treat Power Point presentation as a project. The dashboard action list all the ongoing presentation within
14+
the team and allow the user to create a new one. The edit action provide ability to add or edit team memeber,
15+
tasks, calender events, meetings, coordinator , owner etc. It also show Time line showing different activiies
16+
on project in the rcent time. User get notified of any new notification in real time.
17+
18+
Slide Controller : It manage the process of power point creation. The core action of this controller is desktop. The view is
19+
broadly divided into PPT carousel, Slide Library and incoming section, User can select any slides from
20+
library or the incoming section and drag it into PPt carousel, he can suffel the slide within the PPT carousel.
21+
a Slide within the library can be searched based on text, category or tags.When user click Generate PPT button
22+
all the slides in the carousel are sent to Merger server which combine it in PPt and send it to the user email.
23+
24+
User Model: It handles different user associations, authentication and authorization
25+
26+
Project Model : It handles project business logic like due date, team members, tasks, meetings etc
27+
28+
Slide Model : It implements Slide search within the library based on category, text and tags
29+
30+
Lib/custom_cropper : It override paperclip's trasform method to support image cropping
31+
32+
Lib/drive : It syncs google slide from user's google drive in the application
33+
34+
Lib/tasks/import_country_states.rake : It import country and state list from CSV into DB
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
class Api::GslidesController < ApplicationController
2+
# This filter will be replaced by the common API auth filter, once implemented
3+
before_action :authenticate_user!, except: :completion_notifier
4+
5+
rescue_from ActiveRecord::RecordNotFound, with: :not_found
6+
7+
# google_slides should be enabled to consume the API
8+
require_feature :google_slides, except: :completion_notifier
9+
10+
# Notifies completion of the Assigned slide
11+
def completion_notifier
12+
# TODO: Add another constraint: (assigned_to_id: current_user.id),
13+
# once Authentication for API is in place
14+
pslide = ProjectSlide.find_by!(
15+
slide_id: params[:slide_id],
16+
project_id: google_slide_params[:project_id]
17+
)
18+
user = User.find(pslide.assigned_to_id) # After authentication, use current_user instead of this line of code
19+
GslideImportJob.perform_later(user, pslide.project, pslide.slide)
20+
head :ok
21+
end
22+
23+
# List slides assigned to `current_user`
24+
def list_assigned
25+
@assigned_project_slides = ProjectSlide.joins(:slide)
26+
.where('project_slides.project_id = ? AND project_slides.assigned_to_id = ? AND slides.gurl IS NOT ?',
27+
list_assigned_params[:project_id], current_user, nil)
28+
end
29+
30+
# TODO: Assumption here is that current_user already have an access_token and refresh_token
31+
# available, which at most can expire and we will re-authorize it.
32+
# Ask for google drive access if not given by the user here. <eom>
33+
# This returns temporary thumbnail url for assigned slide
34+
def assigned_slide_url
35+
pslide = ProjectSlide.find_by(id: params[:project_slide_id], assigned_to_id: current_user)
36+
gsip = Google::SlideImageProcessor.new(current_user, pslide)
37+
begin
38+
thumb_name, raw_name = gsip.get_image_paths
39+
@thumb_url = api_slide_temp_images_path(project_slide_id: pslide, image_name: thumb_name)
40+
@raw_url = api_slide_temp_images_path(project_slide_id: pslide, image_name: raw_name)
41+
rescue RestClient::Unauthorized => e
42+
head 401
43+
end
44+
end
45+
46+
# This method responds to an Ajax call called within the application domain with logged in user
47+
# Uses Devise session, API version yet to be included
48+
def assigned_slide_completed
49+
pslide = ProjectSlide.find(assigned_slide_completed_params[:project_slide_id])
50+
GslideImportJob.perform_later(current_user, pslide.project, pslide.slide)
51+
52+
head :ok
53+
end
54+
55+
# Send image preview of requested slide after an authorization check
56+
def slide_temp_images
57+
pslide = ProjectSlide.includes(:slide).find(params[:project_slide_id])
58+
if authorize pslide.slide, :slide_assigned?
59+
img_location = pslide_image_path(pslide).join(params[:image_name])
60+
send_file img_location
61+
else
62+
head 401
63+
end
64+
end
65+
66+
private
67+
68+
# Derive path for temporary Slide preview image
69+
def pslide_image_path(pslide)
70+
derived_path = Google::SlideImageProcessor::BASE_TEMP_PATH
71+
.join(Google::SlideImageProcessor.slide_dir_name(current_user.id, pslide))
72+
head 404 unless File.directory? derived_path
73+
derived_path
74+
end
75+
76+
# Returns 404 status
77+
def not_found
78+
head :not_found
79+
end
80+
81+
# StrongParams filters
82+
83+
def list_assigned_params
84+
params.permit(:project_id)
85+
end
86+
87+
def assigned_slide_completed_params
88+
params.permit(:project_slide_id)
89+
end
90+
91+
def google_slide_params
92+
params.require(:gslide_params).permit(:project_id, :user_id)
93+
end
94+
end

RubyOnRails/lib/drive.rb

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
module Google
2+
module Drive
3+
4+
def self.included(base)
5+
base.send :include, InstanceMethods
6+
end
7+
8+
module InstanceMethods
9+
#retrieve new Google AccessToken if it get expired
10+
def refresh_access_token(user)
11+
access_token_age = (Time.zone.now - user.google_token_updated_at)
12+
if access_token_age > APP_CONSTANTS["google_token_expire_time"]
13+
client_id = Rails.application.secrets.google_drive["client_id"]
14+
client_secret = Rails.application.secrets.google_drive["client_secret"]
15+
payload = "client_id=#{client_id}&client_secret=#{client_secret}&refresh_token=#{user.google_refresh_token}&grant_type=refresh_token"
16+
rest_resource = RestClient::Resource.new("https://www.googleapis.com/oauth2/v3/token")
17+
response = rest_resource.post payload , :content_type => 'application/x-www-form-urlencoded'
18+
new_access_token = JSON.parse(response)["access_token"]
19+
# update user with new Google access token
20+
user.update(google_access_token: new_access_token, google_token_updated_at: Time.now)
21+
end
22+
end
23+
24+
# Delete the file with the file_id from google drive using Google Drive API
25+
def delete_current_presentation(file_id, user)
26+
delete_url = "https://www.googleapis.com/drive/v2/files/#{file_id}"
27+
headers = { 'Authorization': "Bearer #{user.google_access_token}", 'Content-type': 'application/json' }
28+
rest_resource = RestClient::Resource.new(delete_url, :headers => headers)
29+
rest_resource.delete
30+
end
31+
32+
# Get presentation_id from Slide or Presentation URL
33+
def get_presentation_id(slide_url)
34+
starting_index = slide_url.index("/d/")
35+
# Following string matching seems to be lengthy, But is faster the Regular Exp.
36+
ending_index = slide_url.index("/present#slide") || slide_url.index("/present?slide") || slide_url.index("/edit#slide") || slide_url.index("/edit?slide")
37+
return nil unless ending_index and starting_index
38+
presentation_id = slide_url[starting_index + 3..ending_index-1]
39+
end
40+
41+
# Extract slide_id from google presentation/slide url
42+
def get_slide_id( slide_url )
43+
idx = slide_url.index( "=id." )
44+
return nil unless idx
45+
slide_url.slice( idx + 4..-1 )
46+
end
47+
48+
# Returns true if page contains text matching known messages of Unauthorization
49+
def unauthorized?(browser_text)
50+
!!(browser_text.match(/unauthorized/i) || browser_text.match(/You have been signed out/i))
51+
end
52+
53+
# You can see google presentation in 2 modes.
54+
# This returns string :present if slide url contains `/present?slide_id`||`/present#slide_id`
55+
# Or returns string :edit if slide url contains `/edit#slide_id`||`/edit?slide_id`
56+
def slide_url_mode(slide_url)
57+
return :edit if slide_url.match(/\/edit(#|\?)/)
58+
return :presentation if slide_url.match(/\/present(#|\?)/)
59+
end
60+
61+
# Identifying whether it is a SlideUrl in presentation mode or Edit mode and toggles the state
62+
# toggle_edit_present_slide_url('edit_url') #=> present_url
63+
# toggle_edit_present_slide_url('present_url') #=> edit_url
64+
# Warning : Do not use single regex for matching, Performance issues may occur
65+
def toggle_edit_present_slide_url(slide_url)
66+
# Identifying whether it is a SlideUrl in presentation mode or Edit mode
67+
present_matchers = slide_url.match("/present#slide")
68+
69+
case slide_url.match(/\/(present|edit)(#|\?)/).to_s
70+
when "/present#"
71+
return slide_url.gsub("/present#", "/edit#")
72+
when "/present?"
73+
# return slide_url.gsub("/present?", "/edit#")
74+
uri = Addressable::URI.parse(slide_url.gsub('/present?', '/edit?'))
75+
query_values = uri.query_values || {}
76+
uri.fragment = "slide=" + query_values.delete('slide') if query_values.present?
77+
uri.query_values = query_values.present? ? query_values : nil
78+
return uri.to_s
79+
when "/edit#", "/edit?"
80+
return slide_url.gsub(/\/edit(#|\?)/, "/present?")
81+
else
82+
return nil
83+
end
84+
end
85+
end
86+
87+
end
88+
end
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
module Google
2+
# Fetches raw slide preview and processes it into thumbnail
3+
class SlideImageProcessor
4+
include Drive
5+
6+
# Parent directory path for storing images sub-directories
7+
BASE_TEMP_PATH = Rails.root.join('temp_slide_images')
8+
DIR_PREFIX = 'slide_image_dir_'.freeze
9+
THUMB_PREFIX = 'processed_slide_thumb_'.freeze
10+
RAW_IMAGE_PREFIX = 'raw_slide_thumb_'.freeze
11+
IMAGE_EXTENSION = '.png'.freeze
12+
13+
DEFAULTS = { thumbnail_size: '150x150' }.freeze
14+
15+
attr_reader :temp_thumb_path, :raw_image_path
16+
17+
#
18+
# SlideImageProcessor.new( <user>, <project_slide> )
19+
#
20+
def initialize(user, project_slide)
21+
@user = user
22+
@project_slide = project_slide
23+
@slide = @project_slide.slide
24+
25+
verify_thumb_dir
26+
end
27+
28+
# .get_image_paths( '<width>x<height>'(optional) )
29+
def get_image_paths(dimensions = DEFAULTS[:thumbnail_size])
30+
set_images_path
31+
32+
# Will return nil at success
33+
return false if fetch_raw_image
34+
35+
image = MiniMagick::Image.open @raw_image_path
36+
begin
37+
image.resize dimensions
38+
rescue MiniMagick::Error => e
39+
raise "Cannot resize to #{dimensions} - #{e.message}"
40+
end
41+
image.write @temp_thumb_path
42+
[@temp_thumb_path.split.last, @raw_image_path.split.last]
43+
end
44+
45+
# #slide_dir_name( <user_id>, <project_slide_object> )
46+
def self.slide_dir_name(user_id, project_slide)
47+
"#{DIR_PREFIX}#{user_id}_#{project_slide.project_id}_#{project_slide.id}"
48+
end
49+
50+
private
51+
52+
# Delete raw image
53+
def cleanup_raw_image
54+
File.delete(@raw_image_path) if File.exist?(@raw_image_path)
55+
end
56+
57+
# Downloading slide preview from Google Docs
58+
def fetch_raw_image
59+
r_header = { 'Authorization' => "Bearer #{@user.google_access_token}" }
60+
response = RestClient::Request.execute(method: :get, url: slide_download_url, headers: r_header)
61+
img_file = File.open(@raw_image_path, 'wb')
62+
img_file.write response
63+
img_file.close
64+
rescue RestClient::Unauthorized => e
65+
puts e.inspect
66+
raise RestClient::Unauthorized unless re_authorize
67+
retry
68+
end
69+
70+
def set_images_path
71+
random_string = SecureRandom.hex
72+
@temp_thumb_path ||= Pathname.new(@tmp_slide_dir).join("#{THUMB_PREFIX}#{random_string}#{IMAGE_EXTENSION}")
73+
@raw_image_path ||= Pathname.new(@tmp_slide_dir).join("#{RAW_IMAGE_PREFIX}#{random_string}#{IMAGE_EXTENSION}")
74+
end
75+
76+
# Creates directory for keeping thumbnail
77+
# naming convention : <DIR_PREFIX>_<user_id>_<project_id>_<project_slide_id>
78+
def verify_thumb_dir
79+
temp_dir_name = self.class.slide_dir_name(@user.id, @project_slide)
80+
@tmp_slide_dir = BASE_TEMP_PATH.join(temp_dir_name)
81+
if File.directory? @tmp_slide_dir
82+
FileUtils.rm_r(Dir.glob("#{@tmp_slide_dir}/*"))
83+
else
84+
Dir.mkdir(@tmp_slide_dir)
85+
end
86+
end
87+
88+
# Refresh auth token, with a retry pre-set limit
89+
def re_authorize
90+
@re_auth_tries ||= APP_CONSTANTS['authorization_tries_limit']
91+
return false if @re_auth_tries < 1
92+
refresh_access_token(@user)
93+
@user.reload
94+
@re_auth_tries -= 1
95+
end
96+
97+
# Google Docs download url, dependent on service provider(Google Docs)
98+
def slide_download_url
99+
p_id = get_presentation_id(@slide.gurl)
100+
s_id = get_slide_id(@slide.gurl)
101+
'https://docs.google.com/presentation/d/' + p_id + '/export/png?id=' + p_id + '&pageid=' + s_id
102+
end
103+
end
104+
end
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
require 'rails_helper'
2+
3+
RSpec.describe Api::GslidesController, type: :controller do
4+
describe '#completion_notifier' do
5+
# google_slides/:slide_id
6+
let(:user) do
7+
user = create(:user)
8+
user.skip_confirmation!
9+
user
10+
end
11+
let(:project) { create(:project) }
12+
let(:slide) { create('slide1', title: 'Library Slide1') }
13+
let(:project_slide) do
14+
create(:project_slide1, slide_id: slide.id,
15+
project: project, assigned_to_id: user.id)
16+
end
17+
18+
context 'When valid params are passed' do
19+
it 'should enqueue background job' do
20+
expect do
21+
response = get :completion_notifier, slide_id: slide.id, controller: :gslides,
22+
action: :completion_notifier,
23+
gslide_params: { project_id: project.id, user_id: user.id }
24+
end.to change {
25+
Delayed::Job.count
26+
}.by(1)
27+
expect(response.success?).to be_truthy
28+
end
29+
end
30+
31+
context 'When invalid params are passed' do
32+
it 'should respond with 404' do
33+
expect do
34+
response = get :completion_notifier,
35+
slide_id: 'wrong_id',
36+
gslide_params: { project_id: 'wrong_id', user_id: 'wrong_id' }
37+
end.to change {
38+
Delayed::Job.count
39+
}.by(0)
40+
expect(response.success?).to be_falsey
41+
end
42+
end
43+
end
44+
end

0 commit comments

Comments
 (0)