tent-status 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. data/.gitignore +21 -0
  2. data/.kick +8 -0
  3. data/Gemfile +12 -0
  4. data/Gemfile.lock +232 -0
  5. data/LICENSE.txt +22 -0
  6. data/Procfile +1 -0
  7. data/README.md +14 -0
  8. data/Rakefile +43 -0
  9. data/assets/images/.gitkeep +0 -0
  10. data/assets/images/chosen-sprite.png +0 -0
  11. data/assets/images/glyphicons-halflings-white.png +0 -0
  12. data/assets/images/glyphicons-halflings.png +0 -0
  13. data/assets/javascripts/.gitkeep +0 -0
  14. data/assets/javascripts/application.js.coffee +122 -0
  15. data/assets/javascripts/backbone.js +1443 -0
  16. data/assets/javascripts/backbone_sync.js.coffee +60 -0
  17. data/assets/javascripts/boot.js.coffee +3 -0
  18. data/assets/javascripts/chosen.jquery.js +1129 -0
  19. data/assets/javascripts/collections/.gitkeep +0 -0
  20. data/assets/javascripts/collections/followers.js.coffee +5 -0
  21. data/assets/javascripts/collections/followings.js.coffee +5 -0
  22. data/assets/javascripts/collections/groups.js.coffee +5 -0
  23. data/assets/javascripts/collections/posts.js.coffee +5 -0
  24. data/assets/javascripts/fetch_pool.js.coffee +25 -0
  25. data/assets/javascripts/helpers/.gitkeep +0 -0
  26. data/assets/javascripts/helpers/formatting.js.coffee +17 -0
  27. data/assets/javascripts/hogan.js +706 -0
  28. data/assets/javascripts/http.js.coffee +18 -0
  29. data/assets/javascripts/jquery.js +9301 -0
  30. data/assets/javascripts/models/.gitkeep +0 -0
  31. data/assets/javascripts/models/follower.js.coffee +27 -0
  32. data/assets/javascripts/models/following.js.coffee +27 -0
  33. data/assets/javascripts/models/group.js.coffee +4 -0
  34. data/assets/javascripts/models/post.js.coffee +32 -0
  35. data/assets/javascripts/models/profile.js.coffee +30 -0
  36. data/assets/javascripts/moment.js +1106 -0
  37. data/assets/javascripts/paginator.js.coffee +67 -0
  38. data/assets/javascripts/router.js.coffee +71 -0
  39. data/assets/javascripts/routers/.gitkeep +0 -0
  40. data/assets/javascripts/routers/followers.js.coffee +14 -0
  41. data/assets/javascripts/routers/followings.js.coffee +14 -0
  42. data/assets/javascripts/routers/posts.js.coffee +98 -0
  43. data/assets/javascripts/templates/.gitkeep +0 -0
  44. data/assets/javascripts/templates/_follower.js.mustache.slim +17 -0
  45. data/assets/javascripts/templates/_following.js.mustache.slim +17 -0
  46. data/assets/javascripts/templates/_new_post_form.js.mustache.slim +10 -0
  47. data/assets/javascripts/templates/_post.js.mustache.slim +10 -0
  48. data/assets/javascripts/templates/_post_inner.js.mustache.slim +71 -0
  49. data/assets/javascripts/templates/_profile_stats.js.mustache.slim +11 -0
  50. data/assets/javascripts/templates/_reply_form.js.mustache.slim +13 -0
  51. data/assets/javascripts/templates/conversation.js.mustache.slim +13 -0
  52. data/assets/javascripts/templates/followers.js.mustache.slim +19 -0
  53. data/assets/javascripts/templates/followings.js.mustache.slim +22 -0
  54. data/assets/javascripts/templates/posts.js.mustache.slim +25 -0
  55. data/assets/javascripts/templates/profile.js.mustache.slim +39 -0
  56. data/assets/javascripts/underscore.js +1059 -0
  57. data/assets/javascripts/view.js.coffee +140 -0
  58. data/assets/javascripts/views/.gitkeep +0 -0
  59. data/assets/javascripts/views/container.js.coffee +6 -0
  60. data/assets/javascripts/views/expanding_textarea.js.coffee +14 -0
  61. data/assets/javascripts/views/fetch_posts_pool.js.coffee +61 -0
  62. data/assets/javascripts/views/follower_groups_form.js.coffee +26 -0
  63. data/assets/javascripts/views/followers.js.coffee +28 -0
  64. data/assets/javascripts/views/following_groups_form.js.coffee +25 -0
  65. data/assets/javascripts/views/followings.js.coffee +27 -0
  66. data/assets/javascripts/views/new_following_form.js.coffee +15 -0
  67. data/assets/javascripts/views/new_post_form.js.coffee +178 -0
  68. data/assets/javascripts/views/post.js.coffee +139 -0
  69. data/assets/javascripts/views/posts.js.coffee +55 -0
  70. data/assets/javascripts/views/posts/conversation.js.coffee +18 -0
  71. data/assets/javascripts/views/profile.js.coffee +29 -0
  72. data/assets/javascripts/views/profile_follow_button.js.coffee +29 -0
  73. data/assets/javascripts/views/profile_stats.js.coffee +38 -0
  74. data/assets/javascripts/views/remove_follower_btn.js.coffee +18 -0
  75. data/assets/javascripts/views/reply_post_form.js.coffee +30 -0
  76. data/assets/javascripts/views/unfollow_btn.js.coffee +18 -0
  77. data/assets/stylesheets/.gitkeep +0 -0
  78. data/assets/stylesheets/application.css.sass +117 -0
  79. data/assets/stylesheets/bootstrap-responsive.css +1040 -0
  80. data/assets/stylesheets/bootstrap.css.erb +5624 -0
  81. data/assets/stylesheets/chosen.css.erb +397 -0
  82. data/config.ru +14 -0
  83. data/config/asset_sync.rb +12 -0
  84. data/config/evergreen.rb +16 -0
  85. data/lib/tent-status.rb +6 -0
  86. data/lib/tent-status/app.rb +263 -0
  87. data/lib/tent-status/models/user.rb +39 -0
  88. data/lib/tent-status/sprockets/environment.rb +25 -0
  89. data/lib/tent-status/sprockets/helpers.rb +5 -0
  90. data/lib/tent-status/views/application.slim +69 -0
  91. data/lib/tent-status/views/auth.slim +8 -0
  92. data/tent-status.gemspec +34 -0
  93. metadata +415 -0
@@ -0,0 +1,140 @@
1
+ # Custom View Class extending Backbone.View
2
+ #
3
+ # Sample View Class:
4
+ # class TentStatus.Views.DashboardArticles extends TentStatus.View
5
+ # templateName: 'foo/bar'
6
+ #
7
+ # # e.g. dashboard/_header is available as @partials.header
8
+ # partialNames: ['baz/_header']
9
+ #
10
+ # # wait for attributes to be set with @set before rendering
11
+ # dependentRenderAttributes: ['foos']
12
+ #
13
+ # initialize: (options) ->
14
+ # super
15
+ #
16
+ # # Custom view initialization, e.g.:
17
+ # @on 'ready', @bindEvents
18
+ #
19
+ # # setup context object passed to template
20
+ # # context is the data object for the view
21
+ # # See (mustache syntax): http://mustache.github.com/mustache.5.html
22
+ # # See (hogan example): http://twitter.github.com/hogan.js/
23
+ # context: =>
24
+ # foos: foos.toJSON()
25
+ #
26
+ # render: =>
27
+ # # do anything special here
28
+ #
29
+ # super
30
+
31
+ class TentStatus.View extends Backbone.View
32
+ # simpler version of @set and @get than given with Backbone.Model
33
+ # @set sets the key directly on the View instance
34
+ # it's used to pass data from the router to the view
35
+ set: (key, val) =>
36
+ @[key] = val
37
+ @trigger "change:#{key}"
38
+
39
+ # see render method in above sample
40
+ get: (key) =>
41
+ @[key]
42
+
43
+ # fetch template and partials
44
+ # once loaded @template will hold the compiled hogan template specified with @templateName
45
+ # and @partials will hold all compiled templates defined in @partialNames
46
+ initialize: (options) ->
47
+ @container ||= TentStatus.Views.container
48
+
49
+ unless @container
50
+ TentStatus.devWarning @, "You need to define @container View"
51
+
52
+ # fetch main template
53
+ if @templateName
54
+ TentStatus.fetchTemplate @templateName, (@template) =>
55
+ @trigger 'template:load'
56
+
57
+ # load all partials listed in partialNames
58
+ if @partialNames
59
+ @partials = {}
60
+ for p in @partialNames
61
+ do (p) =>
62
+ TentStatus.fetchTemplate p, (template) =>
63
+ name = @getPartialName(p)
64
+ @partials[name] = template
65
+ @trigger "partials:#{name}:load"
66
+
67
+ @on 'ready', @bindViews
68
+ @on 'ready', @bindEvents
69
+
70
+ loadMore: (key) =>
71
+ @get(key)?.nextPage()
72
+
73
+ bindEvents: =>
74
+ _.each $('.btn.load-more', @container?.el), (el) =>
75
+ viewKey = $(el).attr('data-key')
76
+
77
+ $(el).hide() if @get(viewKey)?.onLastPage
78
+ @get(viewKey)?.on 'fetch:start', => $(el).hide()
79
+ @get(viewKey)?.on 'fetch:success', => $(el).show() unless @get(viewKey)?.onLastPage
80
+
81
+ $(el).off().on 'click', (=> @loadMore viewKey)
82
+
83
+ bindViews: =>
84
+ _.each $('[data-view]', @container?.el), (el) =>
85
+ viewClassName = $(el).attr 'data-view'
86
+ if viewClass = TentStatus.Views[viewClassName]
87
+ view = new viewClass el: el, parentView: @
88
+ else
89
+ TentStatus.devWarning @, "TentStatus.Views.#{viewClassName} is not defined!"
90
+ console.log el
91
+
92
+ getPartialName: (path) =>
93
+ path.replace(/.+\/_(.+)$/, "$1")
94
+
95
+ # called before fetching data in the router
96
+ empty: =>
97
+ @container?.render("")
98
+
99
+ context: =>
100
+ TentStatus.devWarning @, "You need to override context in your view class!"
101
+ {}
102
+
103
+ # wait for @template and @partials to load
104
+ # then render @template with @context and @partials
105
+ # and insert html into @container.el
106
+ render: =>
107
+ # wait for template to be loaded
108
+ if @templateName
109
+ unless @template
110
+ @once 'template:load', => @render(arguments...)
111
+ return false
112
+
113
+ # wait for partials to be loaded
114
+ if @partialNames
115
+ for p in @partialNames
116
+ name = @getPartialName(p)
117
+ unless @partials[name]
118
+ @once "partials:#{name}:load", => @render(arguments...)
119
+ return false
120
+
121
+ # wait for data to be loaded
122
+ if @dependentRenderAttributes
123
+ for key in @dependentRenderAttributes
124
+ if @get(key) == null
125
+ @once "change:#{key}", => @render(arguments...)
126
+ return false
127
+
128
+ context = _.extend {
129
+ authenticated: TentStatus.authenticated
130
+ guest_authenticated: TentStatus.guest_authenticated
131
+ }, @context()
132
+
133
+ html = @template.render(context, @partials)
134
+ if @container
135
+ @container.render(html)
136
+ @trigger 'ready'
137
+ true
138
+ else
139
+ html
140
+
File without changes
@@ -0,0 +1,6 @@
1
+ TentStatus.Views.container = new class ContainerView extends Backbone.View
2
+ initialize: ->
3
+ @setElement document.getElementById('main')
4
+
5
+ render: (html) =>
6
+ @$el.html(html)
@@ -0,0 +1,14 @@
1
+ class TentStatus.Views.ExpandingTextArea extends Backbone.View
2
+ initialize: ->
3
+ @$el.on 'keyup', @adjustSize
4
+
5
+ @initialHeight = @$el.height()
6
+
7
+ adjustSize: (shouldExecute = false) =>
8
+ @_padding ||= (parseInt(@$el.css 'padding-top') || 0) + (parseInt(@$el.css 'padding-bottom') || 0)
9
+ scrollY = window.scrollY
10
+ scrollX = window.scrollX
11
+ @$el.height(0)
12
+ @$el.css('height', "#{ Math.max(@el.scrollHeight - @_padding, @initialHeight) }px")
13
+ window.scrollTo scrollX, scrollY
14
+
@@ -0,0 +1,61 @@
1
+ class TentStatus.Views.FetchPostsPool extends Backbone.View
2
+ initialize: (options = {}) ->
3
+ @parentView = options.parentView
4
+
5
+ @$numNewPosts = ($ '.num_new_posts', @$el)
6
+ @numNewPosts = 0
7
+
8
+ @$postsList = ($ 'ul.posts', @parentView.container.$el)
9
+
10
+ @sinceId = @parentView.posts.first()?.get('id')
11
+ @pool = new TentStatus.FetchPool( new TentStatus.Collections.Posts, { sinceId: @sinceId })
12
+ @pool.on 'fetch:success', @update
13
+
14
+ @fetchDelay = 3000
15
+ @fetchDelayOffset = 0
16
+
17
+ @setFetchInterval()
18
+
19
+ @$el.on 'click', (e)=>
20
+ e.preventDefault()
21
+ @emptyPool()
22
+ false
23
+
24
+ setFetchInterval: (interval=@fetchDelay+@fetchDelayOffset) =>
25
+ clearInterval TentStatus._fetchPostsPoolInterval
26
+ TentStatus._fetchPostsPoolInterval = setInterval @pool.fetch, interval
27
+
28
+ update: =>
29
+ lastSinceId = @sinceId
30
+ @sinceId = @pool.sinceId
31
+ if lastSinceId == @sinceId
32
+ @fetchDelayOffset = Math.min(@fetchDelayOffset + @fetchDelay, 57000) # max delay: 1 min
33
+ @setFetchInterval()
34
+ else
35
+ @fetchDelayOffset = 0
36
+ @setFetchInterval()
37
+
38
+ @numNewPosts = @pool.collection.length
39
+ @$numNewPosts.text @numNewPosts
40
+ @show() if @numNewPosts > 0
41
+
42
+ show: => @$el.show()
43
+ hide: => @$el.hide()
44
+
45
+ emptyPool: =>
46
+ for i in [0...@numNewPosts]
47
+ post = @pool.collection.shift()
48
+ TentStatus.Collections.posts.unshift(post)
49
+ # @createPostView(post)
50
+ @parentView.render()
51
+ @pool.sinceId = @parentView.posts.first()?.get('id') || @pool.sinceId
52
+ @numNewPosts = 0
53
+ @$numNewPosts.text @numNewPosts
54
+ @hide()
55
+
56
+ createPostView: (post) =>
57
+ el = ($ '<li>').prependTo(@$postsList)
58
+ view = new TentStatus.Views.Post el: el, parentView: @parentView
59
+ view.post = post
60
+ context = view.context(post)
61
+ view.render(context)
@@ -0,0 +1,26 @@
1
+ class TentStatus.Views.FollowerGroupsForm extends Backbone.View
2
+ initialize: (options = {}) ->
3
+ @parentView = options.parentView
4
+
5
+ followerId = @$el.attr 'data-follower-id'
6
+ @follower = TentStatus.Collections.followers.find (follower) -> follower.get('id') == followerId
7
+
8
+ @$groupsSelect = ($ 'select[name=groups]', @$el)
9
+ @$groupsSelect.chosen
10
+ persistent_create_option: true
11
+ no_results_text: 'No groups match'
12
+ create_option_text: 'Create new group'
13
+ create_option: (name) =>
14
+ group = new TentStatus.Models.Group({ name: name })
15
+ group.once 'sync', =>
16
+ TentStatus.Collections.groups.push(group)
17
+ @follower.set 'groups', (@follower.get('groups') || []).concat([group.get('id')])
18
+ @follower.save()
19
+ @parentView.render()
20
+ group.save()
21
+
22
+ @$groupsSelect.change =>
23
+ @follower.set 'groups', @$groupsSelect.val()
24
+ @follower.once 'sync', => @parentView.render()
25
+ @follower.save()
26
+
@@ -0,0 +1,28 @@
1
+ class TentStatus.Views.Followers extends TentStatus.View
2
+ templateName: 'followers'
3
+ partialNames: ['_follower']
4
+
5
+ dependentRenderAttributes: ['followers', 'groups']
6
+
7
+ initialize: ->
8
+ @container = TentStatus.Views.container
9
+ super
10
+
11
+ @on 'ready', @initAutoPaginate
12
+
13
+ context: =>
14
+ followers: _.map(@followers.toArray(), (follower) => _.extend follower.toJSON(), {
15
+ name: follower.name()
16
+ avatar: follower.avatar()
17
+ groups: _.map(@groups.toArray(), (group) -> _.extend group.toJSON(), {
18
+ selected: follower.get('groups')?.indexOf(group.id) != -1
19
+ })
20
+ })
21
+
22
+ initAutoPaginate: =>
23
+ ($ window).off 'scroll.followers'
24
+ ($ window).on 'scroll.followers', (e)=>
25
+ height = $(document).height() - $(window).height()
26
+ delta = height - window.scrollY
27
+ if delta < 200
28
+ @followers?.nextPage() unless @followers.onLastPage
@@ -0,0 +1,25 @@
1
+ class TentStatus.Views.FollowingGroupsForm extends Backbone.View
2
+ initialize: (options = {}) ->
3
+ @parentView = options.parentView
4
+
5
+ followingId = @$el.attr 'data-following-id'
6
+ @following = TentStatus.Collections.followings.get(followingId)
7
+
8
+ @$groupsSelect = ($ 'select[name=groups]', @$el)
9
+ @$groupsSelect.chosen
10
+ persistent_create_option: true
11
+ no_results_text: 'No groups match'
12
+ create_option_text: 'Create new group'
13
+ create_option: (name) =>
14
+ group = new TentStatus.Models.Group({ name: name })
15
+ group.once 'sync', =>
16
+ TentStatus.Collections.groups.push(group)
17
+ @following.set 'groups', (@following.get('groups') || []).concat([group.get('id')])
18
+ @following.save()
19
+ @parentView.render()
20
+ group.save()
21
+
22
+ @$groupsSelect.change =>
23
+ @following.set 'groups', @$groupsSelect.val()
24
+ @following.once 'sync', => @parentView.render()
25
+ @following.save()
@@ -0,0 +1,27 @@
1
+ class TentStatus.Views.Followings extends TentStatus.View
2
+ templateName: 'followings'
3
+ partialNames: ['_following']
4
+
5
+ dependentRenderAttributes: ['followings', 'groups']
6
+
7
+ initialize: ->
8
+ @container = TentStatus.Views.container
9
+ super
10
+ @on 'ready', @initAutoPaginate
11
+
12
+ context: =>
13
+ followings: _.map(@followings.toArray(), (following) => _.extend following.toJSON(), {
14
+ name: following.name()
15
+ avatar: following.avatar()
16
+ groups: _.map(@groups.toArray(), (group) -> _.extend group.toJSON(), {
17
+ selected: following.get('groups')?.indexOf(group.id) != -1
18
+ })
19
+ })
20
+
21
+ initAutoPaginate: =>
22
+ ($ window).off 'scroll.followings'
23
+ ($ window).on 'scroll.followings', (e)=>
24
+ height = $(document).height() - $(window).height()
25
+ delta = height - window.scrollY
26
+ if delta < 200
27
+ @followings?.nextPage() unless @followings.onLastPage
@@ -0,0 +1,15 @@
1
+ class TentStatus.Views.NewFollowingForm extends Backbone.View
2
+ initialize: (options = {}) ->
3
+ @parentView = options.parentView
4
+
5
+ @$el.on 'submit', @submit
6
+
7
+ submit: (e) =>
8
+ e.preventDefault()
9
+ entity = ($ '[name=entity]', @$el).val()
10
+ following = new TentStatus.Models.Following { entity: entity }
11
+ following.once 'sync', =>
12
+ TentStatus.Collections.followings.unshift(following)
13
+ @parentView.render()
14
+ following.save()
15
+ false
@@ -0,0 +1,178 @@
1
+ class TentStatus.Views.NewPostForm extends Backbone.View
2
+ initialize: (options = {}) ->
3
+ @parentView = options.parentView
4
+
5
+ @action = "#{TentStatus.api_root}/posts"
6
+
7
+ @$errors = ($ '.alert-error', @$el).first().hide()
8
+
9
+ @$post_btn = ($ 'input[type=submit]', @$el)
10
+
11
+ @$el.off().on 'submit', @submit
12
+
13
+ ## form validation and character limit counter
14
+ @$textarea = ($ 'textarea', @$el)
15
+ @$charLimit = ($ '.char-limit', @$el).first()
16
+ @charLimit = parseInt(@$charLimit.text())
17
+ @$textarea.on 'keyup', =>
18
+ clearTimeout @_validateTimeout
19
+ @_validateTimeout = setTimeout @validate, 300
20
+ @updateCharCounter()
21
+ null
22
+ @updateCharCounter()
23
+
24
+ ## cmd/ctr enter == submit
25
+ @$textarea.off('keydown.keysubmit').on 'keydown.keysubmit', (e) =>
26
+ if (e.metaKey || e.ctrlKey) && e.keyCode == 13
27
+ e.preventDefault()
28
+ @submit()
29
+ false
30
+ else
31
+ true
32
+
33
+ ## permissions
34
+ @$publicCheckbox = ($ '[name=public]', @$el)
35
+ @$permissions = ($ 'select[name=permissions]', @$el)
36
+ @$permissions.chosen
37
+ no_results_text: 'No matching entities or groups'
38
+
39
+ # disable public checkbox when permissions are added
40
+ @$permissions.change @checkPublicEnabled
41
+
42
+ ## mentions
43
+ @$mentionsSelect = ($ 'select[name=mentions]', @$el)
44
+ @$mentionsSelect.chosen
45
+ no_results_text: 'No matching entities'
46
+
47
+ ## licenses
48
+ @$licensesSelect = ($ 'select[name=licenses]', @$el)
49
+ @$licensesSelect.chosen
50
+ no_results_text: 'No matching licenses'
51
+
52
+ ## advanced options toggle
53
+ @$advancedOptions = ($ '.advanced-options', @$el).hide()
54
+ @$advancedOptionsToggle = ($ '.advanced-options-toggle', @$el)
55
+ @$advancedOptionsToggle.on 'click', (e) =>
56
+ e.preventDefault()
57
+ @$advancedOptions.toggle()
58
+ false
59
+
60
+ checkPublicEnabled: =>
61
+ if @$permissions.val() == null
62
+ @enablePublic()
63
+ else
64
+ @disablePublic()
65
+
66
+ enablePublic: =>
67
+ @$publicCheckbox.removeAttr('disabled')
68
+
69
+ disablePublic: =>
70
+ @$publicCheckbox.removeAttr('checked')
71
+ @$publicCheckbox.attr('disabled', 'disabled')
72
+
73
+ submit: (e) =>
74
+ e.preventDefault() if e
75
+ data = @getData()
76
+ return false unless @validate data
77
+
78
+ post = new TentStatus.Models.Post data
79
+ post.once 'sync', =>
80
+ window.location.reload() unless @parentView.emptyPool
81
+ @parentView.emptyPool()
82
+ TentStatus.Collections.posts.unshift(post)
83
+ @parentView.set('posts', TentStatus.Collections.posts)
84
+ @parentView.render()
85
+ post.save()
86
+ false
87
+
88
+ buildPermissions: (data) =>
89
+ _public = if data['public'] == 'on' then true else false
90
+ permissions = {
91
+ public: _public
92
+ }
93
+
94
+ _groups = _.each _.flatten(Array data.permissions), (entityOrGroupId) ->
95
+ return unless entityOrGroupId
96
+ [type, value] = [entityOrGroupId.slice(0,2), entityOrGroupId.slice(2, entityOrGroupId.length)]
97
+ switch type
98
+ when 'g:'
99
+ permissions.groups ||= []
100
+ permissions.groups.push { id: value }
101
+ when 'f:'
102
+ permissions.entities ||= {}
103
+ permissions.entities[value] = true
104
+
105
+ delete data['public']
106
+ delete data.permissible_groups
107
+ delete data.permissible_entities
108
+
109
+ data.permissions = permissions
110
+ data
111
+
112
+ buildMentions: (data) =>
113
+ mentions = _.compact (_.map _.flatten(Array data.mentions), (entity) ->
114
+ return unless entity
115
+ { entity: entity }
116
+ )
117
+ delete data.mentions
118
+
119
+ for entity in (data.text.match(/\^(\S+)/g) || [])
120
+ entity = entity.replace(/^\^/, '')
121
+ entity = entity.replace(/^/, 'https://') unless entity.match(/^https?/)
122
+ mentions.push { entity: entity }
123
+
124
+ data.mentions = mentions if mentions.length
125
+ data
126
+
127
+ buildLicenses: (data) =>
128
+ data.licenses = _.flatten(Array data.licenses) if data.licenses
129
+ data
130
+
131
+ buildDataObject: (serializedArray) =>
132
+ data = {}
133
+ for i in serializedArray
134
+ if data[i.name]
135
+ if data[i.name].push
136
+ data[i.name].push i.value
137
+ else
138
+ data[i.name] = [data[i.name], i.value]
139
+ else
140
+ data[i.name] = i.value
141
+
142
+ @buildLicenses(@buildMentions(@buildPermissions(data)))
143
+
144
+ getData: =>
145
+ @buildDataObject @$el.serializeArray()
146
+
147
+ validate: (data = @getData()) =>
148
+ post = new TentStatus.Models.Post data
149
+ errors = post.validate(data)
150
+ @$el.find(".error").removeClass('error')
151
+ @$errors.hide()
152
+ @showErrors(errors) if errors
153
+ return !errors
154
+
155
+ showErrors: (errors) =>
156
+ error_messages = []
157
+ for err in errors
158
+ for name, msg of err
159
+ $input = @$el.find("[name='#{name}']")
160
+ $input.addClass('error')
161
+ error_messages.push msg
162
+ @$errors.html error_messages.join('<br/>')
163
+ @$errors.show()
164
+
165
+ updateCharCounter: =>
166
+ charCount = @$textarea.val()?.length
167
+ delta = @charLimit - charCount
168
+ if delta < 0
169
+ @$post_btn.attr 'disabled', 'disabled'
170
+ @$charLimit.addClass 'alert-error'
171
+ else
172
+ if delta == @charLimit
173
+ @$post_btn.attr 'disabled', 'disabled'
174
+ else
175
+ @$post_btn.removeAttr 'disabled'
176
+ @$charLimit.removeClass 'alert-error'
177
+ @$charLimit.text delta
178
+