diff --git a/controllers/__tests__/admin.test.js b/controllers/__tests__/admin.test.js new file mode 100644 index 000000000..dff09b72e --- /dev/null +++ b/controllers/__tests__/admin.test.js @@ -0,0 +1,146 @@ +/** + * Copyright: The PastVu contributors. + * GNU Affero General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/agpl.txt) + */ + +import { News } from '../../models/News'; +import { CommentN } from '../../models/Comment'; +import { BadParamsError, AuthorizationError, NotFoundError, NoticeError } from '../../app/errors'; +import constants from '../../app/errors/constants'; +import admin from '../admin'; +import comment from '../comment'; +import testHelpers from '../../tests/testHelpers'; + +describe('admin', () => { + beforeEach(async () => { + // Mock non-registerd user handshake. + admin.handshake = { 'usObj': { 'isAdmin': true } }; + }); + + afterEach(() => { + // Delete handshake. + delete admin.handshake; + }); + + describe('save or create news', () => { + it('should create and update news', async () => { + expect.assertions(2); + + const data = { pdate: new Date(), 'title': 'Test news', 'txt': 'Test news content' }; + let result = await admin.saveOrCreateNews(data); + + expect(result.news).toMatchObject(data); + + // Update same record. + data.title = 'Test news updated'; + data.txt = 'Test news content updated'; + data.cid = result.news.cid; + result = await admin.saveOrCreateNews(data); + + expect(result.news).toMatchObject(data); + }); + + it('should create news with commenting disabled', async () => { + expect.assertions(2); + + const data = { 'title': 'Test news', 'txt': 'Test news content', nocomments: true }; + const result = await admin.saveOrCreateNews(data); + + expect(result.news).toMatchObject(data); + expect(result.news.nocomments).toBeTruthy(); + }); + + it('throws on non-admin use', async () => { + expect.assertions(1); + + // Reset handshake. + admin.handshake = { 'usObj': { 'isAdmin': false } }; + + const data = { 'title': 'Test news', 'txt': 'Test news content' }; + + expect(() => admin.saveOrCreateNews(data)).toThrow(new AuthorizationError()); + }); + + it('throws on empty text', async () => { + expect.assertions(1); + + const data = { 'title': 'Test news' }; + + expect(() => admin.saveOrCreateNews(data)).toThrow(new BadParamsError()); + }); + + it('throws on non-existing news', async () => { + expect.assertions(1); + + const data = { cid: 1000, 'title': 'Test news', 'txt': 'Test news content' }; + + await expect(admin.saveOrCreateNews(data)).rejects.toThrow(new NotFoundError(constants.NO_SUCH_NEWS)); + }); + }); + + describe('delete news', () => { + let news; + + beforeEach(async () => { + const data = { pdate: new Date(), 'title': 'Test news', 'txt': 'Test news content' }; + + ({ news } = await admin.saveOrCreateNews(data)); + + const user = await testHelpers.createUser({ login: 'user1', pass: 'pass1' }); + + // Mock non-registered user handshake. + comment.handshake = { 'usObj': { 'isAdmin': true, 'registered': true, user } }; + }); + + afterEach(() => { + // Delete handshake. + delete comment.handshake; + }); + + it('delete news', async () => { + expect.assertions(2); + + // Delete news record. + const del = await admin.deleteNews(news); + + expect(del).toMatchObject({}); + await expect(News.findOne({ cid: news.cid })).resolves.toBeNull(); + }); + + it('throws on non-admin use', async () => { + expect.assertions(1); + + // Reset handshake. + admin.handshake = { 'usObj': { 'isAdmin': false } }; + + await expect(admin.deleteNews(news)).rejects.toThrow(new AuthorizationError()); + }); + + it('throws on missing cid', async () => { + expect.assertions(1); + + news.cid = undefined; + + await expect(admin.deleteNews(news)).rejects.toThrow(new BadParamsError()); + }); + + it('throws on non-existing news', async () => { + expect.assertions(1); + + const data = { cid: 1000 }; + + await expect(admin.deleteNews(data)).rejects.toThrow(new NotFoundError(constants.NO_SUCH_NEWS)); + }); + + it('throws on non-zero comments', async () => { + expect.assertions(2); + + const data = { txt: 'news comment', type: 'news', obj: news.cid }; + + await comment.create(data); + + await expect(CommentN.count({ obj: news })).resolves.toBe(1); + await expect(admin.deleteNews(news)).rejects.toThrow(new NoticeError(constants.NEWS_CONTAINS_COMMENTS)); + }); + }); +}); diff --git a/controllers/admin.js b/controllers/admin.js index 93019cb55..a53ac9492 100755 --- a/controllers/admin.js +++ b/controllers/admin.js @@ -54,6 +54,32 @@ async function saveNews(iAm, { cid, pdate, tdate, title, notice, txt, nocomments return { news: novel }; } +async function deleteNews(data) { + const { handshake: { usObj: iAm } } = this; + + if (!iAm.isAdmin) { + throw new AuthorizationError(); + } + + if (!data.cid) { + throw new BadParamsError(); + } + + const novel = await News.findOne({ cid: data.cid }).exec(); + + if (!novel) { + throw new NotFoundError(constantsError.NO_SUCH_NEWS); + } + + if (novel.ccount > 0) { + throw new NoticeError(constantsError.NEWS_CONTAINS_COMMENTS); + } + + await News.deleteOne({ cid: data.cid }).exec(); + + return {}; +} + function getOnlineStat() { const { handshake: { usObj: iAm } } = this; @@ -220,9 +246,11 @@ async function saveUserCredentials({ login, role, regions }) { getOnlineStat.isPublic = true; saveOrCreateNews.isPublic = true; saveUserCredentials.isPublic = true; +deleteNews.isPublic = true; export default { getOnlineStat, saveOrCreateNews, saveUserCredentials, + deleteNews, }; diff --git a/controllers/index.js b/controllers/index.js index fce63d275..05be3b356 100755 --- a/controllers/index.js +++ b/controllers/index.js @@ -283,8 +283,9 @@ const giveIndexNews = (function () { // News archive async function giveAllNews() { const { handshake: { usObj: iAm } } = this; + // Admin can see all news including scheduled ones. const news = await News.find( - { pdate: { $lte: new Date() } }, + iAm.isAdmin ? {} : { pdate: { $lte: new Date() } }, { cdate: 0, tdate: 0, nocomments: 0 }, { lean: true, sort: { pdate: -1 } } ).populate({ path: 'user', select: { _id: 0, login: 1, avatar: 1, disp: 1 } }).exec(); diff --git a/public/js/module/admin/newsEdit.js b/public/js/module/admin/newsEdit.js index 929393f91..753a50c59 100644 --- a/public/js/module/admin/newsEdit.js +++ b/public/js/module/admin/newsEdit.js @@ -111,14 +111,23 @@ define([ }, routeHandler: function () { const cid = Number(globalVM.router.params().cid); + const action = globalVM.router.params().action; - this.createMode(!cid); + if (action === 'delete') { + this.getOneNews(cid, function () { + this.deleteNews(); + }, this); + } + + this.createMode(action === 'create'); if (!this.createMode()) { + // Edit news. this.getOneNews(cid, function () { this.fillData(); }, this); } else { + // Create news. this.resetData(); } }, @@ -167,6 +176,31 @@ define([ this.news.tdate(''); } }, + deleteNews: function () { + if (this.news.ccount && this.news.ccount() > 0) { + noties.error({ + message: 'Новость содержит комментарии и не может быть удалена', + }); + } else { + const cid = this.news.cid(); + + noties.confirm({ + message: `Новость "${this.news.title()}" будет удалена`, + onOk: function (confirmer) { + confirmer.close(); + socket.run('admin.deleteNews', { cid }, true) + .then(function () { + noties.alert({ + message: 'Новость удалена', + type: 'success', + layout: 'topRight', + }); + globalVM.router.navigate('/admin/news/'); + }); + }, + }); + } + }, toggleNotice: function () { if (this.noticeExists()) { diff --git a/public/js/module/appAdmin.js b/public/js/module/appAdmin.js index 7d1ca1001..67d18ca16 100644 --- a/public/js/module/appAdmin.js +++ b/public/js/module/appAdmin.js @@ -70,8 +70,8 @@ require([ params = { section: section }; modules.push({ module: 'm/admin/main', container: '#bodyContainer' }); } else if (section === 'news') { - if (param1 === 'create' || param1 === 'edit') { - params = { section: section, cid: param2 }; + if (param1 === 'create' || param1 === 'edit' || param1 === 'delete') { + params = { section: section, cid: param2, action: param1 }; modules.push({ module: 'm/admin/newsEdit', container: '#bodyContainer' }); } else { params = { section: section, cid: param1 }; diff --git a/public/style/diff/newsList.less b/public/style/diff/newsList.less index f0cd26703..115ca0a60 100644 --- a/public/style/diff/newsList.less +++ b/public/style/diff/newsList.less @@ -130,6 +130,10 @@ } } } + + &.future { + background-color: darken(@body-bg, 10%); + } } } } diff --git a/views/module/diff/newsList.pug b/views/module/diff/newsList.pug index f55a12426..f6684355d 100644 --- a/views/module/diff/newsList.pug +++ b/views/module/diff/newsList.pug @@ -9,7 +9,7 @@ .news //ko foreach: news hr - .novel.clearfix + .novel.clearfix(data-bind="css: {future: new Date($data.pdate) > new Date()}") .newsLeft .newsAvatar.fringe(data-bind="attr: {href: '/u/' + $data.user.login}") img(data-bind="attr: {src: $data.user.avatar}") @@ -28,8 +28,14 @@ span.glyphicon.glyphicon-pencil | Редактировать // /ko + //ko if: $parent.canEdit() && !$data.ccount + .dotDelimeter · + a.newsEdit(data-bind="attr: {href: '/admin/news/delete/' + $data.cid}") + span.glyphicon.glyphicon-trash + | Delete + // /ko .newsNotice(data-bind="html: $data.notice, css: {expandable: $data.expand}") //ko if: $data.expand a.newsExpand(data-bind="attr: {href: '/news/' + $data.cid}") [Читать полностью..] // /ko - // /ko \ No newline at end of file + // /ko