mirror of
https://github.com/SickGear/SickGear.git
synced 2025-01-05 17:43:37 +00:00
Change Trakt notification config to only handle PIN authentication with the service.
Fix handling of an erroneous PIN input/authentication flow and clean-up the ui and results output. Remove all other Trakt deprecated API V1 service features pending reconsideration.
This commit is contained in:
parent
96467210c8
commit
74feb39eb6
7 changed files with 88 additions and 91 deletions
|
@ -61,6 +61,8 @@
|
|||
* Change default manage episodes selector to Snatched episodes if items exist else Wanted on Episode Status Manage page
|
||||
* Change snatched row colour on Episode Status Manage page to match colour used on the show details page
|
||||
* Change replace trakt with libtrakt for API v2
|
||||
* Change Trakt notification config to only handle PIN authentication with the service
|
||||
* Remove all other Trakt deprecated API V1 service features pending reconsideration
|
||||
|
||||
[develop changelog]
|
||||
Enable Alpha Ratio again now that the secure login page over https is fixed
|
||||
|
|
|
@ -1466,7 +1466,7 @@
|
|||
<div class="component-group-desc">
|
||||
<img class="notifier-icon" src="$sbRoot/images/notifiers/trakt.png" alt="" title="Trakt"/>
|
||||
<h3><a href="<%= anon_url('http://trakt.tv/') %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false;">Trakt</a></h3>
|
||||
<p>trakt helps keep a record of what TV shows and movies you are watching. Based on your favorites, trakt recommends additional shows and movies you'll enjoy!</p>
|
||||
<p>Trakt can keep a record of what TV shows you are watching and recommend additional shows based on your show data.</p>
|
||||
</div>
|
||||
<fieldset class="component-group-list">
|
||||
<div class="field-pair">
|
||||
|
@ -1474,7 +1474,7 @@
|
|||
<span class="component-title">Enable</span>
|
||||
<span class="component-desc">
|
||||
<input type="checkbox" class="enabler" name="use_trakt" id="use_trakt" #if $sickbeard.USE_TRAKT then 'checked="checked"' else ''# />
|
||||
<p>should SickGear send Trakt.tv notifications ?</p>
|
||||
<p>should SickGear use Trakt.tv ?</p>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
@ -1483,15 +1483,15 @@
|
|||
<div class="field-pair">
|
||||
<label for="trakt_pin">
|
||||
<span class="component-title">Trakt PIN:</span>
|
||||
<input type="text" name="trakt_pin" id="trakt_pin" value="" class="form-control input-sm input250" />
|
||||
<input type="button" class="btn" value="Connect" id="pinTrakt" />
|
||||
</label>
|
||||
<div class="testNotification" id="pinTrakt-result"></div>
|
||||
<label>
|
||||
<span class="component-title"> </span>
|
||||
<span class="component-desc">get your PIN at: <a href="<%= anon_url(sickbeard.TRAKT_PIN_URL) %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false;"><b>$sickbeard.TRAKT_PIN_URL</b></a></span>
|
||||
<span class="component-desc">
|
||||
<input type="text" name="trakt_pin" id="trakt_pin" value="" class="form-control input-sm input250" />
|
||||
<input type="button" class="btn" value="Connect" id="trakt-authenticate" />
|
||||
<div class="clear-left"><p>get your PIN at: <a href="<%= anon_url(sickbeard.TRAKT_PIN_URL) %>" rel="noreferrer" onclick="window.open(this.href, '_blank'); return false;"><b>$sickbeard.TRAKT_PIN_URL</b></a></p></div>
|
||||
</span>
|
||||
</label>
|
||||
<div class="testNotification" id="trakt-authentication-result"></div>
|
||||
</div>
|
||||
<!--
|
||||
<div class="field-pair">
|
||||
<label for="trakt_default_indexer">
|
||||
<span class="component-title">Default indexer:</span>
|
||||
|
@ -1565,6 +1565,7 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
<input type="submit" class="btn config_submitter" value="Save Changes" />
|
||||
</div><!-- /content_use_trakt //-->
|
||||
</fieldset>
|
||||
|
|
|
@ -352,23 +352,30 @@ $(document).ready(function(){
|
|||
});
|
||||
});
|
||||
|
||||
$('#pinTrakt').click(function () {
|
||||
var trakt_pin = $.trim($('#trakt_pin').val());
|
||||
if (!trakt_pin) {
|
||||
$('#testTrakt-result').html('Please fill out the necessary fields above.');
|
||||
$('#trakt_pin').addClass('warning');
|
||||
return;
|
||||
var elTraktAuth = $('#trakt-authenticate'), elTraktAuthResult = $('#trakt-authentication-result');
|
||||
elTraktAuth.click(function() {
|
||||
var elTrakt = $('#trakt_pin'), traktPin = $.trim(elTrakt.val());
|
||||
if(!traktPin) {
|
||||
elTrakt.addClass('warning');
|
||||
elTraktAuthResult.html('Please enter a required PIN above.');
|
||||
} else {
|
||||
elTrakt.removeClass('warning');
|
||||
$(this).prop('disabled', true);
|
||||
elTraktAuthResult.html(loading);
|
||||
$.get(sbRoot + '/home/trakt_authenticate', {'pin': traktPin})
|
||||
.done(function(data) {
|
||||
elTraktAuthResult.html(data);
|
||||
elTraktAuth.prop('disabled', false);
|
||||
});
|
||||
}
|
||||
$('#trakt_pin').removeClass('warning');
|
||||
$(this).prop('disabled', true);
|
||||
$('#testTrakt-result').html(loading);
|
||||
$.get(sbRoot + '/home/pinTrakt', {'pin': trakt_pin})
|
||||
.done(function (data) {
|
||||
$('#pinTrakt-result').html(data);
|
||||
$('#pinTrakt').prop('disabled', false);
|
||||
});
|
||||
});
|
||||
|
||||
elTraktAuthResult.html(loading);
|
||||
$.get(sbRoot + '/home/trakt_get_connected_account')
|
||||
.done(function(data) {
|
||||
elTraktAuthResult.html(data);
|
||||
});
|
||||
|
||||
$('#testEmail').click(function () {
|
||||
var status, host, port, tls, from, user, pwd, err, to;
|
||||
status = $('#testEmail-result');
|
||||
|
@ -552,10 +559,4 @@ $(document).ready(function(){
|
|||
}
|
||||
});
|
||||
if ($('input[id="use_plex"]').is(':checked')) {$('.plexinfo').removeClass('hide')}
|
||||
|
||||
$('#testTrakt-result').html(loading);
|
||||
$.get(sbRoot + '/home/get_connected_Trakt_Account')
|
||||
.done(function (data) {
|
||||
$('#pinTrakt-result').html(data);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,10 +9,12 @@ from exceptions import traktException, traktAuthException # , traktServerBusy
|
|||
|
||||
|
||||
class TraktAPI:
|
||||
def __init__(self, ssl_verify=True, timeout=30):
|
||||
|
||||
def __init__(self, ssl_verify=True, timeout=None):
|
||||
|
||||
self.session = requests.Session()
|
||||
self.verify = certifi.where() if ssl_verify else False
|
||||
self.timeout = timeout if timeout else None
|
||||
self.verify = ssl_verify and sickbeard.TRAKT_VERIFY and certifi.where()
|
||||
self.timeout = timeout or sickbeard.TRAKT_TIMEOUT
|
||||
self.auth_url = sickbeard.TRAKT_BASE_URL
|
||||
self.api_url = sickbeard.TRAKT_BASE_URL
|
||||
self.headers = {
|
||||
|
@ -23,11 +25,11 @@ class TraktAPI:
|
|||
|
||||
def trakt_token(self, trakt_pin=None, refresh=False, count=0):
|
||||
|
||||
if count > 3:
|
||||
if 3 <= count:
|
||||
sickbeard.TRAKT_ACCESS_TOKEN = ''
|
||||
return False
|
||||
elif count > 0:
|
||||
time.sleep(2)
|
||||
elif 0 < count:
|
||||
time.sleep(3)
|
||||
|
||||
data = {
|
||||
'client_id': sickbeard.TRAKT_CLIENT_ID,
|
||||
|
@ -40,9 +42,9 @@ class TraktAPI:
|
|||
data['refresh_token'] = sickbeard.TRAKT_REFRESH_TOKEN
|
||||
else:
|
||||
data['grant_type'] = 'authorization_code'
|
||||
if None is not trakt_pin:
|
||||
if trakt_pin:
|
||||
data['code'] = trakt_pin
|
||||
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
@ -57,39 +59,36 @@ class TraktAPI:
|
|||
return False
|
||||
|
||||
def validate_account(self):
|
||||
|
||||
|
||||
resp = self.trakt_request('users/settings')
|
||||
|
||||
if 'account' in resp:
|
||||
return True
|
||||
return False
|
||||
|
||||
return 'account' in resp
|
||||
|
||||
def get_connected_user(self):
|
||||
|
||||
if sickbeard.TRAKT_TOKEN:
|
||||
response = 'Connected to Trakt user account: %s'
|
||||
|
||||
if sickbeard.TRAKT_CONNECTED_ACCOUNT and sickbeard.TRAKT_TOKEN == sickbeard.TRAKT_CONNECTED_ACCOUNT[1] and sickbeard.TRAKT_CONNECTED_ACCOUNT[0]:
|
||||
return 'Connected to Trakt user account: ' + str(sickbeard.TRAKT_CONNECTED_ACCOUNT[0])
|
||||
return response % sickbeard.TRAKT_CONNECTED_ACCOUNT[0]
|
||||
|
||||
resp = self.trakt_request('users/settings')
|
||||
|
||||
if 'user' in resp:
|
||||
sickbeard.TRAKT_CONNECTED_ACCOUNT = [resp['user']['username'], sickbeard.TRAKT_TOKEN]
|
||||
return 'Connected to Trakt user account: ' + str(sickbeard.TRAKT_CONNECTED_ACCOUNT[0])
|
||||
return 'Not Connected to Trakt'
|
||||
return response % sickbeard.TRAKT_CONNECTED_ACCOUNT[0]
|
||||
|
||||
return 'Not connected to Trakt'
|
||||
|
||||
def trakt_request(self, path, data=None, headers=None, url=None, method='GET', count=0):
|
||||
if None is url:
|
||||
url = self.api_url
|
||||
|
||||
count += 1
|
||||
|
||||
if None is headers:
|
||||
headers = self.headers
|
||||
|
||||
if None is sickbeard.TRAKT_TOKEN:
|
||||
logger.log(u'You must get a Trakt TOKEN. Check your Trakt settings', logger.WARNING)
|
||||
logger.log(u'You must get a Trakt token. Check your Trakt settings', logger.WARNING)
|
||||
return {}
|
||||
|
||||
headers = headers or self.headers
|
||||
url = url or self.api_url
|
||||
count += 1
|
||||
|
||||
headers['Authorization'] = 'Bearer ' + sickbeard.TRAKT_TOKEN
|
||||
|
||||
try:
|
||||
|
@ -111,13 +110,13 @@ class TraktAPI:
|
|||
else:
|
||||
logger.log(u'Could not connect to Trakt. Error: {0}'.format(e), logger.WARNING)
|
||||
elif 502 == code:
|
||||
# Retry the request, cloudflare had a proxying issue
|
||||
# Retry the request, Cloudflare had a proxying issue
|
||||
logger.log(u'Retrying trakt api request: %s' % path, logger.WARNING)
|
||||
return self.trakt_request(path, data, headers, url, method)
|
||||
return self.trakt_request(path, data, headers, url, method, count=count)
|
||||
elif 401 == code:
|
||||
if self.trakt_token(refresh=True, count=count):
|
||||
sickbeard.save_config()
|
||||
return self.trakt_request(path, data, headers, url, method)
|
||||
return self.trakt_request(path, data, headers, url, method, count=count)
|
||||
else:
|
||||
logger.log(u'Unauthorized. Please check your Trakt settings', logger.WARNING)
|
||||
raise traktAuthException()
|
||||
|
@ -130,8 +129,8 @@ class TraktAPI:
|
|||
logger.log(u'Could not connect to Trakt. Code error: {0}'.format(code), logger.ERROR)
|
||||
return {}
|
||||
|
||||
# check and confirm trakt call did not fail
|
||||
if isinstance(resp, dict) and 'failure' == resp.get('status', False):
|
||||
# check and confirm Trakt call did not fail
|
||||
if isinstance(resp, dict) and 'failure' == resp.get('status', None):
|
||||
if 'message' in resp:
|
||||
raise traktException(resp['message'])
|
||||
if 'error' in resp:
|
||||
|
|
|
@ -363,7 +363,6 @@ TRAKT_REMOVE_SERIESLIST = False
|
|||
TRAKT_USE_WATCHLIST = False
|
||||
TRAKT_METHOD_ADD = 0
|
||||
TRAKT_START_PAUSED = False
|
||||
TRAKT_USE_RECOMMENDED = False
|
||||
TRAKT_SYNC = False
|
||||
TRAKT_DEFAULT_INDEXER = None
|
||||
|
||||
|
@ -490,7 +489,7 @@ def initialize(consoleLogging=True):
|
|||
USE_XBMC, XBMC_ALWAYS_ON, XBMC_NOTIFY_ONSNATCH, XBMC_NOTIFY_ONDOWNLOAD, XBMC_NOTIFY_ONSUBTITLEDOWNLOAD, XBMC_UPDATE_FULL, XBMC_UPDATE_ONLYFIRST, \
|
||||
XBMC_UPDATE_LIBRARY, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, BACKLOG_FREQUENCY, \
|
||||
USE_KODI, KODI_ALWAYS_ON, KODI_NOTIFY_ONSNATCH, KODI_NOTIFY_ONDOWNLOAD, KODI_NOTIFY_ONSUBTITLEDOWNLOAD, KODI_UPDATE_FULL, KODI_UPDATE_ONLYFIRST, KODI_UPDATE_LIBRARY, KODI_HOST, KODI_USERNAME, KODI_PASSWORD, \
|
||||
USE_TRAKT, TRAKT_CONNECTED_ACCOUNT, TRAKT_VERIFY, TRAKT_REMOVE_WATCHLIST, TRAKT_TOKEN, TRAKT_TIMEOUT, TRAKT_REFRESH_TOKEN, TRAKT_USE_WATCHLIST, TRAKT_METHOD_ADD, TRAKT_START_PAUSED, traktCheckerScheduler, TRAKT_USE_RECOMMENDED, TRAKT_SYNC, TRAKT_DEFAULT_INDEXER, TRAKT_REMOVE_SERIESLIST, \
|
||||
USE_TRAKT, TRAKT_CONNECTED_ACCOUNT, TRAKT_VERIFY, TRAKT_REMOVE_WATCHLIST, TRAKT_TOKEN, TRAKT_TIMEOUT, TRAKT_REFRESH_TOKEN, TRAKT_USE_WATCHLIST, TRAKT_METHOD_ADD, TRAKT_START_PAUSED, traktCheckerScheduler, TRAKT_SYNC, TRAKT_DEFAULT_INDEXER, TRAKT_REMOVE_SERIESLIST, \
|
||||
USE_PLEX, PLEX_NOTIFY_ONSNATCH, PLEX_NOTIFY_ONDOWNLOAD, PLEX_NOTIFY_ONSUBTITLEDOWNLOAD, PLEX_UPDATE_LIBRARY, \
|
||||
PLEX_SERVER_HOST, PLEX_HOST, PLEX_USERNAME, PLEX_PASSWORD, DEFAULT_BACKLOG_FREQUENCY, MIN_BACKLOG_FREQUENCY, MAX_BACKLOG_FREQUENCY, BACKLOG_STARTUP, SKIP_REMOVED_FILES, \
|
||||
showUpdateScheduler, __INITIALIZED__, LAUNCH_BROWSER, TRASH_REMOVE_SHOW, TRASH_ROTATE_LOGS, HOME_SEARCH_FOCUS, SORT_ARTICLE, showList, loadingShowList, UPDATE_SHOWS_ON_START, SHOW_UPDATE_HOUR, ALLOW_INCOMPLETE_SHOWDATA, \
|
||||
|
@ -885,7 +884,6 @@ def initialize(consoleLogging=True):
|
|||
TRAKT_USE_WATCHLIST = bool(check_setting_int(CFG, 'Trakt', 'trakt_use_watchlist', 0))
|
||||
TRAKT_METHOD_ADD = check_setting_int(CFG, 'Trakt', 'trakt_method_add', 0)
|
||||
TRAKT_START_PAUSED = bool(check_setting_int(CFG, 'Trakt', 'trakt_start_paused', 0))
|
||||
TRAKT_USE_RECOMMENDED = bool(check_setting_int(CFG, 'Trakt', 'trakt_use_recommended', 0))
|
||||
TRAKT_SYNC = bool(check_setting_int(CFG, 'Trakt', 'trakt_sync', 0))
|
||||
TRAKT_DEFAULT_INDEXER = check_setting_int(CFG, 'Trakt', 'trakt_default_indexer', 1)
|
||||
|
||||
|
@ -1697,7 +1695,6 @@ def save_config():
|
|||
new_config['Trakt']['trakt_use_watchlist'] = int(TRAKT_USE_WATCHLIST)
|
||||
new_config['Trakt']['trakt_method_add'] = int(TRAKT_METHOD_ADD)
|
||||
new_config['Trakt']['trakt_start_paused'] = int(TRAKT_START_PAUSED)
|
||||
new_config['Trakt']['trakt_use_recommended'] = int(TRAKT_USE_RECOMMENDED)
|
||||
new_config['Trakt']['trakt_sync'] = int(TRAKT_SYNC)
|
||||
new_config['Trakt']['trakt_default_indexer'] = int(TRAKT_DEFAULT_INDEXER)
|
||||
|
||||
|
|
|
@ -1034,7 +1034,7 @@ class PostProcessor(object):
|
|||
notifiers.pytivo_notifier.update_library(ep_obj)
|
||||
|
||||
# do the library update for Trakt
|
||||
notifiers.trakt_notifier.update_library(ep_obj)
|
||||
# notifiers.trakt_notifier.update_library(ep_obj)
|
||||
|
||||
self._run_extra_scripts(ep_obj)
|
||||
|
||||
|
|
|
@ -55,7 +55,8 @@ from lib import subliminal
|
|||
from lib.dateutil import tz
|
||||
from lib.unrar2 import RarFile
|
||||
from lib.libtrakt import TraktAPI
|
||||
from lib.libtrakt.exceptions import traktException
|
||||
from lib.libtrakt.exceptions import traktException, traktAuthException
|
||||
|
||||
|
||||
try:
|
||||
import json
|
||||
|
@ -880,23 +881,24 @@ class Home(MainHandler):
|
|||
return '{"message": "Unable to find NMJ Database at location: %(dbloc)s. Is the right location selected and PCH running?", "database": ""}' % {
|
||||
"dbloc": dbloc}
|
||||
|
||||
def pinTrakt(self, pin=None):
|
||||
def trakt_authenticate(self, pin=None):
|
||||
self.set_header('Cache-Control', 'max-age=0,no-cache,no-store')
|
||||
|
||||
if None is pin:
|
||||
return 'Enter PIN'
|
||||
return 'Trakt PIN required for authentication'
|
||||
|
||||
result = TraktAPI(ssl_verify=False, timeout=sickbeard.TRAKT_TIMEOUT).trakt_token(pin)
|
||||
try:
|
||||
TraktAPI().trakt_token(pin)
|
||||
except traktAuthException:
|
||||
return 'Fail: Trakt NOT authenticated'
|
||||
|
||||
if result:
|
||||
sickbeard.USE_TRAKT = 1
|
||||
sickbeard.save_config()
|
||||
return 'Trakt Authorized'
|
||||
else:
|
||||
return 'Trakt NOT Authorized'
|
||||
sickbeard.USE_TRAKT = True
|
||||
sickbeard.save_config()
|
||||
return '%s %s' % ('Success: Trakt authenticated.', self.trakt_get_connected_account())
|
||||
|
||||
def get_connected_Trakt_Account(self):
|
||||
return TraktAPI(ssl_verify=sickbeard.TRAKT_VERIFY, timeout=sickbeard.TRAKT_TIMEOUT).get_connected_user()
|
||||
@staticmethod
|
||||
def trakt_get_connected_account():
|
||||
return TraktAPI().get_connected_user()
|
||||
|
||||
def loadShowNotifyLists(self, *args, **kwargs):
|
||||
self.set_header('Cache-Control', 'max-age=0,no-cache,no-store')
|
||||
|
@ -2278,7 +2280,7 @@ class NewHomeAddShows(Home):
|
|||
t = PageTemplate(headers=self.request.headers, file='home_trendingShows.tmpl')
|
||||
t.submenu = self.HomeMenu()
|
||||
|
||||
trakt_api = TraktAPI(ssl_verify=sickbeard.TRAKT_VERIFY, timeout=sickbeard.TRAKT_TIMEOUT)
|
||||
trakt_api = TraktAPI()
|
||||
limit_show = 50
|
||||
try:
|
||||
t.trending_shows = trakt_api.trakt_request("shows/trending?limit=" + str(limit_show) + "&extended=full,images") or []
|
||||
|
@ -4380,7 +4382,7 @@ class ConfigNotifications(Config):
|
|||
use_nmjv2=None, nmjv2_host=None, nmjv2_dbloc=None, nmjv2_database=None,
|
||||
use_trakt=None, trakt_pin=None,
|
||||
trakt_remove_watchlist=None, trakt_use_watchlist=None, trakt_method_add=None,
|
||||
trakt_start_paused=None, trakt_use_recommended=None, trakt_sync=None,
|
||||
trakt_start_paused=None, trakt_sync=None,
|
||||
trakt_default_indexer=None, trakt_remove_serieslist=None,
|
||||
use_synologynotifier=None, synologynotifier_notify_onsnatch=None,
|
||||
synologynotifier_notify_ondownload=None, synologynotifier_notify_onsubtitledownload=None,
|
||||
|
@ -4507,19 +4509,14 @@ class ConfigNotifications(Config):
|
|||
synologynotifier_notify_onsubtitledownload)
|
||||
|
||||
sickbeard.USE_TRAKT = config.checkbox_to_value(use_trakt)
|
||||
sickbeard.TRAKT_REMOVE_WATCHLIST = config.checkbox_to_value(trakt_remove_watchlist)
|
||||
sickbeard.TRAKT_REMOVE_SERIESLIST = config.checkbox_to_value(trakt_remove_serieslist)
|
||||
sickbeard.TRAKT_USE_WATCHLIST = config.checkbox_to_value(trakt_use_watchlist)
|
||||
sickbeard.TRAKT_METHOD_ADD = int(trakt_method_add)
|
||||
sickbeard.TRAKT_START_PAUSED = config.checkbox_to_value(trakt_start_paused)
|
||||
sickbeard.TRAKT_USE_RECOMMENDED = config.checkbox_to_value(trakt_use_recommended)
|
||||
sickbeard.TRAKT_SYNC = config.checkbox_to_value(trakt_sync)
|
||||
sickbeard.TRAKT_DEFAULT_INDEXER = int(trakt_default_indexer)
|
||||
|
||||
if sickbeard.USE_TRAKT:
|
||||
sickbeard.traktCheckerScheduler.silent = False
|
||||
else:
|
||||
sickbeard.traktCheckerScheduler.silent = True
|
||||
sickbeard.traktCheckerScheduler.silent = not sickbeard.USE_TRAKT
|
||||
# sickbeard.TRAKT_DEFAULT_INDEXER = int(trakt_default_indexer)
|
||||
# sickbeard.TRAKT_SYNC = config.checkbox_to_value(trakt_sync)
|
||||
# sickbeard.TRAKT_USE_WATCHLIST = config.checkbox_to_value(trakt_use_watchlist)
|
||||
# sickbeard.TRAKT_METHOD_ADD = int(trakt_method_add)
|
||||
# sickbeard.TRAKT_REMOVE_WATCHLIST = config.checkbox_to_value(trakt_remove_watchlist)
|
||||
# sickbeard.TRAKT_REMOVE_SERIESLIST = config.checkbox_to_value(trakt_remove_serieslist)
|
||||
# sickbeard.TRAKT_START_PAUSED = config.checkbox_to_value(trakt_start_paused)
|
||||
|
||||
sickbeard.USE_EMAIL = config.checkbox_to_value(use_email)
|
||||
sickbeard.EMAIL_NOTIFY_ONSNATCH = config.checkbox_to_value(email_notify_onsnatch)
|
||||
|
|
Loading…
Reference in a new issue