Commit b1919289 authored by echel0n's avatar echel0n

Added database table Announcements to track if an announcement has been seen or not.

parent 7e294c51
......@@ -20,8 +20,11 @@
# ##############################################################################
import threading
from sqlalchemy import orm
from sickrage.core.api import APIError
from sickrage.core.api.announcements import AnnouncementsAPI
from sickrage.core.databases.cache import CacheDB
class Announcement(object):
......@@ -29,11 +32,30 @@ class Announcement(object):
Represents an announcement.
"""
def __init__(self, title, description, image, date):
def __init__(self, title, description, image, date, ahash):
self.title = title
self.description = description
self.image = image
self.date = date
self.ahash = ahash
@property
@CacheDB.with_session
def seen(self, session=None):
try:
announcement = session.query(CacheDB.Announcements).filter_by(hash=self.ahash).one()
return True if announcement.seen else False
except orm.exc.NoResultFound:
pass
@seen.setter
@CacheDB.with_session
def seen(self, value, session=None):
try:
announcement = session.query(CacheDB.Announcements).filter_by(hash=self.ahash).one()
announcement.seen = value
except orm.exc.NoResultFound:
pass
class Announcements(object):
......@@ -44,8 +66,7 @@ class Announcements(object):
def __init__(self):
self.name = "ANNOUNCEMENTS"
self.announcements = {}
self.seen = 0
self._announcements = {}
def run(self):
threading.currentThread().setName(self.name)
......@@ -58,15 +79,23 @@ class Announcements(object):
except APIError:
pass
def add(self, ahash, title, description, image, date):
self.announcements[ahash] = Announcement(title, description, image, date)
@CacheDB.with_session
def add(self, ahash, title, description, image, date, session=None):
self._announcements[ahash] = Announcement(title, description, image, date, ahash)
if not session.query(CacheDB.Announcements).filter_by(hash=ahash).count():
session.add(CacheDB.Announcements(**{'hash': ahash}))
@CacheDB.with_session
def clear(self, session=None):
self._announcements.clear()
session.query(CacheDB.Announcements).delete()
def clear(self):
self.announcements.clear()
def get_all(self):
return sorted(self._announcements.values(), key=lambda k: k.date)
def get(self):
self.seen = len(self.announcements)
return sorted(self.announcements.values(), key=lambda k: k.date)
def get(self, ahash):
return self._announcements.get(ahash)
def count(self):
return len(self.announcements) - self.seen
@CacheDB.with_session
def count(self, session=None):
return session.query(CacheDB.Announcements).filter(CacheDB.Announcements.seen == False).count()
......@@ -16,7 +16,7 @@
# You should have received a copy of the GNU General Public License
# along with SiCKRAGE. If not, see <http://www.gnu.org/licenses/>.
from sqlalchemy import Column, Integer, Text, String
from sqlalchemy import Column, Integer, Text, String, Boolean
from sqlalchemy.ext.declarative import as_declarative
from sqlalchemy.orm import sessionmaker
......@@ -32,7 +32,7 @@ class CacheDB(SRDatabase):
session = sessionmaker(class_=ContextSession)
def __init__(self, db_type, db_prefix, db_host, db_port, db_username, db_password):
super(CacheDB, self).__init__('cache', 4, db_type, db_prefix, db_host, db_port, db_username, db_password)
super(CacheDB, self).__init__('cache', 5, db_type, db_prefix, db_host, db_port, db_username, db_password)
CacheDBBase.metadata.create_all(self.engine)
for model in CacheDBBase._decl_class_registry.values():
if hasattr(model, '__tablename__'):
......@@ -138,3 +138,10 @@ class CacheDB(SRDatabase):
expires_in = Column(Integer, nullable=False, default=0)
expires_at = Column(Integer, nullable=False, default=0)
scope = Column(Text, default="")
class Announcements(CacheDBBase):
__tablename__ = 'announcements'
id = Column(Integer, primary_key=True)
hash = Column(String(255), unique=True, nullable=False)
seen = Column(Boolean, default=False)
# ##############################################################################
# Author: echel0n <[email protected]>
# URL: https://sickrage.ca/
# Git: https://git.sickrage.ca/SiCKRAGE/sickrage.git
# -
# This file is part of SiCKRAGE.
# -
# SiCKRAGE 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.
# -
# SiCKRAGE 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 SiCKRAGE. If not, see <http://www.gnu.org/licenses/>.
# ##############################################################################
import json
import os
from json import JSONDecodeError
from sqlalchemy import *
import sickrage
def upgrade(migrate_engine):
pass
def downgrade(migrate_engine):
pass
......@@ -31,6 +31,7 @@ from tornado.web import Application, RedirectHandler, StaticFileHandler
import sickrage
from sickrage.core.helpers import create_https_certificates
from sickrage.core.webserver.handlers.announcements import AnnouncementsHandler, MarkAnnouncementSeenHandler, AnnouncementCountHandler
from sickrage.core.webserver.handlers.api import ApiHandler
from sickrage.core.webserver.handlers.calendar import CalendarHandler
from sickrage.core.webserver.handlers.changelog import ChangelogHandler
......@@ -71,7 +72,7 @@ from sickrage.core.webserver.handlers.irc import IRCHandler
from sickrage.core.webserver.handlers.login import LoginHandler
from sickrage.core.webserver.handlers.logout import LogoutHandler
from sickrage.core.webserver.handlers.logs import LogsHandler, LogsClearAllHanlder, LogsViewHandler, \
LogsClearErrorsHanlder, LogsClearWarningsHanlder
LogsClearErrorsHanlder, LogsClearWarningsHanlder, ErrorCountHandler, WarningCountHandler
from sickrage.core.webserver.handlers.manage import ManageHandler, ShowEpisodeStatusesHandler, EpisodeStatusesHandler, \
ChangeEpisodeStatusesHandler, ShowSubtitleMissedHandler, SubtitleMissedHandler, DownloadSubtitleMissedHandler, \
BacklogShowHandler, BacklogOverviewHandler, MassEditHandler, MassUpdateHandler, FailedDownloadsHandler, EditShowHandler, SetEpisodeStatusHandler
......@@ -81,7 +82,7 @@ from sickrage.core.webserver.handlers.manage.queues import ManageQueuesHandler,
from sickrage.core.webserver.handlers.root import RobotsDotTxtHandler, MessagesDotPoHandler, \
APIBulderHandler, SetHomeLayoutHandler, SetPosterSortByHandler, SetPosterSortDirHandler, \
ToggleDisplayShowSpecialsHandler, SetScheduleLayoutHandler, ToggleScheduleDisplayPausedHandler, \
SetScheduleSortHandler, ScheduleHandler, UnlinkHandler, QuicksearchDotJsonHandler, SetHistoryLayoutHandler, ForceSchedulerJobHandler, AnnouncementsHandler
SetScheduleSortHandler, ScheduleHandler, UnlinkHandler, QuicksearchDotJsonHandler, SetHistoryLayoutHandler, ForceSchedulerJobHandler
from sickrage.core.webserver.handlers.web_file_browser import WebFileBrowserHandler, WebFileBrowserCompleteHandler
from sickrage.core.websocket import WebSocketUIHandler
......@@ -227,6 +228,8 @@ class WebServer(object):
(r'%s/setScheduleSort(/?)' % sickrage.app.config.web_root, SetScheduleSortHandler),
(r'%s/forceSchedulerJob(/?)' % sickrage.app.config.web_root, ForceSchedulerJobHandler),
(r'%s/announcements(/?)' % sickrage.app.config.web_root, AnnouncementsHandler),
(r'%s/announcements/announcementCount(/?)' % sickrage.app.config.web_root, AnnouncementCountHandler),
(r'%s/announcements/mark-seen(/?)' % sickrage.app.config.web_root, MarkAnnouncementSeenHandler),
(r'%s/schedule(/?)' % sickrage.app.config.web_root, ScheduleHandler),
(r'%s/unlink(/?)' % sickrage.app.config.web_root, UnlinkHandler),
(r'%s/setScheduleLayout(/?)' % sickrage.app.config.web_root, SetScheduleLayoutHandler),
......@@ -237,6 +240,8 @@ class WebServer(object):
(r'%s/history/trim(/?)' % sickrage.app.config.web_root, HistoryTrimHandler),
(r'%s/irc(/?)' % sickrage.app.config.web_root, IRCHandler),
(r'%s/logs(/?)' % sickrage.app.config.web_root, LogsHandler),
(r'%s/logs/errorCount(/?)' % sickrage.app.config.web_root, ErrorCountHandler),
(r'%s/logs/warningCount(/?)' % sickrage.app.config.web_root, WarningCountHandler),
(r'%s/logs/view(/?)' % sickrage.app.config.web_root, LogsViewHandler),
(r'%s/logs/clearAll(/?)' % sickrage.app.config.web_root, LogsClearAllHanlder),
(r'%s/logs/clearWarnings(/?)' % sickrage.app.config.web_root, LogsClearWarningsHanlder),
......
# ##############################################################################
# Author: echel0n <[email protected]>
# URL: https://sickrage.ca/
# Git: https://git.sickrage.ca/SiCKRAGE/sickrage.git
# -
# This file is part of SiCKRAGE.
# -
# SiCKRAGE 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.
# -
# SiCKRAGE 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 SiCKRAGE. If not, see <http://www.gnu.org/licenses/>.
# ##############################################################################
import json
from abc import ABC
import sickrage
from sickrage.core.webserver.handlers.base import BaseHandler
from sickrage.libs.trakt.interfaces.base import authenticated
class AnnouncementsHandler(BaseHandler, ABC):
@authenticated
async def get(self, *args, **kwargs):
return self.render(
'announcements.mako',
announcements=sickrage.app.announcements.get_all(),
title=_('Announcements'),
header=_('Announcements'),
topmenu='announcements',
controller='root',
action='announcements'
)
class MarkAnnouncementSeenHandler(BaseHandler, ABC):
@authenticated
async def post(self, *args, **kwargs):
ahash = self.get_argument('ahash')
announcement = sickrage.app.announcements.get(ahash)
if announcement:
announcement.seen = True
return self.write(json.dumps({'success': True}))
class AnnouncementCountHandler(BaseHandler, ABC):
@authenticated
async def get(self, *args, **kwargs):
return self.write(json.dumps({'count': sickrage.app.announcements.count()}))
......@@ -135,9 +135,6 @@ class BaseHandler(RequestHandler, ABC):
'srWebRoot': sickrage.app.config.web_root,
'srLocale': self.get_user_locale().code,
'srLocaleDir': sickrage.LOCALE_DIR,
'numErrors': sickrage.app.log.error_viewer.count(),
'numWarnings': sickrage.app.log.warning_viewer.count(),
'numAnnouncements': sickrage.app.announcements.count(),
'srStartTime': self.startTime,
'makoStartTime': time.time(),
'overall_stats': None,
......
......@@ -18,7 +18,7 @@
# You should have received a copy of the GNU General Public License
# along with SiCKRAGE. If not, see <http://www.gnu.org/licenses/>.
# ##############################################################################
import json
import os
import re
from abc import ABC
......@@ -153,3 +153,15 @@ class LogsViewHandler(BaseHandler, ABC):
controller='logs',
action='view'
)
class ErrorCountHandler(BaseHandler, ABC):
@authenticated
async def get(self, *args, **kwargs):
return self.write(json.dumps({'count': sickrage.app.log.error_viewer.count()}))
class WarningCountHandler(BaseHandler, ABC):
@authenticated
async def get(self, *args, **kwargs):
return self.write(json.dumps({'count': sickrage.app.log.warning_viewer.count()}))
......@@ -270,17 +270,4 @@ class ForceSchedulerJobHandler(BaseHandler, ABC):
service = getattr(sickrage.app, name, None)
if service:
job = sickrage.app.scheduler.get_job(service.name).func(True)
class AnnouncementsHandler(BaseHandler, ABC):
@authenticated
async def get(self, *args, **kwargs):
return self.render(
'announcements.mako',
title=_('Announcements'),
header=_('Announcements'),
topmenu='announcements',
controller='root',
action='announcements'
)
job = sickrage.app.scheduler.get_job(service.name).func(True)
\ No newline at end of file
......@@ -7,14 +7,21 @@
<%block name="content">
<div class="container">
<div class="row">
% for announcement in sickrage.app.announcements.get():
% for announcement in announcements:
<div class="col-md-4 offset-md-0 offset-sm-1 mx-auto">
<div class="card-group">
<div id="${announcement.ahash}" class="announcement">
<div class="card mb-3" style="max-width: 540px; height: 250px">
<div class="row ml-3 mt-3">
<div class="col">
<div class="row ml-3 mr-3 mt-3">
<div class="col-md-11">
<h5 class="card-title">${announcement.title}</h5>
</div>
% if not announcement.seen:
<div class="col-md-1">
<div class="mark-seen">
<i class="fa fa-circle" style="color: dodgerblue"></i>
</div>
</div>
% endif
</div>
<div class="row no-gutters mx-3">
<div class="col-md-2">
......@@ -23,7 +30,9 @@
</div>
<div class="col-md-10">
<div class="card-body pt-0">
<div class="card-text"><div class="text-muted">${datetime.strptime(announcement.date, '%Y-%m-%d').strftime("%b %d, %Y")}</div></div>
<div class="card-text">
<div class="text-muted">${datetime.strptime(announcement.date, '%Y-%m-%d').strftime("%b %d, %Y")}</div>
</div>
<div class="card-text">${announcement.description}</div>
</div>
</div>
......
......@@ -72,21 +72,6 @@
<%block name="modals" />
% if current_user:
<%
toolsBadge = ''
numCombined = numErrors + numWarnings + numAnnouncements
if numCombined:
toolsBadgeClass = ''
if numErrors:
toolsBadgeClass = 'badge-danger'
elif numWarnings:
toolsBadgeClass = 'badge-warning'
elif numAnnouncements:
toolsBadgeClass = 'badge-info'
toolsBadge = '<span class="badge badge-pill ' + toolsBadgeClass + '" style="float:right;margin-bottom:-10px;">' + str(numCombined) + '</span>'
%>
% if current_user and sickrage.app.newest_version_string:
<div class="alert alert-success alert-dismissible fade show text-center m-0 rounded-0">
<strong>${sickrage.app.newest_version_string}</strong>
......@@ -269,10 +254,10 @@
<span class="d-none d-sm-block dropdown-toggle d-md-none">
${_('Tools')}
</span>
<span class="d-sm-none d-md-block">
<span id="profile-container" class="d-sm-none d-md-block">
<img class="rounded-circle shadow"
src="https://gravatar.com/avatar/${md5(current_user['email'].encode('utf-8')).hexdigest()}?d=mm&s=40"/>
${toolsBadge}
<span id="profile-badge" class="badge badge-info" style="float:right;margin-bottom:-10px;"></span>
</span>
</a>
......@@ -289,24 +274,19 @@
</a>
<a class="dropdown-item" href="${srWebRoot}/announcements/">
<i class="fas fa-fw fa-circle"></i>&nbsp;${_('Announcements')}
%if numAnnouncements:
<span class="badge badge-info">${numAnnouncements}</span>
%endif
<span id="numAnnouncements" class="badge badge-info"></span>
</a>
<div class="dropdown-divider"></div>
%if numErrors:
<a class="dropdown-item" href="${srWebRoot}/logs/">
<i class="fas fa-fw fa-exclamation-circle"></i>&nbsp;${_('View Errors')}
<span class="badge badge-danger">${numErrors}</span>
</a>
%endif
%if numWarnings:
<a class="dropdown-item"
href="${srWebRoot}/logs/?level=${sickrage.app.log.WARNING}">
<i class="fas fa-fw fa-exclamation-triangle"></i>&nbsp;${_('View Warnings')}
<span class="badge badge-warning">${numWarnings}</span>
</a>
%endif
<a class="dropdown-item d-none"
href="${srWebRoot}/logs/">
<i class="fas fa-fw fa-exclamation-circle"></i>&nbsp;${_('View Errors')}
<span id="numErrors" class="badge badge-danger"></span>
</a>
<a class="dropdown-item d-none"
href="${srWebRoot}/logs/?level=${sickrage.app.log.WARNING}">
<i class="fas fa-fw fa-exclamation-triangle"></i>&nbsp;${_('View Warnings')}
<span id="numWarnings" class="badge badge-warning"></span>
</a>
<a class="dropdown-item" href="${srWebRoot}/logs/view/">
<i class="fas fa-fw fa-file-archive"></i>&nbsp;${_('View Log')}
</a>
......
......@@ -287,6 +287,57 @@ $(document).ready(function ($) {
});
},
updateProfileBadge: function () {
let total_count = 0;
$.getJSON(SICKRAGE.srWebRoot + "/logs/errorCount", function (data) {
if (data.count) {
total_count += data.count;
$('#numErrors').text(data.count);
$('#numErrors').parent().removeClass('d-none');
} else {
$('#numErrors').parent().addClass('d-none');
}
if (total_count) {
$('#profile-badge').text(total_count);
} else {
$('#profile-badge').text('');
}
});
$.getJSON(SICKRAGE.srWebRoot + "/logs/warningCount", function (data) {
if (data.count) {
total_count += data.count;
$('#numWarnings').text(data.count);
$('#numWarnings').parent().removeClass('d-none');
} else {
$('#numWarnings').parent().addClass('d-none');
}
if (total_count) {
$('#profile-badge').text(total_count);
} else {
$('#profile-badge').text('');
}
});
$.getJSON(SICKRAGE.srWebRoot + "/announcements/announcementCount", function (data) {
if (data.count) {
total_count += data.count;
$('#numAnnouncements').text(data.count);
} else {
$('#numAnnouncements').text('');
}
if (total_count) {
$('#profile-badge').text(total_count);
} else {
$('#profile-badge').text('');
}
});
},
common: {
init: function () {
SICKRAGE.srPID = SICKRAGE.getMeta('srPID');
......@@ -296,6 +347,7 @@ $(document).ready(function ($) {
SICKRAGE.anonURL = SICKRAGE.getMeta('anonURL');
SICKRAGE.ws_notifications();
SICKRAGE.updateProfileBadge();
// add locale translation
$.get(`${SICKRAGE.srWebRoot}/messages.po`, function (data) {
......@@ -324,11 +376,9 @@ $(document).ready(function ($) {
return false;
});
// tooltips
$('[title!=""]').tooltipster();
var imgDefer = document.getElementsByTagName('img');
for (var i = 0; i < imgDefer.length; i++) {
if (imgDefer[i].getAttribute('data-src')) {
......@@ -1597,6 +1647,18 @@ $(document).ready(function ($) {
//}
//});
},
announcements: function () {
$('.mark-seen').on('click', function () {
let elm = $(this);
$.post(SICKRAGE.srWebRoot + "/announcements/mark-seen", {
ahash: $(this).closest('.announcement')[0].id
}, function () {
SICKRAGE.updateProfileBadge();
elm.hide()
});
});
}
},
home: {
......
......@@ -145,13 +145,6 @@ module.exports = {
filename: "../css/core.min.css",
chunkFilename: "[id].css"
}),
new SentryWebpackPlugin({
release: version,
include: path.resolve(__dirname, 'sickrage/core/webserver/static/js'),
ignoreFile: '.sentrycliignore',
ignore: ['node_modules', 'webpack.config.js'],
configFile: 'sentry.properties'
}),
new OptimizeCSSAssetsPlugin(),
makeSprite('core'),
makeSprite('network'),
......@@ -160,4 +153,16 @@ module.exports = {
makeSprite('subtitles'),
makeSprite('flags')
]
};
\ No newline at end of file
};
if (process.env.ENABLE_SENTRY_RELEASE.toLowerCase() === 'true') {
module.exports.plugins.push(
new SentryWebpackPlugin({
release: version,
include: path.resolve(__dirname, 'sickrage/core/webserver/static/js'),
ignoreFile: '.sentrycliignore',
ignore: ['node_modules', 'webpack.config.js'],
configFile: 'sentry.properties'
}),
)
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment