diff --git a/openlp/core/common/i18n.py b/openlp/core/common/i18n.py
index ad4529607..e377995ed 100644
--- a/openlp/core/common/i18n.py
+++ b/openlp/core/common/i18n.py
@@ -336,6 +336,8 @@ class UiStrings(metaclass=Singleton):
"""
self.About = translate('OpenLP.Ui', 'About')
self.Add = translate('OpenLP.Ui', '&Add')
+ self.AddFolder = translate('OpenLP.Ui', 'Add folder')
+ self.AddFolderDot = translate('OpenLP.Ui', 'Add folder.')
self.AddGroup = translate('OpenLP.Ui', 'Add group')
self.AddGroupDot = translate('OpenLP.Ui', 'Add group.')
self.Advanced = translate('OpenLP.Ui', 'Advanced')
diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py
index 31193c851..a12a49ee1 100644
--- a/openlp/core/common/settings.py
+++ b/openlp/core/common/settings.py
@@ -278,6 +278,11 @@ class Settings(QtCore.QSettings):
'media/vlc arguments': '',
'media/live volume': 50,
'media/preview volume': 0,
+ 'media/db type': 'sqlite',
+ 'media/db username': '',
+ 'media/db password': '',
+ 'media/db hostname': '',
+ 'media/db database': '',
'players/background color': '#000000',
'planningcenter/status': PluginStatus.Inactive,
'planningcenter/application_id': '',
@@ -296,6 +301,11 @@ class Settings(QtCore.QSettings):
'presentations/powerpoint control window': QtCore.Qt.Unchecked,
'presentations/impress use display setting': QtCore.Qt.Unchecked,
'presentations/last directory': None,
+ 'presentations/db type': 'sqlite',
+ 'presentations/db username': '',
+ 'presentations/db password': '',
+ 'presentations/db hostname': '',
+ 'presentations/db database': '',
'servicemanager/last directory': None,
'servicemanager/last file': None,
'servicemanager/service theme': None,
diff --git a/openlp/core/lib/db.py b/openlp/core/lib/db.py
index a4df3aa61..d1a392d52 100644
--- a/openlp/core/lib/db.py
+++ b/openlp/core/lib/db.py
@@ -30,10 +30,11 @@ from urllib.parse import quote_plus as urlquote
from alembic.migration import MigrationContext
from alembic.operations import Operations
-from sqlalchemy import Column, MetaData, Table, UnicodeText, create_engine, types
-from sqlalchemy.engine.url import make_url, URL
+from sqlalchemy import Column, ForeignKey, Integer, MetaData, Table, Unicode, UnicodeText, create_engine, types
+from sqlalchemy.engine.url import URL, make_url
from sqlalchemy.exc import DBAPIError, InvalidRequestError, OperationalError, ProgrammingError, SQLAlchemyError
-from sqlalchemy.orm import mapper, scoped_session, sessionmaker
+from sqlalchemy.ext.declarative import declared_attr
+from sqlalchemy.orm import backref, mapper, relationship, scoped_session, sessionmaker
from sqlalchemy.pool import NullPool
from openlp.core.common import delete_file
@@ -43,7 +44,6 @@ from openlp.core.common.json import OpenLPJSONDecoder, OpenLPJSONEncoder
from openlp.core.common.registry import Registry
from openlp.core.lib.ui import critical_error_message_box
-
log = logging.getLogger(__name__)
@@ -224,6 +224,49 @@ def get_upgrade_op(session):
return Operations(context)
+class CommonMixin(object):
+ """
+ Base class to automate table name and ID column.
+ """
+ @declared_attr
+ def __tablename__(self):
+ return self.__name__.lower()
+
+ id = Column(Integer, primary_key=True)
+
+
+class FolderMixin(CommonMixin):
+ """
+ A mixin to provide most of the fields needed for folder support
+ """
+ name = Column(Unicode(255), nullable=False, index=True)
+
+ @declared_attr
+ def parent_id(self):
+ return Column(Integer(), ForeignKey('folder.id'))
+
+ @declared_attr
+ def folders(self):
+ return relationship('Folder', backref=backref('parent', remote_side='Folder.id'), order_by='Folder.name')
+
+ @declared_attr
+ def items(self):
+ return relationship('Item', backref='folder', order_by='Item.name')
+
+
+class ItemMixin(CommonMixin):
+ """
+ A mixin to provide most of the fields needed for folder support
+ """
+ name = Column(Unicode(255), nullable=False, index=True)
+ file_path = Column(Unicode(255))
+ file_hash = Column(Unicode(255))
+
+ @declared_attr
+ def folder_id(self):
+ return Column(Integer(), ForeignKey('folder.id'))
+
+
class BaseModel(object):
"""
BaseModel provides a base object with a set of generic functions
@@ -494,16 +537,19 @@ class Manager(object):
if try_count >= 2:
raise
- def get_object_filtered(self, object_class, filter_clause):
+ def get_object_filtered(self, object_class, *filter_clauses):
"""
Returns an object matching specified criteria
:param object_class: The type of object to return
:param filter_clause: The criteria to select the object by
"""
+ query = self.session.query(object_class)
+ for filter_clause in filter_clauses:
+ query = query.filter(filter_clause)
for try_count in range(3):
try:
- return self.session.query(object_class).filter(filter_clause).first()
+ return query.first()
except OperationalError as oe:
# This exception clause is for users running MySQL which likes to terminate connections on its own
# without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py
index 6074e71d6..57be566f7 100644
--- a/openlp/core/lib/mediamanageritem.py
+++ b/openlp/core/lib/mediamanageritem.py
@@ -21,13 +21,12 @@
"""
Provides the generic functions for interfacing plugins with the Media Manager.
"""
-import logging
import re
from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import UiStrings, translate
-from openlp.core.common.mixins import RegistryProperties
+from openlp.core.common.mixins import LogMixin, RegistryProperties
from openlp.core.common.registry import Registry
from openlp.core.lib import ServiceItemContext
from openlp.core.lib.plugin import StringContent
@@ -39,10 +38,8 @@ from openlp.core.widgets.edits import SearchEdit
from openlp.core.widgets.toolbar import OpenLPToolbar
from openlp.core.widgets.views import ListWidgetWithDnD
-log = logging.getLogger(__name__)
-
-class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
+class MediaManagerItem(QtWidgets.QWidget, RegistryProperties, LogMixin):
"""
MediaManagerItem is a helper widget for plugins.
@@ -75,7 +72,6 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
that is performed automatically by OpenLP when necessary. If this method is not defined, a default will be used
(treat the filename as an image).
"""
- log.info('Media Item loaded')
def __init__(self, parent=None, plugin=None):
"""
@@ -212,12 +208,16 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
self.add_actionlist_to_toolbar(toolbar_actions)
def add_actionlist_to_toolbar(self, toolbar_actions):
- for action in toolbar_actions:
- self.toolbar.add_toolbar_action('{name}{action}Action'.format(name=self.plugin.name, action=action[0]),
- text=self.plugin.get_string(action[1])['title'],
- icon=action[2],
- tooltip=self.plugin.get_string(action[1])['tooltip'],
- triggers=action[3])
+ added_actions = []
+ for action, plugin, icon, triggers in toolbar_actions:
+ added_actions.append(self.toolbar.add_toolbar_action(
+ '{name}{action}Action'.format(name=self.plugin.name, action=action),
+ text=self.plugin.get_string(plugin)['title'],
+ icon=icon,
+ tooltip=self.plugin.get_string(plugin)['tooltip'],
+ triggers=triggers
+ ))
+ return added_actions
def add_list_view_to_toolbar(self):
"""
@@ -350,7 +350,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
self, self.on_new_prompt,
self.settings.value(self.settings_section + '/last directory'),
self.on_new_file_masks)
- log.info('New file(s) {file_paths}'.format(file_paths=file_paths))
+ self.log_info('New file(s) {file_paths}'.format(file_paths=file_paths))
if file_paths:
self.application.set_busy_cursor()
self.validate_and_load(file_paths)
@@ -441,7 +441,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
file_paths = []
for index in range(self.list_view.count()):
list_item = self.list_view.item(index)
- file_path = list_item.data(QtCore.Qt.UserRole)
+ file_path = list_item.data(0, QtCore.Qt.UserRole)
file_paths.append(file_path)
return file_paths
@@ -523,7 +523,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
translate('OpenLP.MediaManagerItem',
'You must select one or more items to preview.'))
else:
- log.debug('{plug} Preview requested'.format(plug=self.plugin.name))
+ self.log_debug('{plug} Preview requested'.format(plug=self.plugin.name))
Registry().set_flag('has doubleclick added item to service', False)
service_item = self.build_service_item()
if service_item:
@@ -558,7 +558,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
:param item_id: item to make live
:param remote: From Remote
"""
- log.debug('%s Live requested', self.plugin.name)
+ self.log_debug('{plugin} Live requested'.format(plugin=self.plugin.name))
item = None
if item_id:
item = self.create_item_from_id(item_id)
@@ -593,7 +593,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
# Is it possible to process multiple list items to generate
# multiple service items?
if self.single_service_item:
- log.debug('{plugin} Add requested'.format(plugin=self.plugin.name))
+ self.log_debug('{plugin} Add requested'.format(plugin=self.plugin.name))
self.add_to_service(replace=self.remote_triggered)
else:
items = self.list_view.selectedIndexes()
@@ -634,7 +634,7 @@ class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
translate('OpenLP.MediaManagerItem',
'You must select one or more items.'))
else:
- log.debug('{plugin} Add requested'.format(plugin=self.plugin.name))
+ self.log_debug('{plugin} Add requested'.format(plugin=self.plugin.name))
service_item = self.service_manager.get_service_item()
if not service_item:
QtWidgets.QMessageBox.information(self, UiStrings().NISs,
diff --git a/openlp/core/projectors/db.py b/openlp/core/projectors/db.py
index 20759abf4..54c1f7583 100644
--- a/openlp/core/projectors/db.py
+++ b/openlp/core/projectors/db.py
@@ -36,10 +36,10 @@ The Projector table keeps track of entries for controlled projectors.
import logging
from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, and_
-from sqlalchemy.ext.declarative import declarative_base, declared_attr
+from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
-from openlp.core.lib.db import Manager, init_db, init_url
+from openlp.core.lib.db import CommonMixin, Manager, init_db, init_url
from openlp.core.projectors import upgrade
from openlp.core.projectors.constants import PJLINK_DEFAULT_CODES
@@ -51,17 +51,6 @@ log.debug('projector.lib.db module loaded')
Base = declarative_base(MetaData())
-class CommonMixin(object):
- """
- Base class to automate table name and ID column.
- """
- @declared_attr
- def __tablename__(self):
- return self.__name__.lower()
-
- id = Column(Integer, primary_key=True)
-
-
class Manufacturer(Base, CommonMixin):
"""
Projector manufacturer table.
diff --git a/openlp/core/ui/folders.py b/openlp/core/ui/folders.py
new file mode 100644
index 000000000..65ca3345e
--- /dev/null
+++ b/openlp/core/ui/folders.py
@@ -0,0 +1,302 @@
+# -*- coding: utf-8 -*-
+
+##########################################################################
+# OpenLP - Open Source Lyrics Projection #
+# ---------------------------------------------------------------------- #
+# Copyright (c) 2008-2021 OpenLP Developers #
+# ---------------------------------------------------------------------- #
+# This program 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. #
+# #
+# This program 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 this program. If not, see . #
+##########################################################################
+
+from PyQt5 import QtCore, QtWidgets
+
+from openlp.core.common.i18n import get_natural_key, translate
+from openlp.core.lib.ui import create_button_box, critical_error_message_box
+
+
+class FolderPopulateMixin(object):
+ """
+ A mixin for common code between the two folder dialogs
+ """
+ def populate_folders(self, parent_id=None, prefix=''):
+ """
+ Recursively add folders to the combobox
+
+ :param folders: A dictionary object of the folders, in a tree.
+ :param parent_id: The ID of the parent folder.
+ :param prefix: A string containing the prefix that will be added in front of the folder name for each
+ level of the tree.
+ """
+ if parent_id is None:
+ self.folder_combobox.clear()
+ # I'm not sure what this is here for, I'm just leaving it in for now.
+ self.folder_combobox.top_level_folder_added = False
+ folders = self.db_manager.get_all_objects(self.folder_class, self.folder_class.parent_id == parent_id)
+ folders.sort(key=lambda folder: get_natural_key(folder.name))
+ for folder in folders:
+ self.folder_combobox.addItem(prefix + folder.name, folder)
+ self.populate_folders(folder.id, prefix + ' ')
+
+
+class AddFolderForm(QtWidgets.QDialog, FolderPopulateMixin):
+ """
+ This class implements the 'Add folder' form for the plugins.
+ """
+ def __init__(self, parent=None, db_manager=None, folder_class=None):
+ """
+ Constructor
+ """
+ super().__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
+ QtCore.Qt.WindowCloseButtonHint)
+ self.setup_ui()
+ self.db_manager = db_manager
+ self.folder_class = folder_class
+
+ def setup_ui(self):
+ self.setObjectName('AddFolderDialog')
+ self.resize(300, 10)
+ self.dialog_layout = QtWidgets.QVBoxLayout(self)
+ self.dialog_layout.setObjectName('dialog_layout')
+ self.name_layout = QtWidgets.QFormLayout()
+ self.name_layout.setObjectName('name_layout')
+ self.parent_folder_label = QtWidgets.QLabel(self)
+ self.parent_folder_label.setObjectName('parent_folder_label')
+ self.folder_combobox = QtWidgets.QComboBox(self)
+ self.folder_combobox.setObjectName('folder_combobox')
+ self.name_layout.addRow(self.parent_folder_label, self.folder_combobox)
+ self.name_label = QtWidgets.QLabel(self)
+ self.name_label.setObjectName('name_label')
+ self.name_edit = QtWidgets.QLineEdit(self)
+ self.name_edit.setObjectName('name_edit')
+ self.name_label.setBuddy(self.name_edit)
+ self.name_layout.addRow(self.name_label, self.name_edit)
+ self.dialog_layout.addLayout(self.name_layout)
+ self.button_box = create_button_box(self, 'button_box', ['cancel', 'save'])
+ self.dialog_layout.addWidget(self.button_box)
+ self.retranslate_ui()
+ self.setMaximumHeight(self.sizeHint().height())
+
+ def retranslate_ui(self):
+ self.setWindowTitle(translate('OpenLP.AddFolderForm', 'Add folder'))
+ self.parent_folder_label.setText(translate('OpenLP.AddFolderForm', 'Parent folder:'))
+ self.name_label.setText(translate('OpenLP.AddFolderForm', 'Folder name:'))
+
+ def exec(self, clear=True, show_top_level_folder=False, selected_folder=None):
+ """
+ Show the form.
+
+ :param clear: Set to False if the text input box should not be cleared when showing the dialog (default: True).
+ :param show_top_level_folder: Set to True when "-- Top level folder --" should be showed as first item
+ (default: False).
+ :param selected_folder: The ID of the folder that should be selected by default when showing the dialog.
+ """
+ self.populate_folders()
+ if clear:
+ self.name_edit.clear()
+ self.name_edit.setFocus()
+ if show_top_level_folder and not self.folder_combobox.top_level_folder_added:
+ self.folder_combobox.insertItem(0, translate('OpenLP.AddFolderForm', '-- Top-level folder --'), 0)
+ self.folder_combobox.top_level_folder_added = True
+ if selected_folder is not None:
+ for i in range(self.folder_combobox.count()):
+ if self.folder_combobox.itemData(i) == selected_folder:
+ self.folder_combobox.setCurrentIndex(i)
+ return QtWidgets.QDialog.exec(self)
+
+ def accept(self):
+ """
+ Override the accept() method from QDialog to make sure something is entered in the text input box.
+ """
+ if not self.name_edit.text():
+ critical_error_message_box(message=translate('OpenLP.AddFolderForm',
+ 'You need to type in a folder name.'))
+ self.name_edit.setFocus()
+ return False
+ elif self.is_existing_folder():
+ critical_error_message_box(message=translate('OpenLP.AddFolderForm',
+ 'This folder already exists, please use a different name.'))
+ return False
+ else:
+ return QtWidgets.QDialog.accept(self)
+
+ @property
+ def parent_id(self):
+ """
+ A property to get the parent folder id
+ """
+ if self.folder_combobox.currentIndex() == 0:
+ return None
+ return self.folder_combobox.itemData(self.folder_combobox.currentIndex(), QtCore.Qt.UserRole).id
+
+ @property
+ def folder_name(self):
+ """
+ A property to return the folder name
+ """
+ return self.name_edit.text()
+
+ @property
+ def new_folder(self):
+ """
+ A Folder object property
+ """
+ return self.folder_class(parent_id=self.parent_id, name=self.folder_name)
+
+ def is_existing_folder(self):
+ """
+ Check if this folder already exists
+ """
+ return self.db_manager.get_object_filtered(self.folder_class,
+ self.folder_class.parent_id == self.parent_id,
+ self.folder_class.name == self.folder_name) is not None
+
+
+class ChooseFolderForm(QtWidgets.QDialog, FolderPopulateMixin):
+ """
+ This class implements the 'Choose folder' form for the plugins.
+ """
+ def __init__(self, parent=None, db_manager=None, folder_class=None):
+ """
+ Constructor
+ """
+ super().__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
+ QtCore.Qt.WindowCloseButtonHint)
+ self.setup_ui()
+ self.db_manager = db_manager
+ self.folder_class = folder_class
+
+ def setup_ui(self):
+ """
+ Set up the UI.
+ """
+ self.setObjectName('ChooseFolderForm')
+ self.resize(399, 119)
+ self.choose_folder_layout = QtWidgets.QFormLayout(self)
+ self.choose_folder_layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow)
+ self.choose_folder_layout.setContentsMargins(8, 8, 8, 8)
+ self.choose_folder_layout.setSpacing(8)
+ self.choose_folder_layout.setLabelAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+ self.choose_folder_layout.setObjectName('choose_folder_layout')
+ self.folder_question_label = QtWidgets.QLabel(self)
+ self.folder_question_label.setWordWrap(True)
+ self.folder_question_label.setObjectName('folder_question_label')
+ self.choose_folder_layout.setWidget(1, QtWidgets.QFormLayout.SpanningRole, self.folder_question_label)
+ self.nofolder_radio_button = QtWidgets.QRadioButton(self)
+ self.nofolder_radio_button.setChecked(True)
+ self.nofolder_radio_button.setObjectName('nofolder_radio_button')
+ self.choose_folder_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.nofolder_radio_button)
+ self.existing_radio_button = QtWidgets.QRadioButton(self)
+ self.existing_radio_button.setChecked(False)
+ self.existing_radio_button.setObjectName('existing_radio_button')
+ self.choose_folder_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.existing_radio_button)
+ self.folder_combobox = QtWidgets.QComboBox(self)
+ self.folder_combobox.setObjectName('folder_combobox')
+ self.folder_combobox.activated.connect(self.on_folder_combobox_selected)
+ self.choose_folder_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.folder_combobox)
+ self.new_radio_button = QtWidgets.QRadioButton(self)
+ self.new_radio_button.setChecked(False)
+ self.new_radio_button.setObjectName('new_radio_button')
+ self.choose_folder_layout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.new_radio_button)
+ self.new_folder_edit = QtWidgets.QLineEdit(self)
+ self.new_folder_edit.setObjectName('new_folder_edit')
+ self.new_folder_edit.textEdited.connect(self.on_new_folder_edit_changed)
+ self.choose_folder_layout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.new_folder_edit)
+ self.folder_button_box = create_button_box(self, 'buttonBox', ['ok'])
+ self.choose_folder_layout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.folder_button_box)
+
+ self.retranslate_ui()
+ QtCore.QMetaObject.connectSlotsByName(self)
+
+ def retranslate_ui(self):
+ """
+ Translate the UI on the fly.
+
+ :param self: The form object (not the class).
+ """
+ self.setWindowTitle(translate('OpenLP.ChooseFolderForm', 'Select Folder'))
+ self.folder_question_label.setText(translate('OpenLP.ChooseFolderForm', 'Add items to folder:'))
+ self.nofolder_radio_button.setText(translate('OpenLP.ChooseFolderForm', 'No folder'))
+ self.existing_radio_button.setText(translate('OpenLP.ChooseFolderForm', 'Existing folder'))
+ self.new_radio_button.setText(translate('OpenLP.ChooseFolderForm', 'New folder'))
+
+ def exec(self, selected_folder=None):
+ """
+ Show the form
+
+ :param selected_folder: The ID of the folder that should be selected by default when showing the dialog.
+ """
+ is_disabled = self.db_manager.get_object_count(self.folder_class) == 0
+ self.existing_radio_button.setDisabled(is_disabled)
+ self.folder_combobox.setDisabled(is_disabled)
+ self.new_folder_edit.clear()
+ self.populate_folders()
+ if selected_folder is not None:
+ for index in range(self.folder_combobox.count()):
+ if self.folder_combobox.itemData(index) == selected_folder:
+ self.folder_combobox.setCurrentIndex(index)
+ self.existing_radio_button.setChecked(True)
+ return QtWidgets.QDialog.exec(self)
+
+ def accept(self):
+ """
+ Override the accept() method from QDialog to make sure something is entered in the text input box.
+ """
+ if self.new_radio_button.isChecked() and not self.new_folder_edit.text():
+ critical_error_message_box(message=translate('OpenLP.ChooseFolderForm',
+ 'You need to type in a folder name.'))
+ self.new_folder_edit.setFocus()
+ return False
+ else:
+ return QtWidgets.QDialog.accept(self)
+
+ def on_folder_combobox_selected(self, index):
+ """
+ Handles the activated signal from the existing folder combobox when the
+ user makes a selection
+
+ :param index: position of the selected item in the combobox
+ """
+ self.existing_radio_button.setChecked(True)
+ self.folder_combobox.setFocus()
+
+ def on_new_folder_edit_changed(self, new_folder):
+ """
+ Handles the textEdited signal from the new folder text input field
+ when the user enters a new folder name
+
+ :param new_folder: new text entered by the user
+ """
+ self.new_radio_button.setChecked(True)
+
+ @property
+ def folder_name(self):
+ """
+ A property to return the folder name
+ """
+ return self.new_folder_edit.text()
+
+ @property
+ def folder(self):
+ if self.existing_radio_button.isChecked() and self.folder_combobox.currentIndex() != -1:
+ return self.folder_combobox.currentData(QtCore.Qt.UserRole)
+ elif self.new_radio_button.isChecked() and self.new_folder_edit.text():
+ return self.folder_class(name=self.new_folder_edit.text())
+ return None
+
+ @property
+ def is_new_folder(self):
+ """
+ A property to indicate if the folder from the ``folder`` property is a new folder or an existing one
+ """
+ return self.new_radio_button.isChecked()
diff --git a/openlp/core/ui/icons.py b/openlp/core/ui/icons.py
index dcee56499..c40485bd7 100644
--- a/openlp/core/ui/icons.py
+++ b/openlp/core/ui/icons.py
@@ -90,6 +90,7 @@ class UiIcons(metaclass=Singleton):
'error': {'icon': 'fa.exclamation', 'attr': 'red'},
'exception': {'icon': 'fa.times-circle'},
'exit': {'icon': 'fa.sign-out'},
+ 'folder': {'icon': 'fa.folder'},
'group': {'icon': 'fa.object-group'},
'inactive': {'icon': 'fa.child', 'attr': 'lightGray'},
'info': {'icon': 'fa.info'},
diff --git a/openlp/core/ui/library.py b/openlp/core/ui/library.py
new file mode 100644
index 000000000..aabf40c37
--- /dev/null
+++ b/openlp/core/ui/library.py
@@ -0,0 +1,387 @@
+# -*- coding: utf-8 -*-
+
+##########################################################################
+# OpenLP - Open Source Lyrics Projection #
+# ---------------------------------------------------------------------- #
+# Copyright (c) 2008-2021 OpenLP Developers #
+# ---------------------------------------------------------------------- #
+# This program 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. #
+# #
+# This program 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 this program. If not, see . #
+##########################################################################
+"""
+Provides additional classes for working in the library
+"""
+import os
+from pathlib import Path
+
+from PyQt5 import QtCore, QtWidgets
+
+from openlp.core.common import sha256_file_hash
+from openlp.core.common.i18n import UiStrings, get_natural_key, translate
+from openlp.core.lib import check_item_selected
+from openlp.core.lib.mediamanageritem import MediaManagerItem
+from openlp.core.lib.ui import critical_error_message_box
+from openlp.core.ui.folders import AddFolderForm, ChooseFolderForm
+from openlp.core.ui.icons import UiIcons
+from openlp.core.widgets.views import TreeWidgetWithDnD
+
+
+class FolderLibraryItem(MediaManagerItem):
+ """
+ This is a custom MediaManagerItem subclass with support for folders
+ """
+ def __init__(self, parent, plugin, folder_class, item_class):
+ super(FolderLibraryItem, self).__init__(parent, plugin)
+ self.manager = self.plugin.manager
+ self.choose_folder_form = ChooseFolderForm(self, self.manager, folder_class)
+ self.add_folder_form = AddFolderForm(self, self.manager, folder_class)
+ self.folder_class = folder_class
+ self.item_class = item_class
+
+ @property
+ def current_folder(self):
+ """
+ Returns the currently active folder, or None
+ """
+ selected_items = self.list_view.selectedItems()
+ selected_folder = None
+ if selected_items:
+ selected_item = selected_items[0]
+ if isinstance(selected_item.data(0, QtCore.Qt.UserRole), self.item_class):
+ selected_item = selected_item.parent()
+ if isinstance(selected_item, QtWidgets.QTreeWidgetItem) and \
+ isinstance(selected_item.data(0, QtCore.Qt.UserRole), self.folder_class):
+ selected_folder = selected_item.data(0, QtCore.Qt.UserRole)
+ return selected_folder
+
+ def retranslate_ui(self):
+ """
+ This method is called automatically to provide OpenLP with the opportunity to translate the ``MediaManagerItem``
+ to another language.
+ """
+ self.add_folder_action.setText(UiStrings().AddFolder)
+ self.add_folder_action.setToolTip(UiStrings().AddFolderDot)
+
+ def on_add_folder_click(self):
+ """
+ Called to add a new folder
+ """
+ # Syntactic sugar, plus a minor performance optimisation
+ Item = self.item_class
+
+ if self.add_folder_form.exec(show_top_level_folder=True, selected_folder=self.current_folder):
+ new_folder = self.add_folder_form.new_folder
+ if self.manager.save_object(new_folder):
+ self.load_list(self.manager.get_all_objects(Item, order_by_ref=Item.file_path))
+ self.expand_folder(new_folder.id)
+ else:
+ critical_error_message_box(
+ message=translate('OpenLP.FolderLibraryItem', 'Could not add the new folder.'))
+
+ def on_delete_click(self):
+ """
+ Remove an item from the list.
+ """
+ # Syntactic sugar, plus a minor performance optimisation
+ Folder, Item = self.folder_class, self.item_class
+
+ # Turn off auto preview triggers.
+ self.list_view.blockSignals(True)
+ if check_item_selected(self.list_view,
+ translate('OpenLP.FolderLibraryItem', 'You must select an item or folder to delete.')):
+ tree_item_list = self.list_view.selectedItems()
+ self.application.set_busy_cursor()
+ self.main_window.display_progress_bar(len(tree_item_list))
+ for tree_item in tree_item_list:
+ if not tree_item:
+ self.main_window.increment_progress_bar()
+ continue
+ item = tree_item.data(0, QtCore.Qt.UserRole)
+ if isinstance(item, Item):
+ self.delete_item(item)
+ if not item.folder_id:
+ self.list_view.takeTopLevelItem(self.list_view.indexOfTopLevelItem(tree_item))
+ else:
+ tree_item.parent().removeChild(tree_item)
+ self.manager.delete_object(Item, item.id)
+ elif isinstance(item, Folder):
+ if QtWidgets.QMessageBox.question(
+ self.list_view.parent(),
+ translate('OpenLP.FolderLibraryItem', 'Remove folder'),
+ translate('OpenLP.FolderLibraryItem',
+ 'Are you sure you want to remove "{name}" and everything in it?'
+ ).format(name=item.name)
+ ) == QtWidgets.QMessageBox.Yes:
+ self.recursively_delete_folder(item)
+ self.manager.delete_object(Folder, item.id)
+ if item.parent_id is None:
+ self.list_view.takeTopLevelItem(self.list_view.indexOfTopLevelItem(tree_item))
+ else:
+ tree_item.parent().removeChild(tree_item)
+ self.main_window.increment_progress_bar()
+ self.main_window.finished_progress_bar()
+ self.application.set_normal_cursor()
+ self.list_view.blockSignals(False)
+
+ def add_list_view_to_toolbar(self):
+ """
+ Creates the main widget for listing items.
+ """
+ # Add the List widget
+ self.list_view = TreeWidgetWithDnD(self, self.plugin.name)
+ self.list_view.setObjectName('{name}TreeView'.format(name=self.plugin.name))
+ # Add to pageLayout
+ self.page_layout.addWidget(self.list_view)
+ # define and add the context menu
+ self.list_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+
+ def add_middle_header_bar(self):
+ """
+ Add buttons after the main buttons
+ """
+ self.add_folder_action = self.toolbar.add_toolbar_action(
+ 'add_folder_action', icon=UiIcons().folder, triggers=self.on_add_folder_click)
+
+ def add_sub_folders(self, folder_list, parent_id=None):
+ """
+ Recursively add subfolders to the given parent folder in a QTreeWidget.
+
+ :param folder_list: The List object that contains all QTreeWidgetItems.
+ :param parent_folder_id: The ID of the folder that will be added recursively.
+ """
+ # Syntactic sugar, plus a minor performance optimisation
+ Folder = self.folder_class
+
+ folders = self.manager.get_all_objects(Folder, Folder.parent_id == parent_id)
+ folders.sort(key=lambda folder_object: get_natural_key(folder_object.name))
+ folder_icon = UiIcons().folder
+ for folder in folders:
+ folder_item = QtWidgets.QTreeWidgetItem()
+ folder_item.setText(0, folder.name)
+ folder_item.setData(0, QtCore.Qt.UserRole, folder)
+ folder_item.setIcon(0, folder_icon)
+ if parent_id is None:
+ self.list_view.addTopLevelItem(folder_item)
+ else:
+ folder_list[parent_id].addChild(folder_item)
+ folder_list[folder.id] = folder_item
+ self.add_sub_folders(folder_list, folder.id)
+
+ def expand_folder(self, folder_id, root_item=None):
+ """
+ Expand folders in the widget recursively.
+
+ :param folder_id: The ID of the folder that will be expanded.
+ :param root_item: This option is only used for recursion purposes.
+ """
+ return_value = False
+ if root_item is None:
+ root_item = self.list_view.invisibleRootItem()
+ for i in range(root_item.childCount()):
+ child = root_item.child(i)
+ if self.expand_folder(folder_id, child):
+ child.setExpanded(True)
+ return_value = True
+ if isinstance(root_item.data(0, QtCore.Qt.UserRole), self.folder_class):
+ if root_item.data(0, QtCore.Qt.UserRole).id == folder_id:
+ return True
+ return return_value
+
+ def recursively_delete_folder(self, folder):
+ """
+ Recursively deletes a folder and all folders and items in it.
+
+ :param folder: The Folder instance of the folder that will be deleted.
+ """
+ # Syntactic sugar, plus a minor performance optimisation
+ Folder, Item = self.folder_class, self.item_class
+
+ items = self.manager.get_all_objects(Item, Item.folder_id == folder.id)
+ for item in items:
+ self.delete_item(item)
+ self.manager.delete_object(Item, item.id)
+ folders = self.manager.get_all_objects(Folder, Folder.parent_id == folder.id)
+ for child in folders:
+ self.recursively_delete_folder(child)
+ self.manager.delete_object(Folder, child.id)
+
+ def file_to_item(self, filename):
+ """
+ This method allows the media item to convert a string filename into an item class
+
+ Override this method to customise your plugin's loading method
+ """
+ if isinstance(filename, Path):
+ name = filename.name
+ filename = str(filename)
+ else:
+ name = os.path.basename(filename)
+ item = self.item_class(name=name, file_path=filename)
+ self.manager.save_object(item)
+ return item
+
+ def load_item(self, item, is_initial_load=False):
+ """
+ This method allows the media item to set up the QTreeWidgetItem the way it wants
+ """
+ raise NotImplementedError('load_item needs to be implemented by the descendant class')
+
+ def delete_item(self, item):
+ """
+ This method allows the media item to delete the Item
+ """
+ raise NotImplementedError('delete_item needs to be implemented by the descendant class')
+
+ def load_list(self, items, is_initial_load=False, target_folder=None):
+ """
+ Load the list of items into the tree view
+
+ :param items: The items to load
+ :param target_folder: The folder to load the items into
+ """
+ if not is_initial_load:
+ self.application.set_busy_cursor()
+ self.main_window.display_progress_bar(len(items))
+ self.list_view.clear()
+ # Load the list of folders and add them to the tree view
+ folder_items = {}
+ self.add_sub_folders(folder_items, parent_id=None)
+ if target_folder is not None:
+ self.expand_folder(target_folder.id)
+ # Convert any filenames to items
+ for counter, filename in enumerate(items):
+ if isinstance(filename, (Path, str)):
+ items[counter] = self.file_to_item(filename)
+ # Sort the files by the filename
+ items.sort(key=lambda item: get_natural_key(item if isinstance(item, str) else item.file_path))
+ for item in items:
+ self.log_debug('Loading item: {name}'.format(name=item.file_path))
+ tree_item = self.load_item(item, is_initial_load)
+ if not tree_item:
+ continue
+ elif not item.folder_id:
+ self.list_view.addTopLevelItem(tree_item)
+ else:
+ folder_items[item.folder_id].addChild(tree_item)
+ if not is_initial_load:
+ self.main_window.increment_progress_bar()
+ if not is_initial_load:
+ self.main_window.finished_progress_bar()
+ self.application.set_normal_cursor()
+
+ def format_search_result(self, item):
+ """
+ Format an item for the search results. The default implementation simply returns
+ [item.file_path, item.file_path]
+
+ :param Item item: An Item to be formatted
+ :return list[str, str]: A list of two items containing the full path and a pretty name
+ """
+ return [item.file_path, item.file_path]
+
+ def search(self, string, show_error):
+ """
+ Performs a search for items containing ``string``
+
+ :param string: String to be displayed
+ :param show_error: Should the error be shown (True)
+ :return: The search result.
+ """
+ string = string.lower()
+ items = self.manager.get_all_objects(self.item_class, self.item_class.file_path.match(string))
+ return [self.format_search_result(item) for item in items]
+
+ def validate_and_load(self, file_paths, target_folder=None):
+ """
+ Process a list for files either from the File Dialog or from Drag and Drop.
+ This method is overloaded from MediaManagerItem.
+
+ :param list[Path] file_paths: A List of paths to be loaded
+ :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
+ """
+ self.application.set_normal_cursor()
+ if target_folder:
+ target_folder = target_folder.data(0, QtCore.Qt.UserRole)
+ elif self.current_folder:
+ target_folder = self.current_folder
+ if not target_folder and self.choose_folder_form.exec() == QtWidgets.QDialog.Accepted:
+ target_folder = self.choose_folder_form.folder
+ if self.choose_folder_form.is_new_folder:
+ self.manager.save_object(target_folder)
+ existing_files = [item.file_path for item in self.manager.get_all_objects(self.item_class)]
+ # Convert file paths to items
+ for file_path in file_paths:
+ if file_paths.count(file_path) > 1 or existing_files.count(file_path) > 0:
+ # If a file already exists in the items or has been selected twice, show an error message
+ critical_error_message_box(translate('OpenLP.FolderLibraryItem', 'File Exists'),
+ translate('OpenLP.FolderLibraryItem',
+ 'An item with that filename already exists.'))
+ continue
+
+ self.log_debug('Adding new item: {name}'.format(name=file_path))
+ item = self.item_class(name=str(file_path), file_path=str(file_path))
+ if isinstance(file_path, Path) and file_path.exists():
+ item.file_hash = sha256_file_hash(file_path)
+ if target_folder:
+ item.folder_id = target_folder.id
+ self.manager.save_object(item)
+ self.main_window.increment_progress_bar()
+ self.load_list(self.manager.get_all_objects(self.item_class, order_by_ref=self.item_class.file_path),
+ target_folder=target_folder)
+ last_dir = Path(file_paths[0]).parent
+ self.settings.setValue(self.settings_section + '/last directory', last_dir)
+
+ def dnd_move_internal(self, target):
+ """
+ Handle drag-and-drop moving of items within the media manager
+
+ :param target: This contains the QTreeWidget that is the target of the DnD action
+ """
+ items_to_move = self.list_view.selectedItems()
+ # Determine group to move images to
+ target_folder = target
+ if target_folder is not None and isinstance(target_folder.data(0, QtCore.Qt.UserRole), self.item_class):
+ target_folder = target.parent()
+ # Move to toplevel
+ if target_folder is None:
+ target_folder = self.list_view.invisibleRootItem()
+ target_folder.setData(0, QtCore.Qt.UserRole, self.folder_class())
+ target_folder.data(0, QtCore.Qt.UserRole).id = 0
+ # Move images in the treeview
+ items_to_save = []
+ for item in items_to_move:
+ if isinstance(item.data(0, QtCore.Qt.UserRole), self.item_class):
+ if isinstance(item.parent(), QtWidgets.QTreeWidgetItem):
+ item.parent().removeChild(item)
+ else:
+ self.list_view.invisibleRootItem().removeChild(item)
+ target_folder.addChild(item)
+ item.setSelected(True)
+ item_data = item.data(0, QtCore.Qt.UserRole)
+ item_data.folder_id = target_folder.data(0, QtCore.Qt.UserRole).id
+ items_to_save.append(item_data)
+ target_folder.setExpanded(True)
+ # Update the folder ID's of the items in the database
+ self.manager.save_objects(items_to_save)
+ # Sort the target folder
+ sort_folders = []
+ sort_items = []
+ for item in target_folder.takeChildren():
+ if isinstance(item.data(0, QtCore.Qt.UserRole), self.folder_class):
+ sort_folders.append(item)
+ if isinstance(item.data(0, QtCore.Qt.UserRole), self.item_class):
+ sort_items.append(item)
+ sort_folders.sort(key=lambda item: get_natural_key(item.text(0)))
+ target_folder.addChildren(sort_folders)
+ sort_items.sort(key=lambda item: get_natural_key(item.text(0)))
+ target_folder.addChildren(sort_items)
diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py
index c68ef63c4..d621115f9 100644
--- a/openlp/core/ui/servicemanager.py
+++ b/openlp/core/ui/servicemanager.py
@@ -1691,6 +1691,8 @@ class ServiceManager(QtWidgets.QWidget, RegistryBase, Ui_ServiceManager, LogMixi
else:
self.drop_position = get_parent_item_data(item) - 1
Registry().execute('{plugin}_add_service_item'.format(plugin=plugin), replace)
+ else:
+ self.log_warning('Unrecognised item')
def update_theme_list(self, theme_list):
"""
diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py
index a1068eea2..d3fad6b6b 100644
--- a/openlp/plugins/bibles/lib/manager.py
+++ b/openlp/plugins/bibles/lib/manager.py
@@ -18,6 +18,7 @@
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see . #
##########################################################################
+import gc
import logging
from pathlib import Path
@@ -141,7 +142,9 @@ class BibleManager(LogMixin, RegistryProperties):
name = bible.get_name()
# Remove corrupted files.
if name is None:
- bible.session.close_all()
+ bible.session.close()
+ bible.session = None
+ gc.collect()
delete_file(self.path / file_path)
continue
log.debug('Bible Name: "{name}"'.format(name=name))
@@ -185,8 +188,9 @@ class BibleManager(LogMixin, RegistryProperties):
"""
log.debug('BibleManager.delete_bible("{name}")'.format(name=name))
bible = self.db_cache[name]
- bible.session.close_all()
+ bible.session.close()
bible.session = None
+ gc.collect()
return delete_file(bible.path / '{name}{suffix}'.format(name=name, suffix=self.suffix))
def get_bibles(self):
diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py
index f29e8ce4f..1115ef123 100644
--- a/openlp/plugins/images/lib/mediaitem.py
+++ b/openlp/plugins/images/lib/mediaitem.py
@@ -97,7 +97,7 @@ class ImageMediaItem(MediaManagerItem):
"""
Set which icons the media manager tab should show.
"""
- MediaManagerItem.required_icons(self)
+ super().required_icons()
self.has_file_icon = True
self.has_new_icon = False
self.has_edit_icon = False
diff --git a/openlp/plugins/media/lib/db.py b/openlp/plugins/media/lib/db.py
new file mode 100644
index 000000000..946ea720c
--- /dev/null
+++ b/openlp/plugins/media/lib/db.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+
+##########################################################################
+# OpenLP - Open Source Lyrics Projection #
+# ---------------------------------------------------------------------- #
+# Copyright (c) 2008-2021 OpenLP Developers #
+# ---------------------------------------------------------------------- #
+# This program 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. #
+# #
+# This program 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 this program. If not, see . #
+##########################################################################
+"""
+The :mod:`~openlp.plugins.media.lib.db` module contains the database layer for the media plugin
+"""
+from sqlalchemy import MetaData
+from sqlalchemy.ext.declarative import declarative_base
+
+from openlp.core.lib.db import FolderMixin, ItemMixin, init_db, init_url
+
+Base = declarative_base(MetaData())
+
+
+class Folder(Base, FolderMixin):
+ """A folder holds items or other folders"""
+
+
+class Item(Base, ItemMixin):
+ """An item is something that can be contained within a folder"""
+
+
+def init_schema(*args, **kwargs):
+ """
+ Set up the media database and initialise the schema
+ """
+ session, metadata = init_db(init_url('media'), base=Base)
+ metadata.create_all(checkfirst=True)
+ return session
diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py
index f23352fb0..fc70d9e07 100644
--- a/openlp/plugins/media/lib/mediaitem.py
+++ b/openlp/plugins/media/lib/mediaitem.py
@@ -21,23 +21,28 @@
import logging
import os
+from pathlib import Path
from PyQt5 import QtCore, QtWidgets
+from sqlalchemy.sql.expression import or_
+from openlp.core.common import delete_file
from openlp.core.common.applocation import AppLocation
-from openlp.core.common.i18n import UiStrings, get_natural_key, translate
-from openlp.core.common.mixins import RegistryProperties
-from openlp.core.common.path import create_paths, path_to_str
+from openlp.core.common.i18n import UiStrings, translate
+from openlp.core.common.path import create_paths
from openlp.core.common.registry import Registry
-from openlp.core.lib import MediaType, ServiceItemContext, check_item_selected
-from openlp.core.lib.mediamanageritem import MediaManagerItem
+from openlp.core.lib import MediaType, ServiceItemContext
+from openlp.core.lib.plugin import StringContent
from openlp.core.lib.serviceitem import ItemCapabilities
-from openlp.core.lib.ui import critical_error_message_box
+from openlp.core.lib.ui import create_action, create_widget_action, critical_error_message_box
from openlp.core.state import State
from openlp.core.ui.icons import UiIcons
+from openlp.core.ui.library import FolderLibraryItem
from openlp.core.ui.media import parse_optical_path, parse_stream_path, format_milliseconds, AUDIO_EXT, VIDEO_EXT
from openlp.core.ui.media.vlcplayer import get_vlc
+from openlp.plugins.media.lib.db import Folder, Item
+
if get_vlc() is not None:
from openlp.plugins.media.forms.mediaclipselectorform import MediaClipSelectorForm
from openlp.plugins.media.forms.streamselectorform import StreamSelectorForm
@@ -47,7 +52,7 @@ if get_vlc() is not None:
log = logging.getLogger(__name__)
-class MediaMediaItem(MediaManagerItem, RegistryProperties):
+class MediaMediaItem(FolderLibraryItem):
"""
This is the custom media manager item for Media Slides.
"""
@@ -57,7 +62,28 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
def __init__(self, parent, plugin):
self.setup()
- super(MediaMediaItem, self).__init__(parent, plugin)
+ super(MediaMediaItem, self).__init__(parent, plugin, Folder, Item)
+
+ def retranslate_ui(self):
+ """
+ This method is called automatically to provide OpenLP with the opportunity to translate the ``MediaManagerItem``
+ to another language.
+ """
+ super().retranslate_ui()
+ self.on_new_prompt = translate('MediaPlugin.MediaItem', 'Select Media')
+
+ def setup_item(self):
+ """
+ Do some additional setup.
+ """
+ self.media_go_live.connect(self.go_live_remote)
+ self.media_add_to_service.connect(self.add_to_service_remote)
+ self.single_service_item = False
+ self.has_search = True
+ self.media_object = None
+ Registry().register_function('mediaitem_media_rebuild', self.rebuild_players)
+ # Allow DnD from the desktop
+ self.list_view.activateDnD()
def setup(self):
"""
@@ -69,33 +95,11 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
self.error_icon = UiIcons().delete
self.clapperboard = UiIcons().clapperboard
- def setup_item(self):
- """
- Do some additional setup.
- """
- self.media_go_live.connect(self.go_live_remote)
- self.media_add_to_service.connect(self.add_to_service_remote)
- self.single_service_item = False
- self.has_search = True
- self.media_object = None
- # self.display_controller = DisplayController(self.parent())
- # Registry().register_function('video_background_replaced', self.video_background_replaced)
- Registry().register_function('mediaitem_media_rebuild', self.rebuild_players)
- # Allow DnD from the desktop
- self.list_view.activateDnD()
-
- def retranslate_ui(self):
- """
- This method is called automatically to provide OpenLP with the opportunity to translate the ``MediaManagerItem``
- to another language.
- """
- self.on_new_prompt = translate('MediaPlugin.MediaItem', 'Select Media')
-
def required_icons(self):
"""
Set which icons the media manager tab should show
"""
- MediaManagerItem.required_icons(self)
+ super().required_icons()
self.has_file_icon = True
self.has_new_icon = False
self.has_edit_icon = False
@@ -106,37 +110,126 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
if State().check_preconditions('media_live'):
self.can_make_live = True
+ def initialise(self):
+ """
+ Initialize media item.
+ """
+ self.log_debug('Initialise')
+ self.list_view.clear()
+ self.list_view.setIndentation(self.list_view.default_indentation)
+ self.list_view.allow_internal_dnd = True
+ self.service_path = AppLocation.get_section_data_path(self.settings_section) / 'thumbnails'
+ create_paths(self.service_path)
+ # Load images from the database
+ self.load_list(self.manager.get_all_objects(Item, order_by_ref=Item.file_path), is_initial_load=True)
+ self.service_path = AppLocation.get_section_data_path('media') / 'thumbnails'
+ self.rebuild_players()
+
def add_list_view_to_toolbar(self):
"""
Creates the main widget for listing items.
"""
- MediaManagerItem.add_list_view_to_toolbar(self)
- # self.list_view.addAction(self.replace_action)
+ super().add_list_view_to_toolbar()
+ if self.has_edit_icon:
+ create_widget_action(
+ self.list_view,
+ text=self.plugin.get_string(StringContent.Edit)['title'],
+ icon=UiIcons().edit,
+ triggers=self.on_edit_click)
+ create_widget_action(self.list_view, separator=True)
+ create_widget_action(
+ self.list_view,
+ 'listView{name}{preview}Item'.format(name=self.plugin.name.title(), preview=StringContent.Preview.title()),
+ text=self.plugin.get_string(StringContent.Preview)['title'],
+ icon=UiIcons().preview,
+ can_shortcuts=True,
+ triggers=self.on_preview_click)
+ create_widget_action(
+ self.list_view,
+ 'listView{name}{live}Item'.format(name=self.plugin.name.title(), live=StringContent.Live.title()),
+ text=self.plugin.get_string(StringContent.Live)['title'],
+ icon=UiIcons().live,
+ can_shortcuts=True,
+ triggers=self.on_live_click)
+ create_widget_action(
+ self.list_view,
+ 'listView{name}{service}Item'.format(name=self.plugin.name.title(), service=StringContent.Service.title()),
+ can_shortcuts=True,
+ text=self.plugin.get_string(StringContent.Service)['title'],
+ icon=UiIcons().add,
+ triggers=self.on_add_click)
+ if self.add_to_service_item:
+ create_widget_action(self.list_view, separator=True)
+ create_widget_action(
+ self.list_view,
+ text=translate('OpenLP.MediaManagerItem', '&Add to selected Service Item'),
+ icon=UiIcons().add,
+ triggers=self.on_add_edit_click)
+ create_widget_action(self.list_view, separator=True)
+ if self.has_delete_icon:
+ create_widget_action(
+ self.list_view,
+ 'listView{name}{delete}Item'.format(name=self.plugin.name.title(), delete=StringContent.Delete.title()),
+ text=self.plugin.get_string(StringContent.Delete)['title'],
+ icon=UiIcons().delete,
+ can_shortcuts=True, triggers=self.on_delete_click)
+ self.add_custom_context_actions()
+ # Create the context menu and add all actions from the list_view.
+ self.menu = QtWidgets.QMenu()
+ self.menu.addActions(self.list_view.actions())
+ self.list_view.doubleClicked.connect(self.on_double_clicked)
+ self.list_view.itemSelectionChanged.connect(self.on_selection_change)
+ self.list_view.customContextMenuRequested.connect(self.context_menu)
- def add_start_header_bar(self):
+ def add_custom_context_actions(self):
"""
- Adds buttons to the start of the header bar.
+ Add custom actions to the context menu.
"""
+ create_widget_action(self.list_view, separator=True)
+ create_widget_action(
+ self.list_view,
+ text=UiStrings().AddFolder, icon=UiIcons().folder, triggers=self.on_add_folder_click)
+ create_widget_action(
+ self.list_view,
+ text=translate('MediaPlugin', 'Add new media'),
+ icon=UiIcons().open, triggers=self.on_file_click)
+ create_widget_action(self.list_view, separator=True)
+
+ def setup_ui(self):
+ """
+ Re-implement to add a dropdown menu to the load icon
+ """
+ super().setup_ui()
if State().check_preconditions('media'):
optical_button_text = translate('MediaPlugin.MediaItem', 'Load CD/DVD')
optical_button_tooltip = translate('MediaPlugin.MediaItem', 'Load CD/DVD')
- self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=UiIcons().optical,
- text=optical_button_text,
- tooltip=optical_button_tooltip,
- triggers=self.on_load_optical)
+ self.load_optical = create_action(self, 'load_optical',
+ icon=UiIcons().optical,
+ text=optical_button_text,
+ tooltip=optical_button_tooltip,
+ triggers=self.on_load_optical)
device_stream_button_text = translate('MediaPlugin.MediaItem', 'Open device stream')
device_stream_button_tooltip = translate('MediaPlugin.MediaItem', 'Open device stream')
- self.open_stream = self.toolbar.add_toolbar_action('open_device_stream', icon=UiIcons().device_stream,
- text=device_stream_button_text,
- tooltip=device_stream_button_tooltip,
- triggers=self.on_open_device_stream)
+ self.open_stream = create_action(self, 'open_device_stream',
+ icon=UiIcons().device_stream,
+ text=device_stream_button_text,
+ tooltip=device_stream_button_tooltip,
+ triggers=self.on_open_device_stream)
network_stream_button_text = translate('MediaPlugin.MediaItem', 'Open network stream')
network_stream_button_tooltip = translate('MediaPlugin.MediaItem', 'Open network stream')
- self.open_network_stream = self.toolbar.add_toolbar_action('open_network_stream',
- icon=UiIcons().network_stream,
- text=network_stream_button_text,
- tooltip=network_stream_button_tooltip,
- triggers=self.on_open_network_stream)
+ self.open_network_stream = create_action(self, 'open_network_stream',
+ icon=UiIcons().network_stream,
+ text=network_stream_button_text,
+ tooltip=network_stream_button_tooltip,
+ triggers=self.on_open_network_stream)
+ self.load_menu = QtWidgets.QMenu(self.toolbar)
+ self.load_menu.setObjectName('load_menu')
+ self.load_menu.addAction(self.load_optical)
+ self.load_menu.addAction(self.open_stream)
+ self.load_menu.addAction(self.open_network_stream)
+ self.toolbar.actions['mediaLoadAction'].setMenu(self.load_menu)
+ button = self.toolbar.widgetForAction(self.toolbar.actions['mediaLoadAction'])
+ button.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
def generate_slide_data(self, service_item, *, item=None, remote=False, context=ServiceItemContext.Service,
**kwargs):
@@ -153,7 +246,12 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
item = self.list_view.currentItem()
if item is None:
return False
- filename = str(item.data(QtCore.Qt.UserRole))
+ if isinstance(item, QtCore.QModelIndex):
+ item = self.list_view.itemFromIndex(item)
+ media_item = item.data(0, QtCore.Qt.UserRole)
+ if not isinstance(media_item, Item):
+ return False
+ filename = media_item.file_path
# Special handling if the filename is a optical clip
if filename.startswith('optical:'):
(name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(filename)
@@ -205,16 +303,6 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
service_item.validate_item()
return True
- def initialise(self):
- """
- Initialize media item.
- """
- self.list_view.clear()
- self.service_path = AppLocation.get_section_data_path('media') / 'thumbnails'
- create_paths(self.service_path)
- self.load_list([path_to_str(file) for file in self.settings.value('media/media files')])
- self.rebuild_players()
-
def rebuild_players(self):
"""
Rebuild the tab in the media manager when changes are made in the settings.
@@ -226,68 +314,91 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
audio=' '.join(AUDIO_EXT),
files=UiStrings().AllFiles)
- def on_delete_click(self):
+ def file_to_item(self, filename):
"""
- Remove a media item from the list.
+ Override the inherited file_to_item() method to handle various tracks
"""
- if check_item_selected(self.list_view,
- translate('MediaPlugin.MediaItem', 'You must select a media file to delete.')):
- row_list = [item.row() for item in self.list_view.selectedIndexes()]
- row_list.sort(reverse=True)
- for row in row_list:
- self.list_view.takeItem(row)
- self.settings.setValue('media/media files', self.get_file_list())
+ if isinstance(filename, Path):
+ name = filename.name
+ filename = str(filename)
+ elif filename.startswith('optical:'):
+ # Handle optical based item
+ _, _, _, _, _, _, name = parse_optical_path(filename)
+ elif filename.startswith('devicestream:') or filename.startswith('networkstream:'):
+ name, _, _ = parse_stream_path(filename)
+ else:
+ name = os.path.basename(filename)
+ item = self.item_class(name=name, file_path=filename)
+ self.manager.save_object(item)
+ return item
- def load_list(self, media, target_group=None):
+ def load_item(self, item, is_initial_load=False):
"""
- Load the media list
+ Given an item object, return a QTreeWidgetItem
+ """
+ track_str = str(item.file_path)
+ track_info = QtCore.QFileInfo(track_str)
+ tree_item = None
+ if track_str.startswith('optical:'):
+ # Handle optical based item
+ (file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track_str)
+ tree_item = QtWidgets.QTreeWidgetItem([clip_name])
+ tree_item.setText(0, clip_name)
+ tree_item.setIcon(0, UiIcons().optical)
+ tree_item.setToolTip(0, '{name}@{start}-{end}'.format(name=file_name,
+ start=format_milliseconds(start),
+ end=format_milliseconds(end)))
+ elif track_str.startswith('devicestream:') or track_str.startswith('networkstream:'):
+ (name, mrl, options) = parse_stream_path(track_str)
+ tree_item = QtWidgets.QTreeWidgetItem([name])
+ tree_item.setText(0, name)
+ if track_str.startswith('devicestream:'):
+ tree_item.setIcon(0, UiIcons().device_stream)
+ else:
+ tree_item.setIcon(0, UiIcons().network_stream)
+ tree_item.setToolTip(0, mrl)
+ elif not Path(item.file_path).exists():
+ # File doesn't exist, mark as error.
+ file_name = Path(item.file_path).name
+ tree_item = QtWidgets.QTreeWidgetItem([file_name])
+ tree_item.setText(0, file_name)
+ tree_item.setIcon(0, UiIcons().error)
+ tree_item.setToolTip(0, track_str)
+ elif track_info.isFile():
+ # Normal media file handling.
+ file_name = Path(item.file_path).name
+ tree_item = QtWidgets.QTreeWidgetItem([file_name])
+ tree_item.setText(0, file_name)
+ search = "*." + file_name.split('.')[-1].lower()
+ if search in AUDIO_EXT:
+ tree_item.setIcon(0, UiIcons().audio)
+ else:
+ tree_item.setIcon(0, UiIcons().video)
+ tree_item.setToolTip(0, track_str)
+ if tree_item:
+ tree_item.setData(0, QtCore.Qt.UserRole, item)
+ return tree_item
- :param media: The media
- :param target_group:
+ def delete_item(self, item):
"""
- media.sort(key=lambda file_path: get_natural_key(os.path.split(str(file_path))[1]))
- for track in media:
- track_str = str(track)
- track_info = QtCore.QFileInfo(track_str)
- item_name = None
- if track_str.startswith('optical:'):
- # Handle optical based item
- (file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track_str)
- item_name = QtWidgets.QListWidgetItem(clip_name)
- item_name.setIcon(UiIcons().optical)
- item_name.setData(QtCore.Qt.UserRole, track)
- item_name.setToolTip('{name}@{start}-{end}'.format(name=file_name,
- start=format_milliseconds(start),
- end=format_milliseconds(end)))
- elif track_str.startswith('devicestream:') or track_str.startswith('networkstream:'):
- (name, mrl, options) = parse_stream_path(track_str)
- item_name = QtWidgets.QListWidgetItem(name)
- if track_str.startswith('devicestream:'):
- item_name.setIcon(UiIcons().device_stream)
- else:
- item_name.setIcon(UiIcons().network_stream)
- item_name.setData(QtCore.Qt.UserRole, track)
- item_name.setToolTip(mrl)
- elif not os.path.exists(track):
- # File doesn't exist, mark as error.
- file_name = os.path.split(track_str)[1]
- item_name = QtWidgets.QListWidgetItem(file_name)
- item_name.setIcon(UiIcons().error)
- item_name.setData(QtCore.Qt.UserRole, track)
- item_name.setToolTip(track_str)
- elif track_info.isFile():
- # Normal media file handling.
- file_name = os.path.split(track_str)[1]
- item_name = QtWidgets.QListWidgetItem(file_name)
- search = "*." + file_name.split('.')[-1].lower()
- if search in AUDIO_EXT:
- item_name.setIcon(UiIcons().audio)
- else:
- item_name.setIcon(UiIcons().video)
- item_name.setData(QtCore.Qt.UserRole, track)
- item_name.setToolTip(track_str)
- if item_name:
- self.list_view.addItem(item_name)
+ Delete any and all thumbnails and things associated with this media item
+ """
+ delete_file(self.service_path / Path(item.file_path).name)
+
+ def format_search_result(self, item):
+ """
+ Format an item for the search results
+
+ :param Item item: An Item to be formatted
+ :return list[str, str]: A list of two items containing the full path and a pretty name
+ """
+ if Path(item.file_path).exists():
+ return [item.file_path, Path(item.file_path).name]
+ elif item.file_path.startswith('device'):
+ (name, _, _) = parse_stream_path(item.file_path)
+ return [item.file_path, name]
+ else:
+ return super().format_search_result(item)
def get_list(self, media_type=MediaType.Audio):
"""
@@ -296,40 +407,16 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
:param media_type: Type to get, defaults to audio.
:return: The media list
"""
- media_file_paths = self.settings.value('media/media files')
- media_file_paths.sort(key=lambda file_path: get_natural_key(os.path.split(str(file_path))[1]))
if media_type == MediaType.Audio:
- extension = AUDIO_EXT
+ extensions = AUDIO_EXT
else:
- extension = VIDEO_EXT
- extension = [x[1:] for x in extension]
- media = [x for x in media_file_paths if os.path.splitext(x)[1] in extension]
- return media
-
- def search(self, string, show_error):
- """
- Performs a search for items containing ``string``
-
- :param string: String to be displayed
- :param show_error: Should the error be shown (True)
- :return: The search result.
- """
- from pathlib import Path
- results = []
- string = string.lower()
- for file_path in self.settings.value('media/media files'):
- if isinstance(file_path, Path):
- file_name = file_path.name
- if file_name.lower().find(string) > -1:
- results.append([str(file_path), file_name])
- else:
- if file_path.lower().find(string) > -1:
- if file_path.startswith('device'):
- (name, _, _) = parse_stream_path(file_path)
- results.append([str(file_path), name])
- else:
- results.append([str(file_path), file_path])
- return results
+ extensions = VIDEO_EXT
+ clauses = []
+ for extension in extensions:
+ # Drop the initial * and add to the list of clauses
+ clauses.append(Item.file_path.endswith(extension[1:]))
+ items = self.manager.get_all_objects(Item, or_(*clauses))
+ return [Path(item.file_path) for item in items]
def on_load_optical(self):
"""
@@ -349,16 +436,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
:param optical: The clip to add.
"""
- file_paths = self.get_file_list()
- # If the clip already is in the media list it isn't added and an error message is displayed.
- if optical in file_paths:
- critical_error_message_box(translate('MediaPlugin.MediaItem', 'Mediaclip already saved'),
- translate('MediaPlugin.MediaItem', 'This mediaclip has already been saved'))
- return
- # Append the optical string to the media list
- file_paths.append(optical)
- self.load_list([str(optical)])
- self.settings.setValue('media/media files', file_paths)
+ self.validate_and_load([str(optical)])
def on_open_device_stream(self):
"""
@@ -378,16 +456,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
:param stream: The clip to add.
"""
- file_paths = self.get_file_list()
- # If the clip already is in the media list it isn't added and an error message is displayed.
- if stream in file_paths:
- critical_error_message_box(translate('MediaPlugin.MediaItem', 'Stream already saved'),
- translate('MediaPlugin.MediaItem', 'This stream has already been saved'))
- return
- # Append the device stream string to the media list
- file_paths.append(stream)
- self.load_list([str(stream)])
- self.settings.setValue('media/media files', file_paths)
+ self.validate_and_load([str(stream)])
def on_open_network_stream(self):
"""
@@ -407,13 +476,4 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
:param stream: The clip to add.
"""
- file_paths = self.get_file_list()
- # If the clip already is in the media list it isn't added and an error message is displayed.
- if stream in file_paths:
- critical_error_message_box(translate('MediaPlugin.MediaItem', 'Stream already saved'),
- translate('MediaPlugin.MediaItem', 'This stream has already been saved'))
- return
- # Append the device stream string to the media list
- file_paths.append(stream)
- self.load_list([str(stream)])
- self.settings.setValue('media/media files', file_paths)
+ self.validate_and_load([str(stream)])
diff --git a/openlp/plugins/media/mediaplugin.py b/openlp/plugins/media/mediaplugin.py
index 31b4083a6..4dc05d740 100644
--- a/openlp/plugins/media/mediaplugin.py
+++ b/openlp/plugins/media/mediaplugin.py
@@ -22,12 +22,19 @@
The Media plugin
"""
import logging
+import os
+from pathlib import Path
-from openlp.core.state import State
+from openlp.core.common import sha256_file_hash
from openlp.core.common.i18n import translate
-from openlp.core.ui.icons import UiIcons
from openlp.core.lib import build_icon
+from openlp.core.lib.db import Manager
from openlp.core.lib.plugin import Plugin, StringContent
+from openlp.core.state import State
+from openlp.core.ui.icons import UiIcons
+from openlp.core.ui.media import parse_optical_path, parse_stream_path
+
+from openlp.plugins.media.lib.db import Item, init_schema
from openlp.plugins.media.lib.mediaitem import MediaMediaItem
@@ -43,7 +50,8 @@ class MediaPlugin(Plugin):
log.info('{name} MediaPlugin loaded'.format(name=__name__))
def __init__(self):
- super(MediaPlugin, self).__init__('media', MediaMediaItem)
+ super().__init__('media', MediaMediaItem)
+ self.manager = Manager(plugin_name='media', init_schema=init_schema)
self.weight = -6
self.icon_path = UiIcons().video
self.icon = build_icon(self.icon_path)
@@ -56,14 +64,29 @@ class MediaPlugin(Plugin):
"""
Override the inherited initialise() method in order to upgrade the media before trying to load it
"""
+ media_files = self.settings.value('media/media files') or []
+ for filename in media_files:
+ if not isinstance(filename, (Path, str)):
+ continue
+ if isinstance(filename, Path):
+ name = filename.name
+ filename = str(filename)
+ elif filename.startswith('devicestream:') or filename.startswith('networkstream:'):
+ name, _, _ = parse_stream_path(filename)
+ elif filename.startswith('optical:'):
+ _, _, _, _, _, _, name = parse_optical_path(filename)
+ else:
+ name = os.path.basename(filename)
+ item = Item(name=name, file_path=filename)
+ if not filename.startswith('devicestream:') and not filename.startswith('networkstream:') \
+ and not filename.startswith('optical:'):
+ file_path = Path(filename)
+ if file_path.is_file() and file_path.exists():
+ item.file_hash = sha256_file_hash(file_path)
+ self.manager.save_object(item)
+ self.settings.remove('media/media files')
super().initialise()
- def app_startup(self):
- """
- Override app_startup() in order to do nothing
- """
- pass
-
def check_pre_conditions(self):
"""
Check the plugin can run and the media controller is available.
diff --git a/openlp/plugins/presentations/lib/db.py b/openlp/plugins/presentations/lib/db.py
new file mode 100644
index 000000000..4e9642e88
--- /dev/null
+++ b/openlp/plugins/presentations/lib/db.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+
+##########################################################################
+# OpenLP - Open Source Lyrics Projection #
+# ---------------------------------------------------------------------- #
+# Copyright (c) 2008-2021 OpenLP Developers #
+# ---------------------------------------------------------------------- #
+# This program 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. #
+# #
+# This program 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 this program. If not, see . #
+##########################################################################
+"""
+The :mod:`~openlp.plugins.presentations.lib.db` module contains the database layer for the presentations plugin
+"""
+from sqlalchemy import MetaData
+from sqlalchemy.ext.declarative import declarative_base
+
+from openlp.core.lib.db import FolderMixin, ItemMixin, init_db, init_url
+
+Base = declarative_base(MetaData())
+
+
+class Folder(Base, FolderMixin):
+ """A folder holds items or other folders"""
+
+
+class Item(Base, ItemMixin):
+ """An item is something that can be contained within a folder"""
+
+
+def init_schema(*args, **kwargs):
+ """
+ Set up the media database and initialise the schema
+ """
+ session, metadata = init_db(init_url('presentations'), base=Base)
+ metadata.create_all(checkfirst=True)
+ return session
diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py
index 9c8d3d1a7..087e4c790 100644
--- a/openlp/plugins/presentations/lib/mediaitem.py
+++ b/openlp/plugins/presentations/lib/mediaitem.py
@@ -26,14 +26,15 @@ from PyQt5 import QtCore, QtWidgets
from pathlib import Path
from openlp.core.common import sha256_file_hash
-from openlp.core.common.i18n import UiStrings, get_natural_key, translate
-from openlp.core.common.path import path_to_str
+from openlp.core.common.i18n import UiStrings, translate
from openlp.core.common.registry import Registry
-from openlp.core.lib import ServiceItemContext, build_icon, check_item_selected, create_thumb, validate_thumb
-from openlp.core.lib.mediamanageritem import MediaManagerItem
+from openlp.core.lib import ServiceItemContext, build_icon, create_thumb, validate_thumb
+from openlp.core.ui.library import FolderLibraryItem
from openlp.core.lib.serviceitem import ItemCapabilities
from openlp.core.lib.ui import create_horizontal_adjusting_combo_box, critical_error_message_box
from openlp.core.ui.icons import UiIcons
+
+from openlp.plugins.presentations.lib.db import Folder, Item
from openlp.plugins.presentations.lib.messagelistener import MessageListener
from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETYPES
@@ -41,7 +42,7 @@ from openlp.plugins.presentations.lib.pdfcontroller import PDF_CONTROLLER_FILETY
log = logging.getLogger(__name__)
-class PresentationMediaItem(MediaManagerItem):
+class PresentationMediaItem(FolderLibraryItem):
"""
This is the Presentation media manager item for Presentation Items. It can present files using Openoffice and
Powerpoint
@@ -56,12 +57,13 @@ class PresentationMediaItem(MediaManagerItem):
"""
self.icon_path = 'presentations/presentation'
self.controllers = controllers
- super(PresentationMediaItem, self).__init__(parent, plugin)
+ super(PresentationMediaItem, self).__init__(parent, plugin, Folder, Item)
def retranslate_ui(self):
"""
The name of the plugin media displayed in UI
"""
+ super().retranslate_ui()
self.on_new_prompt = translate('PresentationPlugin.MediaItem', 'Select Presentation(s)')
self.automatic = translate('PresentationPlugin.MediaItem', 'Automatic')
self.display_type_label.setText(translate('PresentationPlugin.MediaItem', 'Present using:'))
@@ -80,35 +82,31 @@ class PresentationMediaItem(MediaManagerItem):
# Allow DnD from the desktop
self.list_view.activateDnD()
- def build_file_mask_string(self):
- """
- Build the list of file extensions to be used in the Open file dialog.
- """
- file_type_string = ''
- for controller in self.controllers:
- if self.controllers[controller].enabled():
- file_types = self.controllers[controller].supports + self.controllers[controller].also_supports
- for file_type in file_types:
- if file_type not in file_type_string:
- file_type_string += '*.{text} '.format(text=file_type)
- file_type_string = file_type_string.strip()
- self.service_manager.supported_suffixes(file_type_string.split(' '))
- self.on_new_file_masks = translate('PresentationPlugin.MediaItem',
- 'Presentations ({text})').format(text=file_type_string)
-
def required_icons(self):
"""
Set which icons the media manager tab should show.
"""
- MediaManagerItem.required_icons(self)
+ super().required_icons()
self.has_file_icon = True
self.has_new_icon = False
self.has_edit_icon = False
+ def initialise(self):
+ """
+ Populate the media manager tab
+ """
+ self.list_view.clear()
+ self.list_view.setIndentation(self.list_view.default_indentation)
+ self.list_view.allow_internal_dnd = True
+ self.list_view.setIconSize(QtCore.QSize(88, 50))
+ self.load_list(self.manager.get_all_objects(Item, order_by_ref=Item.file_path), is_initial_load=True)
+ self.populate_display_types()
+
def add_middle_header_bar(self):
"""
Display custom media manager items for presentations.
"""
+ super().add_middle_header_bar()
self.presentation_widget = QtWidgets.QWidget(self)
self.presentation_widget.setObjectName('presentation_widget')
self.display_layout = QtWidgets.QFormLayout(self.presentation_widget)
@@ -124,14 +122,21 @@ class PresentationMediaItem(MediaManagerItem):
# Add the Presentation widget to the page layout.
self.page_layout.addWidget(self.presentation_widget)
- def initialise(self):
+ def build_file_mask_string(self):
"""
- Populate the media manager tab
+ Build the list of file extensions to be used in the Open file dialog.
"""
- self.list_view.setIconSize(QtCore.QSize(88, 50))
- file_paths = self.settings.value('presentations/presentations files')
- self.load_list(file_paths, initial_load=True)
- self.populate_display_types()
+ file_type_string = ''
+ for controller in self.controllers:
+ if self.controllers[controller].enabled():
+ file_types = self.controllers[controller].supports + self.controllers[controller].also_supports
+ for file_type in file_types:
+ if file_type not in file_type_string:
+ file_type_string += '*.{text} '.format(text=file_type)
+ file_type_string = file_type_string.strip()
+ self.service_manager.supported_suffixes(file_type_string.split(' '))
+ self.on_new_file_masks = translate('PresentationPlugin.MediaItem',
+ 'Presentations ({text})').format(text=file_type_string)
def populate_display_types(self):
"""
@@ -154,96 +159,57 @@ class PresentationMediaItem(MediaManagerItem):
else:
self.presentation_widget.hide()
- def load_list(self, file_paths, target_group=None, initial_load=False):
+ def load_item(self, item, is_initial_load=False):
"""
- Add presentations into the media manager. This is called both on initial load of the plugin to populate with
- existing files, and when the user adds new files via the media manager.
-
- :param list[pathlib.Path] file_paths: List of file paths to add to the media manager.
- :param list[pathlib.Path] target_group: Group to load.
- :param boolean initial_load: Is this the initial load of the list at start up
+ Given an item object, return a QTreeWidgetItem
"""
- current_paths = self.get_file_list()
- titles = [file_path.name for file_path in current_paths]
- self.application.set_busy_cursor()
- if not initial_load:
- self.main_window.display_progress_bar(len(file_paths))
- # Sort the presentations by its filename considering language specific characters.
- file_paths.sort(key=lambda file_path: get_natural_key(file_path.name))
- for file_path in file_paths:
- if not initial_load:
- self.main_window.increment_progress_bar()
- if current_paths.count(file_path) > 0:
- continue
- file_name = file_path.name
- if not file_path.exists():
- item_name = QtWidgets.QListWidgetItem(file_name)
- item_name.setIcon(UiIcons().delete)
- item_name.setData(QtCore.Qt.UserRole, file_path)
- item_name.setToolTip(str(file_path))
- self.list_view.addItem(item_name)
- else:
- if titles.count(file_name) > 0:
- if not initial_load:
- critical_error_message_box(translate('PresentationPlugin.MediaItem', 'File Exists'),
- translate('PresentationPlugin.MediaItem',
- 'A presentation with that filename already exists.'))
- continue
- controller_name = self.find_controller_by_type(file_path)
- if controller_name:
- controller = self.controllers[controller_name]
- doc = controller.add_document(file_path)
- thumbnail_path = doc.get_thumbnail_folder() / 'icon.png'
+ tree_item = None
+ file_path = Path(item.file_path)
+ file_name = file_path.name
+ if not file_path.exists():
+ tree_item = QtWidgets.QTreeWidgetItem([file_name])
+ tree_item.setIcon(0, UiIcons().delete)
+ tree_item.setData(0, QtCore.Qt.UserRole, item)
+ tree_item.setToolTip(0, str(file_path))
+ else:
+ controller_name = self.find_controller_by_type(file_path)
+ if controller_name:
+ controller = self.controllers[controller_name]
+ doc = controller.add_document(file_path)
+ thumbnail_path = doc.get_thumbnail_folder() / 'icon.png'
+ preview_path = doc.get_thumbnail_path(1, True)
+ if not preview_path and not is_initial_load:
+ doc.load_presentation()
preview_path = doc.get_thumbnail_path(1, True)
- if not preview_path and not initial_load:
- doc.load_presentation()
- preview_path = doc.get_thumbnail_path(1, True)
- doc.close_presentation()
- if not (preview_path and preview_path.exists()):
- icon = UiIcons().delete
- else:
- if validate_thumb(preview_path, thumbnail_path):
- icon = build_icon(thumbnail_path)
- else:
- icon = create_thumb(preview_path, thumbnail_path)
+ doc.close_presentation()
+ if not (preview_path and preview_path.exists()):
+ icon = UiIcons().delete
else:
- if initial_load:
- icon = UiIcons().delete
+ if validate_thumb(preview_path, thumbnail_path):
+ icon = build_icon(thumbnail_path)
else:
- critical_error_message_box(UiStrings().UnsupportedFile,
- translate('PresentationPlugin.MediaItem',
- 'This type of presentation is not supported.'))
- continue
- item_name = QtWidgets.QListWidgetItem(file_name)
- item_name.setData(QtCore.Qt.UserRole, file_path)
- item_name.setIcon(icon)
- item_name.setToolTip(str(file_path))
- self.list_view.addItem(item_name)
- if not initial_load:
- self.main_window.finished_progress_bar()
- self.application.set_normal_cursor()
+ icon = create_thumb(preview_path, thumbnail_path)
+ else:
+ if is_initial_load:
+ icon = UiIcons().delete
+ else:
+ critical_error_message_box(UiStrings().UnsupportedFile,
+ translate('PresentationPlugin.MediaItem',
+ 'This type of presentation is not supported.'))
+ return None
+ tree_item = QtWidgets.QTreeWidgetItem([file_name])
+ tree_item.setData(0, QtCore.Qt.UserRole, item)
+ tree_item.setIcon(0, icon)
+ tree_item.setToolTip(0, str(file_path))
+ return tree_item
- def on_delete_click(self):
+ def delete_item(self, item):
"""
Remove a presentation item from the list.
"""
- if check_item_selected(self.list_view, UiStrings().SelectDelete):
- items = self.list_view.selectedIndexes()
- row_list = [item.row() for item in items]
- row_list.sort(reverse=True)
- self.application.set_busy_cursor()
- self.main_window.display_progress_bar(len(row_list))
- for item in items:
- file_path = item.data(QtCore.Qt.UserRole)
- # cleaning thumbnails depends on being able to calculate sha256 hash of the actual file
- if file_path.exists():
- self.clean_up_thumbnails(file_path)
- self.main_window.increment_progress_bar()
- self.main_window.finished_progress_bar()
- for row in row_list:
- self.list_view.takeItem(row)
- self.settings.setValue('presentations/presentations files', self.get_file_list())
- self.application.set_normal_cursor()
+ file_path = Path(item.file_path)
+ if file_path.exists():
+ self.clean_up_thumbnails(file_path)
def clean_up_thumbnails(self, file_path, clean_for_update=False):
"""
@@ -318,8 +284,10 @@ class PresentationMediaItem(MediaManagerItem):
items = self.list_view.selectedItems()
if len(items) > 1:
return False
+ items = [self.list_view.itemFromIndex(item) if isinstance(item, QtCore.QModelIndex) else item
+ for item in items]
if file_path is None:
- file_path = Path(items[0].data(QtCore.Qt.UserRole))
+ file_path = Path(items[0].data(0, QtCore.Qt.UserRole).file_path)
file_type = file_path.suffix.lower()[1:]
if not self.display_type_combo_box.currentText():
return False
@@ -335,7 +303,7 @@ class PresentationMediaItem(MediaManagerItem):
service_item.theme = -1
for bitem in items:
if file_path is None:
- file_path = bitem.data(QtCore.Qt.UserRole)
+ file_path = Path(bitem.data(0, QtCore.Qt.UserRole).file_path)
path, file_name = file_path.parent, file_path.name
service_item.title = file_name
if file_path.exists():
@@ -372,7 +340,7 @@ class PresentationMediaItem(MediaManagerItem):
service_item.processor = self.display_type_combo_box.currentText()
service_item.add_capability(ItemCapabilities.ProvidesOwnDisplay)
for bitem in items:
- file_path = Path(bitem.data(QtCore.Qt.UserRole))
+ file_path = Path(bitem.data(0, QtCore.Qt.UserRole).file_path)
path, file_name = file_path.parent, file_path.name
service_item.title = file_name
if file_path.exists():
@@ -448,20 +416,3 @@ class PresentationMediaItem(MediaManagerItem):
if file_type in self.controllers[controller].also_supports:
return controller
return None
-
- def search(self, string, show_error):
- """
- Search in files
-
- :param string: name to be found
- :param show_error: not used
- :return:
- """
- file_paths = self.settings.value('presentations/presentations files')
- results = []
- string = string.lower()
- for file_path in file_paths:
- file_name = file_path.name
- if file_name.lower().find(string) > -1:
- results.append([path_to_str(file_path), file_name])
- return results
diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py
index 5ac951eb2..7e86c7928 100644
--- a/openlp/plugins/presentations/presentationplugin.py
+++ b/openlp/plugins/presentations/presentationplugin.py
@@ -26,12 +26,15 @@ import logging
import os
-from openlp.core.common import extension_loader
+from openlp.core.common import extension_loader, sha256_file_hash
from openlp.core.common.i18n import translate
from openlp.core.lib import build_icon
+from openlp.core.lib.db import Manager
from openlp.core.lib.plugin import Plugin, StringContent
from openlp.core.state import State
from openlp.core.ui.icons import UiIcons
+
+from openlp.plugins.presentations.lib.db import Item, init_schema
from openlp.plugins.presentations.lib.presentationcontroller import PresentationController
from openlp.plugins.presentations.lib.mediaitem import PresentationMediaItem
from openlp.plugins.presentations.lib.presentationtab import PresentationTab
@@ -51,12 +54,13 @@ class PresentationPlugin(Plugin):
"""
PluginPresentation constructor.
"""
- log.debug('Initialised')
- self.controllers = {}
- Plugin.__init__(self, 'presentations', None)
+ super().__init__('presentations', PresentationMediaItem)
+ self.manager = Manager(plugin_name='media', init_schema=init_schema)
self.weight = -8
self.icon_path = UiIcons().presentation
self.icon = build_icon(self.icon_path)
+ self.controllers = {}
+ self.dnd_id = 'Presentations'
State().add_service('presentation', self.weight, is_plugin=True)
State().update_pre_conditions('presentation', self.check_pre_conditions())
@@ -73,7 +77,25 @@ class PresentationPlugin(Plugin):
Initialise the plugin. Determine which controllers are enabled are start their processes.
"""
log.info('Presentations Initialising')
- super(PresentationPlugin, self).initialise()
+ # Check if the thumbnail scheme needs to be updated
+ has_old_scheme = False
+ if self.settings.value('presentations/thumbnail_scheme') != 'sha256file':
+ self.settings.setValue('presentations/thumbnail_scheme', 'sha256file')
+ has_old_scheme = True
+ # Migrate each file
+ presentation_paths = self.settings.value('presentations/presentations files') or []
+ for path in presentation_paths:
+ # check to see if the file exists before trying to process it.
+ if not path.exists():
+ continue
+ item = Item(name=path.name, file_path=str(path))
+ self.media_item.clean_up_thumbnails(path, clean_for_update=True)
+ item.file_hash = sha256_file_hash(path)
+ if has_old_scheme:
+ self.media_item.update_thumbnail_scheme(path)
+ self.manager.save_object(item)
+ self.settings.remove('presentations/presentations files')
+ super().initialise()
for controller in self.controllers:
if self.controllers[controller].enabled():
try:
@@ -128,26 +150,6 @@ class PresentationPlugin(Plugin):
self.register_controllers(controller)
return bool(self.controllers)
- def app_startup(self):
- """
- Perform tasks on application startup.
- """
- # TODO: Can be removed when the upgrade path to OpenLP 3.0 is no longer needed, also ensure code in
- # PresentationDocument.get_thumbnail_folder and PresentationDocument.get_temp_folder is removed
- super().app_startup()
- presentation_paths = self.settings.value('presentations/presentations files')
- for path in presentation_paths:
- # check to see if the file exists before trying to process it.
- if path.exists():
- self.media_item.clean_up_thumbnails(path, clean_for_update=True)
- self.media_item.list_view.clear()
- # Update the thumbnail scheme if needed
- if self.settings.value('presentations/thumbnail_scheme') != 'sha256file':
- for path in presentation_paths:
- self.media_item.update_thumbnail_scheme(path)
- self.settings.setValue('presentations/thumbnail_scheme', 'sha256file')
- self.media_item.validate_and_load(presentation_paths)
-
@staticmethod
def about():
"""
diff --git a/tests/openlp_plugins/bibles/test_manager.py b/tests/openlp_plugins/bibles/test_manager.py
index 4efed5d8c..cf717c3e0 100644
--- a/tests/openlp_plugins/bibles/test_manager.py
+++ b/tests/openlp_plugins/bibles/test_manager.py
@@ -36,9 +36,9 @@ def test_delete_bible(settings):
patch('openlp.plugins.bibles.lib.manager.delete_file', return_value=True) as mocked_delete_file:
instance = BibleManager(MagicMock())
# We need to keep a reference to the mock for close_all as it gets set to None later on!
- mocked_close_all = MagicMock()
+ mocked_close = MagicMock()
mocked_bible = MagicMock(file_path='KJV.sqlite', path=Path('bibles'),
- **{'session.close_all': mocked_close_all})
+ **{'session.close': mocked_close})
instance.db_cache = {'KJV': mocked_bible}
# WHEN: Calling delete_bible with 'KJV'
@@ -47,6 +47,6 @@ def test_delete_bible(settings):
# THEN: The session should have been closed and set to None, the bible should be deleted, and the result of
# the deletion returned.
assert result is True
- mocked_close_all.assert_called_once_with()
+ mocked_close.assert_called_once_with()
assert mocked_bible.session is None
mocked_delete_file.assert_called_once_with(Path('bibles') / 'KJV.sqlite')
diff --git a/tests/openlp_plugins/media/test_mediaitem.py b/tests/openlp_plugins/media/test_mediaitem.py
index 90fac5f02..554da47dc 100644
--- a/tests/openlp_plugins/media/test_mediaitem.py
+++ b/tests/openlp_plugins/media/test_mediaitem.py
@@ -23,9 +23,11 @@ Test the media plugin
"""
import pytest
from pathlib import Path
-from unittest.mock import MagicMock, patch
+from unittest.mock import MagicMock, call, patch
+from openlp.core.lib import MediaType
from openlp.core.common.registry import Registry
+from openlp.core.ui.media import AUDIO_EXT, VIDEO_EXT
from openlp.plugins.media.lib.mediaitem import MediaMediaItem
@@ -37,9 +39,10 @@ def media_item(settings):
Registry().register('main_window', mocked_main_window)
Registry().register('live_controller', MagicMock())
mocked_plugin = MagicMock()
- with patch('openlp.plugins.media.lib.mediaitem.MediaManagerItem._setup'), \
+ with patch('openlp.plugins.media.lib.mediaitem.FolderLibraryItem._setup'), \
patch('openlp.plugins.media.lib.mediaitem.MediaMediaItem.setup_item'):
m_item = MediaMediaItem(None, mocked_plugin)
+ m_item.manager = MagicMock()
return m_item
@@ -48,21 +51,11 @@ def test_search_found(media_item):
Media Remote Search Successful find
"""
# GIVEN: The Mediaitem set up a list of media
- media_item.settings.setValue('media/media files', [Path('test.mp3'), Path('test.mp4')])
+ media_item.manager.get_all_objects.return_value = [MagicMock(file_path='test.mp4')]
+
# WHEN: Retrieving the test file
result = media_item.search('test.mp4', False)
- # THEN: a file should be found
- assert result == [['test.mp4', 'test.mp4']], 'The result file contain the file name'
-
-def test_search_found_mixed(media_item):
- """
- Media Remote Search Successful find with Paths and Strings
- """
- # GIVEN: The Mediaitem set up a list of media
- media_item.settings.setValue('media/media files', [Path('test.mp3'), 'test.mp4'])
- # WHEN: Retrieving the test file
- result = media_item.search('test.mp4', False)
# THEN: a file should be found
assert result == [['test.mp4', 'test.mp4']], 'The result file contain the file name'
@@ -72,8 +65,91 @@ def test_search_not_found(media_item):
Media Remote Search not find
"""
# GIVEN: The Mediaitem set up a list of media
- media_item.settings.setValue('media/media files', [Path('test.mp3'), Path('test.mp4')])
+ media_item.manager.get_all_objects.return_value = []
+
# WHEN: Retrieving the test file
result = media_item.search('test.mpx', False)
+
# THEN: a file should be found
assert result == [], 'The result file should be empty'
+
+
+@patch('openlp.plugins.media.lib.mediaitem.or_')
+@patch('openlp.plugins.media.lib.mediaitem.Item')
+def test_get_list_audio(MockItem, mocked_or, media_item):
+ """
+ Test the ``MediaMediaItem.get_list()`` method to return all audio items
+ """
+ # GIVEN: A mocked Item class and some mocked items to return
+ media_item.manager.get_all_objects.return_value = [MagicMock(file_path='test1.mp3'),
+ MagicMock(file_path='test2.ogg')]
+ MockItem.file_path.endswith.side_effect = lambda ext: ext
+ mocked_or.side_effect = lambda *ext: list(ext)
+
+ # WHEN: get_list() is called
+ media_list = media_item.get_list(MediaType.Audio)
+
+ # THEN: All the clauses should have been created, and the right extensions used
+ expected_extensions = [ext[1:] for ext in AUDIO_EXT]
+ expected_calls = [call(ext) for ext in expected_extensions]
+ MockItem.file_path.endswith.assert_has_calls(expected_calls)
+ mocked_or.assert_called_once_with(*expected_extensions)
+ media_item.manager.get_all_objects.assert_called_once_with(MockItem, expected_extensions)
+ assert media_list == [Path('test1.mp3'), Path('test2.ogg')]
+
+
+@patch('openlp.plugins.media.lib.mediaitem.or_')
+@patch('openlp.plugins.media.lib.mediaitem.Item')
+def test_get_list_video(MockItem, mocked_or, media_item):
+ """
+ Test the ``MediaMediaItem.get_list()`` method to return all audio items
+ """
+ # GIVEN: A mocked Item class and some mocked items to return
+ media_item.manager.get_all_objects.return_value = [MagicMock(file_path='test1.mp4'),
+ MagicMock(file_path='test2.ogv')]
+ MockItem.file_path.endswith.side_effect = lambda ext: ext
+ mocked_or.side_effect = lambda *ext: list(ext)
+
+ # WHEN: get_list() is called
+ media_list = media_item.get_list(MediaType.Video)
+
+ # THEN: All the clauses should have been created, and the right extensions used
+ expected_extensions = [ext[1:] for ext in VIDEO_EXT]
+ expected_calls = [call(ext) for ext in expected_extensions]
+ MockItem.file_path.endswith.assert_has_calls(expected_calls)
+ mocked_or.assert_called_once_with(*expected_extensions)
+ media_item.manager.get_all_objects.assert_called_once_with(MockItem, expected_extensions)
+ assert media_list == [Path('test1.mp4'), Path('test2.ogv')]
+
+
+def test_add_optical_clip(media_item):
+ """Test that the ``MediaMediaItem.add_optical_clip()`` method calls validate_and_load()"""
+ # GIVEN: A MediaMediaItem object, with a mocked out `validate_and_load` method
+ with patch.object(media_item, 'validate_and_load') as mocked_validate_and_load:
+ # WHEN: add_optical_clip is called
+ media_item.add_optical_clip('optical:/dev/sr0')
+
+ # THEN: The mocked method should have been called
+ mocked_validate_and_load.assert_called_once_with(['optical:/dev/sr0'])
+
+
+def test_add_device_stream(media_item):
+ """Test that the ``MediaMediaItem.add_device_stream()`` method calls validate_and_load()"""
+ # GIVEN: A MediaMediaItem object, with a mocked out `validate_and_load` method
+ with patch.object(media_item, 'validate_and_load') as mocked_validate_and_load:
+ # WHEN: add_device_stream is called
+ media_item.add_device_stream('devicestream:/dev/v4l')
+
+ # THEN: The mocked method should have been called
+ mocked_validate_and_load.assert_called_once_with(['devicestream:/dev/v4l'])
+
+
+def test_add_network_stream(media_item):
+ """Test that the ``MediaMediaItem.add_network_stream()`` method calls validate_and_load()"""
+ # GIVEN: A MediaMediaItem object, with a mocked out `validate_and_load` method
+ with patch.object(media_item, 'validate_and_load') as mocked_validate_and_load:
+ # WHEN: add_network_stream is called
+ media_item.add_network_stream('networkstream:rmtp://localhost')
+
+ # THEN: The mocked method should have been called
+ mocked_validate_and_load.assert_called_once_with(['networkstream:rmtp://localhost'])
diff --git a/tests/openlp_plugins/presentations/conftest.py b/tests/openlp_plugins/presentations/conftest.py
index b910058fe..beea363a5 100644
--- a/tests/openlp_plugins/presentations/conftest.py
+++ b/tests/openlp_plugins/presentations/conftest.py
@@ -29,13 +29,13 @@ from openlp.plugins.presentations.lib.mediaitem import PresentationMediaItem
@pytest.fixture
-def media_item(settings):
+def media_item(settings, mock_plugin):
"""Local test setup"""
Registry().register('service_manager', MagicMock())
Registry().register('main_window', MagicMock())
- with patch('openlp.plugins.presentations.lib.mediaitem.MediaManagerItem._setup'), \
+ with patch('openlp.plugins.presentations.lib.mediaitem.FolderLibraryItem._setup'), \
patch('openlp.plugins.presentations.lib.mediaitem.PresentationMediaItem.setup_item'):
- m_item = PresentationMediaItem(None, MagicMock, MagicMock())
+ m_item = PresentationMediaItem(None, mock_plugin, MagicMock())
m_item.settings_section = 'media'
return m_item
@@ -44,4 +44,5 @@ def media_item(settings):
def mock_plugin(temp_folder):
m_plugin = MagicMock()
m_plugin.settings_section = temp_folder
+ m_plugin.manager = MagicMock()
yield m_plugin
diff --git a/tests/openlp_plugins/presentations/test_mediaitem.py b/tests/openlp_plugins/presentations/test_mediaitem.py
index f537f555b..93367049a 100644
--- a/tests/openlp_plugins/presentations/test_mediaitem.py
+++ b/tests/openlp_plugins/presentations/test_mediaitem.py
@@ -24,7 +24,6 @@ This module contains tests for the lib submodule of the Presentations plugin.
from pathlib import Path
from unittest.mock import MagicMock, PropertyMock, call, patch
-from openlp.core.common.registry import Registry
from openlp.core.lib import ServiceItemContext
from openlp.core.lib.serviceitem import ItemCapabilities
from openlp.plugins.presentations.lib.mediaitem import PresentationMediaItem
@@ -146,24 +145,37 @@ def test_pdf_generate_slide_data(media_item):
mocked_service_item.add_capability.assert_any_call(ItemCapabilities.ProvidesOwnTheme)
-@patch('openlp.plugins.presentations.lib.mediaitem.MediaManagerItem._setup')
+@patch('openlp.plugins.presentations.lib.mediaitem.FolderLibraryItem._setup')
@patch('openlp.plugins.presentations.lib.mediaitem.PresentationMediaItem.setup_item')
-def test_search(mock_setup, mock_item, registry):
+def test_search_found(mock_setup, mock_item, registry):
"""
- Test that the search method finds the correct results
+ Test that the search method works correctly
"""
- # GIVEN: A mocked Settings class which returns a list of Path objects,
- # and an instance of the PresentationMediaItem
- path_1 = Path('some_dir', 'Impress_file_1')
- path_2 = Path('some_other_dir', 'impress_file_2')
- path_3 = Path('another_dir', 'ppt_file')
- mocked_returned_settings = MagicMock()
- mocked_returned_settings.value.return_value = [path_1, path_2, path_3]
- Registry().register('settings', mocked_returned_settings)
+ # GIVEN: The Mediaitem set up a list of presentations
media_item = PresentationMediaItem(None, MagicMock(), None)
+ media_item.manager = MagicMock()
+ media_item.manager.get_all_objects.return_value = [MagicMock(file_path='test.odp')]
- # WHEN: Calling search
- results = media_item.search('IMPRE', False)
+ # WHEN: Retrieving the test file
+ result = media_item.search('test.odp', False)
- # THEN: The first two results should have been returned
- assert results == [[str(path_1), 'Impress_file_1'], [str(path_2), 'impress_file_2']]
+ # THEN: a file should be found
+ assert result == [['test.odp', 'test.odp']], 'The result file contain the file name'
+
+
+@patch('openlp.plugins.presentations.lib.mediaitem.FolderLibraryItem._setup')
+@patch('openlp.plugins.presentations.lib.mediaitem.PresentationMediaItem.setup_item')
+def test_search_not_found(mock_setup, mock_item, registry):
+ """
+ Test that the search doesn't find anything
+ """
+ # GIVEN: The Mediaitem set up a list of media
+ media_item = PresentationMediaItem(None, MagicMock(), None)
+ media_item.manager = MagicMock()
+ media_item.manager.get_all_objects.return_value = []
+
+ # WHEN: Retrieving the test file
+ result = media_item.search('test.pptx', False)
+
+ # THEN: a file should be found
+ assert result == [], 'The result file should be empty'