infractores 1.0.0

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 (123) hide show
  1. checksums.yaml +7 -0
  2. data/.env.sample +8 -0
  3. data/.gitignore +22 -0
  4. data/.travis.yml +14 -0
  5. data/CODE_OF_CONDUCT.md +49 -0
  6. data/Gemfile +67 -0
  7. data/Gemfile.lock +417 -0
  8. data/Guardfile +8 -0
  9. data/LICENSE +21 -0
  10. data/Procfile +2 -0
  11. data/README.md +48 -0
  12. data/Rakefile +6 -0
  13. data/app/assets/images/.keep +0 -0
  14. data/app/assets/images/gallery-buttons.png +0 -0
  15. data/app/assets/images/logo-infractores.svg +10 -0
  16. data/app/assets/javascripts/application.js +18 -0
  17. data/app/assets/javascripts/bootstrap.js +4 -0
  18. data/app/assets/javascripts/index.js +82 -0
  19. data/app/assets/javascripts/twitter.js +18 -0
  20. data/app/assets/stylesheets/1-utilities/_variables.scss +12 -0
  21. data/app/assets/stylesheets/2-quarks/_typography.scss +32 -0
  22. data/app/assets/stylesheets/3-atoms/_buttons.scss +3 -0
  23. data/app/assets/stylesheets/4-molecules/_footer.scss +32 -0
  24. data/app/assets/stylesheets/4-molecules/_infraction-gallery.scss +101 -0
  25. data/app/assets/stylesheets/4-molecules/_navbar.scss +71 -0
  26. data/app/assets/stylesheets/5-pages/index.scss +165 -0
  27. data/app/assets/stylesheets/5-pages/show.scss +29 -0
  28. data/app/assets/stylesheets/application.css +18 -0
  29. data/app/assets/stylesheets/bootstrap_and_overrides.css +7 -0
  30. data/app/controllers/application_controller.rb +5 -0
  31. data/app/controllers/concerns/.keep +0 -0
  32. data/app/controllers/infractions_controller.rb +23 -0
  33. data/app/controllers/users_controller.rb +7 -0
  34. data/app/helpers/application_helper.rb +19 -0
  35. data/app/helpers/infractions_helper.rb +6 -0
  36. data/app/mailers/.keep +0 -0
  37. data/app/models/.keep +0 -0
  38. data/app/models/concerns/.keep +0 -0
  39. data/app/models/evidence.rb +23 -0
  40. data/app/models/infraction.rb +43 -0
  41. data/app/models/tweet.rb +54 -0
  42. data/app/models/user.rb +34 -0
  43. data/app/services/twitter_service.rb +51 -0
  44. data/app/uploaders/evidence_uploader.rb +62 -0
  45. data/app/views/infractions/_map.html.erb +17 -0
  46. data/app/views/infractions/_reported_by.html.erb +4 -0
  47. data/app/views/infractions/_summary.html.erb +23 -0
  48. data/app/views/infractions/index.html.erb +18 -0
  49. data/app/views/infractions/show.html.erb +24 -0
  50. data/app/views/layouts/application.html.erb +90 -0
  51. data/app/views/shared/_google_analytics.html.erb +12 -0
  52. data/app/views/users/index.html.erb +21 -0
  53. data/app/workers/tweet_inspector.rb +18 -0
  54. data/bin/bundle +3 -0
  55. data/bin/rails +8 -0
  56. data/bin/rake +8 -0
  57. data/bin/setup +29 -0
  58. data/bin/spring +15 -0
  59. data/config.ru +4 -0
  60. data/config/application.rb +27 -0
  61. data/config/boot.rb +3 -0
  62. data/config/database.yml.sample +85 -0
  63. data/config/environment.rb +5 -0
  64. data/config/environments/development.rb +50 -0
  65. data/config/environments/production.rb +79 -0
  66. data/config/environments/test.rb +42 -0
  67. data/config/initializers/assets.rb +15 -0
  68. data/config/initializers/backtrace_silencers.rb +7 -0
  69. data/config/initializers/carrierwave.rb +17 -0
  70. data/config/initializers/cookies_serializer.rb +3 -0
  71. data/config/initializers/filter_parameter_logging.rb +4 -0
  72. data/config/initializers/inflections.rb +16 -0
  73. data/config/initializers/mime_types.rb +4 -0
  74. data/config/initializers/session_store.rb +3 -0
  75. data/config/initializers/twitter.rb +1 -0
  76. data/config/initializers/wrap_parameters.rb +14 -0
  77. data/config/locales/en.bootstrap.yml +23 -0
  78. data/config/locales/en.yml +23 -0
  79. data/config/locales/es.yml +263 -0
  80. data/config/routes.rb +6 -0
  81. data/config/secrets.yml +22 -0
  82. data/db/migrate/20150824210535_add_tweets_table.rb +8 -0
  83. data/db/migrate/20150829133406_create_infractions.rb +11 -0
  84. data/db/migrate/20150829134520_create_users.rb +12 -0
  85. data/db/migrate/20150829141909_add_user_id_to_tweets.rb +5 -0
  86. data/db/migrate/20150829154430_create_locations.rb +10 -0
  87. data/db/migrate/20150830224603_create_evidences_table.rb +8 -0
  88. data/db/migrate/20150911020838_add_valid_column_to_infractions.rb +6 -0
  89. data/db/migrate/20150911025204_add_index_to_users_username.rb +5 -0
  90. data/db/migrate/20151102220702_add_lat_lon_to_infractions.rb +6 -0
  91. data/db/migrate/20151102221811_remove_locations.rb +5 -0
  92. data/db/migrate/20151117194703_add_reported_at_to_infractions.rb +5 -0
  93. data/db/migrate/20160229023500_change_lat_lon_precision.rb +6 -0
  94. data/db/seeds.rb +7 -0
  95. data/infractores.gemspec +25 -0
  96. data/lib/assets/.keep +0 -0
  97. data/lib/infractores.rb +1 -0
  98. data/lib/infractores/version.rb +3 -0
  99. data/lib/tasks/.keep +0 -0
  100. data/lib/tasks/twitter.rake +16 -0
  101. data/lib/tasks/users.rake +9 -0
  102. data/log/.keep +0 -0
  103. data/public/404.html +67 -0
  104. data/public/422.html +67 -0
  105. data/public/500.html +66 -0
  106. data/public/favicon.ico +0 -0
  107. data/public/robots.txt +5 -0
  108. data/vendor/assets/images/blank.gif +0 -0
  109. data/vendor/assets/images/fancybox_loading.gif +0 -0
  110. data/vendor/assets/images/fancybox_loading@2x.gif +0 -0
  111. data/vendor/assets/images/fancybox_overlay.png +0 -0
  112. data/vendor/assets/images/fancybox_sprite.png +0 -0
  113. data/vendor/assets/images/fancybox_sprite@2x.png +0 -0
  114. data/vendor/assets/images/glyphicons-halflings-white.png +0 -0
  115. data/vendor/assets/images/glyphicons-halflings.png +0 -0
  116. data/vendor/assets/javascripts/.keep +0 -0
  117. data/vendor/assets/javascripts/jquery.fancybox-buttons.js +122 -0
  118. data/vendor/assets/javascripts/jquery.fancybox.pack.js +46 -0
  119. data/vendor/assets/javascripts/livereload.js +1183 -0
  120. data/vendor/assets/stylesheets/.keep +0 -0
  121. data/vendor/assets/stylesheets/jquery.fancybox-buttons.css +97 -0
  122. data/vendor/assets/stylesheets/jquery.fancybox.css +274 -0
  123. metadata +215 -0
@@ -0,0 +1,165 @@
1
+ @import "1-utilities/variables";
2
+
3
+ body.overflow{
4
+ overflow: hidden;
5
+ }
6
+ div.infractions {
7
+ padding-top: 50px;
8
+ .main-map{
9
+ position: relative;
10
+ z-index: 0;
11
+ .map{
12
+ background-color: $light_yellow;
13
+ position: fixed !important;
14
+ height: 100%;
15
+ width:100%;
16
+ top: 0;
17
+ }
18
+ }
19
+ h2 {
20
+ font-size: 3em;
21
+ text-align: center;
22
+ margin-top: 1em;
23
+ margin-bottom: 1em;
24
+ }
25
+ .list-container{
26
+ width: 50%;
27
+ position: relative;
28
+ z-index: 1;
29
+ background: rgba(247, 104, 92, 0.85);
30
+ max-width: 550px;
31
+ ul {
32
+ padding: 0px 0 50px 0;
33
+ width: 85%;
34
+ margin: auto;
35
+ li.infraction {
36
+ padding-top: 25%;
37
+ margin-top: -10%;
38
+ &.odd, &.even{
39
+ border: 0;
40
+ background: none;
41
+ }
42
+ .content{
43
+ list-style-type: none;
44
+ overflow: hidden;
45
+ background-color: $white;
46
+ border:0;
47
+ -webkit-border-radius: 4px;
48
+ -moz-border-radius: 4px;
49
+ border-radius: 4px;
50
+ min-height: 400px;
51
+ padding: 0;
52
+ margin: auto;
53
+ .show-infraction{
54
+ margin: 4%;
55
+ padding: 1% 3%;
56
+ border: 1px solid #F7685C;
57
+ display: inline-block;
58
+ font-size: 14px;
59
+ -webkit-border-radius: 4px;
60
+ -moz-border-radius: 4px;
61
+ border-radius: 4px;
62
+ &:hover, &:focus{
63
+ text-decoration: none;
64
+ background: $red;
65
+ color: $white;
66
+ }
67
+ }
68
+ p{
69
+ border-radius: 4px 4px 0 0;
70
+ text-transform: uppercase;
71
+ margin-bottom: 0;
72
+ color: $blue;
73
+ background-color: $light_red;
74
+ overflow: hidden;
75
+ span{
76
+ padding: 0.8em 1em;
77
+ float: left;
78
+ &.date{
79
+ float: right;
80
+ border-left: 2px solid $white;
81
+ background-color: $yellow;
82
+ }
83
+ }
84
+ a{
85
+ text-transform: lowercase;
86
+ color: $red;
87
+ font-weight: 500;
88
+ }
89
+ }
90
+ div.details {
91
+ a {
92
+ padding-right: 2px;
93
+ display: inline-block;
94
+ float: none;
95
+ }
96
+ }
97
+ .description {
98
+ width: 100%;
99
+ float: none;
100
+ min-height: 200px;
101
+ .tweet{margin:2%;}
102
+ a { padding-right: 3px; }
103
+ }
104
+ .infraction-map{
105
+ display: none;
106
+ border-top: 2px solid $red;
107
+ margin: 2%;
108
+ h4{
109
+ a{
110
+ display: block;
111
+ &:after{
112
+ font-family: 'FontAwesome';
113
+ content: "\f078";
114
+ float: right;
115
+ }
116
+ }
117
+ }
118
+ .map {
119
+ display: none;
120
+ width: 100%;
121
+ height: 300px;
122
+ background-color: $light_yellow;
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ }
130
+ }
131
+ @media screen and (max-width: 768px){
132
+ div.infractions{
133
+ .list-container{
134
+ width: 100%;
135
+ max-width: none;
136
+ ul{
137
+ max-width: 500px;
138
+ li.infraction{
139
+ .content{
140
+ .infraction-map{
141
+ display: block;
142
+ }
143
+ .show-infraction{
144
+ display: none;
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ @media screen and (max-width: 500px){
153
+ div.infractions{
154
+ .list-container{
155
+ ul{
156
+ li.infraction{
157
+ p{
158
+ span.date{display: none;}
159
+ }
160
+ .description{min-height: initial;}
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
@@ -0,0 +1,29 @@
1
+ .container.infractions-show {
2
+ img {
3
+ padding: 1em;
4
+ margin: 0.2em;
5
+ }
6
+
7
+ .detail {
8
+ padding: 1em;
9
+ border: 1px solid #CCC;
10
+ margin-bottom: 1em;
11
+ }
12
+
13
+ .row.bordered {
14
+ border: 1px solid #CCC;
15
+ }
16
+
17
+ .map {
18
+ margin-right: 1em;
19
+ margin-top: 1em;
20
+ }
21
+
22
+ .tweet {
23
+ min-height: 210px;
24
+ }
25
+
26
+ .source {
27
+ margin-left: 0;
28
+ }
29
+ }
@@ -0,0 +1,18 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any styles
10
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11
+ * file per style scope.
12
+ *
13
+ *= require 'bootstrap_and_overrides'
14
+ *= require jquery.fancybox
15
+ *= require jquery.fancybox-buttons
16
+ *= require_tree .
17
+ *= require_self
18
+ */
@@ -0,0 +1,7 @@
1
+ /*
2
+ =require twitter-bootstrap-static/bootstrap
3
+
4
+ Use Font Awesome icons (default)
5
+ To use Glyphicons sprites instead of Font Awesome, replace with "require twitter-bootstrap-static/sprites"
6
+ =require twitter-bootstrap-static/fontawesome
7
+ */
@@ -0,0 +1,5 @@
1
+ class ApplicationController < ActionController::Base
2
+ # Prevent CSRF attacks by raising an exception.
3
+ # For APIs, you may want to use :null_session instead.
4
+ protect_from_forgery with: :exception
5
+ end
File without changes
@@ -0,0 +1,23 @@
1
+ class InfractionsController < ApplicationController
2
+ before_filter :load_scope, only: [:index]
3
+
4
+ # GET /infractions
5
+ def index
6
+ @infractions = @scope.legitimate.order("infractions.reported_at DESC")
7
+ @infraction_markers = @infractions.pluck(:lat, :lon, :id)
8
+ end
9
+
10
+ # GET /infractions/:id
11
+ def show
12
+ @infraction = Infraction.includes(:user).find_by(tweet_id: params[:id])
13
+ end
14
+
15
+ private
16
+
17
+ def load_scope
18
+ @scope = Infraction.includes(:evidences, :tweet, :user)
19
+ if params[:username]
20
+ @scope = @scope.where("users.username" => params[:username])
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ class UsersController < ApplicationController
2
+
3
+ # GET /users
4
+ def index
5
+ @users = User.where("infractions_count > 0").order("infractions_count DESC")
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ module ApplicationHelper
2
+
3
+ def calculate_class(*arr)
4
+ arr.any? {|x| current_page?("/#{x}") } ? 'active' : ''
5
+ end
6
+
7
+ def decorated_username(infraction)
8
+ username = infraction.username
9
+ "<a href='https://twitter.com/#{username}' target='_blank'>@#{username}</a>"
10
+ end
11
+
12
+ def screen_name
13
+ "@#{TwitterService.instance.screen_name}"
14
+ end
15
+
16
+ def screen_name_hashtag
17
+ "##{TwitterService.instance.screen_name}"
18
+ end
19
+ end
@@ -0,0 +1,6 @@
1
+ module InfractionsHelper
2
+
3
+ def decorated_time(infraction)
4
+ infraction.tweet.created_at
5
+ end
6
+ end
File without changes
File without changes
File without changes
@@ -0,0 +1,23 @@
1
+ class Evidence < ActiveRecord::Base
2
+ belongs_to :infraction
3
+
4
+ mount_uploader :media, EvidenceUploader
5
+
6
+ # Builds an [Array] of [Evidence] records from a [Tweet] using the media
7
+ # entities
8
+ #
9
+ # @param tweet [Tweet]
10
+ # @return [Array]
11
+ def self.build_from(tweet)
12
+ result = []
13
+
14
+ if extended_entities = tweet.source.extended_entities
15
+ extended_entities["media"].each do |media|
16
+ result << self.new(infraction: tweet.infraction,
17
+ remote_media_url: media["media_url_https"])
18
+ end
19
+ end
20
+
21
+ result
22
+ end
23
+ end
@@ -0,0 +1,43 @@
1
+ class Infraction < ActiveRecord::Base
2
+ belongs_to :tweet
3
+
4
+ has_many :evidences
5
+ has_one :user, through: :tweet
6
+
7
+ validates_presence_of :lon
8
+ validates_presence_of :lat
9
+ validates_presence_of :evidences
10
+
11
+ delegate :source_url, to: :tweet
12
+ delegate :username, :update_infractions_counter!, to: :user
13
+
14
+ after_save :update_infractions_counter!
15
+
16
+ scope :legitimate, -> { where(legitimate: true) }
17
+
18
+ # Builds an {Infraction} using the {Tweet}
19
+ #
20
+ # @param tweet [Tweet]
21
+ # @return [Infraction]
22
+ def self.build_from(tweet)
23
+ if tweet.source.geo
24
+ lat = tweet.source.geo["coordinates"][0]
25
+ lon = tweet.source.geo["coordinates"][1]
26
+ else
27
+ lat = lon = nil
28
+ end
29
+
30
+ result = self.new(
31
+ tweet: tweet,
32
+ description: tweet.source.text,
33
+ lat: lat,
34
+ lon: lon,
35
+ reported_at: tweet.source["created_at"]
36
+ )
37
+
38
+ result.evidences = Evidence.build_from(tweet)
39
+
40
+ result
41
+ end
42
+
43
+ end
@@ -0,0 +1,54 @@
1
+ class Tweet < ActiveRecord::Base
2
+ self.primary_key = :id
3
+
4
+ validates :id, :json, :user_id, presence: true
5
+ validates :id, uniqueness: true
6
+
7
+ belongs_to :user
8
+ has_one :infraction
9
+
10
+ validates_presence_of :user
11
+
12
+ after_save :enqueue_infraction_job
13
+
14
+ # It uses a {Twitter::Tweet} to find and update an existing {Tweet} or create
15
+ # a new one
16
+ #
17
+ # @param tuit [Twitter::Tweet]
18
+ # @return [Tweet]
19
+ def self.find_or_create!(tuit)
20
+ opts = {
21
+ id: tuit.id }
22
+
23
+ if result = self.find_by(opts)
24
+ result.update_attributes!(json: tuit.to_json)
25
+ else
26
+ result = self.new(opts.merge(json: tuit.to_json))
27
+ user = User.find_or_create!(result)
28
+ result.user = user
29
+ result.save!
30
+ end
31
+
32
+ result
33
+ end
34
+
35
+ def source
36
+ @source ||= OpenStruct.new(JSON.load(json))
37
+ end
38
+
39
+ def source_url
40
+ "https://twitter.com/#{user.username}/status/#{source.id_str}"
41
+ end
42
+
43
+ def created_at
44
+ @created_at ||= Time.parse(source["created_at"])
45
+ @created_at.in_time_zone
46
+ end
47
+
48
+ private
49
+
50
+ def enqueue_infraction_job
51
+ TweetInspector.perform_async(self.id)
52
+ end
53
+
54
+ end
@@ -0,0 +1,34 @@
1
+ class User < ActiveRecord::Base
2
+ has_many :tweets
3
+
4
+ validates :username, :name, :twitter_user_id, presence: true
5
+ has_many :infractions, through: :tweets
6
+
7
+ # It uses a {Tweet} to find and update an existing {User} or create
8
+ # a new one
9
+ #
10
+ # @param tuit [Tweet]
11
+ # @return [User]
12
+ def self.find_or_create!(tuit)
13
+ usr = OpenStruct.new(tuit.source["user"])
14
+
15
+ if result = User.find_by(twitter_user_id: usr.id)
16
+ result.update_attributes!(username: usr.screen_name,
17
+ profile_image_url: usr.profile_image_url_https,
18
+ name: usr.name)
19
+ else
20
+ result = User.new(username: usr.screen_name,
21
+ profile_image_url: usr.profile_image_url_https,
22
+ name: usr.name,
23
+ twitter_user_id: usr.id)
24
+
25
+ result.save!
26
+ end
27
+
28
+ result
29
+ end
30
+
31
+ def update_infractions_counter!
32
+ update_column :infractions_count, infractions.legitimate.count
33
+ end
34
+ end