Browse Source

Merge branch 'maxkoryukov-fix/opt-directories' into develop

pull/6004/merge
Ruud 9 years ago
parent
commit
f3a13e6283
  1. 3
      .travis.yml
  2. 2
      Gruntfile.js
  3. 3
      couchpotato/core/_base/_core.py
  4. 5
      couchpotato/core/loader.py
  5. 35
      couchpotato/core/plugins/browser.py
  6. 39
      couchpotato/core/plugins/browser_test.py
  7. 3
      couchpotato/core/plugins/manage.py
  8. 141
      couchpotato/core/settings.py
  9. 122
      couchpotato/core/softchroot.py
  10. 121
      couchpotato/core/softchroot_test.py
  11. 54
      couchpotato/core/test_softchroot.py
  12. 2
      couchpotato/environment.py
  13. 19
      couchpotato/environment_test.py
  14. 16
      couchpotato/runner.py
  15. 7
      couchpotato/static/scripts/combined.base.min.js
  16. 13
      couchpotato/static/scripts/page/settings.js
  17. 12
      couchpotato/static/style/combined.min.css
  18. 5
      py-dev-requirements.txt

3
.travis.yml

@ -14,6 +14,7 @@ cache:
pip: true
directories:
- node_modules
- libs
# command to install dependencies
install:
@ -25,6 +26,8 @@ install:
- pip install --upgrade nose
# - pip install rednose
- nosetests --plugins
# command to run tests
script:
- grunt test

2
Gruntfile.js

@ -201,7 +201,7 @@ module.exports = function(grunt){
config: './.nosetestsrc',
// 'rednose' is a colored output for nose test-runner. But we do not requre colors on travis-ci
rednose: config.colorful_tests_output,
externalNose: true
externalNose: true,
},
test: {

3
couchpotato/core/_base/_core.py

@ -249,6 +249,7 @@ config = [{
{
'name': 'username',
'default': '',
'ui-meta' : 'rw',
},
{
'name': 'password',
@ -302,7 +303,7 @@ config = [{
{
'name': 'api_key',
'default': uuid4().hex,
'readonly': 1,
'ui-meta' : 'ro',
'description': 'Let 3rd party app do stuff. <a target="_self" href="../../docs/">Docs</a>',
},
{

5
couchpotato/core/loader.py

@ -99,6 +99,11 @@ class Loader(object):
path = os.path.join(dir_name, name)
ext = os.path.splitext(path)[1]
ext_length = len(ext)
# SKIP test files:
if path.endswith('_test.py'):
continue
if name != 'static' and ((os.path.isdir(path) and os.path.isfile(os.path.join(path, '__init__.py')))
or (os.path.isfile(path) and ext == '.py')):
name = name[:-ext_length] if ext_length > 0 else name

35
couchpotato/core/plugins/browser.py

@ -10,10 +10,10 @@ from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import sp, ss, toUnicode
from couchpotato.core.helpers.variable import getUserDir
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.softchroot import SoftChroot
log = CPLog(__name__)
from couchpotato.environment import Env
log = CPLog(__name__)
if os.name == 'nt':
import imp
@ -29,13 +29,9 @@ if os.name == 'nt':
autoload = 'FileBrowser'
class FileBrowser(Plugin):
def __init__(self):
soft_chroot_dir = self.conf('soft_chroot', section='core')
self.soft_chroot = SoftChroot(soft_chroot_dir)
addApiView('directory.list', self.view, docs = {
'desc': 'Return the directory list of a given directory',
'params': {
@ -81,17 +77,20 @@ class FileBrowser(Plugin):
return driveletters
def view(self, path = '/', show_hidden = True, **kwargs):
soft_chroot = Env.get('softchroot')
home = getUserDir()
if self.soft_chroot.enabled:
if not self.soft_chroot.is_subdir(home):
home = self.soft_chroot.chdir
if soft_chroot.enabled:
if not soft_chroot.is_subdir(home):
home = soft_chroot.get_chroot()
if not path:
path = home
if path.endswith(os.path.sep):
path = path.rstrip(os.path.sep)
elif self.soft_chroot.enabled:
path = self.soft_chroot.add(path)
else:
path = soft_chroot.chroot2abs(path)
try:
dirs = self.getDirectories(path = path, show_hidden = show_hidden)
@ -99,8 +98,8 @@ class FileBrowser(Plugin):
log.error('Failed getting directory "%s" : %s', (path, traceback.format_exc()))
dirs = []
if self.soft_chroot.enabled:
dirs = map(self.soft_chroot.cut, dirs)
if soft_chroot.enabled:
dirs = map(soft_chroot.abs2chroot, dirs)
parent = os.path.dirname(path.rstrip(os.path.sep))
if parent == path.rstrip(os.path.sep):
@ -111,16 +110,16 @@ class FileBrowser(Plugin):
# TODO : check on windows:
is_root = path == '/'
if self.soft_chroot.enabled:
is_root = self.soft_chroot.is_root_abs(path)
if soft_chroot.enabled:
is_root = soft_chroot.is_root_abs(path)
# fix paths:
if self.soft_chroot.is_subdir(parent):
parent = self.soft_chroot.cut(parent)
if soft_chroot.is_subdir(parent):
parent = soft_chroot.abs2chroot(parent)
else:
parent = os.path.sep
home = self.soft_chroot.cut(home)
home = soft_chroot.abs2chroot(home)
return {
'is_root': is_root,

39
couchpotato/core/plugins/test_browser.py → couchpotato/core/plugins/browser_test.py

@ -1,43 +1,46 @@
import sys
#import sys
import os
import logging
import mock
import unittest
from unittest import TestCase
#from mock import MagicMock
from couchpotato.core.plugins.browser import FileBrowser
from couchpotato.core.softchroot import SoftChroot
CHROOT_DIR = '/tmp/'
# 'couchpotato.core.plugins.browser.Env',
@mock.patch('couchpotato.core.plugins.browser.Env', name='EnvMock')
class FileBrowserChrootedTest(TestCase):
def setUp(self):
self.b = FileBrowser()
# TODO : remove scrutch:
self.b.soft_chroot = SoftChroot(CHROOT_DIR)
# Logger
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# To screen
hdlr = logging.StreamHandler(sys.stderr)
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%m-%d %H:%M:%S')
hdlr.setFormatter(formatter)
#logger.addHandler(hdlr)
def tuneMock(self, env):
#set up mock:
sc = SoftChroot()
sc.initialize(CHROOT_DIR)
env.get.return_value = sc
def test_soft_chroot_enabled(self):
self.assertTrue( self.b.soft_chroot.enabled)
def test_view__chrooted_path_none(self):
def test_view__chrooted_path_none(self, env):
#def view(self, path = '/', show_hidden = True, **kwargs):
self.tuneMock(env)
r = self.b.view(None)
self.assertEqual(r['home'], '/')
self.assertEqual(r['parent'], '/')
self.assertTrue(r['is_root'])
def test_view__chrooted_path_chroot(self):
def test_view__chrooted_path_chroot(self, env):
#def view(self, path = '/', show_hidden = True, **kwargs):
self.tuneMock(env)
for path, parent in [('/asdf','/'), (CHROOT_DIR, '/'), ('/mnk/123/t', '/mnk/123/')]:
r = self.b.view(path)
path_strip = path

3
couchpotato/core/plugins/manage.py

@ -251,8 +251,7 @@ class Manage(Plugin):
def directories(self):
try:
if self.conf('library', default = '').strip():
return splitString(self.conf('library', default = ''), '::')
return self.conf('library', default = [])
except:
pass

141
couchpotato/core/settings.py

@ -7,7 +7,6 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import mergeDicts, tryInt, tryFloat
from couchpotato.core.softchroot import SoftChroot
class Settings(object):
@ -58,6 +57,7 @@ class Settings(object):
self.file = None
self.p = None
self.log = None
self.directories_delimiter = "::"
def setFile(self, config_file):
self.file = config_file
@ -93,6 +93,17 @@ class Settings(object):
for option_name, option in options.items():
self.setDefault(section_name, option_name, option.get('default', ''))
# Set UI-meta for option (hidden/ro/rw)
if option.get('ui-meta'):
value = option.get('ui-meta')
if value:
value = value.lower()
if value in ['hidden', 'rw', 'ro']:
meta_option_name = option_name + self.optionMetaSuffix()
self.setDefault(section_name, meta_option_name, value)
else:
self.log.warning('Wrong value for option %s.%s : ui-meta can not be equal to "%s"', (section_name, option_name, value))
# Migrate old settings from old location to the new location
if option.get('migrate_from'):
if self.p.has_option(option.get('migrate_from'), option_name):
@ -114,7 +125,6 @@ class Settings(object):
self.log.warning('set::option "%s.%s" cancelled, since it is a META option', (section, option))
return None
return self.p.set(section, option, value)
def get(self, option = '', section = 'core', default = None, type = None):
@ -123,9 +133,7 @@ class Settings(object):
return None
try:
try: type = self.types[section][option]
except: type = 'unicode' if not type else type
type = self.getType(section, option)
if hasattr(self, 'get%s' % type.capitalize()):
return getattr(self, 'get%s' % type.capitalize())(section, option)
@ -168,12 +176,21 @@ class Settings(object):
except:
return tryFloat(self.p.get(section, option))
def getDirectories(self, section, option):
value = self.p.get(section, option)
if value:
return map(str.strip, str.split(value, self.directories_delimiter))
return []
def getUnicode(self, section, option):
value = self.p.get(section, option).decode('unicode_escape')
return toUnicode(value).strip()
def getValues(self):
from couchpotato.environment import Env
values = {}
soft_chroot = Env.get('softchroot')
# TODO : There is two commented "continue" blocks (# COMMENTED_SKIPPING). They both are good...
# ... but, they omit output of values of hidden and non-readable options
@ -198,13 +215,24 @@ class Settings(object):
#if not self.isOptionReadable(section, option_name):
# continue
is_password = False
try: is_password = self.types[section][option_name] == 'password'
except: pass
value = self.get(option_name, section)
is_password = self.getType(section, option_name) == 'password'
if is_password and value:
value = len(value) * '*'
values[section][option_name] = self.get(option_name, section)
if is_password and values[section][option_name]:
values[section][option_name] = len(values[section][option_name]) * '*'
# chrootify directory before sending to UI:
if (self.getType(section, option_name) == 'directory') and value:
try: value = soft_chroot.abs2chroot(value)
except: value = ""
# chrootify directories before sending to UI:
if (self.getType(section, option_name) == 'directories'):
if (not value):
value = []
try : value = map(soft_chroot.abs2chroot, value)
except : value = []
values[section][option_name] = value
return values
@ -212,8 +240,6 @@ class Settings(object):
with open(self.file, 'wb') as configfile:
self.p.write(configfile)
self.log.debug('Saved settings')
def addSection(self, section):
if not self.p.has_section(section):
self.p.add_section(section)
@ -228,6 +254,12 @@ class Settings(object):
self.types[section][option] = type
def getType(self, section, option):
type = None
try: type = self.types[section][option]
except: type = 'unicode' if not type else type
return type
def addOptions(self, section_name, options):
# no additional actions (related to ro-rw options) are required here
if not self.options.get(section_name):
@ -243,36 +275,42 @@ class Settings(object):
res = {}
if isinstance(self.options, dict):
for section_key in self.options.keys():
section = self.options[section_key]
section_name = section.get('name') if 'name' in section else section_key
if self.isSectionReadable(section_name) and isinstance(section, dict):
s = {}
sg = []
for section_field in section:
if section_field.lower() != 'groups':
s[section_field] = section[section_field]
else:
groups = section['groups']
for group in groups:
g = {}
go = []
for group_field in group:
if group_field.lower() != 'options':
g[group_field] = group[group_field]
else:
for option in group[group_field]:
option_name = option.get('name')
if self.isOptionReadable(section_name, option_name):
go.append(option)
option['writable'] = self.isOptionWritable(section_name, option_name)
if len(go)>0:
g['options'] = go
sg.append(g)
if len(sg)>0:
s['groups'] = sg
res[section_key] = s
# it is required to filter invisible options for UI, but also we should
# preserve original tree for server's purposes.
# So, next loops do one thing: copy options to res and in the process
# 1. omit NON-READABLE (for UI) options, and
# 2. put flags on READONLY options
for section_key in self.options.keys():
section_orig = self.options[section_key]
section_name = section_orig.get('name') if 'name' in section_orig else section_key
if self.isSectionReadable(section_name):
section_copy = {}
section_copy_groups = []
for section_field in section_orig:
if section_field.lower() != 'groups':
section_copy[section_field] = section_orig[section_field]
else:
for group_orig in section_orig['groups']:
group_copy = {}
group_copy_options = []
for group_field in group_orig:
if group_field.lower() != 'options':
group_copy[group_field] = group_orig[group_field]
else:
for option in group_orig[group_field]:
option_name = option.get('name')
# You should keep in mind, that READONLY = !IS_WRITABLE
# and IS_READABLE is a different thing
if self.isOptionReadable(section_name, option_name):
group_copy_options.append(option)
if not self.isOptionWritable(section_name, option_name):
option['readonly'] = True
if len(group_copy_options)>0:
group_copy['options'] = group_copy_options
section_copy_groups.append(group_copy)
if len(section_copy_groups)>0:
section_copy['groups'] = section_copy_groups
res[section_key] = section_copy
return res
@ -288,10 +326,19 @@ class Settings(object):
option = kwargs.get('name')
value = kwargs.get('value')
if (section in self.types) and (option in self.types[section]) and (self.types[section][option] == 'directory'):
soft_chroot_dir = self.get('soft_chroot', default = None)
soft_chroot = SoftChroot(soft_chroot_dir)
value = soft_chroot.add(str(value))
from couchpotato.environment import Env
soft_chroot = Env.get('softchroot')
if self.getType(section, option) == 'directory':
value = soft_chroot.chroot2abs(value)
if self.getType(section, option) == 'directories':
import json
value = json.loads(value)
if not (value and isinstance(value, list)):
value = []
value = map(soft_chroot.chroot2abs, value)
value = self.directories_delimiter.join(value)
# See if a value handler is attached, use that as value
new_value = fireEvent('setting.save.%s.%s' % (section, option), value, single = True)

122
couchpotato/core/softchroot.py

@ -2,39 +2,99 @@ import os
import sys
class SoftChrootInitError(IOError):
"""Error during soft-chroot initialization"""
pass
class SoftChroot:
def __init__(self, chdir):
self.enabled = False
"""Soft Chroot module
Provides chroot feature for interation with Web-UI. Since it is not real chroot, so the name is SOFT CHROOT.
The module prevents access to entire file-system, allowing access only to subdirs of SOFT-CHROOT directory.
"""
def __init__(self):
self.enabled = None
self.chdir = None
def initialize(self, chdir):
""" initialize module, by setting soft-chroot-directory
Sets soft-chroot directory and 'enabled'-flag
Args:
self (SoftChroot) : self
chdir (string) : absolute path to soft-chroot
Raises:
SoftChrootInitError: when chdir doesn't exist
"""
orig_chdir = chdir
if chdir:
chdir = chdir.strip()
self.chdir = chdir
if (chdir):
# enabling soft-chroot:
if not os.path.isdir(chdir):
raise SoftChrootInitError(2, 'SOFT-CHROOT is requested, but the folder doesn\'t exist', orig_chdir)
if None != self.chdir:
self.chdir = self.chdir.strip()
self.chdir = self.chdir.rstrip(os.path.sep) + os.path.sep
self.enabled = True
self.chdir = chdir.rstrip(os.path.sep) + os.path.sep
else:
self.enabled = False
def get_chroot(self):
"""Returns root in chrooted environment
Raises:
RuntimeError: when `SoftChroot` is not initialized OR enabled
"""
if None == self.enabled:
raise RuntimeError('SoftChroot is not initialized')
if not self.enabled:
raise RuntimeError('SoftChroot is not enabled')
return self.chdir
def is_root_abs(self, abspath):
if not self.enabled:
raise Exception('chroot disabled')
""" Checks whether absolute path @abspath is the root in the soft-chrooted environment"""
if None == self.enabled:
raise RuntimeError('SoftChroot is not initialized')
if None == abspath:
return False
raise ValueError('abspath can not be None')
if not self.enabled:
# if not chroot environment : check, whether parent is the same dir:
parent = os.path.dirname(abspath.rstrip(os.path.sep))
return parent==abspath
# in soft-chrooted env: check, that path == chroot
path = abspath.rstrip(os.path.sep) + os.path.sep
return self.chdir == path
def is_subdir(self, path):
def is_subdir(self, abspath):
""" Checks whether @abspath is subdir (on any level) of soft-chroot"""
if None == self.enabled:
raise RuntimeError('SoftChroot is not initialized')
if None == abspath:
return False
if not self.enabled:
return True
if None == path:
return False
if not abspath.endswith(os.path.sep):
abspath += os.path.sep
if not path.endswith(os.path.sep):
path += os.path.sep
return abspath.startswith(self.chdir)
return path.startswith(self.chdir)
def chroot2abs(self, path):
""" Converts chrooted path to absolute path"""
def add(self, path):
if None == self.enabled:
raise RuntimeError('SoftChroot is not initialized')
if not self.enabled:
return path
@ -42,23 +102,33 @@ class SoftChroot:
return self.chdir
if not path.startswith(os.path.sep):
raise ValueError("path must starts with '/'")
path = os.path.sep + path
return self.chdir[:-1] + path
def cut(self, path):
if not self.enabled:
return path
def abs2chroot(self, path, force = False):
""" Converts absolute path to chrooted path"""
if None == path or 0==len(path):
if None == self.enabled:
raise RuntimeError('SoftChroot is not initialized')
if None == path:
raise ValueError('path is empty')
if not self.enabled:
return path
if path == self.chdir.rstrip(os.path.sep):
return '/'
resulst = None
if not path.startswith(self.chdir):
raise ValueError("path must starts with 'chdir'")
l = len(self.chdir)-1
return path[l:]
if (force):
result = self.get_chroot()
else:
raise ValueError("path must starts with 'chdir': %s" % path)
else:
l = len(self.chdir)-1
result = path[l:]
return result

121
couchpotato/core/softchroot_test.py

@ -0,0 +1,121 @@
import sys
import os
import logging
import unittest
from unittest import TestCase
#from mock import MagicMock
from couchpotato.core.softchroot import SoftChroot
CHROOT_DIR = '/tmp/'
class SoftChrootNonInitialized(TestCase):
def setUp(self):
self.b = SoftChroot()
def test_is_root_abs(self):
with self.assertRaises(RuntimeError):
self.b.is_root_abs('1')
def test_is_subdir(self):
with self.assertRaises(RuntimeError):
self.b.is_subdir('1')
def test_chroot2abs(self):
with self.assertRaises(RuntimeError):
self.b.chroot2abs('1')
def test_abs2chroot(self):
with self.assertRaises(RuntimeError):
self.b.abs2chroot('1')
def test_get_root(self):
with self.assertRaises(RuntimeError):
self.b.get_chroot()
class SoftChrootNOTEnabledTest(TestCase):
def setUp(self):
self.b = SoftChroot()
self.b.initialize(None)
def test_get_root(self):
with self.assertRaises(RuntimeError):
self.b.get_chroot()
def test_chroot2abs_noleading_slash(self):
path = 'no_leading_slash'
self.assertEqual( self.b.chroot2abs(path), path )
def test_chroot2abs(self):
self.assertIsNone( self.b.chroot2abs(None), None )
self.assertEqual( self.b.chroot2abs(''), '' )
self.assertEqual( self.b.chroot2abs('/asdf'), '/asdf' )
def test_abs2chroot_raise_on_empty(self):
with self.assertRaises(ValueError):
self.b.abs2chroot(None)
def test_abs2chroot(self):
self.assertEqual( self.b.abs2chroot(''), '' )
self.assertEqual( self.b.abs2chroot('/asdf'), '/asdf' )
self.assertEqual( self.b.abs2chroot('/'), '/' )
def test_get_root(self):
with self.assertRaises(RuntimeError):
self.b.get_chroot()
class SoftChrootEnabledTest(TestCase):
def setUp(self):
self.b = SoftChroot()
self.b.initialize(CHROOT_DIR)
def test_enabled(self):
self.assertTrue( self.b.enabled)
def test_is_subdir(self):
self.assertFalse( self.b.is_subdir('') )
self.assertFalse( self.b.is_subdir(None) )
self.assertTrue( self.b.is_subdir(CHROOT_DIR) )
noslash = CHROOT_DIR[:-1]
self.assertTrue( self.b.is_subdir(noslash) )
self.assertTrue( self.b.is_subdir(CHROOT_DIR + 'come') )
def test_is_root_abs_none(self):
with self.assertRaises(ValueError):
self.assertFalse( self.b.is_root_abs(None) )
def test_is_root_abs(self):
self.assertFalse( self.b.is_root_abs('') )
self.assertTrue( self.b.is_root_abs(CHROOT_DIR) )
noslash = CHROOT_DIR[:-1]
self.assertTrue( self.b.is_root_abs(noslash) )
self.assertFalse( self.b.is_root_abs(CHROOT_DIR + 'come') )
def test_chroot2abs_noleading_slash(self):
path = 'no_leading_slash'
path_sl = CHROOT_DIR + path
#with self.assertRaises(ValueError):
# self.b.chroot2abs('no_leading_slash')
self.assertEqual( self.b.chroot2abs(path), path_sl )
def test_chroot2abs(self):
self.assertEqual( self.b.chroot2abs(None), CHROOT_DIR )
self.assertEqual( self.b.chroot2abs(''), CHROOT_DIR )
self.assertEqual( self.b.chroot2abs('/asdf'), CHROOT_DIR + 'asdf' )
def test_abs2chroot_raise_on_empty(self):
with self.assertRaises(ValueError): self.b.abs2chroot(None)
with self.assertRaises(ValueError): self.b.abs2chroot('')
def test_abs2chroot(self):
self.assertEqual( self.b.abs2chroot(CHROOT_DIR + 'asdf'), '/asdf' )
self.assertEqual( self.b.abs2chroot(CHROOT_DIR), '/' )
self.assertEqual( self.b.abs2chroot(CHROOT_DIR.rstrip(os.path.sep)), '/' )
def test_get_root(self):
self.assertEqual( self.b.get_chroot(), CHROOT_DIR )

54
couchpotato/core/test_softchroot.py

@ -1,54 +0,0 @@
import sys
import os
import logging
import unittest
from unittest import TestCase
#from mock import MagicMock
from couchpotato.core.softchroot import SoftChroot
CHROOT_DIR = '/tmp/'
class SoftChrootEnabledTest(TestCase):
def setUp(self):
self.b = SoftChroot(CHROOT_DIR)
def test_enabled(self):
self.assertTrue( self.b.enabled)
def test_is_subdir(self):
self.assertFalse( self.b.is_subdir('') )
self.assertFalse( self.b.is_subdir(None) )
self.assertTrue( self.b.is_subdir(CHROOT_DIR) )
noslash = CHROOT_DIR[:-1]
self.assertTrue( self.b.is_subdir(noslash) )
self.assertTrue( self.b.is_subdir(CHROOT_DIR + 'come') )
def test_is_root_abs(self):
self.assertFalse( self.b.is_root_abs('') )
self.assertFalse( self.b.is_root_abs(None) )
self.assertTrue( self.b.is_root_abs(CHROOT_DIR) )
noslash = CHROOT_DIR[:-1]
self.assertTrue( self.b.is_root_abs(noslash) )
self.assertFalse( self.b.is_root_abs(CHROOT_DIR + 'come') )
def test_add(self):
with self.assertRaises(ValueError):
self.b.add('no_leading_slash')
self.assertEqual( self.b.add(None), CHROOT_DIR )
self.assertEqual( self.b.add(''), CHROOT_DIR )
self.assertEqual( self.b.add('/asdf'), CHROOT_DIR + 'asdf' )
def test_cut(self):
with self.assertRaises(ValueError): self.b.cut(None)
with self.assertRaises(ValueError): self.b.cut('')
self.assertEqual( self.b.cut(CHROOT_DIR + 'asdf'), '/asdf' )
self.assertEqual( self.b.cut(CHROOT_DIR), '/' )
self.assertEqual( self.b.cut(CHROOT_DIR.rstrip(os.path.sep)), '/' )

2
couchpotato/environment.py

@ -5,6 +5,7 @@ from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.loader import Loader
from couchpotato.core.settings import Settings
from couchpotato.core.softchroot import SoftChroot
class Env(object):
@ -19,6 +20,7 @@ class Env(object):
_settings = Settings()
_database = Database()
_loader = Loader()
_softchroot = SoftChroot()
_cache = None
_options = None
_args = None

19
couchpotato/environment_test.py

@ -0,0 +1,19 @@
import unittest
from unittest import TestCase
import mock
from couchpotato.environment import Env
class EnvironmentBaseTest(TestCase):
def test_appname(self):
self.assertEqual('CouchPotato', Env.get('appname'))
def test_set_get_appname(self):
x = 'NEWVALUE'
Env.set('appname', x)
self.assertEqual(x, Env.get('appname'))
def test_get_softchroot(self):
from couchpotato.core.softchroot import SoftChroot
sc = Env.get('softchroot')
self.assertIsInstance(sc, SoftChroot)

16
couchpotato/runner.py

@ -23,7 +23,7 @@ import requests
from requests.packages.urllib3 import disable_warnings
from tornado.httpserver import HTTPServer
from tornado.web import Application, StaticFileHandler, RedirectHandler
from couchpotato.core.softchroot import SoftChrootInitError
def getOptions(args):
@ -218,13 +218,13 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
# Check soft-chroot dir exists:
try:
soft_chroot = Env.setting('soft_chroot', section = 'core', default = None, type='unicode' )
if (None != soft_chroot):
soft_chroot = soft_chroot.strip()
if (len(soft_chroot)>0) and (not os.path.isdir(soft_chroot)):
log.error('SOFT-CHROOT is defined, but the folder doesn\'t exist')
return
# Load Soft-Chroot
soft_chroot = Env.get('softchroot')
soft_chroot_dir = Env.setting('soft_chroot', section = 'core', default = None, type='unicode' )
soft_chroot.initialize(soft_chroot_dir)
except SoftChrootInitError as exc:
log.error(exc)
return
except:
log.error('Unable to check whether SOFT-CHROOT is defined')
return

7
couchpotato/static/scripts/combined.base.min.js

@ -1342,7 +1342,7 @@ var OptionBase = new Class({
self.section = section;
self.name = name;
self.value = self.previous_value = value;
self.read_only = !(options && options.writable);
self.read_only = !(options && !options.readonly);
self.createBase();
self.create();
self.createHint();
@ -1784,12 +1784,11 @@ Option.Directory = new Class({
Option.Directories = new Class({
Extends: Option.String,
directories: [],
delimiter: "::",
afterInject: function() {
var self = this;
self.el.setStyle("display", "none");
self.directories = [];
self.getValue().split(self.delimiter).each(function(value) {
self.getSettingValue().each(function(value) {
self.addDirectory(value);
});
self.addDirectory();
@ -1833,7 +1832,7 @@ Option.Directories = new Class({
dirs.include(dir.getValue());
} else $(dir).addClass("is_empty");
});
self.input.set("value", dirs.join(self.delimiter));
self.input.set("value", JSON.encode(dirs));
self.input.fireEvent("change");
self.addDirectory();
}

13
couchpotato/static/scripts/page/settings.js

@ -343,7 +343,7 @@ var OptionBase = new Class({
self.section = section;
self.name = name;
self.value = self.previous_value = value;
self.read_only = !(options && options.writable);
self.read_only = !(options && !options.readonly);
self.createBase();
self.create();
@ -991,7 +991,6 @@ Option.Directories = new Class({
Extends: Option.String,
directories: [],
delimiter: '::',
afterInject: function(){
var self = this;
@ -999,9 +998,11 @@ Option.Directories = new Class({
self.el.setStyle('display', 'none');
self.directories = [];
self.getValue().split(self.delimiter).each(function(value){
self.getSettingValue().each(function(value){
self.addDirectory(value);
});
self.addDirectory();
},
@ -1025,7 +1026,7 @@ Option.Directories = new Class({
else
$(dir).inject(dirs.getLast(), 'after');
// Replace some properties
// TODO : Replace some properties
dir.save = self.saveItems.bind(self);
$(dir).getElement('label').set('text', 'Movie Folder');
$(dir).getElement('.formHint').destroy();
@ -1068,14 +1069,12 @@ Option.Directories = new Class({
$(dir).addClass('is_empty');
});
self.input.set('value', dirs.join(self.delimiter));
self.input.set('value', JSON.encode(dirs) );
self.input.fireEvent('change');
self.addDirectory();
}
});
Option.Choice = new Class({

12
couchpotato/static/style/combined.min.css

@ -54,7 +54,7 @@
.search_form .results_container .results .media_result .options .add a{color:#FFF}
.search_form .results_container .results .media_result .options .button{display:block;background:#ac0000;text-align:center;margin:0}
.dark .search_form .results_container .results .media_result .options .button{background:#f85c22}
.search_form .results_container .results .media_result .options .message{font-size:20px;color:#fff}
.search_form .results_container .results .media_result .options .message{font-size:20px;color:#FFF}
.search_form .results_container .results .media_result .thumbnail{width:30px;min-height:100%;display:block;margin:0;vertical-align:top}
.search_form .results_container .results .media_result .data{position:absolute;height:100%;top:0;left:30px;right:0;cursor:pointer;border-top:1px solid rgba(255,255,255,.08);transition:all .4s cubic-bezier(.9,0,.1,1);will-change:transform;-webkit-transform:translateX(0) rotateZ(360deg);transform:translateX(0) rotateZ(360deg);background:#FFF}
.dark .search_form .results_container .results .media_result .data{background:#2d2d2d;border-color:rgba(255,255,255,.08)}
@ -95,17 +95,15 @@
.page.home .search_form .wrapper .input input{padding-right:44px;font-size:1em}
.page.home .search_form .wrapper .results_container{top:44px;min-height:44px}
}
@media (min-width:480px){.page.home .search_form .wrapper .results_container .results{max-height:400px}
@media (min-width:480px){.page.home .search_form .wrapper .results_container .results .media_result .data,.page.home .search_form .wrapper .results_container .results .media_result .options{left:40px}
.page.home .search_form .wrapper .results_container .results{max-height:400px}
.page.home .search_form .wrapper .results_container .results .media_result{height:66px}
.page.home .search_form .wrapper .results_container .results .media_result .thumbnail{width:40px}
.page.home .search_form .wrapper .results_container .results .media_result .options{left:40px}
.page.home .search_form .wrapper .results_container .results .media_result .options .title{margin-right:5px;width:320px}
}
@media (min-width:480px) and (max-width:480px){.page.home .search_form .wrapper .results_container .results .media_result{height:44px}
.page.home .search_form .wrapper .results_container .results .media_result .options .title{width:140px;margin-right:2px}
}
@media (min-width:480px){.page.home .search_form .wrapper .results_container .results .media_result .data{left:40px}
}
@media (max-width:480px){.page.home .search_form .wrapper .results_container .results .media_result{height:44px}
.page.home .search_form .wrapper .results_container .results .media_result .options .title{width:140px;margin-right:2px}
}
@ -811,7 +809,7 @@ input[type=text],textarea{-webkit-appearance:none}
.mask .message,.mask .spinner{position:absolute;top:50%;left:50%}
.mask .message{color:#FFF;text-align:center;width:320px;margin:-49px 0 0 -160px;font-size:16px}
.mask .message h1{font-size:1.5em}
.mask .spinner{width:22px;height:22px;display:block;background:#fff;margin-top:-11px;margin-left:-11px;outline:transparent solid 1px;-webkit-animation:rotating 2.5s cubic-bezier(.9,0,.1,1) infinite normal;animation:rotating 2.5s cubic-bezier(.9,0,.1,1) infinite normal;-webkit-transform:scale(0);transform:scale(0)}
.mask .spinner{width:22px;height:22px;display:block;background:#FFF;margin-top:-11px;margin-left:-11px;outline:transparent solid 1px;-webkit-animation:rotating 2.5s cubic-bezier(.9,0,.1,1) infinite normal;animation:rotating 2.5s cubic-bezier(.9,0,.1,1) infinite normal;-webkit-transform:scale(0);transform:scale(0)}
.mask.with_message .spinner{margin-top:-88px}
.mask.show{pointer-events:auto;opacity:1}
.mask.show .spinner{-webkit-transform:scale(1);transform:scale(1)}
@ -935,7 +933,7 @@ input[type=text],textarea{-webkit-appearance:none}
.page.settings .option_list h2 .hint{font-weight:300}
.page.settings .combined_table{margin-top:20px}
.page.settings .combined_table .head{margin:0 10px 0 46px;font-size:.8em}
.page.settings .combined_table .head abbr{display:inline-block;font-weight:700;border-bottom:1px dotted #fff;line-height:140%;cursor:help;margin-right:10px;text-align:center}
.page.settings .combined_table .head abbr{display:inline-block;font-weight:700;border-bottom:1px dotted #FFF;line-height:140%;cursor:help;margin-right:10px;text-align:center}
.page.settings .combined_table .head abbr:first-child{display:none}
.page.settings .combined_table input{min-width:0!important;display:inline-block;margin-right:10px}
.page.settings .combined_table .automation_ids,.page.settings .combined_table .automation_urls,.page.settings .combined_table .host{width:200px}

5
py-dev-requirements.txt

@ -0,0 +1,5 @@
nose
rednose
nose-htmloutput
coverage
coveralls
Loading…
Cancel
Save