Albirew/nyaa-pantsu
Archivé
1
0
Bifurcation 0

Merge pull request #1327 from NyaaPantsu/pr/1242

Announcement [done]
Cette révision appartient à :
ewhal 2017-08-03 21:23:05 +10:00 révisé par GitHub
révision 445374242f
23 fichiers modifiés avec 434 ajouts et 15 suppressions

Voir le fichier

@ -0,0 +1,135 @@
package moderatorController
import (
"fmt"
"html"
"math"
"net/http"
"strconv"
"time"
"github.com/NyaaPantsu/nyaa/models"
"github.com/NyaaPantsu/nyaa/models/notifications"
"github.com/NyaaPantsu/nyaa/templates"
"github.com/NyaaPantsu/nyaa/utils/log"
msg "github.com/NyaaPantsu/nyaa/utils/messages"
"github.com/NyaaPantsu/nyaa/utils/validator"
"github.com/NyaaPantsu/nyaa/utils/validator/announcement"
"github.com/gin-gonic/gin"
)
func listAnnouncements(c *gin.Context) {
page := c.Param("page")
pagenum := 1
offset := 100
var err error
messages := msg.GetMessages(c)
deleted := c.Request.URL.Query()["deleted"]
if deleted != nil {
messages.AddInfoTf("infos", "annoucement_deleted")
}
if page != "" {
pagenum, err = strconv.Atoi(html.EscapeString(page))
if !log.CheckError(err) {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
var conditions string
var values []interface{}
announcements, nbAnnouncements := notifications.FindAll(offset, (pagenum-1)*offset, conditions, values...)
nav := templates.Navigation{nbAnnouncements, offset, pagenum, "mod/announcements/p"}
templates.ModelList(c, "admin/announcements.jet.html", announcements, nav, templates.NewSearchForm(c))
}
func addAnnouncement(c *gin.Context) {
announcement := &models.Notification{}
messages := msg.GetMessages(c)
id := c.Query("id")
if id == "" && len(messages.GetInfos("ID_ANNOUNCEMENT")) > 0 {
id = messages.GetInfos("ID_ANNOUNCEMENT")[0]
}
idInt, _ := strconv.Atoi(id)
if idInt > 0 {
var err error
announcement, _ = notifications.FindByID(uint(idInt))
if err != nil {
c.AbortWithStatus(http.StatusNotFound)
}
}
delay := int(math.Ceil(math.Max(1, float64(announcement.Expire.Sub(time.Now())/(24*time.Hour)))))
form := &announcementValidator.CreateForm{
ID: announcement.ID,
Message: announcement.Content,
Delay: delay,
}
c.Bind(form)
if form.Delay == 0 {
form.Delay = delay
}
templates.Form(c, "admin/announcement_form.jet.html", form)
}
func postAnnouncement(c *gin.Context) {
messages := msg.GetMessages(c)
announcement := &models.Notification{}
id, _ := strconv.Atoi(c.Query("id"))
if id > 0 {
var err error
announcement, err = notifications.FindByID(uint(id))
if err != nil {
c.AbortWithStatus(http.StatusNotFound)
}
}
form := &announcementValidator.CreateForm{}
// We bind the request to the form
c.Bind(form)
// We try to validate the form
validator.ValidateForm(form, messages)
// If validation has failed, errors are added in messages variable
if !messages.HasErrors() {
// No errors, check if we update or create
if id > 0 { // announcement exists we update
err := notifications.UpdateAnnouncement(announcement, form) // Making the update query
if err != nil {
// Error, we add it to the messages variable
messages.AddErrorT("errors", "update_failed")
} else {
// Success, we add a notice to the messages variable
messages.AddInfoT("infos", "update_success")
}
} else { // announcement doesn't exist, we create it
var err error
announcement, err := notifications.NotifyAll(form.Message, time.Now().AddDate(0, 0, form.Delay))
if err != nil {
// Error, we add it as a message
messages.AddErrorT("errors", "create_failed")
} else {
// Success, we redirect to the edit form
messages.AddInfoT("infos", "create_anouncement_success")
id := fmt.Sprintf("%d", announcement.ID)
messages.AddInfo("ID_ANNOUNCEMENT", id)
}
}
}
// If we are still here, we show the form
addAnnouncement(c)
}
// deleteAnnouncement : Controller for deleting an announcement
func deleteAnnouncement(c *gin.Context) {
id, _ := strconv.ParseInt(c.Query("id"), 10, 32)
announcement, err := notifications.FindByID(uint(id))
if err != nil {
c.AbortWithStatus(http.StatusNotFound)
return
}
err = announcement.Delete()
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Redirect(http.StatusSeeOther, "/mod/announcement?deleted")
}

Voir le fichier

@ -38,6 +38,10 @@ func init() {
modRoutes.Any("/comments/p/:page", CommentsListPanel)
modRoutes.Any("/comment", CommentsListPanel) // TODO Edit comment view
/* Announcement listing routes */
modRoutes.Any("/announcement", listAnnouncements)
modRoutes.Any("/announcement/p/:page", listAnnouncements)
/* Torrent edit view */
modRoutes.GET("/torrent", TorrentEditModPanel)
modRoutes.POST("/torrent", TorrentPostEditModPanel)
@ -45,6 +49,13 @@ func init() {
/* Torrent delete routes */
modRoutes.Any("/torrent/delete", TorrentDeleteModPanel)
/* Announcement edit view */
modRoutes.GET("/announcement/form", addAnnouncement)
modRoutes.POST("/announcement/form", postAnnouncement)
/* Announcement delete routes */
modRoutes.Any("/announcement/delete", deleteAnnouncement)
/* Torrent lock/unlock route */
modRoutes.Any("/torrent/block", TorrentBlockModPanel)

Voir le fichier

@ -1,6 +1,9 @@
package models
import (
"errors"
"time"
"github.com/NyaaPantsu/nyaa/config"
)
@ -11,6 +14,7 @@ type Notification struct {
Read bool
Identifier string
URL string
Expire time.Time
UserID uint
// User *User `gorm:"AssociationForeignKey:UserID;ForeignKey:user_id"` // Don't think that we need it here
}
@ -24,3 +28,12 @@ func NewNotification(identifier string, c string, url string) Notification {
func (n *Notification) TableName() string {
return config.Get().Models.NotificationsTableName
}
// Delete a notification
func (n *Notification) Delete() error {
if n.ID == 0 {
return errors.New("Can't delete a non existent notification")
}
ORM.Where("id = ?", n.ID).Delete(n)
return nil
}

75
models/notifications/system.go Fichier normal
Voir le fichier

@ -0,0 +1,75 @@
package notifications
import (
"errors"
"time"
"github.com/NyaaPantsu/nyaa/models"
"github.com/NyaaPantsu/nyaa/utils/cache"
"github.com/NyaaPantsu/nyaa/utils/validator/announcement"
)
const identifierAnnouncement = "system.announcement"
// NotifyAll notify all users through an announcement
func NotifyAll(msg string, expire time.Time) (*models.Notification, error) {
announcement := &models.Notification{
Content: msg,
Expire: expire,
Identifier: identifierAnnouncement,
Read: false,
UserID: 0,
}
err := models.ORM.Create(announcement).Error
return announcement, err
}
// UpdateAnnouncement updates an announcement
func UpdateAnnouncement(announcement *models.Notification, form *announcementValidator.CreateForm) error {
announcement.Content = form.Message
if form.Delay > 0 {
announcement.Expire = time.Now().AddDate(0, 0, form.Delay)
}
if models.ORM.Model(announcement).UpdateColumn(announcement).Error != nil {
return errors.New("Announcement was not updated")
}
return nil
}
// CheckAnnouncement check if there are any new announcements
func CheckAnnouncement() ([]models.Notification, error) {
if retrieved, ok := cache.C.Get(identifierAnnouncement); ok {
return retrieved.([]models.Notification), nil
}
var announcements []models.Notification
err := models.ORM.Where("identifier = ? AND expire >= ?", identifierAnnouncement, time.Now().Format("2006-01-02")).Find(&announcements).Error
if err == nil {
cache.C.Set(identifierAnnouncement, announcements, time.Minute*5)
}
return announcements, err
}
// FindAll return all the announcements
func FindAll(limit int, offset int, conditions string, values ...interface{}) ([]models.Notification, int) {
var announcements []models.Notification
var nbAnnouncement int
if conditions == "" {
conditions += "identifier = ?"
} else {
conditions += "AND identifier = ?"
}
values = append(values, identifierAnnouncement)
models.ORM.Model(&announcements).Where(conditions, values...).Count(&nbAnnouncement)
models.ORM.Limit(limit).Offset(offset).Where(conditions, values...).Find(&announcements)
return announcements, nbAnnouncement
}
// FindByID return the notification by its ID
func FindByID(id uint) (*models.Notification, error) {
d := &models.Notification{}
err := models.ORM.Where("id = ?", id).Find(d).Error
if err != nil {
return d, err
}
return d, nil
}

Voir le fichier

@ -163,6 +163,12 @@ td.tr-le, .error-text {
background-size: 100%;
}
#announce {
color: #3a4249;
background-color: #fff5e7;
border-color: #ecd2ae;
}
.form-input.language {
background-color: #f5f5f5;
}

Voir le fichier

@ -402,6 +402,23 @@ select.form-input {
padding: 10px 5px;
}
#announce {
margin-bottom: 15px;
padding: 7px 10px;
background-color: #D9EDF7;
border: 1px solid #c4e1e8;
border-radius: 7px;
}
#announce:before {
content: "!";
vertical-align: middle;
float: left;
margin: -2px 8px 0 0;
font-size: 2.2em;
font-weight: bold;
}
.results {
padding: 0!important;
}

Voir le fichier

@ -186,6 +186,13 @@ td.tr-le, .error-text {
text-decoration: none;
}
#announce {
color: #3a4249;
background-color: #F4E0C9;
border-color: #ae875b;
}
.torrent-info-box {
border-color: #515456;
}

Voir le fichier

@ -0,0 +1,21 @@
{{ extends "layouts/index_admin" }}
{{ import "layouts/partials/helpers/errors" }}
{{block title()}}{{ if Form.ID > 0 }}{{ T("update_annoucement_panel") }}{{ else }}{{ T("create_annoucement_panel") }}{{ end }}{{end}}
{{ block content_body()}}
<div class="results box">
<h3 id="clients">{{ if Form.ID > 0 }}{{ T("update_annoucement_panel") }}{{ else }}{{ T("create_annoucement_panel") }}{{ end }}</h3>
<form style="text-align:left;padding-left:10px;padding-right:10px;" method="POST" action="{{ if Form.ID > 0}}?id={{ Form.ID }}{{end}}">
<div class="form-group">
<label for="message">{{ T("message")}}</label>
<textarea name="message" id="message" class="form-input up-input" placeholder="{{ T("message")}}" required>{{Form.Message}}</textarea>
{{ yield errors(name="Message")}}
</div>
<div class="form-group">
<label for="delay">{{ T("delay")}} ({{ Form.Delay }} {{ T("days") }})</label>
<input type="text" name="delay" id="delay" class="form-input up-input" placeholder="{{ T("delay")}}">
{{ yield errors(name="Delay")}}
</div>
<button type="submit" class="form-input up-input btn-green">{{ T("save_changes")}}</button>
</form>
</div>
{{end}}

Voir le fichier

@ -0,0 +1,34 @@
{{ extends "layouts/index_admin" }}
{{block title()}}{{ T("announcements") }}{{end}}
{{ block content_body()}}
<div class="results box">
<h1>{{ T("announcements") }}</h1>
<table class="table">
<thead class="torrent-info">
<tr>
<th class="tr-name">{{ T("message") }}</th>
<th class="tr-links">{{ T("expire") }}</th>
<th class="tr-actions">{{ T("actions") }}</th>
</tr>
</thead>
<tbody>
{{ range Models}}
<tr>
<td class="tr-name home-td">
<a href="/mod/announcement/form?id={{ .ID }}">{{ .Content }}</a>
</td>
<td class="tr-name home-td">
<a href="/mod/announcement/form?id={{ .ID }}">{{ .Expire }}</a>
</td>
<td class="tr-actions home-td">
<a href="/mod/announcement/delete?id={{ .ID }}" class="form-input btn-red" onclick="if (!confirm('{{ T(" are_you_sure ") }}')) return false;">
<i class="icon-trash"></i> {{ T("delete") }}
</a>
</td>
</tr>
{{end}}
</tbody>
</table>
<a href="/mod/announcement/form" class="form-input btn-green">{{ T("add") }}</a>
</div>
{{end}}

Voir le fichier

@ -1,8 +1,8 @@
{{ extends "layouts/index_admin" }}
{{block title()}}{{ T("clients_list") }}{{end}}
{{block title()}}{{ T("oauth_clients_list") }}{{end}}
{{ block content_body()}}
<div class="results box">
<h1>{{ T("clients_list") }}</h1>
<h1>{{ T("oauth_clients_list") }}</h1>
<table class="table">
<thead class="torrent-info">
<tr>

Voir le fichier

@ -10,6 +10,11 @@
{{block titleBase()}} - {{block title()}}{{end}}{{end}}
{{ block content_body_base()}}
{{ if isset(Infos["system"])}}
{{ range Infos["system"]}}
<div id="announce"><span style="font-weight:bold;">{{T("announcement")}}</span><br>{{ . }}</div>
{{ end }}
{{ end }}
{{ yield search_refine(url=URL.Parse("/search")) }}
{{ block content_body()}}{{end}}
{{end}}

Voir le fichier

@ -23,6 +23,14 @@
<div class="visible-md icon-chat"></div>
<span class="hide-md">{{ T("comments")}}</span>
</a>
<a href="{{URL.Parse("/mod/oauth_client")}}" class="nav-btn">
<div class="visible-md icon-chat"></div>
<span class="hide-md">{{ T("oauth_clients_list")}}</span>
</a>
<a href="{{URL.Parse("/mod/announcement")}}" class="nav-btn">
<div class="visible-md icon-chat"></div>
<span class="hide-md">{{ T("announcements")}}</span>
</a>
<a href="{{URL.Parse("/mod/reports")}}" class="nav-btn">
<div class="visible-md icon-attention"></div>
<span class="hide-md">{{ T("torrent_reports")}}</span>

Voir le fichier

@ -9,6 +9,8 @@ import (
"path"
"testing"
"github.com/NyaaPantsu/nyaa/utils/validator/announcement"
"strings"
"time"
@ -65,6 +67,8 @@ func walkDirTest(dir string, t *testing.T) {
fakeOauthForm := apiValidator.CreateForm{"", "f", []string{fu}, []string{}, []string{}, "", "fedr", fu, fu, fu, fu, []string{em}, ""}
fakeOauthModel := fakeOauthForm.Bind(&models.OauthClient{})
fakeClient := client.Client{"", "", "", []string{""}, []string{""}, []string{""}, "", "", "", "", "", "", []string{""}, false}
fakeAnnouncement := announcementValidator.CreateForm{1, "", 2}
fakeNotification := &models.Notification{1, "test", true, "test", "test", time.Now(), 1}
contextvariables := ContextTest{
"dumps.jet.html": func(variables jet.VarMap) jet.VarMap {
@ -192,6 +196,14 @@ func walkDirTest(dir string, t *testing.T) {
variables.Set("Form", fakeOauthForm)
return variables
},
"announcement_form.jet.html": func(variables jet.VarMap) jet.VarMap {
variables.Set("Form", fakeAnnouncement)
return variables
},
"announcements.jet.html": func(variables jet.VarMap) jet.VarMap {
variables.Set("Models", []models.Notification{*fakeNotification, *fakeNotification})
return variables
},
"tag.jet.html": func(variables jet.VarMap) jet.VarMap {
variables.Set("Form", fakeTag)
return variables

Voir le fichier

@ -1908,7 +1908,7 @@
"translation": "Emails des Besitzers"
},
{
"id": "clients_list",
"id": "oauth_clients_list",
"translation": "OAuth API Clients"
},
{

Voir le fichier

@ -1851,6 +1851,10 @@
"id": "torrent_preview",
"translation": "Preview your torrent"
},
{
"id": "announcement",
"translation": "Announcement"
},
{
"id": "update_client_failed",
"translation": "Update of the client has failed!"
@ -1912,7 +1916,7 @@
"translation": "Owner Emails"
},
{
"id": "clients_list",
"id": "oauth_clients_list",
"translation": "OAuth API Clients"
},
{
@ -1953,10 +1957,34 @@
},
{
"id": "tagtype_quality",
"translation": "Quality tag"
"translation": "Video Quality"
},
{
"id": "torrent_tags",
"translation": "Torrent tags"
},
{
"id": "announcements",
"translation": "Announcements"
},
{
"id": "message",
"translation": "Message"
},
{
"id": "delay",
"translation": "Delay"
},
{
"id": "update_annoucement_panel",
"translation": "Update Announcement"
},
{
"id": "create_annoucement_panel",
"translation": "Create Announcement"
},
{
"id": "expire",
"translation": "Expire"
}
]

Voir le fichier

@ -1912,7 +1912,7 @@
"translation": "Adresses email du propriétaire"
},
{
"id": "clients_list",
"id": "oauth_clients_list",
"translation": "Clients OAuth de l'API"
},
{

Voir le fichier

@ -1912,7 +1912,7 @@
"translation": "オーナーのメールアドレス"
},
{
"id": "clients_list",
"id": "oauth_clients_list",
"translation": "OAuth API クライアント"
},
{

Voir le fichier

@ -14,7 +14,6 @@ import (
"github.com/NyaaPantsu/nyaa/utils/timeHelper"
"github.com/NyaaPantsu/nyaa/utils/validator/user"
"github.com/gin-gonic/gin"
"github.com/gorilla/context"
"github.com/gorilla/securecookie"
)
@ -151,14 +150,14 @@ func CurrentUser(c *gin.Context) (*models.User, int, error) {
return user, http.StatusOK, nil
}
func getUserFromContext(c *gin.Context) *models.User {
if rv := context.Get(c.Request, UserContextKey); rv != nil {
if rv, ok := c.Get(UserContextKey); ok {
return rv.(*models.User)
}
return &models.User{}
}
func setUserToContext(c *gin.Context, val *models.User) {
context.Set(c.Request, UserContextKey, val)
c.Set(UserContextKey, val)
}
// RetrieveUserFromRequest retrieves a user.

Voir le fichier

@ -4,9 +4,9 @@ import (
"errors"
"fmt"
"github.com/NyaaPantsu/nyaa/models/notifications"
"github.com/NyaaPantsu/nyaa/utils/publicSettings"
"github.com/gin-gonic/gin"
"github.com/gorilla/context"
"github.com/nicksnyder/go-i18n/i18n"
)
@ -23,16 +23,21 @@ type Messages struct {
// GetMessages : Initialize or return the messages object from context
func GetMessages(c *gin.Context) *Messages {
if rv := context.Get(c.Request, MessagesKey); rv != nil {
if rv, ok := c.Get(MessagesKey); ok {
mes := rv.(*Messages)
T, _ := publicSettings.GetTfuncAndLanguageFromRequest(c)
mes.T = T
mes.c = c
return mes
}
context.Set(c.Request, MessagesKey, &Messages{})
T, _ := publicSettings.GetTfuncAndLanguageFromRequest(c)
return &Messages{make(map[string][]string), make(map[string][]string), c, T}
mes := &Messages{make(map[string][]string), make(map[string][]string), c, T}
announcements, _ := notifications.CheckAnnouncement()
for _, announcement := range announcements {
mes.AddInfo("system", announcement.Content)
}
c.Set(MessagesKey, mes)
return mes
}
// AddError : Add an error in category name with message msg
@ -175,5 +180,5 @@ func (mes *Messages) HasInfos() bool {
}
func (mes *Messages) setMessagesInContext() {
context.Set(mes.c.Request, MessagesKey, mes)
mes.c.Set(MessagesKey, mes)
}

Voir le fichier

@ -0,0 +1,8 @@
package announcementValidator
// CreateForm is a struct to validate the anouncement before adding to db
type CreateForm struct {
ID uint `validate:"-"`
Message string `validate:"required,min=5" form:"message"`
Delay int `validate:"omitempty,min=1" form:"delay"`
}

Voir le fichier

@ -2,13 +2,28 @@ package apiValidator
import (
"net/http"
"path"
"testing"
"github.com/NyaaPantsu/nyaa/config"
"github.com/NyaaPantsu/nyaa/models"
msg "github.com/NyaaPantsu/nyaa/utils/messages"
"github.com/NyaaPantsu/nyaa/utils/validator"
"github.com/gin-gonic/gin"
)
// run before config/parse.go:init()
var _ = func() (_ struct{}) {
config.Configpaths[1] = path.Join("..", "..", "..", config.Configpaths[1])
config.Configpaths[0] = path.Join("..", "..", "..", config.Configpaths[0])
config.Reload()
config.Get().DBType = models.SqliteType
config.Get().DBParams = ":memory:?cache=shared&mode=memory"
models.ORM, _ = models.GormInit(models.DefaultLogger)
return
}()
func TestForms(t *testing.T) {
fu := "http://nyaa.cat"
em := "cop@cat.fe"

Voir le fichier

@ -2,13 +2,28 @@ package userValidator
import (
"net/http"
"path"
"testing"
"github.com/NyaaPantsu/nyaa/config"
"github.com/NyaaPantsu/nyaa/models"
msg "github.com/NyaaPantsu/nyaa/utils/messages"
"github.com/NyaaPantsu/nyaa/utils/validator"
"github.com/gin-gonic/gin"
)
// run before config/parse.go:init()
var _ = func() (_ struct{}) {
config.Configpaths[1] = path.Join("..", "..", "..", config.Configpaths[1])
config.Configpaths[0] = path.Join("..", "..", "..", config.Configpaths[0])
config.Reload()
config.Get().DBType = models.SqliteType
config.Get().DBParams = ":memory:?cache=shared&mode=memory"
models.ORM, _ = models.GormInit(models.DefaultLogger)
return
}()
func TestForms(t *testing.T) {
t.Parallel()
req, err := http.NewRequest("GET", "/", nil)

Voir le fichier

@ -6,6 +6,7 @@ import (
"testing"
"github.com/NyaaPantsu/nyaa/config"
"github.com/NyaaPantsu/nyaa/models"
msg "github.com/NyaaPantsu/nyaa/utils/messages"
"github.com/gin-gonic/gin"
)
@ -15,6 +16,10 @@ var _ = func() (_ struct{}) {
config.Configpaths[1] = path.Join("..", "..", config.Configpaths[1])
config.Configpaths[0] = path.Join("..", "..", config.Configpaths[0])
config.Reload()
config.Get().DBType = models.SqliteType
config.Get().DBParams = ":memory:?cache=shared&mode=memory"
models.ORM, _ = models.GormInit(models.DefaultLogger)
return
}()