Немного попрапвил про линтеру
parent
f766c1dd6e
commit
36a389f628
32
Makefile
32
Makefile
|
@ -1,5 +1,37 @@
|
|||
cdir = $(shell pwd)
|
||||
|
||||
check-lint:
|
||||
which golangci-lint || (GO111MODULE=off go get -u github.com/golangci/golangci-lint/cmd/golangci-lint)
|
||||
|
||||
lint: check-lint
|
||||
@echo "+ $@"
|
||||
@golangci-lint run -v --timeout 3m \
|
||||
--fast \
|
||||
--issues-exit-code=0 \
|
||||
--print-issued-lines=false \
|
||||
--enable=gocognit \
|
||||
--enable=gocritic \
|
||||
--enable=prealloc \
|
||||
--enable=unparam \
|
||||
--enable=nakedret \
|
||||
--enable=scopelint \
|
||||
--disable=deadcode \
|
||||
--disable=unused \
|
||||
--enable=gocyclo \
|
||||
--enable=golint \
|
||||
--enable=varcheck \
|
||||
--enable=structcheck \
|
||||
--enable=maligned \
|
||||
--enable=errcheck \
|
||||
--enable=dupl \
|
||||
--enable=ineffassign \
|
||||
--enable=interfacer \
|
||||
--enable=unconvert \
|
||||
--enable=goconst \
|
||||
--enable=gosec \
|
||||
--enable=megacheck \
|
||||
./...
|
||||
|
||||
app-up:
|
||||
docker-compose -f ./cicd/dc_app.yml up -d --build
|
||||
|
||||
|
|
22
cmd/main.go
22
cmd/main.go
|
@ -2,20 +2,22 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/codegangsta/martini"
|
||||
"github.com/codegangsta/martini-contrib/binding"
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
"github.com/codegangsta/martini-contrib/sessions"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/handlers"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/pkg/dataset"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/codegangsta/martini"
|
||||
"github.com/codegangsta/martini-contrib/binding"
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
"github.com/codegangsta/martini-contrib/sessions"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/handlers"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/pkg/dataset"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -40,7 +42,7 @@ func main() {
|
|||
Extensions: []string{".tmpl"},
|
||||
}))
|
||||
|
||||
auth.RedirectUrl = "/login"
|
||||
auth.RedirectURL = "/login"
|
||||
auth.RedirectParam = "next"
|
||||
|
||||
m.Get("/404", func(r render.Render) {
|
||||
|
|
|
@ -3,9 +3,11 @@ package application
|
|||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/models"
|
||||
"github.com/tiburon-777/modules/core/config"
|
||||
"log"
|
||||
|
||||
"github.com/tiburon-777/modules/core/config"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/models"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
|
@ -18,7 +20,7 @@ type App struct {
|
|||
func New(configFile, envPrefix string) (app App, err error) {
|
||||
app.Config, err = configure(configFile, envPrefix)
|
||||
if err != nil {
|
||||
return App{}, fmt.Errorf("can't apply config: %w\n", err)
|
||||
return App{}, fmt.Errorf("can't apply config: %w", err)
|
||||
}
|
||||
|
||||
app.DBMaster, err = sql.Open("mysql", app.Config.DSN.User+":"+app.Config.DSN.Pass+"@tcp("+app.Config.DSN.Master+":"+app.Config.DSN.Port+")/"+app.Config.DSN.Base+"?charset=utf8&collation=utf8_unicode_ci")
|
||||
|
|
|
@ -2,19 +2,21 @@ package auth
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/codegangsta/martini"
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
"github.com/codegangsta/martini-contrib/sessions"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// These are the default configuration values for this package. They
|
||||
// can be set at anytime, probably during the initial setup of Martini.
|
||||
var (
|
||||
// RedirectUrl should be the relative URL for your login route
|
||||
RedirectUrl string = "/login"
|
||||
// RedirectURL should be the relative URL for your login route
|
||||
RedirectURL string = "/login"
|
||||
|
||||
// RedirectParam is the query string parameter that will be set
|
||||
// with the page the user was trying to visit before they were
|
||||
|
@ -39,10 +41,10 @@ type User interface {
|
|||
Logout()
|
||||
|
||||
// Return the unique identifier of this user object
|
||||
UniqueId() interface{}
|
||||
UniqueID() interface{}
|
||||
|
||||
// Populate this user object with values
|
||||
GetById(app application.App, id interface{}) error
|
||||
GetByID(app application.App, id interface{}) error
|
||||
}
|
||||
|
||||
// SessionUser will try to read a unique user ID out of the session. Then it tries
|
||||
|
@ -53,11 +55,11 @@ type User interface {
|
|||
// user type.
|
||||
func SessionUser(newUser func() User) martini.Handler {
|
||||
return func(s sessions.Session, c martini.Context, l *log.Logger, app application.App) {
|
||||
userId := s.Get(SessionKey)
|
||||
userID := s.Get(SessionKey)
|
||||
user := newUser()
|
||||
|
||||
if userId != nil {
|
||||
err := user.GetById(app, userId)
|
||||
if userID != nil {
|
||||
err := user.GetByID(app, userID)
|
||||
if err != nil {
|
||||
l.Printf("Login Error: %v\n", err)
|
||||
} else {
|
||||
|
@ -88,8 +90,8 @@ func Logout(s sessions.Session, user User) {
|
|||
// authenticated, they will be redirected to /login with the "next" get parameter
|
||||
// set to the attempted URL.
|
||||
func LoginRequired(r render.Render, user User, req *http.Request) {
|
||||
if user.IsAuthenticated() == false {
|
||||
path := fmt.Sprintf("%s?%s=%s", RedirectUrl, RedirectParam, req.URL.Path)
|
||||
if !user.IsAuthenticated() {
|
||||
path := fmt.Sprintf("%s?%s=%s", RedirectURL, RedirectParam, req.URL.Path)
|
||||
r.Redirect(path, 302)
|
||||
}
|
||||
}
|
||||
|
@ -97,6 +99,6 @@ func LoginRequired(r render.Render, user User, req *http.Request) {
|
|||
// UpdateUser updates the User object stored in the session. This is useful incase a change
|
||||
// is made to the user model that needs to persist across requests.
|
||||
func UpdateUser(s sessions.Session, user User) error {
|
||||
s.Set(SessionKey, user.UniqueId())
|
||||
s.Set(SessionKey, user.UniqueID())
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,12 +2,13 @@ package auth
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"time"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
)
|
||||
|
||||
type UserModel struct {
|
||||
Id int64 `db:"id" form:"id"`
|
||||
ID int64 `db:"id" form:"id"`
|
||||
Username string `db:"username" form:"username"`
|
||||
Password string `db:"password" form:"password"`
|
||||
Name string `db:"name" form:"name"`
|
||||
|
@ -45,11 +46,11 @@ func (u *UserModel) IsAuthenticated() bool {
|
|||
return u.authenticated
|
||||
}
|
||||
|
||||
func (u *UserModel) UniqueId() interface{} {
|
||||
return u.Id
|
||||
func (u *UserModel) UniqueID() interface{} {
|
||||
return u.ID
|
||||
}
|
||||
|
||||
func (u *UserModel) GetById(app application.App, id interface{}) error {
|
||||
func (u *UserModel) GetByID(app application.App, id interface{}) error {
|
||||
var v string
|
||||
query := fmt.Sprintf("SELECT username, name, surname, birthdate, gender, city, interests FROM users WHERE id=%d", id)
|
||||
err := app.DBMaster.QueryRow(query).Scan(&u.Username, &u.Name, &u.Surname, &v, &u.Gender, &u.City, &u.Interests)
|
||||
|
@ -60,6 +61,6 @@ func (u *UserModel) GetById(app application.App, id interface{}) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Id = id.(int64)
|
||||
u.ID = id.(int64)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetFeed(app application.App, r render.Render, user auth.User) {
|
||||
|
@ -16,7 +18,7 @@ func GetFeed(app application.App, r render.Render, user auth.User) {
|
|||
var post Post
|
||||
var posts []Post
|
||||
var results, err = app.DBMaster.Query(`SELECT
|
||||
posts.Id AS Id,
|
||||
posts.ID AS Id,
|
||||
users.Username AS Author,
|
||||
posts.Created AS Created,
|
||||
posts.Subject AS Subject,
|
||||
|
@ -28,13 +30,13 @@ func GetFeed(app application.App, r render.Render, user auth.User) {
|
|||
AND posts.Author=relations.friendId
|
||||
AND relations.userId=?
|
||||
ORDER by Created DESC`,
|
||||
user.(*auth.UserModel).Id)
|
||||
user.(*auth.UserModel).ID)
|
||||
if err != nil || results == nil {
|
||||
err500("can't get feed from DB: ", err, r)
|
||||
}
|
||||
defer results.Close()
|
||||
for results.Next() {
|
||||
err = results.Scan(&post.Id, &post.Author, &tmpTime, &post.Subject, &post.Body)
|
||||
err = results.Scan(&post.ID, &post.Author, &tmpTime, &post.Subject, &post.Body)
|
||||
if err != nil {
|
||||
err500("can't scan result from DB: ", err, r)
|
||||
}
|
||||
|
|
|
@ -2,19 +2,22 @@ package handlers
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
"github.com/codegangsta/martini-contrib/sessions"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
"github.com/codegangsta/martini-contrib/sessions"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
// MySQL driver
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||
)
|
||||
|
||||
type Post struct{
|
||||
Id int `db:"Id"`
|
||||
ID int `db:"Id"`
|
||||
Author string `db:"Author"`
|
||||
Created time.Time `db:"Created"`
|
||||
Subject string `db:"Subject"`
|
||||
|
@ -42,13 +45,13 @@ func GetHome(app application.App, r render.Render, user auth.User) {
|
|||
relations.friendId=users.Id
|
||||
AND relations.userId=?
|
||||
GROUP BY users.Id`,
|
||||
user.(*auth.UserModel).Id)
|
||||
user.(*auth.UserModel).ID)
|
||||
if err != nil || results == nil {
|
||||
err500("can't get user list from DB: ", err, r)
|
||||
}
|
||||
defer results.Close()
|
||||
for results.Next() {
|
||||
err = results.Scan(&tmp.Id, &tmp.Name, &tmp.Surname, &tmpTime, &tmp.Gender, &tmp.City)
|
||||
err = results.Scan(&tmp.ID, &tmp.Name, &tmp.Surname, &tmpTime, &tmp.Gender, &tmp.City)
|
||||
if err != nil {
|
||||
err500("can't scan result from DB: ", err, r)
|
||||
}
|
||||
|
@ -60,13 +63,13 @@ func GetHome(app application.App, r render.Render, user auth.User) {
|
|||
|
||||
var post Post
|
||||
var posts []Post
|
||||
results, err = app.DBMaster.Query(`SELECT Id, Created, Subject, Body FROM posts WHERE Author=? ORDER BY Created DESC;`, user.(*auth.UserModel).Id)
|
||||
results, err = app.DBMaster.Query(`SELECT Id, Created, Subject, Body FROM posts WHERE Author=? ORDER BY Created DESC;`, user.(*auth.UserModel).ID)
|
||||
if err != nil || results == nil {
|
||||
err500("can't get user list from DB: ", err, r)
|
||||
}
|
||||
defer results.Close()
|
||||
for results.Next() {
|
||||
err = results.Scan(&post.Id, &tmpTime, &post.Subject, &post.Body)
|
||||
err = results.Scan(&post.ID, &tmpTime, &post.Subject, &post.Body)
|
||||
if err != nil {
|
||||
err500("can't scan result from DB: ", err, r)
|
||||
}
|
||||
|
@ -122,25 +125,24 @@ func PostSignup(app application.App, postedUser auth.UserModel, r render.Render)
|
|||
|
||||
func PostLogin(app application.App, session sessions.Session, postedUser auth.UserModel, r render.Render, req *http.Request) {
|
||||
user := auth.UserModel{}
|
||||
err1 := app.DBMaster.QueryRow("SELECT id, password FROM users WHERE username=?", postedUser.Username).Scan(&user.Id, &user.Password)
|
||||
err1 := app.DBMaster.QueryRow("SELECT id, password FROM users WHERE username=?", postedUser.Username).Scan(&user.ID, &user.Password)
|
||||
err2 := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(postedUser.Password))
|
||||
if err1 != nil || err2 != nil {
|
||||
doc := map[string]interface{}{
|
||||
"msg": "Wrong user or password. You may sign in.",
|
||||
}
|
||||
r.HTML(200, "login", doc)
|
||||
r.Redirect(auth.RedirectUrl)
|
||||
return
|
||||
} else {
|
||||
err := auth.AuthenticateSession(session, &user)
|
||||
if err != nil {
|
||||
err500("can't auth session: ", err, r)
|
||||
}
|
||||
params := req.URL.Query()
|
||||
redirect := params.Get(auth.RedirectParam)
|
||||
r.Redirect(redirect)
|
||||
r.Redirect(auth.RedirectURL)
|
||||
return
|
||||
}
|
||||
err := auth.AuthenticateSession(session, &user)
|
||||
if err != nil {
|
||||
err500("can't auth session: ", err, r)
|
||||
}
|
||||
params := req.URL.Query()
|
||||
redirect := params.Get(auth.RedirectParam)
|
||||
r.Redirect(redirect)
|
||||
return
|
||||
}
|
||||
|
||||
func str2Time(s string, r render.Render) time.Time {
|
||||
|
@ -152,7 +154,7 @@ func str2Time(s string, r render.Render) time.Time {
|
|||
}
|
||||
|
||||
func err500(s string, err error, r render.Render) {
|
||||
e := fmt.Errorf("s% %w", s, err)
|
||||
e := fmt.Errorf("%s %w", s, err)
|
||||
log.Println(e)
|
||||
doc := map[string]interface{}{
|
||||
"Error": e,
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||
)
|
||||
|
||||
func GetAddPost(app application.App, r render.Render) {
|
||||
|
@ -17,7 +19,7 @@ func PostAddPost(app application.App, user auth.User, r render.Render, req *http
|
|||
postBody := req.FormValue("body")
|
||||
var results, err = app.DBMaster.Query(`INSERT INTO posts (Author, Created, Subject, Body) VALUES (
|
||||
?, ?, ?, ?)`,
|
||||
user.(*auth.UserModel).Id,
|
||||
user.(*auth.UserModel).ID,
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
postSubj,
|
||||
postBody,
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||
)
|
||||
|
||||
func GetUserList(app application.App, r render.Render) {
|
||||
|
@ -46,8 +48,8 @@ func PostUserList(app application.App, user auth.User, r render.Render, req *htt
|
|||
WHERE
|
||||
relations.userId=?)
|
||||
AND ( users.Name LIKE concat(?, '%') AND users.Surname LIKE concat(?, '%') )`,
|
||||
user.(*auth.UserModel).Id,
|
||||
user.(*auth.UserModel).Id,
|
||||
user.(*auth.UserModel).ID,
|
||||
user.(*auth.UserModel).ID,
|
||||
postName,
|
||||
postSurname,
|
||||
)
|
||||
|
@ -56,7 +58,7 @@ func PostUserList(app application.App, user auth.User, r render.Render, req *htt
|
|||
}
|
||||
defer results.Close()
|
||||
for results.Next() {
|
||||
err = results.Scan(&tmp.Id, &tmp.Name, &tmp.Surname, &tmpTime, &tmp.Gender, &tmp.City)
|
||||
err = results.Scan(&tmp.ID, &tmp.Name, &tmp.Surname, &tmpTime, &tmp.Gender, &tmp.City)
|
||||
if err != nil {
|
||||
err500("can't scan result from DB: ", err, r)
|
||||
}
|
||||
|
@ -109,7 +111,7 @@ func PostUserSearch(app application.App, r render.Render, req *http.Request) {
|
|||
}
|
||||
defer results.Close()
|
||||
for results.Next() {
|
||||
err = results.Scan(&tmp.Id, &tmp.Name, &tmp.Surname, &tmpTime, &tmp.Gender, &tmp.City)
|
||||
err = results.Scan(&tmp.ID, &tmp.Name, &tmp.Surname, &tmpTime, &tmp.Gender, &tmp.City)
|
||||
if err != nil {
|
||||
err500("can't scan result from DB: ", err, r)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/codegangsta/martini-contrib/render"
|
||||
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/application"
|
||||
"github.com/tiburon-777/OTUS_HighLoad/internal/auth"
|
||||
)
|
||||
|
||||
func GetSubscribe(app application.App, r render.Render, user auth.User, req *http.Request) {
|
||||
|
@ -17,11 +19,11 @@ func GetSubscribe(app application.App, r render.Render, user auth.User, req *htt
|
|||
if err != nil {
|
||||
err500("can't convert URL query value: ", err, r)
|
||||
}
|
||||
_, err = app.DBMaster.Exec(`REPLACE INTO relations (userId, friendId) values (?, ?)`, user.(*auth.UserModel).Id, did)
|
||||
_, err = app.DBMaster.Exec(`REPLACE INTO relations (userId, friendId) values (?, ?)`, user.(*auth.UserModel).ID, did)
|
||||
if err != nil {
|
||||
err500("can't create relation in DB: ", err, r)
|
||||
}
|
||||
_, err = app.DBMaster.Exec(`REPLACE INTO relations (userId, friendId) values (?, ?)`, did, user.(*auth.UserModel).Id)
|
||||
_, err = app.DBMaster.Exec(`REPLACE INTO relations (userId, friendId) values (?, ?)`, did, user.(*auth.UserModel).ID)
|
||||
if err != nil {
|
||||
err500("can't create relation in DB: ", err, r)
|
||||
}
|
||||
|
@ -37,7 +39,7 @@ func GetUnSubscribe(app application.App, r render.Render, user auth.User, req *h
|
|||
if err != nil {
|
||||
err500("can't convert URL query value: ", err, r)
|
||||
}
|
||||
_, err = app.DBMaster.Exec(`DELETE FROM relations WHERE (userId,friendId) IN ((?, ?),(?, ?))`, user.(*auth.UserModel).Id, did, did, user.(*auth.UserModel).Id)
|
||||
_, err = app.DBMaster.Exec(`DELETE FROM relations WHERE (userId,friendId) IN ((?, ?),(?, ?))`, user.(*auth.UserModel).ID, did, did, user.(*auth.UserModel).ID)
|
||||
if err != nil {
|
||||
err500("can't remove relation from DB: ", err, r)
|
||||
}
|
||||
|
|
|
@ -2,12 +2,13 @@ package dataset
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/mdigger/translit"
|
||||
"log"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mdigger/translit"
|
||||
)
|
||||
|
||||
type Person struct {
|
||||
|
@ -53,7 +54,7 @@ func FillDB(db *sql.DB, lim int) {
|
|||
}
|
||||
uCount = lim - uCount
|
||||
if uCount <= 0 {
|
||||
log.Printf("Ok. We have more users then %s.", lim)
|
||||
log.Printf("Ok. We have more users then %d.", lim)
|
||||
}
|
||||
log.Printf("Try to generate %d rows and fill the DB...", uCount)
|
||||
for i := 1; i < uCount; i++ {
|
||||
|
|
Loading…
Reference in New Issue