Merge branch 'atomaka/feature/subcreddit' into 'master'
Add subcreddits Reddit has subreddits. We have subcreddits. Get it? Because we're subcreddit. See merge request !8
This commit is contained in:
commit
2cac3d9f1c
22 changed files with 450 additions and 22 deletions
1
Gemfile
1
Gemfile
|
@ -50,4 +50,5 @@ group :test do
|
||||||
gem 'capybara-webkit'
|
gem 'capybara-webkit'
|
||||||
gem 'shoulda'
|
gem 'shoulda'
|
||||||
gem 'simplecov', require: false
|
gem 'simplecov', require: false
|
||||||
|
gem 'timecop'
|
||||||
end
|
end
|
||||||
|
|
|
@ -338,6 +338,7 @@ GEM
|
||||||
thor (0.19.1)
|
thor (0.19.1)
|
||||||
thread_safe (0.3.5)
|
thread_safe (0.3.5)
|
||||||
tilt (1.4.1)
|
tilt (1.4.1)
|
||||||
|
timecop (0.7.4)
|
||||||
tzinfo (1.2.2)
|
tzinfo (1.2.2)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
uglifier (2.7.1)
|
uglifier (2.7.1)
|
||||||
|
@ -392,6 +393,7 @@ DEPENDENCIES
|
||||||
spring
|
spring
|
||||||
spring-commands-rspec
|
spring-commands-rspec
|
||||||
sqlite3
|
sqlite3
|
||||||
|
timecop
|
||||||
uglifier (>= 1.3.0)
|
uglifier (>= 1.3.0)
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
// Place all the styles related to the Tests controller here.
|
// Place all the styles related to the Subcreddits controller here.
|
||||||
// They will automatically be included in application.css.
|
// They will automatically be included in application.css.
|
||||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
// You can use Sass (SCSS) here: http://sass-lang.com/
|
50
app/controllers/subcreddits_controller.rb
Normal file
50
app/controllers/subcreddits_controller.rb
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
class SubcredditsController < ApplicationController
|
||||||
|
before_filter :set_subcreddit, only: [:show, :edit, :update]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@subcreddits = Subcreddit.all
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@subcreddit = Subcreddit.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@subcreddit = Subcreddit.new(create_subcreddit_params)
|
||||||
|
@subcreddit.owner = current_user
|
||||||
|
|
||||||
|
if @subcreddit.save
|
||||||
|
redirect_to @subcreddit, notice: 'Subcreddit was created!'
|
||||||
|
else
|
||||||
|
render :new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @subcreddit.update(update_subcreddit_params)
|
||||||
|
redirect_to @subcreddit, notice: 'Subcreddit was updated!'
|
||||||
|
else
|
||||||
|
render :edit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_subcreddit_params
|
||||||
|
params.require(:subcreddit).permit(:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_subcreddit_params
|
||||||
|
params.require(:subcreddit).permit(:closed)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_subcreddit
|
||||||
|
@subcreddit = Subcreddit.find_by_slug(params[:id])
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,7 +0,0 @@
|
||||||
class TestsController < ApplicationController
|
|
||||||
def index
|
|
||||||
end
|
|
||||||
|
|
||||||
def show
|
|
||||||
end
|
|
||||||
end
|
|
53
app/models/subcreddit.rb
Normal file
53
app/models/subcreddit.rb
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
RESERVED_SLUGS = %w(new edit)
|
||||||
|
|
||||||
|
class Subcreddit < ActiveRecord::Base
|
||||||
|
belongs_to :owner, class_name: 'User'
|
||||||
|
|
||||||
|
attr_accessor :closed
|
||||||
|
|
||||||
|
before_save :set_slug
|
||||||
|
before_save :set_closed_at
|
||||||
|
|
||||||
|
validates :name,
|
||||||
|
presence: true,
|
||||||
|
format: /\A(?! )[a-z0-9 ]*(?<! )\z/i,
|
||||||
|
uniqueness: { case_sensitive: false },
|
||||||
|
length: { minimum: 3, maximum: 21 }
|
||||||
|
|
||||||
|
validate :slug_is_not_a_route
|
||||||
|
|
||||||
|
validates :closed,
|
||||||
|
format: /\A[01]?\z/
|
||||||
|
|
||||||
|
def to_param
|
||||||
|
self.slug
|
||||||
|
end
|
||||||
|
|
||||||
|
def closed?
|
||||||
|
self.closed_at != nil
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_slug
|
||||||
|
self.slug = sluggify_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def sluggify_name
|
||||||
|
self.name.downcase.tr(' ', '_')
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_closed_at
|
||||||
|
if closed == '1' && closed_at == nil
|
||||||
|
self.closed_at = Time.now
|
||||||
|
elsif closed == '0' && closed_at != nil
|
||||||
|
self.closed_at = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def slug_is_not_a_route
|
||||||
|
if RESERVED_SLUGS.include?(sluggify_name)
|
||||||
|
errors.add(:name, 'cannot be a reserved keyword')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,10 +8,11 @@
|
||||||
span.icon-bar
|
span.icon-bar
|
||||||
= link_to 'Creddit', root_path, class: 'navbar-brand'
|
= link_to 'Creddit', root_path, class: 'navbar-brand'
|
||||||
.collapse.navbar-collapse
|
.collapse.navbar-collapse
|
||||||
ul.nav.navbar-nav.navbar-right
|
ul.nav.navbar-nav
|
||||||
- if logged_in?
|
li= link_to 'Subcreddits', subcreddits_path
|
||||||
li= link_to 'Sign Out', signout_path
|
ul.nav.navbar-nav.navbar-right
|
||||||
- else
|
- if logged_in?
|
||||||
li= link_to 'Create Account', signup_path
|
li= link_to 'Sign Out', signout_path
|
||||||
li= link_to 'Sign In', signin_path
|
- else
|
||||||
|
li= link_to 'Create Account', signup_path
|
||||||
|
li= link_to 'Sign In', signin_path
|
||||||
|
|
8
app/views/subcreddits/_form.html.slim
Normal file
8
app/views/subcreddits/_form.html.slim
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
= simple_form_for subcreddit do |f|
|
||||||
|
.form-inputs
|
||||||
|
- if subcreddit.new_record?
|
||||||
|
= f.input :name
|
||||||
|
- unless subcreddit.new_record?
|
||||||
|
= f.input :closed, as: :boolean, input_html: { checked: subcreddit.closed? }
|
||||||
|
.form-actions
|
||||||
|
= f.button :submit
|
1
app/views/subcreddits/edit.html.slim
Normal file
1
app/views/subcreddits/edit.html.slim
Normal file
|
@ -0,0 +1 @@
|
||||||
|
== render 'form', subcreddit: @subcreddit
|
7
app/views/subcreddits/index.html.slim
Normal file
7
app/views/subcreddits/index.html.slim
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
= link_to 'Create', new_subcreddit_path
|
||||||
|
|
||||||
|
ul
|
||||||
|
- @subcreddits.each do |subcreddit|
|
||||||
|
li
|
||||||
|
= link_to subcreddit.name, subcreddit
|
||||||
|
== " (#{link_to 'Edit', edit_subcreddit_path(subcreddit)})"
|
1
app/views/subcreddits/new.html.slim
Normal file
1
app/views/subcreddits/new.html.slim
Normal file
|
@ -0,0 +1 @@
|
||||||
|
== render 'form', subcreddit: @subcreddit
|
3
app/views/subcreddits/show.html.slim
Normal file
3
app/views/subcreddits/show.html.slim
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
h1= @subcreddit.name
|
||||||
|
- if @subcreddit.closed?
|
||||||
|
= "Board has been closed"
|
0
app/views/subcreddits/update.html.slim
Normal file
0
app/views/subcreddits/update.html.slim
Normal file
|
@ -1 +0,0 @@
|
||||||
= 'tests#index'
|
|
|
@ -1 +0,0 @@
|
||||||
= 'tests#show'
|
|
|
@ -1,12 +1,11 @@
|
||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
resources :tests, only: [:index, :show]
|
|
||||||
|
|
||||||
get 'signup', to: 'users#new', as: :signup
|
get 'signup', to: 'users#new', as: :signup
|
||||||
get 'signin', to: 'user_sessions#new', as: :signin
|
get 'signin', to: 'user_sessions#new', as: :signin
|
||||||
get 'signout', to: 'user_sessions#destroy', as: :signout
|
get 'signout', to: 'user_sessions#destroy', as: :signout
|
||||||
|
|
||||||
resources :users, only: [:new, :create]
|
resources :subcreddits, path: 'c', except: [:destroy]
|
||||||
resources :user_sessions, only: [:new, :create, :destroy]
|
resources :user_sessions, only: [:new, :create, :destroy]
|
||||||
|
resources :users, only: [:new, :create]
|
||||||
|
|
||||||
root to: 'tests#index'
|
root to: 'subcreddits#index'
|
||||||
end
|
end
|
||||||
|
|
12
db/migrate/20150713045745_create_subcreddits.rb
Normal file
12
db/migrate/20150713045745_create_subcreddits.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
class CreateSubcreddits < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :subcreddits do |t|
|
||||||
|
t.references :owner, index: true, foreign_key: true
|
||||||
|
t.string :name
|
||||||
|
t.string :slug
|
||||||
|
t.datetime :closed_at
|
||||||
|
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
13
db/schema.rb
13
db/schema.rb
|
@ -11,7 +11,18 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20150710040354) do
|
ActiveRecord::Schema.define(version: 20150713045745) do
|
||||||
|
|
||||||
|
create_table "subcreddits", force: :cascade do |t|
|
||||||
|
t.integer "owner_id"
|
||||||
|
t.string "name"
|
||||||
|
t.string "slug"
|
||||||
|
t.datetime "closed_at"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index "subcreddits", ["owner_id"], name: "index_subcreddits_on_owner_id"
|
||||||
|
|
||||||
create_table "user_sessions", force: :cascade do |t|
|
create_table "user_sessions", force: :cascade do |t|
|
||||||
t.integer "user_id"
|
t.integer "user_id"
|
||||||
|
|
156
spec/controllers/subcreddits_controller_spec.rb
Normal file
156
spec/controllers/subcreddits_controller_spec.rb
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe SubcredditsController, type: :controller do
|
||||||
|
let!(:user) { build(:user) }
|
||||||
|
let(:data) do
|
||||||
|
{
|
||||||
|
name: 'Testing Subcreddit 1'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
before(:each) do
|
||||||
|
allow_any_instance_of(ApplicationController)
|
||||||
|
.to receive(:current_user).and_return(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#index' do
|
||||||
|
let!(:subcreddits) { 3.times.collect { create(:subcreddit) } }
|
||||||
|
|
||||||
|
it 'should render :index' do
|
||||||
|
get :index
|
||||||
|
|
||||||
|
expect(response).to render_template(:index)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should assign all Subcreddits to @subcreddits' do
|
||||||
|
get :index
|
||||||
|
|
||||||
|
expect(assigns(:subcreddits)).to eq(subcreddits)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#show' do
|
||||||
|
let(:subcreddit) { create(:subcreddit) }
|
||||||
|
before(:each) { get :show, id: subcreddit }
|
||||||
|
|
||||||
|
it 'should render :show' do
|
||||||
|
expect(response).to render_template(:show)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should assign the Subcreddit to @subcreddit' do
|
||||||
|
expect(assigns(:subcreddit)).to eq(subcreddit)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#new' do
|
||||||
|
it 'should render :new' do
|
||||||
|
get :new
|
||||||
|
|
||||||
|
expect(response).to render_template(:new)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should assign new Subcreddit to @subcreddit' do
|
||||||
|
get :new
|
||||||
|
|
||||||
|
expect(assigns(:subcreddit)).to be_a_new(Subcreddit)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#create' do
|
||||||
|
context 'with valid data' do
|
||||||
|
it 'should create a subcreddit' do
|
||||||
|
expect { post :create, subcreddit: data }
|
||||||
|
.to change(Subcreddit, :count).by(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should redirect to new subcreddit page index' do
|
||||||
|
expect(post :create, subcreddit: data)
|
||||||
|
.to redirect_to(assigns(:subcreddit))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should send a notice flash message' do
|
||||||
|
post :create, subcreddit: data
|
||||||
|
|
||||||
|
expect(flash[:notice]).to be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid data' do
|
||||||
|
before(:each) { data['name'] = 'Bad name ' }
|
||||||
|
|
||||||
|
it 'should not create a subcreddit' do
|
||||||
|
expect { post :create, subcreddit: data }
|
||||||
|
.to change(Subcreddit, :count).by(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should render :new' do
|
||||||
|
post :create, subcreddit: data
|
||||||
|
|
||||||
|
expect(response).to render_template(:new)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#edit' do
|
||||||
|
context 'with valid subcreddit' do
|
||||||
|
let(:subcreddit) { create(:subcreddit) }
|
||||||
|
|
||||||
|
it 'should assign @subcreddit to the existing subcreddit' do
|
||||||
|
get :edit, id: subcreddit
|
||||||
|
|
||||||
|
expect(assigns(:subcreddit)).to eq(subcreddit)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should render :edit' do
|
||||||
|
get :edit, id: subcreddit
|
||||||
|
|
||||||
|
expect(response).to render_template(:edit)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#update' do
|
||||||
|
let(:subcreddit) { create(:subcreddit) }
|
||||||
|
let(:data) do
|
||||||
|
{
|
||||||
|
closed: '1'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'wth valid data' do
|
||||||
|
it 'should assign @subcreddit to the existing subcreddit' do
|
||||||
|
put :update, id: subcreddit, subcreddit: data
|
||||||
|
|
||||||
|
expect(assigns(:subcreddit)).to eq(subcreddit)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should update the subcreddit' do
|
||||||
|
put :update, id: subcreddit, subcreddit: data
|
||||||
|
subcreddit.reload
|
||||||
|
|
||||||
|
expect(subcreddit.closed_at).to_not eq(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should redirect to the subcreddit' do
|
||||||
|
put :update, id: subcreddit, subcreddit: data
|
||||||
|
|
||||||
|
expect(response).to redirect_to(subcreddit_url(subcreddit))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should display a notice flash message' do
|
||||||
|
put :update, id: subcreddit, subcreddit: data
|
||||||
|
|
||||||
|
expect(flash[:notice]).to be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid data' do
|
||||||
|
before(:each) { data[:closed] = 'bad' }
|
||||||
|
|
||||||
|
it 'should render :edit' do
|
||||||
|
put :update, id: subcreddit, subcreddit: data
|
||||||
|
|
||||||
|
expect(response).to render_template(:edit)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
6
spec/factories/subcreddit_factory.rb
Normal file
6
spec/factories/subcreddit_factory.rb
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
FactoryGirl.define do
|
||||||
|
factory :subcreddit do
|
||||||
|
owner { create(:user) }
|
||||||
|
name { Faker::Team.name.first(21) } # 21 is the max length...brittle
|
||||||
|
end
|
||||||
|
end
|
126
spec/models/subcreddit_spec.rb
Normal file
126
spec/models/subcreddit_spec.rb
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Subcreddit, type: :model do
|
||||||
|
let(:subcreddit) { build(:subcreddit) }
|
||||||
|
|
||||||
|
it { should belong_to(:owner).class_name('User') }
|
||||||
|
|
||||||
|
context 'with valid data' do
|
||||||
|
it 'should be valid' do
|
||||||
|
expect(subcreddit).to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should allow spaces in the name' do
|
||||||
|
subcreddit.name = 'Testing Spaces'
|
||||||
|
expect(subcreddit).to be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should sluggify the name' do
|
||||||
|
subcreddit.name = 'Testing Spaces 1'
|
||||||
|
subcreddit.save
|
||||||
|
expect(subcreddit.slug).to eq('testing_spaces_1')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should set closed_at if closed is true and closed_at is nil' do
|
||||||
|
Timecop.freeze do
|
||||||
|
subcreddit.closed = '1'
|
||||||
|
subcreddit.save
|
||||||
|
expect(subcreddit.closed_at).to eq(Time.now)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not change closed_at if closed and closed_at is not nil' do
|
||||||
|
subcreddit.closed_at = Time.now - 3.days
|
||||||
|
|
||||||
|
Timecop.freeze do
|
||||||
|
subcreddit.closed = '1'
|
||||||
|
subcreddit.save
|
||||||
|
expect(subcreddit.closed_at).to_not eq(Time.now)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should clear closed_at if closed is "0"' do
|
||||||
|
subcreddit.closed_at = Time.now
|
||||||
|
|
||||||
|
subcreddit.closed = '0'
|
||||||
|
subcreddit.save
|
||||||
|
expect(subcreddit.closed_at).to eq(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid data' do
|
||||||
|
it 'should not allow a blank name' do
|
||||||
|
subcreddit.name = ''
|
||||||
|
|
||||||
|
expect(subcreddit).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not allow short names' do
|
||||||
|
subcreddit.name = 'a' * 2
|
||||||
|
|
||||||
|
expect(subcreddit).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not allow long names' do
|
||||||
|
subcreddit.name = 'a' * 22
|
||||||
|
|
||||||
|
expect(subcreddit).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should only allow acceptable characters in the name' do
|
||||||
|
subcreddit.name = 'Testing!'
|
||||||
|
|
||||||
|
expect(subcreddit).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not be allowed to end with a space' do
|
||||||
|
subcreddit.name = 'Testing '
|
||||||
|
|
||||||
|
expect(subcreddit).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not be allowed to begin with a space' do
|
||||||
|
subcreddit.name = ' Testing'
|
||||||
|
|
||||||
|
expect(subcreddit).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not allow duplicate names' do
|
||||||
|
original = create(:subcreddit)
|
||||||
|
duplicate = build(:subcreddit, name: original.name)
|
||||||
|
|
||||||
|
expect(duplicate).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not allow duplicate names (case insensitive)' do
|
||||||
|
original = create(:subcreddit)
|
||||||
|
duplicate = build(:subcreddit, name: original.name.upcase)
|
||||||
|
|
||||||
|
expect(duplicate).to be_invalid
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not use a reserved keyword' do
|
||||||
|
subcreddit.name = 'New'
|
||||||
|
|
||||||
|
expect(subcreddit).to be_invalid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context '#closed?' do
|
||||||
|
let(:subcreddit) { build(:subcreddit) }
|
||||||
|
|
||||||
|
context 'when a subcreddit is closed' do
|
||||||
|
before(:each) { subcreddit.closed_at = Time.now }
|
||||||
|
|
||||||
|
it 'should return true' do
|
||||||
|
expect(subcreddit.closed?).to be(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when a subcreddit is open' do
|
||||||
|
it 'should return false' do
|
||||||
|
expect(subcreddit.closed?).to be(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue