Merge branch 'migrate-images-to-folder-classes' into 'master'

Migrate Images plugin to use shared folder code

See merge request openlp/openlp!593
This commit is contained in:
Raoul Snyman 2023-03-14 22:31:59 +00:00
commit 1fc1bd4124
6 changed files with 234 additions and 789 deletions

View File

@ -21,16 +21,17 @@
import logging
from openlp.core.state import State
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.images.lib import upgrade
from openlp.plugins.images.lib.mediaitem import ImageMediaItem
from openlp.plugins.images.lib.imagetab import ImageTab
from openlp.plugins.images.lib.db import init_schema
from openlp.plugins.images.lib.imagetab import ImageTab
from openlp.plugins.images.lib.mediaitem import ImageMediaItem
log = logging.getLogger(__name__)

View File

@ -21,9 +21,8 @@
"""
The :mod:`db` module provides the database and schema that is the backend for the Images plugin.
"""
from sqlalchemy import Column, ForeignKey, MetaData
from sqlalchemy import MetaData
from sqlalchemy.orm import Session
from sqlalchemy.types import Integer, Unicode
# Maintain backwards compatibility with older versions of SQLAlchemy while supporting SQLAlchemy 1.4+
try:
@ -31,31 +30,22 @@ try:
except ImportError:
from sqlalchemy.ext.declarative import declarative_base
from openlp.core.lib.db import PathType, init_db
from openlp.core.lib.db import FolderMixin, ItemMixin, init_db
Base = declarative_base(MetaData())
class ImageGroups(Base):
class Folder(Base, FolderMixin):
"""
ImageGroups model.
Folder model.
"""
__tablename__ = 'image_groups'
id = Column(Integer(), primary_key=True)
parent_id = Column(Integer())
group_name = Column(Unicode(128))
class ImageFilenames(Base):
class Item(Base, ItemMixin):
"""
ImageFilenames model.
Item model.
"""
__tablename__ = 'image_filenames'
id = Column(Integer(), primary_key=True)
group_id = Column(Integer(), ForeignKey('image_groups.id'), default=None)
file_path = Column(PathType(), nullable=False)
file_hash = Column(Unicode(128), nullable=False)
def init_schema(url: str) -> Session:

View File

@ -24,28 +24,25 @@ from pathlib import Path
from PyQt5 import QtCore, QtWidgets
from openlp.core.common import delete_file, get_images_filter, sha256_file_hash
from openlp.core.common import delete_file, get_images_filter
from openlp.core.common.applocation import AppLocation
from openlp.core.common.i18n import UiStrings, get_natural_key, translate
from openlp.core.common.enum import ImageThemeMode
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.common.enum import ImageThemeMode
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.plugin import StringContent
from openlp.core.lib.serviceitem import ItemCapabilities
from openlp.core.lib.ui import create_widget_action, critical_error_message_box
from openlp.core.ui.icons import UiIcons
from openlp.core.widgets.views import TreeWidgetWithDnD
from openlp.plugins.images.forms.addgroupform import AddGroupForm
from openlp.plugins.images.forms.choosegroupform import ChooseGroupForm
from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups
from openlp.core.ui.library import FolderLibraryItem
from openlp.plugins.images.lib.db import Folder, Item
log = logging.getLogger(__name__)
class ImageMediaItem(MediaManagerItem):
class ImageMediaItem(FolderLibraryItem):
"""
This is the custom media manager item for images.
"""
@ -56,9 +53,7 @@ class ImageMediaItem(MediaManagerItem):
def __init__(self, parent, plugin):
self.icon_path = 'images/image'
self.manager = None
self.choose_group_form = None
self.add_group_form = None
super(ImageMediaItem, self).__init__(parent, plugin)
super(ImageMediaItem, self).__init__(parent, plugin, Folder, Item)
def setup_item(self):
"""
@ -68,22 +63,17 @@ class ImageMediaItem(MediaManagerItem):
self.images_add_to_service.connect(self.add_to_service_remote)
self.quick_preview_allowed = True
self.has_search = True
self.manager = self.plugin.manager
self.choose_group_form = ChooseGroupForm(self)
self.add_group_form = AddGroupForm(self)
self.fill_groups_combobox(self.choose_group_form.group_combobox)
self.fill_groups_combobox(self.add_group_form.parent_group_combobox)
self.single_service_item = False
Registry().register_function('live_theme_changed', self.on_display_changed)
Registry().register_function('slidecontroller_live_started', self.on_display_changed)
# Allow DnD from the desktop.
self.list_view.activateDnD()
def retranslate_ui(self):
super().retranslate_ui()
self.on_new_prompt = translate('ImagePlugin.MediaItem', 'Select Image(s)')
file_formats = get_images_filter()
self.on_new_file_masks = '{formats};;{files} (*)'.format(formats=file_formats, files=UiStrings().AllFiles)
self.add_group_action.setText(UiStrings().AddGroupDot)
self.add_group_action.setToolTip(UiStrings().AddGroupDot)
self.replace_action.setText(UiStrings().ReplaceBG)
self.replace_action.setToolTip(UiStrings().ReplaceLiveBG)
self.replace_action_context.setText(UiStrings().ReplaceBG)
@ -112,101 +102,23 @@ class ImageMediaItem(MediaManagerItem):
self.service_path = AppLocation.get_section_data_path(self.settings_section) / 'thumbnails'
create_paths(self.service_path)
# Load images from the database
self.load_full_list(
self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.file_path), initial_load=True)
def add_list_view_to_toolbar(self):
"""
Creates the main widget for listing items the media item is tracking. This method overloads
MediaManagerItem.add_list_view_to_toolbar.
"""
# Add the List widget
self.list_view = TreeWidgetWithDnD(self, self.plugin.name)
self.list_view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.list_view.setAlternatingRowColors(True)
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)
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)
self.list_view.addAction(self.replace_action)
self.load_list(self.manager.get_all_objects(Item, order_by_ref=Item.file_path), is_initial_load=True)
def add_custom_context_actions(self):
"""
Add custom actions to the context menu.
"""
create_widget_action(self.list_view, separator=True)
create_widget_action(
self.list_view,
text=UiStrings().AddGroup, icon=UiIcons().folder, triggers=self.on_add_group_click)
create_widget_action(
self.list_view,
text=translate('ImagePlugin', 'Add new image(s)'),
icon=UiIcons().open, triggers=self.on_file_click)
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('ImagePlugin', 'Add new image(s)'),
icon=UiIcons().open, triggers=self.on_file_click)
create_widget_action(self.list_view, separator=True)
self.replace_action_context = create_widget_action(
self.list_view, text=UiStrings().ReplaceBG, icon=UiIcons().theme, triggers=self.on_replace_click)
self.reset_action_context = create_widget_action(
self.list_view, text=UiStrings().ReplaceLiveBG, icon=UiIcons().close,
visible=False, triggers=self.on_reset_click)
def add_middle_header_bar(self):
"""
Add custom buttons to the start of the toolbar.
"""
self.add_group_action = self.toolbar.add_toolbar_action('add_group_action',
icon=UiIcons().folder,
triggers=self.on_add_group_click)
self.replace_action_context = create_widget_action(self.list_view, text=UiStrings().ReplaceBG,
icon=UiIcons().theme, triggers=self.on_replace_click)
self.reset_action_context = create_widget_action(self.list_view, text=UiStrings().ReplaceLiveBG,
icon=UiIcons().close, visible=False,
triggers=self.on_reset_click)
def add_end_header_bar(self):
"""
@ -219,329 +131,49 @@ class ImageMediaItem(MediaManagerItem):
icon=UiIcons().close,
visible=False, triggers=self.on_reset_click)
def recursively_delete_group(self, image_group):
"""
Recursively deletes a group and all groups and images in it.
:param image_group: The ImageGroups instance of the group that will be deleted.
"""
images = self.manager.get_all_objects(ImageFilenames, ImageFilenames.group_id == image_group.id)
for image in images:
delete_file(self.service_path / image.file_path.name)
delete_file(self.generate_thumbnail_path(image))
self.manager.delete_object(ImageFilenames, image.id)
image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == image_group.id)
for group in image_groups:
self.recursively_delete_group(group)
self.manager.delete_object(ImageGroups, group.id)
def on_delete_click(self):
"""
Remove an image item from the list.
"""
# Turn off auto preview triggers.
self.list_view.blockSignals(True)
if check_item_selected(self.list_view, translate('ImagePlugin.MediaItem',
'You must select an image or group to delete.')):
item_list = self.list_view.selectedItems()
self.application.set_busy_cursor()
self.main_window.display_progress_bar(len(item_list))
for row_item in item_list:
if row_item:
item_data = row_item.data(0, QtCore.Qt.UserRole)
if isinstance(item_data, ImageFilenames):
delete_file(self.service_path / row_item.text(0))
delete_file(self.generate_thumbnail_path(item_data))
if item_data.group_id == 0:
self.list_view.takeTopLevelItem(self.list_view.indexOfTopLevelItem(row_item))
else:
row_item.parent().removeChild(row_item)
self.manager.delete_object(ImageFilenames, row_item.data(0, QtCore.Qt.UserRole).id)
elif isinstance(item_data, ImageGroups):
if QtWidgets.QMessageBox.question(
self.list_view.parent(),
translate('ImagePlugin.MediaItem', 'Remove group'),
translate('ImagePlugin.MediaItem',
'Are you sure you want to remove "{name}" and everything in it?'
).format(name=item_data.group_name)
) == QtWidgets.QMessageBox.Yes:
self.recursively_delete_group(item_data)
self.manager.delete_object(ImageGroups, row_item.data(0, QtCore.Qt.UserRole).id)
if item_data.parent_id == 0:
self.list_view.takeTopLevelItem(self.list_view.indexOfTopLevelItem(row_item))
else:
row_item.parent().removeChild(row_item)
self.fill_groups_combobox(self.choose_group_form.group_combobox)
self.fill_groups_combobox(self.add_group_form.parent_group_combobox)
self.main_window.increment_progress_bar()
self.main_window.finished_progress_bar()
self.application.set_normal_cursor()
self.list_view.blockSignals(False)
def add_sub_groups(self, group_list, parent_group_id):
"""
Recursively add subgroups to the given parent group in a QTreeWidget.
:param group_list: The List object that contains all QTreeWidgetItems.
:param parent_group_id: The ID of the group that will be added recursively.
"""
image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == parent_group_id)
image_groups.sort(key=lambda group_object: get_natural_key(group_object.group_name))
folder_icon = UiIcons().folder
for image_group in image_groups:
group = QtWidgets.QTreeWidgetItem()
group.setText(0, image_group.group_name)
group.setData(0, QtCore.Qt.UserRole, image_group)
group.setIcon(0, folder_icon)
if parent_group_id == 0:
self.list_view.addTopLevelItem(group)
else:
group_list[parent_group_id].addChild(group)
group_list[image_group.id] = group
self.add_sub_groups(group_list, image_group.id)
def fill_groups_combobox(self, combobox, parent_group_id=0, prefix=''):
"""
Recursively add groups to the combobox in the 'Add group' dialog.
:param combobox: The QComboBox to add the options to.
:param parent_group_id: The ID of the group that will be added.
:param prefix: A string containing the prefix that will be added in front of the groupname for each level of
the tree.
"""
if parent_group_id == 0:
combobox.clear()
combobox.top_level_group_added = False
image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == parent_group_id)
image_groups.sort(key=lambda group_object: get_natural_key(group_object.group_name))
for image_group in image_groups:
combobox.addItem(prefix + image_group.group_name, image_group.id)
self.fill_groups_combobox(combobox, image_group.id, prefix + ' ')
def expand_group(self, group_id, root_item=None):
"""
Expand groups in the widget recursively.
:param group_id: The ID of the group 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_group(group_id, child):
child.setExpanded(True)
return_value = True
if isinstance(root_item.data(0, QtCore.Qt.UserRole), ImageGroups):
if root_item.data(0, QtCore.Qt.UserRole).id == group_id:
return True
return return_value
def generate_thumbnail_path(self, image):
def generate_thumbnail_path(self, item: Item) -> Path:
"""
Generate a path to the thumbnail
:param openlp.plugins.images.lib.db.ImageFilenames image: The image to generate the thumbnail path for.
:param openlp.plugins.images.lib.db.Item image: The image to generate the thumbnail path for.
:return: A path to the thumbnail
:rtype: Path
"""
ext = image.file_path.suffix.lower()
return self.service_path / '{name:s}{ext}'.format(name=image.file_hash or image.file_path.stem, ext=ext)
file_path = Path(item.file_path)
ext = file_path.suffix.lower()
return self.service_path / '{name:s}{ext}'.format(name=item.file_hash or file_path.stem, ext=ext)
def load_full_list(self, images, initial_load=False, open_group=None):
"""
Replace the list of images and groups in the interface.
:param list[openlp.plugins.images.lib.db.ImageFilenames] images: A List of Image Filenames objects that will be
used to reload the mediamanager list.
:param initial_load: When set to False, the busy cursor and progressbar will be shown while loading images.
:param open_group: ImageGroups object of the group that must be expanded after reloading the list in the
interface.
"""
if not initial_load:
self.application.set_busy_cursor()
self.main_window.display_progress_bar(len(images))
self.list_view.clear()
# Load the list of groups and add them to the treeView.
group_items = {}
self.add_sub_groups(group_items, parent_group_id=0)
if open_group is not None:
self.expand_group(open_group.id)
# Sort the images by its filename considering language specific.
# characters.
images.sort(key=lambda image_object: get_natural_key(image_object.file_path.name))
for image in images:
log.debug('Loading image: {name}'.format(name=image.file_path))
file_name = image.file_path.name
thumbnail_path = self.generate_thumbnail_path(image)
if not image.file_path.exists():
icon = UiIcons().delete
else:
if validate_thumb(image.file_path, thumbnail_path):
icon = build_icon(thumbnail_path)
else:
icon = create_thumb(image.file_path, thumbnail_path)
item_name = QtWidgets.QTreeWidgetItem([file_name])
item_name.setText(0, file_name)
item_name.setIcon(0, icon)
item_name.setToolTip(0, str(image.file_path))
item_name.setData(0, QtCore.Qt.UserRole, image)
if image.group_id == 0:
self.list_view.addTopLevelItem(item_name)
else:
group_items[image.group_id].addChild(item_name)
if not initial_load:
self.main_window.increment_progress_bar()
if not initial_load:
self.main_window.finished_progress_bar()
self.application.set_normal_cursor()
def validate_and_load(self, file_paths, target_group=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()
self.load_list(file_paths, target_group)
last_dir = file_paths[0].parent
self.settings.setValue('images/last directory', last_dir)
def load_list(self, image_paths, target_group=None, initial_load=False):
"""
Add new images to the database. This method is called when adding images using the Add button or DnD.
:param list[Path] image_paths: A list of file paths to the images to be loaded
:param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
:param initial_load: When set to False, the busy cursor and progressbar will be shown while loading images
"""
parent_group = None
if target_group is None:
# Find out if a group must be pre-selected
preselect_group = None
selected_items = self.list_view.selectedItems()
if selected_items:
selected_item = selected_items[0]
if isinstance(selected_item.data(0, QtCore.Qt.UserRole), ImageFilenames):
selected_item = selected_item.parent()
if isinstance(selected_item, QtWidgets.QTreeWidgetItem):
if isinstance(selected_item.data(0, QtCore.Qt.UserRole), ImageGroups):
preselect_group = selected_item.data(0, QtCore.Qt.UserRole).id
# Enable and disable parts of the 'choose group' form
if self.manager.get_object_count(ImageGroups) == 0:
self.choose_group_form.existing_radio_button.setDisabled(True)
self.choose_group_form.group_combobox.setDisabled(True)
else:
self.choose_group_form.existing_radio_button.setDisabled(False)
self.choose_group_form.group_combobox.setDisabled(False)
# Ask which group the image_paths should be saved in
if self.choose_group_form.exec(selected_group=preselect_group):
if self.choose_group_form.nogroup_radio_button.isChecked():
# User chose 'No group'
parent_group = ImageGroups()
parent_group.id = 0
elif self.choose_group_form.existing_radio_button.isChecked():
# User chose 'Existing group'
group_id = self.choose_group_form.group_combobox.itemData(
self.choose_group_form.group_combobox.currentIndex(), QtCore.Qt.UserRole)
parent_group = self.manager.get_object_filtered(ImageGroups, ImageGroups.id == group_id)
elif self.choose_group_form.new_radio_button.isChecked():
# User chose 'New group'
parent_group = ImageGroups()
parent_group.parent_id = 0
parent_group.group_name = self.choose_group_form.new_group_edit.text()
self.manager.save_object(parent_group)
self.fill_groups_combobox(self.choose_group_form.group_combobox)
self.fill_groups_combobox(self.add_group_form.parent_group_combobox)
def load_item(self, item: Item, is_initial_load: bool = False) -> QtWidgets.QTreeWidgetItem:
"""Given an item object, return a QTreeWidgetItem"""
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:
parent_group = target_group.data(0, QtCore.Qt.UserRole)
if isinstance(parent_group, ImageFilenames):
if parent_group.group_id == 0:
parent_group = ImageGroups()
parent_group.id = 0
else:
parent_group = target_group.parent().data(0, QtCore.Qt.UserRole)
# If no valid parent group is found, do nothing
if not isinstance(parent_group, ImageGroups):
return
# Initialize busy cursor and progress bar
self.application.set_busy_cursor()
self.main_window.display_progress_bar(len(image_paths))
# Save the new image_paths in the database
self.save_new_images_list(image_paths, group_id=parent_group.id, reload_list=False)
self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.file_path),
initial_load=initial_load, open_group=parent_group)
self.application.set_normal_cursor()
log.debug('Loading image: {name}'.format(name=item.file_path))
thumbnail_path = self.generate_thumbnail_path(item)
if validate_thumb(file_path, thumbnail_path):
icon = build_icon(thumbnail_path)
else:
icon = create_thumb(file_path, thumbnail_path)
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 save_new_images_list(self, image_paths, group_id=0, reload_list=True):
def delete_item(self, item: Item):
"""
Convert a list of image filenames to ImageFilenames objects and save them in the database.
:param list[Path] image_paths: A List of file paths to image
:param group_id: The ID of the group to save the images in
:param reload_list: This boolean is set to True when the list in the interface should be reloaded after saving
the new images
Remove an image item from the list.
"""
for image_path in image_paths:
if not isinstance(image_path, Path):
continue
log.debug('Adding new image: {name}'.format(name=image_path))
image_file = ImageFilenames()
image_file.group_id = group_id
image_file.file_path = image_path
image_file.file_hash = sha256_file_hash(image_path)
self.manager.save_object(image_file)
self.main_window.increment_progress_bar()
if reload_list and image_paths:
self.load_full_list(self.manager.get_all_objects(ImageFilenames, order_by_ref=ImageFilenames.file_path))
def dnd_move_internal(self, target):
"""
Handle drag-and-drop moving of images 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_group = target
if target_group is not None and isinstance(target_group.data(0, QtCore.Qt.UserRole), ImageFilenames):
target_group = target.parent()
# Move to toplevel
if target_group is None:
target_group = self.list_view.invisibleRootItem()
target_group.setData(0, QtCore.Qt.UserRole, ImageGroups())
target_group.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), ImageFilenames):
if isinstance(item.parent(), QtWidgets.QTreeWidgetItem):
item.parent().removeChild(item)
else:
self.list_view.invisibleRootItem().removeChild(item)
target_group.addChild(item)
item.setSelected(True)
item_data = item.data(0, QtCore.Qt.UserRole)
item_data.group_id = target_group.data(0, QtCore.Qt.UserRole).id
items_to_save.append(item_data)
target_group.setExpanded(True)
# Update the group ID's of the images in the database
self.manager.save_objects(items_to_save)
# Sort the target group
group_items = []
image_items = []
for item in target_group.takeChildren():
if isinstance(item.data(0, QtCore.Qt.UserRole), ImageGroups):
group_items.append(item)
if isinstance(item.data(0, QtCore.Qt.UserRole), ImageFilenames):
image_items.append(item)
group_items.sort(key=lambda item: get_natural_key(item.text(0)))
target_group.addChildren(group_items)
image_items.sort(key=lambda item: get_natural_key(item.text(0)))
target_group.addChildren(image_items)
file_path = Path(item.file_path)
if file_path.exists():
delete_file(self.service_path / file_path.name)
delete_file(self.generate_thumbnail_path(item))
def generate_slide_data(self, service_item, *, item=None, remote=False, context=ServiceItemContext.Service,
**kwargs):
@ -561,7 +193,7 @@ class ImageMediaItem(MediaManagerItem):
if not items:
return False
# Determine service item title
if isinstance(items[0].data(0, QtCore.Qt.UserRole), ImageGroups) or len(items) == 1:
if isinstance(items[0].data(0, QtCore.Qt.UserRole), Folder) or len(items) == 1:
service_item.title = items[0].text(0)
else:
service_item.title = str(self.plugin.name_strings['plural'])
@ -583,19 +215,19 @@ class ImageMediaItem(MediaManagerItem):
existing_images = []
# Expand groups to images
for bitem in items:
if isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageGroups) or bitem.data(0, QtCore.Qt.UserRole) is None:
if isinstance(bitem.data(0, QtCore.Qt.UserRole), Folder) or bitem.data(0, QtCore.Qt.UserRole) is None:
for index in range(0, bitem.childCount()):
if isinstance(bitem.child(index).data(0, QtCore.Qt.UserRole), ImageFilenames):
if isinstance(bitem.child(index).data(0, QtCore.Qt.UserRole), Item):
images.append(bitem.child(index).data(0, QtCore.Qt.UserRole))
elif isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageFilenames):
elif isinstance(bitem.data(0, QtCore.Qt.UserRole), Item):
images.append(bitem.data(0, QtCore.Qt.UserRole))
# Don't try to display empty groups
if not images:
return False
# Find missing files
for image in images:
if not image.file_path.exists():
missing_items_file_names.append(str(image.file_path))
if not Path(image.file_path).exists():
missing_items_file_names.append(image.file_path)
else:
existing_images.append(image)
# We cannot continue, as all images do not exist.
@ -616,55 +248,11 @@ class ImageMediaItem(MediaManagerItem):
return False
# Continue with the existing images.
for image in existing_images:
name = image.file_path.name
name = Path(image.file_path).name
thumbnail_path = self.generate_thumbnail_path(image)
service_item.add_from_image(image.file_path, name, thumbnail_path)
service_item.add_from_image(Path(image.file_path), name, thumbnail_path)
return True
def check_group_exists(self, new_group):
"""
Returns *True* if the given Group already exists in the database, otherwise *False*.
:param new_group: The ImageGroups object that contains the name of the group that will be checked
"""
groups = self.manager.get_all_objects(ImageGroups, ImageGroups.group_name == new_group.group_name)
if groups:
return True
else:
return False
def on_add_group_click(self):
"""
Called to add a new group
"""
# Find out if a group must be pre-selected
preselect_group = 0
selected_items = self.list_view.selectedItems()
if selected_items:
selected_item = selected_items[0]
if isinstance(selected_item.data(0, QtCore.Qt.UserRole), ImageFilenames):
selected_item = selected_item.parent()
if isinstance(selected_item, QtWidgets.QTreeWidgetItem):
if isinstance(selected_item.data(0, QtCore.Qt.UserRole), ImageGroups):
preselect_group = selected_item.data(0, QtCore.Qt.UserRole).id
# Show 'add group' dialog
if self.add_group_form.exec(show_top_level_group=True, selected_group=preselect_group):
new_group = ImageGroups(parent_id=self.add_group_form.parent_group_combobox.itemData(
self.add_group_form.parent_group_combobox.currentIndex(), QtCore.Qt.UserRole),
group_name=self.add_group_form.name_edit.text())
if not self.check_group_exists(new_group):
if self.manager.save_object(new_group):
self.load_full_list(self.manager.get_all_objects(
ImageFilenames, order_by_ref=ImageFilenames.file_path))
self.expand_group(new_group.id)
self.fill_groups_combobox(self.choose_group_form.group_combobox)
self.fill_groups_combobox(self.add_group_form.parent_group_combobox)
else:
critical_error_message_box(
message=translate('ImagePlugin.AddGroupForm', 'Could not add the new group.'))
else:
critical_error_message_box(message=translate('ImagePlugin.AddGroupForm', 'This group already exists.'))
def on_reset_click(self):
"""
Called to reset the Live background with the image selected.
@ -688,10 +276,10 @@ class ImageMediaItem(MediaManagerItem):
self.list_view,
translate('ImagePlugin.MediaItem', 'You must select an image to replace the background with.')):
bitem = self.list_view.selectedItems()[0]
if not isinstance(bitem.data(0, QtCore.Qt.UserRole), ImageFilenames):
if not isinstance(bitem.data(0, QtCore.Qt.UserRole), Item):
# Only continue when an image is selected.
return
file_path = bitem.data(0, QtCore.Qt.UserRole).file_path
file_path = Path(bitem.data(0, QtCore.Qt.UserRole).file_path)
if file_path.exists():
self.live_controller.set_background_image(file_path)
self.reset_action.setVisible(True)
@ -701,32 +289,3 @@ class ImageMediaItem(MediaManagerItem):
UiStrings().LiveBGError,
translate('ImagePlugin.MediaItem', 'There was a problem replacing your background, '
'the image file "{name}" no longer exists.').format(name=file_path))
def search(self, string, show_error=True):
"""
Perform a search on the image file names.
:param str string: The glob to search for
:param bool show_error: Unused.
"""
files = self.manager.get_all_objects(
ImageFilenames, filter_clause=ImageFilenames.file_path.contains(string),
order_by_ref=ImageFilenames.file_path)
results = []
for file_object in files:
file_name = file_object.file_path.name
results.append([str(file_object.file_path), file_name])
return results
def create_item_from_id(self, item_id):
"""
Create a media item from an item id. Overridden from the parent method to change the item type.
:param item_id: Id to make live
"""
item_id = Path(item_id)
item = QtWidgets.QTreeWidgetItem()
item_data = self.manager.get_object_filtered(ImageFilenames, ImageFilenames.file_path == item_id)
item.setText(0, item_data.file_path.name)
item.setData(0, QtCore.Qt.UserRole, item_data)
return item

View File

@ -26,7 +26,9 @@ import logging
import shutil
from pathlib import Path
from sqlalchemy import Column, Table, types
from sqlalchemy import Column, ForeignKey, MetaData, Table, inspect
from sqlalchemy.orm import Session
from sqlalchemy.types import Integer, Unicode
from openlp.core.common import sha256_file_hash
from openlp.core.common.applocation import AppLocation
@ -36,17 +38,17 @@ from openlp.core.lib.db import PathType, get_upgrade_op
log = logging.getLogger(__name__)
__version__ = 3
__version__ = 4
def upgrade_1(session, metadata):
def upgrade_1(session: Session, metadata: MetaData):
"""
Version 1 upgrade - old db might/might not be versioned.
"""
log.debug('Skipping upgrade_1 of files DB - not used')
def upgrade_2(session, metadata):
def upgrade_2(session: Session, metadata: MetaData):
"""
Version 2 upgrade - Move file path from old db to JSON encoded path to new db. Added during 2.5 dev
"""
@ -70,7 +72,7 @@ def upgrade_2(session, metadata):
op.drop_column('image_filenames', 'filenames')
def upgrade_3(session, metadata):
def upgrade_3(session: Session, metadata: MetaData):
"""
Version 3 upgrade - add sha256 hash
"""
@ -78,7 +80,7 @@ def upgrade_3(session, metadata):
old_table = Table('image_filenames', metadata, autoload=True)
if 'file_hash' not in [col.name for col in old_table.c.values()]:
op = get_upgrade_op(session)
op.add_column('image_filenames', Column('file_hash', types.Unicode(128)))
op.add_column('image_filenames', Column('file_hash', Unicode(128)))
conn = op.get_bind()
results = conn.execute('SELECT * FROM image_filenames')
thumb_path = AppLocation.get_data_path() / 'images' / 'thumbnails'
@ -101,3 +103,58 @@ def upgrade_3(session, metadata):
except OSError:
log.exception('Failed in renaming image thumb from {oldt} to {newt}'.format(oldt=old_thumb,
newt=new_thumb))
def upgrade_4(session: Session, metadata: MetaData):
"""
Version 4 upgrade - convert to the common folders/items model
"""
log.debug('Starting upgrade_4 for converting to common folders/items model')
op = get_upgrade_op(session)
conn = op.get_bind()
# Check if the folder table exists
table_names = inspect(conn).get_table_names()
if 'image_groups' not in table_names:
# Bypass this upgrade, it has already been performed
return
# Get references to the old tables
old_folder_table = Table('image_groups', metadata, autoload=True)
old_item_table = Table('image_filenames', metadata, autoload=True)
# Create the new tables
if 'folder' not in table_names:
new_folder_table = op.create_table(
'folder',
Column('id', Integer, primary_key=True),
Column('name', Unicode(255), nullable=False, index=True),
Column('parent_id', Integer, ForeignKey('folder.id'))
)
else:
new_folder_table = Table('folder', metadata, autoload=True)
if 'item' not in table_names:
new_item_table = op.create_table(
'item',
Column('id', Integer, primary_key=True),
Column('name', Unicode(255), nullable=False, index=True),
Column('file_path', Unicode(255)),
Column('file_hash', Unicode(255)),
Column('folder_id', Integer)
)
else:
new_item_table = Table('item', metadata, autoload=True)
# Bulk insert all the data from the old tables to the new tables
folders = []
for old_folder in conn.execute(old_folder_table.select()).fetchall():
folders.append({'id': old_folder.id, 'name': old_folder.group_name,
'parent_id': old_folder.parent_id if old_folder.parent_id != 0 else None})
op.bulk_insert(new_folder_table, folders)
items = []
for old_item in conn.execute(old_item_table.select()).fetchall():
file_path = json.loads(old_item.file_path, cls=OpenLPJSONDecoder)
items.append({'id': old_item.id, 'name': file_path.name, 'file_path': str(file_path),
'file_hash': old_item.file_hash, 'folder_id': old_item.group_id})
op.bulk_insert(new_item_table, items)
# Remove the old tables
del old_item_table
del old_folder_table
op.drop_table('image_filenames')
op.drop_table('image_groups')

View File

@ -18,7 +18,8 @@
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
from unittest.mock import MagicMock
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from PyQt5 import QtCore, QtWidgets
@ -28,18 +29,20 @@ from openlp.core.ui.library import FolderLibraryItem
class MockItem(MagicMock):
file_path = 'path/to/video.mp4'
folder_id = None
class MockFolder(MagicMock):
pass
parent_id = None
@pytest.fixture
def folder_library_item(registry, settings):
mocked_item = MagicMock()
registry.register('main_window', MagicMock())
mocked_manager = MagicMock()
mocked_manager.get_object_filtered.return_value = mocked_item
library_item = FolderLibraryItem(None, MagicMock(manager=mocked_manager), MockFolder, MockItem)
with patch('openlp.core.lib.mediamanageritem.MediaManagerItem._setup'), \
patch('openlp.core.lib.mediamanageritem.MediaManagerItem.setup_item'):
library_item = FolderLibraryItem(None, MagicMock(manager=mocked_manager), MockFolder, MockItem)
return library_item
@ -56,32 +59,28 @@ def test_folderlibrary_retranslate_ui(folder_library_item):
folder_library_item.add_folder_action.setToolTip.assert_called_once_with('Add folder.')
def test_folderlibrary_create_item_from_id_path(registry, settings):
def test_folderlibrary_create_item_from_id_path(folder_library_item):
"""Test the create_item_from_id method"""
# GIVEN: An instance of the FolderLibraryItem
mocked_item = MagicMock()
mocked_manager = MagicMock()
mocked_manager.get_object_filtered.return_value = mocked_item
library_item = FolderLibraryItem(None, MagicMock(manager=mocked_manager), MockFolder, MockItem)
mocked_item = MockItem()
folder_library_item.manager.get_object_filtered.return_value = mocked_item
# WHEN: create_item_from_id is called
result = library_item.create_item_from_id('path/to/video.mp4')
result = folder_library_item.create_item_from_id('path/to/video.mp4')
# THEN: The result should be a QTreeWidgetItem with a mocked object as data
assert isinstance(result, QtWidgets.QTreeWidgetItem)
assert result.data(0, QtCore.Qt.UserRole) is mocked_item
def test_folderlibrary_create_item_from_id_object(registry, settings):
def test_folderlibrary_create_item_from_id_object(folder_library_item):
"""Test the create_item_from_id method"""
# GIVEN: An instance of the FolderLibraryItem
mocked_item = MagicMock()
mocked_manager = MagicMock()
mocked_manager.get_object_filtered.return_value = mocked_item
library_item = FolderLibraryItem(None, MagicMock(manager=mocked_manager), MockFolder, MockItem)
mocked_item = MockItem()
folder_library_item.manager.get_object_filtered.return_value = mocked_item
# WHEN: create_item_from_id is called
result = library_item.create_item_from_id(mocked_item)
result = folder_library_item.create_item_from_id(mocked_item)
# THEN: The result should be a QTreeWidgetItem with a mocked object as data
assert isinstance(result, QtWidgets.QTreeWidgetItem)
@ -103,3 +102,83 @@ def test_folderlibrary_current_folder(folder_library_item):
# THEN: The current folder should be the mocked folder
assert folder is mocked_folder
def test_validate_and_load(folder_library_item):
"""
Test that the validate_and_load() method when called with a folder
"""
# GIVEN: A list of files, and a mocked out load_list
file_list = ['path1/image1.jpg', 'path2/image2.jpg']
expected_list = [MagicMock(file_path=fp) for fp in file_list]
folder_library_item.manager.get_all_objects.return_value = expected_list
folder_library_item.load_list = MagicMock()
folder_library_item.list_view = MagicMock(**{'selectedItems.return_value': None})
folder_library_item.settings_section = 'tests'
folder_library_item.choose_folder_form = MagicMock(**{'exec.return_value': QtWidgets.QDialog.Accepted,
'folder': None})
# WHEN: Calling validate_and_load with the list of files and a group
folder_library_item.validate_and_load(file_list)
# THEN: load_list should have been called with the file list and the group name,
# the directory should have been saved to the settings
folder_library_item.load_list.assert_called_once_with(expected_list, target_folder=None)
@patch('openlp.core.lib.serviceitem.sha256_file_hash')
def test_recursively_delete_folder(mocked_sha256_file_hash, folder_library_item):
"""
Test that recursively_delete_folder() works
"""
# GIVEN: A Folder object and mocked functions
folder_library_item.manager.get_all_objects.side_effect = [
[MockItem(), MockItem(), MockItem()],
[MockFolder()],
[MockItem(), MockItem()],
[]
]
folder_library_item.service_path = Path()
folder_library_item.manager.delete_object = MagicMock()
folder_library_item.delete_item = MagicMock()
mocked_folder = MockFolder()
mocked_folder.id = 1
mocked_folder.parent_id = None
mocked_sha256_file_hash.return_value = 'abcd'
# WHEN: recursively_delete_group() is called
folder_library_item.recursively_delete_folder(mocked_folder)
# THEN: delete_file() should have been called 12 times and manager.delete_object() 7 times.
assert folder_library_item.manager.delete_object.call_count == 6, \
'delete_object() should be called 6 times, called {} times'.format(
folder_library_item.manager.delete_object.call_count)
assert folder_library_item.delete_item.call_count == 5, 'delete_item() should have been called 5 times'
@patch('openlp.core.lib.serviceitem.sha256_file_hash')
def test_on_delete_click(mocked_sha256_file_hash, folder_library_item):
"""
Test that on_delete_click() works
"""
# GIVEN: An ImageGroups object and mocked functions
folder_library_item.check_item_selected = MagicMock(return_value=True)
folder_library_item.delete_item = MagicMock()
mocked_item = MockItem()
mocked_item.id = 1
mocked_item.group_id = 1
mocked_item.file_path = 'imagefile.png'
mocked_item.file_hash = 'abcd'
folder_library_item.service_path = Path()
folder_library_item.list_view = MagicMock()
mocked_row_item = MagicMock()
mocked_row_item.data.return_value = mocked_item
mocked_row_item.text.return_value = ''
folder_library_item.list_view.selectedItems.return_value = [mocked_row_item]
mocked_sha256_file_hash.return_value = 'abcd'
# WHEN: Calling on_delete_click
folder_library_item.on_delete_click()
# THEN: delete_file should have been called twice
assert folder_library_item.delete_item.call_count == 1, 'delete_item() should have been called once'

View File

@ -21,16 +21,14 @@
"""
This module contains tests for the lib submodule of the Images plugin.
"""
import pytest
from pathlib import Path
from unittest.mock import ANY, MagicMock, patch
from PyQt5 import QtCore, QtWidgets
import pytest
from openlp.core.common.registry import Registry
from openlp.core.common.enum import ImageThemeMode
from openlp.core.lib.serviceitem import ItemCapabilities
from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups
from openlp.plugins.images.lib.mediaitem import ImageMediaItem
@ -42,136 +40,14 @@ def media_item(mock_settings):
Registry().register('main_window', mocked_main_window)
Registry().register('live_controller', MagicMock())
mocked_plugin = MagicMock()
with patch('openlp.plugins.images.lib.mediaitem.MediaManagerItem._setup'), \
with patch('openlp.plugins.images.lib.mediaitem.FolderLibraryItem._setup'), \
patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.setup_item'):
m_item = ImageMediaItem(None, mocked_plugin)
m_item.settings_section = 'images'
m_item.list_view = MagicMock()
return m_item
def _recursively_delete_group_side_effect(*args, **kwargs):
"""
Side effect method that creates custom return values for the recursively_delete_group method
"""
if args[0] == ImageFilenames and args[1]:
# Create some fake objects that should be removed
returned_object1 = ImageFilenames()
returned_object1.id = 1
returned_object1.file_path = Path('/', 'tmp', 'test_file_1.jpg')
returned_object1.file_hash = 'abcd1'
returned_object2 = ImageFilenames()
returned_object2.id = 2
returned_object2.file_path = Path('/', 'tmp', 'test_file_2.jpg')
returned_object2.file_hash = 'abcd2'
returned_object3 = ImageFilenames()
returned_object3.id = 3
returned_object3.file_path = Path('/', 'tmp', 'test_file_3.jpg')
returned_object3.file_hash = 'abcd3'
return [returned_object1, returned_object2, returned_object3]
if args[0] == ImageGroups and args[1]:
# Change the parent_id that is matched so we don't get into an endless loop
ImageGroups.parent_id = 0
# Create a fake group that will be used in the next run
returned_object1 = ImageGroups()
returned_object1.id = 1
return [returned_object1]
return []
@patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list')
def test_save_new_images_list_empty_list(mocked_load_full_list, media_item):
"""
Test that the save_new_images_list() method handles empty lists gracefully
"""
# GIVEN: An empty image_list
image_list = []
media_item.manager = MagicMock()
# WHEN: We run save_new_images_list with the empty list
media_item.save_new_images_list(image_list)
# THEN: The save_object() method should not have been called
assert media_item.manager.save_object.call_count == 0, \
'The save_object() method should not have been called'
@patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list')
@patch('openlp.plugins.images.lib.mediaitem.sha256_file_hash')
def test_save_new_images_list_single_image_with_reload(mocked_sha256_file_hash, mocked_load_full_list, media_item):
"""
Test that the save_new_images_list() calls load_full_list() when reload_list is set to True
"""
# GIVEN: A list with 1 image and a mocked out manager
image_list = [Path('test_image.jpg')]
ImageFilenames.file_path = None
media_item.manager = MagicMock()
mocked_sha256_file_hash.return_value = 'abcd'
# WHEN: We run save_new_images_list with reload_list=True
media_item.save_new_images_list(image_list, reload_list=True)
# THEN: load_full_list() should have been called
assert mocked_load_full_list.call_count == 1, 'load_full_list() should have been called'
# CLEANUP: Remove added attribute from ImageFilenames
delattr(ImageFilenames, 'file_path')
@patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list')
@patch('openlp.plugins.images.lib.mediaitem.sha256_file_hash')
def test_save_new_images_list_single_image_without_reload(mocked_sha256_file_hash, mocked_load_full_list, media_item):
"""
Test that the save_new_images_list() doesn't call load_full_list() when reload_list is set to False
"""
# GIVEN: A list with 1 image and a mocked out manager
image_list = [Path('test_image.jpg')]
media_item.manager = MagicMock()
mocked_sha256_file_hash.return_value = 'abcd'
# WHEN: We run save_new_images_list with reload_list=False
media_item.save_new_images_list(image_list, reload_list=False)
# THEN: load_full_list() should not have been called
assert mocked_load_full_list.call_count == 0, 'load_full_list() should not have been called'
@patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list')
@patch('openlp.plugins.images.lib.mediaitem.sha256_file_hash')
def test_save_new_images_list_multiple_images(mocked_sha256_file_hash, mocked_load_full_list, media_item):
"""
Test that the save_new_images_list() saves all images in the list
"""
# GIVEN: A list with 3 images
image_list = [Path('test_image_1.jpg'), Path('test_image_2.jpg'), Path('test_image_3.jpg')]
media_item.manager = MagicMock()
mocked_sha256_file_hash.return_value = 'abcd'
# WHEN: We run save_new_images_list with the list of 3 images
media_item.save_new_images_list(image_list, reload_list=False)
# THEN: load_full_list() should not have been called
assert media_item.manager.save_object.call_count == 3, \
'load_full_list() should have been called three times'
@patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_full_list')
@patch('openlp.plugins.images.lib.mediaitem.sha256_file_hash')
def test_save_new_images_list_other_objects_in_list(mocked_sha256_file_hash, mocked_load_full_list, media_item):
"""
Test that the save_new_images_list() ignores everything in the provided list except strings
"""
# GIVEN: A list with images and objects
image_list = [Path('test_image_1.jpg'), None, True, ImageFilenames(), Path('test_image_2.jpg')]
media_item.manager = MagicMock()
mocked_sha256_file_hash.return_value = 'abcd'
# WHEN: We run save_new_images_list with the list of images and objects
media_item.save_new_images_list(image_list, reload_list=False)
# THEN: load_full_list() should not have been called
assert media_item.manager.save_object.call_count == 2, 'load_full_list() should have been called only once'
def test_on_reset_click(media_item):
"""
Test that on_reset_click() actually resets the background
@ -263,123 +139,6 @@ def test_on_replace_click(mocked_exists, mocked_isinstance, mocked_check_item_se
media_item.live_controller.set_background_image.assert_called_with(ANY)
@patch('openlp.plugins.images.lib.mediaitem.delete_file')
@patch('openlp.core.lib.serviceitem.sha256_file_hash')
def test_recursively_delete_group(mocked_sha256_file_hash, mocked_delete_file, media_item):
"""
Test that recursively_delete_group() works
"""
# GIVEN: An ImageGroups object and mocked functions
ImageFilenames.group_id = 1
ImageGroups.parent_id = 1
media_item.manager = MagicMock()
media_item.manager.get_all_objects.side_effect = _recursively_delete_group_side_effect
media_item.service_path = Path()
test_group = ImageGroups()
test_group.id = 1
mocked_sha256_file_hash.return_value = 'abcd'
# WHEN: recursively_delete_group() is called
media_item.recursively_delete_group(test_group)
# THEN: delete_file() should have been called 12 times and manager.delete_object() 7 times.
assert mocked_delete_file.call_count == 12, 'delete_file() should have been called 12 times'
assert media_item.manager.delete_object.call_count == 7, \
'manager.delete_object() should be called exactly 7 times'
# CLEANUP: Remove added attribute from Image Filenames and ImageGroups
delattr(ImageFilenames, 'group_id')
delattr(ImageGroups, 'parent_id')
@patch('openlp.plugins.images.lib.mediaitem.delete_file')
@patch('openlp.plugins.images.lib.mediaitem.check_item_selected')
@patch('openlp.core.lib.serviceitem.sha256_file_hash')
def test_on_delete_click(mocked_sha256_file_hash, mocked_check_item_selected, mocked_delete_file, media_item):
"""
Test that on_delete_click() works
"""
# GIVEN: An ImageGroups object and mocked functions
mocked_check_item_selected.return_value = True
test_image = ImageFilenames()
test_image.id = 1
test_image.group_id = 1
test_image.file_path = Path('imagefile.png')
test_image.file_hash = 'abcd'
media_item.manager = MagicMock()
media_item.service_path = Path()
media_item.list_view = MagicMock()
mocked_row_item = MagicMock()
mocked_row_item.data.return_value = test_image
mocked_row_item.text.return_value = ''
media_item.list_view.selectedItems.return_value = [mocked_row_item]
mocked_sha256_file_hash.return_value = 'abcd'
# WHEN: Calling on_delete_click
media_item.on_delete_click()
# THEN: delete_file should have been called twice
assert mocked_delete_file.call_count == 2, 'delete_file() should have been called twice'
def test_create_item_from_id(media_item):
"""
Test that the create_item_from_id() method returns a valid QTreeWidgetItem with a pre-created ImageFilenames
"""
# GIVEN: An ImageFilenames that already exists in the database
image_file = ImageFilenames()
image_file.id = 1
image_file.file_path = Path('/', 'tmp', 'test_file_1.jpg')
media_item.manager = MagicMock()
media_item.manager.get_object_filtered.return_value = image_file
ImageFilenames.file_path = None
# WHEN: create_item_from_id() is called
item = media_item.create_item_from_id('1')
# THEN: A QTreeWidgetItem should be created with the above model object as it's data
assert isinstance(item, QtWidgets.QTreeWidgetItem)
assert 'test_file_1.jpg' == item.text(0)
item_data = item.data(0, QtCore.Qt.UserRole)
assert isinstance(item_data, ImageFilenames)
assert 1 == item_data.id
assert Path('/', 'tmp', 'test_file_1.jpg') == item_data.file_path
@patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_list')
def test_validate_and_load(mocked_load_list, media_item):
"""
Test that the validate_and_load_test() method when called without a group
"""
# GIVEN: A list of files
file_list = [Path('path1', 'image1.jpg'), Path('path2', 'image2.jpg')]
# WHEN: Calling validate_and_load with the list of files
media_item.validate_and_load(file_list)
# THEN: load_list should have been called with the file list and None,
# the directory should have been saved to the settings
mocked_load_list.assert_called_once_with(file_list, None)
Registry().get('settings').setValue.assert_called_once_with(ANY, Path('path1'))
@patch('openlp.plugins.images.lib.mediaitem.ImageMediaItem.load_list')
def test_validate_and_load_group(mocked_load_list, media_item):
"""
Test that the validate_and_load_test() method when called with a group
"""
# GIVEN: A list of files
file_list = [Path('path1', 'image1.jpg'), Path('path2', 'image2.jpg')]
# WHEN: Calling validate_and_load with the list of files and a group
media_item.validate_and_load(file_list, 'group')
# THEN: load_list should have been called with the file list and the group name,
# the directory should have been saved to the settings
mocked_load_list.assert_called_once_with(file_list, 'group')
Registry().get('settings').setValue.assert_called_once_with(ANY, Path('path1'))
def test_generate_thumbnail_path_hash(media_item):
"""
Test that the thumbnail path is correctly generated with the file hash