Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] OH integration #438

Merged
merged 38 commits into from Nov 21, 2017
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6c0b965
started OH integration
gedankenstuecke Nov 14, 2017
045ecf9
further work on integration
gedankenstuecke Nov 14, 2017
6e3831d
hound happiness
gedankenstuecke Nov 14, 2017
2137966
Merge branch 'master' into oh-integration
gedankenstuecke Nov 14, 2017
ffb92bb
put OH logic into service
gedankenstuecke Nov 15, 2017
b4de79a
Merge branch 'oh-integration' of https://github.com/openSNP/snpr into…
gedankenstuecke Nov 15, 2017
a562047
🐶
gedankenstuecke Nov 15, 2017
74eaa46
🐶
gedankenstuecke Nov 15, 2017
d4f875c
🐶
gedankenstuecke Nov 15, 2017
63bed79
🐶
gedankenstuecke Nov 15, 2017
7ad4dc3
🐶
gedankenstuecke Nov 15, 2017
3a640e4
added sql update
gedankenstuecke Nov 15, 2017
351c023
further refactor
gedankenstuecke Nov 15, 2017
fecae22
🐶
gedankenstuecke Nov 15, 2017
136a182
🐶
gedankenstuecke Nov 15, 2017
bafefe4
🐶
gedankenstuecke Nov 15, 2017
abae3aa
fix rubocop on codeclimate
gedankenstuecke Nov 15, 2017
682d03f
rubocop...
gedankenstuecke Nov 15, 2017
38b9218
rubocop...
gedankenstuecke Nov 15, 2017
685f591
Revert "rubocop..."
gedankenstuecke Nov 15, 2017
5190efc
Revert "rubocop..."
gedankenstuecke Nov 15, 2017
b343b70
Revert "fix rubocop on codeclimate"
gedankenstuecke Nov 15, 2017
4f5686b
views
gedankenstuecke Nov 15, 2017
83b8cd7
🐶
gedankenstuecke Nov 15, 2017
736fd08
🐶
gedankenstuecke Nov 15, 2017
191cb4b
🐶
gedankenstuecke Nov 15, 2017
e55ec4b
🐶
gedankenstuecke Nov 15, 2017
4089227
added views
gedankenstuecke Nov 16, 2017
d8bad8b
removed active fitbit connection
gedankenstuecke Nov 16, 2017
953d10b
🐶
gedankenstuecke Nov 17, 2017
925daaf
Merge branch 'master' into oh-integration
gedankenstuecke Nov 17, 2017
2d7797b
comments of helge
gedankenstuecke Nov 17, 2017
a039afe
🐶
gedankenstuecke Nov 17, 2017
35f19cb
🐶
gedankenstuecke Nov 17, 2017
9fcdc3c
making rubocop happy 🤖
gedankenstuecke Nov 17, 2017
ae2d762
added some tests for OH connections
gedankenstuecke Nov 17, 2017
097be95
🐶
gedankenstuecke Nov 17, 2017
ede8af2
🐶
gedankenstuecke Nov 17, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 33 additions & 0 deletions app/controllers/open_humans_profiles_controller.rb
@@ -0,0 +1,33 @@
# frozen_string_literal: true

class OpenHumansProfilesController < ApplicationController
before_filter :require_user, except: [:index]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer before_action over before_filter.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer before_action over before_filter.


def start_auth
# there's little to do for the start. Just read the .env for our client_id
# then lead ppl to OH.org to give us a key
client_id = ENV.fetch('OH_client_id')
redirect_to "https://www.openhumans.org/direct-sharing/projects/oauth2/authorize/?client_id=#{client_id}&response_type=code"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long. [128/100]

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long. [128/100]

end

def authorize
# let's get the current user and their code
@user = current_user
@code = params[:code]
oh_service = OpenHumansService.new()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use parentheses for method calls with no arguments.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use parentheses for method calls with no arguments.


# does the user have an OH profile on openSNP? if not, create one
if @user.open_humans_profile == nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer the use of the nil? predicate.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer the use of the nil? predicate.

@user.open_humans_profile = OpenHumansProfile.new
@user.save
end
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was confused because an OpenHumansProfile is assigned but not saved to the database, which happens later in the OpenHumansService. Maybe you move the assignment into the service as well. Seems to be a good place to do that.

# lets convert
oh_service.get_access_tokens(@user,@code)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing after comma.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing after comma.

oh_service.set_open_humans_ids(@user.open_humans_profile)
# delete old files if there are any
begin
oh_service.delete_opensnp_id(@user)
end
oh_service.upload_opensnp_id(@user)
end
end
5 changes: 5 additions & 0 deletions app/models/open_humans_profile.rb
@@ -0,0 +1,5 @@
# frozen_string_literal: true

class OpenHumansProfile < ActiveRecord::Base
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an empty line after magic comments.
Models should subclass ApplicationRecord.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an empty line after magic comments.
Models should subclass ApplicationRecord.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Models should subclass ApplicationRecord.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For that we'd need Rails5, first... 🐩

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly do you mean here? Follows the same convention as all other models?

Copy link
Collaborator

@tsujigiri tsujigiri Nov 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rubocop wants the models to subclass a common ApplicationRecord class. I tried doing this in another project using Rails 4 and it didn't work. But, thinking about it, this may have been due to some gem we are using. Long story short, just ignore it for now. We can do this separately.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok!

belongs_to :user
end
3 changes: 2 additions & 1 deletion app/models/user.rb
Expand Up @@ -13,7 +13,7 @@ class User < ActiveRecord::Base
content_type: %r(\Aimage/.*\Z)

# call on authlogic
acts_as_authentic do |c|
acts_as_authentic do |c|
# replace SHA512 by bcrypt
c.transition_from_crypto_providers = Authlogic::CryptoProviders::Sha512
c.crypto_provider = Authlogic::CryptoProviders::BCrypt
Expand All @@ -36,6 +36,7 @@ class User < ActiveRecord::Base
has_many :phenotype_comments, dependent: :destroy
has_many :picture_phenotype_comments, dependent: :destroy
has_one :fitbit_profile, dependent: :destroy
has_one :open_humans_profile, dependent: :destroy

# needed to edit several user_phenotypes at once, add and delete, and not empty
accepts_nested_attributes_for :homepages, allow_destroy: true
Expand Down
116 changes: 116 additions & 0 deletions app/services/open_humans_service.rb
@@ -0,0 +1,116 @@
class OpenHumansService
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing magic comment # frozen_string_literal: true.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is fixed…

# used to do the interfacing w/ Open Humans

def get_access_tokens(user,code)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing after comma.

# authenticate w/ our client id/secret against API
# post with the key the user provided us with.
url = URI.parse('https://www.openhumans.org/oauth2/token/')
req = Net::HTTP::Post.new(url.request_uri)
req.basic_auth ENV.fetch('OH_client_id'), ENV.fetch('OH_client_secret')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The convention for environment variables is all-caps.

req.set_form_data({'grant_type' => 'authorization_code',
'code' => code,
'redirect_uri' => "http://localhost:3000/openhumans/authorize"
})
# set up request to use https
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = (url.scheme == 'https')
# do actual request, get the json it returns
response = http.request(req)
response_json = JSON.parse(response.body)
update_tokens(user.open_humans_profile,response_json)
end

def refresh_token(user)
oh_profile = user.open_humans_profile
url = URI.parse('https://www.openhumans.org/oauth2/token/')
req = Net::HTTP::Post.new(url.request_uri)
req.set_form_data({'grant_type' => 'refresh_token',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space inside { missing.
Redundant curly braces around a hash parameter.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space inside { missing.
Redundant curly braces around a hash parameter.

'refresh_token' => oh_profile.refresh_token
})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closing hash brace must be on the same line as the last hash element when opening brace is on the same line as the first hash element.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closing hash brace must be on the same line as the last hash element when opening brace is on the same line as the first hash element.

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = (url.scheme == 'https')
response = http.request(req)
response_json = JSON.parse(response.body)
update_tokens(oh_profile,response_json)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing after comma.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing after comma.

end


Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra blank line detected.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra blank line detected.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra blank line detected.

def set_open_humans_ids(oh_profile)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use empty lines between method definitions.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use empty lines between method definitions.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use empty lines between method definitions.

uri = URI.parse('https://www.openhumans.org/api/direct-sharing/project/exchange-member/')
uri_params = {:access_token => oh_profile.access_token}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space inside { missing.
Use the new Ruby 1.9 hash syntax.
Space inside } missing.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space inside { missing.
Use the new Ruby 1.9 hash syntax.
Space inside } missing.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space inside { missing.
Use the new Ruby 1.9 hash syntax.
Space inside } missing.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space inside { missing.
Use the new Ruby 1.9 hash syntax.
Space inside } missing.

uri.query = URI.encode_www_form(uri_params)
res = Net::HTTP.get_response(uri)
res_json = JSON.parse(res.body)
oh_profile.project_member_id = res_json["project_member_id"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer single-quoted strings when you don't need string interpolation or special symbols.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer single-quoted strings when you don't need string interpolation or special symbols.

oh_profile.open_humans_user_id = res_json["username"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer single-quoted strings when you don't need string interpolation or special symbols.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer single-quoted strings when you don't need string interpolation or special symbols.

oh_profile.save
end

def upload_opensnp_id(user)
uri = URI.parse("https://www.openhumans.org/api/direct-sharing/project/files/upload/?access_token=#{user.open_humans_profile.access_token}")
boundary = '0P3NSNPH34RT50PENHUM4N5'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could put that into a constant, too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done :)

http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https')
upload = Net::HTTP::Post.new(uri.request_uri)
upload.content_type = "multipart/form-data; boundary=#{boundary}"
post_body = generate_form_body(user,boundary)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing after comma.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing after comma.

upload.body = post_body.join
puts post_body.join
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not write to stdout. Use Rails's logger if you want to log.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not write to stdout. Use Rails's logger if you want to log.

http.request(upload)
end

def delete_opensnp_id(user)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you use the user all over the place, how about passing it into the initializer and keeping it as an instance variable?

url = URI.parse("https://www.openhumans.org/api/direct-sharing/project/files/delete/?access_token=#{user.open_humans_profile.access_token}")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long. [144/100]

req = Net::HTTP::Post.new(url.request_uri)
oh_profile = user.open_humans_profile
req.set_form_data({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant curly braces around a hash parameter.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant curly braces around a hash parameter.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant curly braces around a hash parameter.

project_member_id: oh_profile.project_member_id,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use 2 spaces for indentation in a hash, relative to the first position after the preceding left parenthesis.

all_files: true
})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indent the right brace the same as the first position after the preceding left parenthesis.

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = (url.scheme == 'https')
http.set_debug_output($stdout)
http.request(req)
end

private

def update_tokens(oh_profile,tokens)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing after comma.

oh_profile.expires_in = Time.now + tokens["expires_in"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use Time.now without zone. Use one of Time.zone.now, Time.current, Time.now.in_time_zone, Time.now.utc, Time.now.getlocal, Time.now.iso8601, Time.now.jisx0301, Time.now.rfc3339, Time.now.to_i, Time.now.to_f instead.
Prefer single-quoted strings when you don't need string interpolation or special symbols.

oh_profile.access_token = tokens["access_token"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer single-quoted strings when you don't need string interpolation or special symbols.

oh_profile.refresh_token = tokens["refresh_token"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer single-quoted strings when you don't need string interpolation or special symbols.

oh_profile.save
end

def generate_json(user)
json = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Useless assignment to variable - json.

user_name: user.name,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use 2 spaces for indentation in a hash, relative to the start of the line where the left curly brace is.

user_id: user.id,
has_sequence: user.has_sequence,
user_uri: "https://opensnp.org/users/#{user.id}"
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indent the right brace the same as the start of the line where the left brace is.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closing hash brace must be on the same line as the last hash element when opening brace is on the same line as the first hash element.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closing hash brace must be on the same line as the last hash element when opening brace is on the same line as the first hash element.

end

def generate_metadata(user)
metadata = generate_json(user)
metadata['tags'] = ['opensnp']
metadata['description'] = "links to openSNP user #{user.id}"
return metadata
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant return detected.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is actually not redundant as it otherwise breaks the hash…

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be pretty redundant, as it is the last expression in the method. Am I missing something?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know. If I'm not running the return it only returns "links to openSNP user #{user.id}" instead of the whole hash. That's why I put it back in, otherwise the OH API doesn't accept our upload.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aaaaah! Now I get it. 💡 You don't need the return, but you still need the metadata. Without the metadata in the end, it returns the return value of the assignment above it, which is the value you assign.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant return detected.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant return detected.

end

def generate_form_body(user,boundary)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space missing after comma.

metadata = generate_metadata(user)
file_content = generate_json(user)
post_body = []
post_body << "--#{boundary}\r\n"
post_body << "Content-Disposition: form-data; name=\"project_member_id\"\r\n\r\n"
post_body << user.open_humans_profile.project_member_id
post_body << "\r\n--#{boundary}\r\n"
post_body << "Content-Disposition: form-data; name=\"metadata\"\r\n\r\n"
post_body << metadata.to_json
post_body << "\r\n--#{boundary}\r\n"
post_body << "Content-Disposition: form-data; name=\"data_file\"; filename=\"#{user.id}.json\"\r\n\r\n"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line is too long. [107/100]

post_body << file_content.to_json
post_body << "\r\n\r\n--#{boundary}--\r\n"
end
end
7 changes: 7 additions & 0 deletions app/views/open_humans_profiles/authorize.html.erb
@@ -0,0 +1,7 @@
<div class="general__container container">
<div class="row">
<div class="col-md-6">
<h3>Authorize</h3>
</div>
</div>
</div>
2 changes: 2 additions & 0 deletions config/routes.rb
Expand Up @@ -35,6 +35,8 @@
resources :user_achievements
resources :index

get '/openhumans/new', to: 'open_humans_profiles#start_auth'
get '/openhumans/authorize', to: 'open_humans_profiles#authorize'
post '/fitbit/notification/', to: 'fitbit_profiles#new_notification'
get '/fitbit/start_auth', to: 'fitbit_profiles#start_auth'
get '/fitbit/verify', to: 'fitbit_profiles#verify_auth'
Expand Down
19 changes: 19 additions & 0 deletions db/migrate/20171113104813_create_open_humans_profiles.rb
@@ -0,0 +1,19 @@
# frozen_string_literal: true
class CreateOpenHumansProfiles < ActiveRecord::Migration
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an empty line after magic comments.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an empty line after magic comments.

def self.up
create_table :open_humans_profiles do |t|
t.string :open_humans_user_id
t.string :project_member_id
t.belongs_to :user
t.string :access_token
t.string :refresh_token
t.timestamp :expires_in
t.timestamps
end
end

def self.down
drop_table :open_humans_profiles
end

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra empty line detected at class body end.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra empty line detected at class body end.

end