Planningcenter plugin

This commit is contained in:
John Kirkland 2019-11-09 16:17:41 +00:00 committed by Raoul Snyman
parent b9e26a1cfb
commit dbcdf11c0a
28 changed files with 2224 additions and 7 deletions

View File

@ -100,6 +100,7 @@ class UiIcons(metaclass=Singleton):
'open': {'icon': 'fa.folder-open'},
'optical': {'icon': 'fa.file-video-o'},
'pause': {'icon': 'fa.pause'},
'planning_center': {'icon': 'fa.cloud-download'},
'play': {'icon': 'fa.play'},
'player': {'icon': 'fa.tablet'},
'plugin_list': {'icon': 'fa.puzzle-piece'},

View File

@ -64,7 +64,7 @@ class SettingsForm(QtWidgets.QDialog, Ui_SettingsDialog, RegistryProperties):
self.advanced_tab = None
self.api_tab = None
def exec(self):
def exec(self, starting_tab_name=None):
"""
Execute the form
"""
@ -84,8 +84,14 @@ class SettingsForm(QtWidgets.QDialog, Ui_SettingsDialog, RegistryProperties):
for plugin in State().list_plugins():
if plugin.settings_tab:
self.insert_tab(plugin.settings_tab, plugin.is_active())
self.setting_list_widget.setCurrentRow(0)
self.setting_list_widget.blockSignals(False)
starting_tab_row = 0
for index in range(self.setting_list_widget.count()):
item = self.setting_list_widget.item(index)
if item.text() == starting_tab_name:
starting_tab_row = index
break
self.setting_list_widget.setCurrentRow(starting_tab_row)
return QtWidgets.QDialog.exec(self)
def insert_tab(self, tab_widget, is_visible=True):

View File

@ -231,6 +231,7 @@ class CustomMediaItem(MediaManagerItem):
service_item.add_capability(ItemCapabilities.CanLoop)
service_item.add_capability(ItemCapabilities.CanSoftBreak)
service_item.add_capability(ItemCapabilities.OnLoadUpdate)
service_item.add_capability(ItemCapabilities.CanWordSplit)
custom_slide = self.plugin.db_manager.get_object(CustomSlide, item_id)
title = custom_slide.title
credit = custom_slide.credits

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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; version 2 of the License. #
# #
# 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, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`planningcenter` module provides the PlanningCenter plugin.
The PlanningCenter plugin interfaces with the PlanningCenter v2 API to
download services into OpenLP. """

View File

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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; version 2 of the License. #
# #
# 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, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`~openlp.plugins.planningcenter.forms.selectplandialog` module contains the user interface code for the dialog
"""
from PyQt5 import QtWidgets
from openlp.core.common.i18n import translate
class Ui_SelectPlanDialog(object):
"""
The actual Qt components that make up the dialog.
"""
def setup_ui(self, planningcenter_dialog):
planningcenter_dialog.setObjectName('planningcenter_dialog')
planningcenter_dialog.resize(400, 280)
self.planningcenter_layout = QtWidgets.QFormLayout(planningcenter_dialog)
self.planningcenter_layout.setContentsMargins(50, 50, 50, 50)
self.planningcenter_layout.setSpacing(8)
self.planningcenter_layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow)
# Service Type GUI Elements -- service_type combo_box
self.service_type_label = QtWidgets.QLabel(planningcenter_dialog)
self.service_type_combo_box = QtWidgets.QComboBox(planningcenter_dialog)
self.planningcenter_layout.addRow(self.service_type_label, self.service_type_combo_box)
# Plan Selection GUI Elements
self.plan_selection_label = QtWidgets.QLabel(planningcenter_dialog)
self.plan_selection_combo_box = QtWidgets.QComboBox(planningcenter_dialog)
self.planningcenter_layout.addRow(self.plan_selection_label, self.plan_selection_combo_box)
# Theme List for Songs and Custom Slides
self.song_theme_selection_label = QtWidgets.QLabel(planningcenter_dialog)
self.song_theme_selection_combo_box = QtWidgets.QComboBox(planningcenter_dialog)
self.planningcenter_layout.addRow(self.song_theme_selection_label, self.song_theme_selection_combo_box)
self.slide_theme_selection_label = QtWidgets.QLabel(planningcenter_dialog)
self.slide_theme_selection_combo_box = QtWidgets.QComboBox(planningcenter_dialog)
self.planningcenter_layout.addRow(self.slide_theme_selection_label, self.slide_theme_selection_combo_box)
# Import Button
self.button_layout = QtWidgets.QDialogButtonBox(planningcenter_dialog)
self.import_as_new_button = QtWidgets.QPushButton(planningcenter_dialog)
self.button_layout.addButton(self.import_as_new_button, QtWidgets.QDialogButtonBox.AcceptRole)
self.update_existing_button = QtWidgets.QPushButton(planningcenter_dialog)
self.button_layout.addButton(self.update_existing_button, QtWidgets.QDialogButtonBox.AcceptRole)
self.edit_auth_button = QtWidgets.QPushButton(planningcenter_dialog)
self.button_layout.addButton(self.edit_auth_button, QtWidgets.QDialogButtonBox.ActionRole)
self.planningcenter_layout.addRow(self.button_layout)
self.retranslate_ui(planningcenter_dialog)
def retranslate_ui(self, planningcenter_dialog):
"""
Translate the GUI.
"""
planningcenter_dialog.setWindowTitle(translate('PlanningCenterPlugin.PlanningCenterForm',
'Planning Center Online Service Importer'))
self.service_type_label.setText(translate('PlanningCenterPlugin.PlanningCenterForm', 'Service Type'))
self.plan_selection_label.setText(translate('PlanningCenterPlugin.PlanningCenterForm', 'Select Plan'))
self.import_as_new_button.setText(translate('PlanningCenterPlugin.PlanningCenterForm', 'Import New'))
self.import_as_new_button.setToolTip(translate('PlanningCenterPlugin.PlanningCenterForm',
'Import As New Service'))
self.update_existing_button.setText(translate('PlanningCenterPlugin.PlanningCenterForm', 'Refresh Service'))
self.update_existing_button.setToolTip(translate('PlanningCenterPlugin.PlanningCenterForm',
'Refresh Existing Service from Planning Center. \
This will update song lyrics or item orders that \
have changed'))
self.edit_auth_button.setText(translate('PlanningCenterPlugin.PlanningCenterForm', 'Edit Authentication'))
self.edit_auth_button.setToolTip(translate('PlanningCenterPlugin.PlanningCenterForm', 'Edit the Application \
ID and Secret Code to login to Planning Center Online'))
self.song_theme_selection_label.setText(translate('PlanningCenterPlugin.PlanningCenterForm', 'Song Theme'))
self.slide_theme_selection_label.setText(translate('PlanningCenterPlugin.PlanningCenterForm', 'Slide Theme'))

View File

@ -0,0 +1,289 @@
# -*- coding: utf-8 -*-
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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; version 2 of the License. #
# #
# 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, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`~openlp.plugins.planningcenter.forms.selectplanform` module contains
the GUI for the PlanningCenter Service importer
"""
import logging
import re
from datetime import date, datetime
from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import translate
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.plugins.bibles.lib import parse_reference
from openlp.plugins.planningcenter.forms.selectplandialog import Ui_SelectPlanDialog
from openlp.plugins.planningcenter.lib.customimport import PlanningCenterCustomImport
from openlp.plugins.planningcenter.lib.planningcenter_api import PlanningCenterAPI
from openlp.plugins.planningcenter.lib.songimport import PlanningCenterSongImport
log = logging.getLogger(__name__)
class SelectPlanForm(QtWidgets.QDialog, Ui_SelectPlanDialog):
"""
The :class:`PlanningCenterForm` class is the PlanningCenter dialog.
"""
def __init__(self, parent=None, plugin=None):
QtWidgets.QDialog.__init__(self, parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint)
self.plugin = plugin
# create an Planning Center API Object
application_id = Settings().value("planningcenter/application_id")
secret = Settings().value("planningcenter/secret")
self.planning_center_api = PlanningCenterAPI(application_id, secret)
self.setup_ui(self)
self.service_type_combo_box.currentIndexChanged.connect(self.on_service_type_combobox_changed)
self.plan_selection_combo_box.currentIndexChanged.connect(self.on_plan_selection_combobox_changed)
self.import_as_new_button.clicked.connect(self.on_import_as_new_button_clicked)
self.update_existing_button.clicked.connect(self.on_update_existing_button_clicked)
self.edit_auth_button.clicked.connect(self.on_edit_auth_button_clicked)
def exec(self):
"""
Execute the dialog. This method sets everything back to its initial
values.
"""
self.import_as_new_button.setEnabled(False)
self.update_existing_button.setEnabled(False)
# check our credentials and connection to the PlanningCenter server
organization = self.planning_center_api.check_credentials()
if len(organization) == 0:
QtWidgets.QMessageBox.warning(self.parent(), "Authentication Failed", "Authentiation Failed. Check your \
credentials in OpenLP Settings", QtWidgets.QMessageBox.Ok)
return
# set the Service Type Dropdown Box from PCO
service_types_list = self.planning_center_api.get_service_type_list()
self.service_type_combo_box.clear()
for service_type in service_types_list:
self.service_type_combo_box.addItem(service_type['attributes']['name'], service_type['id'])
self.service_type_combo_box.setCurrentIndex(0)
self.on_plan_selection_combobox_changed()
# Set the 2 lists of themes
theme_manager = Registry().get('theme_manager')
for theme in theme_manager.get_themes():
self.song_theme_selection_combo_box.addItem(theme)
self.slide_theme_selection_combo_box.addItem(theme)
return QtWidgets.QDialog.exec(self)
def done(self, result_code):
"""
Close dialog.
:param r: The result of the dialog.
"""
log.debug('Closing PlanningCenterForm')
return QtWidgets.QDialog.done(self, result_code)
def on_edit_auth_button_clicked(self):
"""
Open the edit auth screen
"""
self.done(QtWidgets.QDialog.Accepted)
settings_form = Registry().get('settings_form')
settings_form.exec(translate('PlanningCenterPlugin', 'PlanningCenter'))
def on_service_type_combobox_changed(self):
"""
Set the plan_selection_combo_box content based upon the current service_type_combo_box setting.
"""
# set the Plan Dropdown Box from PCO
service_type_id = self.service_type_combo_box.itemData(self.service_type_combo_box.currentIndex())
if service_type_id:
plan_list = self.planning_center_api.get_plan_list(service_type_id)
self.plan_selection_combo_box.clear()
self.plan_selection_combo_box.addItem('Select Plan Date')
self.plan_selection_combo_box.setCurrentIndex(0)
# Get Today's date and see if it is listed... if it is, then select it in the combobox
today = date.today()
for plan in plan_list:
self.plan_selection_combo_box.addItem(plan['attributes']['dates'], plan['id'])
# sort_date=str: 2018-12-21T19:00:00Z
plan_datetime = datetime.strptime(plan['attributes']['sort_date'].rstrip("Z"), '%Y-%m-%dT%H:%M:%S')
plan_date = date(plan_datetime.year, plan_datetime.month, plan_datetime.day)
# if we have any date that matches today or in the future, select it
if plan_date >= today:
self.plan_selection_combo_box.setCurrentIndex(self.plan_selection_combo_box.count() - 1)
self.import_as_new_button.setEnabled(True)
self.update_existing_button.setEnabled(True)
def on_plan_selection_combobox_changed(self):
"""
Set the Import button enable/disable based upon the current plan_selection_combo_box setting.
"""
current_index = self.plan_selection_combo_box.currentIndex()
if current_index == 0 or current_index == -1:
self.import_as_new_button.setEnabled(False)
self.update_existing_button.setEnabled(False)
else:
self.import_as_new_button.setEnabled(True)
self.update_existing_button.setEnabled(True)
def on_update_existing_button_clicked(self):
"""
Call the import function but tell it to also do an update so that it can
keep changed items
"""
self._do_import(update=True)
self.done(QtWidgets.QDialog.Accepted)
def on_import_as_new_button_clicked(self):
"""
Create a new service and import all of the PCO items into it
"""
self._do_import(update=False)
self.done(QtWidgets.QDialog.Accepted)
def _do_import(self, update=False):
"""
Utility function to perform the import or update as requested
"""
service_manager = Registry().get('service_manager')
old_service_items = []
if update:
old_service_items = service_manager.service_items.copy()
service_manager.new_file()
else:
service_manager.on_new_service_clicked()
# we only continue here if the service_manager is now empty
if len(service_manager.service_items) == 0:
service_manager.application.set_busy_cursor()
# get the plan ID for the current plan selection
plan_id = self.plan_selection_combo_box.itemData(self.plan_selection_combo_box.currentIndex())
# get the items array from Planning Center
planning_center_items_dict = self.planning_center_api.get_items_dict(plan_id)
service_manager.main_window.display_progress_bar(len(planning_center_items_dict['data']))
# convert the planning center dict to Songs and Add them to the ServiceManager
planning_center_id_to_openlp_id = {}
for item in planning_center_items_dict['data']:
item_title = item['attributes']['title']
media_type = ''
openlp_id = -1
if item['attributes']['item_type'] == 'song':
arrangement_id = item['relationships']['arrangement']['data']['id']
song_id = item['relationships']['song']['data']['id']
if song_id not in planning_center_id_to_openlp_id:
# get arrangement from "included" resources
arrangement_data = {}
song_data = {}
for included_item in planning_center_items_dict['included']:
if included_item['type'] == 'Song' and included_item['id'] == song_id:
song_data = included_item
elif included_item['type'] == 'Arrangement' and included_item['id'] == arrangement_id:
arrangement_data = included_item
# if we have both song and arrangement set, stop iterating
if len(song_data) and len(arrangement_data):
break
author = song_data['attributes']['author']
lyrics = arrangement_data['attributes']['lyrics']
arrangement_updated_at = datetime.strptime(arrangement_data['attributes']['updated_at'].
rstrip("Z"), '%Y-%m-%dT%H:%M:%S')
# start importing the song
planning_center_import = PlanningCenterSongImport()
theme_name = self.song_theme_selection_combo_box.currentText()
openlp_id = planning_center_import.add_song(item_title, author, lyrics,
theme_name, arrangement_updated_at)
planning_center_id_to_openlp_id[song_id] = openlp_id
openlp_id = planning_center_id_to_openlp_id[song_id]
media_type = 'songs'
else:
# if we have "details" for the item, create slides from those
html_details = item['attributes']['html_details']
theme_name = self.slide_theme_selection_combo_box.currentText()
custom_import = PlanningCenterCustomImport()
openlp_id = custom_import.add_slide(item_title, html_details, theme_name)
media_type = 'custom'
# add the media to the service
media_type_plugin = Registry().get(media_type)
# the variable suffix names below for "songs" is "song", so change media_type to song
media_type_suffix = media_type
if media_type == 'songs':
media_type_suffix = 'song'
# turn on remote song feature to add to service
media_type_plugin.remote_triggered = True
setattr(media_type_plugin, "remote_{0}".format(media_type_suffix), openlp_id)
media_type_plugin.add_to_service(remote=openlp_id)
# also add verse references if they are there
if media_type == 'custom' and not html_details:
# check if the slide title is also a verse reference
# get a reference to the bible manager
bible_media = Registry().get('bibles')
bibles = bible_media.plugin.manager.get_bibles()
# get the current bible selected from the bibles plugin screen
bible = bible_media.version_combo_box.currentText()
if len(bible) == 0 and len(bibles) > 0:
# if we have no bible in the version_combo_box, but we have
# one or more bibles available, use one of those
bible = next(iter(bibles))
language_selection = bible_media.plugin.manager.get_language_selection(bible)
# replace long dashes with normal dashes -- why do these get inserted in PCO?
tmp_item_title = re.sub('', '-', item_title)
ref_list = parse_reference(tmp_item_title, bibles[bible], language_selection)
if ref_list:
bible_media.search_results = bibles[bible].get_verses(ref_list)
bible_media.list_view.clear()
bible_media.display_results()
bible_media.add_to_service()
service_manager.main_window.increment_progress_bar()
if update:
for old_service_item in old_service_items:
# see if this service_item contained within the current set of service items
# see if we have this same value in the new service
for service_index, service_item in enumerate(service_manager.service_items):
# we can compare songs to songs and custom to custom but not between them
if old_service_item['service_item'].name == 'songs' and \
service_item['service_item'].name == 'songs':
if old_service_item['service_item'].audit == service_item['service_item'].audit:
# get the timestamp from the xml of both the old and new and compare...
# modifiedDate="2018-06-30T18:44:35Z"
old_match = re.search('modifiedDate="(.+?)Z*"',
old_service_item['service_item'].xml_version)
old_datetime = datetime.strptime(old_match.group(1), '%Y-%m-%dT%H:%M:%S')
new_match = re.search('modifiedDate="(.+?)Z*"',
service_item['service_item'].xml_version)
new_datetime = datetime.strptime(new_match.group(1), '%Y-%m-%dT%H:%M:%S')
# if old timestamp is more recent than new, then copy old to new
if old_datetime > new_datetime:
service_manager.service_items[service_index] = old_service_item
break
elif old_service_item['service_item'].name == 'custom' and \
service_item['service_item'].name == 'custom':
""" we don't get actual slide content from the V2 PC API, so all we create by default is a
single slide with matching title and content. If the content
is different between the old serviceitem (previously imported
from PC and the new content that we are importing now, then
the assumption is that we updated this content and we want to
keep the old content after this update. If we actually updated
something on the PC site in this slide, it would have a
different title because that is all we can get the v2API """
if old_service_item['service_item'].title == service_item['service_item'].title:
if old_service_item['service_item'].slides != service_item['service_item'].slides:
service_manager.service_items[service_index] = old_service_item
break
service_manager.main_window.finished_progress_bar()
# select the first item
item = service_manager.service_manager_list.topLevelItem(0)
service_manager.service_manager_list.setCurrentItem(item)
service_manager.repaint_service_list(-1, -1)
service_manager.application.set_normal_cursor()

View File

@ -0,0 +1,124 @@
# -*- coding: utf-8 -*-
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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; version 2 of the License. #
# #
# 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, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`~openlp.plugins.planningcenter.lib.customimport` module provides
a function that imports a single custom slide into the database and returns
the database ID of the slide. This mimics the implementation for SongPlugin
that was used to import songs from Planning Center.
"""
import re
from openlp.core.common.i18n import translate
from openlp.core.common.registry import Registry
from openlp.core.lib.formattingtags import FormattingTags
from openlp.plugins.custom.lib.customxmlhandler import CustomXMLBuilder
from openlp.plugins.custom.lib.db import CustomSlide
class PlanningCenterCustomImport(object):
"""
Creates a custom slide and returns the database ID of that slide
:param item_title: The text to put on the slide.
:param html_details: The "details" element from PCO, with html formatting
:param theme_name: The theme_name to use for the slide.
"""
def add_slide(self, item_title, html_details, theme_name):
custom_slide = CustomSlide()
custom_slide.title = item_title
sxml = CustomXMLBuilder()
slide_content = ''
if html_details is None:
slide_content = item_title
else:
# we need non-html here, but the input is html
slide_content = self._process_details(html_details)
sxml.add_verse_to_lyrics('custom', str(1), slide_content)
custom_slide.text = str(sxml.extract_xml(), 'utf-8')
custom_slide.credits = 'pco'
custom_slide.theme_name = theme_name
custom = Registry().get('custom')
custom_db_manager = custom.plugin.db_manager
custom_db_manager.save_object(custom_slide)
return custom_slide.id
"""
Converts the "details" section of a PCO slide into text suitable for
slides in OLP
:param html_details: The html_details string from the PCO API
"""
def _process_details(self, html_details):
# convert <br> into new lines
html_details = re.sub(r'<br>', '\n', html_details)
# covert formatting tags from list to dict to I can do arbitrary lookups
FormattingTags.load_tags()
openlp_formatting_tags = {}
for formatting_tag in FormattingTags.get_html_tags():
openlp_formatting_tags[formatting_tag['desc']] = {}
openlp_formatting_tags[formatting_tag['desc']]['start tag'] = formatting_tag['start tag']
openlp_formatting_tags[formatting_tag['desc']]['end tag'] = formatting_tag['end tag']
# convert bold
html_details = re.sub(r'<strong>(.*?)</strong>',
r"{start}\1{end}".format(
start=openlp_formatting_tags[
translate('OpenLP.FormattingTags', 'Bold')]['start tag'],
end=openlp_formatting_tags[
translate('OpenLP.FormattingTags', 'Bold')]['end tag']),
html_details)
# convert underline
html_details = re.sub(r'<span style="text-decoration: underline;">(.*?)</span>',
r"{start}\1{end}".format(
start=openlp_formatting_tags[
translate('OpenLP.FormattingTags', 'Underline')]['start tag'],
end=openlp_formatting_tags[
translate('OpenLP.FormattingTags', 'Underline')]['end tag']),
html_details)
# convert italics
html_details = re.sub(r'<em>(.*?)</em>',
r"{start}\1{end}".format(
start=openlp_formatting_tags[
translate('OpenLP.FormattingTags', 'Italics')]['start tag'],
end=openlp_formatting_tags[
translate('OpenLP.FormattingTags', 'Italics')]['end tag']),
html_details)
# convert PlanningCenter colors to OpenLP base tags
color_lookup = {
'Red': 'ff0000',
'Black': '000000',
'Blue': '0000ff',
'Yellow': 'ffff00',
'Green': '008000',
'Pink': 'ff99cc',
'Orange': 'ff6600',
'Purple': '800080',
'White': 'ffffff',
}
for color in color_lookup.keys():
html_details = re.sub(r'<span style="color: #{number};">(.*?)</span>'.format(number=color_lookup[color]),
r"{start}\1{end}".format(
start=openlp_formatting_tags[
translate('OpenLP.FormattingTags', color)]['start tag'],
end=openlp_formatting_tags[
translate('OpenLP.FormattingTags', color)]['end tag']),
html_details)
# throw away the rest of the html
html_details = re.sub(r'(<!--.*?-->|<[^>]*>)', '', html_details)
return html_details

View File

@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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; version 2 of the License. #
# #
# 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, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`~openlp.plugins.planningcenter.lib.planningcenter_api` module contains
an API interface for the V2API for Planning Center Online
"""
import logging
import os
import re
import ssl
import urllib.error
import urllib.request
from json import loads, load, dump
log = logging.getLogger(__name__)
class PlanningCenterAPI:
"""
The :class:`PlanningCenterAPI` class is Planning Center Online API Class for
the V2API.
"""
def __init__(self, application_id, secret):
"""
Initialize.
:param application_id: The Application ID from Planning Center Online
for API authentication.
:param secret: The secret key from Planning Center Online for API
authentication
"""
self.api_url = "https://api.planningcenteronline.com/services/v2/"
self.application_id = application_id
self.secret = secret
""" airplane mode will cache responses from PCO such that if you request
the same resource a second time, it will respond with the cached
response. This is useful for doing development work on an airplane,
and for automated testing.
The production default is False """
self.airplane_mode = False
module_directory = os.path.dirname(__file__)
self.airplane_mode_directory = "{0}/airplane_mode".format(module_directory)
if os.path.isdir(self.airplane_mode_directory):
self.airplane_mode = True
def get_from_services_api(self, url_suffix):
"""
Gets the response from the API for the provided url_suffix and returns
the response as an object.
:param url_suffix: The query part of the URL for the API. The base is
in self.api_url, and this suffix is appended to that to make the query.
"""
airplane_mode_suffix = re.sub(r'\W', '_', url_suffix)
if len(airplane_mode_suffix) == 0:
airplane_mode_suffix = 'null'
airplane_mode_filename = "{0}/{1}.json".format(
self.airplane_mode_directory, airplane_mode_suffix)
if self.airplane_mode:
if os.path.isfile(airplane_mode_filename):
with open(airplane_mode_filename) as json_file:
api_response_object = load(json_file)
return api_response_object
else:
log.warning("Airplane Mode file not present:{filename}".format(filename=airplane_mode_filename))
if (not os.environ.get('PYTHONHTTPSVERIFY', '') and
getattr(ssl, '_create_unverified_context', None)):
ssl._create_default_https_context = ssl._create_unverified_context
# create a password manager
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
password_mgr.add_password(realm=None, uri=self.api_url,
user=self.application_id, passwd=self.secret)
handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
# create "opener" (OpenerDirector instance)
opener = urllib.request.build_opener(handler)
# use the opener to fetch a URL
opener.open(self.api_url + url_suffix)
# Install the opener.
# Now all calls to urllib.request.urlopen use our opener.
urllib.request.install_opener(opener)
api_response_string = urllib.request.urlopen(self.api_url + url_suffix,
timeout=30).read()
api_response_object = loads(api_response_string.decode('utf-8'))
# write out the responses as json files for airplane mode...
if self.airplane_mode:
with open(airplane_mode_filename, 'w') as outfile:
dump(api_response_object, outfile)
return api_response_object
def check_credentials(self):
"""
Attempts to connect to the base URL (self.api_url) with the credentials
provided during initialization.
If successful, it returns the name of the organization for the Planning
Center Online account. If failure, it returns an empty string.
"""
try:
response = self.get_from_services_api('')
organization = response['data']['attributes']['name']
return organization
except urllib.error.HTTPError as error:
if error.code == 401:
return ''
else:
raise
def get_service_type_list(self):
"""
Gets the list of service types (i.e. sunday morning, sunday evening,
special events, etc) configured in the Planning Center Online Interface
and the IDs for those service types.
"""
# Get ServiceTypes
service_types_url_suffix = 'service_types'
service_types = self.get_from_services_api(service_types_url_suffix)
return service_types['data']
def get_plan_list(self, service_type_id):
"""
Returns the list of plans available for a given service_type_id.
:param service_type_id: The ID of the service_type
(from self.get_service_type_list) from which to list the available plans.
"""
if service_type_id:
self.current_service_type_id = service_type_id
# get the 10 next future services (including today)
future_plans_url_suffix = \
"service_types/{0}/plans?filter=future&per_page=10&order=sort_date".format(service_type_id)
future_plans = self.get_from_services_api(future_plans_url_suffix)
# get the 10 most recent past services
past_plans_url_suffix = \
"service_types/{0}/plans?filter=past&per_page=10&order=-sort_date".format(service_type_id)
past_plans = self.get_from_services_api(past_plans_url_suffix)
plan_list = list(reversed(future_plans['data'])) + past_plans['data']
return plan_list
def get_items_dict(self, plan_id):
"""
Gets all items for a given plan ID (from self.get_plan_list), along with
their songs and arrangements
:param plan_id: The ID of the Plan from which to query all Plan Items.
"""
itemsURLSuffix = "service_types/{0}/plans/{1}/items?include=song,arrangement&per_page=100".\
format(self.current_service_type_id, plan_id)
items = self.get_from_services_api(itemsURLSuffix)
return items

View File

@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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 <https://www.gnu.org/licenses/>. #
##########################################################################
"""
The :mod:`~openlp.plugins.planningcenter.lib.planningcentertab` module contains
the settings tab for the PlanningCenter plugin
"""
from PyQt5 import QtCore, QtWidgets
from openlp.core.common.i18n import translate
from openlp.core.common.settings import Settings
from openlp.core.lib.settingstab import SettingsTab
from openlp.plugins.planningcenter.lib.planningcenter_api import PlanningCenterAPI
class PlanningCenterTab(SettingsTab):
"""
PlanningCenterTab is the alerts settings tab in the settings dialog.
"""
def setup_ui(self):
self.setObjectName('PlanningCenterTab')
self.tab_layout = QtWidgets.QVBoxLayout(self)
self.tab_layout.setObjectName('tab_layout')
self.tab_layout.setAlignment(QtCore.Qt.AlignTop)
self.auth_group_box = QtWidgets.QGroupBox(self)
self.tab_layout.addWidget(self.auth_group_box)
self.auth_layout = QtWidgets.QFormLayout(self.auth_group_box)
self.auth_layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.AllNonFixedFieldsGrow)
# notice
self.notice_label = QtWidgets.QLabel(self.auth_group_box)
self.notice_label.setWordWrap(True)
self.auth_layout.addRow(self.notice_label)
# instructions
self.instructions_label = QtWidgets.QLabel(self.auth_group_box)
self.instructions_label.setWordWrap(True)
self.auth_layout.addRow(self.instructions_label)
# application_id
self.application_id_label = QtWidgets.QLabel(self.auth_group_box)
self.application_id_line_edit = QtWidgets.QLineEdit(self.auth_group_box)
self.application_id_line_edit.setMaxLength(64)
self.auth_layout.addRow(self.application_id_label, self.application_id_line_edit)
# secret
self.secret_label = QtWidgets.QLabel(self.auth_group_box)
self.secret_line_edit = QtWidgets.QLineEdit(self.auth_group_box)
self.secret_line_edit.setMaxLength(64)
self.auth_layout.addRow(self.secret_label, self.secret_line_edit)
# Buttons
self.button_layout = QtWidgets.QDialogButtonBox(self.auth_group_box)
self.test_credentials_button = QtWidgets.QPushButton(self.auth_group_box)
self.button_layout.addButton(self.test_credentials_button, QtWidgets.QDialogButtonBox.AcceptRole)
self.auth_layout.addRow(self.button_layout)
# signals
self.test_credentials_button.clicked.connect(self.on_test_credentials_button_clicked)
def retranslate_ui(self):
self.auth_group_box.setTitle(translate('PlanningCenterPlugin.PlanningCenterTab', 'Authentication Settings'))
self.application_id_label.setText(translate('PlanningCenterPlugin.PlanningCenterTab', 'Application ID:'))
self.secret_label.setText(translate('PlanningCenterPlugin.PlanningCenterTab', 'Secret:'))
self.notice_label.setText(
translate('PlanningCenterPlugin.PlanningCenterTab', '<strong>Note:</strong> '
'An Internet connection and a Planning Center Online Account are required in order to \
import plans from Planning Center Online.')
)
self.instructions_label.setText(
translate('PlanningCenterPlugin.PlanningCenterTab',
"""Enter your <b>Planning Center Online</b> <i>Personal Access Token</i> details in the text boxes \
below. Personal Access Tokens are created by doing the following:
<ol>
<li>Login to your Planning Center Online account at<br>
<a href=https://api.planningcenteronline.com/oauth/applications>
https://api.planningcenteronline.com/oauth/applications</a></li>
<li>Click the "New Personal Access Token" button at the bottom of the screen.</li>
<li>Enter a description of your use case (eg. "OpenLP Integration")</li>
<li>Copy and paste the provided Application ID and Secret values below.</li>
</ol>"""))
self.test_credentials_button.setText(translate('PlanningCenterPlugin.PlanningCenterAuthForm',
'Test Credentials'))
def resizeEvent(self, event=None):
"""
Don't call SettingsTab resize handler because we are not using left/right columns.
"""
QtWidgets.QWidget.resizeEvent(self, event)
def load(self):
"""
Load the settings into the UI.
"""
settings = Settings()
settings.beginGroup(self.settings_section)
self.application_id = settings.value('application_id')
self.secret = settings.value('secret')
settings.endGroup()
self.application_id_line_edit.setText(self.application_id)
self.secret_line_edit.setText(self.secret)
def save(self):
"""
Save the changes on exit of the Settings dialog.
"""
settings = Settings()
settings.beginGroup(self.settings_section)
settings.setValue('application_id', self.application_id_line_edit.text())
settings.setValue('secret', self.secret_line_edit.text())
settings.endGroup()
def on_test_credentials_button_clicked(self):
"""
Tests if the credentials are valid
"""
application_id = self.application_id_line_edit.text()
secret = self.secret_line_edit.text()
if len(application_id) == 0 or len(secret) == 0:
QtWidgets.QMessageBox.warning(self, "Authentication Failed",
"Please enter values for both Application ID and Secret",
QtWidgets.QMessageBox.Ok)
return
test_auth = PlanningCenterAPI(application_id, secret)
organization = test_auth.check_credentials()
if len(organization):
QtWidgets.QMessageBox.information(self, 'Planning Center Online Authentication Test',
"Authentication successful for organization: {0}".format(organization),
QtWidgets.QMessageBox.Ok)
else:
QtWidgets.QMessageBox.warning(self, "Authentication Failed",
"Authentiation Failed",
QtWidgets.QMessageBox.Ok)

View File

@ -0,0 +1,171 @@
# -*- coding: utf-8 -*-
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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; version 2 of the License. #
# #
# 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, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`~openlp.plugins.planningcenter.lib.songimport` module provides
2 classes that are used to import Planning Center Online Service Songs into
an OpenLP Service.
"""
import re
from openlp.core.common.registry import Registry
from openlp.plugins.songs.lib.db import Song
from openlp.plugins.songs.lib.importers.songimport import SongImport
class PlanningCenterSongImport(SongImport):
"""
The :class:`PlanningCenterSongImport` class subclasses SongImport
and provides interfaces for parsing lyrics and adding songs from a
Planning Center Service into an OpenLP Service.
"""
def __init__(self):
"""
Initialize
"""
songs = Registry().get('songs')
manager = songs.plugin.manager
SongImport.__init__(self, manager, file_path=None)
def add_song(self, item_title, author, lyrics, theme_name, last_modified):
"""
Builds and adds song to the database and returns the Song ID
:param item_title: The song title.
:param author: Author String from Planning Center
:param lyrics: Lyrics String from Planning Center
:param theme_name: Theme String to use for this song
:param last_modified: DateTime of last modified date for this song
"""
self.set_defaults()
self.title = item_title
self.theme_name = theme_name
if author:
self.parse_author(author)
# handle edge condition where a song has no lyrics set
if lyrics is None:
self.add_verse(item_title)
else:
verses = self._split_lyrics_into_verses(lyrics)
for verse in verses:
if len(verse['verse_type']):
verse_def = verse['verse_type'] + verse['verse_number']
self.add_verse(verse_text=verse['verse_text'], verse_def=verse_def)
else:
self.add_verse(verse_text=verse['verse_text'])
openlp_id = self.finish(temporary_flag=True)
# set the last_updated date/time based on the PCO date/time so I can look for updates
song = self.manager.get_object(Song, openlp_id)
song.last_modified = last_modified
self.manager.save_object(song)
return openlp_id
def _split_lyrics_into_verses(self, lyrics):
"""
Parses Planning Center Lyrics and returns an list of verses, where each
verse is a dictionary that looks like:
verse['verse_type']
verse['verse_number']
verse['verse_text']
:param lyrics: Lyrics String from Planning Center
"""
# create a regular expression for potential VERSE,CHORUS tags included inline inside the lyrics...
verse_marker_pattern = re.compile(r'^(v|verse|c|chorus|bridge|prechorus|instrumental|intro|outro|vamp|breakdown|ending|interlude|tag|misc)\s*(\d*)$', re.IGNORECASE) # noqa: E501
# create regex for an END marker. content after this marker will be ignored
end_marker_pattern = re.compile('^{(<.*?>)?END(<.*?>)?}$')
verse_type = ''
verse_number = ''
verse_text = ''
is_end_marker = False
output_verses = []
input_verses = lyrics.split("\n\n")
for verse in input_verses:
for line in verse.split("\n"):
if end_marker_pattern.search(line):
# see if this is the end marker (stop parsing verses if it is)
is_end_marker = True
# strip out curly braces and the content inside {}
line = re.sub('{.*?}+', '', line)
# strip out any extraneous tags <...>
line = re.sub('<.*?>', '', line)
# remove beginning/trailing whitespace and line breaks
line = line.rstrip()
line = line.lstrip()
regex_match = verse_marker_pattern.search(line)
if regex_match:
if len(verse_text):
# if we have verse_text from the previous verse, submit it now
self._add_verse(output_verses, verse_type, verse_number, verse_text)
verse_text = ''
verse_type = self._lookup_openlp_verse_type(regex_match.group(1))
# if we have a number after the verse marker, capture it here
if regex_match.group(2):
verse_number = regex_match.group(2)
else:
# if empty, let openlp auto-create it for us
verse_number = ''
continue
else:
if len(verse_text):
verse_text += "\n{0}".format(line)
else:
verse_text = line
if len(verse_text):
self._add_verse(output_verses, verse_type, verse_number, verse_text)
verse_text = ''
if is_end_marker:
return output_verses
return output_verses
def _lookup_openlp_verse_type(self, pco_verse_type):
"""
Provides a lookup table to map from a Planning Center Verse Type
to an OpenLP verse type.
:param pco_verse_type: Planning Center Verse Type String
"""
pco_verse_type_to_openlp = {}
pco_verse_type_to_openlp['VERSE'] = 'v'
pco_verse_type_to_openlp['V'] = 'v'
pco_verse_type_to_openlp['C'] = 'c'
pco_verse_type_to_openlp['CHORUS'] = 'c'
pco_verse_type_to_openlp['PRECHORUS'] = 'p'
pco_verse_type_to_openlp['INTRO'] = 'i'
pco_verse_type_to_openlp['ENDING'] = 'e'
pco_verse_type_to_openlp['BRIDGE'] = 'b'
pco_verse_type_to_openlp['OTHER'] = 'o'
openlp_verse_type = pco_verse_type_to_openlp['OTHER']
if pco_verse_type.upper() in pco_verse_type_to_openlp:
openlp_verse_type = pco_verse_type_to_openlp[pco_verse_type.upper()]
return openlp_verse_type
def _add_verse(self, output_verses, verse_type, verse_number, verse_text):
"""
Simple utility function that takes verse attributes and adds the verse
to the output_verses list.
:param output_verses: The list of verses that will ultimately be returned by
the _split_lyrics_into_verses function
:param verse_type: The OpenLP Verse Type, like v,c,e, etc...
:param verse_number: The verse number, if known, like 1, 2, 3, etc.
:param verse_text: The parsed verse text returned from _split_lyrics_into_verses
"""
new_verse = {}
new_verse['verse_type'] = verse_type
new_verse['verse_number'] = verse_number
new_verse['verse_text'] = verse_text
output_verses.append(new_verse)

View File

@ -0,0 +1,164 @@
# -*- coding: utf-8 -*-
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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; version 2 of the License. #
# #
# 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, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`~openlp.plugins.planningcenter.PlanningCenterPlugin` module contains
the Plugin class for the PlanningCenter plugin.
"""
import logging
from openlp.core.common.i18n import translate
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.lib.plugin import Plugin, StringContent
from openlp.core.lib.ui import create_action
from openlp.core.state import State
from openlp.core.ui.icons import UiIcons
from openlp.plugins.custom.lib.db import CustomSlide
from openlp.plugins.planningcenter.forms.selectplanform import SelectPlanForm
from openlp.plugins.planningcenter.lib.planningcentertab import PlanningCenterTab
from openlp.plugins.songs.lib.db import Song
log = logging.getLogger(__name__)
__default_settings__ = {
'planningcenter/application_id': '',
'planningcenter/secret': ''
}
class PlanningCenterPlugin(Plugin):
"""
This plugin enables the user to import services from Planning Center Online.
"""
log.info('PlanningCenter Plugin loaded')
def __init__(self):
"""
Create and set up the PlanningCenter plugin.
"""
super(PlanningCenterPlugin, self).__init__('planningcenter', __default_settings__,
settings_tab_class=PlanningCenterTab)
self.planningcenter_form = None
self.icon = UiIcons().planning_center
self.icon_path = self.icon
self.weight = -1
State().add_service('planning_center', self.weight, is_plugin=True)
State().update_pre_conditions('planning_center', self.check_pre_conditions())
def initialise(self):
"""
Initialise the plugin
"""
log.info('PlanningCenter Initialising')
super(PlanningCenterPlugin, self).initialise()
self.import_planning_center.setVisible(True)
def add_import_menu_item(self, import_menu):
"""
Add "PlanningCenter Service" to the **Import** menu.
:param import_menu: The actual **Import** menu item, so that your
actions can use it as their parent.
"""
self.import_planning_center = create_action(import_menu, 'import_planning_center',
text=translate('PlanningCenterPlugin', 'Planning Center Service'),
visible=False,
statustip=translate('PlanningCenterPlugin',
'Import Planning Center Service Plan \
from Planning Center Online.'),
triggers=self.on_import_planning_center_triggered
)
import_menu.addAction(self.import_planning_center)
def on_import_planning_center_triggered(self):
"""
Run the PlanningCenter importer.
"""
# Determine which dialog to show based on whether the auth values are set yet
self.application_id = Settings().value("planningcenter/application_id")
self.secret = Settings().value("planningcenter/secret")
if len(self.application_id) == 0 or len(self.secret) == 0:
self.planningcenter_form = Registry().get('settings_form')
self.planningcenter_form.exec(translate('PlanningCenterPlugin', 'PlanningCenter'))
else:
self.planningcenter_form = SelectPlanForm(Registry().get('main_window'), self)
self.planningcenter_form.exec()
@staticmethod
def about():
"""
Provides information for the plugin manager to display.
:return: A translatable string with some basic information about the
PlanningCenter plugin
"""
return translate('PlanningCenterPlugin', '<strong>PlanningCenter Plugin</strong>'
'<br />The planningcenter plugin provides an interface to import \
service plans from the Planning Center Online v2 API.')
def set_plugin_text_strings(self):
"""
Called to define all translatable texts of the plugin
"""
# Name PluginList
self.text_strings[StringContent.Name] = {
'singular': translate('PlanningCenterPlugin', 'PlanningCenter',
'name singular'),
'plural': translate('PlanningCenterPlugin', 'PlanningCenter',
'name plural')
}
# Name for MediaDockManager, SettingsManager
self.text_strings[StringContent.VisibleName] = {
'title': translate('PlanningCenterPlugin', 'PlanningCenter',
'container title')
}
# Middle Header Bar
tooltips = {
'load': '',
'import': translate('PlanningCenterPlugin', 'Import All Plan Items \
into Current Service'),
'new': '',
'edit': '',
'delete': '',
'preview': '',
'live': '',
'service': '',
}
self.set_plugin_ui_text_strings(tooltips)
def finalise(self):
"""
Tidy up on exit
"""
log.info('PlanningCenter Finalising')
self.import_planning_center.setVisible(False)
self.new_service_created()
# call songs plugin manager to delete temporary songs
songs_manager = Registry().get('songs').plugin.manager
songs = songs_manager.get_all_objects(Song, Song.temporary == True) # noqa: E712
for song in songs:
songs_manager.delete_object(Song, song.id)
# call custom manager to delete pco slides
custom_manager = Registry().get('custom').plugin.db_manager
pco_slides = custom_manager.get_all_objects(CustomSlide, CustomSlide.credits == 'pco')
for slide in pco_slides:
custom_manager.delete_object(CustomSlide, slide.id)
super(PlanningCenterPlugin, self).finalise()

View File

@ -128,11 +128,11 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
self.username_edit.setFocus()
return QtWidgets.QDialog.exec(self)
def done(self, r):
def done(self, result_code):
"""
Log out of SongSelect.
:param r: The result of the dialog.
:param result_code: The result of the dialog.
"""
log.debug('Closing SongSelectForm')
if self.stacked_widget.currentIndex() > 0:
@ -149,7 +149,7 @@ class SongSelectForm(QtWidgets.QDialog, Ui_SongSelectDialog, RegistryProperties)
self.song_select_importer.logout()
self.application.process_events()
progress_dialog.setValue(2)
return QtWidgets.QDialog.done(self, r)
return QtWidgets.QDialog.done(self, result_code)
def _update_login_progress(self):
"""

View File

@ -319,9 +319,11 @@ class SongImport(QtCore.QObject):
else:
return True
def finish(self):
def finish(self, temporary_flag=False):
"""
All fields have been set to this song. Write the song to disk.
:param temporary_flag: should this song be marked as temporary in the db (default=False)
"""
if not self.check_complete():
self.set_defaults()
@ -379,6 +381,7 @@ class SongImport(QtCore.QObject):
if topic is None:
topic = Topic.populate(name=topic_text)
song.topics.append(topic)
song.temporary = temporary_flag
# We need to save the song now, before adding the media files, so that
# we know where to save the media files to.
clean_song(self.manager, song)
@ -393,7 +396,7 @@ class SongImport(QtCore.QObject):
song.media_files.append(MediaFile.populate(file_path=file_path, weight=weight))
self.manager.save_object(song)
self.set_defaults()
return True
return song.id
def copy_media_file(self, song_id, file_path):
"""

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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 <https://www.gnu.org/licenses/>. #
##########################################################################

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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 <https://www.gnu.org/licenses/>. #
##########################################################################

View File

@ -0,0 +1,389 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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 <https://www.gnu.org/licenses/>. #
##########################################################################
"""
Package to test the openlp.plugins.planningcenter.forms.selectplanform package.
"""
import os
import re
from datetime import date, datetime
from unittest import TestCase, skip
from unittest.mock import patch, MagicMock
from PyQt5 import QtWidgets, QtTest, QtCore
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.state import State
from openlp.core.ui.servicemanager import ServiceManager
from openlp.core.ui.settingsform import SettingsForm
from openlp.core.ui.thememanager import ThemeManager
from openlp.plugins.bibles.bibleplugin import BiblePlugin
from openlp.plugins.bibles.lib.mediaitem import BibleMediaItem
from openlp.plugins.custom.customplugin import CustomPlugin
from openlp.plugins.custom.lib.mediaitem import CustomMediaItem
from openlp.plugins.planningcenter.forms.selectplanform import SelectPlanForm
from openlp.plugins.planningcenter.planningcenterplugin import PlanningCenterPlugin
from openlp.plugins.songs.lib.mediaitem import SongMediaItem
from openlp.plugins.songs.songsplugin import SongsPlugin
from tests.helpers.testmixin import TestMixin
TEST_PATH = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', 'resources', 'planningcenter'))
class TestSelectPlanForm(TestCase, TestMixin):
"""
Test the SelectPlanForm class
"""
def setUp(self):
"""
Create the UI
"""
self.registry = Registry()
Registry.create()
self.setup_application()
self.build_settings()
State().load_settings()
Registry().register('main_window', MagicMock(service_manager_settings_section='servicemanager'))
self.application_id = 'abc'
self.secret = '123'
Settings().setValue('planningcenter/application_id', self.application_id)
Settings().setValue('planningcenter/secret', self.secret)
# init the planning center plugin so we have default values defined for Settings()
self.planning_center_plugin = PlanningCenterPlugin()
# setup our form
self.form = SelectPlanForm()
self.form.planning_center_api.airplane_mode = True
self.form.planning_center_api.airplane_mode_directory = TEST_PATH
self.theme_manager = ThemeManager(None)
self.theme_manager.get_themes = MagicMock()
self.theme_manager.get_themes.return_value = ['themeA', 'themeB']
def tearDown(self):
"""
Delete all the C++ objects at the end so that we don't have a segfault
"""
del self.form
del self.planning_center_plugin
del self.theme_manager
del self.registry
self.destroy_settings()
def test_initial_defaults(self):
"""
Test that the SelectPlanForm displays with correct defaults
"""
# GIVEN: An SelectPlanForm instance with airplane mode enabled, resources available,
# a theme manager with mocked themes, and a fake date = Sunday (7/29/2018)
with patch('PyQt5.QtWidgets.QDialog.exec'), \
patch('openlp.plugins.planningcenter.forms.selectplanform.date') as mock_date:
# need to always return 9/29/2019 for date.today()
mock_date.today.return_value = date(2019, 9, 29)
mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
# WHEN: The form is shown
self.form.exec()
# THEN: The correct count of service types show up in the combo box
combo_box_count = self.form.service_type_combo_box.count()
self.assertEqual(combo_box_count, 2,
'The service_type_combo_box contains 2 items')
# The first service type is selected
self.assertEqual(self.form.service_type_combo_box.currentText(), 'gbf',
'The service_type_combo_box defaults to "gbf"')
# the selected plan is today (the mocked date is a Sunday)
self.assertEqual(self.form.plan_selection_combo_box.currentText(),
date.strftime(mock_date.today.return_value, '%B %d, %Y'),
'Incorrect default date selected for Plan Date')
# count the number of themes listed and make sure it matches expected value
self.assertEqual(self.form.song_theme_selection_combo_box.count(),
2, 'Count of song themes is incorrect')
self.assertEqual(self.form.slide_theme_selection_combo_box.count(),
2, 'Count of custom slide themes is incorrect')
def test_warning_messagebox_shown_for_bad_credentials(self):
"""
Test that if we don't have good credentials, then it will show a QMessageBox with a warning in it
"""
# GIVEN: A SelectPlanForm instance with airplane mode enabled, resources, available,
# mocked check_credentials function to return ''
with patch('PyQt5.QtWidgets.QDialog.exec'), \
patch.object(self.form.planning_center_api, 'check_credentials', return_value=''), \
patch('PyQt5.QtWidgets.QMessageBox.warning') as mock_warning:
# WHEN: form is displayed
self.form.exec()
# THEN: we should have called a warning messagebox
mock_warning.assert_called_once()
def test_disable_import_buttons(self):
"""
Test that the import buttons are disabled when the "Select Plan Date" element in the
Plan Selection List is selected.
"""
# GIVEN: An SelectPlanForm instance with airplane mode enabled, resources available, and the form
with patch('PyQt5.QtWidgets.QDialog.exec'):
self.form.exec()
# WHEN: The Select Plan combo box is set to "Select Plan Date"
index = self.form.plan_selection_combo_box.findText('Select Plan Date')
self.form.plan_selection_combo_box.setCurrentIndex(index)
# THEN: "Import New" and "Refresh Service" buttons become inactive
self.assertEqual(self.form.import_as_new_button.isEnabled(), False,
'"Import as New" button should be disabled')
self.assertEqual(self.form.update_existing_button.isEnabled(), False,
'"Refresh Service" button should be disabled')
def test_default_plan_date_is_next_sunday(self):
"""
Test that the SelectPlanForm displays Next Sunday's Date by Default
"""
# GIVEN: An SelectPlanForm instance with airplane mode enabled, resources available,
# a theme manager with mocked themes, and a fake date = (9/24/2019)
with patch('PyQt5.QtWidgets.QDialog.exec'), \
patch('openlp.plugins.planningcenter.forms.selectplanform.date') as mock_date:
# need to always return 9/24/2019 for date.today()
mock_date.today.return_value = date(2019, 9, 24)
mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
self.form.exec()
# WHEN: The second (index=1) service type is selected
self.form.service_type_combo_box.setCurrentIndex(1)
# THEN: The plan selection date is 9/29 (the following Sunday)
self.assertEqual(self.form.plan_selection_combo_box.currentText(), 'September 29, 2019',
'The next Sunday\'s Date is not selected in the plan_selection_combo_box')
def test_service_type_changed_called_when_service_type_combo_changed(self):
"""
Test that the "on_service_type_combobox_changed" function is executed when the
service_type_combobox is changed
"""
# GIVEN: An SelectPlanForm instance with airplane mode enabled, resources available
with patch('PyQt5.QtWidgets.QDialog.exec'):
self.form.exec()
# WHEN: The Service Type combo is set to index 1
self.form.service_type_combo_box.setCurrentIndex(1)
# THEN: The on_service_type_combobox_changed function is called
assert self.form.plan_selection_combo_box.count() > 0, 'Plan Selection Combo Box is not empty'
assert self.form.plan_selection_combo_box.itemText(0) == 'Select Plan Date', 'Plan Combo Box has default text'
def test_plan_selection_changed_called_when_plan_selection_combo_changed(self):
"""
Test that the "on_plan_selection_combobox_changed" function is executed when the
plan_selection_combobox is changed
"""
# GIVEN: An SelectPlanForm instance with airplane mode enabled, resources available,
with patch('PyQt5.QtWidgets.QDialog.exec'):
self.form.exec()
# WHEN: The Service Type combo is set to index 1
self.form.service_type_combo_box.setCurrentIndex(1)
self.form.plan_selection_combo_box.setCurrentIndex(1)
# THEN: The import and update buttons should be enabled
assert self.form.import_as_new_button.isEnabled() is True, 'Import button should be enabled'
assert self.form.update_existing_button.isEnabled() is True, 'Update button should be enabled'
def test_settings_tab_displayed_when_edit_auth_button_clicked(self):
"""
Test that the settings dialog is displayed when the edit_auth_button is clicked
"""
# GIVEN: A SelectPlanForm instance with airplane mode enabled and resources available
with patch('PyQt5.QtWidgets.QDialog.exec'), \
patch('openlp.core.ui.settingsform.SettingsForm.exec') as mock_settings_form:
SettingsForm()
self.form.exec()
# WHEN: the edit_auth_button is clicked
QtTest.QTest.mouseClick(self.form.edit_auth_button, QtCore.Qt.LeftButton)
self.assertEqual(mock_settings_form.called, 1, "Settings Form opened when edit_auth_button clicked")
def test_import_function_called_when_import_button_clicked(self):
"""
Test that the "on_import_as_new_button_clicked" function is executed when the
"Import New" button is clicked
"""
# GIVEN: An SelectPlanForm instance with airplane mode enabled, resources available,
with patch('PyQt5.QtWidgets.QDialog.exec'), \
patch('openlp.plugins.planningcenter.forms.selectplanform.SelectPlanForm._do_import') \
as mock_do_import:
self.form.exec()
# WHEN: The Service Type combo is set to index 1 and the Select Plan combo box is set
# to index 1 and the "Import New" button is clicked
self.form.service_type_combo_box.setCurrentIndex(1)
self.form.plan_selection_combo_box.setCurrentIndex(4)
QtTest.QTest.mouseClick(self.form.import_as_new_button, QtCore.Qt.LeftButton)
# THEN: The on_import_as_new_button_cliced function is called
mock_do_import.assert_called_with(update=False)
def test_service_imported_when_import_button_clicked(self):
"""
Test that a service is imported when the "Import New" button is clicked
"""
# GIVEN: An SelectPlanForm instance with airplane mode enabled, resources available,
# mocked out "on_new_service_clicked"
with patch('PyQt5.QtWidgets.QDialog.exec'), \
patch('openlp.core.common.registry.Registry.get'), \
patch('openlp.plugins.planningcenter.lib.songimport.PlanningCenterSongImport.finish') \
as mock_song_import, \
patch('openlp.plugins.planningcenter.lib.customimport.CustomSlide') as mock_custom_slide_import, \
patch('openlp.plugins.planningcenter.forms.selectplanform.parse_reference') as mock_bible_import, \
patch('openlp.plugins.planningcenter.forms.selectplanform.date') as mock_date:
# need to always return 9/29/2019 for date.today()
mock_date.today.return_value = date(2019, 9, 29)
mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
self.form.exec()
# WHEN: The Service Type combo is set to index 1 and the Select Plan combo box is set to
# index 1 and the "Import New" button is clicked
self.form.service_type_combo_box.setCurrentIndex(1)
QtTest.QTest.mouseClick(self.form.import_as_new_button, QtCore.Qt.LeftButton)
# THEN: There should be 5 service items added, 1 song, 3 custom slides (one is a bible
# title slide), and 1 bible verse
self.assertEqual(mock_song_import.call_count, 1, '1 song added via song_media_item')
self.assertEqual(mock_custom_slide_import.call_count, 4, '4 custom slide added via custom_media_item')
self.assertEqual(mock_bible_import.call_count, 2, '2 bible verses submitted for parsing')
def test_service_refreshed_when_refresh_button_clicked(self):
"""
Test that a service is refreshed when the "Refresh Service" button is clicked
"""
# GIVEN: An SelectPlanForm instance with airplane mode enabled, resources available,
# mocked out "on_new_service_clicked"
with patch('PyQt5.QtWidgets.QDialog.exec'), \
patch('openlp.core.common.registry.Registry.get'), \
patch('openlp.plugins.planningcenter.lib.songimport.PlanningCenterSongImport.finish') \
as mock_song_import, \
patch('openlp.plugins.planningcenter.lib.customimport.CustomSlide') as mock_custom_slide_import, \
patch('openlp.plugins.planningcenter.forms.selectplanform.parse_reference') as mock_bible_import, \
patch('openlp.plugins.planningcenter.forms.selectplanform.date') as mock_date:
# need to always return 9/29/2019 for date.today()
mock_date.today.return_value = date(2019, 9, 29)
mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
self.form.exec()
# WHEN: The Service Type combo is set to index 1 and the Select Plan combo box is
# set to index 1 and the "Update" button is clicked
self.form.service_type_combo_box.setCurrentIndex(1)
QtTest.QTest.mouseClick(self.form.update_existing_button, QtCore.Qt.LeftButton)
# THEN: There should be 5 service items added, 1 song, 3 custom slides (one is a bible
# title slide), and 1 bible verse
self.assertEqual(mock_song_import.call_count, 1, '1 song added via song_media_item')
self.assertEqual(mock_custom_slide_import.call_count, 4, '4 custom slide added via custom_media_item')
self.assertEqual(mock_bible_import.call_count, 2, '2 bible verses submitted for parsing')
def test_other_bible_is_used_when_bible_gui_form_is_blank(self):
"""
Test that an other bible is used when the GUI has an empty string for current selected bible
"""
# GIVEN: An SelectPlanForm instance with airplane mode enabled, resources available,
# mocked out "on_new_service_clicked"
with patch('PyQt5.QtWidgets.QDialog.exec'), \
patch('openlp.core.common.registry.Registry.get') as mock_get, \
patch('openlp.plugins.planningcenter.lib.songimport.PlanningCenterSongImport.finish'), \
patch('openlp.plugins.planningcenter.lib.customimport.CustomSlide'), \
patch('openlp.plugins.planningcenter.forms.selectplanform.parse_reference') as mock_bible_import, \
patch('openlp.plugins.planningcenter.forms.selectplanform.date') as mock_date:
# need to always return 9/29/2019 for date.today()
mock_date.today.return_value = date(2019, 9, 29)
mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
mock_bibles = {}
mock_bibles['other_bible'] = MagicMock()
mock_get.return_value.plugin.manager.get_bibles.return_value = mock_bibles
mock_get.return_value.version_combo_box.currentText.return_value = ''
self.form.exec()
# WHEN: The Service Type combo is set to index 1 and the Select Plan combo box
# is set to index 1 and the "Import New" button is clicked
self.form.service_type_combo_box.setCurrentIndex(1)
QtTest.QTest.mouseClick(self.form.import_as_new_button, QtCore.Qt.LeftButton)
# THEN: There should be 2 bible verse parse attempts
self.assertEqual(mock_bible_import.call_count, 2, '2 bible verses submitted for parsing')
def _create_mock_action(self, name, **kwargs):
"""
Create a fake action with some "real" attributes for Service Manager
"""
action = QtWidgets.QAction(self.service_manager)
action.setObjectName(name)
if kwargs.get('triggers'):
action.triggered.connect(kwargs.pop('triggers'))
self.service_manager.toolbar.actions[name] = action
return action
@skip("fails to run when executed with all other openlp tests. awaiting pytest fixtures to enable again")
def test_less_mocking_service_refreshed_when_refresh_button_clicked_test(self):
"""
Test that a service is refreshed when the "Refresh Service" button is clicked
"""
# GIVEN: An SelectPlanForm instance with airplane mode enabled, resources available,
# mocked out "on_new_service_clicked"
with patch('PyQt5.QtWidgets.QDialog.exec'), \
patch('openlp.plugins.planningcenter.forms.selectplanform.date') as mock_date:
# need to always return 9/29/2019 for date.today()
mock_date.today.return_value = date(2019, 9, 29)
mock_date.side_effect = lambda *args, **kw: date(*args, **kw)
# init ServiceManager
Registry().register('plugin_manager', MagicMock())
Registry().register('application', MagicMock())
Registry().register('renderer', MagicMock())
self.service_manager = ServiceManager()
self.service_manager.setup_ui(self.service_manager)
# init songs plugin
with patch('openlp.plugins.songs.lib.mediaitem.EditSongForm'), \
patch('openlp.plugins.custom.lib.mediaitem.EditCustomForm'), \
patch('openlp.core.lib.mediamanageritem.create_widget_action'), \
patch('openlp.core.widgets.toolbar.create_widget_action'):
# init songs plugin
songs_plugin = SongsPlugin()
song_media_item = SongMediaItem(None, songs_plugin)
song_media_item.search_text_edit = MagicMock()
song_media_item.settings_section = 'songs'
song_media_item.initialise()
# init custom plugin
custom_plugin = CustomPlugin()
CustomMediaItem(None, custom_plugin)
# init bible plugin
bible_plugin = BiblePlugin()
bible_media_item = BibleMediaItem(None, bible_plugin)
bible_media_item.build_display_results = MagicMock()
self.form.exec()
# WHEN:
# The Service Type combo is set to index 1 and "Import New" button is clicked
self.form.service_type_combo_box.setCurrentIndex(1)
QtTest.QTest.mouseClick(self.form.import_as_new_button, QtCore.Qt.LeftButton)
# make changes to the now imported service items
# first, for serviceitem[0] update last_updated in xml_string and change "sweet" to "sublime"
old_match = re.search('modifiedDate="(.+?)Z*"',
self.service_manager.service_items[0]['service_item'].xml_version)
old_string = old_match.group(1)
now_string = datetime.now().strftime('%Y-%m-%dT%H:%M:%S')
self.service_manager.service_items[0]['service_item'].xml_version = \
self.service_manager.service_items[0]['service_item'].xml_version.replace(old_string, now_string)
self.service_manager.service_items[0]['service_item'].xml_version = \
self.service_manager.service_items[0]['service_item'].xml_version.replace("sweet", "sublime")
# second, add the word modified to the slide text for serviceitem[1]
self.service_manager.service_items[1]['service_item'].slides[0]['text'] = \
self.service_manager.service_items[1]['service_item'].slides[0]['text'].replace("Test", "Modified Test")
# third, delete serviceitems[2] and serviceitem[3]
del self.service_manager.service_items[3]
del self.service_manager.service_items[2]
# last, draw the form again and request refresh
self.form.exec()
self.form.service_type_combo_box.setCurrentIndex(1)
QtTest.QTest.mouseClick(self.form.update_existing_button, QtCore.Qt.LeftButton)
# THEN:
# There should be 4 service items added
self.assertEqual(len(self.service_manager.service_items), 5, '5 items should be in the ServiceManager')
# Amazon Grace should still include sublime
self.assertTrue('sublime' in self.service_manager.service_items[0]['service_item'].xml_version)
# Slides in service_item[1] should still contain the word "Modified"
self.assertTrue('Modified' in self.service_manager.service_items[1]['service_item'].slides[0]['text'])

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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 <https://www.gnu.org/licenses/>. #
##########################################################################

View File

@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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 <https://www.gnu.org/licenses/>. #
##########################################################################
"""
Package to test the openlp.plugins.planningcenter.lib.planningcenter_api package.
"""
import os
import urllib.error
from unittest import TestCase
from unittest.mock import patch
from openlp.plugins.planningcenter.lib.planningcenter_api import PlanningCenterAPI
from tests.helpers.testmixin import TestMixin
TEST_PATH = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', '..', '..', '..', 'resources', 'planningcenter'))
class TestPlanningCenterAPI(TestCase, TestMixin):
"""
Test the PlanningCenterAPI class
"""
def test_init(self):
"""
Test that the api class can be instantiated with an application_id and secret
"""
# GIVEN: A PlanningCenterAPI Class
# WHEN: __init__ is called with an application id and secret
api = PlanningCenterAPI('application_id', 'secret')
# THEN:
# airplane mode should be false
self.assertFalse(api.airplane_mode, 'Class init without airplane mode')
def test_init_with_airplane_mode(self):
"""
Test that the api class can be instantiated with an application_id and secret
"""
# GIVEN: A PlanningCenterAPI Class
# WHEN: __init__ is called with an application id and secret and airplane_dir mocked
with patch('openlp.plugins.planningcenter.lib.planningcenter_api.os.path.isdir') as airplane_isdir:
airplane_isdir.return_value = True
api = PlanningCenterAPI('application_id', 'secret')
# THEN:
# airplane mode should be true
self.assertTrue(api.airplane_mode, 'Class init with airplane mode')
def test_get_from_services_api(self):
"""
Test that the get_from_services_api can be called in airplane mode
"""
# GIVEN: A PlanningCenterAPI Class
# WHEN: get_from_services_api is called with empty string as input ('') and airplane mode enabled
with patch('openlp.plugins.planningcenter.lib.planningcenter_api.os.path.isdir') as airplane_isdir, \
patch('openlp.plugins.planningcenter.lib.planningcenter_api.urllib.request.build_opener') \
as mock_opener, \
patch('openlp.plugins.planningcenter.lib.planningcenter_api.open'):
airplane_isdir.return_value = True
api = PlanningCenterAPI('application_id', 'secret')
mock_opener().open().read.return_value = "{\"foo\" : \"bar\"}".encode(encoding='UTF-8')
return_value = api.get_from_services_api('test')
# THEN:
# we should get back the return value we mocked
self.assertEqual(return_value['foo'], 'bar', "get_from_services_api returns correct value")
def test_check_credentials_returns_empty_string_for_bad_credentials(self):
"""
Test that check_credentials returns an empty string if authentication fails
"""
# GIVEN: A PlanningCenterAPI Class with mocked out get_from_services_api that returns a 401 (http auth) error
api = PlanningCenterAPI('application_id', 'secret')
with patch.object(api, 'get_from_services_api') as mock_get_services:
mock_get_services.side_effect = urllib.error.HTTPError(None, 401, None, None, None)
# WHEN: check_credentials is called
return_value = api.check_credentials()
# THEN: we have an empty string returns
assert return_value == '', "return string is empty for bad authentication"
def test_check_credentials_raises_other_exceptions(self):
# GIVEN: A PlanningCenterAPI Class with mocked out get_from_services_api that returns a 400 error
api = PlanningCenterAPI('application_id', 'secret')
with patch.object(api, 'get_from_services_api') as mock_get_services:
mock_get_services.side_effect = urllib.error.HTTPError(None, 300, None, None, None)
# WHEN: check_credentials is called in a try block
error_code = 0
try:
api.check_credentials()
except urllib.error.HTTPError as error:
error_code = error.code
# THEN: we received an exception with code of 300
assert error_code == 300, "correct exception is raised from check_credentials"
def test_check_credentials_pass(self):
"""
Test that check_credentials can be called in airplane mode
"""
# GIVEN: A PlanningCenterAPI Class
# WHEN: get_from_services_api is called with empty string as input ('') and airplane mode enabled
with patch('openlp.plugins.planningcenter.lib.planningcenter_api.os.path.isdir') as airplane_isdir, \
patch('openlp.plugins.planningcenter.lib.planningcenter_api.urllib.request.build_opener') \
as mock_opener, \
patch('openlp.plugins.planningcenter.lib.planningcenter_api.open'):
airplane_isdir.return_value = True
api = PlanningCenterAPI('application_id', 'secret')
mock_opener().open().read.return_value = "{\"data\": {\"attributes\": {\"name\": \"jpk\"}}}".\
encode(encoding='UTF-8')
return_value = api.check_credentials()
# THEN:
# Check credentials returns our mocked value
self.assertEqual(return_value, 'jpk', "check_credentials returns correct value for pass")

View File

@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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 <https://www.gnu.org/licenses/>. #
##########################################################################
"""
Package to test the openlp.plugins.planningcenter.lib.planningcentertab package.
"""
from unittest import TestCase
from unittest.mock import patch
from PyQt5 import QtCore, QtTest, QtWidgets
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.state import State
from openlp.plugins.planningcenter.lib.planningcentertab import PlanningCenterTab
from openlp.plugins.planningcenter.planningcenterplugin import PlanningCenterPlugin
from tests.helpers.testmixin import TestMixin
class TestPlanningCenterTab(TestCase, TestMixin):
"""
Test the PlanningCenterTab class
"""
def setUp(self):
"""
Create the UI
"""
self.setup_application()
self.registry = Registry()
Registry.create()
State().load_settings()
self.plugin = PlanningCenterPlugin()
Settings().setValue('planningcenter/application_id', 'abc')
Settings().setValue('planningcenter/secret', '123')
self.dialog = QtWidgets.QDialog()
self.tab = PlanningCenterTab(self.dialog, 'PlanningCenter')
self.tab.setup_ui()
self.tab.retranslate_ui()
self.tab.load()
self.tab.resizeEvent()
def tearDown(self):
"""
Delete all the C++ objects at the end so that we don't have a segfault
"""
del self.tab
del self.dialog
del self.registry
def test_bad_authentication_credentials(self):
"""
Test that the tab can be created and Test authentication clicked for bad application id and secret
"""
# GIVEN: A PlanningCenterTab Class
# WHEN: tab is drawn and application_id/secret values are entered in the GUI and the test buttin is clicked
with patch('openlp.plugins.planningcenter.lib.planningcentertab.PlanningCenterAPI.check_credentials') \
as mock_check_credentials, \
patch('openlp.plugins.planningcenter.lib.planningcentertab.QtWidgets.QMessageBox.warning') \
as mock_qmessagebox:
mock_check_credentials.return_value = ''
QtTest.QTest.mouseClick(self.tab.test_credentials_button, QtCore.Qt.LeftButton)
# THEN:
# The warning messagebox should be called
self.assertEqual(mock_qmessagebox.call_count, 1, 'Warning dialog used for bad credentials')
def test_empty_authentication_credentials(self):
"""
Test that the tab can be created and Test authentication clicked for missing application id and secret
"""
# GIVEN: A PlanningCenterTab Class
# WHEN: tab is drawn and application_id/secret values are entered in the GUI and the test buttin is clicked
with patch('openlp.plugins.planningcenter.lib.planningcentertab.QtWidgets.QMessageBox.warning') \
as mock_qmessagebox:
self.tab.application_id_line_edit.setText('')
self.tab.secret_line_edit.setText('')
QtTest.QTest.mouseClick(self.tab.test_credentials_button, QtCore.Qt.LeftButton)
# THEN:
# The warning messagebox should be called
self.assertEqual(mock_qmessagebox.call_count, 1, 'Warning dialog used for missing credentials')
def test_good_authentication_credentials(self):
"""
Test that the tab can be created and Test authentication clicked for good application id and secret
"""
# GIVEN: A PlanningCenterTab Class
# WHEN: tab is drawn and application_id/secret values are entered in the GUI and the test buttin is clicked
with patch('openlp.plugins.planningcenter.lib.planningcentertab.PlanningCenterAPI.check_credentials') \
as mock_check_credentials, \
patch('openlp.plugins.planningcenter.lib.planningcentertab.QtWidgets.QMessageBox.information') \
as mock_qmessagebox:
mock_check_credentials.return_value = 'good'
QtTest.QTest.mouseClick(self.tab.test_credentials_button, QtCore.Qt.LeftButton)
# THEN:
# The information messagebox should be called
self.assertEqual(mock_qmessagebox.call_count, 1, 'Information dialog used for good credentials')
def test_save_credentials(self):
"""
Test that credentials are saved in settings when the save function is called
"""
# GIVEN: A PlanningCenterTab Class
# WHEN: application id and secret values are set to something and the save function is called
new_application_id = 'planningcenter'
new_secret = 'woohoo'
self.tab.application_id_line_edit.setText(new_application_id)
self.tab.secret_line_edit.setText(new_secret)
self.tab.save()
# THEN: The settings version of application_id and secret should reflect the new values
settings = Settings()
settings.beginGroup(self.tab.settings_section)
application_id = settings.value('application_id')
secret = settings.value('secret')
self.assertEqual(application_id, new_application_id)
self.assertEqual(secret, new_secret)
settings.endGroup()

View File

@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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 <https://www.gnu.org/licenses/>. #
##########################################################################
"""
Package to test the openlp.plugins.planningcenter.lib.songimport package.
"""
import datetime
from unittest import TestCase
from unittest.mock import patch
from openlp.core.common.registry import Registry
from openlp.plugins.planningcenter.lib.songimport import PlanningCenterSongImport
from tests.helpers.testmixin import TestMixin
class TestSongImport(TestCase, TestMixin):
"""
Test the PlanningcenterPlugin class
"""
def setUp(self):
"""
Create the class
"""
self.registry = Registry()
Registry.create()
with patch('openlp.core.common.registry.Registry.get'):
self.song_import = PlanningCenterSongImport()
def tearDown(self):
"""
Delete all the C++ objects at the end so that we don't have a segfault
"""
del self.registry
del self.song_import
def test_add_song_without_lyrics(self):
"""
Test that a song can be added with None lyrics
"""
# GIVEN: A PlanningCenterSongImport Class
# WHEN: A song is added without lyrics
item_title = 'Title'
author = 'Author'
lyrics = None
theme_name = 'Theme Name'
last_modified = datetime.datetime.now()
with patch('openlp.plugins.songs.lib.importers.songimport.Song') as mock_song, \
patch('openlp.plugins.songs.lib.importers.songimport.Author'):
self.song_import.add_song(item_title, author, lyrics, theme_name, last_modified)
# THEN: A mock song has valid title, lyrics, and theme_name values
self.assertEqual(mock_song.return_value.title, item_title, "Mock Song Title matches input title")
self.assertTrue(item_title in mock_song.return_value.lyrics, "Mock Song Lyrics contain input title")
self.assertEqual(mock_song.return_value.theme_name, theme_name, "Mock Song Theme matches input theme")
def test_add_song_with_lyrics(self):
"""
Test that a song can be added with lyrics
"""
# GIVEN: A PlanningCenterSongImport Class
# WHEN: A song is added without lyrics
item_title = 'Title'
author = 'Author'
lyrics = 'This is my song!'
theme_name = 'Theme Name'
last_modified = datetime.datetime.now()
with patch('openlp.plugins.songs.lib.importers.songimport.Song') as mock_song, \
patch('openlp.plugins.songs.lib.importers.songimport.Author'):
self.song_import.add_song(item_title, author, lyrics, theme_name, last_modified)
# THEN: A mock song has valid title, lyrics, and theme_name values
self.assertEqual(mock_song.return_value.title, item_title, "Mock Song Title matches input title")
self.assertTrue(lyrics in mock_song.return_value.lyrics, "Mock Song Lyrics contain input lyrics")
self.assertEqual(mock_song.return_value.theme_name, theme_name, "Mock Song Theme matches input theme")
def test_add_song_with_verse(self):
"""
Test that a song can be added with lyrics that contain a verse header
"""
# GIVEN: A PlanningCenterSongImport Class
# WHEN: A song is added with lyrics that contain a verse tag
item_title = 'Title'
author = 'Author'
lyrics = 'V1\nThis is my song!'
theme_name = 'Theme Name'
last_modified = datetime.datetime.now()
with patch('openlp.plugins.songs.lib.importers.songimport.Song') as mock_song, \
patch('openlp.plugins.songs.lib.importers.songimport.Author'):
self.song_import.add_song(item_title, author, lyrics, theme_name, last_modified)
# THEN: A mock song has valid title, lyrics, and theme_name values
self.assertEqual(mock_song.return_value.title, item_title,
"Mock Song Title matches input title")
self.assertTrue("This is my song!" in mock_song.return_value.lyrics,
"Mock Song Lyrics contain input lyrics")
self.assertTrue("type=\"v\"" in mock_song.return_value.lyrics,
"Mock Song Lyrics contain input verse")
self.assertEqual(mock_song.return_value.theme_name, theme_name,
"Mock Song Theme matches input theme")
def test_parse_lyrics_with_end_marker(self):
"""
Test that a lyrics after an END marker are skipped
"""
# GIVEN: A PlanningCenterSongImport Class
# WHEN: _split_lyrics_into_verses is called with lyrics that contain an END marker
lyrics = 'This is my song!\n\n{<b>END</b>}\n\nSkip this part of the song'
output_verses = self.song_import._split_lyrics_into_verses(lyrics)
# THEN: A mock song has valid title, lyrics, and theme_name values
self.assertEqual(len(output_verses), 1, "A single verse is returned")
self.assertTrue("This is my song!" in output_verses[0]['verse_text'], "Output lyrics contain input lyrics")
self.assertFalse("END" in output_verses[0]['verse_text'], "Output Lyrics do not contain END")
self.assertFalse("Skip this part of the song" in output_verses[0]['verse_text'], "Output Lyrics stop at end")
def test_parse_lyrics_with_multiple_verses(self):
"""
Test lyrics with verse marker inside
"""
# GIVEN: A PlanningCenterSongImport Class
# WHEN: _split_lyrics_into_verses is called with lyrics that contain a verse marker inside (chorus)
lyrics = 'First Verse\n\nChorus\nThis is my Chorus\n'
output_verses = self.song_import._split_lyrics_into_verses(lyrics)
# THEN: A mock song has valid title, lyrics, and theme_name values
self.assertEqual(len(output_verses), 2, "Two output verses are returned")
self.assertEqual(output_verses[1]['verse_type'], 'c', "Second verse is a chorus")
def test_parse_lyrics_with_single_spaced_verse_tags(self):
"""
Test lyrics with verse marker inside
"""
# GIVEN: A PlanningCenterSongImport Class
# WHEN: _split_lyrics_into_verses is called with lyrics that contain a verse marker inside (chorus)
lyrics = 'V1\nFirst Verse\nV2\nSecondVerse\n'
output_verses = self.song_import._split_lyrics_into_verses(lyrics)
# THEN: A mock song has valid title, lyrics, and theme_name values
self.assertEqual(len(output_verses), 2, "Two output verses are returned")

View File

@ -0,0 +1,172 @@
# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 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 <https://www.gnu.org/licenses/>. #
##########################################################################
"""
Package to test the openlp.plugins.planningcenter.planningcenterplugin package.
"""
from unittest import TestCase
from unittest.mock import patch, MagicMock
from PyQt5 import QtWidgets
from openlp.core.common.registry import Registry
from openlp.core.common.settings import Settings
from openlp.core.state import State
from openlp.core.ui.icons import UiIcons
from openlp.core.ui.settingsform import SettingsForm
from openlp.plugins.planningcenter.planningcenterplugin import PlanningCenterPlugin
from tests.helpers.testmixin import TestMixin
class TestPlanningCenterPlugin(TestCase, TestMixin):
"""
Test the PlanningcenterPlugin class
"""
def setUp(self):
"""
Create the UI
"""
self.setup_application()
self.registry = Registry()
Registry.create()
State().load_settings()
self.plugin = PlanningCenterPlugin()
self.settings_form = SettingsForm()
def tearDown(self):
"""
Delete all the C++ objects at the end so that we don't have a segfault
"""
del self.registry
del self.plugin
del self.settings_form
def test_class_init_defaults(self):
"""
Test that the plugin class is instantiated with the correct defaults
"""
# GIVEN: A PlanningcenterPlugin Class
# WHEN: the class has been through __init__
# THEN:
# planningcenter form is set to None
self.assertEqual(self.plugin.planningcenter_form, None, "Init plugin set to None")
# icon is set correctly
self.assertEqual(self.plugin.icon, UiIcons().planning_center, "Init icon set to planning_center icon")
# weight is -1
self.assertEqual(self.plugin.weight, -1, "Init weight set to -1")
# the planning_center module is registered active
self.assertEqual(State().is_module_active('planning_center'), True, "Init State() is active")
def test_initialise(self):
"""
Test that the initialise function can be called and it passes a call along
to its parent class
"""
# GIVEN: A PlanningcenterPlugin Class
# WHEN: initialise has been called on the class
with patch('openlp.plugins.planningcenter.planningcenterplugin.PlanningCenterPlugin.import_planning_center',
create=True):
return_value = self.plugin.initialise()
# THEN:
# the function returns and does not fail... it doesn't do much at this point, so this
# is mainly to improve test coverage
self.assertEqual(return_value, None, "Initialise was called on the class and it didn't crash")
def test_import_menu_item_added(self):
"""
Test that the add_import_menu_item function adds the menu item
"""
# GIVEN: A PlanningcenterPlugin Class
# WHEN: add_import_menu_item is called
import_menu = QtWidgets.QMenu()
self.plugin.add_import_menu_item(import_menu)
self.plugin.import_planning_center.setVisible(True)
# THEN:
# the menu should not be empty
self.assertEqual(import_menu.isEmpty(), False, "Menu Item is populated")
@patch('openlp.plugins.planningcenter.forms.selectplanform.SelectPlanForm.exec')
@patch('openlp.core.ui.settingsform.SettingsForm.exec')
def test_on_import_planning_center_triggered_with_auth_settings(self, mock_editauth_exec, mock_selectplan_exec):
"""
Test that the on_import_planning_center_triggered function correctly returns
the correct form to display.
"""
# GIVEN: A PlanningCenterPlugin Class with mocked exec calls on both
# PlanningCenter forms and settings set
application_id = 'abc'
secret = '123'
Settings().setValue('planningcenter/application_id', application_id)
Settings().setValue('planningcenter/secret', secret)
# init the planning center plugin so we have default values defined for Settings()
# WHEN: on_import_planning_center_triggered is called
self.plugin.on_import_planning_center_triggered()
# THEN:
self.assertEqual(mock_selectplan_exec.call_count, 1, "Select Plan Form was shown")
self.assertEqual(mock_editauth_exec.call_count, 0, "Edit Auth Form was not shown")
@patch('openlp.plugins.planningcenter.forms.selectplanform.SelectPlanForm.exec')
@patch('openlp.core.ui.settingsform.SettingsForm.exec')
def test_on_import_planning_center_triggered_without_auth_settings(self, mock_editauth_exec, mock_selectplan_exec):
"""
Test that the on_import_planning_center_triggered function correctly returns
the correct form to display.
"""
# GIVEN: A PlanningCenterPlugin Class with mocked exec calls on both
# PlanningCenter forms and settings set
application_id = ''
secret = ''
Settings().setValue('planningcenter/application_id', application_id)
Settings().setValue('planningcenter/secret', secret)
# init the planning center plugin so we have default values defined for Settings()
# WHEN: on_import_planning_center_triggered is called
self.plugin.on_import_planning_center_triggered()
# THEN:
self.assertEqual(mock_selectplan_exec.call_count, 0, "Select Plan Form was not shown")
self.assertEqual(mock_editauth_exec.call_count, 1, "Edit Auth Form was shown")
def test_about(self):
"""
Test that the about function returns text.
"""
# GIVEN: A PlanningCenterPlugin Class
# WHEN: about() is called
return_value = self.plugin.about()
# THEN:
self.assertGreater(len(return_value), 0, "About function returned some text")
def test_finalise(self):
"""
Test finalise function
"""
# GIVEN: A PlanningCenterPlugin Class
# WHEN: finalise() is called
with patch('openlp.core.common.registry.Registry.get') as mock_get, \
patch('openlp.plugins.planningcenter.planningcenterplugin.Song'), \
patch('openlp.plugins.planningcenter.planningcenterplugin.CustomSlide'), \
patch('openlp.plugins.planningcenter.planningcenterplugin.PlanningCenterPlugin.import_planning_center',
create=True):
mock_get.return_value.plugin.manager.get_all_objects.return_value = [MagicMock()]
mock_get.return_value.plugin.db_manager.get_all_objects.return_value = [MagicMock()]
self.plugin.finalise()
self.assertEqual(mock_get.return_value.plugin.manager.get_all_objects.call_count, 1, 'Get All Object Counts')
self.assertEqual(mock_get.return_value.plugin.manager.delete_object.call_count, 1, 'Get All Object Counts')
self.assertEqual(mock_get.return_value.plugin.db_manager.get_all_objects.call_count, 1, 'Get All Object Counts')
self.assertEqual(mock_get.return_value.plugin.db_manager.delete_object.call_count, 1, 'Delete Object Counts')

View File

@ -0,0 +1 @@
{"data": {"type": "Organization", "id": "123", "attributes": {"ccli": "123", "ccli_connected": true, "ccli_reporting_enabled": true, "created_at": "2019-01-01T11:11:11Z", "date_format": "US", "file_storage_exceeded": false, "legacy_id": "123", "music_stand_enabled": true, "name": "Church Name", "owner_name": "This Person", "people_allowed": 150, "people_remaining": 1, "projector_enabled": false, "secret": "123", "time_zone": "America/Chicago", "twenty_four_hour_time": true, "updated_at": "2019-10-30T20:35:07Z"}, "links": {"attachment_types": "https://api.planningcenteronline.com/services/v2/attachment_types", "email_templates": "https://api.planningcenteronline.com/services/v2/email_templates", "folders": "https://api.planningcenteronline.com/services/v2/folders", "media": "https://api.planningcenteronline.com/services/v2/media", "people": "https://api.planningcenteronline.com/services/v2/people", "plans": null, "series": "https://api.planningcenteronline.com/services/v2/series", "service_types": "https://api.planningcenteronline.com/services/v2/service_types", "songs": "https://api.planningcenteronline.com/services/v2/songs", "tag_groups": "https://api.planningcenteronline.com/services/v2/tag_groups", "teams": "https://api.planningcenteronline.com/services/v2/teams", "self": "https://api.planningcenteronline.com/services/v2"}}, "included": [], "meta": {}}

View File

@ -0,0 +1 @@
{"links": {"self": "https://api.planningcenteronline.com/services/v2/service_types"}, "data": [{"type": "ServiceType", "id": "122061", "attributes": {"archived_at": null, "attachment_types_enabled": true, "background_check_permissions": "default", "comment_permissions": "Scheduled Viewer", "created_at": "2011-08-04T01:31:57Z", "deleted_at": null, "frequency": "Weekly", "last_plan_from": "position", "name": "gbf", "permissions": "Editor", "sequence": 0, "updated_at": "2019-08-03T23:31:21Z"}, "relationships": {"parent": {"data": null}}, "links": {"self": "https://api.planningcenteronline.com/services/v2/service_types/122061"}}, {"type": "ServiceType", "id": "561347", "attributes": {"archived_at": null, "attachment_types_enabled": false, "background_check_permissions": "default", "comment_permissions": "Viewer", "created_at": "2016-02-04T20:18:28Z", "deleted_at": null, "frequency": "Weekly", "last_plan_from": "position", "name": "OpenLP Testing", "permissions": "Editor", "sequence": 0, "updated_at": "2019-02-22T17:44:13Z"}, "relationships": {"parent": {"data": null}}, "links": {"self": "https://api.planningcenteronline.com/services/v2/service_types/561347"}}], "included": [], "meta": {"total_count": 2, "count": 2, "can_order_by": ["sequence", "name"], "can_query_by": ["id", "parent_id"], "can_include": ["time_preference_options"], "parent": {"id": "59443", "type": "Organization"}}}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"links": {"self": "https://api.planningcenteronline.com/services/v2/service_types/561347/plans?filter=future&order=sort_date&per_page=10"}, "data": [], "included": [], "meta": {"total_count": 0, "count": 0, "can_order_by": ["title", "created_at", "updated_at", "sort_date"], "can_query_by": ["created_at", "updated_at", "title"], "can_include": ["contributors", "my_schedules", "plan_times", "series"], "can_filter": ["future", "past", "after", "before", "no_dates"], "parent": {"id": "561347", "type": "ServiceType"}}}

File diff suppressed because one or more lines are too long