Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
cs_comments_service
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
cs_comments_service
Commits
6a27f7aa
Commit
6a27f7aa
authored
Mar 29, 2013
by
Kevin Chugh
Browse files
Options
Browse Files
Download
Plain Diff
fix mega merge conflicts
parents
605fc702
ac7a0a32
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
341 additions
and
17 deletions
+341
-17
.gitignore
+3
-1
api/comment_threads.rb
+14
-3
api/commentables.rb
+15
-4
api/pins.rb
+8
-0
app.rb
+2
-1
config/mongoid.yml
+26
-0
config/newrelic.yml
+11
-0
lib/helpers.rb
+19
-1
lib/tasks/kpis.rake
+158
-0
models/comment_thread.rb
+24
-5
models/content.rb
+61
-2
No files found.
.gitignore
View file @
6a27f7aa
...
@@ -30,4 +30,6 @@ benchmark_log
...
@@ -30,4 +30,6 @@ benchmark_log
bin/
bin/
log/
log/
/.redcar
#redcar
.redcar/
/nbproject
api/comment_threads.rb
View file @
6a27f7aa
get
"
#{
APIPREFIX
}
/threads"
do
# retrieve threads by course
get
"
#{
APIPREFIX
}
/threads"
do
# retrieve threads by course
handle_threads_query
(
CommentThread
.
where
(
course_id:
params
[
"course_id"
]))
#if a group id is sent, then process the set of threads with that group id or with no group id
if
params
[
"group_id"
]
threads
=
CommentThread
.
any_of
(
{
:course_id
=>
params
[
"course_id"
],
:group_id
=>
params
[
:group_id
]},
{
:course_id
=>
params
[
"course_id"
],
:group_id
.
exists
=>
false
},
)
else
threads
=
CommentThread
.
where
(
course_id:
params
[
"course_id"
])
#else process them all
end
handle_threads_query
(
threads
)
end
end
get
"
#{
APIPREFIX
}
/threads/:thread_id"
do
|
thread_id
|
get
"
#{
APIPREFIX
}
/threads/:thread_id"
do
|
thread_id
|
...
@@ -14,12 +24,13 @@ get "#{APIPREFIX}/threads/:thread_id" do |thread_id|
...
@@ -14,12 +24,13 @@ get "#{APIPREFIX}/threads/:thread_id" do |thread_id|
end
end
put
"
#{
APIPREFIX
}
/threads/:thread_id"
do
|
thread_id
|
put
"
#{
APIPREFIX
}
/threads/:thread_id"
do
|
thread_id
|
thread
.
update_attributes
(
params
.
slice
(
*
%w[title body closed commentable_id]
))
thread
.
update_attributes
(
params
.
slice
(
*
%w[title body closed commentable_id group_id]
))
if
params
[
"tags"
]
if
params
[
"tags"
]
thread
.
tags
=
params
[
"tags"
]
thread
.
tags
=
params
[
"tags"
]
thread
.
save
thread
.
save
end
end
if
thread
.
errors
.
any?
if
thread
.
errors
.
any?
error
400
,
thread
.
errors
.
full_messages
.
to_json
error
400
,
thread
.
errors
.
full_messages
.
to_json
else
else
...
...
api/commentables.rb
View file @
6a27f7aa
...
@@ -4,16 +4,27 @@ delete "#{APIPREFIX}/:commentable_id/threads" do |commentable_id|
...
@@ -4,16 +4,27 @@ delete "#{APIPREFIX}/:commentable_id/threads" do |commentable_id|
end
end
get
"
#{
APIPREFIX
}
/:commentable_id/threads"
do
|
commentable_id
|
get
"
#{
APIPREFIX
}
/:commentable_id/threads"
do
|
commentable_id
|
if
params
[
"group_id"
]
handle_threads_query
(
commentable
.
comment_threads
)
threads
=
CommentThread
.
any_of
(
{
:commentable_id
=>
commentable_id
,
:group_id
=>
params
[
:group_id
]},
{
:commentable_id
=>
commentable_id
,
:group_id
.
exists
=>
false
},
)
else
threads
=
commentable
.
comment_threads
end
handle_threads_query
(
threads
)
end
end
post
"
#{
APIPREFIX
}
/:commentable_id/threads"
do
|
commentable_id
|
post
"
#{
APIPREFIX
}
/:commentable_id/threads"
do
|
commentable_id
|
thread
=
CommentThread
.
new
(
params
.
slice
(
*
%w[title body course_id]
).
merge
(
commentable_id:
commentable_id
))
thread
=
CommentThread
.
new
(
params
.
slice
(
*
%w[title body course_id
]
).
merge
(
commentable_id:
commentable_id
))
thread
.
anonymous
=
bool_anonymous
||
false
thread
.
anonymous
=
bool_anonymous
||
false
thread
.
anonymous_to_peers
=
bool_anonymous_to_peers
||
false
thread
.
anonymous_to_peers
=
bool_anonymous_to_peers
||
false
thread
.
tags
=
params
[
"tags"
]
||
""
thread
.
tags
=
params
[
"tags"
]
||
""
if
params
[
"group_id"
]
thread
.
group_id
=
params
[
"group_id"
]
end
thread
.
author
=
user
thread
.
author
=
user
thread
.
save
thread
.
save
if
thread
.
errors
.
any?
if
thread
.
errors
.
any?
...
...
api/pins.rb
0 → 100644
View file @
6a27f7aa
put
"
#{
APIPREFIX
}
/threads/:thread_id/pin"
do
|
thread_id
|
pin
thread
end
put
"
#{
APIPREFIX
}
/threads/:thread_id/unpin"
do
|
thread_id
|
unpin
thread
end
app.rb
View file @
6a27f7aa
...
@@ -18,7 +18,7 @@ module CommentService
...
@@ -18,7 +18,7 @@ module CommentService
API_PREFIX
=
"/api/
#{
API_VERSION
}
"
API_PREFIX
=
"/api/
#{
API_VERSION
}
"
end
end
if
[
"staging"
,
"production"
,
"loadtest"
].
include?
environment
if
[
"staging"
,
"production"
,
"loadtest"
,
"edgestage"
,
"edgeprod"
].
include?
environment
require
'newrelic_rpm'
require
'newrelic_rpm'
end
end
...
@@ -61,6 +61,7 @@ require './api/comments'
...
@@ -61,6 +61,7 @@ require './api/comments'
require
'./api/users'
require
'./api/users'
require
'./api/votes'
require
'./api/votes'
require
'./api/flags'
require
'./api/flags'
require
'./api/pins'
require
'./api/notifications_and_subscriptions'
require
'./api/notifications_and_subscriptions'
if
RACK_ENV
.
to_s
==
"development"
if
RACK_ENV
.
to_s
==
"development"
...
...
config/mongoid.yml
View file @
6a27f7aa
...
@@ -30,6 +30,32 @@ production:
...
@@ -30,6 +30,32 @@ production:
safe
:
true
safe
:
true
consistency
:
strong
consistency
:
strong
edgeprod
:
sessions
:
default
:
hosts
:
-
sayid.member1.mongohq.com:10001
username
:
<%= ENV['MONGOHQ_USER'] %>
password
:
<%= ENV['MONGOHQ_PASS'] %>
database
:
app10640640
options
:
skip_version_check
:
true
safe
:
true
consistency
:
strong
edgestage
:
sessions
:
default
:
hosts
:
-
vincent.mongohq.com:10001
username
:
<%= ENV['MONGOHQ_USER'] %>
password
:
<%= ENV['MONGOHQ_PASS'] %>
database
:
app10640673
options
:
skip_version_check
:
true
safe
:
true
consistency
:
strong
staging
:
staging
:
sessions
:
sessions
:
default
:
default
:
...
...
config/newrelic.yml
View file @
6a27f7aa
...
@@ -258,3 +258,14 @@ loadtest:
...
@@ -258,3 +258,14 @@ loadtest:
<<
:
*default_settings
<<
:
*default_settings
monitor_mode
:
true
monitor_mode
:
true
app_name
:
<%= ENV["NEW_RELIC_APP_NAME"] %> (Load Test)
app_name
:
<%= ENV["NEW_RELIC_APP_NAME"] %> (Load Test)
edgestage
:
<<
:
*default_settings
monitor_mode
:
true
app_name
:
<%= ENV["NEW_RELIC_APP_NAME"] %> (Edge Stage)
edgeprod
:
<<
:
*default_settings
monitor_mode
:
true
app_name
:
<%= ENV["NEW_RELIC_APP_NAME"] %> (Edge Prod)
lib/helpers.rb
View file @
6a27f7aa
...
@@ -58,6 +58,22 @@ helpers do
...
@@ -58,6 +58,22 @@ helpers do
end
end
def
pin
(
obj
)
raise
ArgumentError
,
"User id is required"
unless
user
obj
.
pinned
=
true
obj
.
save
obj
.
reload
.
to_hash
.
to_json
end
def
unpin
(
obj
)
raise
ArgumentError
,
"User id is required"
unless
user
obj
.
pinned
=
nil
obj
.
save
obj
.
reload
.
to_hash
.
to_json
end
def
value_to_boolean
(
value
)
def
value_to_boolean
(
value
)
!!
(
value
.
to_s
=~
/^true$/i
)
!!
(
value
.
to_s
=~
/^true$/i
)
end
end
...
@@ -128,7 +144,9 @@ helpers do
...
@@ -128,7 +144,9 @@ helpers do
else
else
page
=
(
params
[
"page"
]
||
DEFAULT_PAGE
).
to_i
page
=
(
params
[
"page"
]
||
DEFAULT_PAGE
).
to_i
per_page
=
(
params
[
"per_page"
]
||
DEFAULT_PER_PAGE
).
to_i
per_page
=
(
params
[
"per_page"
]
||
DEFAULT_PER_PAGE
).
to_i
comment_threads
=
comment_threads
.
order_by
(
"
#{
sort_key
}
#{
sort_order
}
"
)
if
sort_key
&&
sort_order
#KChugh turns out we don't need to go through all the extra work on the back end because the client is resorting anyway
#KChugh boy was I wrong, we need to sort for pagination
comment_threads
=
comment_threads
.
order_by
(
"pinned DESC,
#{
sort_key
}
#{
sort_order
}
"
)
if
sort_key
&&
sort_order
num_pages
=
[
1
,
(
comment_threads
.
count
/
per_page
.
to_f
).
ceil
].
max
num_pages
=
[
1
,
(
comment_threads
.
count
/
per_page
.
to_f
).
ceil
].
max
page
=
[
num_pages
,
[
1
,
page
].
max
].
min
page
=
[
num_pages
,
[
1
,
page
].
max
].
min
paged_comment_threads
=
comment_threads
.
page
(
page
).
per
(
per_page
)
paged_comment_threads
=
comment_threads
.
page
(
page
).
per
(
per_page
)
...
...
lib/tasks/kpis.rake
0 → 100644
View file @
6a27f7aa
require
'rest_client'
roots
=
{}
roots
[
'development'
]
=
"http://localhost:8000"
roots
[
'test'
]
=
"http://localhost:8000"
roots
[
'production'
]
=
"http://edx.org"
roots
[
'staging'
]
=
"http://stage.edx.org"
ROOT
=
roots
[
ENV
[
'SINATRA_ENV'
]]
namespace
:kpis
do
task
:prolific
=>
:environment
do
#USAGE
#SINATRA_ENV=development rake kpis:prolific
#or
#SINATRA_ENV=development bundle exec rake kpis:prolific
courses
=
Content
.
all
.
distinct
(
"course_id"
)
puts
"
\n\n
*********************************************************************"
puts
" Users who have created the most forum content on edX (
#{
Date
.
today
}
) "
puts
"*********************************************************************
\n\n
"
courses
.
each
do
|
c
|
contributors
=
Content
.
prolific_metric
({
"course_id"
=>
c
},
10
)
#now output
puts
c
puts
"*********************"
contributors
.
each
do
|
p
|
url
=
ROOT
+
"/courses/
#{
c
}
/discussion/forum/users/
#{
p
[
'_id'
]
}
"
count_string
=
"
#{
p
[
'value'
].
to_i
}
contributions:"
.
rjust
(
25
)
puts
"
#{
count_string
}
#{
url
}
"
end
puts
"
\n
"
end
end
task
:starters
=>
:environment
do
#USAGE
#SINATRA_ENV=development rake kpis:starters
#or
#SINATRA_ENV=development bundle exec rake kpis:starters
courses
=
Content
.
all
.
distinct
(
"course_id"
)
puts
"
\n\n
*********************************************************************"
puts
" Users who have started the most threads on edX (
#{
Date
.
today
}
) "
puts
"*********************************************************************
\n\n
"
courses
.
each
do
|
c
|
contributors
=
Content
.
prolific_metric
({
"course_id"
=>
c
,
"_type"
=>
"CommentThread"
},
10
)
#now output
puts
c
puts
"*********************"
contributors
.
each
do
|
p
|
url
=
ROOT
+
"/courses/
#{
c
}
/discussion/forum/users/
#{
p
[
'_id'
]
}
"
count_string
=
"
#{
p
[
'value'
].
to_i
}
contributions:"
.
rjust
(
25
)
puts
"
#{
count_string
}
#{
url
}
"
end
puts
"
\n
"
end
end
task
:ppu
=>
:environment
do
#USAGE
#SINATRA_ENV=development rake kpis:ppu
#or
#SINATRA_ENV=development bundle exec rake kpis:ppu
courses
=
Content
.
all
.
distinct
(
"course_id"
)
puts
"
\n\n
*********************************************************************"
puts
"Average threads per contributing user per course on edX (
#{
Date
.
today
}
) "
puts
"*********************************************************************
\n\n
"
courses
.
each
do
|
c
|
#first, get all the users who have contributed
contributors
=
Content
.
prolific_metric
({
"course_id"
=>
c
},
10
)
total_users
=
contributors
.
count
#now, get the threads
total_threads
=
Content
.
where
(
"_type"
=>
"CommentThread"
,
"course_id"
=>
c
).
count
ratio
=
total_threads
.
to_f
/
total_users
.
to_f
#now output
puts
c
puts
"*********************"
puts
"Total Threads:
#{
total_threads
}
"
puts
"Total Users:
#{
total_users
}
"
puts
"Average Thread/User:
#{
ratio
}
"
puts
"
\n
"
end
end
task
:epu
=>
:environment
do
#USAGE
#SINATRA_ENV=development rake kpis:epu
#or
#SINATRA_ENV=development bundle exec rake kpis:epu
courses
=
Content
.
all
.
distinct
(
"course_id"
)
puts
"
\n\n
*****************************************************************************************************************"
puts
"Average contributions (votes, threads, or comments) per contributing user per course on edX (
#{
Date
.
today
}
) "
puts
"*********************************************************************************************************************
\n\n
"
courses
.
each
do
|
c
|
#first, get all the users who have contributed
summary
=
Content
.
summary
({
"course_id"
=>
c
})
total_users
=
summary
[
"contributor_count"
]
total_activity
=
summary
[
'thread_count'
]
total_activity
+=
summary
[
'comment_count'
]
total_activity
+=
summary
[
'vote_count'
]
ratio
=
total_activity
.
to_f
/
total_users
.
to_f
puts
c
puts
"*********************"
puts
"Total Threads:
#{
summary
[
'thread_count'
]
}
"
puts
"Total Comments:
#{
summary
[
'comment_count'
]
}
"
puts
"Total Votes:
#{
summary
[
'vote_count'
]
}
\n\n
"
puts
"Total Users:
#{
summary
[
'contributor_count'
]
}
"
puts
"Total Engagements:
#{
total_activity
}
\n\n
"
puts
"Average Engagement Per Engaging User:
#{
ratio
}
\n\n\n
"
end
end
task
:orphans
=>
:environment
do
#USAGE
#SINATRA_ENV=development rake kpis:orphans
#or
#SINATRA_ENV=development bundle exec rake kpis:orphans
courses
=
Content
.
all
.
distinct
(
"course_id"
)
puts
"
\n\n
****************************************************"
puts
"thread reply rate per course on edX (
#{
Date
.
today
}
) "
puts
"****************************************************
\n\n
"
courses
.
each
do
|
c
|
#first, get all the users who have contributed
threads
=
Content
.
where
({
"course_id"
=>
c
,
"_type"
=>
"CommentThread"
})
orphans
=
Content
.
where
({
"course_id"
=>
c
,
"_type"
=>
"CommentThread"
,
"comment_count"
=>
0
})
ratio
=
orphans
.
count
.
to_f
/
threads
.
count
.
to_f
puts
c
puts
"*********************"
puts
"Total Threads:
#{
threads
.
count
}
"
puts
"Total Orphaned Threads:
#{
orphans
.
count
}
"
if
threads
.
count
>
0
puts
"Orphan Ratio:
#{
(
ratio
*
1000
).
round
.
to_f
/
10.0
}
%"
end
puts
"
\n\n\n
"
end
end
end
models/comment_thread.rb
View file @
6a27f7aa
...
@@ -21,6 +21,8 @@ class CommentThread < Content
...
@@ -21,6 +21,8 @@ class CommentThread < Content
field
:closed
,
type:
Boolean
,
default:
false
field
:closed
,
type:
Boolean
,
default:
false
field
:at_position_list
,
type:
Array
,
default:
[]
field
:at_position_list
,
type:
Array
,
default:
[]
field
:last_activity_at
,
type:
Time
field
:last_activity_at
,
type:
Time
field
:group_id
,
type:
Integer
field
:pinned
,
type:
Boolean
index
({
author_id:
1
,
course_id:
1
})
index
({
author_id:
1
,
course_id:
1
})
...
@@ -39,9 +41,11 @@ class CommentThread < Content
...
@@ -39,9 +41,11 @@ class CommentThread < Content
indexes
:comment_count
,
type: :integer
,
included_in_all:
false
indexes
:comment_count
,
type: :integer
,
included_in_all:
false
indexes
:votes_point
,
type: :integer
,
as:
'votes_point'
,
included_in_all:
false
indexes
:votes_point
,
type: :integer
,
as:
'votes_point'
,
included_in_all:
false
indexes
:course_id
,
type: :string
,
index: :not_analyzed
,
incldued_in_all:
false
indexes
:course_id
,
type: :string
,
index: :not_analyzed
,
included_in_all:
false
indexes
:commentable_id
,
type: :string
,
index: :not_analyzed
,
incldued_in_all:
false
indexes
:commentable_id
,
type: :string
,
index: :not_analyzed
,
included_in_all:
false
indexes
:author_id
,
type: :string
,
as:
'author_id'
,
index: :not_analyzed
,
incldued_in_all:
false
indexes
:author_id
,
type: :string
,
as:
'author_id'
,
index: :not_analyzed
,
included_in_all:
false
indexes
:group_id
,
type: :integer
,
as:
'group_id'
,
index: :not_analyzed
,
included_in_all:
false
#indexes :pinned, type: :boolean, as: 'pinned', index: :not_analyzed, included_in_all: false
end
end
belongs_to
:author
,
class_name:
"User"
,
inverse_of: :comment_threads
,
index:
true
#, autosave: true
belongs_to
:author
,
class_name:
"User"
,
inverse_of: :comment_threads
,
index:
true
#, autosave: true
...
@@ -107,14 +111,27 @@ class CommentThread < Content
...
@@ -107,14 +111,27 @@ class CommentThread < Content
search
.
filter
(
:term
,
commentable_id:
params
[
"commentable_id"
])
if
params
[
"commentable_id"
]
search
.
filter
(
:term
,
commentable_id:
params
[
"commentable_id"
])
if
params
[
"commentable_id"
]
search
.
filter
(
:terms
,
commentable_id:
params
[
"commentable_ids"
])
if
params
[
"commentable_ids"
]
search
.
filter
(
:terms
,
commentable_id:
params
[
"commentable_ids"
])
if
params
[
"commentable_ids"
]
search
.
filter
(
:term
,
course_id:
params
[
"course_id"
])
if
params
[
"course_id"
]
search
.
filter
(
:term
,
course_id:
params
[
"course_id"
])
if
params
[
"course_id"
]
if
params
[
"group_id"
]
search
.
filter
:or
,
[
{
:not
=>
{
:exists
=>
{
:field
=>
:group_id
}}},
{
:term
=>
{
:group_id
=>
params
[
"group_id"
]}}
]
end
search
.
sort
{
|
sort
|
sort
.
by
sort_key
,
sort_order
}
if
sort_key
&&
sort_order
#TODO should have search option 'auto sort or sth'
search
.
sort
{
|
sort
|
sort
.
by
sort_key
,
sort_order
}
if
sort_key
&&
sort_order
#TODO should have search option 'auto sort or sth'
search
.
size
per_page
search
.
size
per_page
search
.
from
per_page
*
(
page
-
1
)
search
.
from
per_page
*
(
page
-
1
)
results
=
search
.
results
if
CommentService
.
config
[
:cache_enabled
]
if
CommentService
.
config
[
:cache_enabled
]
Sinatra
::
Application
.
cache
.
set
(
memcached_key
,
search
.
results
,
CommentService
.
config
[
:cache_timeout
][
:threads_search
].
to_i
)
Sinatra
::
Application
.
cache
.
set
(
memcached_key
,
results
,
CommentService
.
config
[
:cache_timeout
][
:threads_search
].
to_i
)
end
end
search
.
results
results
end
end
def
activity_since
(
from_time
=
nil
)
def
activity_since
(
from_time
=
nil
)
...
@@ -161,6 +178,8 @@ class CommentThread < Content
...
@@ -161,6 +178,8 @@ class CommentThread < Content
"abuse_flaggers"
=>
abuse_flaggers
,
"abuse_flaggers"
=>
abuse_flaggers
,
"tags"
=>
tags_array
,
"tags"
=>
tags_array
,
"type"
=>
"thread"
,
"type"
=>
"thread"
,
"group_id"
=>
group_id
,
"pinned"
=>
pinned?
,
"endorsed"
=>
endorsed?
)
"endorsed"
=>
endorsed?
)
if
params
[
:recursive
]
if
params
[
:recursive
]
...
...
models/content.rb
View file @
6a27f7aa
...
@@ -13,8 +13,6 @@ class Content
...
@@ -13,8 +13,6 @@ class Content
end
end
end
end
def
self
.
flagged
def
self
.
flagged
#return an array of flagged content
#return an array of flagged content
holder
=
[]
holder
=
[]
...
@@ -23,5 +21,66 @@ class Content
...
@@ -23,5 +21,66 @@ class Content
end
end
holder
holder
end
end
def
self
.
prolific_metric
what
,
count
#take a hash of criteria (what) and return a hash of hashes
#course => user => count
contributors
=
{}
map
=
"function(){emit(this.author_id,1)}"
reduce
=
"function(k, vals) { var sum = 0; for(var i in vals) sum += vals[i]; return sum; }"
contributors
=
[]
self
.
where
(
what
).
map_reduce
(
map
,
reduce
).
out
(
replace:
"results"
).
each
do
|
d
|
contributors
<<
d
end
#now sort and limit them
#first sort destructively
contributors
.
sort!
{
|
a
,
b
|
-
a
[
"value"
]
<=>
-
b
[
"value"
]}
#then trim it
contributors
=
contributors
[
0
..
(
count
-
1
)]
contributors
end
def
self
.
summary
what
#take a hash of criteria (what) and return a hash of hashes
#of total users, votes, comments, endorsements,
answer
=
{}
vote_count
=
0
thread_count
=
0
comment_count
=
0
contributors
=
[]
content
=
self
.
where
(
what
)
content
.
each
do
|
c
|
contributors
<<
c
.
author_id
contributors
<<
c
[
"votes"
][
"up"
]
contributors
<<
c
[
"votes"
][
"down"
]
vote_count
+=
c
[
"votes"
][
"count"
]
if
c
.
_type
==
"CommentThread"
thread_count
+=
1
elsif
c
.
_type
==
"Comment"
comment_count
+=
1
end
end
#uniquify contributors
contributors
=
contributors
.
uniq
#assemble the answer and ship
answer
[
"vote_count"
]
=
vote_count
answer
[
"thread_count"
]
=
thread_count
answer
[
"comment_count"
]
=
comment_count
answer
[
"contributor_count"
]
=
contributors
.
count
answer
end
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment