[PATCH] Email system is configured via a web interface now.
by Darryl L. Pierce
From: Darryl L. Pierce <dpierce(a)redhat.com>
This patch requires a migration.
This patch removes the need for config/mailer.yml. It also introduces a
new base class, ConfigGroup, for aggregating different properties to
representing a single logical group.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/controllers/admin_controller.rb | 30 ++++++
app/controllers/application.rb | 1 +
app/controllers/emails_controller.rb | 71 ++++++++++++++
app/controllers/users_controller.rb | 29 +++---
app/helpers/emails_helper.rb | 2 +
app/models/config_group.rb | 33 +++++++
app/models/mail_config.rb | 28 ++++++
app/models/user_mailer.rb | 27 ++++--
app/views/admin/index.html.erb | 3 +
app/views/emails/edit.html.erb | 55 +++++++++++
app/views/emails/show.html.erb | 51 ++++++++++
app/views/emails/update.html.erb | 2 +
app/views/layouts/default.html.erb | 3 +
app/views/report/index.html.erb | 1 -
config/initializers/mailer.rb | 39 ++++----
config/initializers/schedules.rb | 4 +
config/mailer.yml.example | 12 ---
config/routes.rb | 6 +
db/migrate/001_create_users.rb | 10 +-
.../025_add_admin_site_to_user_privileges.rb | 31 ++++++
test/fixtures/user_privileges.yml | 1 +
test/functional/emails_controller_test.rb | 100 ++++++++++++++++++++
22 files changed, 479 insertions(+), 60 deletions(-)
create mode 100644 app/controllers/admin_controller.rb
create mode 100644 app/controllers/emails_controller.rb
create mode 100644 app/helpers/emails_helper.rb
create mode 100644 app/models/config_group.rb
create mode 100644 app/models/mail_config.rb
create mode 100644 app/views/admin/index.html.erb
create mode 100644 app/views/emails/edit.html.erb
create mode 100644 app/views/emails/show.html.erb
create mode 100644 app/views/emails/update.html.erb
delete mode 100644 config/mailer.yml.example
create mode 100644 db/migrate/025_add_admin_site_to_user_privileges.rb
create mode 100644 test/functional/emails_controller_test.rb
diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb
new file mode 100644
index 0000000..e6b4938
--- /dev/null
+++ b/app/controllers/admin_controller.rb
@@ -0,0 +1,30 @@
+# admin_controller.rb
+# Copyright (C) 2008, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+class AdminController < ApplicationController
+ before_filter :authenticated
+ before_filter :can_admin_site
+
+ def can_admin_site
+ unless @user && @user.privileges.admin_site
+ respond_to do |format|
+ flash[:error] = 'You do not have administrative privileges.'
+ format.html { redirect_to error_path }
+ end
+ end
+ end
+end
diff --git a/app/controllers/application.rb b/app/controllers/application.rb
index 22c7661..ae9cc2a 100644
--- a/app/controllers/application.rb
+++ b/app/controllers/application.rb
@@ -58,6 +58,7 @@ class ApplicationController < ActionController::Base
def handle_exceptions
yield
rescue Exception => error
+ puts error.message
puts error.backtrace
erase_results
@title = "An Error Has Occurred."
diff --git a/app/controllers/emails_controller.rb b/app/controllers/emails_controller.rb
new file mode 100644
index 0000000..13afd87
--- /dev/null
+++ b/app/controllers/emails_controller.rb
@@ -0,0 +1,71 @@
+# emails_controller.rb
+# Copyright (C) 2008, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# Allows an admin to modify the email system.
+class EmailsController < AdminController
+ before_filter :load_config, :only => [:edit, :show, :update]
+
+ # DISABLED
+ def new
+ respond_to do |format|
+ flash[:error] = 'The NEW function is not supported for the email system.'
+ format.html { redirect_to error_path }
+ end
+ end
+
+ # GET /admin/emails/edit
+ def edit
+ end
+
+ # GET /admin/emails
+ def show
+ end
+
+ # PUT /admin/emails
+ def update
+ ConfigProperty.transaction do
+ puts "params[:hostname] == #{params[:hostname]}"
+ @config.hostname = params[:hostname]
+ @config.hostport = params[:hostport]
+ @config.from_address = params[:from_address]
+ @config.username = params[:server_username]
+ @config.password = params[:server_password]
+ @config.auth_type = params[:auth_type]
+ @config.use_tls = params[:use_tls]
+ end
+
+ # restart the email system
+ load_mailer_config
+
+ respond_to do |format|
+ format.html { redirect_to admin_email_path }
+ end
+ end
+
+ # DISABLED
+ def destroy
+ respond_to do |format|
+ format.html { redirect_to error_path }
+ end
+ end
+
+ private
+
+ def load_config
+ @config = MailConfig.new
+ end
+end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 87ca3e2..3706f03 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -63,22 +63,23 @@ class UsersController < ApplicationController
def create
respond_to do |format|
if (@user == nil) || (@user.create_users?)
- @this_user = User.new(params[:user])
- @this_user.privileges = UserPrivilege.new
- @this_user.notifications = Notifications.new
- @this_user.verification = UserVerification.new(
- :sent => Date.today,
- :token => UserVerification.create_token)
-
- if @this_user.save
- UserMailer.deliver_email_verification(@this_user,(a)this_user.verification.token)
+ User.transaction do
+ @this_user = User.new(params[:user])
+ @this_user.privileges = UserPrivilege.new
+ @this_user.notifications = Notifications.new
+ @this_user.verification = UserVerification.new(:sent => Date.today,
+ :token => UserVerification.create_token)
+
+ if @this_user.save
+ UserMailer.deliver_email_verification(@this_user,(a)this_user.verification.token)
- flash[:message] = "An email verification has been sent to #{(a)this_user.email}."
- format.html { redirect_to user_path(@this_user) }
- else
- @title = "User Account (New)"
- @this_user.valid?
- format.html { render :action => :edit }
+ flash[:message] = "An email verification has been sent to #{(a)this_user.email}."
+ format.html { redirect_to user_path(@this_user) }
+ else
+ @title = "User Account (New)"
+ @this_user.valid?
+ format.html { render :action => :edit }
+ end
end
else
flash[:error] = "You cannot create a new user account."
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
new file mode 100644
index 0000000..b4dc6ec
--- /dev/null
+++ b/app/helpers/emails_helper.rb
@@ -0,0 +1,2 @@
+module EmailsHelper
+end
diff --git a/app/models/config_group.rb b/app/models/config_group.rb
new file mode 100644
index 0000000..5e93d78
--- /dev/null
+++ b/app/models/config_group.rb
@@ -0,0 +1,33 @@
+# Copyright (C) 2008, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# A +ConfigGroup+ pulls together several +ConfigProperty+ instances to represent a
+# single, logical group.
+class ConfigGroup
+ class << self
+ def property(field, key, *args)
+ module_eval <<-"end;"
+ def #{field}()
+ ConfigProperty.fetch("#{key}")
+ end
+
+ def #{field}=(val)
+ ConfigProperty.store("#{key}", val)
+ end
+ end;
+ end
+ end
+end
diff --git a/app/models/mail_config.rb b/app/models/mail_config.rb
new file mode 100644
index 0000000..5a3dbf0
--- /dev/null
+++ b/app/models/mail_config.rb
@@ -0,0 +1,28 @@
+''# mail_config.rb
+# Copyright (C) 2008, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+class MailConfig < ConfigGroup
+ AUTH_TYPES = {"Login" => "login", "Plain" => "plain"}
+
+ property :hostname, "email.server.name"
+ property :hostport, "email.server.port"
+ property :from_address, "email.server.from-address"
+ property :username, "email.server.username"
+ property :password, "email.server.password"
+ property :auth_type, "email.server.auth-type"
+ property :use_tls, "email.server.use-tls"
+end
diff --git a/app/models/user_mailer.rb b/app/models/user_mailer.rb
index 5609dc8..a50235f 100644
--- a/app/models/user_mailer.rb
+++ b/app/models/user_mailer.rb
@@ -18,32 +18,36 @@
class UserMailer < ActionMailer::Base
# Sends an email verification to the specified user.
def email_verification(user, token)
+ load_mailer_config
recipients user.email
- from MAIL_CONFIG[:from]
+ from ConfigProperty.fetch("email.from-address")
subject "Email verification"
body :user => user, :token => token
end
# Sends an email to the old address when the user changes his email address.
def email_change_notification(user, old_email)
+ load_mailer_config
recipients old_email
- from MAIL_CONFIG[:from]
+ from ConfigProperty.fetch("email.from-address")
subject "Email change notification"
body :user => user, :old_email => old_email
end
# Send an e-mail to an user and notify him his new password.
def new_generated_password(user, new_password)
+ load_mailer_config
recipients user.email
- from MAIL_CONFIG[:from]
+ from ConfigProperty.fetch("email.from-address")
subject "Your password"
body :user => user, :new_password => new_password
end
# Sends an email to a product owner when a user requests a product role.
def product_role_request(requested_role)
+ load_mailer_config
recipients requested_role.product.owner.email
- from MAIL_CONFIG[:from]
+ from ConfigProperty.fetch("email.from-address")
subject "Role request for #{requested_role.product.name}..."
body :requested_role => requested_role
end
@@ -51,17 +55,19 @@ class UserMailer < ActionMailer::Base
# Sends an email to the user letting them know the disposition for their
# product role request.
def product_role_disposition(role)
+ load_mailer_config
recipients role.user.email
- from MAIL_CONFIG[:from]
+ from ConfigProperty.fetch("email.from-address")
subject "Role request status..."
body :role => role
end
# Sends an email to a user letting him know his daily activities.
def daily_updates(user, cc_list, open_items, completed_items, task_performed)
+ load_mailer_config
recipients user.email
cc cc_list
- from MAIL_CONFIG[:from]
+ from ConfigProperty.fetch("email.from-address")
subject "Daily updates for #{user.display_name}"
body :user => user, :open_items => open_items,
:completed_items => completed_items, :tasks_performed => task_performed
@@ -70,8 +76,9 @@ class UserMailer < ActionMailer::Base
# Send an email to a user to notify him that no activity has been detected in
# his backlog.
def no_activity_recorded(user, backlog_items)
+ load_mailer_config
recipients user.email
- from MAIL_CONFIG[:from]
+ from ConfigProperty.fetch("email.from-address")
subject "No activity has been detected"
body :user => user, :backlog_items => backlog_items
end
@@ -79,8 +86,9 @@ class UserMailer < ActionMailer::Base
# Send an email to a user to notify him that no task has been detected in his
# backlog.
def no_task_recorded(user, backlog_items)
+ load_mailer_config
recipients user.email
- from MAIL_CONFIG[:from]
+ from ConfigProperty.fetch("email.from-address")
subject "No task has been detected"
body :user => user, :backlog_items => backlog_items
end
@@ -88,8 +96,9 @@ class UserMailer < ActionMailer::Base
# Send an email to a user to notify him the status of each sprint of his
# products.
def sprints_products_status(user, products)
+ load_mailer_config
recipients user.email
- from MAIL_CONFIG[:from]
+ from ConfigProperty.fetch("email.from-address")
subject "Status of sprints"
body :user => user, :products => products
end
diff --git a/app/views/admin/index.html.erb b/app/views/admin/index.html.erb
new file mode 100644
index 0000000..ab027b7
--- /dev/null
+++ b/app/views/admin/index.html.erb
@@ -0,0 +1,3 @@
+<ul>
+ <li><%= link_to "Configuration Email", admin_email_url %></li>
+</ul>
diff --git a/app/views/emails/edit.html.erb b/app/views/emails/edit.html.erb
new file mode 100644
index 0000000..231127a
--- /dev/null
+++ b/app/views/emails/edit.html.erb
@@ -0,0 +1,55 @@
+s<fieldset id="email-config">
+ <legend>Email Configuration</legend>
+ <% form_tag admin_email_path, :method => :put do %>
+ <table class="edit">
+ <tbody>
+ <tr>
+ <td class="label">Server hostname</td>
+ <td class="value"><%= text_field_tag :hostname, @config.hostname %></td>
+ </tr>
+
+ <tr>
+ <td class="label">Server port</td>
+ <td class="value"><%= text_field_tag :hostport, @config.hostport, :maxlength => 5, :size => 5 %></td>
+ </tr>
+
+ <tr>
+ <td class="label">From address</td>
+ <td class="value"<%= text_field_tag :from_address, @config.from_address %></td>
+ </tr>
+
+ <tr>
+ <td class="label">Authentication type</td>
+ <td class="value">
+ <%= select_tag :auth_type,
+ options_for_select(MailConfig::AUTH_TYPES, @config.auth_type) %>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="label">Username</td>
+ <td class="value"><%= text_field_tag :server_username, @config.username %></td>
+ </tr>
+
+ <tr>
+ <td class="label">Password</td>
+ <td class="value"><%= password_field_tag :server_password, @config.password %></td>
+ </tr>
+
+ <tr>
+ <td />
+ <td class="value">
+ <%= check_box_tag :use_tls, true, @config.use_tls %>
+ <%= label_tag :require_tls, "Requires TLS for connections." %>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="buttons" colspan="2">
+ <%= submit_tag "Save" %>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <% end %>
+</fieldset>
diff --git a/app/views/emails/show.html.erb b/app/views/emails/show.html.erb
new file mode 100644
index 0000000..24fac52
--- /dev/null
+++ b/app/views/emails/show.html.erb
@@ -0,0 +1,51 @@
+<table class="detail">
+ <thead>
+ <tr>
+ <th class="title" colspan="2">Email Configuration</th>
+ </tr>
+ <tr>
+ <th class="toolbar" colspan="2">
+ <%= link_to(image_tag("icons/edit.png", :title => "Edit this configuration..."),
+ edit_admin_email_url) %>
+ </th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <tr>
+ <td class="label">Hostname:</td>
+ <td class="value"><%= @config.hostname %></td>
+ </tr>
+
+ <tr>
+ <td class="label">Port:</td>
+ <td class="value"><%= @config.hostport %></td>
+ </tr>
+
+ <tr>
+ <td class="label">From address:</td>
+ <td class="value"><%= @config.from_address %></td>
+ </tr>
+
+ <tr>
+ <td class="label">Username:</td>
+ <td class="value"><%= @config.username %></td>
+ </tr>
+
+ <tr>
+ <td class="label">Password:</td>
+ <td class="value"><%= @config.password %></td>
+ </tr>
+
+ <tr>
+ <td class="label">Authentication type:</td>
+ <td class="value"><%= @config.auth_type %></td>
+ </tr>
+
+ <tr>
+ <td class="label">Use TLS:</td>
+ <td class="value"><%= @config.use_tls %></td>
+ </tr>
+
+ </tbody>
+</table>
diff --git a/app/views/emails/update.html.erb b/app/views/emails/update.html.erb
new file mode 100644
index 0000000..4e496a6
--- /dev/null
+++ b/app/views/emails/update.html.erb
@@ -0,0 +1,2 @@
+<h1>Emails#update</h1>
+<p>Find me in app/views/emails/update.html.erb</p>
diff --git a/app/views/layouts/default.html.erb b/app/views/layouts/default.html.erb
index d22ef97..9e27d4d 100644
--- a/app/views/layouts/default.html.erb
+++ b/app/views/layouts/default.html.erb
@@ -32,6 +32,9 @@
<li><%= link_to "Products (#{@product_count})", products_path %></li>
<li><%= link_to "Users (#{@user_count})", users_path %></li>
<li><%= link_to "Reports", :controller => :report, :action => :index %></li>
+ <% if @user && @user.privileges.admin_site %>
+ <li><%= link_to "Admin", admin_url %></li>
+ <% end %>
</ul>
<% if flash[:message] %>
diff --git a/app/views/report/index.html.erb b/app/views/report/index.html.erb
index 4436714..840753b 100644
--- a/app/views/report/index.html.erb
+++ b/app/views/report/index.html.erb
@@ -1,4 +1,3 @@
-
<table>
<% form_tag(:action => :effort) do %>
diff --git a/config/initializers/mailer.rb b/config/initializers/mailer.rb
index d660238..f62783e 100644
--- a/config/initializers/mailer.rb
+++ b/config/initializers/mailer.rb
@@ -15,25 +15,26 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Load the config or error
-mailer_file_path = "#{RAILS_ROOT}/config/mailer.yml"
-if not File.exist?(mailer_file_path)
- raise "Mailer configuration file not found at #{mailer_file_path}"
-end
-MAIL_CONFIG = YAML.load(File.open(mailer_file_path))
-require 'tlsmail'
+# Initializes the email configuration.
+def load_mailer_config
+ require 'tlsmail'
+
+ Net::SMTP.enable_tls(OpenSSL::SSL::VERIFY_NONE)
+ ActionMailer::Base.delivery_method = :smtp
+ # ActionMailer::Base.perform_deliveries = true
+ # ActionMailer::Base.raise_delivery_errors = truey
-Net::SMTP.enable_tls(OpenSSL::SSL::VERIFY_NONE)
-ActionMailer::Base.delivery_method = MAIL_CONFIG[:delivery_method]
-ActionMailer::Base.perform_deliveries = MAIL_CONFIG[:perform_deliveries]
-ActionMailer::Base.default_charset = MAIL_CONFIG[:default_charset]
-ActionMailer::Base.raise_delivery_errors = MAIL_CONFIG[:raise_delivery_errors]
+ config = MailConfig.new
+
+ ActionMailer::Base.smtp_settings = {
+ :address => config.hostname,
+ :port => config.hostport.to_i,
+ :tls => config.use_tls,
+ :authentication => config.auth_type,
+ :user_name => config.username,
+ :password => config.password
+ }
+end
-ActionMailer::Base.smtp_settings = {
- :address => MAIL_CONFIG[:server_name],
- :port => MAIL_CONFIG[:server_port],
- :tls => MAIL_CONFIG[:require_smtp_tls],
- :authentication => MAIL_CONFIG[:server_auth_mode],
- :user_name => MAIL_CONFIG[:server_username],
- :password => MAIL_CONFIG[:server_password]
-}
+# load_mailer_config
diff --git a/config/initializers/schedules.rb b/config/initializers/schedules.rb
index 16996c2..c95ba3f 100644
--- a/config/initializers/schedules.rb
+++ b/config/initializers/schedules.rb
@@ -29,6 +29,7 @@ SCHEDULES_CONFIG = YAML.load(File.open(schedules_file_path))
threads = {}
threads["daily updates"] = Thread.new do
+ load_mail_config
scheduler = Scheduler.new
scheduler.start
@@ -87,6 +88,7 @@ threads["daily updates"] = Thread.new do
end
threads["daily reminders"] = Thread.new do
+ load_mail_config
scheduler = Scheduler.new
scheduler.start
@@ -134,6 +136,7 @@ threads["daily reminders"] = Thread.new do
end
threads["product status"] = Thread.new do
+ load_mail_config
scheduler = Scheduler.new
scheduler.start
@@ -158,6 +161,7 @@ threads["product status"] = Thread.new do
end
threads["user verification expiration"] = Thread.new do
+ load_mail_config
scheduler = Scheduler.new
scheduler.start
diff --git a/config/mailer.yml.example b/config/mailer.yml.example
deleted file mode 100644
index 1dd4aa2..0000000
--- a/config/mailer.yml.example
+++ /dev/null
@@ -1,12 +0,0 @@
----
-:delivery_method: :smtp
-:require_smtp_tls: true
-:perform_deliveries: true
-:default_charset: utf-8
-:raise_delivery_errors: true
-:server_name: smtp.gmail.com
-:server_port: 587
-:server_auth_mode: :login
-:server_username: your_username
-:server_password: your_password
-:from: noreply(a)projxp.org
diff --git a/config/routes.rb b/config/routes.rb
index 6ae69bc..6bf6212 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -16,6 +16,12 @@
#
ActionController::Routing::Routes.draw do |map|
+ map.admin "/admin", :controller => 'admin', :action => 'index'
+
+ map.resource :admin do |admin|
+ admin.resource :email
+ end
+
map.resources :projects
map.resources :products do |product|
product.resources :roles
diff --git a/db/migrate/001_create_users.rb b/db/migrate/001_create_users.rb
index 3349083..49b9e6e 100644
--- a/db/migrate/001_create_users.rb
+++ b/db/migrate/001_create_users.rb
@@ -20,16 +20,16 @@ class CreateUsers < ActiveRecord::Migration
t.string :display_name, :null => false, :limit => 128
t.string :hashed_password, :null => false
t.string :salt, :null => false
- t.text :introduction, :null => true
+ t.text :introduction, :null => true
t.timestamps
end
-
+
add_index :users, :email, :unique => true
- user = User.new(:email => 'admin(a)localhost.localdomain',
- :display_name => 'Admin')
- user.password = 'chiroxp'
+ user = User.new(:email => 'admin(a)projxp.org',
+ :display_name => 'Admin')
+ user.password = 'projxp'
user.save!
end
diff --git a/db/migrate/025_add_admin_site_to_user_privileges.rb b/db/migrate/025_add_admin_site_to_user_privileges.rb
new file mode 100644
index 0000000..cb89cb8
--- /dev/null
+++ b/db/migrate/025_add_admin_site_to_user_privileges.rb
@@ -0,0 +1,31 @@
+# add_admin_site_to_user_privileges.rb
+# Copyright (C) 2009, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+class AddAdminSiteToUserPrivileges < ActiveRecord::Migration
+ def self.up
+ add_column :user_privileges, :admin_site, :boolean, :nil => true, :default => false
+ user = User.find(:all).each do |user|
+ user.privileges.admin_site = user.privileges.admin_users && user.privileges.admin_projects
+ puts "Granting admin rights to #{user.display_name}." if user.privileges.admin_site
+ user.privileges.save!
+ end
+ end
+
+ def self.down
+ remove_column :user_privileges, :admin_site
+ end
+end
diff --git a/test/fixtures/user_privileges.yml b/test/fixtures/user_privileges.yml
index 38ffbf7..ba57357 100644
--- a/test/fixtures/user_privileges.yml
+++ b/test/fixtures/user_privileges.yml
@@ -4,6 +4,7 @@ admin_privileges:
user_id: <%= Fixtures.identify(:admin) %>
admin_projects: true
admin_users: true
+ admin_site: true
projxp_owner_privileges:
user_id: <%= Fixtures.identify(:projxp_owner) %>
diff --git a/test/functional/emails_controller_test.rb b/test/functional/emails_controller_test.rb
new file mode 100644
index 0000000..c4782be
--- /dev/null
+++ b/test/functional/emails_controller_test.rb
@@ -0,0 +1,100 @@
+# emails_controller.rb
+# Copyright (C) 2009, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class EmailsControllerTest < ActionController::TestCase
+ fixtures :users
+
+ def setup
+ @admin = users(:admin)
+ raise "user must be a site admin!" unless @admin.privileges.admin_site
+
+ @nonadmin = users(:mcpierce)
+ raise "User must not be an admin!" if @nonadmin.privileges.admin_site
+
+ @config = {
+ :hostname => "email.server.projxp.org",
+ :hostport => "999",
+ :from_addres => "dude(a)lebowski.com",
+ :username => "username",
+ :password => "password",
+ :auth_type => "login",
+ :use_tls => "true"}
+ end
+
+ # Ensures that the new action is disabled.
+ def test_new
+ get :new, {}, {:user_id => @admin.id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that anonymous users cannot access the edit page.
+ def test_edit_as_anonymous
+ get :edit
+
+ assert_redirected_to login_path
+ end
+
+ # Ensures that only users with admin privileges can edit the
+ # email configuration.
+ def test_edit_as_nonadmin
+ get :edit, {}, {:user_id => @nonadmin.id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that admins can edit the email configuration.
+ def test_edit
+ get :edit, {}, {:user_id => @admin.id}
+
+ assert_response :success
+ end
+
+ # Ensures anonymous users cannot update the email config.
+ def test_update_as_anonymous
+ put :update
+
+ assert_redirected_to login_url
+ end
+
+ # Ensures that non-admins cannot update the email config.
+ def test_update_as_nonadmin
+ put :update, {}, {:user_id => @nonadmin.id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that admins can update the email config.
+ def test_update
+ put :update,
+ @config,
+ {:user_id => @admin.id}
+
+ assert_redirected_to admin_email_url
+ assert_equal @config[:hostname], ConfigProperty.find_by_name('email.server.name').value,
+ "Values were not properly saved."
+ end
+
+ # Ensures that the destroy method is disabled.
+ def test_destroy
+ delete :destroy, {}, {:user_id => @admin.id }
+
+ assert_redirected_to error_path
+ end
+end
--
1.6.0.2
15 years, 3 months
[PATCH] Created a new model for configuration values.
by Darryl L. Pierce
A migration is needed for this patch.
The new table is named config_properties and contains individual
configuration elements. The goal is to replace all configuration
files, exception database.yml, with this entries in this table.
Each element has a name and a value. Values can be null or empty,
but the name must be non-null and unique.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
.gitignore | 1 +
app/controllers/application.rb | 1 -
app/models/config_property.rb | 60 ++++++++++
db/migrate/024_create_config_properties.rb | 31 ++++++
db/schema.rb | 163 ----------------------------
test/fixtures/config_properties.yml | 3 +
test/unit/config_property_test.rb | 95 ++++++++++++++++
7 files changed, 190 insertions(+), 164 deletions(-)
create mode 100644 app/models/config_property.rb
create mode 100644 db/migrate/024_create_config_properties.rb
delete mode 100644 db/schema.rb
create mode 100644 test/fixtures/config_properties.yml
create mode 100644 test/unit/config_property_test.rb
diff --git a/.gitignore b/.gitignore
index 0dca223..2b1aabd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
config/database.yml
config/mailer.yml
config/schedules.yml
+db/schema.rb
diff --git a/app/controllers/application.rb b/app/controllers/application.rb
index d318bad..22c7661 100644
--- a/app/controllers/application.rb
+++ b/app/controllers/application.rb
@@ -48,7 +48,6 @@ class ApplicationController < ActionController::Base
end
def load_counts
- puts "LOADING COUNTS"
@project_count = Project.find(:all).size
@product_count = Product.find(:all).size
@user_count = User.find(:all).size
diff --git a/app/models/config_property.rb b/app/models/config_property.rb
new file mode 100644
index 0000000..85255e7
--- /dev/null
+++ b/app/models/config_property.rb
@@ -0,0 +1,60 @@
+# config_property.rb
+# Copyright (C) 2009, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# A +ConfigProperty+ represents a single configuration element.
+#
+# The +name+ is the unique key for the element.
+#
+# The +value+ is the value for the element.
+#
+# The +type+ indicates the element type.
+#
+# Configuration values should not be retrieved directly. Instead, the developer
+# should fetch and store values by name. An instance of +ConfigProperty+ should
+# never be used directly.
+class ConfigProperty < ActiveRecord::Base
+ validates_presence_of :name,
+ :message => "A name is required."
+ validates_uniqueness_of :name,
+ :message => "The name must be unique."
+
+ # Fetches a configuration value by name.
+ # If not found, the default value is returned.
+ # If no default value is provided, then +nil+ is returned.
+ def self.fetch(name, default = nil)
+ raise Exception.new("Missing property name") unless name && !name.empty?
+
+ result = ConfigProperty.find_by_name(name)
+
+ return result.value if result
+ return default
+ end
+
+ # Saves a configuration value.
+ def self.store(name, value = nil)
+ raise Exception.new("Missing property name") unless name && !name.empty?
+
+ property = ConfigProperty.find_by_name(name)
+
+ if property
+ property.value = value
+ property.save
+ else
+ ConfigProperty.create(:name => name, :value => value)
+ end
+ end
+end
diff --git a/db/migrate/024_create_config_properties.rb b/db/migrate/024_create_config_properties.rb
new file mode 100644
index 0000000..b0e8873
--- /dev/null
+++ b/db/migrate/024_create_config_properties.rb
@@ -0,0 +1,31 @@
+# 024_create_config_properties.rb
+# Copyright (C) 2008, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+class CreateConfigProperties < ActiveRecord::Migration
+ def self.up
+ create_table :config_properties do |t|
+ t.string :name, :null => false, :unique => true, :limit => 128
+ t.text :value, :null => true, :unique => false
+
+ t.timestamps
+ end
+ end
+
+ def self.down
+ drop_table :config_properties
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
deleted file mode 100644
index dc8868f..0000000
--- a/db/schema.rb
+++ /dev/null
@@ -1,163 +0,0 @@
-# This file is auto-generated from the current state of the database. Instead of editing this file,
-# please use the migrations feature of Active Record to incrementally modify your database, and
-# then regenerate this schema definition.
-#
-# Note that this schema.rb definition is the authoritative source for your database schema. If you need
-# to create the application database on another system, you should be using db:schema:load, not running
-# all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations
-# you'll amass, the slower it'll run and the greater likelihood for issues).
-#
-# It's strongly recommended to check this file into your version control system.
-
-ActiveRecord::Schema.define(:version => 22) do
-
- create_table "backlog_items", :force => true do |t|
- t.integer "sprint_id", :null => false
- t.integer "user_story_id", :null => false
- t.integer "owner_id"
- t.decimal "estimated_hours", :precision => 5, :scale => 2, :default => 0.0, :null => false
- t.text "notes"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.integer "state", :default => 0, :null => false
- end
-
- add_index "backlog_items", ["sprint_id", "user_story_id"], :name => "index_backlog_items_on_sprint_id_and_user_story_id", :unique => true
-
- create_table "notifications", :force => true do |t|
- t.integer "user_id"
- t.boolean "task_reminders"
- t.boolean "daily_updates"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.boolean "updates_to_product_list", :default => false
- end
-
- create_table "product_roles", :force => true do |t|
- t.integer "user_id", :null => false
- t.integer "product_id", :null => false
- t.integer "role_id", :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.boolean "pending", :default => true
- t.boolean "is_approved", :default => false
- end
-
- add_index "product_roles", ["product_id", "user_id"], :name => "index_product_roles_on_user_id_and_product_id", :unique => true
-
- create_table "products", :force => true do |t|
- t.integer "project_id"
- t.integer "owner_id"
- t.string "name", :limit => 128
- t.text "description"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "mailing_list", :limit => 128
- end
-
- add_index "products", ["name"], :name => "index_products_on_name", :unique => true
-
- create_table "projects", :force => true do |t|
- t.integer "owner_id", :null => false
- t.string "name", :limit => 50, :null => false
- t.string "url"
- t.text "description"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- add_index "projects", ["name"], :name => "index_projects_on_name", :unique => true
-
- create_table "remaining_hours_estimates", :force => true do |t|
- t.integer "backlog_item_id", :null => false
- t.integer "user_id", :null => false
- t.decimal "hours", :precision => 5, :scale => 2, :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.datetime "estimated_on", :null => false
- end
-
- create_table "roles", :force => true do |t|
- t.string "name", :null => false
- t.boolean "can_manage_backlog_items", :default => false, :null => false
- t.boolean "can_close_user_story", :default => false, :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- add_index "roles", ["name"], :name => "index_roles_on_name", :unique => true
-
- create_table "sessions", :force => true do |t|
- t.string "session_id", :null => false
- t.text "data"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- add_index "sessions", ["session_id"], :name => "index_sessions_on_session_id"
- add_index "sessions", ["updated_at"], :name => "index_sessions_on_updated_at"
-
- create_table "sprints", :force => true do |t|
- t.integer "product_id", :null => false
- t.string "title", :limit => 100, :null => false
- t.date "start", :null => false
- t.integer "duration", :null => false
- t.string "goals", :limit => 1000, :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.integer "status", :default => 0, :null => false
- end
-
- create_table "tasks", :force => true do |t|
- t.integer "backlog_item_id", :null => false
- t.integer "primary_id", :null => false
- t.integer "backup_id"
- t.text "description", :null => false
- t.decimal "hours", :precision => 4, :scale => 2, :default => 0.0, :null => false
- t.date "when_entered", :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "user_privileges", :force => true do |t|
- t.integer "user_id", :null => false
- t.boolean "admin_projects", :default => false, :null => false
- t.boolean "admin_users", :default => false, :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "user_stories", :force => true do |t|
- t.integer "product_id"
- t.integer "priority"
- t.boolean "closed", :default => false
- t.string "title", :limit => 100
- t.text "description"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "user_verifications", :force => true do |t|
- t.integer "user_id", :null => false
- t.string "token", :limit => 16, :null => false
- t.datetime "sent", :null => false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.integer "attempts", :default => 0, :null => false
- end
-
- add_index "user_verifications", ["user_id"], :name => "index_user_verifications_on_user_id", :unique => true
-
- create_table "users", :force => true do |t|
- t.string "email", :limit => 128, :null => false
- t.string "display_name", :limit => 128, :null => false
- t.string "hashed_password", :null => false
- t.string "salt", :null => false
- t.text "introduction"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- add_index "users", ["email"], :name => "index_users_on_email", :unique => true
-
-end
diff --git a/test/fixtures/config_properties.yml b/test/fixtures/config_properties.yml
new file mode 100644
index 0000000..05d87bf
--- /dev/null
+++ b/test/fixtures/config_properties.yml
@@ -0,0 +1,3 @@
+email_server_name:
+ name: email.server.name
+ value: smtp.google.com
diff --git a/test/unit/config_property_test.rb b/test/unit/config_property_test.rb
new file mode 100644
index 0000000..86f61d2
--- /dev/null
+++ b/test/unit/config_property_test.rb
@@ -0,0 +1,95 @@
+# config_property_test.rb
+# Copyright (C) 2008, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ConfigPropertyTest < ActiveSupport::TestCase
+ def setup
+ @newproperty = ConfigProperty.new(:name => 'name', :value => 'value')
+ @oldproperty = config_properties(:email_server_name)
+ end
+
+ # Ensures that the name is required.
+ def test_name_required
+ @newproperty.name = nil
+
+ fail "A name is required." if @newproperty.valid?
+ end
+
+ # Ensures that the name is required to be unique.
+ def test_name_unique
+ @newproperty.name = @oldproperty.name
+
+ fail "The name must be unique." if @newproperty.valid?
+ end
+
+ # Ensures that a value is not required.
+ def test_value_not_required
+ @newproperty.value = nil
+
+ fails "The value is not required." unless @newproperty.valid?
+ end
+
+ # Ensures that a well-formed property is valid.
+ def test_valid
+ fail "There is a problem with validation." unless @newproperty.valid?
+ end
+
+ # Ensures that a name must be provided when fetching a property.
+ def test_fetch_without_name
+ assert_raises(Exception) { ConfigProperty.fetch(nil) }
+ end
+
+ # Ensures that retrieving an existing value works.
+ def test_fetch_for_existing
+ result = ConfigProperty.fetch((a)oldproperty.name, nil)
+
+ assert result
+ assert_equal @oldproperty.value, result, "returned the wrong value"
+ end
+
+ # Ensures that the default value's returned when no such property is found.
+ def test_fetch_with_default_value
+ result = ConfigProperty.fetch((a)oldproperty.name.reverse, "default value")
+
+ assert result
+ assert_equal "default value", result, "Did not return the default value."
+ end
+
+ # Ensures that a name is required when saving a property.
+ def test_store_without_name
+ assert_raises(Exception) { ConfigProperty.store(nil) }
+ end
+
+ # Ensures that a new configuration property is saved property.
+ def test_store_new_property
+ assert_nil ConfigProperty.fetch((a)oldproperty.name.reverse), "Whoa! That shouldn't exist!"
+
+ ConfigProperty.store((a)oldproperty.name.reverse, "farkle")
+ result = ConfigProperty.fetch((a)oldproperty.name.reverse)
+
+ assert_equal "farkle", result, "Property was not property saved."
+ end
+
+ # Ensures that an existing property is overwritten.
+ def test_store
+ ConfigProperty.store((a)oldproperty.name, "farkle")
+
+ result = ConfigProperty.fetch((a)oldproperty.name)
+ assert_equal "farkle", result, "Property was not overwritten."
+ end
+end
--
1.6.0.2
15 years, 3 months
[PATCH] Mailer configuration is moved to the database. #124
by Darryl L. Pierce
This patch requires a migration. It introduces a new database, called
config_properties, that holds site configuration details. It also adds
a new column to the user_privileges table called site_admin. Users with
this privilege are able to access and change site configuration
properties.
A configuration property is composed of a name and a value. Names are
hierarchical is nature, with each segment separated by a period (.). So,
for example, the email server name is "email.server.name".
This patch moves the content of config/mailer.yml to the database.
To access the configuration, the user goes to /admin/email. The new controller,
EmailController, extends the equally new AdminController. All administration
controllers must extend this base class to inherit admin functionality.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/controllers/admin_controller.rb | 48 +++++++++
app/controllers/application.rb | 1 +
app/controllers/emails_controller.rb | 62 +++++++++++
app/helpers/admin_helper.rb | 2 +
app/helpers/emails_helper.rb | 19 ++++
app/models/config_property.rb | 31 ++++++
app/models/user_mailer.rb | 18 ++--
app/views/emails/edit.html.erb | 26 +++++
app/views/emails/show.html.erb | 33 ++++++
app/views/emails/show.html.erb~ | 2 +
app/views/sprints/plan.html.erb | 6 +-
config/initializers/mailer.rb | 35 ++++---
config/mailer.yml.example | 12 --
config/routes.rb | 3 +
.../023_add_admin_site_to_user_privileges.rb | 33 ++++++
db/migrate/024_create_config_properties.rb | 31 ++++++
db/schema.rb | 10 ++-
public/stylesheets/forms.css | 2 -
test/fixtures/config_properties.yml | 3 +
test/fixtures/user_privileges.yml | 1 +
test/functional/admin_controller_test.rb | 8 ++
test/functional/emails_controller_test.rb | 108 ++++++++++++++++++++
test/unit/config_property_test.rb | 46 ++++++++
23 files changed, 497 insertions(+), 43 deletions(-)
create mode 100644 app/controllers/admin_controller.rb
create mode 100644 app/controllers/emails_controller.rb
create mode 100644 app/helpers/admin_helper.rb
create mode 100644 app/helpers/emails_helper.rb
create mode 100644 app/models/config_property.rb
create mode 100644 app/views/emails/edit.html.erb
create mode 100644 app/views/emails/show.html.erb
create mode 100644 app/views/emails/show.html.erb~
delete mode 100644 config/mailer.yml.example
create mode 100644 db/migrate/023_add_admin_site_to_user_privileges.rb
create mode 100644 db/migrate/024_create_config_properties.rb
create mode 100644 test/fixtures/config_properties.yml
create mode 100644 test/functional/admin_controller_test.rb
create mode 100644 test/functional/emails_controller_test.rb
create mode 100644 test/unit/config_property_test.rb
diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb
new file mode 100644
index 0000000..2075f4f
--- /dev/null
+++ b/app/controllers/admin_controller.rb
@@ -0,0 +1,48 @@
+# admin_controller_test.rb
+# Copyright (C) 2008, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+class AdminController < ApplicationController
+ before_filter :authenticated
+ before_filter :authorized
+
+ private
+
+ def authorized
+ unless @user && @user.privileges.admin_site
+ flash[:message] = "You do not have administrative rights to this system."
+ respond_to do |format|
+ format.html { redirect_to error_path }
+ end
+ end
+ end
+
+ # Loads specific properties formthe database
+ def load_properties(names)
+ result = Hash.new
+
+ names.each do |name|
+ property = ConfigProperty.find_by_name(name)
+ if property
+ result["#{name}"] = property.value
+ else
+ result["#{name}"] = ''
+ end
+ end
+
+ return result
+ end
+end
diff --git a/app/controllers/application.rb b/app/controllers/application.rb
index d318bad..cc4aecc 100644
--- a/app/controllers/application.rb
+++ b/app/controllers/application.rb
@@ -59,6 +59,7 @@ class ApplicationController < ActionController::Base
def handle_exceptions
yield
rescue Exception => error
+ puts "CAUSE EXCEPTION: #{error.message}"
puts error.backtrace
erase_results
@title = "An Error Has Occurred."
diff --git a/app/controllers/emails_controller.rb b/app/controllers/emails_controller.rb
new file mode 100644
index 0000000..bfd2ef7
--- /dev/null
+++ b/app/controllers/emails_controller.rb
@@ -0,0 +1,62 @@
+# emails_controller.rb
+# Copyright (C) 2008, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# +EmailsController+ allows administrators to administer the email system for the
+# server.
+class EmailsController < AdminController
+ EMAIL_PROPERTIES = ['email.server.type',
+ 'email.server.smtp_tls',
+ 'email.server.perform_deliveries',
+ 'email.server.name',
+ 'email.server.port',
+ 'email.server.auth_mode',
+ 'email.server.username',
+ 'email.server.password',
+ 'email.from_address'
+ ]
+
+ # GET /admin/email
+ def show
+ @properties = load_properties(EMAIL_PROPERTIES)
+ end
+
+ # GET /admin/email/edit
+ def edit
+ @properties = load_properties(EMAIL_PROPERTIES)
+ end
+
+ # PUT /admin/email
+ def update
+ @properties = params[:properties]
+
+ ConfigProperty.transaction do
+ EMAIL_PROPERTIES.each do |property|
+ config_property = ConfigProperty.find_by_name(property) || ConfigProperty.new(:name => property)
+ config_property.value = (@properties[property] || '').strip
+
+ config_property.save!
+ end
+ end
+
+ # restart the email service
+ load_email_config
+
+ respond_to do |format|
+ format.html { redirect_to admin_email_path }
+ end
+ end
+end
diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb
new file mode 100644
index 0000000..d5c6d35
--- /dev/null
+++ b/app/helpers/admin_helper.rb
@@ -0,0 +1,2 @@
+module AdminHelper
+end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
new file mode 100644
index 0000000..694790c
--- /dev/null
+++ b/app/helpers/emails_helper.rb
@@ -0,0 +1,19 @@
+# emails_helper.rb
+# Copyright (C) 2008, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+module EmailsHelper
+end
diff --git a/app/models/config_property.rb b/app/models/config_property.rb
new file mode 100644
index 0000000..ad0265e
--- /dev/null
+++ b/app/models/config_property.rb
@@ -0,0 +1,31 @@
+# config_property.rb
+# Copyright (C) 2008, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# A +ConfigProperty+ represents a single configuration value.
+class ConfigProperty < ActiveRecord::Base
+ validates_presence_of :name,
+ :message => "A configuration property must have a name."
+ validates_uniqueness_of :name,
+ :message => "Configuration property names must be unique."
+
+ def self.value_by_name(name)
+ property = ConfigProperty.find_by_name(name)
+
+ return property.value if property
+ return ''
+ end
+end
diff --git a/app/models/user_mailer.rb b/app/models/user_mailer.rb
index 5609dc8..734b820 100644
--- a/app/models/user_mailer.rb
+++ b/app/models/user_mailer.rb
@@ -19,7 +19,7 @@ class UserMailer < ActionMailer::Base
# Sends an email verification to the specified user.
def email_verification(user, token)
recipients user.email
- from MAIL_CONFIG[:from]
+ from ConfigProperty.value_by_name('email.from_address')
subject "Email verification"
body :user => user, :token => token
end
@@ -27,7 +27,7 @@ class UserMailer < ActionMailer::Base
# Sends an email to the old address when the user changes his email address.
def email_change_notification(user, old_email)
recipients old_email
- from MAIL_CONFIG[:from]
+ from ConfigProperty.value_by_name('email.from_address')
subject "Email change notification"
body :user => user, :old_email => old_email
end
@@ -35,7 +35,7 @@ class UserMailer < ActionMailer::Base
# Send an e-mail to an user and notify him his new password.
def new_generated_password(user, new_password)
recipients user.email
- from MAIL_CONFIG[:from]
+ from ConfigProperty.value_by_name('email.from_address')
subject "Your password"
body :user => user, :new_password => new_password
end
@@ -43,7 +43,7 @@ class UserMailer < ActionMailer::Base
# Sends an email to a product owner when a user requests a product role.
def product_role_request(requested_role)
recipients requested_role.product.owner.email
- from MAIL_CONFIG[:from]
+ from ConfigProperty.value_by_name('email.from_address')
subject "Role request for #{requested_role.product.name}..."
body :requested_role => requested_role
end
@@ -52,7 +52,7 @@ class UserMailer < ActionMailer::Base
# product role request.
def product_role_disposition(role)
recipients role.user.email
- from MAIL_CONFIG[:from]
+ from ConfigProperty.value_by_name('email.from_address')
subject "Role request status..."
body :role => role
end
@@ -61,7 +61,7 @@ class UserMailer < ActionMailer::Base
def daily_updates(user, cc_list, open_items, completed_items, task_performed)
recipients user.email
cc cc_list
- from MAIL_CONFIG[:from]
+ from ConfigProperty.value_by_name('email.from_address')
subject "Daily updates for #{user.display_name}"
body :user => user, :open_items => open_items,
:completed_items => completed_items, :tasks_performed => task_performed
@@ -71,7 +71,7 @@ class UserMailer < ActionMailer::Base
# his backlog.
def no_activity_recorded(user, backlog_items)
recipients user.email
- from MAIL_CONFIG[:from]
+ from ConfigProperty.value_by_name('email.from_address')
subject "No activity has been detected"
body :user => user, :backlog_items => backlog_items
end
@@ -80,7 +80,7 @@ class UserMailer < ActionMailer::Base
# backlog.
def no_task_recorded(user, backlog_items)
recipients user.email
- from MAIL_CONFIG[:from]
+ from ConfigProperty.value_by_name('email.from_address')
subject "No task has been detected"
body :user => user, :backlog_items => backlog_items
end
@@ -89,7 +89,7 @@ class UserMailer < ActionMailer::Base
# products.
def sprints_products_status(user, products)
recipients user.email
- from MAIL_CONFIG[:from]
+ from ConfigProperty.value_by_name('email.from_address')
subject "Status of sprints"
body :user => user, :products => products
end
diff --git a/app/views/emails/edit.html.erb b/app/views/emails/edit.html.erb
new file mode 100644
index 0000000..ae27d80
--- /dev/null
+++ b/app/views/emails/edit.html.erb
@@ -0,0 +1,26 @@
+<% form_tag :admin_email, :method => :put do %>
+<table class="edit">
+ <colgroup>
+ <col class="label" />
+ <col class="value" />
+ </colgroup>
+
+ <tbody>
+ <% EmailsController::EMAIL_PROPERTIES.each do |property| %>
+ <tr>
+ <td class="label"><%= property %></td>
+ <td class="value">
+ <%= text_field_tag "properties[#{property}]",
+ @properties["#{property}"] %>
+ </td>
+ </tr>
+ <% end %>
+ <tr>
+ <td class="buttonbar" colspan="2">
+ <%= submit_tag "Update" %>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+<% end %>
diff --git a/app/views/emails/show.html.erb b/app/views/emails/show.html.erb
new file mode 100644
index 0000000..416a628
--- /dev/null
+++ b/app/views/emails/show.html.erb
@@ -0,0 +1,33 @@
+<table class="detail">
+ <colgroup>
+ <col class="label" />
+ <col class="value" />
+ </colgroup>
+
+ <thead>
+ <tr>
+ <th class="title" colspan="2">Email Configuration</th>
+ </tr>
+ <tr>
+ <th class="toolbar" colspan="2">
+ <%= link_to(image_tag("icons/edit.png", :title => 'Edit email settings...'),
+ edit_admin_email_path) %>
+ </th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <% EmailsController::EMAIL_PROPERTIES.each do |property| %>
+ <tr>
+ <td class="label"><%= "#{property}" %></td>
+ <td class="value">
+ <% unless (@properties[property]).empty? %>
+ <%= "#{@properties[property]}" %>
+ <% else %>
+ [UNDEFINED]
+ <% end %>
+ </td>
+ </tr>
+ <% end %>
+ </tbody>
+</table>
diff --git a/app/views/emails/show.html.erb~ b/app/views/emails/show.html.erb~
new file mode 100644
index 0000000..819ed4d
--- /dev/null
+++ b/app/views/emails/show.html.erb~
@@ -0,0 +1,2 @@
+<table class="detail">
+</table>
diff --git a/app/views/sprints/plan.html.erb b/app/views/sprints/plan.html.erb
index ace6139..34867b3 100644
--- a/app/views/sprints/plan.html.erb
+++ b/app/views/sprints/plan.html.erb
@@ -20,10 +20,8 @@
</thead>
<tbody>
- <% @user_stories.each_with_index do |story, index| %>
- <% row_class = index%2 == 0 ? 'even' : 'odd' %>
- <% backlog_item = @sprint.backlog_items.find_by_user_story_id(story.id) %>
- <tr class="<%= row_class %>">
+ <% @user_stories.each do |story| %>
+ <tr class="<%= cycle('odd', 'even') %>">
<td><%= story.id %></td>
<td><%= story.priority %></td>
<td>
diff --git a/config/initializers/mailer.rb b/config/initializers/mailer.rb
index c41382b..21c47c4 100644
--- a/config/initializers/mailer.rb
+++ b/config/initializers/mailer.rb
@@ -14,21 +14,26 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-MAIL_CONFIG = YAML.load(File.open("#{RAILS_ROOT}/config/mailer.yml"))
-
require 'tlsmail'
-Net::SMTP.enable_tls(OpenSSL::SSL::VERIFY_NONE)
-ActionMailer::Base.delivery_method = MAIL_CONFIG[:delivery_method]
-ActionMailer::Base.perform_deliveries = MAIL_CONFIG[:perform_deliveries]
-ActionMailer::Base.default_charset = MAIL_CONFIG[:default_charset]
-ActionMailer::Base.raise_delivery_errors = MAIL_CONFIG[:raise_delivery_errors]
+def load_email_config
+ puts "Reloading email configuration"
+ Net::SMTP.enable_tls(OpenSSL::SSL::VERIFY_NONE)
+ ActionMailer::Base.delivery_method = ConfigProperty.value_by_name('email.server.type')
+ ActionMailer::Base.perform_deliveries = ConfigProperty.value_by_name('email.server.perform_deliveries')
+ ActionMailer::Base.default_charset = 'utf-8'
+ ActionMailer::Base.raise_delivery_errors = true
+
+ ActionMailer::Base.smtp_settings = {
+ :address => ConfigProperty.value_by_name('email.server.name'),
+ :port => ConfigProperty.value_by_name('email.server.port'),
+ :tls => ConfigProperty.value_by_name('email.server.smtp_tls'),
+ :authentication => ConfigProperty.value_by_name('email.server.auth_mode'),
+ :user_name => ConfigProperty.value_by_name('email.server.username'),
+ :password => ConfigProperty.value_by_name('email.server.password')
+ }
+
+ puts ActionMailer::Base.smtp_settings
+end
-ActionMailer::Base.smtp_settings = {
- :address => MAIL_CONFIG[:server_name],
- :port => MAIL_CONFIG[:server_port],
- :tls => MAIL_CONFIG[:require_smtp_tls],
- :authentication => MAIL_CONFIG[:server_auth_mode],
- :user_name => MAIL_CONFIG[:server_username],
- :password => MAIL_CONFIG[:server_password]
-}
+load_email_config
diff --git a/config/mailer.yml.example b/config/mailer.yml.example
deleted file mode 100644
index 1dd4aa2..0000000
--- a/config/mailer.yml.example
+++ /dev/null
@@ -1,12 +0,0 @@
----
-:delivery_method: :smtp
-:require_smtp_tls: true
-:perform_deliveries: true
-:default_charset: utf-8
-:raise_delivery_errors: true
-:server_name: smtp.gmail.com
-:server_port: 587
-:server_auth_mode: :login
-:server_username: your_username
-:server_password: your_password
-:from: noreply(a)projxp.org
diff --git a/config/routes.rb b/config/routes.rb
index 6ae69bc..c5df8ce 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -16,6 +16,9 @@
#
ActionController::Routing::Routes.draw do |map|
+ map.resource :admin, :plural => :admin do |admin|
+ admin.resource :email, :plural => :email
+ end
map.resources :projects
map.resources :products do |product|
product.resources :roles
diff --git a/db/migrate/023_add_admin_site_to_user_privileges.rb b/db/migrate/023_add_admin_site_to_user_privileges.rb
new file mode 100644
index 0000000..565f414
--- /dev/null
+++ b/db/migrate/023_add_admin_site_to_user_privileges.rb
@@ -0,0 +1,33 @@
+# 023_add_admin_site_to_user_privileges.rb
+# Copyright (C) 2008, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+class AddAdminSiteToUserPrivileges < ActiveRecord::Migration
+ def self.up
+ add_column :user_privileges, :admin_site, :boolean, :default => false
+
+ # make the admin account a site admin
+ privileges = UserPrivilege.find_by_user_id(1)
+ if privileges
+ privileges.admin_site = true
+ privileges.save!
+ end
+ end
+
+ def self.down
+ remove_column :user_privileges, :admin_site
+ end
+end
diff --git a/db/migrate/024_create_config_properties.rb b/db/migrate/024_create_config_properties.rb
new file mode 100644
index 0000000..5bcb87b
--- /dev/null
+++ b/db/migrate/024_create_config_properties.rb
@@ -0,0 +1,31 @@
+# 024_create_config_properties.rb
+# Copyright (C) 2008, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+class CreateConfigProperties < ActiveRecord::Migration
+ def self.up
+ create_table :config_properties do |t|
+ t.string :name, :null => false, :limit => 128
+ t.text :value, :null => true
+
+ t.timestamps
+ end
+ end
+
+ def self.down
+ drop_table :config_properties
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index dc8868f..099266d 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -9,7 +9,7 @@
#
# It's strongly recommended to check this file into your version control system.
-ActiveRecord::Schema.define(:version => 22) do
+ActiveRecord::Schema.define(:version => 24) do
create_table "backlog_items", :force => true do |t|
t.integer "sprint_id", :null => false
@@ -24,6 +24,13 @@ ActiveRecord::Schema.define(:version => 22) do
add_index "backlog_items", ["sprint_id", "user_story_id"], :name => "index_backlog_items_on_sprint_id_and_user_story_id", :unique => true
+ create_table "config_properties", :force => true do |t|
+ t.string "name", :limit => 128, :null => false
+ t.text "value"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
create_table "notifications", :force => true do |t|
t.integer "user_id"
t.boolean "task_reminders"
@@ -125,6 +132,7 @@ ActiveRecord::Schema.define(:version => 22) do
t.boolean "admin_users", :default => false, :null => false
t.datetime "created_at"
t.datetime "updated_at"
+ t.boolean "admin_site", :default => false
end
create_table "user_stories", :force => true do |t|
diff --git a/public/stylesheets/forms.css b/public/stylesheets/forms.css
index 07dc248..51bcc2e 100644
--- a/public/stylesheets/forms.css
+++ b/public/stylesheets/forms.css
@@ -23,11 +23,9 @@ table.edit {
}
table.edit col.label {
- width: 15em;
}
table.edit col.value {
- width: 100%;
}
table.edit {
diff --git a/test/fixtures/config_properties.yml b/test/fixtures/config_properties.yml
new file mode 100644
index 0000000..b83d3d6
--- /dev/null
+++ b/test/fixtures/config_properties.yml
@@ -0,0 +1,3 @@
+mail_server_name:
+ name: email.server.name
+ value: smtp.gmail.com
diff --git a/test/fixtures/user_privileges.yml b/test/fixtures/user_privileges.yml
index 38ffbf7..ba57357 100644
--- a/test/fixtures/user_privileges.yml
+++ b/test/fixtures/user_privileges.yml
@@ -4,6 +4,7 @@ admin_privileges:
user_id: <%= Fixtures.identify(:admin) %>
admin_projects: true
admin_users: true
+ admin_site: true
projxp_owner_privileges:
user_id: <%= Fixtures.identify(:projxp_owner) %>
diff --git a/test/functional/admin_controller_test.rb b/test/functional/admin_controller_test.rb
new file mode 100644
index 0000000..d4c1dd4
--- /dev/null
+++ b/test/functional/admin_controller_test.rb
@@ -0,0 +1,8 @@
+require 'test_helper'
+
+class AdminControllerTest < ActionController::TestCase
+ # Replace this with your real tests.
+ def test_truth
+ assert true
+ end
+end
diff --git a/test/functional/emails_controller_test.rb b/test/functional/emails_controller_test.rb
new file mode 100644
index 0000000..86f4bca
--- /dev/null
+++ b/test/functional/emails_controller_test.rb
@@ -0,0 +1,108 @@
+# email_controller_test.rb
+# Copyright (C) 2008, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class EmailsControllerTest < ActionController::TestCase
+ fixtures :users
+
+ def setup
+ @admin = users(:admin)
+ raise "Admin must have site admin rights." unless @admin.privileges.admin_site
+ @nonadmin = users(:mcpierce)
+ raise "Nonadmin cannot have site admin rights." if @nonadmin.privileges.admin_site
+ end
+
+ # Ensures that anonymous users cannot view the email configuration.
+ def test_show_as_anonymous
+ get :show
+
+ assert_redirected_to login_path
+ end
+
+ # Ensures that users who are not site admins cannot view the configuration.
+ def test_show_as_nonadmin
+ get :show, {}, {:user_id => @nonadmin.id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that administrators can view the email configuration.
+ def test_show
+ get :show, {}, {:user_id => @admin.id}
+
+ assert_response :success
+ assert assigns['properties']
+ properties = assigns['properties']
+ EmailsController::EMAIL_PROPERTIES.each do |property|
+ assert properties["#{property}"], "Missing property: #{property}"
+ end
+ end
+
+ # Ensures that an anonymous user cannot access the edit email page.
+ def test_edit_as_anonymous
+ get :edit
+
+ assert_redirected_to login_url
+ end
+
+ # Ensures that a non-admin cannot access the edit page.
+ def test_edit_as_nonadmin
+ get :edit, {}, {:user_id => @nonadmin.id}
+
+ assert_redirected_to error_url
+ end
+
+ # Ensures that an admin can edit the email settings.
+ def test_edit
+ get :edit, {}, {:user_id => @admin.id}
+
+ assert_response :success
+ end
+
+ # Ensures that an anonymous user cannot update the mail settings.
+ def test_update_as_anonymous
+ put :update
+
+ assert_redirected_to :login_url
+ end
+
+ # Ensures that a nonadmin cannot update the mail settings.
+ def test_update_as_nonadmin
+ put :update, {}, {:user_id => @nonadmin.id}
+
+ assert_redirected_to error_url
+ end
+
+ # Ensures that updating works as expected.
+ def test_update
+ updates = Hash.new
+ EmailsController::EMAIL_PROPERTIES.each do |property|
+ updates[property] = property.reverse
+ end
+
+ put :update, {:properties => updates}, {:user_id => @admin.id}
+
+ assert_redirected_to admin_email_url
+ EmailsController::EMAIL_PROPERTIES.each do |property|
+ config_property = ConfigProperty.find_by_name(property)
+ assert config_property, "Property was not saved."
+ assert_equal updates[property], config_property.value,
+ "Value was not saved correctly."
+ end
+ end
+end
diff --git a/test/unit/config_property_test.rb b/test/unit/config_property_test.rb
new file mode 100644
index 0000000..98c305d
--- /dev/null
+++ b/test/unit/config_property_test.rb
@@ -0,0 +1,46 @@
+# config_property_test.rb
+# Copyright (C) 2008, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+require File.dirname(__FILE__) + '/../test_helper'
+
+class ConfigPropertyTest < ActiveSupport::TestCase
+ def setup
+ @config_property = ConfigProperty.new(:name => 'name', :value => 'value')
+ @existing_property = config_properties(:mail_server_name)
+ end
+
+ # Ensures that a config property requires a name.
+ def test_name_required
+ @config_property.name = nil
+
+ fail "The name must be required." if @config_property.valid?
+ end
+
+ # Ensures that a config property name must be unique.
+ def test_name_unique
+ @config_property.name = @existing_property.name
+
+ fail "The name must be unique." if @config_property.valid?
+ end
+
+ # Ensures that a value does not need to be present.
+ def test_value_not_required
+ @config_property.value = nil
+
+ fail "The value is not required." unless @config_property.valid?
+ end
+end
--
1.6.0.2
15 years, 3 months