forked from openlp/openlp
Moved some import related stuff out of the db module
This commit is contained in:
parent
80cdc35433
commit
223b198dc8
@ -23,23 +23,21 @@
|
||||
from lxml import etree, objectify
|
||||
from zipfile import is_zipfile
|
||||
|
||||
from openlp.core.common import OpenLPMixin, languages, translate
|
||||
from openlp.core.common import OpenLPMixin, Registry, RegistryProperties, languages, translate
|
||||
from openlp.core.lib import ValidationError
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB
|
||||
from openlp.plugins.bibles.lib.db import AlternativeBookNamesDB, BibleDB, BiblesResourcesDB
|
||||
|
||||
|
||||
class BibleImport(OpenLPMixin, BibleDB):
|
||||
class BibleImport(OpenLPMixin, RegistryProperties, BibleDB):
|
||||
"""
|
||||
Helper class to import bibles from a third party source into OpenLP
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.filename = kwargs['filename'] if 'filename' in kwargs else None
|
||||
|
||||
def set_current_chapter(self, book_name, chapter_name):
|
||||
self.wizard.increment_progress_bar(translate('BiblesPlugin.OsisImport', 'Importing {book} {chapter}...')
|
||||
.format(book=book_name, chapter=chapter_name))
|
||||
self.wizard = None
|
||||
Registry().register_function('openlp_stop_wizard', self.stop_import)
|
||||
|
||||
@staticmethod
|
||||
def is_compressed(file):
|
||||
@ -56,6 +54,45 @@ class BibleImport(OpenLPMixin, BibleDB):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_book_ref_id_by_name(self, book, maxbooks, language_id=None):
|
||||
self.log_debug('BibleDB.get_book_ref_id_by_name:("{book}", "{lang}")'.format(book=book, lang=language_id))
|
||||
book_id = None
|
||||
if BiblesResourcesDB.get_book(book, True):
|
||||
book_temp = BiblesResourcesDB.get_book(book, True)
|
||||
book_id = book_temp['id']
|
||||
elif BiblesResourcesDB.get_alternative_book_name(book):
|
||||
book_id = BiblesResourcesDB.get_alternative_book_name(book)
|
||||
elif AlternativeBookNamesDB.get_book_reference_id(book):
|
||||
book_id = AlternativeBookNamesDB.get_book_reference_id(book)
|
||||
else:
|
||||
from openlp.plugins.bibles.forms import BookNameForm
|
||||
book_name = BookNameForm(self.wizard)
|
||||
if book_name.exec(book, self.get_books(), maxbooks):
|
||||
book_id = book_name.book_id
|
||||
if book_id:
|
||||
AlternativeBookNamesDB.create_alternative_book_name(
|
||||
book, book_id, language_id)
|
||||
return book_id
|
||||
|
||||
def get_language(self, bible_name=None):
|
||||
"""
|
||||
If no language is given it calls a dialog window where the user could select the bible language.
|
||||
Return the language id of a bible.
|
||||
|
||||
:param bible_name: The language the bible is.
|
||||
"""
|
||||
self.log_debug('BibleImpoer.get_language()')
|
||||
from openlp.plugins.bibles.forms import LanguageForm
|
||||
language_id = None
|
||||
language_form = LanguageForm(self.wizard)
|
||||
if language_form.exec(bible_name):
|
||||
combo_box = language_form.language_combo_box
|
||||
language_id = combo_box.itemData(combo_box.currentIndex())
|
||||
if not language_id:
|
||||
return None
|
||||
self.save_meta('language_id', language_id)
|
||||
return language_id
|
||||
|
||||
def get_language_id(self, file_language=None, bible_name=None):
|
||||
"""
|
||||
Get the language_id for the language of the bible. Fallback to user input if we cannot do this pragmatically.
|
||||
@ -138,6 +175,28 @@ class BibleImport(OpenLPMixin, BibleDB):
|
||||
.format(file_name=e.filename, error=e.strerror))
|
||||
return None
|
||||
|
||||
def register(self, wizard):
|
||||
"""
|
||||
This method basically just initialises the database. It is called from the Bible Manager when a Bible is
|
||||
imported. Descendant classes may want to override this method to supply their own custom
|
||||
initialisation as well.
|
||||
|
||||
:param wizard: The actual Qt wizard form.
|
||||
"""
|
||||
self.wizard = wizard
|
||||
return self.name
|
||||
|
||||
def set_current_chapter(self, book_name, chapter_name):
|
||||
self.wizard.increment_progress_bar(translate('BiblesPlugin.OsisImport', 'Importing {book} {chapter}...')
|
||||
.format(book=book_name, chapter=chapter_name))
|
||||
|
||||
def stop_import(self):
|
||||
"""
|
||||
Stops the import of the Bible.
|
||||
"""
|
||||
self.log_debug('Stopping import')
|
||||
self.stop_import_flag = True
|
||||
|
||||
def validate_xml_file(self, filename, tag):
|
||||
"""
|
||||
Validate the supplied file
|
||||
|
@ -33,7 +33,7 @@ from sqlalchemy.exc import OperationalError
|
||||
from sqlalchemy.orm import class_mapper, mapper, relation
|
||||
from sqlalchemy.orm.exc import UnmappedClassError
|
||||
|
||||
from openlp.core.common import Registry, RegistryProperties, AppLocation, translate, clean_filename
|
||||
from openlp.core.common import AppLocation, translate, clean_filename
|
||||
from openlp.core.lib.db import BaseModel, init_db, Manager
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.plugins.bibles.lib import upgrade
|
||||
@ -106,7 +106,7 @@ def init_schema(url):
|
||||
return session
|
||||
|
||||
|
||||
class BibleDB(Manager, RegistryProperties):
|
||||
class BibleDB(Manager):
|
||||
"""
|
||||
This class represents a database-bound Bible. It is used as a base class for all the custom importers, so that
|
||||
the can implement their own import methods, but benefit from the database methods in here via inheritance,
|
||||
@ -153,15 +153,6 @@ class BibleDB(Manager, RegistryProperties):
|
||||
self.get_name()
|
||||
if 'path' in kwargs:
|
||||
self.path = kwargs['path']
|
||||
self.wizard = None
|
||||
Registry().register_function('openlp_stop_wizard', self.stop_import)
|
||||
|
||||
def stop_import(self):
|
||||
"""
|
||||
Stops the import of the Bible.
|
||||
"""
|
||||
log.debug('Stopping import')
|
||||
self.stop_import_flag = True
|
||||
|
||||
def get_name(self):
|
||||
"""
|
||||
@ -171,17 +162,6 @@ class BibleDB(Manager, RegistryProperties):
|
||||
self.name = version_name.value if version_name else None
|
||||
return self.name
|
||||
|
||||
def register(self, wizard):
|
||||
"""
|
||||
This method basically just initialises the database. It is called from the Bible Manager when a Bible is
|
||||
imported. Descendant classes may want to override this method to supply their own custom
|
||||
initialisation as well.
|
||||
|
||||
:param wizard: The actual Qt wizard form.
|
||||
"""
|
||||
self.wizard = wizard
|
||||
return self.name
|
||||
|
||||
def create_book(self, name, bk_ref_id, testament=1):
|
||||
"""
|
||||
Add a book to the database.
|
||||
@ -306,26 +286,6 @@ class BibleDB(Manager, RegistryProperties):
|
||||
log.debug('BibleDB.get_book_by_book_ref_id("{ref}")'.format(ref=ref_id))
|
||||
return self.get_object_filtered(Book, Book.book_reference_id.like(ref_id))
|
||||
|
||||
def get_book_ref_id_by_name(self, book, maxbooks, language_id=None):
|
||||
log.debug('BibleDB.get_book_ref_id_by_name:("{book}", "{lang}")'.format(book=book, lang=language_id))
|
||||
book_id = None
|
||||
if BiblesResourcesDB.get_book(book, True):
|
||||
book_temp = BiblesResourcesDB.get_book(book, True)
|
||||
book_id = book_temp['id']
|
||||
elif BiblesResourcesDB.get_alternative_book_name(book):
|
||||
book_id = BiblesResourcesDB.get_alternative_book_name(book)
|
||||
elif AlternativeBookNamesDB.get_book_reference_id(book):
|
||||
book_id = AlternativeBookNamesDB.get_book_reference_id(book)
|
||||
else:
|
||||
from openlp.plugins.bibles.forms import BookNameForm
|
||||
book_name = BookNameForm(self.wizard)
|
||||
if book_name.exec(book, self.get_books(), maxbooks):
|
||||
book_id = book_name.book_id
|
||||
if book_id:
|
||||
AlternativeBookNamesDB.create_alternative_book_name(
|
||||
book, book_id, language_id)
|
||||
return book_id
|
||||
|
||||
def get_book_ref_id_by_localised_name(self, book, language_selection):
|
||||
"""
|
||||
Return the id of a named book.
|
||||
@ -462,25 +422,6 @@ class BibleDB(Manager, RegistryProperties):
|
||||
return 0
|
||||
return count
|
||||
|
||||
def get_language(self, bible_name=None):
|
||||
"""
|
||||
If no language is given it calls a dialog window where the user could select the bible language.
|
||||
Return the language id of a bible.
|
||||
|
||||
:param bible_name: The language the bible is.
|
||||
"""
|
||||
log.debug('BibleDB.get_language()')
|
||||
from openlp.plugins.bibles.forms import LanguageForm
|
||||
language_id = None
|
||||
language_form = LanguageForm(self.wizard)
|
||||
if language_form.exec(bible_name):
|
||||
combo_box = language_form.language_combo_box
|
||||
language_id = combo_box.itemData(combo_box.currentIndex())
|
||||
if not language_id:
|
||||
return None
|
||||
self.save_meta('language_id', language_id)
|
||||
return language_id
|
||||
|
||||
def dump_bible(self):
|
||||
"""
|
||||
Utility debugging method to dump the contents of a bible.
|
||||
|
@ -62,6 +62,9 @@ class TestBibleImport(TestCase):
|
||||
side_effect=lambda module, string_to_translate, *args: string_to_translate)
|
||||
self.addCleanup(self.translate_patcher.stop)
|
||||
self.mocked_translate = self.translate_patcher.start()
|
||||
self.registry_patcher = patch('openlp.plugins.bibles.lib.bibleimport.Registry')
|
||||
self.addCleanup(self.registry_patcher.stop)
|
||||
self.registry_patcher.start()
|
||||
|
||||
def init_kwargs_none_test(self):
|
||||
"""
|
||||
@ -88,6 +91,54 @@ class TestBibleImport(TestCase):
|
||||
self.assertEqual(instance.filename, 'bible.xml')
|
||||
self.assertIsInstance(instance, BibleDB)
|
||||
|
||||
def get_language_canceled_test(self):
|
||||
"""
|
||||
Test the BibleImport.get_language method when the user rejects the dialog box
|
||||
"""
|
||||
# GIVEN: A mocked LanguageForm with an exec method which returns QtDialog.Rejected and an instance of BibleDB
|
||||
with patch.object(BibleDB, '_setup'), patch('openlp.plugins.bibles.forms.LanguageForm') as mocked_language_form:
|
||||
|
||||
# The integer value of QtDialog.Rejected is 0. Using the enumeration causes a seg fault for some reason
|
||||
mocked_language_form_instance = MagicMock(**{'exec.return_value': 0})
|
||||
mocked_language_form.return_value = mocked_language_form_instance
|
||||
instance = BibleImport(MagicMock())
|
||||
mocked_wizard = MagicMock()
|
||||
instance.wizard = mocked_wizard
|
||||
|
||||
# WHEN: Calling get_language()
|
||||
result = instance.get_language()
|
||||
|
||||
# THEN: get_language() should return False
|
||||
mocked_language_form.assert_called_once_with(mocked_wizard)
|
||||
mocked_language_form_instance.exec.assert_called_once_with(None)
|
||||
self.assertFalse(result, 'get_language() should return False if the user rejects the dialog box')
|
||||
|
||||
def get_language_accepted_test(self):
|
||||
"""
|
||||
Test the BibleImport.get_language method when the user accepts the dialog box
|
||||
"""
|
||||
# GIVEN: A mocked LanguageForm with an exec method which returns QtDialog.Accepted an instance of BibleDB and
|
||||
# a combobox with the selected item data as 10
|
||||
with patch.object(BibleDB, 'save_meta'), patch.object(BibleDB, '_setup'), \
|
||||
patch('openlp.plugins.bibles.forms.LanguageForm') as mocked_language_form:
|
||||
|
||||
# The integer value of QtDialog.Accepted is 1. Using the enumeration causes a seg fault for some reason
|
||||
mocked_language_form_instance = MagicMock(**{'exec.return_value': 1,
|
||||
'language_combo_box.itemData.return_value': 10})
|
||||
mocked_language_form.return_value = mocked_language_form_instance
|
||||
instance = BibleImport(MagicMock())
|
||||
mocked_wizard = MagicMock()
|
||||
instance.wizard = mocked_wizard
|
||||
|
||||
# WHEN: Calling get_language()
|
||||
result = instance.get_language('Bible Name')
|
||||
|
||||
# THEN: get_language() should return the id of the selected language in the combo box
|
||||
mocked_language_form.assert_called_once_with(mocked_wizard)
|
||||
mocked_language_form_instance.exec.assert_called_once_with('Bible Name')
|
||||
self.assertEqual(result, 10, 'get_language() should return the id of the language the user has chosen when '
|
||||
'they accept the dialog box')
|
||||
|
||||
def get_language_id_language_found_test(self):
|
||||
"""
|
||||
Test get_language_id() when called with a name found in the languages list
|
||||
@ -172,6 +223,38 @@ class TestBibleImport(TestCase):
|
||||
self.assertFalse(instance.save_meta.called)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def is_compressed_compressed_test(self):
|
||||
"""
|
||||
Test is_compressed when the 'file' being tested is compressed
|
||||
"""
|
||||
# GIVEN: An instance of BibleImport and a mocked is_zipfile which returns True
|
||||
with patch('openlp.plugins.bibles.lib.bibleimport.is_zipfile', return_value=True):
|
||||
instance = BibleImport(MagicMock())
|
||||
|
||||
# WHEN: Calling is_compressed
|
||||
result = instance.is_compressed('file.ext')
|
||||
|
||||
# THEN: Then critical_error_message_box should be called informing the user that the file is compressed and
|
||||
# True should be returned
|
||||
self.mocked_critical_error_message_box.assert_called_once_with(
|
||||
message='The file "file.ext" you supplied is compressed. You must decompress it before import.')
|
||||
self.assertTrue(result)
|
||||
|
||||
def is_compressed_not_compressed_test(self):
|
||||
"""
|
||||
Test is_compressed when the 'file' being tested is compressed
|
||||
"""
|
||||
# GIVEN: An instance of BibleImport and a mocked is_zipfile which returns True
|
||||
with patch('openlp.plugins.bibles.lib.bibleimport.is_zipfile', return_value=False):
|
||||
instance = BibleImport(MagicMock())
|
||||
|
||||
# WHEN: Calling is_compressed
|
||||
result = instance.is_compressed('file.ext')
|
||||
|
||||
# THEN: False should be returned and critical_error_message_box should not have been called
|
||||
self.assertFalse(result)
|
||||
self.assertFalse(self.mocked_critical_error_message_box.called)
|
||||
|
||||
def parse_xml_etree_test(self):
|
||||
"""
|
||||
Test BibleImport.parse_xml() when called with the use_objectify default value
|
||||
@ -259,27 +342,6 @@ class TestBibleImport(TestCase):
|
||||
# THEN: The result returned should contain the correct data
|
||||
self.assertEqual(etree.tostring(result), b'<root>\n <data>Testdatatokeep</data>\n <data/>\n</root>')
|
||||
|
||||
def parse_xml_file_file_not_found_exception_test(self):
|
||||
"""
|
||||
Test that validate_xml_file raises a ValidationError with an OpenSong root tag
|
||||
"""
|
||||
# GIVEN: A mocked open which raises a FileNotFoundError and an instance of BibleImporter
|
||||
exception = FileNotFoundError()
|
||||
exception.filename = 'file.tst'
|
||||
exception.strerror = 'No such file or directory'
|
||||
self.mocked_open.side_effect = exception
|
||||
importer = BibleImport(MagicMock(), path='.', name='.', filename='')
|
||||
|
||||
# WHEN: Calling parse_xml
|
||||
result = importer.parse_xml('file.tst')
|
||||
|
||||
# THEN: parse_xml should have caught the error, informed the user and returned None
|
||||
self.mocked_log.exception.assert_called_once_with('Opening file.tst failed.')
|
||||
self.mocked_critical_error_message_box.assert_called_once_with(
|
||||
title='An Error Occured When Opening A File',
|
||||
message='The following error occurred when trying to open\nfile.tst:\n\nNo such file or directory')
|
||||
self.assertIsNone(result)
|
||||
|
||||
def parse_xml_file_file_not_found_exception_test(self):
|
||||
"""
|
||||
Test that parse_xml handles a FileNotFoundError exception correctly
|
||||
@ -324,6 +386,20 @@ class TestBibleImport(TestCase):
|
||||
message='The following error occurred when trying to open\nfile.tst:\n\nPermission denied')
|
||||
self.assertIsNone(result)
|
||||
|
||||
def set_current_chapter_test(self):
|
||||
"""
|
||||
Test set_current_chapter
|
||||
"""
|
||||
# GIVEN: An instance of BibleImport and a mocked wizard
|
||||
importer = BibleImport(MagicMock(), path='.', name='.', filename='')
|
||||
importer.wizard = MagicMock()
|
||||
|
||||
# WHEN: Calling set_current_chapter
|
||||
importer.set_current_chapter('Book_Name', 'Chapter')
|
||||
|
||||
# THEN: Increment_progress_bar should have been called with a text string
|
||||
importer.wizard.increment_progress_bar.assert_called_once_with('Importing Book_Name Chapter...')
|
||||
|
||||
def validate_xml_file_compressed_file_test(self):
|
||||
"""
|
||||
Test that validate_xml_file raises a ValidationError when is_compressed returns True
|
||||
|
@ -48,7 +48,7 @@ class TestCSVImport(TestCase):
|
||||
self.manager_patcher = patch('openlp.plugins.bibles.lib.db.Manager')
|
||||
self.addCleanup(self.manager_patcher.stop)
|
||||
self.manager_patcher.start()
|
||||
self.registry_patcher = patch('openlp.plugins.bibles.lib.db.Registry')
|
||||
self.registry_patcher = patch('openlp.plugins.bibles.lib.bibleimport.Registry')
|
||||
self.addCleanup(self.registry_patcher.stop)
|
||||
self.registry_patcher.start()
|
||||
|
||||
|
@ -25,63 +25,9 @@ This module contains tests for the db submodule of the Bibles plugin.
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
from openlp.plugins.bibles.lib.db import BibleDB
|
||||
from tests.functional import MagicMock, patch
|
||||
|
||||
|
||||
class TestBibleDB(TestCase):
|
||||
"""
|
||||
Test the functions in the BibleDB class.
|
||||
"""
|
||||
|
||||
def test_get_language_canceled(self):
|
||||
"""
|
||||
Test the BibleDB.get_language method when the user rejects the dialog box
|
||||
"""
|
||||
# GIVEN: A mocked LanguageForm with an exec method which returns QtDialog.Rejected and an instance of BibleDB
|
||||
with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'),\
|
||||
patch('openlp.plugins.bibles.forms.LanguageForm') as mocked_language_form:
|
||||
|
||||
# The integer value of QtDialog.Rejected is 0. Using the enumeration causes a seg fault for some reason
|
||||
mocked_language_form_instance = MagicMock(**{'exec.return_value': 0})
|
||||
mocked_language_form.return_value = mocked_language_form_instance
|
||||
mocked_parent = MagicMock()
|
||||
instance = BibleDB(mocked_parent)
|
||||
mocked_wizard = MagicMock()
|
||||
instance.wizard = mocked_wizard
|
||||
|
||||
# WHEN: Calling get_language()
|
||||
result = instance.get_language()
|
||||
|
||||
# THEN: get_language() should return False
|
||||
mocked_language_form.assert_called_once_with(mocked_wizard)
|
||||
mocked_language_form_instance.exec.assert_called_once_with(None)
|
||||
self.assertFalse(result, 'get_language() should return False if the user rejects the dialog box')
|
||||
|
||||
def test_get_language_accepted(self):
|
||||
"""
|
||||
Test the BibleDB.get_language method when the user accepts the dialog box
|
||||
"""
|
||||
# GIVEN: A mocked LanguageForm with an exec method which returns QtDialog.Accepted an instance of BibleDB and
|
||||
# a combobox with the selected item data as 10
|
||||
with patch('openlp.plugins.bibles.lib.db.BibleDB._setup'), \
|
||||
patch('openlp.plugins.bibles.lib.db.BibleDB.save_meta'), \
|
||||
patch('openlp.plugins.bibles.forms.LanguageForm') as mocked_language_form:
|
||||
|
||||
# The integer value of QtDialog.Accepted is 1. Using the enumeration causes a seg fault for some reason
|
||||
mocked_language_form_instance = MagicMock(**{'exec.return_value': 1,
|
||||
'language_combo_box.itemData.return_value': 10})
|
||||
mocked_language_form.return_value = mocked_language_form_instance
|
||||
mocked_parent = MagicMock()
|
||||
instance = BibleDB(mocked_parent)
|
||||
mocked_wizard = MagicMock()
|
||||
instance.wizard = mocked_wizard
|
||||
|
||||
# WHEN: Calling get_language()
|
||||
result = instance.get_language('Bible Name')
|
||||
|
||||
# THEN: get_language() should return the id of the selected language in the combo box
|
||||
mocked_language_form.assert_called_once_with(mocked_wizard)
|
||||
mocked_language_form_instance.exec.assert_called_once_with('Bible Name')
|
||||
self.assertEqual(result, 10, 'get_language() should return the id of the language the user has chosen when '
|
||||
'they accept the dialog box')
|
||||
pass
|
||||
|
@ -49,7 +49,7 @@ class TestOsisImport(TestCase):
|
||||
self.find_and_create_book_patch = patch.object(BibleImport, 'find_and_create_book')
|
||||
self.addCleanup(self.find_and_create_book_patch.stop)
|
||||
self.mocked_find_and_create_book = self.find_and_create_book_patch.start()
|
||||
self.registry_patcher = patch('openlp.plugins.bibles.lib.db.Registry')
|
||||
self.registry_patcher = patch('openlp.plugins.bibles.lib.bibleimport.Registry')
|
||||
self.addCleanup(self.registry_patcher.stop)
|
||||
self.registry_patcher.start()
|
||||
self.manager_patcher = patch('openlp.plugins.bibles.lib.db.Manager')
|
||||
@ -409,7 +409,7 @@ class TestOsisImportFileImports(TestCase):
|
||||
Test the functions in the :mod:`osisimport` module.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.registry_patcher = patch('openlp.plugins.bibles.lib.db.Registry')
|
||||
self.registry_patcher = patch('openlp.plugins.bibles.lib.bibleimport.Registry')
|
||||
self.addCleanup(self.registry_patcher.stop)
|
||||
self.registry_patcher.start()
|
||||
self.manager_patcher = patch('openlp.plugins.bibles.lib.db.Manager')
|
||||
|
@ -46,7 +46,7 @@ class TestSwordImport(TestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.registry_patcher = patch('openlp.plugins.bibles.lib.db.Registry')
|
||||
self.registry_patcher = patch('openlp.plugins.bibles.lib.bibleimport.Registry')
|
||||
self.registry_patcher.start()
|
||||
self.manager_patcher = patch('openlp.plugins.bibles.lib.db.Manager')
|
||||
self.manager_patcher.start()
|
||||
|
@ -41,7 +41,7 @@ class TestZefaniaImport(TestCase):
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.registry_patcher = patch('openlp.plugins.bibles.lib.db.Registry')
|
||||
self.registry_patcher = patch('openlp.plugins.bibles.lib.bibleimport.Registry')
|
||||
self.addCleanup(self.registry_patcher.stop)
|
||||
self.registry_patcher.start()
|
||||
self.manager_patcher = patch('openlp.plugins.bibles.lib.db.Manager')
|
||||
|
Loading…
Reference in New Issue
Block a user