campagne 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,7 +4,8 @@ module Campagne
4
4
 
5
5
  attr_accessible :email, :name
6
6
 
7
- validates :email, :presence => true, :uniqueness => true, :email => true, :incorrect_email => true, :denied_email => true
7
+ validates :email, :presence => true, :uniqueness => true
8
+ validates_format_of :email, :with => /^(|(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+@((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6})$/i
8
9
 
9
10
  end
10
11
  end
@@ -1,5 +1,3 @@
1
- <% title "Deliveries" %>
2
-
3
1
  <h2><%= @campaign.name %></h2>
4
2
 
5
3
  <table style="width:100%">
@@ -1,5 +1,3 @@
1
- <% title "Edit Campaign" %>
2
-
3
1
  <%= render 'form' %>
4
2
 
5
3
  <p>
@@ -1,5 +1,3 @@
1
- <% title "Campaigns" %>
2
-
3
1
  <p><%= link_to "New Campaign", new_campagne_campagne_campaign_path %></p>
4
2
 
5
3
  <table>
@@ -1,5 +1,3 @@
1
- <% title "New Campaign" %>
2
-
3
1
  <%= render 'form' %>
4
2
 
5
3
  <p><%= link_to "Back", campagne_campagne_campaigns_path %></p>
@@ -1,5 +1,3 @@
1
- <% title "Campaign" %>
2
-
3
1
  <%= form_tag schedule_campagne_campagne_campaign_path(@campaign) do %>
4
2
  <%= datetime_select("schedule", 'at', :default => Time.now) %>
5
3
  <%= submit_tag 'Schedule' %>
@@ -1,5 +1,3 @@
1
- <% title "Import contacts" %>
2
-
3
1
  <h2><%= @list.name %></h2>
4
2
 
5
3
  <%= form_tag import_campagne_campagne_list_path(@list) do %>
@@ -1,5 +1,3 @@
1
- <% title "Lists" %>
2
-
3
1
  <p><%= link_to "New List", new_campagne_campagne_list_path %></p>
4
2
 
5
3
  <table>
@@ -1,5 +1,3 @@
1
- <% title "New List" %>
2
-
3
1
  <%= render 'form' %>
4
2
 
5
3
  <p><%= link_to "Back", campagne_campagne_lists_path %></p>
@@ -1,5 +1,3 @@
1
- <% title "List" %>
2
-
3
1
  <p>
4
2
  <strong>Name:</strong>
5
3
  <%= @list.name %>
@@ -1,11 +1,118 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
- <title><%= content_for?(:title) ? yield(:title) : "Untitled" %></title>
5
- <%= stylesheet_link_tag :campagne %>
6
- <%= javascript_include_tag :campagne %>
4
+ <title>Campagne</title>
7
5
  <%= csrf_meta_tag %>
8
- <%= yield(:head) %>
6
+
7
+ <style>
8
+ <!--
9
+ body {
10
+ background-color: #CCC;
11
+ font-family: "Helvetica Nue", Helvetica, Arial;
12
+ font-size: 14px;
13
+ }
14
+
15
+ a img {
16
+ border: none;
17
+ }
18
+
19
+ a {
20
+ color: #0000FF;
21
+ }
22
+
23
+ .clear {
24
+ clear: both;
25
+ height: 0;
26
+ overflow: hidden;
27
+ }
28
+
29
+ #nav {
30
+ width: 75%;
31
+ margin: 0 auto;
32
+ padding: 0 40px;
33
+ margin-top: 20px;
34
+ }
35
+
36
+ #container {
37
+ width: 75%;
38
+ margin: 0 auto;
39
+ background-color: #FFF;
40
+ padding: 20px 40px;
41
+ border: solid 1px black;
42
+ margin-top: 20px;
43
+ }
44
+
45
+ #flash_notice, #flash_error {
46
+ padding: 5px 8px;
47
+ margin: 10px 0;
48
+ }
49
+
50
+ #flash_notice {
51
+ background-color: #CFC;
52
+ border: solid 1px #6C6;
53
+ }
54
+
55
+ #flash_error {
56
+ background-color: #FCC;
57
+ border: solid 1px #C66;
58
+ }
59
+
60
+ .fieldWithErrors {
61
+ display: inline;
62
+ }
63
+
64
+ #errorExplanation {
65
+ width: 400px;
66
+ border: 2px solid #CF0000;
67
+ padding: 0px;
68
+ padding-bottom: 12px;
69
+ margin-bottom: 20px;
70
+ background-color: #f0f0f0;
71
+ }
72
+
73
+ #errorExplanation h2 {
74
+ text-align: left;
75
+ font-weight: bold;
76
+ padding: 5px 5px 5px 15px;
77
+ font-size: 12px;
78
+ margin: 0;
79
+ background-color: #c00;
80
+ color: #fff;
81
+ }
82
+
83
+ #errorExplanation p {
84
+ color: #333;
85
+ margin-bottom: 0;
86
+ padding: 8px;
87
+ }
88
+
89
+ #errorExplanation ul {
90
+ margin: 2px 24px;
91
+ }
92
+
93
+ #errorExplanation ul li {
94
+ font-size: 12px;
95
+ list-style: disc;
96
+ }
97
+
98
+ table {
99
+ width: 50%;
100
+ border-top: 1px solid #CCC;
101
+ border-bottom: 1px solid #CCC;
102
+ }
103
+
104
+ table td {
105
+ border-top: 1px solid #CCC;
106
+ padding: 4px;
107
+ }
108
+
109
+ table th {
110
+ text-align: left;
111
+ padding: 4px;
112
+ }
113
+ -->
114
+ </style>
115
+
9
116
  </head>
10
117
  <body>
11
118
 
data/campagne.gemspec CHANGED
@@ -10,11 +10,8 @@ Gem::Specification.new do |s|
10
10
  s.summary = "campagne-#{s.version}"
11
11
  s.description = "A simple Rails 3 engine gem to manage and send newsletters."
12
12
 
13
- s.add_dependency "rails", "~> 3.2.0"
14
- s.add_dependency "mysql2", "~> 0.3.11"
15
- s.add_dependency "jquery-rails", "~> 2.0.2"
16
- s.add_dependency "resque", "~> 1.20.0"
17
- s.add_dependency "resque-scheduler", "~> 1.9.9"
13
+ s.add_dependency "rails", "3.2.3"
14
+ s.add_dependency "mysql2", "0.3.11"
18
15
 
19
16
  s.files = `git ls-files`.split("\n")
20
17
  s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
data/config/routes.rb CHANGED
@@ -2,7 +2,6 @@ Rails.application.routes.draw do
2
2
 
3
3
  namespace :campagne do
4
4
  root to: 'campagne_lists#index'
5
- mount Resque::Server.new, at: 'resque'
6
5
  resources :campagne_lists do
7
6
  member do
8
7
  get 'import'
@@ -8,16 +8,5 @@ module Campagne
8
8
  end
9
9
  end
10
10
 
11
- initializer "campagne.load_static_assets" do |app|
12
- app.middleware.use ::ActionDispatch::Static, "#{root}/public"
13
- end
14
-
15
- initializer "campagne.load_resque_config" do |app|
16
- require 'resque/server'
17
- Resque::Server.use(Rack::Auth::Basic)
18
- require 'resque_scheduler'
19
- require 'resque_scheduler/server'
20
- end
21
-
22
11
  end
23
12
  end
@@ -1,3 +1,3 @@
1
1
  module Campagne
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -1,8 +1,3 @@
1
- require 'resque/server'
2
- Resque::Server.use(Rack::Auth::Basic) do |username, password|
3
- username == 'admin' && password == "secret"
4
- end
5
-
6
1
  Rails.application.class.configure do
7
2
  config.campagne_from_name = 'Foo'
8
3
  config.campagne_from_email = 'foo@bar.com'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: campagne
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -13,59 +13,26 @@ date: 2012-05-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
16
- requirement: &70317158616080 !ruby/object:Gem::Requirement
16
+ requirement: &70221788572980 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
- - - ~>
19
+ - - =
20
20
  - !ruby/object:Gem::Version
21
- version: 3.2.0
21
+ version: 3.2.3
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70317158616080
24
+ version_requirements: *70221788572980
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: mysql2
27
- requirement: &70317158613840 !ruby/object:Gem::Requirement
27
+ requirement: &70221788572380 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
- - - ~>
30
+ - - =
31
31
  - !ruby/object:Gem::Version
32
32
  version: 0.3.11
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70317158613840
36
- - !ruby/object:Gem::Dependency
37
- name: jquery-rails
38
- requirement: &70317158612180 !ruby/object:Gem::Requirement
39
- none: false
40
- requirements:
41
- - - ~>
42
- - !ruby/object:Gem::Version
43
- version: 2.0.2
44
- type: :runtime
45
- prerelease: false
46
- version_requirements: *70317158612180
47
- - !ruby/object:Gem::Dependency
48
- name: resque
49
- requirement: &70317158611120 !ruby/object:Gem::Requirement
50
- none: false
51
- requirements:
52
- - - ~>
53
- - !ruby/object:Gem::Version
54
- version: 1.20.0
55
- type: :runtime
56
- prerelease: false
57
- version_requirements: *70317158611120
58
- - !ruby/object:Gem::Dependency
59
- name: resque-scheduler
60
- requirement: &70317158610360 !ruby/object:Gem::Requirement
61
- none: false
62
- requirements:
63
- - - ~>
64
- - !ruby/object:Gem::Version
65
- version: 1.9.9
66
- type: :runtime
67
- prerelease: false
68
- version_requirements: *70317158610360
35
+ version_requirements: *70221788572380
69
36
  description: A simple Rails 3 engine gem to manage and send newsletters.
70
37
  email:
71
38
  - arleylobato@gmail.com
@@ -78,22 +45,17 @@ files:
78
45
  - Gemfile
79
46
  - README.md
80
47
  - Rakefile
81
- - app/assets/.DS_Store
82
- - app/assets/javascripts/campagne.js
83
- - app/assets/stylesheets/campagne.css
84
48
  - app/controllers/campagne/.DS_Store
85
49
  - app/controllers/campagne/campagne_campaigns_controller.rb
86
50
  - app/controllers/campagne/campagne_deliveries_controller.rb
87
51
  - app/controllers/campagne/campagne_lists_controller.rb
88
52
  - app/helpers/campagne/error_messages_helper.rb
89
- - app/helpers/campagne/layout_helper.rb
90
53
  - app/models/campagne/campagne_campaign.rb
91
54
  - app/models/campagne/campagne_contact.rb
92
55
  - app/models/campagne/campagne_delivery.rb
93
56
  - app/models/campagne/campagne_list.rb
94
57
  - app/models/campagne/sender.rb
95
58
  - app/models/campagne/sender_job.rb
96
- - app/validators/email_validator.rb
97
59
  - app/views/campagne/.DS_Store
98
60
  - app/views/campagne/campagne_campaigns/_form.html.erb
99
61
  - app/views/campagne/campagne_campaigns/deliveries.html.erb
@@ -109,7 +71,6 @@ files:
109
71
  - app/views/campagne/campagne_lists/show.html.erb
110
72
  - app/views/layouts/campagne/campagne.html.erb
111
73
  - campagne.gemspec
112
- - config/locales/en.yml
113
74
  - config/routes.rb
114
75
  - lib/campagne.rb
115
76
  - lib/campagne/engine.rb
@@ -118,8 +79,6 @@ files:
118
79
  - lib/generators/campagne/templates/1x1.gif
119
80
  - lib/generators/campagne/templates/initializer.rb
120
81
  - lib/generators/campagne/templates/migration.rb
121
- - lib/tasks/resque.rake
122
- - test/test_helper.rb
123
82
  homepage: http://github.com/alobato/campagne
124
83
  licenses: []
125
84
  post_install_message:
@@ -134,7 +93,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
134
93
  version: '0'
135
94
  segments:
136
95
  - 0
137
- hash: -85764927293489237
96
+ hash: 2088531969562291298
138
97
  required_rubygems_version: !ruby/object:Gem::Requirement
139
98
  none: false
140
99
  requirements:
@@ -143,11 +102,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
143
102
  version: '0'
144
103
  segments:
145
104
  - 0
146
- hash: -85764927293489237
105
+ hash: 2088531969562291298
147
106
  requirements: []
148
107
  rubyforge_project:
149
108
  rubygems_version: 1.8.17
150
109
  signing_key:
151
110
  specification_version: 3
152
- summary: campagne-0.1.2
111
+ summary: campagne-0.2.0
153
112
  test_files: []
data/app/assets/.DS_Store DELETED
Binary file
@@ -1,15 +0,0 @@
1
- // This is a manifest file that'll be compiled into application.js, which will include all the files
2
- // listed below.
3
- //
4
- // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
- // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6
- //
7
- // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
- // the compiled file.
9
- //
10
- // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
11
- // GO AFTER THE REQUIRES BELOW.
12
- //
13
- //= require jquery
14
- //= require jquery_ujs
15
- //= require_tree .
@@ -1,118 +0,0 @@
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 vendor/assets/stylesheets of plugins, if any, 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 top of the
9
- * compiled file, but it's generally better to create a new file per style scope.
10
- *
11
- *= require_self
12
- *= require_tree .
13
- */
14
-
15
- body {
16
- background-color: #CCC;
17
- font-family: "Helvetica Nue", Helvetica, Arial;
18
- font-size: 14px;
19
- }
20
-
21
- a img {
22
- border: none;
23
- }
24
-
25
- a {
26
- color: #0000FF;
27
- }
28
-
29
- .clear {
30
- clear: both;
31
- height: 0;
32
- overflow: hidden;
33
- }
34
-
35
- #nav {
36
- width: 75%;
37
- margin: 0 auto;
38
- padding: 0 40px;
39
- margin-top: 20px;
40
- }
41
-
42
- #container {
43
- width: 75%;
44
- margin: 0 auto;
45
- background-color: #FFF;
46
- padding: 20px 40px;
47
- border: solid 1px black;
48
- margin-top: 20px;
49
- }
50
-
51
- #flash_notice, #flash_error {
52
- padding: 5px 8px;
53
- margin: 10px 0;
54
- }
55
-
56
- #flash_notice {
57
- background-color: #CFC;
58
- border: solid 1px #6C6;
59
- }
60
-
61
- #flash_error {
62
- background-color: #FCC;
63
- border: solid 1px #C66;
64
- }
65
-
66
- .fieldWithErrors {
67
- display: inline;
68
- }
69
-
70
- #errorExplanation {
71
- width: 400px;
72
- border: 2px solid #CF0000;
73
- padding: 0px;
74
- padding-bottom: 12px;
75
- margin-bottom: 20px;
76
- background-color: #f0f0f0;
77
- }
78
-
79
- #errorExplanation h2 {
80
- text-align: left;
81
- font-weight: bold;
82
- padding: 5px 5px 5px 15px;
83
- font-size: 12px;
84
- margin: 0;
85
- background-color: #c00;
86
- color: #fff;
87
- }
88
-
89
- #errorExplanation p {
90
- color: #333;
91
- margin-bottom: 0;
92
- padding: 8px;
93
- }
94
-
95
- #errorExplanation ul {
96
- margin: 2px 24px;
97
- }
98
-
99
- #errorExplanation ul li {
100
- font-size: 12px;
101
- list-style: disc;
102
- }
103
-
104
- table {
105
- width: 50%;
106
- border-top: 1px solid #CCC;
107
- border-bottom: 1px solid #CCC;
108
- }
109
-
110
- table td {
111
- border-top: 1px solid #CCC;
112
- padding: 4px;
113
- }
114
-
115
- table th {
116
- text-align: left;
117
- padding: 4px;
118
- }
@@ -1,26 +0,0 @@
1
- # These helper methods can be called in your template to set variables to be used in the layout
2
- # This module should be included in all views globally,
3
- # to do so you may need to add this line to your ApplicationController
4
- # helper :layout
5
- module Campagne
6
- module LayoutHelper
7
-
8
- def title(page_title, show_title = true)
9
- content_for(:title) { h(page_title.to_s) }
10
- @show_title = show_title
11
- end
12
-
13
- def show_title?
14
- @show_title
15
- end
16
-
17
- def stylesheet(*args)
18
- content_for(:head) { stylesheet_link_tag(*args) }
19
- end
20
-
21
- def javascript(*args)
22
- content_for(:head) { javascript_include_tag(*args) }
23
- end
24
-
25
- end
26
- end
@@ -1,56 +0,0 @@
1
- # encoding: UTF-8
2
- require 'mail'
3
-
4
- class EmailValidator < ActiveModel::EachValidator
5
- def validate_each(record, attribute, value)
6
- return if value.blank?
7
- begin
8
- # http://my.rails-royce.org/2010/07/21/email-validation-in-ruby-on-rails-without-regexp/
9
- m = Mail::Address.new(value)
10
- # We must check that value contains a domain and that value is an email address
11
- r = m.domain && m.address == value
12
- t = m.__send__(:tree)
13
- # We need to dig into treetop
14
- # A valid domain must have dot_atom_text elements size > 1
15
- # user@localhost is excluded
16
- # treetop must respond to domain
17
- # We exclude valid email values like <user@localhost.com>
18
- # Hence we use m.__send__(tree).domain
19
- r &&= (t.domain.dot_atom_text.elements.size > 1)
20
- rescue Exception => e
21
- r = false
22
- end
23
- record.errors[attribute] << (options[:message] || 'inválido') unless r
24
- end
25
- end
26
-
27
- class IncorrectEmailValidator < ActiveModel::EachValidator
28
- def validate_each(record, attribute, value)
29
- domains = %w(
30
- hotmail.com.br gmail.com.br
31
- hotamil.com hotimail.com hotmail.com.br hotmail.com.com hotmail.con hotmal.com hoymail.com hotmil.com
32
- gamil.com gmail.com.br gmal.com gmeil.com.br gmial.com
33
- teste.com teste.com.br
34
- yaoo.com
35
- .com.be
36
- )
37
- domains.each do |d|
38
- if value && value.include?("@#{d}")
39
- record.errors[attribute] << 'está incorreto'
40
- break
41
- end
42
- end
43
- end
44
- end
45
-
46
- class DeniedEmailValidator < ActiveModel::EachValidator
47
- def validate_each(record, attribute, value)
48
- domains = %w(mailinator.com dodgit.com uggsrock.com spambox.us spamhole.com spam.la trashymail.com guerrillamailblock.com spamspot.com spamfree tempomail.fr jetable.net maileater.com meltmail.com)
49
- domains.each do |d|
50
- if value && value.include?("@#{d}")
51
- record.errors[attribute] << 'inválido'
52
- break
53
- end
54
- end
55
- end
56
- end
@@ -1,5 +0,0 @@
1
- # Sample localization file for English. Add more files in this directory for other locales.
2
- # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3
-
4
- en:
5
- hello: "Hello world"
@@ -1,100 +0,0 @@
1
- require 'resque/tasks'
2
- require 'resque_scheduler/tasks'
3
-
4
- def run_worker(queue, count = 1)
5
- puts "Starting #{count} worker(s) with QUEUE: #{queue}"
6
- ops = {:pgroup => true, :err => [(Rails.root + "log/workers_error.log").to_s, "a"],
7
- :out => [(Rails.root + "log/workers.log").to_s, "a"]}
8
- env_vars = {
9
- "QUEUE" => queue.to_s,
10
- "BACKGROUND" => "1",
11
- "PIDFILE" => (Rails.root + "tmp/pids/resque.pid").to_s,
12
- "VERBOSE" => "1"
13
- }
14
- count.times {
15
- pid = spawn(env_vars, "rake resque:work", ops)
16
- Process.detach(pid)
17
- }
18
- end
19
-
20
- def run_scheduler
21
- puts "Starting resque scheduler"
22
- env_vars = {
23
- "BACKGROUND" => "1",
24
- "PIDFILE" => (Rails.root + "tmp/pids/resque_scheduler.pid").to_s,
25
- "VERBOSE" => "1"
26
- }
27
- ops = {:pgroup => true, :err => [(Rails.root + "log/scheduler_error.log").to_s, "a"],
28
- :out => [(Rails.root + "log/scheduler.log").to_s, "a"]}
29
- pid = spawn(env_vars, "rake resque:scheduler", ops)
30
- Process.detach(pid)
31
- end
32
-
33
- namespace :resque do
34
- task :setup => :environment
35
-
36
- desc "Restart running workers"
37
- task :restart_workers => :environment do
38
- Rake::Task['resque:stop_workers'].invoke
39
- Rake::Task['resque:start_workers'].invoke
40
- end
41
-
42
- desc "Quit running workers"
43
- task :stop_workers => :environment do
44
- pids = Array.new
45
- Resque.workers.each do |worker|
46
- pids.concat(worker.worker_pids)
47
- end
48
- if pids.empty?
49
- puts "No workers to kill"
50
- else
51
- syscmd = "kill -s QUIT #{pids.join(' ')}"
52
- puts "Running syscmd: #{syscmd}"
53
- system(syscmd)
54
- end
55
- end
56
-
57
- desc "Start workers"
58
- task :start_workers => :environment do
59
- run_worker("default", 1)
60
- end
61
-
62
- desc "Restart scheduler"
63
- task :restart_scheduler => :environment do
64
- Rake::Task['resque:stop_scheduler'].invoke
65
- Rake::Task['resque:start_scheduler'].invoke
66
- end
67
-
68
- desc "Quit scheduler"
69
- task :stop_scheduler => :environment do
70
- pidfile = Rails.root + "tmp/pids/resque_scheduler.pid"
71
- if !File.exists?(pidfile)
72
- puts "Scheduler not running"
73
- else
74
- pid = File.read(pidfile).to_i
75
- syscmd = "kill -s QUIT #{pid}"
76
- puts "Running syscmd: #{syscmd}"
77
- system(syscmd)
78
- FileUtils.rm_f(pidfile)
79
- end
80
- end
81
-
82
- desc "Start scheduler"
83
- task :start_scheduler => :environment do
84
- run_scheduler
85
- end
86
-
87
- desc "Reload schedule"
88
- task :reload_schedule => :environment do
89
- pidfile = Rails.root + "tmp/pids/resque_scheduler.pid"
90
-
91
- if !File.exists?(pidfile)
92
- puts "Scheduler not running"
93
- else
94
- pid = File.read(pidfile).to_i
95
- syscmd = "kill -s USR2 #{pid}"
96
- puts "Running syscmd: #{syscmd}"
97
- system(syscmd)
98
- end
99
- end
100
- end
data/test/test_helper.rb DELETED
File without changes