From 1b7469aad3ce605373be98f2cb87038d6d1a3a40 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Mon, 19 Jan 2015 21:30:41 +0000 Subject: [PATCH 001/110] partial fix bug #1000729 'Support more song fields in the search' Fixes: https://launchpad.net/bugs/1000729 --- openlp/plugins/songs/lib/mediaitem.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 35285f24e..692cd3162 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -44,7 +44,7 @@ from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm from openlp.plugins.songs.forms.songimportform import SongImportForm from openlp.plugins.songs.forms.songexportform import SongExportForm from openlp.plugins.songs.lib import VerseType, clean_string, delete_song -from openlp.plugins.songs.lib.db import Author, AuthorType, Song, Book, MediaFile +from openlp.plugins.songs.lib.db import Author, AuthorType, Song, Book, MediaFile, Topic from openlp.plugins.songs.lib.ui import SongStrings from openlp.plugins.songs.lib.openlyricsxml import OpenLyrics, SongXML @@ -60,7 +60,8 @@ class SongSearch(object): Lyrics = 3 Authors = 4 Books = 5 - Themes = 6 + Topics = 6 + Themes = 7 class SongMediaItem(MediaManagerItem): @@ -156,6 +157,8 @@ class SongMediaItem(MediaManagerItem): translate('SongsPlugin.MediaItem', 'Search Authors...')), (SongSearch.Books, ':/songs/song_book_edit.png', SongStrings.SongBooks, translate('SongsPlugin.MediaItem', 'Search Song Books...')), + (SongSearch.Topics, ':/songs/topic_add.png', SongStrings.Topics, + translate('SongsPlugin.MediaItem', 'Search Topics...')), (SongSearch.Themes, ':/slides/slide_theme.png', UiStrings().Themes, UiStrings().SearchThemes) ]) self.search_text_edit.set_current_search_type(Settings().value('%s/last search type' % self.settings_section)) @@ -199,6 +202,12 @@ class SongMediaItem(MediaManagerItem): Book.name.like(search_string), Book.name.asc()) song_number = re.sub(r'[^0-9]', '', search_keywords[2]) self.display_results_book(search_results, song_number) + elif search_type == SongSearch.Topics: + log.debug('Topics Search') + search_string = '%' + search_keywords + '%' + search_results = self.plugin.manager.get_all_objects( + Topic, Topic.name.like(search_string), Topic.name.asc()) + self.display_results_topic(search_results) elif search_type == SongSearch.Themes: log.debug('Theme Search') search_string = '%' + search_keywords + '%' @@ -274,6 +283,19 @@ class SongMediaItem(MediaManagerItem): song_name.setData(QtCore.Qt.UserRole, song.id) self.list_view.addItem(song_name) + def display_results_topic(self, search_results): + log.debug('display results Topic') + self.list_view.clear() + for topic in search_results: + for song in topic.songs: + # Do not display temporary songs + if song.temporary: + continue + song_detail = '%s (%s)' % (topic.name, song.title) + song_name = QtGui.QListWidgetItem(song_detail) + song_name.setData(QtCore.Qt.UserRole, song.id) + self.list_view.addItem(song_name) + def on_clear_text_button_click(self): """ Clear the search text. From b9d3f43aa16e7946021d39332834f7b66f209791 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Tue, 20 Jan 2015 14:08:58 +0000 Subject: [PATCH 002/110] partial fix bug #1000729 'Support more song fields in the search' - update icon Fixes: https://launchpad.net/bugs/1000729 --- openlp/plugins/songs/lib/mediaitem.py | 2 +- resources/images/openlp-2.qrc | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 4e0275dc7..89092aa92 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -150,7 +150,7 @@ class SongMediaItem(MediaManagerItem): translate('SongsPlugin.MediaItem', 'Search Authors...')), (SongSearch.Books, ':/songs/song_book_edit.png', SongStrings.SongBooks, translate('SongsPlugin.MediaItem', 'Search Song Books...')), - (SongSearch.Topics, ':/songs/topic_add.png', SongStrings.Topics, + (SongSearch.Topics, ':/songs/song_search_topic.png', SongStrings.Topics, translate('SongsPlugin.MediaItem', 'Search Topics...')), (SongSearch.Themes, ':/slides/slide_theme.png', UiStrings().Themes, UiStrings().SearchThemes) ]) diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index ba0f10e96..77a878fc7 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -4,6 +4,7 @@ song_search_author.png song_search_lyrics.png song_search_title.png + song_search_topic.png topic_edit.png author_add.png author_delete.png From 97603b20a32c2f081f07e53a635a2c3423b5ac6c Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Tue, 20 Jan 2015 14:09:54 +0000 Subject: [PATCH 003/110] partial fix bug #1000729 'Support more song fields in the search' - add icon Fixes: https://launchpad.net/bugs/1000729 --- resources/images/song_search_topic.png | Bin 0 -> 724 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/images/song_search_topic.png diff --git a/resources/images/song_search_topic.png b/resources/images/song_search_topic.png new file mode 100644 index 0000000000000000000000000000000000000000..eb1f4c598d2cdc683ec659c397dc353d573e36b7 GIT binary patch literal 724 zcmV;_0xSKAP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vH0TL1tzTLGDa0>%IU02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E;VI^GGzb&0!2wgK~y+TW55QCjg7g2a(u$mJHlt@^hd2r zXbf&~3bE0GO5+Fi{`Ly#Q^U{IZAWsX+JqJ`NE?~@B zveWaxjJrwyrr%Ef-+s&Qf6fu7|GE1d|9BSIIzbiT2f>w2wo`7!|Cw_m>wnkv;QvKO zT>hmjao7cv!kq?CfjvvZVZZB>Z^r)Xyc+Po@`UTZuy)%Ns3cK<(aFbpLrrJizqs7s z|1*|O{3oX&XABaDND~H(b~aWEW=@~&z55R1ym$!WPbJ3a98^ZRRLWYh!{lK^5NAm#>QRv^YOj17nd zfLI2IHGtSeO-ac)G&DF2$TkOJeIQl=Vvs>RKn%2yfdK%8MCE0XmVBN70000 Date: Mon, 26 Jan 2015 12:53:23 +0000 Subject: [PATCH 004/110] partial fix bug #1000729 'Support more song fields in the search' - add copyright, CCLI number search, fix topic search, theme list Fixes: https://launchpad.net/bugs/1000729 --- openlp/plugins/songs/lib/mediaitem.py | 71 ++++++++++++++++++++----- resources/images/openlp-2.qrc | 2 + resources/images/song_search_ccli.png | Bin 0 -> 403 bytes resources/images/song_search_copy.png | Bin 0 -> 498 bytes resources/images/song_search_topic.png | Bin 724 -> 993 bytes 5 files changed, 60 insertions(+), 13 deletions(-) create mode 100644 resources/images/song_search_ccli.png create mode 100644 resources/images/song_search_copy.png diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 89092aa92..d2cbad3c1 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -26,7 +26,7 @@ import os import shutil from PyQt4 import QtCore, QtGui -from sqlalchemy.sql import or_ +from sqlalchemy.sql import and_, or_ from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, UiStrings, translate from openlp.core.lib import MediaManagerItem, ItemCapabilities, PluginStatus, ServiceItemContext, \ @@ -52,9 +52,11 @@ class SongSearch(object): Titles = 2 Lyrics = 3 Authors = 4 - Books = 5 - Topics = 6 + Topics = 5 + Books = 6 Themes = 7 + Copyright = 8 + CCLInumber = 9 class SongMediaItem(MediaManagerItem): @@ -148,11 +150,17 @@ class SongMediaItem(MediaManagerItem): translate('SongsPlugin.MediaItem', 'Search Lyrics...')), (SongSearch.Authors, ':/songs/song_search_author.png', SongStrings.Authors, translate('SongsPlugin.MediaItem', 'Search Authors...')), - (SongSearch.Books, ':/songs/song_book_edit.png', SongStrings.SongBooks, - translate('SongsPlugin.MediaItem', 'Search Song Books...')), (SongSearch.Topics, ':/songs/song_search_topic.png', SongStrings.Topics, translate('SongsPlugin.MediaItem', 'Search Topics...')), - (SongSearch.Themes, ':/slides/slide_theme.png', UiStrings().Themes, UiStrings().SearchThemes) + (SongSearch.Books, ':/songs/song_book_edit.png', SongStrings.SongBooks, + translate('SongsPlugin.MediaItem', 'Search Song Books...')), + (SongSearch.Themes, ':/slides/slide_theme.png', UiStrings().Themes, UiStrings().SearchThemes), + (SongSearch.Copyright, ':/songs/song_search_copy.png', + translate('SongsPlugin.MediaItem', 'Copyright'), + translate('SongsPlugin.MediaItem', 'Search Copyright...')), + (SongSearch.CCLInumber, ':/songs/song_search_ccli.png', + translate('SongsPlugin.MediaItem', 'CCLI number'), + translate('SongsPlugin.MediaItem', 'Search CCLI number...')) ]) self.search_text_edit.set_current_search_type(Settings().value('%s/last search type' % self.settings_section)) self.config_update() @@ -183,6 +191,12 @@ class SongMediaItem(MediaManagerItem): search_results = self.plugin.manager.get_all_objects( Author, Author.display_name.like(search_string), Author.display_name.asc()) self.display_results_author(search_results) + elif search_type == SongSearch.Topics: + log.debug('Topics Search') + search_string = '%' + search_keywords + '%' + search_results = self.plugin.manager.get_all_objects( + Topic, Topic.name.like(search_string), Topic.name.asc()) + self.display_results_topic(search_results) elif search_type == SongSearch.Books: log.debug('Books Search') search_string = '%' + search_keywords + '%' @@ -195,17 +209,24 @@ class SongMediaItem(MediaManagerItem): Book.name.like(search_string), Book.name.asc()) song_number = re.sub(r'[^0-9]', '', search_keywords[2]) self.display_results_book(search_results, song_number) - elif search_type == SongSearch.Topics: - log.debug('Topics Search') - search_string = '%' + search_keywords + '%' - search_results = self.plugin.manager.get_all_objects( - Topic, Topic.name.like(search_string), Topic.name.asc()) - self.display_results_topic(search_results) elif search_type == SongSearch.Themes: log.debug('Theme Search') search_string = '%' + search_keywords + '%' - search_results = self.plugin.manager.get_all_objects(Song, Song.theme_name.like(search_string)) + search_results = self.plugin.manager.get_all_objects( + Song, Song.theme_name.like(search_string), Song.theme_name.asc()) + self.display_results_themes(search_results) + elif search_type == SongSearch.Copyright: + log.debug('Copyright Search') + search_string = '%' + search_keywords + '%' + search_results = self.plugin.manager.get_all_objects( + Song, and_(Song.copyright.like(search_string), Song.copyright != '')) self.display_results_song(search_results) + elif search_type == SongSearch.CCLInumber: + log.debug('CCLI number Search') + search_string = '%' + search_keywords + '%' + search_results = self.plugin.manager.get_all_objects( + Song, and_(Song.ccli_number.like(search_string), Song.ccli_number != ''), Song.ccli_number.asc()) + self.display_results_cclinumber(search_results) self.check_search_result() def search_entire(self, search_keywords): @@ -289,6 +310,30 @@ class SongMediaItem(MediaManagerItem): song_name.setData(QtCore.Qt.UserRole, song.id) self.list_view.addItem(song_name) + def display_results_themes(self, search_results): + log.debug('display results Themes') + self.list_view.clear() + for song in search_results: + # Do not display temporary songs + if song.temporary: + continue + song_detail = '%s (%s)' % (song.theme_name, song.title) + song_name = QtGui.QListWidgetItem(song_detail) + song_name.setData(QtCore.Qt.UserRole, song.id) + self.list_view.addItem(song_name) + + def display_results_cclinumber(self, search_results): + log.debug('display results CCLI number') + self.list_view.clear() + for song in search_results: + # Do not display temporary songs + if song.temporary: + continue + song_detail = '%s (%s)' % (song.ccli_number, song.title) + song_name = QtGui.QListWidgetItem(song_detail) + song_name.setData(QtCore.Qt.UserRole, song.id) + self.list_view.addItem(song_name) + def on_clear_text_button_click(self): """ Clear the search text. diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index 77a878fc7..30ac036d3 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -2,6 +2,8 @@ song_search_all.png song_search_author.png + song_search_ccli.png + song_search_copy.png song_search_lyrics.png song_search_title.png song_search_topic.png diff --git a/resources/images/song_search_ccli.png b/resources/images/song_search_ccli.png new file mode 100644 index 0000000000000000000000000000000000000000..48be487f9f8b4c704286e93ea27cb4565b00917c GIT binary patch literal 403 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJbFq_W2nPqp?T7vkfLzW3kH}&M z2FBeW%xLxI@gtz1WQl7;NpOBzNqJ&XDnogBxn5>oc5!lIL8@MUQTpt6Hc~)EjR8I( zuK)l42QneRpibFqy;r~C=+UER&z`+}`SRVncb`9h z`S$JGcOdxr{l~8#KY#xG^XJdMfB%5CYwLE*16t2g666=mAY*4A$isCG$S?79aSW-r z)q4K4&_M?YmxuPY@5+P7a@RQ*dL$|@~-@k<0=2{1A*Is1K ja4mIWc!Q71m*RJ<@85~B#;ZvRgM!1;)z4*}Q$iB}T#dk8 literal 0 HcmV?d00001 diff --git a/resources/images/song_search_copy.png b/resources/images/song_search_copy.png new file mode 100644 index 0000000000000000000000000000000000000000..c4ac7d8068032435b062f33016f047fd111de90f GIT binary patch literal 498 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucL5ULAh?3y^w370~qEv>0#LT=By}Z;C1rt33 zJ=4@yqg0@w$(}BbAr}5iCkuu+2TB~*=V5DhG~j4*lwfIAkYEF1o_(a;P>iVEj{Oec+Rc+jYq6saGm0v6P*7<`YCUS!S-t>g|vFUKh1Q! zy^C9D%J(=doX8N0k6@<@Y69#PB$3#F6v3!%aE(_pYda~N7kxFi8KE>zdmL9 mqWn^8?bPgW*8hHDbqox#TTcmXloJI;34^DrpUXO@geCy#jLX>o literal 0 HcmV?d00001 diff --git a/resources/images/song_search_topic.png b/resources/images/song_search_topic.png index eb1f4c598d2cdc683ec659c397dc353d573e36b7..e506ff408ee65e4d27ab5688e6ea0ecfb72c6b7f 100644 GIT binary patch delta 971 zcmV;+12p{91>pyfB!3BTNLh0L05@C!05@C#%g3a-00006VoOIv0RI600RN!9r;`8x z15-&vK~xwSUBO>Wm1P_U@bB;Uyyt!4z&VFF9YzOGG9^VW+F7As;S{H^+;n5++G<6k zmfE^W*IIwJZi>qG3KQQ2@alD{eb1UFs_QDYIv!^NKe{~m+uWzy_qB}ud}fk9Idn|= zAMMAXzGpykQ?ToeZiomi%#_52w5sLt+RwA~6&saj#ZnBhLcxLwrZ4-i?|rQ>u!s_# zq>r&_a}#Lmu77QqzUbq^@52D@Q9XwrK2f_SwZ%~h4F)S1kcg;+;-z^*ljr7lqg=jm zJjFURmfBu=1mp>uP%LZJk!j^KUb!;N4m&OrF$oJ13ABZE7O%u#HM}A{4A2L(PMoM> z37iy&1>n0YibkUt3Y20>hysDgh=sMnN#!DRY{N&ilYarvw&qX4@roW877_y-Cax!4 zfm2E3I*vF>5rLq^Kw?M*mtmfxzYfaR7683RT|%8bo$#k(T>OENgp=$z>W@^^bBkhS ziHRYQDAE)JVkjatH>RIfS$&ik9L2^`Gy1S;@K;Ev*l4kfSGc!vi+d!MOj)tQfi2?J z?a=CoSbtBM;m@8bZU&-g-GgbQS301=_BNzU8e`wjViBWQ#;>02KE7&ozEfg0bMiM| zpYdUQqM5UCcV#UGEx_8=AS`R&@fa5VNPlAL&sofbqX4ShH&1tr)zqig2XPSJ zxG|fU8pXa$BYh}VI9jr{rgsPzs$K?RSqmS)L;EN2+`(-aIdPRe&$UCx$G&V@e}7LY zjPwJIYn_4q_cz8m9=SJ{Wk)$y&sXE8UrvMYKL=iTQRQ+ub8htfk@gK6dh>NF&3$!w z(|^6Qs|CQNa<7bnuq3#HaYsI1i|6~Eg;g527H$H{`b%*UPC3)H$*UHC#V1nK4Gy)Z|ta3WYB+nX1*}V8O|1tnH07Ur*JMN5r6ib%c0000YdQ@0+Q*UN;cVTj6 t004N}D=#nC%goCzPEIUH)ypqR2LLwM23QbN%3J^d002ovPDHLkV1kpl#Nq$| delta 700 zcmV;t0z>`b2h;_SB!2;OQb$4nuFf3k00004XF*Lt006O%3;baP00009a7bBm001{z z001{z0hxmW#sB~S7<5HgbW?9;ba!ELWdLwtX>N2bZe?^JG%heMHD!e|WdHyIMM*?K zR5(v#zy^$sjk$tye8ST^!e{67N3Ba}3~q4>vC)D`;|KQs_J0cLQ^U{IZAa0R^X@g>0#%L)EMzP=Lzh_G zOxR=fFZGb^|Fpw)V4Sem>VMcK%m0xZt^PR`n?yjBV}A#MP1bRx=Uo0ZT=xH8f7$PU z*#+1CX$OIZ?6CV6v)Jm4yu3XJR4FcC%v!S3^T3R|N&lwbPX6D1%kO{A5vTvT`yBsx z7T7vL72yZLl}@%(ZpHtZb0h12*Y)84MMqrzr7UsS1(d>_22g=LOT%Hm>yvNB{_DIN z@W1ke>wmwncH0!FBvF9T$;Wy_O=sS}xZL3XGnP*LC#NB23=)S(69$ZSHdYH}PM`E| z;)K3`t5>i32NaV9iGyW{0E|{v<};>F>iaiq=8S*Sr%xx!08jw212G>Ei^|E#X}LH% zJ@E7M`)g!m)C3fh0Ae8^<_2O`AjU9^4TuGRSTzQSHGtSeO-ac)G&DF2$TkOJeIQl= iVvs>RKn%2yfdK%8MCE0XmVBN70000 Date: Mon, 26 Jan 2015 13:34:20 +0000 Subject: [PATCH 005/110] partial fix bug #1000729 'Support more song fields in the search' - fixed sort for topic search - by song Fixes: https://launchpad.net/bugs/1000729 --- openlp/plugins/songs/lib/mediaitem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index d2cbad3c1..d83034402 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -301,7 +301,8 @@ class SongMediaItem(MediaManagerItem): log.debug('display results Topic') self.list_view.clear() for topic in search_results: - for song in topic.songs: + songs = sorted(topic.songs, key=lambda song: song.sort_key) + for song in songs: # Do not display temporary songs if song.temporary: continue From c8f200b20fd77b5b4486a1306f1bdd96e2cd9ea4 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Fri, 13 Feb 2015 18:29:42 +0000 Subject: [PATCH 006/110] fixed bug #1280295 'Enable natural sorting for song book searches' --fixes 1280294 --- openlp/plugins/songs/lib/mediaitem.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index c0c58ff90..ae804878e 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -255,7 +255,7 @@ class SongMediaItem(MediaManagerItem): log.debug('display results Book') self.list_view.clear() for book in search_results: - songs = sorted(book.songs, key=lambda song: int(re.match(r'[0-9]+', '0' + song.song_number).group())) + songs = sorted(book.songs, key=lambda song: self._natural_sort_key(song.song_number)) for song in songs: # Do not display temporary songs if song.temporary: @@ -583,6 +583,24 @@ class SongMediaItem(MediaManagerItem): # List must be empty at the end return not author_list + def _try_int(self, s): + """ + Convert string s to an integer if possible. Fail silently and return + the string as-is if it isn't an integer. + :param s: The string to try to convert. + """ + try: + return int(s) + except (TypeError, ValueError): + return s + + def _natural_sort_key(self, s): + """ + Return a tuple by which s is sorted. + :param s: A string value from the list we want to sort. + """ + return list(map(self._try_int, re.findall(r'(\d+|\D+)', s))) + def search(self, string, show_error): """ Search for some songs From 17d756514c9618d02c7a723776906d5e3a801ad8 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Fri, 13 Feb 2015 18:49:31 +0000 Subject: [PATCH 007/110] fixed bug #1000729 'Support more song fields in the search' - corrected CCLI number sorting' Fixes: https://launchpad.net/bugs/1000729 --- openlp/plugins/songs/lib/mediaitem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index d83034402..4d5846480 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -225,7 +225,7 @@ class SongMediaItem(MediaManagerItem): log.debug('CCLI number Search') search_string = '%' + search_keywords + '%' search_results = self.plugin.manager.get_all_objects( - Song, and_(Song.ccli_number.like(search_string), Song.ccli_number != ''), Song.ccli_number.asc()) + Song, and_(Song.ccli_number.like(search_string), Song.ccli_number != '')) self.display_results_cclinumber(search_results) self.check_search_result() @@ -326,6 +326,7 @@ class SongMediaItem(MediaManagerItem): def display_results_cclinumber(self, search_results): log.debug('display results CCLI number') self.list_view.clear() + search_results.sort(key=lambda song: int(song.ccli_number)) for song in search_results: # Do not display temporary songs if song.temporary: From bd9b10bd928da2c2c4404f0899f0ec064ab194b2 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 14 Feb 2015 18:31:28 +0000 Subject: [PATCH 008/110] fixed bug #1000729 'Support more song fields in the search' - switched to natural sort, added doc strings Fixes: https://launchpad.net/bugs/1000729 --- openlp/plugins/songs/lib/mediaitem.py | 58 ++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 4d5846480..13cc13bcf 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -250,6 +250,12 @@ class SongMediaItem(MediaManagerItem): log.debug('on_song_list_load - finished') def display_results_song(self, search_results): + """ + Display the song search results in the media manager list + + :param search_results: A list of db Song objects + :return: None + """ log.debug('display results Song') self.save_auto_select_id() self.list_view.clear() @@ -269,6 +275,12 @@ class SongMediaItem(MediaManagerItem): self.auto_select_id = -1 def display_results_author(self, search_results): + """ + Display the song search results in the media manager list, grouped by author + + :param search_results: A list of db Author objects + :return: None + """ log.debug('display results Author') self.list_view.clear() for author in search_results: @@ -282,6 +294,12 @@ class SongMediaItem(MediaManagerItem): self.list_view.addItem(song_name) def display_results_book(self, search_results, song_number=False): + """ + Display the song search results in the media manager list, grouped by book + + :param search_results: A list of db Book objects + :return: None + """ log.debug('display results Book') self.list_view.clear() for book in search_results: @@ -298,6 +316,12 @@ class SongMediaItem(MediaManagerItem): self.list_view.addItem(song_name) def display_results_topic(self, search_results): + """ + Display the song search results in the media manager list, grouped by topic + + :param search_results: A list of db Topic objects + :return: None + """ log.debug('display results Topic') self.list_view.clear() for topic in search_results: @@ -312,6 +336,12 @@ class SongMediaItem(MediaManagerItem): self.list_view.addItem(song_name) def display_results_themes(self, search_results): + """ + Display the song search results in the media manager list, sorted by theme + + :param search_results: A list of db Song objects + :return: None + """ log.debug('display results Themes') self.list_view.clear() for song in search_results: @@ -324,10 +354,16 @@ class SongMediaItem(MediaManagerItem): self.list_view.addItem(song_name) def display_results_cclinumber(self, search_results): + """ + Display the song search results in the media manager list, sorted by CCLI number + + :param search_results: A list of db Song objects + :return: None + """ log.debug('display results CCLI number') self.list_view.clear() - search_results.sort(key=lambda song: int(song.ccli_number)) - for song in search_results: + songs = sorted(search_results, key=lambda song: self._natural_sort_key(song.ccli_number)) + for song in songs: # Do not display temporary songs if song.temporary: continue @@ -652,6 +688,24 @@ class SongMediaItem(MediaManagerItem): # List must be empty at the end return not author_list + def _try_int(self, s): + """ + Convert string s to an integer if possible. Fail silently and return + the string as-is if it isn't an integer. + :param s: The string to try to convert. + """ + try: + return int(s) + except (TypeError, ValueError): + return s + + def _natural_sort_key(self, s): + """ + Return a tuple by which s is sorted. + :param s: A string value from the list we want to sort. + """ + return list(map(self._try_int, re.findall(r'(\d+|\D+)', s))) + def search(self, string, show_error): """ Search for some songs From ed9146ae06964c6cd9d6dfdd465938cff085acf4 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sun, 14 Jun 2015 21:58:56 +0100 Subject: [PATCH 009/110] fixed bug #1000729 'Support more song fields in the search' - added unit tests Fixes: https://launchpad.net/bugs/1000729 --- .../openlp_plugins/songs/test_mediaitem.py | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index 4e28eed93..099264347 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -48,6 +48,11 @@ class TestMediaItem(TestCase, TestMixin): with patch('openlp.core.lib.mediamanageritem.MediaManagerItem._setup'), \ patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__'): self.media_item = SongMediaItem(None, MagicMock()) + self.media_item.list_view = MagicMock() + self.media_item.list_view.save_auto_select_id = MagicMock() + self.media_item.list_view.clear = MagicMock() + self.media_item.list_view.addItem = MagicMock() + self.media_item.auto_select_id = -1 self.media_item.display_songbook = False self.media_item.display_copyright_symbol = False self.setup_application() @@ -60,6 +65,181 @@ class TestMediaItem(TestCase, TestMixin): """ self.destroy_settings() + def display_results_song_test(self): + """ + Test displaying song search results with basic song + """ + # GIVEN: Search results, plus a mocked QtListWidgetItem + with patch('openlp.core.lib.QtGui.QListWidgetItem') as MockedQListWidgetItem, \ + patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole: + mock_search_results = [] + mock_song = MagicMock() + mock_song.id = 1 + mock_song.title = 'My Song' + mock_song.sort_key = 'My Song' + mock_song.authors = [] + mock_author = MagicMock() + mock_author.display_name = 'My Author' + mock_song.authors.append(mock_author) + mock_song.temporary = False + mock_search_results.append(mock_song) + mock_qlist_widget = MagicMock() + MockedQListWidgetItem.return_value = mock_qlist_widget + + # WHEN: I display song search results + self.media_item.display_results_song(mock_search_results) + + # THEN: The current list view is cleared, the widget is created, and the relevant attributes set + self.media_item.list_view.clear.assert_called_with() + MockedQListWidgetItem.assert_called_with('My Song (My Author)') + mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id) + self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget) + + def display_results_author_test(self): + """ + Test displaying song search results grouped by author with basic song + """ + # GIVEN: Search results grouped by author, plus a mocked QtListWidgetItem + with patch('openlp.core.lib.QtGui.QListWidgetItem') as MockedQListWidgetItem, \ + patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole: + mock_search_results = [] + mock_author = MagicMock() + mock_song = MagicMock() + mock_author.display_name = 'My Author' + mock_author.songs = [] + mock_song.id = 1 + mock_song.title = 'My Song' + mock_song.sort_key = 'My Song' + mock_song.temporary = False + mock_author.songs.append(mock_song) + mock_search_results.append(mock_author) + mock_qlist_widget = MagicMock() + MockedQListWidgetItem.return_value = mock_qlist_widget + + # WHEN: I display song search results grouped by author + self.media_item.display_results_author(mock_search_results) + + # THEN: The current list view is cleared, the widget is created, and the relevant attributes set + self.media_item.list_view.clear.assert_called_with() + MockedQListWidgetItem.assert_called_with('My Author (My Song)') + mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id) + self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget) + + def display_results_book_test(self): + """ + Test displaying song search results grouped by book with basic song + """ + # GIVEN: Search results grouped by book, plus a mocked QtListWidgetItem + with patch('openlp.core.lib.QtGui.QListWidgetItem') as MockedQListWidgetItem, \ + patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole: + mock_search_results = [] + mock_book = MagicMock() + mock_song = MagicMock() + mock_book.name = 'My Book' + mock_book.songs = [] + mock_song.id = 1 + mock_song.title = 'My Song' + mock_song.sort_key = 'My Song' + mock_song.song_number = '123' + mock_song.temporary = False + mock_book.songs.append(mock_song) + mock_search_results.append(mock_book) + mock_qlist_widget = MagicMock() + MockedQListWidgetItem.return_value = mock_qlist_widget + + # WHEN: I display song search results grouped by book + self.media_item.display_results_book(mock_search_results) + + # THEN: The current list view is cleared, the widget is created, and the relevant attributes set + self.media_item.list_view.clear.assert_called_with() + MockedQListWidgetItem.assert_called_with('My Book - 123 (My Song)') + mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id) + self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget) + + def display_results_topic_test(self): + """ + Test displaying song search results grouped by topic with basic song + """ + # GIVEN: Search results grouped by topic, plus a mocked QtListWidgetItem + with patch('openlp.core.lib.QtGui.QListWidgetItem') as MockedQListWidgetItem, \ + patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole: + mock_search_results = [] + mock_topic = MagicMock() + mock_song = MagicMock() + mock_topic.name = 'My Topic' + mock_topic.songs = [] + mock_song.id = 1 + mock_song.title = 'My Song' + mock_song.sort_key = 'My Song' + mock_song.temporary = False + mock_topic.songs.append(mock_song) + mock_search_results.append(mock_topic) + mock_qlist_widget = MagicMock() + MockedQListWidgetItem.return_value = mock_qlist_widget + + # WHEN: I display song search results grouped by topic + self.media_item.display_results_topic(mock_search_results) + + # THEN: The current list view is cleared, the widget is created, and the relevant attributes set + self.media_item.list_view.clear.assert_called_with() + MockedQListWidgetItem.assert_called_with('My Topic (My Song)') + mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id) + self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget) + + def display_results_themes_test(self): + """ + Test displaying song search results sorted by theme with basic song + """ + # GIVEN: Search results sorted by theme, plus a mocked QtListWidgetItem + with patch('openlp.core.lib.QtGui.QListWidgetItem') as MockedQListWidgetItem, \ + patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole: + mock_search_results = [] + mock_song = MagicMock() + mock_song.id = 1 + mock_song.title = 'My Song' + mock_song.sort_key = 'My Song' + mock_song.theme_name = 'My Theme' + mock_song.temporary = False + mock_search_results.append(mock_song) + mock_qlist_widget = MagicMock() + MockedQListWidgetItem.return_value = mock_qlist_widget + + # WHEN: I display song search results sorted by theme + self.media_item.display_results_themes(mock_search_results) + + # THEN: The current list view is cleared, the widget is created, and the relevant attributes set + self.media_item.list_view.clear.assert_called_with() + MockedQListWidgetItem.assert_called_with('My Theme (My Song)') + mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id) + self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget) + + def display_results_cclinumber_test(self): + """ + Test displaying song search results sorted by CCLI number with basic song + """ + # GIVEN: Search results sorted by CCLI number, plus a mocked QtListWidgetItem + with patch('openlp.core.lib.QtGui.QListWidgetItem') as MockedQListWidgetItem, \ + patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole: + mock_search_results = [] + mock_song = MagicMock() + mock_song.id = 1 + mock_song.title = 'My Song' + mock_song.sort_key = 'My Song' + mock_song.ccli_number = '12345' + mock_song.temporary = False + mock_search_results.append(mock_song) + mock_qlist_widget = MagicMock() + MockedQListWidgetItem.return_value = mock_qlist_widget + + # WHEN: I display song search results sorted by CCLI number + self.media_item.display_results_cclinumber(mock_search_results) + + # THEN: The current list view is cleared, the widget is created, and the relevant attributes set + self.media_item.list_view.clear.assert_called_with() + MockedQListWidgetItem.assert_called_with('12345 (My Song)') + mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id) + self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget) + def build_song_footer_one_author_test(self): """ Test build songs footer with basic song and one author @@ -257,3 +437,44 @@ class TestMediaItem(TestCase, TestMixin): # THEN: They should not match self.assertFalse(result, "Authors should not match") + + def try_int_with_string_integer_test(self): + """ + Test the _try_int function with a string containing an integer + """ + # GIVEN: A string that is an integer + string_integer = '123' + + # WHEN: We "convert" it to an integer + integer_result = self.media_item._try_int(string_integer) + + # THEN: We should get back an integer + self.assertIsInstance(integer_result, int, 'The result should be an integer') + self.assertEqual(integer_result, 123, 'The result should be 123') + + def try_int_with_string_noninteger_test(self): + """ + Test the _try_int function with a string not containing an integer + """ + # GIVEN: A string that is not an integer + string_noninteger = 'abc' + + # WHEN: We "convert" it to an integer + noninteger_result = self.media_item._try_int(string_noninteger) + + # THEN: We should get back the original string + self.assertIsInstance(noninteger_result, type(string_noninteger), 'The result type should be the same') + self.assertEqual(noninteger_result, string_noninteger, 'The result value should be the same') + + def natural_sort_key_test(self): + """ + Test the _natural_sort_key function + """ + # GIVEN: A string to be converted into a sort key + string_sort_key = 'A1B12C123' + + # WHEN: We attempt to create a sort key + sort_key_result = self.media_item._natural_sort_key(string_sort_key) + + # THEN: We should get back a tuple split on integers + self.assertEqual(sort_key_result, ['A', 1, 'B', 12, 'C', 123]) \ No newline at end of file From 81908970b341c10ba39d057f16ba3259040befdd Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 20 Jun 2015 23:29:03 +0100 Subject: [PATCH 010/110] added unit tests --- .../openlp_plugins/songs/test_mediaitem.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index 4e28eed93..e3585546f 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -48,6 +48,10 @@ class TestMediaItem(TestCase, TestMixin): with patch('openlp.core.lib.mediamanageritem.MediaManagerItem._setup'), \ patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__'): self.media_item = SongMediaItem(None, MagicMock()) + self.media_item.list_view = MagicMock() + self.media_item.list_view.save_auto_select_id = MagicMock() + self.media_item.list_view.clear = MagicMock() + self.media_item.list_view.addItem = MagicMock() self.media_item.display_songbook = False self.media_item.display_copyright_symbol = False self.setup_application() @@ -60,6 +64,37 @@ class TestMediaItem(TestCase, TestMixin): """ self.destroy_settings() + def display_results_book_test(self): + """ + Test displaying song search results grouped by book with basic song + """ + # GIVEN: Search results grouped by book, plus a mocked QtListWidgetItem + with patch('openlp.core.lib.QtGui.QListWidgetItem') as MockedQListWidgetItem, \ + patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole: + mock_search_results = [] + mock_book = MagicMock() + mock_song = MagicMock() + mock_book.name = 'My Book' + mock_book.songs = [] + mock_song.id = 1 + mock_song.title = 'My Song' + mock_song.sort_key = 'My Song' + mock_song.song_number = '123' + mock_song.temporary = False + mock_book.songs.append(mock_song) + mock_search_results.append(mock_book) + mock_qlist_widget = MagicMock() + MockedQListWidgetItem.return_value = mock_qlist_widget + + # WHEN: I display song search results grouped by book + self.media_item.display_results_book(mock_search_results) + + # THEN: The current list view is cleared, the widget is created, and the relevant attributes set + self.media_item.list_view.clear.assert_called_with() + MockedQListWidgetItem.assert_called_with('My Book - 123 (My Song)') + mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id) + self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget) + def build_song_footer_one_author_test(self): """ Test build songs footer with basic song and one author @@ -257,3 +292,44 @@ class TestMediaItem(TestCase, TestMixin): # THEN: They should not match self.assertFalse(result, "Authors should not match") + + def try_int_with_string_integer_test(self): + """ + Test the _try_int function with a string containing an integer + """ + # GIVEN: A string that is an integer + string_integer = '123' + + # WHEN: We "convert" it to an integer + integer_result = self.media_item._try_int(string_integer) + + # THEN: We should get back an integer + self.assertIsInstance(integer_result, int, 'The result should be an integer') + self.assertEqual(integer_result, 123, 'The result should be 123') + + def try_int_with_string_noninteger_test(self): + """ + Test the _try_int function with a string not containing an integer + """ + # GIVEN: A string that is not an integer + string_noninteger = 'abc' + + # WHEN: We "convert" it to an integer + noninteger_result = self.media_item._try_int(string_noninteger) + + # THEN: We should get back the original string + self.assertIsInstance(noninteger_result, type(string_noninteger), 'The result type should be the same') + self.assertEqual(noninteger_result, string_noninteger, 'The result value should be the same') + + def natural_sort_key_test(self): + """ + Test the _natural_sort_key function + """ + # GIVEN: A string to be converted into a sort key + string_sort_key = 'A1B12C123' + + # WHEN: We attempt to create a sort key + sort_key_result = self.media_item._natural_sort_key(string_sort_key) + + # THEN: We should get back a tuple split on integers + self.assertEqual(sort_key_result, ['A', 1, 'B', 12, 'C', 123]) From c95ca007b57bd274036c1bd89e07a8abc50e57c6 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 20 Jun 2015 23:35:22 +0100 Subject: [PATCH 011/110] updated unit test --- tests/functional/openlp_plugins/songs/test_mediaitem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index 099264347..0fb911dbd 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -48,6 +48,7 @@ class TestMediaItem(TestCase, TestMixin): with patch('openlp.core.lib.mediamanageritem.MediaManagerItem._setup'), \ patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__'): self.media_item = SongMediaItem(None, MagicMock()) + self.media_item.save_auto_select_id = MagicMock() self.media_item.list_view = MagicMock() self.media_item.list_view.save_auto_select_id = MagicMock() self.media_item.list_view.clear = MagicMock() @@ -91,6 +92,7 @@ class TestMediaItem(TestCase, TestMixin): # THEN: The current list view is cleared, the widget is created, and the relevant attributes set self.media_item.list_view.clear.assert_called_with() + self.media_item.save_auto_select_id.assert_called_with() MockedQListWidgetItem.assert_called_with('My Song (My Author)') mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id) self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget) From 960fbb8865d0b0f0e81ff02b27ed998014da7bf2 Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Mon, 18 Jan 2016 03:16:37 +1030 Subject: [PATCH 012/110] Implemented Better Preview for Service Manager Blueprint --- openlp/core/common/settings.py | 1 + openlp/core/ui/advancedtab.py | 7 ++++++ openlp/core/ui/servicemanager.py | 9 ++++++++ .../openlp_core_ui/test_servicemanager.py | 22 +++++++++++++++++-- 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index 68b0763df..7b43f6f39 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -131,6 +131,7 @@ class Settings(QtCore.QSettings): 'advanced/save current plugin': False, 'advanced/slide limits': SlideLimits.End, 'advanced/single click preview': False, + 'advanced/single click service preview': False, 'advanced/x11 bypass wm': X11_BYPASS_DEFAULT, 'advanced/search as type': True, 'crashreport/last directory': '', diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index 4421b432f..fe8c09131 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -77,6 +77,9 @@ class AdvancedTab(SettingsTab): self.single_click_preview_check_box = QtWidgets.QCheckBox(self.ui_group_box) self.single_click_preview_check_box.setObjectName('single_click_preview_check_box') self.ui_layout.addRow(self.single_click_preview_check_box) + self.single_click_service_preview_check_box = QtWidgets.QCheckBox(self.ui_group_box) + self.single_click_service_preview_check_box.setObjectName('single_click_service_preview_check_box') + self.ui_layout.addRow(self.single_click_service_preview_check_box) self.expand_service_item_check_box = QtWidgets.QCheckBox(self.ui_group_box) self.expand_service_item_check_box.setObjectName('expand_service_item_check_box') self.ui_layout.addRow(self.expand_service_item_check_box) @@ -270,6 +273,8 @@ class AdvancedTab(SettingsTab): 'Double-click to send items straight to live')) self.single_click_preview_check_box.setText(translate('OpenLP.AdvancedTab', 'Preview items when clicked in Media Manager')) + self.single_click_service_preview_check_box.setText(translate('OpenLP.AdvancedTab', + 'Preview items when clicked in Service Manager')) self.expand_service_item_check_box.setText(translate('OpenLP.AdvancedTab', 'Expand new service items on creation')) self.enable_auto_close_check_box.setText(translate('OpenLP.AdvancedTab', @@ -339,6 +344,7 @@ class AdvancedTab(SettingsTab): self.media_plugin_check_box.setChecked(settings.value('save current plugin')) self.double_click_live_check_box.setChecked(settings.value('double click live')) self.single_click_preview_check_box.setChecked(settings.value('single click preview')) + self.single_click_service_preview_check_box.setChecked(settings.value('single click service preview')) self.expand_service_item_check_box.setChecked(settings.value('expand service item')) self.enable_auto_close_check_box.setChecked(settings.value('enable exit confirmation')) self.hide_mouse_check_box.setChecked(settings.value('hide mouse')) @@ -420,6 +426,7 @@ class AdvancedTab(SettingsTab): settings.setValue('save current plugin', self.media_plugin_check_box.isChecked()) settings.setValue('double click live', self.double_click_live_check_box.isChecked()) settings.setValue('single click preview', self.single_click_preview_check_box.isChecked()) + settings.setValue('single click service preview', self.single_click_service_preview_check_box.isChecked()) settings.setValue('expand service item', self.expand_service_item_check_box.isChecked()) settings.setValue('enable exit confirmation', self.enable_auto_close_check_box.isChecked()) settings.setValue('hide mouse', self.hide_mouse_check_box.isChecked()) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index e4a1b143a..a90ab1e8b 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -212,6 +212,7 @@ class Ui_ServiceManager(object): # Connect up our signals and slots self.theme_combo_box.activated.connect(self.on_theme_combo_box_selected) self.service_manager_list.doubleClicked.connect(self.on_make_live) + self.service_manager_list.clicked.connect(self.on_single_click_preview) self.service_manager_list.itemCollapsed.connect(self.collapsed) self.service_manager_list.itemExpanded.connect(self.expanded) # Last little bits of setting up @@ -1461,6 +1462,14 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa """ self.make_live() + def on_single_click_preview(self, field=None): + """ + If single click previewing is enabled, send the current item to the Preview slide controller but triggered by a tablewidget click event. + :param field: + """ + if Settings().value('advanced/single click service preview'): + self.make_preview() + def make_live(self, row=-1): """ Send the current item to the Live slide controller diff --git a/tests/functional/openlp_core_ui/test_servicemanager.py b/tests/functional/openlp_core_ui/test_servicemanager.py index 1f9070249..3bd9d8898 100644 --- a/tests/functional/openlp_core_ui/test_servicemanager.py +++ b/tests/functional/openlp_core_ui/test_servicemanager.py @@ -24,11 +24,11 @@ Package to test the openlp.core.ui.slidecontroller package. """ from unittest import TestCase -from openlp.core.common import Registry, ThemeLevel +from openlp.core.common import Registry, ThemeLevel, Settings from openlp.core.lib import ServiceItem, ServiceItemType, ItemCapabilities from openlp.core.ui import ServiceManager -from tests.functional import MagicMock +from tests.functional import MagicMock, patch class TestServiceManager(TestCase): @@ -540,3 +540,21 @@ class TestServiceManager(TestCase): self.assertEquals(service_manager.timed_slide_interval.setChecked.call_count, 0, 'Should not be called') self.assertEquals(service_manager.theme_menu.menuAction().setVisible.call_count, 1, 'Should have be called once') + + @patch(u'openlp.core.lib.mediamanageritem.Settings') + @patch(u'openlp.core.ui.servicemanager.ServiceManager.make_preview') + def single_click_preview_test(self, mocked_make_preview, MockedSettings): + """ + Test that when "Preview items when clicked in Service Manager" is enabled that the item goes to preview + """ + # GIVEN: A setting to enable "Preview items when clicked in Service Manager" and a service manager. + mocked_settings = MagicMock() + mocked_settings.value.side_effect = lambda x: x == 'advanced/single click service preview' + MockedSettings.return_value = mocked_settings + service_manager = ServiceManager(None) + + # WHEN: on_double_clicked() is called + service_manager.on_single_click_preview() + + # THEN: on_live_click() should have been called + mocked_make_preview.assert_called_with() \ No newline at end of file From 34c771688c8d65be076f517d9eeb410065662a3e Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Mon, 18 Jan 2016 12:49:20 +1030 Subject: [PATCH 013/110] Tests for Better Preview for Service Manager Blueprint --- .../openlp_core_ui/test_servicemanager.py | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/tests/functional/openlp_core_ui/test_servicemanager.py b/tests/functional/openlp_core_ui/test_servicemanager.py index 3bd9d8898..f013b9ada 100644 --- a/tests/functional/openlp_core_ui/test_servicemanager.py +++ b/tests/functional/openlp_core_ui/test_servicemanager.py @@ -540,21 +540,35 @@ class TestServiceManager(TestCase): self.assertEquals(service_manager.timed_slide_interval.setChecked.call_count, 0, 'Should not be called') self.assertEquals(service_manager.theme_menu.menuAction().setVisible.call_count, 1, 'Should have be called once') - - @patch(u'openlp.core.lib.mediamanageritem.Settings') + + @patch(u'openlp.core.ui.servicemanager.Settings') @patch(u'openlp.core.ui.servicemanager.ServiceManager.make_preview') - def single_click_preview_test(self, mocked_make_preview, MockedSettings): + def single_click_preview_test_true(self, mocked_make_preview, MockedSettings): """ Test that when "Preview items when clicked in Service Manager" is enabled that the item goes to preview """ # GIVEN: A setting to enable "Preview items when clicked in Service Manager" and a service manager. mocked_settings = MagicMock() - mocked_settings.value.side_effect = lambda x: x == 'advanced/single click service preview' + mocked_settings.value.return_value = True MockedSettings.return_value = mocked_settings service_manager = ServiceManager(None) - - # WHEN: on_double_clicked() is called + # WHEN: on_single_click_preview() is called service_manager.on_single_click_preview() - - # THEN: on_live_click() should have been called - mocked_make_preview.assert_called_with() \ No newline at end of file + # THEN: make_preview() should have been called + self.assertEquals(mocked_make_preview.call_count, 1, 'Should have been called once') + + @patch(u'openlp.core.ui.servicemanager.Settings') + @patch(u'openlp.core.ui.servicemanager.ServiceManager.make_preview') + def single_click_preview_test_false(self, mocked_make_preview, MockedSettings): + """ + Test that when "Preview items when clicked in Service Manager" is disabled that the item does not goes to preview + """ + # GIVEN: A setting to enable "Preview items when clicked in Service Manager" and a service manager. + mocked_settings = MagicMock() + mocked_settings.value.return_value = False + MockedSettings.return_value = mocked_settings + service_manager = ServiceManager(None) + # WHEN: on_single_click_preview() is called + service_manager.on_single_click_preview() + # THEN: make_preview() should have been called + self.assertEquals(mocked_make_preview.call_count, 0, 'Should not be called') From 7f8e19adcade618b9e3aa9afe98495cc8968b8f4 Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Tue, 19 Jan 2016 17:22:23 +1030 Subject: [PATCH 014/110] Prevented Single Click Preview when a double click had triggered & Updated tests for this --- openlp/core/ui/advancedtab.py | 2 +- openlp/core/ui/servicemanager.py | 22 ++++++- .../openlp_core_ui/test_servicemanager.py | 65 ++++++++++++++++--- 3 files changed, 75 insertions(+), 14 deletions(-) diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index fe8c09131..08adc0f29 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -274,7 +274,7 @@ class AdvancedTab(SettingsTab): self.single_click_preview_check_box.setText(translate('OpenLP.AdvancedTab', 'Preview items when clicked in Media Manager')) self.single_click_service_preview_check_box.setText(translate('OpenLP.AdvancedTab', - 'Preview items when clicked in Service Manager')) + 'Preview items when clicked in Service Manager')) self.expand_service_item_check_box.setText(translate('OpenLP.AdvancedTab', 'Expand new service items on creation')) self.enable_auto_close_check_box.setText(translate('OpenLP.AdvancedTab', diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index a90ab1e8b..e75a24d9e 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -211,7 +211,7 @@ class Ui_ServiceManager(object): self.layout.addWidget(self.order_toolbar) # Connect up our signals and slots self.theme_combo_box.activated.connect(self.on_theme_combo_box_selected) - self.service_manager_list.doubleClicked.connect(self.on_make_live) + self.service_manager_list.doubleClicked.connect(self.on_double_click_live) self.service_manager_list.clicked.connect(self.on_single_click_preview) self.service_manager_list.itemCollapsed.connect(self.collapsed) self.service_manager_list.itemExpanded.connect(self.expanded) @@ -320,6 +320,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa self._modified = False self._file_name = '' self.service_has_all_original_files = True + self.list_double_clicked = False def bootstrap_initialise(self): """ @@ -1455,19 +1456,34 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa else: return self.service_items[item]['service_item'] - def on_make_live(self, field=None): + def on_double_click_live(self, field=None): """ Send the current item to the Live slide controller but triggered by a tablewidget click event. :param field: """ + self.list_double_clicked = True self.make_live() def on_single_click_preview(self, field=None): """ - If single click previewing is enabled, send the current item to the Preview slide controller but triggered by a tablewidget click event. + If single click previewing is enabled, and triggered by a tablewidget click event, start a timeout to verify a double-click hasn't triggered. :param field: """ if Settings().value('advanced/single click service preview'): + if not self.list_double_clicked: + # If a double click has not registered start a timer, otherwise wait for the existing timer to finish. + QtCore.QTimer.singleShot(QtWidgets.QApplication.instance().doubleClickInterval(), self.on_single_click_preview_timeout) + + def on_single_click_preview_timeout(self): + """ + If a single click, but not a double click has been triggered, send the current item to the Preview slide controller. + :param field: + """ + if self.list_double_clicked: + # If a double click has registered, clear it. + self.list_double_clicked = False + else: + # Otherwise preview the item. self.make_preview() def make_live(self, row=-1): diff --git a/tests/functional/openlp_core_ui/test_servicemanager.py b/tests/functional/openlp_core_ui/test_servicemanager.py index f013b9ada..f731f7761 100644 --- a/tests/functional/openlp_core_ui/test_servicemanager.py +++ b/tests/functional/openlp_core_ui/test_servicemanager.py @@ -22,6 +22,7 @@ """ Package to test the openlp.core.ui.slidecontroller package. """ +import PyQt5 from unittest import TestCase from openlp.core.common import Registry, ThemeLevel, Settings @@ -540,12 +541,12 @@ class TestServiceManager(TestCase): self.assertEquals(service_manager.timed_slide_interval.setChecked.call_count, 0, 'Should not be called') self.assertEquals(service_manager.theme_menu.menuAction().setVisible.call_count, 1, 'Should have be called once') - + @patch(u'openlp.core.ui.servicemanager.Settings') - @patch(u'openlp.core.ui.servicemanager.ServiceManager.make_preview') - def single_click_preview_test_true(self, mocked_make_preview, MockedSettings): + @patch(u'PyQt5.QtCore.QTimer.singleShot') + def single_click_preview_test_true(self, mocked_singleShot, MockedSettings): """ - Test that when "Preview items when clicked in Service Manager" is enabled that the item goes to preview + Test that when "Preview items when clicked in Service Manager" enabled the preview timer starts """ # GIVEN: A setting to enable "Preview items when clicked in Service Manager" and a service manager. mocked_settings = MagicMock() @@ -554,14 +555,14 @@ class TestServiceManager(TestCase): service_manager = ServiceManager(None) # WHEN: on_single_click_preview() is called service_manager.on_single_click_preview() - # THEN: make_preview() should have been called - self.assertEquals(mocked_make_preview.call_count, 1, 'Should have been called once') - + # THEN: timer should have been started + mocked_singleShot.assert_called_with(PyQt5.QtWidgets.QApplication.instance().doubleClickInterval(),service_manager.on_single_click_preview_timeout) + @patch(u'openlp.core.ui.servicemanager.Settings') - @patch(u'openlp.core.ui.servicemanager.ServiceManager.make_preview') - def single_click_preview_test_false(self, mocked_make_preview, MockedSettings): + @patch(u'PyQt5.QtCore.QTimer.singleShot') + def single_click_preview_test_false(self, mocked_singleShot, MockedSettings): """ - Test that when "Preview items when clicked in Service Manager" is disabled that the item does not goes to preview + Test that when "Preview items when clicked in Service Manager" disabled the preview timer doesn't start """ # GIVEN: A setting to enable "Preview items when clicked in Service Manager" and a service manager. mocked_settings = MagicMock() @@ -570,5 +571,49 @@ class TestServiceManager(TestCase): service_manager = ServiceManager(None) # WHEN: on_single_click_preview() is called service_manager.on_single_click_preview() + # THEN: timer should not be started + self.assertEquals(mocked_singleShot.call_count, 0, 'Should not be called') + + @patch(u'openlp.core.ui.servicemanager.Settings') + @patch(u'PyQt5.QtCore.QTimer.singleShot') + @patch(u'openlp.core.ui.servicemanager.ServiceManager.make_live') + def single_click_preview_test_double(self, mocked_make_live, mocked_singleShot, MockedSettings): + """ + Test that when a double click has registered the preview timer doesn't start + """ + # GIVEN: A setting to enable "Preview items when clicked in Service Manager" and a service manager. + mocked_settings = MagicMock() + mocked_settings.value.return_value = True + MockedSettings.return_value = mocked_settings + service_manager = ServiceManager(None) + # WHEN: on_single_click_preview() is called following a double click + service_manager.on_double_click_live() + service_manager.on_single_click_preview() + # THEN: timer should not be started + self.assertEquals(mocked_singleShot.call_count, 0, 'Should not be called') + + @patch(u'openlp.core.ui.servicemanager.ServiceManager.make_preview') + def single_click_timeout_test_single(self, mocked_make_preview): + """ + Test that when a single click has been registered, the item is sent to preview + """ + # GIVEN: A service manager. + service_manager = ServiceManager(None) + # WHEN: on_single_click_preview() is called + service_manager.on_single_click_preview_timeout() + # THEN: make_preview() should have been called + self.assertEquals(mocked_make_preview.call_count, 1, 'Should have been called once') + + @patch(u'openlp.core.ui.servicemanager.ServiceManager.make_preview') + @patch(u'openlp.core.ui.servicemanager.ServiceManager.make_live') + def single_click_timeout_test_double(self, mocked_make_live, mocked_make_preview): + """ + Test that when a double click has been registered, the item does not goes to preview + """ + # GIVEN: A service manager. + service_manager = ServiceManager(None) + # WHEN: on_single_click_preview() is called after a double click + service_manager.on_double_click_live() + service_manager.on_single_click_preview_timeout() # THEN: make_preview() should have been called self.assertEquals(mocked_make_preview.call_count, 0, 'Should not be called') From fbd3f9f3dde455dabfadc6a26f2ae1e2131da5c0 Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Tue, 19 Jan 2016 17:32:47 +1030 Subject: [PATCH 015/110] Corrected pep8 fails --- openlp/core/ui/servicemanager.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index e75a24d9e..fdae5c069 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -1466,17 +1466,19 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa def on_single_click_preview(self, field=None): """ - If single click previewing is enabled, and triggered by a tablewidget click event, start a timeout to verify a double-click hasn't triggered. + If single click previewing is enabled, and triggered by a tablewidget click event, + start a timeout to verify a double-click hasn't triggered. :param field: """ if Settings().value('advanced/single click service preview'): if not self.list_double_clicked: # If a double click has not registered start a timer, otherwise wait for the existing timer to finish. - QtCore.QTimer.singleShot(QtWidgets.QApplication.instance().doubleClickInterval(), self.on_single_click_preview_timeout) - + QtCore.QTimer.singleShot(QtWidgets.QApplication.instance().doubleClickInterval(), + self.on_single_click_preview_timeout) + def on_single_click_preview_timeout(self): """ - If a single click, but not a double click has been triggered, send the current item to the Preview slide controller. + If a single click ok, but double click not triggered, send the current item to the Preview slide controller. :param field: """ if self.list_double_clicked: From 3900c33083e695f26996a151043d50450bbf2ad9 Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Tue, 19 Jan 2016 17:39:09 +1030 Subject: [PATCH 016/110] Corrected pep8 fails --- tests/functional/openlp_core_ui/test_servicemanager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/functional/openlp_core_ui/test_servicemanager.py b/tests/functional/openlp_core_ui/test_servicemanager.py index f731f7761..822703443 100644 --- a/tests/functional/openlp_core_ui/test_servicemanager.py +++ b/tests/functional/openlp_core_ui/test_servicemanager.py @@ -556,7 +556,8 @@ class TestServiceManager(TestCase): # WHEN: on_single_click_preview() is called service_manager.on_single_click_preview() # THEN: timer should have been started - mocked_singleShot.assert_called_with(PyQt5.QtWidgets.QApplication.instance().doubleClickInterval(),service_manager.on_single_click_preview_timeout) + mocked_singleShot.assert_called_with(PyQt5.QtWidgets.QApplication.instance().doubleClickInterval(), + service_manager.on_single_click_preview_timeout) @patch(u'openlp.core.ui.servicemanager.Settings') @patch(u'PyQt5.QtCore.QTimer.singleShot') From 5f513a9a1f325e757309437baf45e9ee85972b6d Mon Sep 17 00:00:00 2001 From: suutari-olli Date: Fri, 22 Jan 2016 19:06:35 +0200 Subject: [PATCH 017/110] This simple fix should make blank to desktop, black and theme available during single screen mode --- openlp/core/ui/slidecontroller.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 37c4836b9..64b63083a 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -607,7 +607,10 @@ class SlideController(DisplayController, RegistryProperties): widget.addActions([ self.previous_item, self.next_item, self.previous_service, self.next_service, - self.escape_item]) + self.escape_item, + self.desktop_screen, + self.theme_screen, + self.blank_screen]) def preview_size_changed(self): """ From dee737e3fb38b38d96c99d88e2c5c4233c72fbcb Mon Sep 17 00:00:00 2001 From: suutari-olli Date: Sat, 23 Jan 2016 17:28:16 +0200 Subject: [PATCH 018/110] Added proper comment --- openlp/core/ui/slidecontroller.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 64b63083a..4a3b91b35 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -603,6 +603,9 @@ class SlideController(DisplayController, RegistryProperties): Add actions to the widget specified by `widget` :param widget: The UI widget for the actions + This defines the controls available when Live display has stolen focus. + Examples of this happening: Clicking anything in the live window or certain single screen mode scenarios. + Needles to say, blank to modes should not be removed from here. """ widget.addActions([ self.previous_item, self.next_item, From 76e7faf1aa0714691faf62a38dfdce2e5394c656 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sun, 7 Feb 2016 09:27:28 +0000 Subject: [PATCH 019/110] Remove _try_int function - spurious --- openlp/plugins/songs/lib/mediaitem.py | 15 ++------- .../openlp_plugins/songs/test_mediaitem.py | 32 ++----------------- 2 files changed, 5 insertions(+), 42 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 187dbfa05..87a3d81d1 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -328,6 +328,7 @@ class SongMediaItem(MediaManagerItem): """ log.debug('display results Topic') self.list_view.clear() + search_results = sorted(search_results, key=lambda topic: self._natural_sort_key(topic.name)) for topic in search_results: songs = sorted(topic.songs, key=lambda song: song.sort_key) for song in songs: @@ -693,23 +694,13 @@ class SongMediaItem(MediaManagerItem): # List must be empty at the end return not author_list - def _try_int(self, s): - """ - Convert string s to an integer if possible. Fail silently and return - the string as-is if it isn't an integer. - :param s: The string to try to convert. - """ - try: - return int(s) - except (TypeError, ValueError): - return s - def _natural_sort_key(self, s): """ Return a tuple by which s is sorted. :param s: A string value from the list we want to sort. """ - return list(map(self._try_int, re.findall(r'(\d+|\D+)', s))) + return [int(text) if text.isdecimal() else text + for text in re.split('(\d+)', s)] def search(self, string, show_error): """ diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index e9dd3e018..2b586a96a 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -416,46 +416,18 @@ class TestMediaItem(TestCase, TestMixin): # THEN: They should not match self.assertFalse(result, "Authors should not match") - def try_int_with_string_integer_test(self): - """ - Test the _try_int function with a string containing an integer - """ - # GIVEN: A string that is an integer - string_integer = '123' - - # WHEN: We "convert" it to an integer - integer_result = self.media_item._try_int(string_integer) - - # THEN: We should get back an integer - self.assertIsInstance(integer_result, int, 'The result should be an integer') - self.assertEqual(integer_result, 123, 'The result should be 123') - - def try_int_with_string_noninteger_test(self): - """ - Test the _try_int function with a string not containing an integer - """ - # GIVEN: A string that is not an integer - string_noninteger = 'abc' - - # WHEN: We "convert" it to an integer - noninteger_result = self.media_item._try_int(string_noninteger) - - # THEN: We should get back the original string - self.assertIsInstance(noninteger_result, type(string_noninteger), 'The result type should be the same') - self.assertEqual(noninteger_result, string_noninteger, 'The result value should be the same') - def natural_sort_key_test(self): """ Test the _natural_sort_key function """ # GIVEN: A string to be converted into a sort key - string_sort_key = 'A1B12C123' + string_sort_key = 'A1B12C' # WHEN: We attempt to create a sort key sort_key_result = self.media_item._natural_sort_key(string_sort_key) # THEN: We should get back a tuple split on integers - self.assertEqual(sort_key_result, ['A', 1, 'B', 12, 'C', 123]) + self.assertEqual(sort_key_result, ['A', 1, 'B', 12, 'C']) def build_remote_search_test(self): """ From 41d627e3f9d80ec2e1e444762e720fd964fa8db4 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 13 Feb 2016 16:57:09 +0000 Subject: [PATCH 020/110] natural sort lower case --- openlp/plugins/songs/lib/mediaitem.py | 2 +- tests/functional/openlp_plugins/songs/test_mediaitem.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 87a3d81d1..9dd4e29f8 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -699,7 +699,7 @@ class SongMediaItem(MediaManagerItem): Return a tuple by which s is sorted. :param s: A string value from the list we want to sort. """ - return [int(text) if text.isdecimal() else text + return [int(text) if text.isdecimal() else text.lower() for text in re.split('(\d+)', s)] def search(self, string, show_error): diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index 2b586a96a..d09f5b76e 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -427,7 +427,7 @@ class TestMediaItem(TestCase, TestMixin): sort_key_result = self.media_item._natural_sort_key(string_sort_key) # THEN: We should get back a tuple split on integers - self.assertEqual(sort_key_result, ['A', 1, 'B', 12, 'C']) + self.assertEqual(sort_key_result, ['a', 1, 'b', 12, 'c']) def build_remote_search_test(self): """ From 442f9578d1658314d6253ccea510f3df4f6f9171 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 13 Feb 2016 17:09:46 +0000 Subject: [PATCH 021/110] test fix for trunk --- .../openlp_plugins/songs/test_mediaitem.py | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index 0f7cdb7fc..428f9bca3 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -48,10 +48,6 @@ class TestMediaItem(TestCase, TestMixin): with patch('openlp.core.lib.mediamanageritem.MediaManagerItem._setup'), \ patch('openlp.plugins.songs.forms.editsongform.EditSongForm.__init__'): self.media_item = SongMediaItem(None, MagicMock()) - self.media_item.list_view = MagicMock() - self.media_item.list_view.save_auto_select_id = MagicMock() - self.media_item.list_view.clear = MagicMock() - self.media_item.list_view.addItem = MagicMock() self.media_item.display_songbook = False self.media_item.display_copyright_symbol = False self.setup_application() @@ -64,37 +60,6 @@ class TestMediaItem(TestCase, TestMixin): """ self.destroy_settings() - def display_results_book_test(self): - """ - Test displaying song search results grouped by book with basic song - """ - # GIVEN: Search results grouped by book, plus a mocked QtListWidgetItem - with patch('openlp.core.lib.QtGui.QListWidgetItem') as MockedQListWidgetItem, \ - patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole: - mock_search_results = [] - mock_book = MagicMock() - mock_song = MagicMock() - mock_book.name = 'My Book' - mock_book.songs = [] - mock_song.id = 1 - mock_song.title = 'My Song' - mock_song.sort_key = 'My Song' - mock_song.song_number = '123' - mock_song.temporary = False - mock_book.songs.append(mock_song) - mock_search_results.append(mock_book) - mock_qlist_widget = MagicMock() - MockedQListWidgetItem.return_value = mock_qlist_widget - - # WHEN: I display song search results grouped by book - self.media_item.display_results_book(mock_search_results) - - # THEN: The current list view is cleared, the widget is created, and the relevant attributes set - self.media_item.list_view.clear.assert_called_with() - MockedQListWidgetItem.assert_called_with('My Book - 123 (My Song)') - mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id) - self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget) - def build_song_footer_one_author_test(self): """ Test build songs footer with basic song and one author From 7887dcbf2b977fb851ab725d3ae849c9beca41d9 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 13 Feb 2016 17:11:35 +0000 Subject: [PATCH 022/110] cosmetic --- openlp/plugins/songs/lib/mediaitem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index fda607bbe..420379e2b 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -253,7 +253,7 @@ class SongMediaItem(MediaManagerItem): search_keywords = search_keywords.rpartition(' ') search_book = search_keywords[0] search_entry = re.sub(r'[^0-9]', '', search_keywords[2]) - + songbook_entries = (self.plugin.manager.session.query(SongBookEntry) .join(Book)) songbook_entries = sorted(songbook_entries, key=lambda songbook_entry: (songbook_entry.songbook.name, self._natural_sort_key(songbook_entry.entry))) From 4c883c8cf0a4eedfa409a268d3f35d325cdddb67 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 15 Feb 2016 21:49:23 +0100 Subject: [PATCH 023/110] Remove songs used in tests that is not in public domain. --- .../songs/test_presentationmanagerimport.py | 2 - .../songs/test_propresenterimport.py | 2 - .../songs/test_sundayplusimport.py | 2 - .../songs/test_worshipassistantimport.py | 2 - .../presentationmanagersongs/Agnus Dei.json | 14 ------- .../presentationmanagersongs/Agnus Dei.sng | 34 ---------------- .../propresentersongs/Vaste Grond.json | 34 ---------------- .../propresentersongs/Vaste Grond.pro4 | 1 - .../resources/sundayplussongs/Abba Fader.ptf | 8 ---- .../resources/sundayplussongs/abba-fader.json | 13 ------ .../lift_up_your_heads.csv | 40 ------------------- .../lift_up_your_heads.json | 13 ------ 12 files changed, 165 deletions(-) delete mode 100644 tests/resources/presentationmanagersongs/Agnus Dei.json delete mode 100644 tests/resources/presentationmanagersongs/Agnus Dei.sng delete mode 100644 tests/resources/propresentersongs/Vaste Grond.json delete mode 100644 tests/resources/propresentersongs/Vaste Grond.pro4 delete mode 100644 tests/resources/sundayplussongs/Abba Fader.ptf delete mode 100644 tests/resources/sundayplussongs/abba-fader.json delete mode 100644 tests/resources/worshipassistantsongs/lift_up_your_heads.csv delete mode 100644 tests/resources/worshipassistantsongs/lift_up_your_heads.json diff --git a/tests/functional/openlp_plugins/songs/test_presentationmanagerimport.py b/tests/functional/openlp_plugins/songs/test_presentationmanagerimport.py index 54a0ff02a..ce9fd71b7 100644 --- a/tests/functional/openlp_plugins/songs/test_presentationmanagerimport.py +++ b/tests/functional/openlp_plugins/songs/test_presentationmanagerimport.py @@ -44,7 +44,5 @@ class TestPresentationManagerFileImport(SongImportTestHelper): """ self.file_import([os.path.join(TEST_PATH, 'Great Is Thy Faithfulness.sng')], self.load_external_result_data(os.path.join(TEST_PATH, 'Great Is Thy Faithfulness.json'))) - self.file_import([os.path.join(TEST_PATH, 'Agnus Dei.sng')], - self.load_external_result_data(os.path.join(TEST_PATH, 'Agnus Dei.json'))) self.file_import([os.path.join(TEST_PATH, 'Amazing Grace.sng')], self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json'))) diff --git a/tests/functional/openlp_plugins/songs/test_propresenterimport.py b/tests/functional/openlp_plugins/songs/test_propresenterimport.py index 1bdb56034..bb6cb2bf9 100644 --- a/tests/functional/openlp_plugins/songs/test_propresenterimport.py +++ b/tests/functional/openlp_plugins/songs/test_propresenterimport.py @@ -45,5 +45,3 @@ class TestProPresenterFileImport(SongImportTestHelper): """ self.file_import([os.path.join(TEST_PATH, 'Amazing Grace.pro4')], self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json'))) - self.file_import([os.path.join(TEST_PATH, 'Vaste Grond.pro4')], - self.load_external_result_data(os.path.join(TEST_PATH, 'Vaste Grond.json'))) diff --git a/tests/functional/openlp_plugins/songs/test_sundayplusimport.py b/tests/functional/openlp_plugins/songs/test_sundayplusimport.py index 3b01e6ec1..2e03d2886 100644 --- a/tests/functional/openlp_plugins/songs/test_sundayplusimport.py +++ b/tests/functional/openlp_plugins/songs/test_sundayplusimport.py @@ -45,7 +45,5 @@ class TestSundayPlusFileImport(SongImportTestHelper): with patch('openlp.plugins.songs.lib.importers.sundayplus.retrieve_windows_encoding') as \ mocked_retrieve_windows_encoding: mocked_retrieve_windows_encoding.return_value = 'cp1252' - self.file_import([os.path.join(TEST_PATH, 'Abba Fader.ptf')], - self.load_external_result_data(os.path.join(TEST_PATH, 'abba-fader.json'))) self.file_import([os.path.join(TEST_PATH, 'Amazing Grace.ptf')], self.load_external_result_data(os.path.join(TEST_PATH, 'Amazing Grace.json'))) diff --git a/tests/functional/openlp_plugins/songs/test_worshipassistantimport.py b/tests/functional/openlp_plugins/songs/test_worshipassistantimport.py index 6c96382f2..9d824c404 100644 --- a/tests/functional/openlp_plugins/songs/test_worshipassistantimport.py +++ b/tests/functional/openlp_plugins/songs/test_worshipassistantimport.py @@ -49,5 +49,3 @@ class TestWorshipAssistantFileImport(SongImportTestHelper): self.load_external_result_data(os.path.join(TEST_PATH, 'would_you_be_free.json'))) self.file_import(os.path.join(TEST_PATH, 'would_you_be_free2.csv'), self.load_external_result_data(os.path.join(TEST_PATH, 'would_you_be_free.json'))) - self.file_import(os.path.join(TEST_PATH, 'lift_up_your_heads.csv'), - self.load_external_result_data(os.path.join(TEST_PATH, 'lift_up_your_heads.json'))) diff --git a/tests/resources/presentationmanagersongs/Agnus Dei.json b/tests/resources/presentationmanagersongs/Agnus Dei.json deleted file mode 100644 index 71c916d38..000000000 --- a/tests/resources/presentationmanagersongs/Agnus Dei.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "title": "Agnus Dei", - "verse_order_list": ["v1", "v2"], - "verses": [ - [ - "Alleluia Alleluluia \nfor the Lord almighty reigns \nAlleluia Alleluluia \nHoly holy are you Lord God Almighty \nWorthy is the lamb \nWorthy is the lamb \nHoly holy are you Lord God Almighty", - "v1" - ], - [ - "Worthy is the lamb \nWorthy is the lamb \nYou are holy holy \nAre you lamb \nWorthy is the lamb \nYou are holy holy \nYou are holy holy", - "v2" - ] - ] -} diff --git a/tests/resources/presentationmanagersongs/Agnus Dei.sng b/tests/resources/presentationmanagersongs/Agnus Dei.sng deleted file mode 100644 index ce4385c55..000000000 --- a/tests/resources/presentationmanagersongs/Agnus Dei.sng +++ /dev/null @@ -1,34 +0,0 @@ - - - -Agnus Dei - - - - - - - -Alleluia Alleluluia -for the Lord almighty reigns -Alleluia Alleluluia -Holy holy are you Lord God Almighty -Worthy is the lamb -Worthy is the lamb -Holy holy are you Lord God Almighty - - - - -Worthy is the lamb -Worthy is the lamb -You are holy holy -Are you lamb -Worthy is the lamb -You are holy holy -You are holy holy - - - - - diff --git a/tests/resources/propresentersongs/Vaste Grond.json b/tests/resources/propresentersongs/Vaste Grond.json deleted file mode 100644 index 75ffac7a2..000000000 --- a/tests/resources/propresentersongs/Vaste Grond.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "title": "Vaste Grond", - "verse_order_list": [], - "verses": [ - [ - "God voor U is niets onmogelijk\nHoe ongelofelijk\nU heeft alles in de hand", - "v1" - ], - [ - "U bent God en trekt Uw eigen plan\nU bent voor niemand bang\nVoor niets en niemand bang", - "v2" - ], - [ - "U houd me vast en geeft me moed\nOm door te gaan als ik niet durf\nIk wil van U zijn", - "v3" - ], - [ - "U geeft me kracht, en bent de vaste grond\nwaarop ik stevig sta\nik wil van U zijn, voor altijd van U zijn\nO God.", - "v4" - ], - [ - "Grote God, U bent uitzonderlijk\nen ondoorgrondelijk\nU biedt Uw liefde aan", - "v5" - ], - [ - "Wie ben ik, dat U mij ziet staan\nen met mij om wilt gaan?\nIk kan U niet weerstaan", - "v6" - ], - [ - "Onweerstaanbaar,\nonweerstaanbare God", - "v7" - ] - ] -} diff --git a/tests/resources/propresentersongs/Vaste Grond.pro4 b/tests/resources/propresentersongs/Vaste Grond.pro4 deleted file mode 100644 index 7abfb593d..000000000 --- a/tests/resources/propresentersongs/Vaste Grond.pro4 +++ /dev/null @@ -1 +0,0 @@ -<_-RVRect3D-_position x="32.37209" y="29" z="0" width="1074.349" height="818.7442"><_-D-_serializedShadow containerClass="NSMutableDictionary"><_-RVRect3D-_position x="32.37209" y="29" z="0" width="1074.349" height="818.7442"><_-D-_serializedShadow containerClass="NSMutableDictionary"><_-RVRect3D-_position x="32.37209" y="29" z="0" width="1074.349" height="818.7442"><_-D-_serializedShadow containerClass="NSMutableDictionary"><_-RVRect3D-_position x="32.37209" y="29" z="0" width="1074.349" height="818.7442"><_-D-_serializedShadow containerClass="NSMutableDictionary"><_-RVRect3D-_position x="32.37209" y="29" z="0" width="1074.349" height="818.7442"><_-D-_serializedShadow containerClass="NSMutableDictionary"><_-RVRect3D-_position x="32.37209" y="29" z="0" width="1074.349" height="818.7442"><_-D-_serializedShadow containerClass="NSMutableDictionary"><_-RVRect3D-_position x="32.37209" y="29" z="0" width="1074.349" height="818.7442"><_-D-_serializedShadow containerClass="NSMutableDictionary"> \ No newline at end of file diff --git a/tests/resources/sundayplussongs/Abba Fader.ptf b/tests/resources/sundayplussongs/Abba Fader.ptf deleted file mode 100644 index 28dc36cb3..000000000 --- a/tests/resources/sundayplussongs/Abba Fader.ptf +++ /dev/null @@ -1,8 +0,0 @@ -[#PTFVersion: 2, #GLOBAL_RECT: rect(47,2,1026,770), #opacity: 100, #SHADOW_ON: 0, #SHADOW_COLOR: rgb( 0, 0, 0), #SHADOW_OPACITY: 100, #SHADOW_POSITION: "RB", #SHADOW_OFFSET: [0, 0], #FILE_TYPE: "Song", #title: "Abba Fader", #Author: "Okänd", #Copyright: "ccc", #CELL1: [#MARKER_NAME: "Abba Fader", #Hotkey: "1", #rtf: "{\rtf1\ansi\ansicpg1252\deff0\deflang1053{\fonttbl{\f0\froman\fprq2\fcharset0 Verdana;}{\f1\froman\fcharset0 Verdana;}} -{\colortbl ;\red255\green255\blue0;\red224\green223\blue227;} -\viewkind4\uc1\pard\cf1\b\f0\fs86 Abba Fader\par -\par -Vi \^e4r h\^e4r f\^f6r att prisa Dig\line Vi \^e4r h\^e4r med f\^f6rv\^e4ntan\line Vi \^e4r h\^e4r som ett enat folk\line Vi kommer fram till Dig\line Med v\^e5r lovs\^e5ng\line\fs59\line\fs86 Vi ropar Abba Fader\line Du som har all makt\line Vi ropar Abba Fader\line Till Dig st\^e5r allt v\^e5rt hopp\line Vi ropar Abba Fader\line V\^e5r fr\^e4lsare, befriare \^e4r Du\b0\line\pard\tx720\f1\par -\cf2\par -} -", #Align: #Left]] \ No newline at end of file diff --git a/tests/resources/sundayplussongs/abba-fader.json b/tests/resources/sundayplussongs/abba-fader.json deleted file mode 100644 index 7172872e5..000000000 --- a/tests/resources/sundayplussongs/abba-fader.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "authors": [ - ["Okänd"] - ], - "title": "Abba Fader", - "verse_order_list": [], - "verses": [ - [ - "Abba Fader\n\nVi är här för att prisa Dig\nVi är här med förväntan\nVi är här som ett enat folk\nVi kommer fram till Dig\nMed vÃ¥r lovsÃ¥ng\n\nVi ropar Abba Fader\nDu som har all makt\nVi ropar Abba Fader\nTill Dig stÃ¥r allt vÃ¥rt hopp\nVi ropar Abba Fader\nVÃ¥r frälsare, befriare är Du", - "v1" - ] - ] -} diff --git a/tests/resources/worshipassistantsongs/lift_up_your_heads.csv b/tests/resources/worshipassistantsongs/lift_up_your_heads.csv deleted file mode 100644 index 18be3110b..000000000 --- a/tests/resources/worshipassistantsongs/lift_up_your_heads.csv +++ /dev/null @@ -1,40 +0,0 @@ -"SongID","SongNr","Title","Author","Copyright","FirstLine","PriKey","AltKey","Tempo","Focus","Theme","Scripture","Active","Songbook","TimeSig","Introduced","LastUsed","TimesUsed","CCLINr","User1","User2","User3","User4","User5","Roadmap","Overmap","FileLink1","FileLink2","Updated","Lyrics","Info","Lyrics2","Background" -"000013ab-0000-0000-0000-000000000000","0","Lift Up Your Heads"," Bryan Mierau","Public Domain","Lift up your heads and the doors","Em","NULL","NULL","NULL","NULL","NULL","1","1","NULL","NULL","NULL","0","NULL","NULL","NULL","NULL","NULL","NULL","NULL","NULL","NULL","NULL","2004-04-07 06:36:18.952",".Em D C D - Lift up your heads and the doors of your heart -. Am B7 Em - And the King of glory will come in -(Repeat) - -.G Am D - Who is this King of Glory? -. B7 Em - The Lord strong and mighty! -.G Am D - Who is this King of Glory? -. B7 - The Lord, mighty in battle! - -.G Am D - Who is this King of Glory? -.B7 Em - Jesus our Messiah! -.G Am D - Who is this King of Glory? -.B7 Em - Jesus, Lord of Lords! - -","NULL","Lift up your heads and the doors of your heart -And the King of glory will come in -(Repeat) - -Who is this King of Glory? -The Lord strong and mighty! -Who is this King of Glory? -The Lord, mighty in battle! - -Who is this King of Glory? -Jesus our Messiah! -Who is this King of Glory? -Jesus, Lord of Lords! - -","NULL" diff --git a/tests/resources/worshipassistantsongs/lift_up_your_heads.json b/tests/resources/worshipassistantsongs/lift_up_your_heads.json deleted file mode 100644 index d3ef07f44..000000000 --- a/tests/resources/worshipassistantsongs/lift_up_your_heads.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "authors": [ - "Bryan Mierau" - ], - "title": "Lift Up Your Heads", - "verse_order_list": [], - "verses": [ - [ - "Lift up your heads and the doors of your heart\nAnd the King of glory will come in\n(Repeat)\n\nWho is this King of Glory?\nThe Lord strong and mighty!\nWho is this King of Glory?\nThe Lord, mighty in battle!\n\nWho is this King of Glory?\nJesus our Messiah!\nWho is this King of Glory?\nJesus, Lord of Lords!\n", - "v1" - ] - ] -} From 034344ae4856d3d8e7cb1573c7e061ca103df5b1 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 15 Feb 2016 22:11:38 +0100 Subject: [PATCH 024/110] Add missing import. --- tests/interfaces/openlp_core_ui/test_projectorsourceform.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/interfaces/openlp_core_ui/test_projectorsourceform.py b/tests/interfaces/openlp_core_ui/test_projectorsourceform.py index 93aeb4c0a..3405a2346 100644 --- a/tests/interfaces/openlp_core_ui/test_projectorsourceform.py +++ b/tests/interfaces/openlp_core_ui/test_projectorsourceform.py @@ -28,6 +28,7 @@ import logging log = logging.getLogger(__name__) log.debug('test_projectorsourceform loaded') import os +import time from unittest import TestCase from PyQt5.QtWidgets import QDialog From 7c43ae6b88d7395c18d0662e222e7858542c9b91 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sat, 20 Feb 2016 22:42:31 +0100 Subject: [PATCH 025/110] Another attempt to fully fix bug 1531319, Fixes: https://launchpad.net/bugs/1531319 --- openlp/core/ui/maindisplay.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index a4bfa4bc5..d9a9a6468 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -408,10 +408,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): self.application.process_events() # Workaround for bug #1531319, should not be needed with PyQt 5.6. if is_win(): - # Workaround for bug #1531319, should not be needed with PyQt 5.6. fade_shake_timer.stop() - elif is_win(): - self.shake_web_view() # Wait for the webview to update before getting the preview. # Important otherwise first preview will miss the background ! while not self.web_loaded: @@ -429,6 +426,9 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): self.setVisible(True) else: self.setVisible(True) + # Workaround for bug #1531319, should not be needed with PyQt 5.6. + if is_win(): + self.shake_web_view() return self.grab() def build_html(self, service_item, image_path=''): From 6ef2cc8b598eec2e54ed89f158e80d19321a602c Mon Sep 17 00:00:00 2001 From: suutari-olli Date: Fri, 26 Feb 2016 23:28:01 +0200 Subject: [PATCH 026/110] Added test, fixed comment. --- openlp/core/ui/slidecontroller.py | 7 +++-- .../openlp_core_ui/test_slidecontroller.py | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 4a3b91b35..350e3fb59 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -601,11 +601,13 @@ class SlideController(DisplayController, RegistryProperties): def __add_actions_to_widget(self, widget): """ Add actions to the widget specified by `widget` - - :param widget: The UI widget for the actions This defines the controls available when Live display has stolen focus. Examples of this happening: Clicking anything in the live window or certain single screen mode scenarios. Needles to say, blank to modes should not be removed from here. + For some reason this required a test. It may be found in test_slidecontroller.py as + "live_stolen_focus_shortcuts_test. If you want to modify things here, you must also modify them there. (Duh) + + :param widget: The UI widget for the actions """ widget.addActions([ self.previous_item, self.next_item, @@ -983,6 +985,7 @@ class SlideController(DisplayController, RegistryProperties): self.update_preview() self.on_toggle_loop() + def on_theme_display(self, checked=None): """ Handle the Theme screen button diff --git a/tests/functional/openlp_core_ui/test_slidecontroller.py b/tests/functional/openlp_core_ui/test_slidecontroller.py index 7f071a835..af11c00bb 100644 --- a/tests/functional/openlp_core_ui/test_slidecontroller.py +++ b/tests/functional/openlp_core_ui/test_slidecontroller.py @@ -685,6 +685,33 @@ class TestSlideController(TestCase): self.assertEqual('mocked_presentation_item_stop', mocked_execute.call_args_list[1][0][0], 'The presentation should have been stopped.') + def live_stolen_focus_shortcuts_test(self): + """ + Test that all the needed shortcuts are available in scenarios where Live has stolen focus. + These are found under def __add_actions_to_widget(self, widget): in slidecontroller.py + """ + # GIVEN: A slide controller, actions needed + slide_controller = SlideController(None) + mocked_widget = MagicMock() + slide_controller.previous_item = MagicMock() + slide_controller.next_item = MagicMock() + slide_controller.previous_service = MagicMock() + slide_controller.next_service = MagicMock() + slide_controller.escape_item = MagicMock() + slide_controller.desktop_screen = MagicMock() + slide_controller.blank_screen = MagicMock() + slide_controller.theme_screen = MagicMock() + + # WHEN: __add_actions_to_widget is called + slide_controller._SlideController__add_actions_to_widget(mocked_widget) + + # THEN: The call to addActions should be correct + mocked_widget.addActions.assert_called_with([ + slide_controller.previous_item, slide_controller.next_item, + slide_controller.previous_service, slide_controller.next_service, + slide_controller.escape_item, slide_controller.desktop_screen, + slide_controller.theme_screen, slide_controller.blank_screen + ]) class TestInfoLabel(TestCase): From e3c4f53dc4948c06d6597853d9ccff7f87756346 Mon Sep 17 00:00:00 2001 From: suutari-olli Date: Sat, 27 Feb 2016 00:03:32 +0200 Subject: [PATCH 027/110] Seems like I added one empty row at some point... Removing it... --- openlp/core/ui/slidecontroller.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 350e3fb59..45047c100 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -985,7 +985,6 @@ class SlideController(DisplayController, RegistryProperties): self.update_preview() self.on_toggle_loop() - def on_theme_display(self, checked=None): """ Handle the Theme screen button From e5485183314287a592ce0d62561ffcbfc7df0349 Mon Sep 17 00:00:00 2001 From: suutari-olli Date: Sat, 27 Feb 2016 16:25:31 +0200 Subject: [PATCH 028/110] Added 2nd empty row after last test in class. --- tests/functional/openlp_core_ui/test_slidecontroller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/functional/openlp_core_ui/test_slidecontroller.py b/tests/functional/openlp_core_ui/test_slidecontroller.py index af11c00bb..20e48cbe0 100644 --- a/tests/functional/openlp_core_ui/test_slidecontroller.py +++ b/tests/functional/openlp_core_ui/test_slidecontroller.py @@ -713,6 +713,7 @@ class TestSlideController(TestCase): slide_controller.theme_screen, slide_controller.blank_screen ]) + class TestInfoLabel(TestCase): def paint_event_text_fits_test(self): From 9774618d377f0f9af8bc94635c947672677b9a69 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Sun, 28 Feb 2016 02:26:38 -0800 Subject: [PATCH 029/110] Bugfix 1550891 - non-standard class reply from projector --- openlp/core/lib/projector/pjlink1.py | 10 +++++++++- .../openlp_core_lib/test_projector_pjlink1.py | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 5feda33f4..3a99dd7da 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -665,7 +665,15 @@ class PJLink1(QTcpSocket): :param data: Class that projector supports. """ - self.pjlink_class = data + # bug 1550891: Projector returns non-standard class response: + # : Expected: %1CLSS=1 + # : Received: %1CLSS=Class 1 + if len(data) > 1: + # Split non-standard information from response + clss = data.split()[-1] + else: + clss = data + self.pjlink_class = clss log.debug('(%s) Setting pjlink_class for this projector to "%s"' % (self.ip, self.pjlink_class)) return diff --git a/tests/functional/openlp_core_lib/test_projector_pjlink1.py b/tests/functional/openlp_core_lib/test_projector_pjlink1.py index 92ce02acd..067818957 100644 --- a/tests/functional/openlp_core_lib/test_projector_pjlink1.py +++ b/tests/functional/openlp_core_lib/test_projector_pjlink1.py @@ -60,3 +60,17 @@ class TestPJLink(TestCase): "Connection request should have been called with TEST_SALT")) self.assertTrue(mock_qmd5_hash.called_with(TEST_PIN, "Connection request should have been called with TEST_PIN")) + + def non_standard_class_reply_test(self): + """ + bugfix 1550891 - CLSS request returns non-standard 'Class N' reply + """ + # GIVEN: Test object + pjlink = pjlink_test + + # WHEN: Process non-standard reply + pjlink.process_clss('Class 1') + + # THEN: Projector class should be set with proper value + self.assertEquals(pjlink.pjlink_class, '1', + 'Non-standard class reply should have set proper class') From 7c23941f359f1a6a6ca49019a639ddd54f8c0533 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Sun, 28 Feb 2016 04:35:15 -0800 Subject: [PATCH 030/110] Fix sending unicode string when expecting ascii (binary) data --- openlp/core/lib/projector/pjlink1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/lib/projector/pjlink1.py b/openlp/core/lib/projector/pjlink1.py index 3a99dd7da..c5c765d62 100644 --- a/openlp/core/lib/projector/pjlink1.py +++ b/openlp/core/lib/projector/pjlink1.py @@ -515,7 +515,7 @@ class PJLink1(QTcpSocket): self.socket_timer.start() try: self.projectorNetwork.emit(S_NETWORK_SENDING) - sent = self.write(out) + sent = self.write(out.encode('ascii')) self.waitForBytesWritten(2000) # 2 seconds should be enough if sent == -1: # Network error? From 1e94cd92e9e86f409fca53f8d6cdae7f89847d25 Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Mon, 29 Feb 2016 15:31:05 +1030 Subject: [PATCH 031/110] Split auto-scroll & height cap features to new branch --- openlp/core/ui/listpreviewwidget.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index fb6481e56..496c3e8ec 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -47,6 +47,9 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): """ super(QtWidgets.QTableWidget, self).__init__(parent) self._setup(screen_ratio) + + # max row height for non-text slides in pixels. If <= 0, will disable max row height. + self.max_img_row_height = 200 def _setup(self, screen_ratio): """ @@ -82,8 +85,10 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): self.resizeRowsToContents() else: # Sort out image heights. + height = self.viewport().width() // self.screen_ratio ### Moved out of loop as only needs to run once + if self.max_img_row_height > 0 and height > self.max_img_row_height: ### Apply row height cap. + height = self.max_img_row_height for frame_number in range(len(self.service_item.get_frames())): - height = self.viewport().width() // self.screen_ratio self.setRowHeight(frame_number, height) def screen_size_changed(self, screen_ratio): @@ -139,7 +144,20 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): pixmap = QtGui.QPixmap.fromImage(image) pixmap.setDevicePixelRatio(label.devicePixelRatio()) label.setPixmap(pixmap) - self.setCellWidget(frame_number, 0, label) + ### begin added/modified content + if self.max_img_row_height > 0: + label.setMaximumWidth(self.max_img_row_height * self.screen_ratio) ### set max width based on max height + label.resize(self.max_img_row_height * self.screen_ratio,self.max_img_row_height) ### resize to max width and max height; may be adjusted when setRowHeight called. + container = QtWidgets.QWidget() ### container widget + hbox = QtWidgets.QHBoxLayout() ### hbox to allow for horizonal stretch padding + hbox.setContentsMargins(0, 0, 0, 0) ### 0 contents margins to avoid extra padding + hbox.addWidget(label,stretch=1) ### add slide, stretch allows growing to max-width + hbox.addStretch(0) ### add strech padding with lowest priority; will only grow when slide has hit max-width + container.setLayout(hbox) ### populate container widget + self.setCellWidget(frame_number, 0, container) ### populate cell with container + else: + self.setCellWidget(frame_number, 0, label) ### populate cell with slide + ### end added/modified content slide_height = width // self.screen_ratio row += 1 text.append(str(row)) From 5809a2d6f47ba1ece04c41f710d42f23a36f0bd8 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 29 Feb 2016 20:29:32 +0100 Subject: [PATCH 032/110] Added test --- .../openlp_plugins/songusage/test_songusage.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/functional/openlp_plugins/songusage/test_songusage.py b/tests/functional/openlp_plugins/songusage/test_songusage.py index 6b103c9f1..61d8a22bb 100644 --- a/tests/functional/openlp_plugins/songusage/test_songusage.py +++ b/tests/functional/openlp_plugins/songusage/test_songusage.py @@ -81,3 +81,19 @@ class TestSongUsage(TestCase): # THEN: It should return True self.assertTrue(ret) + + @patch('openlp.plugins.songusage.songusageplugin.Manager') + def toggle_song_usage_state_test(self, MockedManager): + """ + Test that toggle_song_usage_state does toggle song_usage_state + """ + # GIVEN: A SongUsagePlugin + song_usage = SongUsagePlugin() + song_usage.set_button_state = MagicMock() + song_usage.song_usage_active = True + + # WHEN: calling toggle_song_usage_state + song_usage.toggle_song_usage_state() + + # THEN: song_usage_state should have been toogled + self.assertFalse(song_usage.song_usage_active) From d28ca7500e682f93a47d7edc126b405835a26242 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 29 Feb 2016 22:35:53 +0100 Subject: [PATCH 033/110] Beginning of an OPS Pro importer --- openlp/plugins/songs/lib/importer.py | 13 +++ openlp/plugins/songs/lib/importers/opspro.py | 87 ++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 openlp/plugins/songs/lib/importers/opspro.py diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index 5e099dde9..409bc897a 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -170,6 +170,7 @@ class SongFormat(object): WorshipAssistant = 23 WorshipCenterPro = 24 ZionWorx = 25 + OPSPro = 26 # Set optional attribute defaults __defaults__ = { @@ -382,6 +383,17 @@ class SongFormat(object): 'First convert your ZionWorx database to a CSV text file, as ' 'explained in the User Manual.') + }, + OPSPro: { + 'name': 'OPS Pro', + 'prefix': 'OPSPro', + 'canDisable': True, + 'selectMode': SongFormatSelect.SingleFile, + 'filter': '%s (*.mdb)' % translate('SongsPlugin.ImportWizardForm', 'OPS Pro database'), + 'disabledLabelText': translate('SongsPlugin.ImportWizardForm', + 'The OPS Pro importer is only supported on Windows. It has been ' + 'disabled due to a missing Python module. If you want to use this ' + 'importer, you will need to install the "pyodbc" module.') } } @@ -417,6 +429,7 @@ class SongFormat(object): SongFormat.WorshipAssistant, SongFormat.WorshipCenterPro, SongFormat.ZionWorx, + SongFormat.OPSPro ]) @staticmethod diff --git a/openlp/plugins/songs/lib/importers/opspro.py b/openlp/plugins/songs/lib/importers/opspro.py new file mode 100644 index 000000000..e0e727261 --- /dev/null +++ b/openlp/plugins/songs/lib/importers/opspro.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 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:`opspro` module provides the functionality for importing +a OPS Pro database into the OpenLP database. +""" +import logging +import re +import pyodbc + +from openlp.core.common import translate +from openlp.plugins.songs.lib.importers.songimport import SongImport + +log = logging.getLogger(__name__) + + +class OpsProImport(SongImport): + """ + The :class:`OpsProImport` class provides the ability to import the + WorshipCenter Pro Access Database + """ + def __init__(self, manager, **kwargs): + """ + Initialise the WorshipCenter Pro importer. + """ + super(OpsProImport, self).__init__(manager, **kwargs) + + def do_import(self): + """ + Receive a single file to import. + """ + try: + conn = pyodbc.connect('DRIVER={Microsoft Access Driver (*.mdb)};DBQ=%s' % self.import_source) + except (pyodbc.DatabaseError, pyodbc.IntegrityError, pyodbc.InternalError, pyodbc.OperationalError) as e: + log.warning('Unable to connect the OPS Pro database %s. %s', self.import_source, str(e)) + # Unfortunately no specific exception type + self.log_error(self.import_source, translate('SongsPlugin.OpsProImport', + 'Unable to connect the OPS Pro database.')) + return + cursor = conn.cursor() + cursor.execute('SELECT Song.ID, Song.SongNumber, Song.SongBookID, Song.Title, Song.CopyrightText, Version, Origin FROM Song ORDER BY Song.Title') + songs = cursor.fetchall() + self.import_wizard.progress_bar.setMaximum(len(songs)) + for song in songs: + if self.stop_import_flag: + break + cursor.execute('SELECT Lyrics FROM Lyrics WHERE SongID = %s ORDER BY Type, Number' + % song.ID) + verses = cursor.fetchall() + cursor.execute('SELECT CategoryName FROM Category INNER JOIN SongCategory ON SongCategory.CategoryID = Category.CategoryID ' + 'WHERE SongCategory.SongID = %s' % song.ID) + topics = cursor.fetchall() + + + self.process_song(song, verses, topics) + + def process_song(self, song, verses, verse_order, topics): + """ + Create the song, i.e. title, verse etc. + """ + self.set_defaults() + self.title = song.Title + self.parse_author(song.CopyrightText) + self.add_copyright(song.Origin) + for topic in topics: + self.topics.append(topic.Name) + self.add_verse(verses.Lyrics) + self.finish() From 4a74c9ce68cedf678ddc5916c3ddaab1048f8288 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 3 Mar 2016 09:53:29 -0800 Subject: [PATCH 034/110] Projector - String standards --- openlp/core/ui/projector/manager.py | 36 ++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/openlp/core/ui/projector/manager.py b/openlp/core/ui/projector/manager.py index 52b640956..fc40ee386 100644 --- a/openlp/core/ui/projector/manager.py +++ b/openlp/core/ui/projector/manager.py @@ -83,60 +83,60 @@ class Ui_ProjectorManager(object): self.one_toolbar.add_toolbar_action('new_projector', text=translate('OpenLP.ProjectorManager', 'Add Projector'), icon=':/projector/projector_new.png', - tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector'), + tooltip=translate('OpenLP.ProjectorManager', 'Add a new projector.'), triggers=self.on_add_projector) # Show edit/delete when projector not connected self.one_toolbar.add_toolbar_action('edit_projector', text=translate('OpenLP.ProjectorManager', 'Edit Projector'), icon=':/general/general_edit.png', - tooltip=translate('OpenLP.ProjectorManager', 'Edit selected projector'), + tooltip=translate('OpenLP.ProjectorManager', 'Edit selected projector.'), triggers=self.on_edit_projector) self.one_toolbar.add_toolbar_action('delete_projector', text=translate('OpenLP.ProjectorManager', 'Delete Projector'), icon=':/general/general_delete.png', - tooltip=translate('OpenLP.ProjectorManager', 'Delete selected projector'), + tooltip=translate('OpenLP.ProjectorManager', 'Delete selected projector.'), triggers=self.on_delete_projector) # Show source/view when projector connected self.one_toolbar.add_toolbar_action('source_view_projector', text=translate('OpenLP.ProjectorManager', 'Select Input Source'), icon=':/projector/projector_hdmi.png', tooltip=translate('OpenLP.ProjectorManager', - 'Choose input source on selected projector'), + 'Choose input source on selected projector.'), triggers=self.on_select_input) self.one_toolbar.add_toolbar_action('view_projector', text=translate('OpenLP.ProjectorManager', 'View Projector'), icon=':/system/system_about.png', tooltip=translate('OpenLP.ProjectorManager', - 'View selected projector information'), + 'View selected projector information.'), triggers=self.on_status_projector) self.one_toolbar.addSeparator() self.one_toolbar.add_toolbar_action('connect_projector', text=translate('OpenLP.ProjectorManager', - 'Connect to selected projector'), + 'Connect to selected projector.'), icon=':/projector/projector_connect.png', tooltip=translate('OpenLP.ProjectorManager', - 'Connect to selected projector'), + 'Connect to selected projector.'), triggers=self.on_connect_projector) self.one_toolbar.add_toolbar_action('connect_projector_multiple', text=translate('OpenLP.ProjectorManager', 'Connect to selected projectors'), icon=':/projector/projector_connect_tiled.png', tooltip=translate('OpenLP.ProjectorManager', - 'Connect to selected projector'), + 'Connect to selected projectors.'), triggers=self.on_connect_projector) self.one_toolbar.add_toolbar_action('disconnect_projector', text=translate('OpenLP.ProjectorManager', 'Disconnect from selected projectors'), icon=':/projector/projector_disconnect.png', tooltip=translate('OpenLP.ProjectorManager', - 'Disconnect from selected projector'), + 'Disconnect from selected projector.'), triggers=self.on_disconnect_projector) self.one_toolbar.add_toolbar_action('disconnect_projector_multiple', text=translate('OpenLP.ProjectorManager', 'Disconnect from selected projector'), icon=':/projector/projector_disconnect_tiled.png', tooltip=translate('OpenLP.ProjectorManager', - 'Disconnect from selected projector'), + 'Disconnect from selected projectors.'), triggers=self.on_disconnect_projector) self.one_toolbar.addSeparator() self.one_toolbar.add_toolbar_action('poweron_projector', @@ -144,26 +144,26 @@ class Ui_ProjectorManager(object): 'Power on selected projector'), icon=':/projector/projector_power_on.png', tooltip=translate('OpenLP.ProjectorManager', - 'Power on selected projector'), + 'Power on selected projector.'), triggers=self.on_poweron_projector) self.one_toolbar.add_toolbar_action('poweron_projector_multiple', text=translate('OpenLP.ProjectorManager', 'Power on selected projector'), icon=':/projector/projector_power_on_tiled.png', tooltip=translate('OpenLP.ProjectorManager', - 'Power on selected projector'), + 'Power on selected projectors.'), triggers=self.on_poweron_projector) self.one_toolbar.add_toolbar_action('poweroff_projector', text=translate('OpenLP.ProjectorManager', 'Standby selected projector'), icon=':/projector/projector_power_off.png', tooltip=translate('OpenLP.ProjectorManager', - 'Put selected projector in standby'), + 'Put selected projector in standby.'), triggers=self.on_poweroff_projector) self.one_toolbar.add_toolbar_action('poweroff_projector_multiple', text=translate('OpenLP.ProjectorManager', 'Standby selected projector'), icon=':/projector/projector_power_off_tiled.png', tooltip=translate('OpenLP.ProjectorManager', - 'Put selected projector in standby'), + 'Put selected projectors in standby.'), triggers=self.on_poweroff_projector) self.one_toolbar.addSeparator() self.one_toolbar.add_toolbar_action('blank_projector', @@ -175,24 +175,24 @@ class Ui_ProjectorManager(object): triggers=self.on_blank_projector) self.one_toolbar.add_toolbar_action('blank_projector_multiple', text=translate('OpenLP.ProjectorManager', - 'Blank selected projector screen'), + 'Blank selected projectors screen'), icon=':/projector/projector_blank_tiled.png', tooltip=translate('OpenLP.ProjectorManager', - 'Blank selected projector screen'), + 'Blank selected projectors screen.'), triggers=self.on_blank_projector) self.one_toolbar.add_toolbar_action('show_projector', text=translate('OpenLP.ProjectorManager', 'Show selected projector screen'), icon=':/projector/projector_show.png', tooltip=translate('OpenLP.ProjectorManager', - 'Show selected projector screen'), + 'Show selected projector screen.'), triggers=self.on_show_projector) self.one_toolbar.add_toolbar_action('show_projector_multiple', text=translate('OpenLP.ProjectorManager', 'Show selected projector screen'), icon=':/projector/projector_show_tiled.png', tooltip=translate('OpenLP.ProjectorManager', - 'Show selected projector screen'), + 'Show selected projectors screen.'), triggers=self.on_show_projector) self.layout.addWidget(self.one_toolbar) self.projector_one_widget = QtWidgets.QWidgetAction(self.one_toolbar) From 544319ba924e8822d6cc9ee9ff7b6c184b5fbbf8 Mon Sep 17 00:00:00 2001 From: Ken Roberts Date: Thu, 3 Mar 2016 10:19:42 -0800 Subject: [PATCH 035/110] Add projector: status_change_test --- .../openlp_core_lib/test_projector_pjlink1.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/functional/openlp_core_lib/test_projector_pjlink1.py b/tests/functional/openlp_core_lib/test_projector_pjlink1.py index 067818957..7e19ff065 100644 --- a/tests/functional/openlp_core_lib/test_projector_pjlink1.py +++ b/tests/functional/openlp_core_lib/test_projector_pjlink1.py @@ -26,6 +26,7 @@ Package to test the openlp.core.lib.projector.pjlink1 package. from unittest import TestCase from openlp.core.lib.projector.pjlink1 import PJLink1 +from openlp.core.lib.projector.constants import E_PARAMETER, ERROR_STRING from tests.functional import patch from tests.resources.projector.data import TEST_PIN, TEST_SALT, TEST_CONNECT_AUTHENTICATE @@ -74,3 +75,20 @@ class TestPJLink(TestCase): # THEN: Projector class should be set with proper value self.assertEquals(pjlink.pjlink_class, '1', 'Non-standard class reply should have set proper class') + + @patch.object(pjlink_test, 'change_status') + def status_change_test(self, mock_change_status): + """ + Test process_command call with ERR2 (Parameter) status + """ + # GIVEN: Test object + pjlink = pjlink_test + + # WHEN: process_command is called with "ERR2" status from projector + pjlink.process_command('POWR', 'ERR2') + + # THEN: change_status should have called change_status with E_UNDEFINED + # as first parameter + mock_change_status.called_with(E_PARAMETER, + 'change_status should have been called with "{}"'.format( + ERROR_STRING[E_PARAMETER])) From 68460f5e3f5a8813481840d42a5ef727c35854c9 Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Sun, 6 Mar 2016 03:11:32 +1030 Subject: [PATCH 036/110] Added smart scaling when manually resized, integrated with settings dialog, fixed some pep8 errors --- openlp/core/common/settings.py | 1 + openlp/core/ui/advancedtab.py | 12 ++++ openlp/core/ui/listpreviewwidget.py | 64 ++++++++++++------- .../openlp_core_ui/test_listpreviewwidget.py | 4 +- 4 files changed, 58 insertions(+), 23 deletions(-) diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index 8ef2b3c8b..5c103ed9a 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -121,6 +121,7 @@ class Settings(QtCore.QSettings): 'advanced/double click live': False, 'advanced/enable exit confirmation': True, 'advanced/expand service item': False, + 'advanced/slide max height': 0, 'advanced/hide mouse': True, 'advanced/is portable': False, 'advanced/max recent files': 20, diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index 4421b432f..bc9fe520f 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -80,6 +80,13 @@ class AdvancedTab(SettingsTab): self.expand_service_item_check_box = QtWidgets.QCheckBox(self.ui_group_box) self.expand_service_item_check_box.setObjectName('expand_service_item_check_box') self.ui_layout.addRow(self.expand_service_item_check_box) + self.slide_max_height_label = QtWidgets.QLabel(self.ui_group_box) + self.slide_max_height_label.setObjectName('slide_max_height_label') + self.slide_max_height_spin_box = QtWidgets.QSpinBox(self.ui_group_box) + self.slide_max_height_spin_box.setObjectName('slide_max_height_spin_box') + self.slide_max_height_spin_box.setRange(0,1000) + self.slide_max_height_spin_box.setSingleStep(20) + self.ui_layout.addRow(self.slide_max_height_label,self.slide_max_height_spin_box) self.search_as_type_check_box = QtWidgets.QCheckBox(self.ui_group_box) self.search_as_type_check_box.setObjectName('SearchAsType_check_box') self.ui_layout.addRow(self.search_as_type_check_box) @@ -272,6 +279,9 @@ class AdvancedTab(SettingsTab): 'Preview items when clicked in Media Manager')) self.expand_service_item_check_box.setText(translate('OpenLP.AdvancedTab', 'Expand new service items on creation')) + self.slide_max_height_label.setText(translate('OpenLP.AdvancedTab', + 'Max height for non-text slides\nin slide controller:')) + self.slide_max_height_spin_box.setSpecialValueText(translate('OpenLP.AdvancedTab', 'Disabled')) self.enable_auto_close_check_box.setText(translate('OpenLP.AdvancedTab', 'Enable application exit confirmation')) self.service_name_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Default Service Name')) @@ -340,6 +350,7 @@ class AdvancedTab(SettingsTab): self.double_click_live_check_box.setChecked(settings.value('double click live')) self.single_click_preview_check_box.setChecked(settings.value('single click preview')) self.expand_service_item_check_box.setChecked(settings.value('expand service item')) + self.slide_max_height_spin_box.setValue(settings.value('slide max height')) self.enable_auto_close_check_box.setChecked(settings.value('enable exit confirmation')) self.hide_mouse_check_box.setChecked(settings.value('hide mouse')) self.service_name_day.setCurrentIndex(settings.value('default service day')) @@ -421,6 +432,7 @@ class AdvancedTab(SettingsTab): settings.setValue('double click live', self.double_click_live_check_box.isChecked()) settings.setValue('single click preview', self.single_click_preview_check_box.isChecked()) settings.setValue('expand service item', self.expand_service_item_check_box.isChecked()) + settings.setValue('slide max height', self.slide_max_height_spin_box.value()) settings.setValue('enable exit confirmation', self.enable_auto_close_check_box.isChecked()) settings.setValue('hide mouse', self.hide_mouse_check_box.isChecked()) settings.setValue('alternate rows', self.alternate_rows_check_box.isChecked()) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index 496c3e8ec..68c983d42 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -26,7 +26,7 @@ It is based on a QTableWidget but represents its contents in list form. from PyQt5 import QtCore, QtGui, QtWidgets -from openlp.core.common import RegistryProperties +from openlp.core.common import RegistryProperties, Settings from openlp.core.lib import ImageSource, ServiceItem @@ -47,9 +47,6 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): """ super(QtWidgets.QTableWidget, self).__init__(parent) self._setup(screen_ratio) - - # max row height for non-text slides in pixels. If <= 0, will disable max row height. - self.max_img_row_height = 200 def _setup(self, screen_ratio): """ @@ -66,6 +63,8 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): # Initialize variables. self.service_item = ServiceItem() self.screen_ratio = screen_ratio + # Connect signals + self.verticalHeader().sectionResized.connect(self.row_resized) def resizeEvent(self, event): """ @@ -83,14 +82,30 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): # Sort out songs, bibles, etc. if self.service_item.is_text(): self.resizeRowsToContents() + # Sort out image heights. else: - # Sort out image heights. - height = self.viewport().width() // self.screen_ratio ### Moved out of loop as only needs to run once - if self.max_img_row_height > 0 and height > self.max_img_row_height: ### Apply row height cap. - height = self.max_img_row_height + height = self.viewport().width() // self.screen_ratio + max_img_row_height = Settings().value('advanced/slide max height') + # Adjust for row height cap if in use. + if max_img_row_height > 0 and height > max_img_row_height: + height = max_img_row_height + # Apply new height to slides for frame_number in range(len(self.service_item.get_frames())): self.setRowHeight(frame_number, height) + def row_resized(self, row, old_height, new_height): + """ + Will scale non-image slides. + """ + # Only for non-text slides when row height cap in use + if self.service_item.is_text() or Settings().value('advanced/slide max height') <= 0: + return + # Get and validate label widget containing slide & adjust max width + try: + self.cellWidget(row, 0).children()[1].setMaximumWidth(new_height * self.screen_ratio) + except: + return + def screen_size_changed(self, screen_ratio): """ This method is called whenever the live screen size changes, which then makes a layout recalculation necessary @@ -144,21 +159,26 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): pixmap = QtGui.QPixmap.fromImage(image) pixmap.setDevicePixelRatio(label.devicePixelRatio()) label.setPixmap(pixmap) - ### begin added/modified content - if self.max_img_row_height > 0: - label.setMaximumWidth(self.max_img_row_height * self.screen_ratio) ### set max width based on max height - label.resize(self.max_img_row_height * self.screen_ratio,self.max_img_row_height) ### resize to max width and max height; may be adjusted when setRowHeight called. - container = QtWidgets.QWidget() ### container widget - hbox = QtWidgets.QHBoxLayout() ### hbox to allow for horizonal stretch padding - hbox.setContentsMargins(0, 0, 0, 0) ### 0 contents margins to avoid extra padding - hbox.addWidget(label,stretch=1) ### add slide, stretch allows growing to max-width - hbox.addStretch(0) ### add strech padding with lowest priority; will only grow when slide has hit max-width - container.setLayout(hbox) ### populate container widget - self.setCellWidget(frame_number, 0, container) ### populate cell with container - else: - self.setCellWidget(frame_number, 0, label) ### populate cell with slide - ### end added/modified content slide_height = width // self.screen_ratio + # Setup row height cap if in use. + max_img_row_height = Settings().value('advanced/slide max height') + if max_img_row_height > 0: + if slide_height > max_img_row_height: + slide_height = max_img_row_height + label.setMaximumWidth(max_img_row_height * self.screen_ratio) + label.resize(max_img_row_height * self.screen_ratio, max_img_row_height) + # Build widget with stretch padding + container = QtWidgets.QWidget() + hbox = QtWidgets.QHBoxLayout() + hbox.setContentsMargins(0, 0, 0, 0) + hbox.addWidget(label, stretch=1) + hbox.addStretch(0) + container.setLayout(hbox) + # Add to table + self.setCellWidget(frame_number, 0, container) + else: + # Add to table + self.setCellWidget(frame_number, 0, label) row += 1 text.append(str(row)) self.setItem(frame_number, 0, item) diff --git a/tests/functional/openlp_core_ui/test_listpreviewwidget.py b/tests/functional/openlp_core_ui/test_listpreviewwidget.py index 6f27fbde3..5d1135e23 100644 --- a/tests/functional/openlp_core_ui/test_listpreviewwidget.py +++ b/tests/functional/openlp_core_ui/test_listpreviewwidget.py @@ -23,9 +23,11 @@ Package to test the openlp.core.ui.listpreviewwidget package. """ from unittest import TestCase + +from openlp.core.common import Settings from openlp.core.ui.listpreviewwidget import ListPreviewWidget -from tests.functional import patch +from tests.functional import MagicMock, patch class TestListPreviewWidget(TestCase): From e67ad21740a82be508a6e48680955240a15d3aae Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 7 Mar 2016 23:27:28 +0100 Subject: [PATCH 037/110] Getting closer... --- openlp/plugins/songs/lib/importer.py | 11 ++ openlp/plugins/songs/lib/importers/opspro.py | 135 ++++++++++++-- .../openlp_plugins/songs/test_opsproimport.py | 166 ++++++++++++++++++ 3 files changed, 296 insertions(+), 16 deletions(-) create mode 100644 tests/functional/openlp_plugins/songs/test_opsproimport.py diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index 409bc897a..d7837ce7d 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -48,6 +48,7 @@ from .importers.powerpraise import PowerPraiseImport from .importers.presentationmanager import PresentationManagerImport from .importers.lyrix import LyrixImport from .importers.videopsalm import VideoPsalmImport +from .importers.opspro import OpsProImport log = logging.getLogger(__name__) @@ -78,6 +79,13 @@ if is_win(): HAS_WORSHIPCENTERPRO = True except ImportError: log.exception('Error importing %s', 'WorshipCenterProImport') +HAS_OPSPRO = False +if is_win(): + try: + from .importers.opspro import OpsProImport + HAS_OPSPRO = True + except ImportError: + log.exception('Error importing %s', 'OpsProImport') class SongFormatSelect(object): @@ -478,6 +486,9 @@ if HAS_MEDIASHOUT: SongFormat.set(SongFormat.WorshipCenterPro, 'availability', HAS_WORSHIPCENTERPRO) if HAS_WORSHIPCENTERPRO: SongFormat.set(SongFormat.WorshipCenterPro, 'class', WorshipCenterProImport) +SongFormat.set(SongFormat.OPSPro, 'availability', HAS_OPSPRO) +if HAS_OPSPRO: + SongFormat.set(SongFormat.OPSPro, 'class', OpsProImport) __all__ = ['SongFormat', 'SongFormatSelect'] diff --git a/openlp/plugins/songs/lib/importers/opspro.py b/openlp/plugins/songs/lib/importers/opspro.py index e0e727261..957f16f81 100644 --- a/openlp/plugins/songs/lib/importers/opspro.py +++ b/openlp/plugins/songs/lib/importers/opspro.py @@ -25,7 +25,10 @@ a OPS Pro database into the OpenLP database. """ import logging import re -import pyodbc +import os +if os.name == 'nt': + import pyodbc +import struct from openlp.core.common import translate from openlp.plugins.songs.lib.importers.songimport import SongImport @@ -48,8 +51,9 @@ class OpsProImport(SongImport): """ Receive a single file to import. """ + password = self.extract_mdb_password() try: - conn = pyodbc.connect('DRIVER={Microsoft Access Driver (*.mdb)};DBQ=%s' % self.import_source) + conn = pyodbc.connect('DRIVER={Microsoft Access Driver (*.mdb)};DBQ=%s;PWD=%s' % (self.import_source, password)) except (pyodbc.DatabaseError, pyodbc.IntegrityError, pyodbc.InternalError, pyodbc.OperationalError) as e: log.warning('Unable to connect the OPS Pro database %s. %s', self.import_source, str(e)) # Unfortunately no specific exception type @@ -57,31 +61,130 @@ class OpsProImport(SongImport): 'Unable to connect the OPS Pro database.')) return cursor = conn.cursor() - cursor.execute('SELECT Song.ID, Song.SongNumber, Song.SongBookID, Song.Title, Song.CopyrightText, Version, Origin FROM Song ORDER BY Song.Title') + cursor.execute('SELECT Song.ID, SongNumber, SongBookName, Title, CopyrightText, Version, Origin FROM Song ' + 'LEFT JOIN SongBook ON Song.SongBookID = SongBook.ID ORDER BY Title') songs = cursor.fetchall() self.import_wizard.progress_bar.setMaximum(len(songs)) for song in songs: if self.stop_import_flag: break - cursor.execute('SELECT Lyrics FROM Lyrics WHERE SongID = %s ORDER BY Type, Number' - % song.ID) - verses = cursor.fetchall() - cursor.execute('SELECT CategoryName FROM Category INNER JOIN SongCategory ON SongCategory.CategoryID = Category.CategoryID ' - 'WHERE SongCategory.SongID = %s' % song.ID) + cursor.execute('SELECT Lyrics, Type, IsDualLanguage FROM Lyrics WHERE SongID = %d AND Type < 2 ORDER BY Type DESC' % song.ID) + lyrics = cursor.fetchone() + cursor.execute('SELECT CategoryName FROM Category INNER JOIN SongCategory ' + 'ON Category.ID = SongCategory.CategoryID WHERE SongCategory.SongID = %d ' + 'ORDER BY CategoryName' % song.ID) topics = cursor.fetchall() + self.process_song(song, lyrics, topics) + break - - self.process_song(song, verses, topics) - - def process_song(self, song, verses, verse_order, topics): + def process_song(self, song, lyrics, topics): """ Create the song, i.e. title, verse etc. """ self.set_defaults() self.title = song.Title - self.parse_author(song.CopyrightText) - self.add_copyright(song.Origin) + if song.CopyrightText: + self.parse_author(song.CopyrightText) + if song.Origin: + self.comments = song.Origin + if song.SongBookName: + self.song_book_name = song.SongBookName + if song.SongNumber: + self.song_number = song.SongNumber for topic in topics: - self.topics.append(topic.Name) - self.add_verse(verses.Lyrics) + self.topics.append(topic.CategoryName) + # Try to split lyrics based on various rules + print(song.ID) + if lyrics: + lyrics_text = lyrics.Lyrics + # Remove whitespaces around the join-tag to keep verses joint + lyrics_text = re.sub('\w*\[join\]\w*', '[join]', lyrics_text, flags=re.IGNORECASE) + lyrics_text = re.sub('\w*\[splits?\]\w*', '[split]', lyrics_text, flags=re.IGNORECASE) + verses = lyrics_text.split('\r\n\r\n') + verse_tag_defs = {} + verse_tag_texts = {} + chorus = '' + for verse_text in verses: + verse_def = 'v' + # Try to detect verse number + verse_number = re.match('^(\d+)\r\n', verse_text) + if verse_number: + verse_text = re.sub('^\d+\r\n', '', verse_text) + verse_def = 'v' + verse_number.group(1) + # Detect verse tags + elif re.match('^.*?:\r\n', verse_text): + tag_match = re.match('^(.*?)(\w.+)?:\r\n(.*)', verse_text) + tag = tag_match.group(1) + verse_text = tag_match.group(3) + if 'refrain' in tag.lower(): + verse_def = 'c' + elif 'bridge' in tag.lower(): + verse_def = 'b' + verse_tag_defs[tag] = verse_def + elif re.match('^\(.*\)$', verse_text): + tag_match = re.match('^\((.*)\)$', verse_text) + tag = tag_match.group(1) + if tag in verse_tag_defs: + verse_text = verse_tag_texts[tag] + verse_def = verse_tag_defs[tag] + # Try to detect end tag + elif re.match('^\[slot\]\r\n', verse_text, re.IGNORECASE): + verse_def = 'e' + verse_text = re.sub('^\[slot\]\r\n', '', verse_text, flags=re.IGNORECASE) + # Handle tags + # Replace the join tag with line breaks + verse_text = re.sub('\[join\]', '\r\n\r\n\r\n', verse_text) + # Replace the split tag with line breaks and an optional split + verse_text = re.sub('\[split\]', '\r\n\r\n[---]\r\n', verse_text) + # Handle translations + #if lyrics.IsDualLanguage: + # ... + + # Remove comments + verse_text = re.sub('\(.*?\)\r\n', '', verse_text, flags=re.IGNORECASE) + self.add_verse(verse_text, verse_def) + print(verse_def) + print(verse_text) self.finish() + + def extract_mdb_password(self): + """ + Extract password from mdb. Based on code from + http://tutorialsto.com/database/access/crack-access-*.-mdb-all-current-versions-of-the-password.html + """ + # The definition of 13 bytes as the source XOR Access2000. Encrypted with the corresponding signs are 0x13 + xor_pattern_2k = (0xa1, 0xec, 0x7a, 0x9c, 0xe1, 0x28, 0x34, 0x8a, 0x73, 0x7b, 0xd2, 0xdf, 0x50) + # Access97 XOR of the source + xor_pattern_97 = (0x86, 0xfb, 0xec, 0x37, 0x5d, 0x44, 0x9c, 0xfa, 0xc6, 0x5e, 0x28, 0xe6, 0x13) + mdb = open(self.import_source, 'rb') + mdb.seek(0x14) + version = struct.unpack('B', mdb.read(1))[0] + # Get encrypted logo + mdb.seek(0x62) + EncrypFlag = struct.unpack('B', mdb.read(1))[0] + # Get encrypted password + mdb.seek(0x42); + encrypted_password = mdb.read(26) + mdb.close() + # "Decrypt" the password based on the version + decrypted_password = '' + if version < 0x01: + # Access 97 + if int (encrypted_password[0] ^ xor_pattern_97[0]) == 0: + # No password + decrypted_password = '' + else: + for j in range(0, 12): + decrypted_password = decrypted_password + chr(encrypted_password[j] ^ xor_pattern_97[j]) + else: + # Access 2000 or 2002 + for j in range(0, 12): + if j% 2 == 0: + # Every byte with a different sign or encrypt. Encryption signs here for the 0x13 + t1 = chr (0x13 ^ EncrypFlag ^ encrypted_password[j * 2] ^ xor_pattern_2k[j]) + else: + t1 = chr(encrypted_password[j * 2] ^ xor_pattern_2k[j]); + decrypted_password = decrypted_password + t1; + if ord(decrypted_password[1]) < 0x20 or ord(decrypted_password[1]) > 0x7e: + decrypted_password = '' + return decrypted_password diff --git a/tests/functional/openlp_plugins/songs/test_opsproimport.py b/tests/functional/openlp_plugins/songs/test_opsproimport.py new file mode 100644 index 000000000..8289ae0dc --- /dev/null +++ b/tests/functional/openlp_plugins/songs/test_opsproimport.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2016 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 # +############################################################################### +""" +This module contains tests for the WorshipCenter Pro song importer. +""" +import os +from unittest import TestCase, SkipTest + +from tests.functional import patch, MagicMock + +from openlp.core.common import Registry +from openlp.plugins.songs.lib.importers.opspro import OpsProImport + + +class TestRecord(object): + """ + Microsoft Access Driver is not available on non Microsoft Systems for this reason the :class:`TestRecord` is used + to simulate a recordset that would be returned by pyobdc. + """ + def __init__(self, id, field, value): + # The case of the following instance variables is important as it needs to be the same as the ones in use in the + # WorshipCenter Pro database. + self.ID = id + self.Field = field + self.Value = value + + +RECORDSET_TEST_DATA = [TestRecord(1, 'TITLE', 'Amazing Grace'), + TestRecord(1, 'AUTHOR', 'John Newton'), + TestRecord(1, 'CCLISONGID', '12345'), + TestRecord(1, 'COMMENTS', 'The original version'), + TestRecord(1, 'COPY', 'Public Domain'), + TestRecord( + 1, 'LYRICS', + 'Amazing grace! How&crlf;sweet the sound&crlf;That saved a wretch like me!&crlf;' + 'I once was lost,&crlf;but now am found;&crlf;Was blind, but now I see.&crlf;&crlf;' + '\'Twas grace that&crlf;taught my heart to fear,&crlf;And grace my fears relieved;&crlf;' + 'How precious did&crlf;that grace appear&crlf;The hour I first believed.&crlf;&crlf;' + 'Through many dangers,&crlf;toils and snares,&crlf;I have already come;&crlf;' + '\'Tis grace hath brought&crlf;me safe thus far,&crlf;' + 'And grace will lead me home.&crlf;&crlf;The Lord has&crlf;promised good to me,&crlf;' + 'His Word my hope secures;&crlf;He will my Shield&crlf;and Portion be,&crlf;' + 'As long as life endures.&crlf;&crlf;Yea, when this flesh&crlf;and heart shall fail,&crlf;' + 'And mortal life shall cease,&crlf;I shall possess,&crlf;within the veil,&crlf;' + 'A life of joy and peace.&crlf;&crlf;The earth shall soon&crlf;dissolve like snow,&crlf;' + 'The sun forbear to shine;&crlf;But God, Who called&crlf;me here below,&crlf;' + 'Shall be forever mine.&crlf;&crlf;When we\'ve been there&crlf;ten thousand years,&crlf;' + 'Bright shining as the sun,&crlf;We\'ve no less days to&crlf;sing God\'s praise&crlf;' + 'Than when we\'d first begun.&crlf;&crlf;'), + TestRecord(2, 'TITLE', 'Beautiful Garden Of Prayer, The'), + TestRecord( + 2, 'LYRICS', + 'There\'s a garden where&crlf;Jesus is waiting,&crlf;' + 'There\'s a place that&crlf;is wondrously fair,&crlf;For it glows with the&crlf;' + 'light of His presence.&crlf;\'Tis the beautiful&crlf;garden of prayer.&crlf;&crlf;' + 'Oh, the beautiful garden,&crlf;the garden of prayer!&crlf;Oh, the beautiful&crlf;' + 'garden of prayer!&crlf;There my Savior awaits,&crlf;and He opens the gates&crlf;' + 'To the beautiful&crlf;garden of prayer.&crlf;&crlf;There\'s a garden where&crlf;' + 'Jesus is waiting,&crlf;And I go with my&crlf;burden and care,&crlf;' + 'Just to learn from His&crlf;lips words of comfort&crlf;In the beautiful&crlf;' + 'garden of prayer.&crlf;&crlf;There\'s a garden where&crlf;Jesus is waiting,&crlf;' + 'And He bids you to come,&crlf;meet Him there;&crlf;Just to bow and&crlf;' + 'receive a new blessing&crlf;In the beautiful&crlf;garden of prayer.&crlf;&crlf;')] +SONG_TEST_DATA = [{'title': 'Amazing Grace', + 'verses': [ + ('Amazing grace! How\nsweet the sound\nThat saved a wretch like me!\nI once was lost,\n' + 'but now am found;\nWas blind, but now I see.'), + ('\'Twas grace that\ntaught my heart to fear,\nAnd grace my fears relieved;\nHow precious did\n' + 'that grace appear\nThe hour I first believed.'), + ('Through many dangers,\ntoils and snares,\nI have already come;\n\'Tis grace hath brought\n' + 'me safe thus far,\nAnd grace will lead me home.'), + ('The Lord has\npromised good to me,\nHis Word my hope secures;\n' + 'He will my Shield\nand Portion be,\nAs long as life endures.'), + ('Yea, when this flesh\nand heart shall fail,\nAnd mortal life shall cease,\nI shall possess,\n' + 'within the veil,\nA life of joy and peace.'), + ('The earth shall soon\ndissolve like snow,\nThe sun forbear to shine;\nBut God, Who called\n' + 'me here below,\nShall be forever mine.'), + ('When we\'ve been there\nten thousand years,\nBright shining as the sun,\n' + 'We\'ve no less days to\nsing God\'s praise\nThan when we\'d first begun.')], + 'author': 'John Newton', + 'comments': 'The original version', + 'copyright': 'Public Domain'}, + {'title': 'Beautiful Garden Of Prayer, The', + 'verses': [ + ('There\'s a garden where\nJesus is waiting,\nThere\'s a place that\nis wondrously fair,\n' + 'For it glows with the\nlight of His presence.\n\'Tis the beautiful\ngarden of prayer.'), + ('Oh, the beautiful garden,\nthe garden of prayer!\nOh, the beautiful\ngarden of prayer!\n' + 'There my Savior awaits,\nand He opens the gates\nTo the beautiful\ngarden of prayer.'), + ('There\'s a garden where\nJesus is waiting,\nAnd I go with my\nburden and care,\n' + 'Just to learn from His\nlips words of comfort\nIn the beautiful\ngarden of prayer.'), + ('There\'s a garden where\nJesus is waiting,\nAnd He bids you to come,\nmeet Him there;\n' + 'Just to bow and\nreceive a new blessing\nIn the beautiful\ngarden of prayer.')]}] + + +class TestOpsProSongImport(TestCase): + """ + Test the functions in the :mod:`opsproimport` module. + """ + def setUp(self): + """ + Create the registry + """ + Registry.create() + + @patch('openlp.plugins.songs.lib.importers.opspro.SongImport') + def create_importer_test(self, mocked_songimport): + """ + Test creating an instance of the OPS Pro file importer + """ + # GIVEN: A mocked out SongImport class, and a mocked out "manager" + mocked_manager = MagicMock() + + # WHEN: An importer object is created + importer = OpsProImport(mocked_manager, filenames=[]) + + # THEN: The importer object should not be None + self.assertIsNotNone(importer, 'Import should not be none') + + @patch('openlp.plugins.songs.lib.importers.opspro.SongImport') + def detect_chorus_test(self, mocked_songimport): + """ + Test importing lyrics with a chorus in OPS Pro + """ + # GIVEN: A mocked out SongImport class, and a mocked out "manager" + mocked_manager = MagicMock() + importer = OpsProImport(mocked_manager, filenames=[]) + song = MagicMock() + song.ID = 100 + song.SongNumber = 123 + song.SongBookName = 'The Song Book' + song.Title = 'Song Title' + song.CopyrightText = 'Music and text by me' + song.Version = '1' + song.Origin = '...' + lyrics = MagicMock() + lyrics.Lyrics = 'sd' + lyrics.Type = 1 + lyrics.IsDualLanguage = True + importer.finish = MagicMock() + + # WHEN: An importer object is created + importer.process_song(song, lyrics, []) + + # THEN: The importer object should not be None + print(importer.verses) + print(importer.verse_order_list) + self.assertIsNone(importer, 'Import should not be none') \ No newline at end of file From 98eb50e9b2dd6bb0a3ff83a7e7e87981bf6b5d91 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 8 Mar 2016 22:43:10 +0100 Subject: [PATCH 038/110] Made the anchor/tag detection more generic. --- openlp/plugins/songs/lib/importers/opspro.py | 31 ++++---- .../openlp_plugins/songs/test_opsproimport.py | 75 ++++++++++--------- 2 files changed, 56 insertions(+), 50 deletions(-) diff --git a/openlp/plugins/songs/lib/importers/opspro.py b/openlp/plugins/songs/lib/importers/opspro.py index 957f16f81..5f423418b 100644 --- a/openlp/plugins/songs/lib/importers/opspro.py +++ b/openlp/plugins/songs/lib/importers/opspro.py @@ -98,13 +98,15 @@ class OpsProImport(SongImport): if lyrics: lyrics_text = lyrics.Lyrics # Remove whitespaces around the join-tag to keep verses joint - lyrics_text = re.sub('\w*\[join\]\w*', '[join]', lyrics_text, flags=re.IGNORECASE) - lyrics_text = re.sub('\w*\[splits?\]\w*', '[split]', lyrics_text, flags=re.IGNORECASE) - verses = lyrics_text.split('\r\n\r\n') + lyrics_text = re.sub('\s*\[join\]\s*', '[join]', lyrics_text, flags=re.IGNORECASE) + lyrics_text = re.sub('\s*\[splits?\]\s*', '[split]', lyrics_text, flags=re.IGNORECASE) + verses = re.split('\r\n\s*?\r\n', lyrics_text) verse_tag_defs = {} verse_tag_texts = {} chorus = '' for verse_text in verses: + if verse_text.strip() == '': + continue verse_def = 'v' # Try to detect verse number verse_number = re.match('^(\d+)\r\n', verse_text) @@ -112,18 +114,21 @@ class OpsProImport(SongImport): verse_text = re.sub('^\d+\r\n', '', verse_text) verse_def = 'v' + verse_number.group(1) # Detect verse tags - elif re.match('^.*?:\r\n', verse_text): - tag_match = re.match('^(.*?)(\w.+)?:\r\n(.*)', verse_text) - tag = tag_match.group(1) - verse_text = tag_match.group(3) - if 'refrain' in tag.lower(): + elif re.match('^.+?\:\r\n', verse_text): + tag_match = re.match('^(.+?)\:\r\n(.*)', verse_text, flags=re.DOTALL) + tag = tag_match.group(1).lower() + tag = tag.split(' ')[0] + verse_text = tag_match.group(2) + if 'refrein' in tag: verse_def = 'c' - elif 'bridge' in tag.lower(): + elif 'bridge' in tag: verse_def = 'b' verse_tag_defs[tag] = verse_def - elif re.match('^\(.*\)$', verse_text): - tag_match = re.match('^\((.*)\)$', verse_text) - tag = tag_match.group(1) + verse_tag_texts[tag] = verse_text + # Detect tag reference + elif re.match('^\(.*?\)$', verse_text): + tag_match = re.match('^\((.*?)\)$', verse_text) + tag = tag_match.group(1).lower() if tag in verse_tag_defs: verse_text = verse_tag_texts[tag] verse_def = verse_tag_defs[tag] @@ -133,7 +138,7 @@ class OpsProImport(SongImport): verse_text = re.sub('^\[slot\]\r\n', '', verse_text, flags=re.IGNORECASE) # Handle tags # Replace the join tag with line breaks - verse_text = re.sub('\[join\]', '\r\n\r\n\r\n', verse_text) + verse_text = re.sub('\[join\]', '\r\n\r\n', verse_text) # Replace the split tag with line breaks and an optional split verse_text = re.sub('\[split\]', '\r\n\r\n[---]\r\n', verse_text) # Handle translations diff --git a/tests/functional/openlp_plugins/songs/test_opsproimport.py b/tests/functional/openlp_plugins/songs/test_opsproimport.py index 8289ae0dc..ec6dd14fb 100644 --- a/tests/functional/openlp_plugins/songs/test_opsproimport.py +++ b/tests/functional/openlp_plugins/songs/test_opsproimport.py @@ -43,43 +43,44 @@ class TestRecord(object): self.Field = field self.Value = value +SONG_TEST_DATA1 = ('Refrein 2x:\r\n' +'Kom zing een nieuw lied\r\n' +'want dit is een nieuwe dag.\r\n' +'Zet de poorten open en zing je lied voor Hem.\r\n' +'[splits]\r\n' +'Kom zing een nieuw lied\r\n' +'Hij heeft je roep gehoord.\r\n' +'En de trouw en liefde van God zijn ook voor jou!\r\n' +' \r\n' +'Hij glimlacht en schijnt zijn licht op ons.\r\n' +'Hij redt ons en steunt ons liefdevol.\r\n' +'Mijn Redder, mijn sterkte is de Heer.\r\n' +'Deze dag leef ik voor Hem - en geef Hem eer!\r\n' +' \r\n' +'(refrein)\r\n' +'\r\n' +'Zijn goedheid rust elke dag op ons.\r\n' +'Zijn liefde verdrijft de angst in ons.\r\n' +'Mijn schuilplaats, mijn toevlucht is de Heer.\r\n' +'Deze dag leef ik voor Hem - en geef Hem eer!\r\n' +' \r\n' +'(refrein)\r\n' +'\r\n' +'Bridge 3x:\r\n' +'Breng dank aan de Heer jouw God.\r\n' +'Geef eer met een dankbaar hart,\r\n' +'Hij toont zijn liefde hier vandaag!\r\n' +'[splits]\r\n' +'Breng dank aan de Heer, jouw God.\r\n' +'Geef eer met een dankbaar hart.\r\n' +'Open je hart voor Hem vandaag!\r\n' +'\r\n' +'Ik zing een nieuw lied en breng Hem de hoogste eer\r\n' +'want de nieuwe dag is vol zegen van de Heer!\r\n' +'Ik zing een nieuw lied en breng Hem de hoogste eer.\r\n' +'Zet je hart wijd open en zing je lied voor Hem!\r\n') + -RECORDSET_TEST_DATA = [TestRecord(1, 'TITLE', 'Amazing Grace'), - TestRecord(1, 'AUTHOR', 'John Newton'), - TestRecord(1, 'CCLISONGID', '12345'), - TestRecord(1, 'COMMENTS', 'The original version'), - TestRecord(1, 'COPY', 'Public Domain'), - TestRecord( - 1, 'LYRICS', - 'Amazing grace! How&crlf;sweet the sound&crlf;That saved a wretch like me!&crlf;' - 'I once was lost,&crlf;but now am found;&crlf;Was blind, but now I see.&crlf;&crlf;' - '\'Twas grace that&crlf;taught my heart to fear,&crlf;And grace my fears relieved;&crlf;' - 'How precious did&crlf;that grace appear&crlf;The hour I first believed.&crlf;&crlf;' - 'Through many dangers,&crlf;toils and snares,&crlf;I have already come;&crlf;' - '\'Tis grace hath brought&crlf;me safe thus far,&crlf;' - 'And grace will lead me home.&crlf;&crlf;The Lord has&crlf;promised good to me,&crlf;' - 'His Word my hope secures;&crlf;He will my Shield&crlf;and Portion be,&crlf;' - 'As long as life endures.&crlf;&crlf;Yea, when this flesh&crlf;and heart shall fail,&crlf;' - 'And mortal life shall cease,&crlf;I shall possess,&crlf;within the veil,&crlf;' - 'A life of joy and peace.&crlf;&crlf;The earth shall soon&crlf;dissolve like snow,&crlf;' - 'The sun forbear to shine;&crlf;But God, Who called&crlf;me here below,&crlf;' - 'Shall be forever mine.&crlf;&crlf;When we\'ve been there&crlf;ten thousand years,&crlf;' - 'Bright shining as the sun,&crlf;We\'ve no less days to&crlf;sing God\'s praise&crlf;' - 'Than when we\'d first begun.&crlf;&crlf;'), - TestRecord(2, 'TITLE', 'Beautiful Garden Of Prayer, The'), - TestRecord( - 2, 'LYRICS', - 'There\'s a garden where&crlf;Jesus is waiting,&crlf;' - 'There\'s a place that&crlf;is wondrously fair,&crlf;For it glows with the&crlf;' - 'light of His presence.&crlf;\'Tis the beautiful&crlf;garden of prayer.&crlf;&crlf;' - 'Oh, the beautiful garden,&crlf;the garden of prayer!&crlf;Oh, the beautiful&crlf;' - 'garden of prayer!&crlf;There my Savior awaits,&crlf;and He opens the gates&crlf;' - 'To the beautiful&crlf;garden of prayer.&crlf;&crlf;There\'s a garden where&crlf;' - 'Jesus is waiting,&crlf;And I go with my&crlf;burden and care,&crlf;' - 'Just to learn from His&crlf;lips words of comfort&crlf;In the beautiful&crlf;' - 'garden of prayer.&crlf;&crlf;There\'s a garden where&crlf;Jesus is waiting,&crlf;' - 'And He bids you to come,&crlf;meet Him there;&crlf;Just to bow and&crlf;' - 'receive a new blessing&crlf;In the beautiful&crlf;garden of prayer.&crlf;&crlf;')] SONG_TEST_DATA = [{'title': 'Amazing Grace', 'verses': [ ('Amazing grace! How\nsweet the sound\nThat saved a wretch like me!\nI once was lost,\n' @@ -152,7 +153,7 @@ class TestOpsProSongImport(TestCase): song.Version = '1' song.Origin = '...' lyrics = MagicMock() - lyrics.Lyrics = 'sd' + lyrics.Lyrics = SONG_TEST_DATA1 lyrics.Type = 1 lyrics.IsDualLanguage = True importer.finish = MagicMock() From 51ffb92d40ad29201b935b8bfa41785903fb1f32 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Wed, 9 Mar 2016 22:44:15 +0100 Subject: [PATCH 039/110] Started work on tests --- openlp/plugins/songs/lib/importers/opspro.py | 11 +++ .../openlp_plugins/songs/test_opsproimport.py | 72 +------------------ .../opsprosongs/you are so faithfull.txt | 37 ++++++++++ 3 files changed, 51 insertions(+), 69 deletions(-) create mode 100644 tests/resources/opsprosongs/you are so faithfull.txt diff --git a/openlp/plugins/songs/lib/importers/opspro.py b/openlp/plugins/songs/lib/importers/opspro.py index 5f423418b..9572f8923 100644 --- a/openlp/plugins/songs/lib/importers/opspro.py +++ b/openlp/plugins/songs/lib/importers/opspro.py @@ -80,6 +80,17 @@ class OpsProImport(SongImport): def process_song(self, song, lyrics, topics): """ Create the song, i.e. title, verse etc. + + The OPS Pro format is a fairly simple text format using tags and anchors/labels. Linebreaks are \r\n. + Double linebreaks are slide dividers. OPS Pro support dual language using tags. + Tags are in [], see the liste below: + [join] are used to separate verses that should be keept on the same slide. + [split] or [splits] can be used to split a verse over several slides, while still being the same verse + Dual language tags: + [trans off] or [vertaal uit] turns dual language mode off for the following text + [trans on] or [vertaal aan] turns dual language mode on for the following text + [taal a] means the following lines are language a + [taal b] means the following lines are language b """ self.set_defaults() self.title = song.Title diff --git a/tests/functional/openlp_plugins/songs/test_opsproimport.py b/tests/functional/openlp_plugins/songs/test_opsproimport.py index ec6dd14fb..0eb03af9f 100644 --- a/tests/functional/openlp_plugins/songs/test_opsproimport.py +++ b/tests/functional/openlp_plugins/songs/test_opsproimport.py @@ -30,6 +30,7 @@ from tests.functional import patch, MagicMock from openlp.core.common import Registry from openlp.plugins.songs.lib.importers.opspro import OpsProImport +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'opsprosongs')) class TestRecord(object): """ @@ -43,74 +44,6 @@ class TestRecord(object): self.Field = field self.Value = value -SONG_TEST_DATA1 = ('Refrein 2x:\r\n' -'Kom zing een nieuw lied\r\n' -'want dit is een nieuwe dag.\r\n' -'Zet de poorten open en zing je lied voor Hem.\r\n' -'[splits]\r\n' -'Kom zing een nieuw lied\r\n' -'Hij heeft je roep gehoord.\r\n' -'En de trouw en liefde van God zijn ook voor jou!\r\n' -' \r\n' -'Hij glimlacht en schijnt zijn licht op ons.\r\n' -'Hij redt ons en steunt ons liefdevol.\r\n' -'Mijn Redder, mijn sterkte is de Heer.\r\n' -'Deze dag leef ik voor Hem - en geef Hem eer!\r\n' -' \r\n' -'(refrein)\r\n' -'\r\n' -'Zijn goedheid rust elke dag op ons.\r\n' -'Zijn liefde verdrijft de angst in ons.\r\n' -'Mijn schuilplaats, mijn toevlucht is de Heer.\r\n' -'Deze dag leef ik voor Hem - en geef Hem eer!\r\n' -' \r\n' -'(refrein)\r\n' -'\r\n' -'Bridge 3x:\r\n' -'Breng dank aan de Heer jouw God.\r\n' -'Geef eer met een dankbaar hart,\r\n' -'Hij toont zijn liefde hier vandaag!\r\n' -'[splits]\r\n' -'Breng dank aan de Heer, jouw God.\r\n' -'Geef eer met een dankbaar hart.\r\n' -'Open je hart voor Hem vandaag!\r\n' -'\r\n' -'Ik zing een nieuw lied en breng Hem de hoogste eer\r\n' -'want de nieuwe dag is vol zegen van de Heer!\r\n' -'Ik zing een nieuw lied en breng Hem de hoogste eer.\r\n' -'Zet je hart wijd open en zing je lied voor Hem!\r\n') - - -SONG_TEST_DATA = [{'title': 'Amazing Grace', - 'verses': [ - ('Amazing grace! How\nsweet the sound\nThat saved a wretch like me!\nI once was lost,\n' - 'but now am found;\nWas blind, but now I see.'), - ('\'Twas grace that\ntaught my heart to fear,\nAnd grace my fears relieved;\nHow precious did\n' - 'that grace appear\nThe hour I first believed.'), - ('Through many dangers,\ntoils and snares,\nI have already come;\n\'Tis grace hath brought\n' - 'me safe thus far,\nAnd grace will lead me home.'), - ('The Lord has\npromised good to me,\nHis Word my hope secures;\n' - 'He will my Shield\nand Portion be,\nAs long as life endures.'), - ('Yea, when this flesh\nand heart shall fail,\nAnd mortal life shall cease,\nI shall possess,\n' - 'within the veil,\nA life of joy and peace.'), - ('The earth shall soon\ndissolve like snow,\nThe sun forbear to shine;\nBut God, Who called\n' - 'me here below,\nShall be forever mine.'), - ('When we\'ve been there\nten thousand years,\nBright shining as the sun,\n' - 'We\'ve no less days to\nsing God\'s praise\nThan when we\'d first begun.')], - 'author': 'John Newton', - 'comments': 'The original version', - 'copyright': 'Public Domain'}, - {'title': 'Beautiful Garden Of Prayer, The', - 'verses': [ - ('There\'s a garden where\nJesus is waiting,\nThere\'s a place that\nis wondrously fair,\n' - 'For it glows with the\nlight of His presence.\n\'Tis the beautiful\ngarden of prayer.'), - ('Oh, the beautiful garden,\nthe garden of prayer!\nOh, the beautiful\ngarden of prayer!\n' - 'There my Savior awaits,\nand He opens the gates\nTo the beautiful\ngarden of prayer.'), - ('There\'s a garden where\nJesus is waiting,\nAnd I go with my\nburden and care,\n' - 'Just to learn from His\nlips words of comfort\nIn the beautiful\ngarden of prayer.'), - ('There\'s a garden where\nJesus is waiting,\nAnd He bids you to come,\nmeet Him there;\n' - 'Just to bow and\nreceive a new blessing\nIn the beautiful\ngarden of prayer.')]}] - class TestOpsProSongImport(TestCase): """ @@ -153,7 +86,8 @@ class TestOpsProSongImport(TestCase): song.Version = '1' song.Origin = '...' lyrics = MagicMock() - lyrics.Lyrics = SONG_TEST_DATA1 + test_file = open(os.path.join(TEST_PATH, 'you are so faithfull.txt'), 'rb') + lyrics.Lyrics = test_file.read().decode() lyrics.Type = 1 lyrics.IsDualLanguage = True importer.finish = MagicMock() diff --git a/tests/resources/opsprosongs/you are so faithfull.txt b/tests/resources/opsprosongs/you are so faithfull.txt new file mode 100644 index 000000000..ff9ced2c2 --- /dev/null +++ b/tests/resources/opsprosongs/you are so faithfull.txt @@ -0,0 +1,37 @@ +1 +You are so faithful +so faithful, so faithful. +You are so faithful +so faithful, so faithful. + +Refrein: +That's why I praise you +in the morning +That's why I praise you +in the noontime. +That's why I praise you +in the evening +That's why I praise you +all the time. + +2 +You are so loving +so loving, so loving. +You are so loving +so loving, so loving. + +(refrein) + +3 +You are so caring +so caring, so caring. +You are so caring +so caring, so caring. + +(refrein) + +4 +You are so mighty +so mighty, so mighty. +You are so mighty +so mighty, so mighty. From e9e5976d2208939742bb397b9fcf62bd67fa82c1 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Fri, 11 Mar 2016 22:56:07 +0100 Subject: [PATCH 040/110] Finished first test. --- openlp/plugins/songs/lib/importers/opspro.py | 4 +-- .../openlp_plugins/songs/test_opsproimport.py | 18 +++++++---- .../openlp_plugins/songs/test_videopsalm.py | 3 -- .../opsprosongs/You are so faithful.json | 31 +++++++++++++++++++ 4 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 tests/resources/opsprosongs/You are so faithful.json diff --git a/openlp/plugins/songs/lib/importers/opspro.py b/openlp/plugins/songs/lib/importers/opspro.py index 9572f8923..56463d093 100644 --- a/openlp/plugins/songs/lib/importers/opspro.py +++ b/openlp/plugins/songs/lib/importers/opspro.py @@ -159,8 +159,8 @@ class OpsProImport(SongImport): # Remove comments verse_text = re.sub('\(.*?\)\r\n', '', verse_text, flags=re.IGNORECASE) self.add_verse(verse_text, verse_def) - print(verse_def) - print(verse_text) + #print(verse_def) + #print(verse_text) self.finish() def extract_mdb_password(self): diff --git a/tests/functional/openlp_plugins/songs/test_opsproimport.py b/tests/functional/openlp_plugins/songs/test_opsproimport.py index 0eb03af9f..67b7c5959 100644 --- a/tests/functional/openlp_plugins/songs/test_opsproimport.py +++ b/tests/functional/openlp_plugins/songs/test_opsproimport.py @@ -23,6 +23,7 @@ This module contains tests for the WorshipCenter Pro song importer. """ import os +import json from unittest import TestCase, SkipTest from tests.functional import patch, MagicMock @@ -44,7 +45,6 @@ class TestRecord(object): self.Field = field self.Value = value - class TestOpsProSongImport(TestCase): """ Test the functions in the :mod:`opsproimport` module. @@ -74,7 +74,7 @@ class TestOpsProSongImport(TestCase): """ Test importing lyrics with a chorus in OPS Pro """ - # GIVEN: A mocked out SongImport class, and a mocked out "manager" + # GIVEN: A mocked out SongImport class, a mocked out "manager" and a mocked song and lyrics entry mocked_manager = MagicMock() importer = OpsProImport(mocked_manager, filenames=[]) song = MagicMock() @@ -95,7 +95,13 @@ class TestOpsProSongImport(TestCase): # WHEN: An importer object is created importer.process_song(song, lyrics, []) - # THEN: The importer object should not be None - print(importer.verses) - print(importer.verse_order_list) - self.assertIsNone(importer, 'Import should not be none') \ No newline at end of file + # THEN: The imported data should look like expected + result_file = open(os.path.join(TEST_PATH, 'You are so faithful.json'), 'rb') + result_data = json.loads(result_file.read().decode()) + self.assertListEqual(importer.verses, self._get_data(result_data, 'verses')) + self.assertListEqual(importer.verse_order_list_generated, self._get_data(result_data, 'verse_order_list')) + + def _get_data(self, data, key): + if key in data: + return data[key] + return '' diff --git a/tests/functional/openlp_plugins/songs/test_videopsalm.py b/tests/functional/openlp_plugins/songs/test_videopsalm.py index f75a67627..1bf13241d 100644 --- a/tests/functional/openlp_plugins/songs/test_videopsalm.py +++ b/tests/functional/openlp_plugins/songs/test_videopsalm.py @@ -23,11 +23,8 @@ This module contains tests for the VideoPsalm song importer. """ import os -from unittest import TestCase from tests.helpers.songfileimport import SongImportTestHelper -from openlp.core.common import Registry -from tests.functional import patch, MagicMock TEST_PATH = os.path.abspath( os.path.join(os.path.dirname(__file__), '..', '..', '..', 'resources', 'videopsalmsongs')) diff --git a/tests/resources/opsprosongs/You are so faithful.json b/tests/resources/opsprosongs/You are so faithful.json new file mode 100644 index 000000000..965d73ab8 --- /dev/null +++ b/tests/resources/opsprosongs/You are so faithful.json @@ -0,0 +1,31 @@ +{ + "title": "You are so faithful", + "verse_order_list": ["v1", "c1", "v2", "c1", "v3", "c1", "v4"], + "verses": [ + [ + "v1", + "You are so faithful\r\nso faithful, so faithful.\r\nYou are so faithful\r\nso faithful, so faithful.", + null + ], + [ + "c1", + "That's why I praise you\r\nin the morning\r\nThat's why I praise you\r\nin the noontime.\r\nThat's why I praise you\r\nin the evening\r\nThat's why I praise you\r\nall the time.", + null + ], + [ + "v2", + "You are so loving\r\nso loving, so loving.\r\nYou are so loving\r\nso loving, so loving.", + null + ], + [ + "v3", + "You are so caring\r\nso caring, so caring.\r\nYou are so caring\r\nso caring, so caring.", + null + ], + [ + "v4", + "You are so mighty\r\nso mighty, so mighty.\r\nYou are so mighty\r\nso mighty, so mighty.", + null + ] + ] +} From 4cea5e8b43756c593111a057d0640288f2b4b15f Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sat, 12 Mar 2016 22:22:21 +0100 Subject: [PATCH 041/110] Fix traceback in the bug-report dialog. Fixes bug 1554428. Fixes: https://launchpad.net/bugs/1554428 --- openlp/core/ui/exceptionform.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index e985829ba..2e4661579 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -180,11 +180,13 @@ class ExceptionForm(QtWidgets.QDialog, Ui_ExceptionDialog, RegistryProperties): if ':' in line: exception = line.split('\n')[-1].split(':')[0] subject = 'Bug report: %s in %s' % (exception, source) - mail_to_url = QtCore.QUrlQuery('mailto:bugs@openlp.org') - mail_to_url.addQueryItem('subject', subject) - mail_to_url.addQueryItem('body', self.report_text % content) + mail_urlquery = QtCore.QUrlQuery() + mail_urlquery.addQueryItem('subject', subject) + mail_urlquery.addQueryItem('body', self.report_text % content) if self.file_attachment: - mail_to_url.addQueryItem('attach', self.file_attachment) + mail_urlquery.addQueryItem('attach', self.file_attachment) + mail_to_url = QtCore.QUrl('mailto:bugs@openlp.org') + mail_to_url.setQuery(mail_urlquery) QtGui.QDesktopServices.openUrl(mail_to_url) def on_description_updated(self): From 4b57a2bae63b5483461141ed7eef8d7a673624a7 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sat, 12 Mar 2016 22:23:06 +0100 Subject: [PATCH 042/110] Fix weird test bug. --- .../openlp_core_lib/test_pluginmanager.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/interfaces/openlp_core_lib/test_pluginmanager.py b/tests/interfaces/openlp_core_lib/test_pluginmanager.py index 914bc7fb0..5efb1e3c7 100644 --- a/tests/interfaces/openlp_core_lib/test_pluginmanager.py +++ b/tests/interfaces/openlp_core_lib/test_pluginmanager.py @@ -32,7 +32,7 @@ from PyQt5 import QtWidgets from openlp.core.common import Registry, Settings from openlp.core.lib.pluginmanager import PluginManager -from tests.interfaces import MagicMock +from tests.interfaces import MagicMock, patch from tests.helpers.testmixin import TestMixin @@ -45,13 +45,12 @@ class TestPluginManager(TestCase, TestMixin): """ Some pre-test setup required. """ - Settings.setDefaultFormat(Settings.IniFormat) + self.setup_application() self.build_settings() self.temp_dir = mkdtemp('openlp') Settings().setValue('advanced/data path', self.temp_dir) Registry.create() Registry().register('service_list', MagicMock()) - self.setup_application() self.main_window = QtWidgets.QMainWindow() Registry().register('main_window', self.main_window) @@ -64,7 +63,13 @@ class TestPluginManager(TestCase, TestMixin): gc.collect() shutil.rmtree(self.temp_dir) - def find_plugins_test(self): + @patch('openlp.plugins.songusage.lib.db.init_schema') + @patch('openlp.plugins.songs.lib.db.init_schema') + @patch('openlp.plugins.images.lib.db.init_schema') + @patch('openlp.plugins.custom.lib.db.init_schema') + @patch('openlp.plugins.alerts.lib.db.init_schema') + @patch('openlp.plugins.bibles.lib.db.init_schema') + def find_plugins_test(self, mocked_is1, mocked_is2, mocked_is3, mocked_is4, mocked_is5, mocked_is6): """ Test the find_plugins() method to ensure it imports the correct plugins """ From 1dfad12edca5650edc62539eff7344a58b765905 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sat, 12 Mar 2016 22:25:39 +0100 Subject: [PATCH 043/110] Fix EasyWorship import issues with missing verses and traceback on unknown chars. Fixes: https://launchpad.net/bugs/1553922, https://launchpad.net/bugs/1547234 --- .../songs/lib/importers/easyworship.py | 72 ++++++++++--------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/openlp/plugins/songs/lib/importers/easyworship.py b/openlp/plugins/songs/lib/importers/easyworship.py index 5760e419d..25bbc37ff 100644 --- a/openlp/plugins/songs/lib/importers/easyworship.py +++ b/openlp/plugins/songs/lib/importers/easyworship.py @@ -289,40 +289,45 @@ class EasyWorshipSongImport(SongImport): for i in range(rec_count): if self.stop_import_flag: break - raw_record = db_file.read(record_size) - self.fields = self.record_structure.unpack(raw_record) - self.set_defaults() - self.title = self.get_field(fi_title).decode(self.encoding) - # Get remaining fields. - copy = self.get_field(fi_copy) - admin = self.get_field(fi_admin) - ccli = self.get_field(fi_ccli) - authors = self.get_field(fi_author) - words = self.get_field(fi_words) - if copy: - self.copyright = copy.decode(self.encoding) - if admin: + try: + raw_record = db_file.read(record_size) + self.fields = self.record_structure.unpack(raw_record) + self.set_defaults() + self.title = self.get_field(fi_title).decode(self.encoding) + # Get remaining fields. + copy = self.get_field(fi_copy) + admin = self.get_field(fi_admin) + ccli = self.get_field(fi_ccli) + authors = self.get_field(fi_author) + words = self.get_field(fi_words) if copy: - self.copyright += ', ' - self.copyright += translate('SongsPlugin.EasyWorshipSongImport', - 'Administered by %s') % admin.decode(self.encoding) - if ccli: - self.ccli_number = ccli.decode(self.encoding) - if authors: - authors = authors.decode(self.encoding) - else: - authors = '' - # Set the SongImport object members. - self.set_song_import_object(authors, words) - if self.stop_import_flag: - break - if self.entry_error_log: + self.copyright = copy.decode(self.encoding) + if admin: + if copy: + self.copyright += ', ' + self.copyright += translate('SongsPlugin.EasyWorshipSongImport', + 'Administered by %s') % admin.decode(self.encoding) + if ccli: + self.ccli_number = ccli.decode(self.encoding) + if authors: + authors = authors.decode(self.encoding) + else: + authors = '' + # Set the SongImport object members. + self.set_song_import_object(authors, words) + if self.stop_import_flag: + break + if self.entry_error_log: + self.log_error(self.import_source, + translate('SongsPlugin.EasyWorshipSongImport', '"%s" could not be imported. %s') + % (self.title, self.entry_error_log)) + self.entry_error_log = '' + elif not self.finish(): + self.log_error(self.import_source) + except Exception as e: self.log_error(self.import_source, translate('SongsPlugin.EasyWorshipSongImport', '"%s" could not be imported. %s') - % (self.title, self.entry_error_log)) - self.entry_error_log = '' - elif not self.finish(): - self.log_error(self.import_source) + % (self.title, e)) db_file.close() self.memo_file.close() @@ -368,7 +373,7 @@ class EasyWorshipSongImport(SongImport): first_line_is_tag = False # EW tags: verse, chorus, pre-chorus, bridge, tag, # intro, ending, slide - for tag in VerseType.tags + ['tag', 'slide']: + for tag in VerseType.names + ['tag', 'slide', 'end']: tag = tag.lower() ew_tag = verse_split[0].strip().lower() if ew_tag.startswith(tag): @@ -390,6 +395,9 @@ class EasyWorshipSongImport(SongImport): if not number_found: verse_type += '1' break + # If the verse only consist of the tag-line, add an empty line to create an empty slide + if first_line_is_tag and len(verse_split) == 1: + verse_split.append("") self.add_verse(verse_split[-1].strip() if first_line_is_tag else verse, verse_type) if len(self.comments) > 5: self.comments += str(translate('SongsPlugin.EasyWorshipSongImport', From faa434d937072b238d8d06d3f657d7f9fc6a58af Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sun, 13 Mar 2016 19:37:08 +0100 Subject: [PATCH 044/110] pep8 fixes --- openlp/plugins/songs/lib/mediaitem.py | 4 ++-- tests/functional/openlp_plugins/songs/test_mediaitem.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 9dd4e29f8..8772f0771 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -295,7 +295,7 @@ class SongMediaItem(MediaManagerItem): :param search_keywords: A list of search keywords - book first, then number :return: None """ - + log.debug('display results Book') self.list_view.clear() @@ -700,7 +700,7 @@ class SongMediaItem(MediaManagerItem): :param s: A string value from the list we want to sort. """ return [int(text) if text.isdecimal() else text.lower() - for text in re.split('(\d+)', s)] + for text in re.split('(\d+)', s)] def search(self, string, show_error): """ diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index d09f5b76e..4ae15909b 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -422,10 +422,10 @@ class TestMediaItem(TestCase, TestMixin): """ # GIVEN: A string to be converted into a sort key string_sort_key = 'A1B12C' - + # WHEN: We attempt to create a sort key sort_key_result = self.media_item._natural_sort_key(string_sort_key) - + # THEN: We should get back a tuple split on integers self.assertEqual(sort_key_result, ['a', 1, 'b', 12, 'c']) From e908799240565564937372fdbce900ad6dbcb1c1 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sun, 13 Mar 2016 22:55:47 +0100 Subject: [PATCH 045/110] Fix slide order change when splitting custom slides. Fixes bug 1554748. Fixes: https://launchpad.net/bugs/1554748 --- openlp/plugins/custom/forms/editcustomform.py | 1 + .../custom/forms/test_customform.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/openlp/plugins/custom/forms/editcustomform.py b/openlp/plugins/custom/forms/editcustomform.py index 3aaee5290..b639c2692 100644 --- a/openlp/plugins/custom/forms/editcustomform.py +++ b/openlp/plugins/custom/forms/editcustomform.py @@ -198,6 +198,7 @@ class EditCustomForm(QtWidgets.QDialog, Ui_CustomEditDialog): # Insert all slides to make the old_slides list complete. for slide in slides: old_slides.insert(old_row, slide) + old_row += 1 self.slide_list_view.addItems(old_slides) self.slide_list_view.repaint() diff --git a/tests/interfaces/openlp_plugins/custom/forms/test_customform.py b/tests/interfaces/openlp_plugins/custom/forms/test_customform.py index b19f17fe7..333f03896 100644 --- a/tests/interfaces/openlp_plugins/custom/forms/test_customform.py +++ b/tests/interfaces/openlp_plugins/custom/forms/test_customform.py @@ -128,3 +128,19 @@ class TestEditCustomForm(TestCase, TestMixin): # THEN: The validate method should have returned False. assert not result, 'The _validate() method should have retured False' mocked_critical_error_message_box.assert_called_with(message='You need to add at least one slide.') + + def update_slide_list_test(self): + """ + Test the update_slide_list() method + """ + # GIVEN: Mocked slide_list_view with a slide with 3 lines + self.form.slide_list_view = MagicMock() + self.form.slide_list_view.count.return_value = 1 + self.form.slide_list_view.currentRow.return_value = 0 + self.form.slide_list_view.item.return_value = MagicMock(return_value='1st Slide\n2nd Slide\n3rd Slide') + + # WHEN: updating the slide by splitting the lines into slides + self.form.update_slide_list(['1st Slide', '2nd Slide', '3rd Slide']) + + # THEN: The slides should be created in correct order + self.form.slide_list_view.addItems.assert_called_with(['1st Slide', '2nd Slide', '3rd Slide']) From 302fcb221b377d77f66cc374b24e79601e82faf4 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Wed, 16 Mar 2016 22:28:29 +0100 Subject: [PATCH 046/110] Added another OPS Pro import test --- openlp/plugins/songs/lib/importers/opspro.py | 2 +- .../openlp_plugins/songs/test_opsproimport.py | 50 +++++++++++++------ .../resources/opsprosongs/Amazing Grace.json | 21 ++++++++ tests/resources/opsprosongs/amazing grace.txt | 24 +++++++++ 4 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 tests/resources/opsprosongs/Amazing Grace.json create mode 100644 tests/resources/opsprosongs/amazing grace.txt diff --git a/openlp/plugins/songs/lib/importers/opspro.py b/openlp/plugins/songs/lib/importers/opspro.py index 56463d093..85d58f7c9 100644 --- a/openlp/plugins/songs/lib/importers/opspro.py +++ b/openlp/plugins/songs/lib/importers/opspro.py @@ -130,7 +130,7 @@ class OpsProImport(SongImport): tag = tag_match.group(1).lower() tag = tag.split(' ')[0] verse_text = tag_match.group(2) - if 'refrein' in tag: + if 'refrein' in tag or 'chorus' in tag: verse_def = 'c' elif 'bridge' in tag: verse_def = 'b' diff --git a/tests/functional/openlp_plugins/songs/test_opsproimport.py b/tests/functional/openlp_plugins/songs/test_opsproimport.py index 67b7c5959..b3501f2bf 100644 --- a/tests/functional/openlp_plugins/songs/test_opsproimport.py +++ b/tests/functional/openlp_plugins/songs/test_opsproimport.py @@ -77,21 +77,8 @@ class TestOpsProSongImport(TestCase): # GIVEN: A mocked out SongImport class, a mocked out "manager" and a mocked song and lyrics entry mocked_manager = MagicMock() importer = OpsProImport(mocked_manager, filenames=[]) - song = MagicMock() - song.ID = 100 - song.SongNumber = 123 - song.SongBookName = 'The Song Book' - song.Title = 'Song Title' - song.CopyrightText = 'Music and text by me' - song.Version = '1' - song.Origin = '...' - lyrics = MagicMock() - test_file = open(os.path.join(TEST_PATH, 'you are so faithfull.txt'), 'rb') - lyrics.Lyrics = test_file.read().decode() - lyrics.Type = 1 - lyrics.IsDualLanguage = True importer.finish = MagicMock() - + song, lyrics = self._build_test_data('you are so faithfull.txt') # WHEN: An importer object is created importer.process_song(song, lyrics, []) @@ -101,7 +88,42 @@ class TestOpsProSongImport(TestCase): self.assertListEqual(importer.verses, self._get_data(result_data, 'verses')) self.assertListEqual(importer.verse_order_list_generated, self._get_data(result_data, 'verse_order_list')) + @patch('openlp.plugins.songs.lib.importers.opspro.SongImport') + def join_and_split_test(self, mocked_songimport): + """ + Test importing lyrics with a split and join tags works in OPS Pro + """ + # GIVEN: A mocked out SongImport class, a mocked out "manager" and a mocked song and lyrics entry + mocked_manager = MagicMock() + importer = OpsProImport(mocked_manager, filenames=[]) + importer.finish = MagicMock() + song, lyrics = self._build_test_data('amazing grace.txt') + # WHEN: An importer object is created + importer.process_song(song, lyrics, []) + + # THEN: The imported data should look like expected + result_file = open(os.path.join(TEST_PATH, 'Amazing Grace.json'), 'rb') + result_data = json.loads(result_file.read().decode()) + self.assertListEqual(importer.verses, self._get_data(result_data, 'verses')) + self.assertListEqual(importer.verse_order_list_generated, self._get_data(result_data, 'verse_order_list')) + def _get_data(self, data, key): if key in data: return data[key] return '' + + def _build_test_data(self, test_file): + song = MagicMock() + song.ID = 100 + song.SongNumber = 123 + song.SongBookName = 'The Song Book' + song.Title = 'Song Title' + song.CopyrightText = 'Music and text by me' + song.Version = '1' + song.Origin = '...' + lyrics = MagicMock() + test_file = open(os.path.join(TEST_PATH, test_file), 'rb') + lyrics.Lyrics = test_file.read().decode() + lyrics.Type = 1 + lyrics.IsDualLanguage = True + return song, lyrics diff --git a/tests/resources/opsprosongs/Amazing Grace.json b/tests/resources/opsprosongs/Amazing Grace.json new file mode 100644 index 000000000..9d6df40da --- /dev/null +++ b/tests/resources/opsprosongs/Amazing Grace.json @@ -0,0 +1,21 @@ +{ + "title": "Amazing Grace", + "verse_order_list": ["v1", "v2", "v3"], + "verses": [ + [ + "v1", + "Amazing grace! How sweet the sound!\r\nThat saved a wretch like me!\r\nI once was lost, but now am found;\r\nWas blind, but now I see.\r\n\r\n'Twas grace that taught my heart to fear,\r\nAnd grace my fears relieved.\r\nHow precious did that grace appear,\r\nThe hour I first believed.", + null + ], + [ + "v2", + "The Lord has promised good to me,\r\nHis Word my hope secures.\r\nHe will my shield and portion be\r\nAs long as life endures.", + null + ], + [ + "v3", + "Thro' many dangers, toils and snares\r\nI have already come.\r\n'Tis grace that brought me safe thus far,\r\nAnd grace will lead me home.\r\n\r\n[---]\r\nWhen we've been there ten thousand years,\r\nBright shining as the sun,\r\nWe've no less days to sing God's praise,\r\nThan when we first begun.", + null + ] + ] +} diff --git a/tests/resources/opsprosongs/amazing grace.txt b/tests/resources/opsprosongs/amazing grace.txt new file mode 100644 index 000000000..d12466e85 --- /dev/null +++ b/tests/resources/opsprosongs/amazing grace.txt @@ -0,0 +1,24 @@ +Amazing grace! How sweet the sound! +That saved a wretch like me! +I once was lost, but now am found; +Was blind, but now I see. +[join] +'Twas grace that taught my heart to fear, +And grace my fears relieved. +How precious did that grace appear, +The hour I first believed. + +The Lord has promised good to me, +His Word my hope secures. +He will my shield and portion be +As long as life endures. + +Thro' many dangers, toils and snares +I have already come. +'Tis grace that brought me safe thus far, +And grace will lead me home. +[split] +When we've been there ten thousand years, +Bright shining as the sun, +We've no less days to sing God's praise, +Than when we first begun. From fdc22b4e4cb57e35fefba48f9797951fa8db06cf Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Fri, 18 Mar 2016 23:09:49 +0100 Subject: [PATCH 047/110] Add translations support --- openlp/plugins/songs/lib/importers/opspro.py | 63 ++++++++++++++----- .../openlp_plugins/songs/test_opsproimport.py | 27 ++++++-- .../resources/opsprosongs/amazing grace2.txt | 29 +++++++++ 3 files changed, 101 insertions(+), 18 deletions(-) create mode 100644 tests/resources/opsprosongs/amazing grace2.txt diff --git a/openlp/plugins/songs/lib/importers/opspro.py b/openlp/plugins/songs/lib/importers/opspro.py index 85d58f7c9..69b864116 100644 --- a/openlp/plugins/songs/lib/importers/opspro.py +++ b/openlp/plugins/songs/lib/importers/opspro.py @@ -75,7 +75,6 @@ class OpsProImport(SongImport): 'ORDER BY CategoryName' % song.ID) topics = cursor.fetchall() self.process_song(song, lyrics, topics) - break def process_song(self, song, lyrics, topics): """ @@ -105,21 +104,16 @@ class OpsProImport(SongImport): for topic in topics: self.topics.append(topic.CategoryName) # Try to split lyrics based on various rules - print(song.ID) if lyrics: lyrics_text = lyrics.Lyrics - # Remove whitespaces around the join-tag to keep verses joint - lyrics_text = re.sub('\s*\[join\]\s*', '[join]', lyrics_text, flags=re.IGNORECASE) - lyrics_text = re.sub('\s*\[splits?\]\s*', '[split]', lyrics_text, flags=re.IGNORECASE) verses = re.split('\r\n\s*?\r\n', lyrics_text) verse_tag_defs = {} verse_tag_texts = {} - chorus = '' for verse_text in verses: if verse_text.strip() == '': continue verse_def = 'v' - # Try to detect verse number + # Detect verse number verse_number = re.match('^(\d+)\r\n', verse_text) if verse_number: verse_text = re.sub('^\d+\r\n', '', verse_text) @@ -143,19 +137,60 @@ class OpsProImport(SongImport): if tag in verse_tag_defs: verse_text = verse_tag_texts[tag] verse_def = verse_tag_defs[tag] - # Try to detect end tag + # Detect end tag elif re.match('^\[slot\]\r\n', verse_text, re.IGNORECASE): verse_def = 'e' verse_text = re.sub('^\[slot\]\r\n', '', verse_text, flags=re.IGNORECASE) - # Handle tags # Replace the join tag with line breaks - verse_text = re.sub('\[join\]', '\r\n\r\n', verse_text) + verse_text = re.sub('\[join\]', '', verse_text) # Replace the split tag with line breaks and an optional split - verse_text = re.sub('\[split\]', '\r\n\r\n[---]\r\n', verse_text) + verse_text = re.sub('\[split\]', '\r\n[---]', verse_text) # Handle translations - #if lyrics.IsDualLanguage: - # ... - + if lyrics.IsDualLanguage: + language = None + translation = True + translation_verse_text = '' + start_tag = '{translation}' + end_tag = '{/translation}' + verse_text_lines = verse_text.splitlines() + idx = 0 + while idx < len(verse_text_lines): + # Detect if translation is turned on or off + if verse_text_lines[idx] in ['[trans off]', '[vertaal uit]']: + translation = False + idx += 1 + elif verse_text_lines[idx] in ['[trans on]', '[vertaal aan]']: + translation = True + idx += 1 + elif verse_text_lines[idx] == '[taal a]': + language = 'a' + idx += 1 + elif verse_text_lines[idx] == '[taal b]': + language = 'b' + idx += 1 + # Handle the text based on whether translation is off or on + if language: + translation_verse_text += verse_text_lines[idx] + '\r\n' + idx += 1 + while idx < len(verse_text_lines) and not verse_text_lines[idx].startswith('['): + if language == 'a': + translation_verse_text += verse_text_lines[idx] + '\r\n' + else: + translation_verse_text += start_tag + verse_text_lines[idx] + end_tag + '\r\n' + idx += 1 + language = None + elif translation: + translation_verse_text += verse_text_lines[idx] + '\r\n' + idx += 1 + translation_verse_text += start_tag + verse_text_lines[idx] + end_tag + '\r\n' + idx += 1 + else: + translation_verse_text += verse_text_lines[idx] + '\r\n' + idx += 1 + while idx < len(verse_text_lines) and not verse_text_lines[idx].startswith('['): + translation_verse_text += verse_text_lines[idx] + '\r\n' + idx += 1 + verse_text = translation_verse_text # Remove comments verse_text = re.sub('\(.*?\)\r\n', '', verse_text, flags=re.IGNORECASE) self.add_verse(verse_text, verse_def) diff --git a/tests/functional/openlp_plugins/songs/test_opsproimport.py b/tests/functional/openlp_plugins/songs/test_opsproimport.py index b3501f2bf..e4ce742b7 100644 --- a/tests/functional/openlp_plugins/songs/test_opsproimport.py +++ b/tests/functional/openlp_plugins/songs/test_opsproimport.py @@ -78,7 +78,7 @@ class TestOpsProSongImport(TestCase): mocked_manager = MagicMock() importer = OpsProImport(mocked_manager, filenames=[]) importer.finish = MagicMock() - song, lyrics = self._build_test_data('you are so faithfull.txt') + song, lyrics = self._build_test_data('you are so faithfull.txt', False) # WHEN: An importer object is created importer.process_song(song, lyrics, []) @@ -97,7 +97,26 @@ class TestOpsProSongImport(TestCase): mocked_manager = MagicMock() importer = OpsProImport(mocked_manager, filenames=[]) importer.finish = MagicMock() - song, lyrics = self._build_test_data('amazing grace.txt') + song, lyrics = self._build_test_data('amazing grace.txt', False) + # WHEN: An importer object is created + importer.process_song(song, lyrics, []) + + # THEN: The imported data should look like expected + result_file = open(os.path.join(TEST_PATH, 'Amazing Grace.json'), 'rb') + result_data = json.loads(result_file.read().decode()) + self.assertListEqual(importer.verses, self._get_data(result_data, 'verses')) + self.assertListEqual(importer.verse_order_list_generated, self._get_data(result_data, 'verse_order_list')) + + @patch('openlp.plugins.songs.lib.importers.opspro.SongImport') + def trans_off_tag_test(self, mocked_songimport): + """ + Test importing lyrics with a split and join and translations tags works in OPS Pro + """ + # GIVEN: A mocked out SongImport class, a mocked out "manager" and a mocked song and lyrics entry + mocked_manager = MagicMock() + importer = OpsProImport(mocked_manager, filenames=[]) + importer.finish = MagicMock() + song, lyrics = self._build_test_data('amazing grace2.txt', True) # WHEN: An importer object is created importer.process_song(song, lyrics, []) @@ -112,7 +131,7 @@ class TestOpsProSongImport(TestCase): return data[key] return '' - def _build_test_data(self, test_file): + def _build_test_data(self, test_file, dual_language): song = MagicMock() song.ID = 100 song.SongNumber = 123 @@ -125,5 +144,5 @@ class TestOpsProSongImport(TestCase): test_file = open(os.path.join(TEST_PATH, test_file), 'rb') lyrics.Lyrics = test_file.read().decode() lyrics.Type = 1 - lyrics.IsDualLanguage = True + lyrics.IsDualLanguage = dual_language return song, lyrics diff --git a/tests/resources/opsprosongs/amazing grace2.txt b/tests/resources/opsprosongs/amazing grace2.txt new file mode 100644 index 000000000..1e18a6b62 --- /dev/null +++ b/tests/resources/opsprosongs/amazing grace2.txt @@ -0,0 +1,29 @@ +[trans off] +Amazing grace! How sweet the sound! +That saved a wretch like me! +I once was lost, but now am found; +Was blind, but now I see. +[join] +[trans off] +'Twas grace that taught my heart to fear, +And grace my fears relieved. +How precious did that grace appear, +The hour I first believed. + +[trans off] +The Lord has promised good to me, +His Word my hope secures. +He will my shield and portion be +As long as life endures. + +[trans off] +Thro' many dangers, toils and snares +I have already come. +'Tis grace that brought me safe thus far, +And grace will lead me home. +[trans off] +[split] +When we've been there ten thousand years, +Bright shining as the sun, +We've no less days to sing God's praise, +Than when we first begun. From 7b69634552c89fa8168235df3081235446adfae2 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sat, 19 Mar 2016 07:20:12 +0100 Subject: [PATCH 048/110] Fixes for translation support + test --- openlp/plugins/songs/lib/importers/opspro.py | 18 +++++------ .../openlp_plugins/songs/test_opsproimport.py | 23 ++++++++++++++ .../resources/opsprosongs/Amazing Grace3.json | 31 +++++++++++++++++++ 3 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 tests/resources/opsprosongs/Amazing Grace3.json diff --git a/openlp/plugins/songs/lib/importers/opspro.py b/openlp/plugins/songs/lib/importers/opspro.py index 69b864116..2395fc3f8 100644 --- a/openlp/plugins/songs/lib/importers/opspro.py +++ b/openlp/plugins/songs/lib/importers/opspro.py @@ -170,20 +170,20 @@ class OpsProImport(SongImport): idx += 1 # Handle the text based on whether translation is off or on if language: - translation_verse_text += verse_text_lines[idx] + '\r\n' - idx += 1 + if language == 'b': + translation_verse_text += start_tag while idx < len(verse_text_lines) and not verse_text_lines[idx].startswith('['): - if language == 'a': - translation_verse_text += verse_text_lines[idx] + '\r\n' - else: - translation_verse_text += start_tag + verse_text_lines[idx] + end_tag + '\r\n' + translation_verse_text += verse_text_lines[idx] + '\r\n' idx += 1 + if language == 'b': + translation_verse_text += end_tag language = None elif translation: translation_verse_text += verse_text_lines[idx] + '\r\n' idx += 1 - translation_verse_text += start_tag + verse_text_lines[idx] + end_tag + '\r\n' - idx += 1 + if idx < len(verse_text_lines) and not verse_text_lines[idx].startswith('['): + translation_verse_text += start_tag + verse_text_lines[idx] + end_tag + '\r\n' + idx += 1 else: translation_verse_text += verse_text_lines[idx] + '\r\n' idx += 1 @@ -194,8 +194,6 @@ class OpsProImport(SongImport): # Remove comments verse_text = re.sub('\(.*?\)\r\n', '', verse_text, flags=re.IGNORECASE) self.add_verse(verse_text, verse_def) - #print(verse_def) - #print(verse_text) self.finish() def extract_mdb_password(self): diff --git a/tests/functional/openlp_plugins/songs/test_opsproimport.py b/tests/functional/openlp_plugins/songs/test_opsproimport.py index e4ce742b7..b2f9371dc 100644 --- a/tests/functional/openlp_plugins/songs/test_opsproimport.py +++ b/tests/functional/openlp_plugins/songs/test_opsproimport.py @@ -79,6 +79,7 @@ class TestOpsProSongImport(TestCase): importer = OpsProImport(mocked_manager, filenames=[]) importer.finish = MagicMock() song, lyrics = self._build_test_data('you are so faithfull.txt', False) + # WHEN: An importer object is created importer.process_song(song, lyrics, []) @@ -98,6 +99,7 @@ class TestOpsProSongImport(TestCase): importer = OpsProImport(mocked_manager, filenames=[]) importer.finish = MagicMock() song, lyrics = self._build_test_data('amazing grace.txt', False) + # WHEN: An importer object is created importer.process_song(song, lyrics, []) @@ -117,6 +119,7 @@ class TestOpsProSongImport(TestCase): importer = OpsProImport(mocked_manager, filenames=[]) importer.finish = MagicMock() song, lyrics = self._build_test_data('amazing grace2.txt', True) + # WHEN: An importer object is created importer.process_song(song, lyrics, []) @@ -126,6 +129,26 @@ class TestOpsProSongImport(TestCase): self.assertListEqual(importer.verses, self._get_data(result_data, 'verses')) self.assertListEqual(importer.verse_order_list_generated, self._get_data(result_data, 'verse_order_list')) + @patch('openlp.plugins.songs.lib.importers.opspro.SongImport') + def trans_tag_test(self, mocked_songimport): + """ + Test importing lyrics with various translations tags works in OPS Pro + """ + # GIVEN: A mocked out SongImport class, a mocked out "manager" and a mocked song and lyrics entry + mocked_manager = MagicMock() + importer = OpsProImport(mocked_manager, filenames=[]) + importer.finish = MagicMock() + song, lyrics = self._build_test_data('amazing grace3.txt', True) + + # WHEN: An importer object is created + importer.process_song(song, lyrics, []) + + # THEN: The imported data should look like expected + result_file = open(os.path.join(TEST_PATH, 'Amazing Grace3.json'), 'rb') + result_data = json.loads(result_file.read().decode()) + self.assertListEqual(importer.verses, self._get_data(result_data, 'verses')) + self.assertListEqual(importer.verse_order_list_generated, self._get_data(result_data, 'verse_order_list')) + def _get_data(self, data, key): if key in data: return data[key] diff --git a/tests/resources/opsprosongs/Amazing Grace3.json b/tests/resources/opsprosongs/Amazing Grace3.json new file mode 100644 index 000000000..d9ce5cc45 --- /dev/null +++ b/tests/resources/opsprosongs/Amazing Grace3.json @@ -0,0 +1,31 @@ +{ + "title": "Amazing Grace", + "verse_order_list": ["v1", "v2", "v3", "v4", "v5"], + "verses": [ + [ + "v1", + "Amazing grace! How sweet the sound!\r\n{translation}That saved a wretch like me!{/translation}\r\nI once was lost, but now am found;\r\n{translation}Was blind, but now I see.{/translation}", + null + ], + [ + "v2", + "'Twas grace that taught my heart to fear,\r\nAnd grace my fears relieved.\r\n{translation}How precious did that grace appear,\r\nThe hour I first believed.\r\n{/translation}", + null + ], + [ + "v3", + "The Lord has promised good to me,\r\nHis Word my hope secures.\r\nHe will my shield and portion be\r\n{translation}As long as life endures.{/translation}", + null + ], + [ + "v4", + "Thro' many dangers, toils and snares\r\nI have already come.\r\n'Tis grace that brought me safe thus far,\r\n{translation}And grace will lead me home.{/translation}", + null + ], + [ + "v5", + "[end]\r\n{translation}When we've been there ten thousand years,{/translation}\r\nBright shining as the sun,\r\n{translation}We've no less days to sing God's praise,{/translation}\r\nThan when we first begun.", + null + ] + ] +} From 79b4c474d613c101ba125b5413002e2e299b8619 Mon Sep 17 00:00:00 2001 From: Ian Knight Date: Sat, 19 Mar 2016 19:10:11 +1030 Subject: [PATCH 049/110] Added testing --- .../openlp_core_ui/test_listpreviewwidget.py | 263 +++++++++++++++++- 1 file changed, 258 insertions(+), 5 deletions(-) diff --git a/tests/functional/openlp_core_ui/test_listpreviewwidget.py b/tests/functional/openlp_core_ui/test_listpreviewwidget.py index 5d1135e23..e4cd334d4 100644 --- a/tests/functional/openlp_core_ui/test_listpreviewwidget.py +++ b/tests/functional/openlp_core_ui/test_listpreviewwidget.py @@ -26,19 +26,23 @@ from unittest import TestCase from openlp.core.common import Settings from openlp.core.ui.listpreviewwidget import ListPreviewWidget +from openlp.core.lib import ServiceItem -from tests.functional import MagicMock, patch +from tests.functional import MagicMock, patch, call class TestListPreviewWidget(TestCase): + def setUp(self): """ Mock out stuff for all the tests """ - self.setup_patcher = patch('openlp.core.ui.listpreviewwidget.ListPreviewWidget._setup') - self.mocked_setup = self.setup_patcher.start() - self.addCleanup(self.setup_patcher.stop) + self.parent_patcher = patch('openlp.core.ui.listpreviewwidget.ListPreviewWidget.parent') + self.mocked_parent = self.parent_patcher.start() + self.mocked_parent.width.return_value = 100 + self.addCleanup(self.parent_patcher.stop) + def new_list_preview_widget_test(self): """ @@ -51,4 +55,253 @@ class TestListPreviewWidget(TestCase): # THEN: The object is not None, and the _setup() method was called. self.assertIsNotNone(list_preview_widget, 'The ListPreviewWidget object should not be None') - self.mocked_setup.assert_called_with(1) + self.assertEquals(list_preview_widget.screen_ratio, 1, 'Should not be called') + #self.mocked_setup.assert_called_with(1) + + + @patch(u'openlp.core.ui.listpreviewwidget.Settings') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') + def replace_recalculate_layout_test_text(self, mocked_setRowHeight, mocked_resizeRowsToContents, + mocked_viewport, mocked_Settings): + """ + Test if "Max height for non-text slides in slide controller" enabled, text-based slides not affected in replace_service_item and __recalculate_layout. + """ + # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", + # a text ServiceItem and a ListPreviewWidget. + + # Mock Settings().value('advanced/slide max height') + mocked_Settings_obj = MagicMock() + mocked_Settings_obj.value.return_value = 100 + mocked_Settings.return_value = mocked_Settings_obj + # Mock self.viewport().width() + mocked_viewport_obj = MagicMock() + mocked_viewport_obj.width.return_value = 200 + mocked_viewport.return_value = mocked_viewport_obj + # Mock text service item + service_item = MagicMock() + service_item.is_text.return_value = True + service_item.get_frames.return_value = [{'title': None, 'text': None, 'verseTag': None}, + {'title': None, 'text': None, 'verseTag': None}] + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + list_preview_widget.replace_service_item(service_item, 200, 0) + # Change viewport width before forcing a resize + mocked_viewport_obj.width.return_value = 400 + + # WHEN: __recalculate_layout() is called (via resizeEvent) + list_preview_widget.resizeEvent(None) + + # THEN: resizeRowsToContents should be called twice + # (once each in __recalculate_layout and replace_service_item) + self.assertEquals(mocked_resizeRowsToContents.call_count, 2, 'Should be called') + self.assertEquals(mocked_setRowHeight.call_count, 0, 'Should not be called') + + + @patch(u'openlp.core.ui.listpreviewwidget.Settings') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') + def replace_recalculate_layout_test_img(self, mocked_setRowHeight, mocked_resizeRowsToContents, + mocked_viewport, mocked_Settings): + """ + Test if "Max height for non-text slides in slide controller" disabled, image-based slides not resized to the max-height in replace_service_item and __recalculate_layout. + """ + # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", + # an image ServiceItem and a ListPreviewWidget. + + # Mock Settings().value('advanced/slide max height') + mocked_Settings_obj = MagicMock() + mocked_Settings_obj.value.return_value = 0 + mocked_Settings.return_value = mocked_Settings_obj + # Mock self.viewport().width() + mocked_viewport_obj = MagicMock() + mocked_viewport_obj.width.return_value = 200 + mocked_viewport.return_value = mocked_viewport_obj + # Mock image service item + service_item = MagicMock() + service_item.is_text.return_value = False + service_item.get_frames.return_value = [{'title': None, 'path': None, 'image': None}, + {'title': None, 'path': None, 'image': None}] + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + list_preview_widget.replace_service_item(service_item, 200, 0) + # Change viewport width before forcing a resize + mocked_viewport_obj.width.return_value = 400 + + # WHEN: __recalculate_layout() is called (via resizeEvent) + list_preview_widget.resizeEvent(None) + + # THEN: timer should have been started + self.assertEquals(mocked_resizeRowsToContents.call_count, 0, 'Should not be called') + self.assertEquals(mocked_setRowHeight.call_count, 4, 'Should be called twice for each slide') + calls = [call(0,200), call(1,200),call(0,400), call(1,400)] + mocked_setRowHeight.assert_has_calls(calls) + + + @patch(u'openlp.core.ui.listpreviewwidget.Settings') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') + def replace_recalculate_layout_test_img_max(self, mocked_setRowHeight, mocked_resizeRowsToContents, + mocked_viewport, mocked_Settings): + """ + Test if "Max height for non-text slides in slide controller" enabled, image-based slides are resized to the max-height in replace_service_item and __recalculate_layout. + """ + + # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", + # an image ServiceItem and a ListPreviewWidget. + # Mock Settings().value('advanced/slide max height') + mocked_Settings_obj = MagicMock() + mocked_Settings_obj.value.return_value = 100 + mocked_Settings.return_value = mocked_Settings_obj + # Mock self.viewport().width() + mocked_viewport_obj = MagicMock() + mocked_viewport_obj.width.return_value = 200 + mocked_viewport.return_value = mocked_viewport_obj + # Mock image service item + service_item = MagicMock() + service_item.is_text.return_value = False + service_item.get_frames.return_value = [{'title': None, 'path': None, 'image': None}, + {'title': None, 'path': None, 'image': None}] + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + list_preview_widget.replace_service_item(service_item, 200, 0) + # Change viewport width before forcing a resize + mocked_viewport_obj.width.return_value = 400 + + # WHEN: __recalculate_layout() is called (via resizeEvent) + list_preview_widget.resizeEvent(None) + + # THEN: timer should have been started + self.assertEquals(mocked_resizeRowsToContents.call_count, 0, 'Should not be called') + self.assertEquals(mocked_setRowHeight.call_count, 4, 'Should be called twice for each slide') + calls = [call(0,100), call(1,100),call(0,100), call(1,100)] + mocked_setRowHeight.assert_has_calls(calls) + + + @patch(u'openlp.core.ui.listpreviewwidget.Settings') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget') + def row_resized_test_text(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents, + mocked_viewport, mocked_Settings): + """ + Test if "Max height for non-text slides in slide controller" enabled, text-based slides not affected in row_resized. + """ + # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", + # a text ServiceItem and a ListPreviewWidget. + + # Mock Settings().value('advanced/slide max height') + mocked_Settings_obj = MagicMock() + mocked_Settings_obj.value.return_value = 100 + mocked_Settings.return_value = mocked_Settings_obj + # Mock self.viewport().width() + mocked_viewport_obj = MagicMock() + mocked_viewport_obj.width.return_value = 200 + mocked_viewport.return_value = mocked_viewport_obj + # Mock text service item + service_item = MagicMock() + service_item.is_text.return_value = True + service_item.get_frames.return_value = [{'title': None, 'text': None, 'verseTag': None}, + {'title': None, 'text': None, 'verseTag': None}] + # Mock self.cellWidget().children().setMaximumWidth() + mocked_cellWidget_child = MagicMock() + mocked_cellWidget_obj = MagicMock() + mocked_cellWidget_obj.children.return_value = [None,mocked_cellWidget_child] + mocked_cellWidget.return_value = mocked_cellWidget_obj + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + list_preview_widget.replace_service_item(service_item, 200, 0) + + # WHEN: row_resized() is called + list_preview_widget.row_resized(0, 100, 150) + + # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should not be called + self.assertEquals(mocked_cellWidget_child.setMaximumWidth.call_count, 0, 'Should not be called') + + + @patch(u'openlp.core.ui.listpreviewwidget.Settings') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget') + def row_resized_test_img(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents, + mocked_viewport, mocked_Settings): + """ + Test if "Max height for non-text slides in slide controller" disabled, image-based slides not affected in row_resized. + """ + # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", + # an image ServiceItem and a ListPreviewWidget. + + # Mock Settings().value('advanced/slide max height') + mocked_Settings_obj = MagicMock() + mocked_Settings_obj.value.return_value = 0 + mocked_Settings.return_value = mocked_Settings_obj + # Mock self.viewport().width() + mocked_viewport_obj = MagicMock() + mocked_viewport_obj.width.return_value = 200 + mocked_viewport.return_value = mocked_viewport_obj + # Mock image service item + service_item = MagicMock() + service_item.is_text.return_value = False + service_item.get_frames.return_value = [{'title': None, 'path': None, 'image': None}, + {'title': None, 'path': None, 'image': None}] + # Mock self.cellWidget().children().setMaximumWidth() + mocked_cellWidget_child = MagicMock() + mocked_cellWidget_obj = MagicMock() + mocked_cellWidget_obj.children.return_value = [None,mocked_cellWidget_child] + mocked_cellWidget.return_value = mocked_cellWidget_obj + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + list_preview_widget.replace_service_item(service_item, 200, 0) + + # WHEN: row_resized() is called + list_preview_widget.row_resized(0, 100, 150) + + # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should not be called + self.assertEquals(mocked_cellWidget_child.setMaximumWidth.call_count, 0, 'Should not be called') + + + @patch(u'openlp.core.ui.listpreviewwidget.Settings') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.viewport') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.setRowHeight') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.cellWidget') + def row_resized_test_img_max(self, mocked_cellWidget, mocked_setRowHeight, mocked_resizeRowsToContents, + mocked_viewport, mocked_Settings): + """ + Test if "Max height for non-text slides in slide controller" enabled, image-based slides are scaled in row_resized. + """ + # GIVEN: A setting to adjust "Max height for non-text slides in slide controller", + # an image ServiceItem and a ListPreviewWidget. + + # Mock Settings().value('advanced/slide max height') + mocked_Settings_obj = MagicMock() + mocked_Settings_obj.value.return_value = 100 + mocked_Settings.return_value = mocked_Settings_obj + # Mock self.viewport().width() + mocked_viewport_obj = MagicMock() + mocked_viewport_obj.width.return_value = 200 + mocked_viewport.return_value = mocked_viewport_obj + # Mock image service item + service_item = MagicMock() + service_item.is_text.return_value = False + service_item.get_frames.return_value = [{'title': None, 'path': None, 'image': None}, + {'title': None, 'path': None, 'image': None}] + # Mock self.cellWidget().children().setMaximumWidth() + mocked_cellWidget_child = MagicMock() + mocked_cellWidget_obj = MagicMock() + mocked_cellWidget_obj.children.return_value = [None,mocked_cellWidget_child] + mocked_cellWidget.return_value = mocked_cellWidget_obj + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + list_preview_widget.replace_service_item(service_item, 200, 0) + + # WHEN: row_resized() is called + list_preview_widget.row_resized(0, 100, 150) + + # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should be called + mocked_cellWidget_child.setMaximumWidth.assert_called_once_with(150) From bb0adc6f5dc1afdb3a15e69ea8682f8f63675423 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 19 Mar 2016 15:01:10 +0000 Subject: [PATCH 050/110] fixed bug #1280295 'Enable natural sorting for song book searches', refactored to move filtering to database, updated test Fixes: https://launchpad.net/bugs/1280295 --- openlp/plugins/songs/lib/mediaitem.py | 30 +++++++---------- .../openlp_plugins/songs/test_mediaitem.py | 32 +++++++++++++++++++ 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 12579c56d..12ef3de3d 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -203,7 +203,13 @@ class SongMediaItem(MediaManagerItem): self.display_results_topic(search_results) elif search_type == SongSearch.Books: log.debug('Songbook Search') - self.display_results_book(search_keywords) + search_keywords = search_keywords.rpartition(' ') + search_book = search_keywords[0] + '%' + search_entry = search_keywords[2] + '%' + search_results = (self.plugin.manager.session.query(SongBookEntry) + .join(Book) + .filter(Book.name.like(search_book), SongBookEntry.entry.like(search_entry)).all()) + self.display_results_book(search_results) elif search_type == SongSearch.Themes: log.debug('Theme Search') search_string = '%' + search_keywords + '%' @@ -288,31 +294,19 @@ class SongMediaItem(MediaManagerItem): song_name.setData(QtCore.Qt.UserRole, song.id) self.list_view.addItem(song_name) - def display_results_book(self, search_keywords): + def display_results_book(self, search_results): """ - Display the song search results in the media manager list, grouped by book + Display the song search results in the media manager list, grouped by book and entry - :param search_keywords: A list of search keywords - book first, then number + :param search_results: A list of db SongBookEntry objects :return: None """ - log.debug('display results Book') self.list_view.clear() - - search_keywords = search_keywords.rpartition(' ') - search_book = search_keywords[0] - search_entry = re.sub(r'[^0-9]', '', search_keywords[2]) - - songbook_entries = (self.plugin.manager.session.query(SongBookEntry) - .join(Book)) - songbook_entries = sorted(songbook_entries, key=lambda songbook_entry: (songbook_entry.songbook.name, self._natural_sort_key(songbook_entry.entry))) - for songbook_entry in songbook_entries: + search_results = sorted(search_results, key=lambda songbook_entry: (songbook_entry.songbook.name, self._natural_sort_key(songbook_entry.entry))) + for songbook_entry in search_results: if songbook_entry.song.temporary: continue - if search_book.lower() not in songbook_entry.songbook.name.lower(): - continue - if search_entry not in songbook_entry.entry: - continue song_detail = '%s #%s: %s' % (songbook_entry.songbook.name, songbook_entry.entry, songbook_entry.song.title) song_name = QtWidgets.QListWidgetItem(song_detail) song_name.setData(QtCore.Qt.UserRole, songbook_entry.song.id) diff --git a/tests/functional/openlp_plugins/songs/test_mediaitem.py b/tests/functional/openlp_plugins/songs/test_mediaitem.py index d09f5b76e..865b81ba9 100644 --- a/tests/functional/openlp_plugins/songs/test_mediaitem.py +++ b/tests/functional/openlp_plugins/songs/test_mediaitem.py @@ -127,6 +127,38 @@ class TestMediaItem(TestCase, TestMixin): mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_song.id) self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget) + def display_results_book_test(self): + """ + Test displaying song search results grouped by book and entry with basic song + """ + # GIVEN: Search results grouped by book and entry, plus a mocked QtListWidgetItem + with patch('openlp.core.lib.QtWidgets.QListWidgetItem') as MockedQListWidgetItem, \ + patch('openlp.core.lib.QtCore.Qt.UserRole') as MockedUserRole: + mock_search_results = [] + mock_songbook_entry = MagicMock() + mock_songbook = MagicMock() + mock_song = MagicMock() + mock_songbook_entry.entry = '1' + mock_songbook.name = 'My Book' + mock_song.id = 1 + mock_song.title = 'My Song' + mock_song.sort_key = 'My Song' + mock_song.temporary = False + mock_songbook_entry.song = mock_song + mock_songbook_entry.songbook = mock_songbook + mock_search_results.append(mock_songbook_entry) + mock_qlist_widget = MagicMock() + MockedQListWidgetItem.return_value = mock_qlist_widget + + # WHEN: I display song search results grouped by book + self.media_item.display_results_book(mock_search_results) + + # THEN: The current list view is cleared, the widget is created, and the relevant attributes set + self.media_item.list_view.clear.assert_called_with() + MockedQListWidgetItem.assert_called_with('My Book #1: My Song') + mock_qlist_widget.setData.assert_called_with(MockedUserRole, mock_songbook_entry.song.id) + self.media_item.list_view.addItem.assert_called_with(mock_qlist_widget) + def display_results_topic_test(self): """ Test displaying song search results grouped by topic with basic song From 3db138ea8d1334a2aabec934c3b663260143eea8 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 19 Mar 2016 15:50:56 +0000 Subject: [PATCH 051/110] coding standards fix --- openlp/plugins/songs/lib/mediaitem.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 67dee4023..06636bcc6 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -207,8 +207,8 @@ class SongMediaItem(MediaManagerItem): search_book = search_keywords[0] + '%' search_entry = search_keywords[2] + '%' search_results = (self.plugin.manager.session.query(SongBookEntry) - .join(Book) - .filter(Book.name.like(search_book), SongBookEntry.entry.like(search_entry)).all()) + .join(Book) + .filter(Book.name.like(search_book), SongBookEntry.entry.like(search_entry)).all()) self.display_results_book(search_results) elif search_type == SongSearch.Themes: log.debug('Theme Search') @@ -303,7 +303,8 @@ class SongMediaItem(MediaManagerItem): """ log.debug('display results Book') self.list_view.clear() - search_results = sorted(search_results, key=lambda songbook_entry: (songbook_entry.songbook.name, self._natural_sort_key(songbook_entry.entry))) + search_results = sorted(search_results, key=lambda songbook_entry: ( + songbook_entry.songbook.name, self._natural_sort_key(songbook_entry.entry))) for songbook_entry in search_results: if songbook_entry.song.temporary: continue From 5e33c0508055807318d7ca9c559b718025496a09 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 19 Mar 2016 16:00:35 +0000 Subject: [PATCH 052/110] coding standards fix 2 --- openlp/plugins/songs/lib/mediaitem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 06636bcc6..fd7dfe986 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -304,7 +304,7 @@ class SongMediaItem(MediaManagerItem): log.debug('display results Book') self.list_view.clear() search_results = sorted(search_results, key=lambda songbook_entry: ( - songbook_entry.songbook.name, self._natural_sort_key(songbook_entry.entry))) + songbook_entry.songbook.name, self._natural_sort_key(songbook_entry.entry))) for songbook_entry in search_results: if songbook_entry.song.temporary: continue From 2feedf6f383c8fad2e11b4928900a43bf51ee668 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 19 Mar 2016 16:01:23 +0000 Subject: [PATCH 053/110] coding standards fix --- openlp/plugins/songs/lib/mediaitem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index fd7dfe986..e46b95fbf 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -303,8 +303,8 @@ class SongMediaItem(MediaManagerItem): """ log.debug('display results Book') self.list_view.clear() - search_results = sorted(search_results, key=lambda songbook_entry: ( - songbook_entry.songbook.name, self._natural_sort_key(songbook_entry.entry))) + search_results = sorted(search_results, key=lambda songbook_entry: (songbook_entry.songbook.name, + self._natural_sort_key(songbook_entry.entry))) for songbook_entry in search_results: if songbook_entry.song.temporary: continue From ec0379ea44130c879b3237fde3e8a456bc9da267 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sat, 19 Mar 2016 18:49:55 +0000 Subject: [PATCH 054/110] Refactor --- openlp/plugins/remotes/html/{ => assets}/jquery.js | 0 .../plugins/remotes/html/{ => assets}/jquery.min.js | 0 .../remotes/html/{ => assets}/jquery.mobile.js | 2 +- .../remotes/html/assets/jquery.mobile.min.css | 2 ++ .../plugins/remotes/html/assets/jquery.mobile.min.js | 2 ++ openlp/plugins/remotes/html/{ => css}/main.css | 0 openlp/plugins/remotes/html/{ => css}/openlp.css | 4 ++-- openlp/plugins/remotes/html/{ => css}/stage.css | 0 openlp/plugins/remotes/html/index.html | 12 ++++++------ openlp/plugins/remotes/html/jquery.mobile.min.css | 2 -- openlp/plugins/remotes/html/jquery.mobile.min.js | 2 -- openlp/plugins/remotes/html/{ => js}/main.js | 0 openlp/plugins/remotes/html/{ => js}/openlp.js | 0 openlp/plugins/remotes/html/{ => js}/stage.js | 0 openlp/plugins/remotes/html/main.html | 8 ++++---- openlp/plugins/remotes/html/stage.html | 8 ++++---- openlp/plugins/remotes/lib/httprouter.py | 8 ++++++-- 17 files changed, 27 insertions(+), 23 deletions(-) rename openlp/plugins/remotes/html/{ => assets}/jquery.js (100%) rename openlp/plugins/remotes/html/{ => assets}/jquery.min.js (100%) rename openlp/plugins/remotes/html/{ => assets}/jquery.mobile.js (99%) create mode 100644 openlp/plugins/remotes/html/assets/jquery.mobile.min.css create mode 100644 openlp/plugins/remotes/html/assets/jquery.mobile.min.js rename openlp/plugins/remotes/html/{ => css}/main.css (100%) rename openlp/plugins/remotes/html/{ => css}/openlp.css (93%) rename openlp/plugins/remotes/html/{ => css}/stage.css (100%) delete mode 100644 openlp/plugins/remotes/html/jquery.mobile.min.css delete mode 100644 openlp/plugins/remotes/html/jquery.mobile.min.js rename openlp/plugins/remotes/html/{ => js}/main.js (100%) rename openlp/plugins/remotes/html/{ => js}/openlp.js (100%) rename openlp/plugins/remotes/html/{ => js}/stage.js (100%) diff --git a/openlp/plugins/remotes/html/jquery.js b/openlp/plugins/remotes/html/assets/jquery.js similarity index 100% rename from openlp/plugins/remotes/html/jquery.js rename to openlp/plugins/remotes/html/assets/jquery.js diff --git a/openlp/plugins/remotes/html/jquery.min.js b/openlp/plugins/remotes/html/assets/jquery.min.js similarity index 100% rename from openlp/plugins/remotes/html/jquery.min.js rename to openlp/plugins/remotes/html/assets/jquery.min.js diff --git a/openlp/plugins/remotes/html/jquery.mobile.js b/openlp/plugins/remotes/html/assets/jquery.mobile.js similarity index 99% rename from openlp/plugins/remotes/html/jquery.mobile.js rename to openlp/plugins/remotes/html/assets/jquery.mobile.js index c5b71fa15..5cc32659b 100644 --- a/openlp/plugins/remotes/html/jquery.mobile.js +++ b/openlp/plugins/remotes/html/assets/jquery.mobile.js @@ -12,7 +12,7 @@ (function ( root, doc, factory ) { if ( typeof define === "function" && define.amd ) { // AMD. Register as an anonymous module. - define( [ "jquery" ], function ( $ ) { + define( [ "jquery" ], function ($ ) { factory( $, root, doc ); return $.mobile; }); diff --git a/openlp/plugins/remotes/html/assets/jquery.mobile.min.css b/openlp/plugins/remotes/html/assets/jquery.mobile.min.css new file mode 100644 index 000000000..33b269a65 --- /dev/null +++ b/openlp/plugins/remotes/html/assets/jquery.mobile.min.css @@ -0,0 +1,2 @@ +/*! jQuery Mobile vGit Build: SHA1: 27e3c18acfebab2d47ee7ed37bd50fc4942c8838 <> Date: Fri Mar 22 08:50:04 2013 -0600 jquerymobile.com | jquery.org/license !*/ +.ui-bar-a{border:1px solid #333;background:#111;color:#fff;font-weight:bold;text-shadow:0 -1px 1px #000;background-image:-webkit-gradient(linear,left top,left bottom,from( #3c3c3c ),to( #111 ));background-image:-webkit-linear-gradient( #3c3c3c,#111 );background-image:-moz-linear-gradient( #3c3c3c,#111 );background-image:-ms-linear-gradient( #3c3c3c,#111 );background-image:-o-linear-gradient( #3c3c3c,#111 );background-image:linear-gradient( #3c3c3c,#111 )}.ui-bar-a,.ui-bar-a input,.ui-bar-a select,.ui-bar-a textarea,.ui-bar-a button{font-family:Helvetica,Arial,sans-serif}.ui-bar-a .ui-link-inherit{color:#fff}.ui-bar-a a.ui-link{color:#7cc4e7;font-weight:bold}.ui-bar-a a.ui-link:visited{color:#2489ce}.ui-bar-a a.ui-link:hover{color:#2489ce}.ui-bar-a a.ui-link:active{color:#2489ce}.ui-body-a,.ui-overlay-a{border:1px solid #444;background:#222;color:#fff;text-shadow:0 1px 1px #111;font-weight:normal;background-image:-webkit-gradient(linear,left top,left bottom,from( #444 ),to( #222 ));background-image:-webkit-linear-gradient( #444,#222 );background-image:-moz-linear-gradient( #444,#222 );background-image:-ms-linear-gradient( #444,#222 );background-image:-o-linear-gradient( #444,#222 );background-image:linear-gradient( #444,#222 )}.ui-overlay-a{background-image:none;border-width:0}.ui-body-a,.ui-body-a input,.ui-body-a select,.ui-body-a textarea,.ui-body-a button{font-family:Helvetica,Arial,sans-serif}.ui-body-a .ui-link-inherit{color:#fff}.ui-body-a .ui-link{color:#2489ce;font-weight:bold}.ui-body-a .ui-link:visited{color:#2489ce}.ui-body-a .ui-link:hover{color:#2489ce}.ui-body-a .ui-link:active{color:#2489ce}.ui-btn-up-a{border:1px solid #111;background:#333;font-weight:bold;color:#fff;text-shadow:0 1px 1px #111;background-image:-webkit-gradient(linear,left top,left bottom,from( #444 ),to( #2d2d2d ));background-image:-webkit-linear-gradient( #444,#2d2d2d );background-image:-moz-linear-gradient( #444,#2d2d2d );background-image:-ms-linear-gradient( #444,#2d2d2d );background-image:-o-linear-gradient( #444,#2d2d2d );background-image:linear-gradient( #444,#2d2d2d )}.ui-btn-up-a:visited,.ui-btn-up-a a.ui-link-inherit{color:#fff}.ui-btn-hover-a{border:1px solid #000;background:#444;font-weight:bold;color:#fff;text-shadow:0 1px 1px #111;background-image:-webkit-gradient(linear,left top,left bottom,from( #555 ),to( #383838 ));background-image:-webkit-linear-gradient( #555,#383838 );background-image:-moz-linear-gradient( #555,#383838 );background-image:-ms-linear-gradient( #555,#383838 );background-image:-o-linear-gradient( #555,#383838 );background-image:linear-gradient( #555,#383838 )}.ui-btn-hover-a:visited,.ui-btn-hover-a:hover,.ui-btn-hover-a a.ui-link-inherit{color:#fff}.ui-btn-down-a{border:1px solid #000;background:#222;font-weight:bold;color:#fff;text-shadow:0 1px 1px #111;background-image:-webkit-gradient(linear,left top,left bottom,from( #202020 ),to( #2c2c2c ));background-image:-webkit-linear-gradient( #202020,#2c2c2c );background-image:-moz-linear-gradient( #202020,#2c2c2c );background-image:-ms-linear-gradient( #202020,#2c2c2c );background-image:-o-linear-gradient( #202020,#2c2c2c );background-image:linear-gradient( #202020,#2c2c2c )}.ui-btn-down-a:visited,.ui-btn-down-a:hover,.ui-btn-down-a a.ui-link-inherit{color:#fff}.ui-btn-up-a,.ui-btn-hover-a,.ui-btn-down-a{font-family:Helvetica,Arial,sans-serif;text-decoration:none}.ui-bar-b{border:1px solid #456f9a;background:#5e87b0;color:#fff;font-weight:bold;text-shadow:0 1px 1px #3e6790;background-image:-webkit-gradient(linear,left top,left bottom,from( #6facd5 ),to( #497bae ));background-image:-webkit-linear-gradient( #6facd5,#497bae );background-image:-moz-linear-gradient( #6facd5,#497bae );background-image:-ms-linear-gradient( #6facd5,#497bae );background-image:-o-linear-gradient( #6facd5,#497bae );background-image:linear-gradient( #6facd5,#497bae )}.ui-bar-b,.ui-bar-b input,.ui-bar-b select,.ui-bar-b textarea,.ui-bar-b button{font-family:Helvetica,Arial,sans-serif}.ui-bar-b .ui-link-inherit{color:#fff}.ui-bar-b a.ui-link{color:#ddf0f8;font-weight:bold}.ui-bar-b a.ui-link:visited{color:#ddf0f8}.ui-bar-b a.ui-link:hover{color:#ddf0f8}.ui-bar-b a.ui-link:active{color:#ddf0f8}.ui-body-b,.ui-overlay-b{border:1px solid #999;background:#f3f3f3;color:#222;text-shadow:0 1px 0 #fff;font-weight:normal;background-image:-webkit-gradient(linear,left top,left bottom,from( #ddd ),to( #ccc ));background-image:-webkit-linear-gradient( #ddd,#ccc );background-image:-moz-linear-gradient( #ddd,#ccc );background-image:-ms-linear-gradient( #ddd,#ccc );background-image:-o-linear-gradient( #ddd,#ccc );background-image:linear-gradient( #ddd,#ccc )}.ui-overlay-b{background-image:none;border-width:0}.ui-body-b,.ui-body-b input,.ui-body-b select,.ui-body-b textarea,.ui-body-b button{font-family:Helvetica,Arial,sans-serif}.ui-body-b .ui-link-inherit{color:#333}.ui-body-b .ui-link{color:#2489ce;font-weight:bold}.ui-body-b .ui-link:visited{color:#2489ce}.ui-body-b .ui-link:hover{color:#2489ce}.ui-body-b .ui-link:active{color:#2489ce}.ui-btn-up-b{border:1px solid #044062;background:#396b9e;font-weight:bold;color:#fff;text-shadow:0 1px 1px #194b7e;background-image:-webkit-gradient(linear,left top,left bottom,from( #5f9cc5 ),to( #396b9e ));background-image:-webkit-linear-gradient( #5f9cc5,#396b9e );background-image:-moz-linear-gradient( #5f9cc5,#396b9e );background-image:-ms-linear-gradient( #5f9cc5,#396b9e );background-image:-o-linear-gradient( #5f9cc5,#396b9e );background-image:linear-gradient( #5f9cc5,#396b9e )}.ui-btn-up-b:visited,.ui-btn-up-b a.ui-link-inherit{color:#fff}.ui-btn-hover-b{border:1px solid #00415e;background:#4b88b6;font-weight:bold;color:#fff;text-shadow:0 1px 1px #194b7e;background-image:-webkit-gradient(linear,left top,left bottom,from( #6facd5 ),to( #4272a4 ));background-image:-webkit-linear-gradient( #6facd5,#4272a4 );background-image:-moz-linear-gradient( #6facd5,#4272a4 );background-image:-ms-linear-gradient( #6facd5,#4272a4 );background-image:-o-linear-gradient( #6facd5,#4272a4 );background-image:linear-gradient( #6facd5,#4272a4 )}.ui-btn-hover-b:visited,.ui-btn-hover-b:hover,.ui-btn-hover-b a.ui-link-inherit{color:#fff}.ui-btn-down-b{border:1px solid #225377;background:#4e89c5;font-weight:bold;color:#fff;text-shadow:0 1px 1px #194b7e;background-image:-webkit-gradient(linear,left top,left bottom,from( #295b8e ),to( #3e79b5 ));background-image:-webkit-linear-gradient( #295b8e,#3e79b5 );background-image:-moz-linear-gradient( #295b8e,#3e79b5 );background-image:-ms-linear-gradient( #295b8e,#3e79b5 );background-image:-o-linear-gradient( #295b8e,#3e79b5 );background-image:linear-gradient( #295b8e,#3e79b5 )}.ui-btn-down-b:visited,.ui-btn-down-b:hover,.ui-btn-down-b a.ui-link-inherit{color:#fff}.ui-btn-up-b,.ui-btn-hover-b,.ui-btn-down-b{font-family:Helvetica,Arial,sans-serif;text-decoration:none}.ui-bar-c{border:1px solid #b3b3b3;background:#eee;color:#3e3e3e;font-weight:bold;text-shadow:0 1px 1px #fff;background-image:-webkit-gradient(linear,left top,left bottom,from( #f0f0f0 ),to( #ddd ));background-image:-webkit-linear-gradient( #f0f0f0,#ddd );background-image:-moz-linear-gradient( #f0f0f0,#ddd );background-image:-ms-linear-gradient( #f0f0f0,#ddd );background-image:-o-linear-gradient( #f0f0f0,#ddd );background-image:linear-gradient( #f0f0f0,#ddd )}.ui-bar-c .ui-link-inherit{color:#3e3e3e}.ui-bar-c a.ui-link{color:#7cc4e7;font-weight:bold}.ui-bar-c a.ui-link:visited{color:#2489ce}.ui-bar-c a.ui-link:hover{color:#2489ce}.ui-bar-c a.ui-link:active{color:#2489ce}.ui-bar-c,.ui-bar-c input,.ui-bar-c select,.ui-bar-c textarea,.ui-bar-c button{font-family:Helvetica,Arial,sans-serif}.ui-body-c,.ui-overlay-c{border:1px solid #aaa;color:#333;text-shadow:0 1px 0 #fff;background:#f9f9f9;background-image:-webkit-gradient(linear,left top,left bottom,from( #f9f9f9 ),to( #eee ));background-image:-webkit-linear-gradient( #f9f9f9,#eee );background-image:-moz-linear-gradient( #f9f9f9,#eee );background-image:-ms-linear-gradient( #f9f9f9,#eee );background-image:-o-linear-gradient( #f9f9f9,#eee );background-image:linear-gradient( #f9f9f9,#eee )}.ui-overlay-c{background-image:none;border-width:0}.ui-body-c,.ui-body-c input,.ui-body-c select,.ui-body-c textarea,.ui-body-c button{font-family:Helvetica,Arial,sans-serif}.ui-body-c .ui-link-inherit{color:#333}.ui-body-c .ui-link{color:#2489ce;font-weight:bold}.ui-body-c .ui-link:visited{color:#2489ce}.ui-body-c .ui-link:hover{color:#2489ce}.ui-body-c .ui-link:active{color:#2489ce}.ui-btn-up-c{border:1px solid #ccc;background:#eee;font-weight:bold;color:#222;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from( #fff ),to( #f1f1f1 ));background-image:-webkit-linear-gradient( #fff,#f1f1f1 );background-image:-moz-linear-gradient( #fff,#f1f1f1 );background-image:-ms-linear-gradient( #fff,#f1f1f1 );background-image:-o-linear-gradient( #fff,#f1f1f1 );background-image:linear-gradient( #fff,#f1f1f1 )}.ui-btn-up-c:visited,.ui-btn-up-c a.ui-link-inherit{color:#2f3e46}.ui-btn-hover-c{border:1px solid #bbb;background:#dfdfdf;font-weight:bold;color:#222;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from( #f6f6f6 ),to( #e0e0e0 ));background-image:-webkit-linear-gradient( #f6f6f6,#e0e0e0 );background-image:-moz-linear-gradient( #f6f6f6,#e0e0e0 );background-image:-ms-linear-gradient( #f6f6f6,#e0e0e0 );background-image:-o-linear-gradient( #f6f6f6,#e0e0e0 );background-image:linear-gradient( #f6f6f6,#e0e0e0 )}.ui-btn-hover-c:visited,.ui-btn-hover-c:hover,.ui-btn-hover-c a.ui-link-inherit{color:#2f3e46}.ui-btn-down-c{border:1px solid #bbb;background:#d6d6d6;font-weight:bold;color:#222;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from( #d0d0d0 ),to( #dfdfdf ));background-image:-webkit-linear-gradient( #d0d0d0,#dfdfdf );background-image:-moz-linear-gradient( #d0d0d0,#dfdfdf );background-image:-ms-linear-gradient( #d0d0d0,#dfdfdf );background-image:-o-linear-gradient( #d0d0d0,#dfdfdf );background-image:linear-gradient( #d0d0d0,#dfdfdf )}.ui-btn-down-c:visited,.ui-btn-down-c:hover,.ui-btn-down-c a.ui-link-inherit{color:#2f3e46}.ui-btn-up-c,.ui-btn-hover-c,.ui-btn-down-c{font-family:Helvetica,Arial,sans-serif;text-decoration:none}.ui-bar-d{border:1px solid #bbb;background:#bbb;color:#333;font-weight:bold;text-shadow:0 1px 0 #eee;background-image:-webkit-gradient(linear,left top,left bottom,from( #ddd ),to( #bbb ));background-image:-webkit-linear-gradient( #ddd,#bbb );background-image:-moz-linear-gradient( #ddd,#bbb );background-image:-ms-linear-gradient( #ddd,#bbb );background-image:-o-linear-gradient( #ddd,#bbb );background-image:linear-gradient( #ddd,#bbb )}.ui-bar-d,.ui-bar-d input,.ui-bar-d select,.ui-bar-d textarea,.ui-bar-d button{font-family:Helvetica,Arial,sans-serif}.ui-bar-d .ui-link-inherit{color:#333}.ui-bar-d a.ui-link{color:#2489ce;font-weight:bold}.ui-bar-d a.ui-link:visited{color:#2489ce}.ui-bar-d a.ui-link:hover{color:#2489ce}.ui-bar-d a.ui-link:active{color:#2489ce}.ui-body-d,.ui-overlay-d{border:1px solid #bbb;color:#333;text-shadow:0 1px 0 #fff;background:#fff;background-image:-webkit-gradient(linear,left top,left bottom,from( #fff ),to( #fff ));background-image:-webkit-linear-gradient( #fff,#fff );background-image:-moz-linear-gradient( #fff,#fff );background-image:-ms-linear-gradient( #fff,#fff );background-image:-o-linear-gradient( #fff,#fff );background-image:linear-gradient( #fff,#fff )}.ui-overlay-d{background-image:none;border-width:0}.ui-body-d,.ui-body-d input,.ui-body-d select,.ui-body-d textarea,.ui-body-d button{font-family:Helvetica,Arial,sans-serif}.ui-body-d .ui-link-inherit{color:#333}.ui-body-d .ui-link{color:#2489ce;font-weight:bold}.ui-body-d .ui-link:visited{color:#2489ce}.ui-body-d .ui-link:hover{color:#2489ce}.ui-body-d .ui-link:active{color:#2489ce}.ui-btn-up-d{border:1px solid #bbb;background:#fff;font-weight:bold;color:#333;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from( #fafafa ),to( #f6f6f6 ));background-image:-webkit-linear-gradient( #fafafa,#f6f6f6 );background-image:-moz-linear-gradient( #fafafa,#f6f6f6 );background-image:-ms-linear-gradient( #fafafa,#f6f6f6 );background-image:-o-linear-gradient( #fafafa,#f6f6f6 );background-image:linear-gradient( #fafafa,#f6f6f6 )}.ui-btn-up-d:visited,.ui-btn-up-d a.ui-link-inherit{color:#333}.ui-btn-hover-d{border:1px solid #aaa;background:#eee;font-weight:bold;color:#333;cursor:pointer;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from( #eee ),to( #fff ));background-image:-webkit-linear-gradient( #eee,#fff );background-image:-moz-linear-gradient( #eee,#fff );background-image:-ms-linear-gradient( #eee,#fff );background-image:-o-linear-gradient( #eee,#fff );background-image:linear-gradient( #eee,#fff )}.ui-btn-hover-d:visited,.ui-btn-hover-d:hover,.ui-btn-hover-d a.ui-link-inherit{color:#333}.ui-btn-down-d{border:1px solid #aaa;background:#eee;font-weight:bold;color:#333;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from( #e5e5e5 ),to( #f2f2f2 ));background-image:-webkit-linear-gradient( #e5e5e5,#f2f2f2 );background-image:-moz-linear-gradient( #e5e5e5,#f2f2f2 );background-image:-ms-linear-gradient( #e5e5e5,#f2f2f2 );background-image:-o-linear-gradient( #e5e5e5,#f2f2f2 );background-image:linear-gradient( #e5e5e5,#f2f2f2 )}.ui-btn-down-d:visited,.ui-btn-down-d:hover,.ui-btn-down-d a.ui-link-inherit{color:#333}.ui-btn-up-d,.ui-btn-hover-d,.ui-btn-down-d{font-family:Helvetica,Arial,sans-serif;text-decoration:none}.ui-bar-e{border:1px solid #f7c942;background:#fadb4e;color:#333;font-weight:bold;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from( #fceda7 ),to( #fbef7e ));background-image:-webkit-linear-gradient( #fceda7,#fbef7e );background-image:-moz-linear-gradient( #fceda7,#fbef7e );background-image:-ms-linear-gradient( #fceda7,#fbef7e );background-image:-o-linear-gradient( #fceda7,#fbef7e );background-image:linear-gradient( #fceda7,#fbef7e )}.ui-bar-e,.ui-bar-e input,.ui-bar-e select,.ui-bar-e textarea,.ui-bar-e button{font-family:Helvetica,Arial,sans-serif}.ui-bar-e .ui-link-inherit{color:#333}.ui-bar-e a.ui-link{color:#2489ce;font-weight:bold}.ui-bar-e a.ui-link:visited{color:#2489ce}.ui-bar-e a.ui-link:hover{color:#2489ce}.ui-bar-e a.ui-link:active{color:#2489ce}.ui-body-e,.ui-overlay-e{border:1px solid #f7c942;color:#222;text-shadow:0 1px 0 #fff;background:#fff9df;background-image:-webkit-gradient(linear,left top,left bottom,from( #fffadf ),to( #fff3a5 ));background-image:-webkit-linear-gradient( #fffadf,#fff3a5 );background-image:-moz-linear-gradient( #fffadf,#fff3a5 );background-image:-ms-linear-gradient( #fffadf,#fff3a5 );background-image:-o-linear-gradient( #fffadf,#fff3a5 );background-image:linear-gradient( #fffadf,#fff3a5 )}.ui-overlay-e{background-image:none;border-width:0}.ui-body-e,.ui-body-e input,.ui-body-e select,.ui-body-e textarea,.ui-body-e button{font-family:Helvetica,Arial,sans-serif}.ui-body-e .ui-link-inherit{color:#222}.ui-body-e .ui-link{color:#2489ce;font-weight:bold}.ui-body-e .ui-link:visited{color:#2489ce}.ui-body-e .ui-link:hover{color:#2489ce}.ui-body-e .ui-link:active{color:#2489ce}.ui-btn-up-e{border:1px solid #f4c63f;background:#fadb4e;font-weight:bold;color:#222;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from( #ffefaa ),to( #ffe155 ));background-image:-webkit-linear-gradient( #ffefaa,#ffe155 );background-image:-moz-linear-gradient( #ffefaa,#ffe155 );background-image:-ms-linear-gradient( #ffefaa,#ffe155 );background-image:-o-linear-gradient( #ffefaa,#ffe155 );background-image:linear-gradient( #ffefaa,#ffe155 )}.ui-btn-up-e:visited,.ui-btn-up-e a.ui-link-inherit{color:#222}.ui-btn-hover-e{border:1px solid #f2c43d;background:#fbe26f;font-weight:bold;color:#111;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from( #fff5ba ),to( #fbdd52 ));background-image:-webkit-linear-gradient( #fff5ba,#fbdd52 );background-image:-moz-linear-gradient( #fff5ba,#fbdd52 );background-image:-ms-linear-gradient( #fff5ba,#fbdd52 );background-image:-o-linear-gradient( #fff5ba,#fbdd52 );background-image:linear-gradient( #fff5ba,#fbdd52 )}.ui-btn-hover-e:visited,.ui-btn-hover-e:hover,.ui-btn-hover-e a.ui-link-inherit{color:#333}.ui-btn-down-e{border:1px solid #f2c43d;background:#fceda7;font-weight:bold;color:#111;text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left top,left bottom,from( #f8d94c ),to( #fadb4e ));background-image:-webkit-linear-gradient( #f8d94c,#fadb4e );background-image:-moz-linear-gradient( #f8d94c,#fadb4e );background-image:-ms-linear-gradient( #f8d94c,#fadb4e );background-image:-o-linear-gradient( #f8d94c,#fadb4e );background-image:linear-gradient( #f8d94c,#fadb4e )}.ui-btn-down-e:visited,.ui-btn-down-e:hover,.ui-btn-down-e a.ui-link-inherit{color:#333}.ui-btn-up-e,.ui-btn-hover-e,.ui-btn-down-e{font-family:Helvetica,Arial,sans-serif;text-decoration:none}a.ui-link-inherit{text-decoration:none!important}.ui-btn-active{border:1px solid #2373a5;background:#5393c5;font-weight:bold;color:#fff;cursor:pointer;text-shadow:0 1px 1px #3373a5;text-decoration:none;background-image:-webkit-gradient(linear,left top,left bottom,from( #5393c5 ),to( #6facd5 ));background-image:-webkit-linear-gradient( #5393c5,#6facd5 );background-image:-moz-linear-gradient( #5393c5,#6facd5 );background-image:-ms-linear-gradient( #5393c5,#6facd5 );background-image:-o-linear-gradient( #5393c5,#6facd5 );background-image:linear-gradient( #5393c5,#6facd5 );font-family:Helvetica,Arial,sans-serif}.ui-btn-active:visited,.ui-btn-active:hover,.ui-btn-active a.ui-link-inherit{color:#fff}.ui-btn-inner{border-top:1px solid #fff;border-color:rgba(255,255,255,.3)}.ui-corner-tl{-moz-border-radius-topleft:.6em;-webkit-border-top-left-radius:.6em;border-top-left-radius:.6em}.ui-corner-tr{-moz-border-radius-topright:.6em;-webkit-border-top-right-radius:.6em;border-top-right-radius:.6em}.ui-corner-bl{-moz-border-radius-bottomleft:.6em;-webkit-border-bottom-left-radius:.6em;border-bottom-left-radius:.6em}.ui-corner-br{-moz-border-radius-bottomright:.6em;-webkit-border-bottom-right-radius:.6em;border-bottom-right-radius:.6em}.ui-corner-top{-moz-border-radius-topleft:.6em;-webkit-border-top-left-radius:.6em;border-top-left-radius:.6em;-moz-border-radius-topright:.6em;-webkit-border-top-right-radius:.6em;border-top-right-radius:.6em}.ui-corner-bottom{-moz-border-radius-bottomleft:.6em;-webkit-border-bottom-left-radius:.6em;border-bottom-left-radius:.6em;-moz-border-radius-bottomright:.6em;-webkit-border-bottom-right-radius:.6em;border-bottom-right-radius:.6em}.ui-corner-right{-moz-border-radius-topright:.6em;-webkit-border-top-right-radius:.6em;border-top-right-radius:.6em;-moz-border-radius-bottomright:.6em;-webkit-border-bottom-right-radius:.6em;border-bottom-right-radius:.6em}.ui-corner-left{-moz-border-radius-topleft:.6em;-webkit-border-top-left-radius:.6em;border-top-left-radius:.6em;-moz-border-radius-bottomleft:.6em;-webkit-border-bottom-left-radius:.6em;border-bottom-left-radius:.6em}.ui-corner-all{-moz-border-radius:.6em;-webkit-border-radius:.6em;border-radius:.6em}.ui-corner-none{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0}.ui-br{border-bottom:rgb(130,130,130);border-bottom:rgba(130,130,130,.3);border-bottom-width:1px;border-bottom-style:solid}.ui-disabled{filter:Alpha(Opacity=30);opacity:.3;zoom:1}.ui-disabled,.ui-disabled a{cursor:default!important;pointer-events:none}.ui-icon,.ui-icon-searchfield:after{background:#666;background:rgba(0,0,0,.4);background-image:url(../images/icons-18-white.png);background-repeat:no-repeat;-moz-border-radius:9px;-webkit-border-radius:9px;border-radius:9px} .ui-icon-alt{background:#fff;background:rgba(255,255,255,.3);background-image:url(../images/icons-18-black.png);background-repeat:no-repeat}@media only screen and (-webkit-min-device-pixel-ratio:1.5),only screen and (min--moz-device-pixel-ratio:1.5),only screen and (min-resolution:240dpi){.ui-icon-plus,.ui-icon-minus,.ui-icon-delete,.ui-icon-arrow-r,.ui-icon-arrow-l,.ui-icon-arrow-u,.ui-icon-arrow-d,.ui-icon-check,.ui-icon-gear,.ui-icon-refresh,.ui-icon-forward,.ui-icon-back,.ui-icon-grid,.ui-icon-star,.ui-icon-alert,.ui-icon-info,.ui-icon-home,.ui-icon-search,.ui-icon-searchfield:after,.ui-icon-checkbox-off,.ui-icon-checkbox-on,.ui-icon-radio-off,.ui-icon-radio-on{background-image:url(../images/icons-36-white.png);-moz-background-size:776px 18px;-o-background-size:776px 18px;-webkit-background-size:776px 18px;background-size:776px 18px} .ui-icon-alt{background-image:url(../images/icons-36-black.png)}} .ui-icon-plus{background-position:-0 50%} .ui-icon-minus{background-position:-36px 50%} .ui-icon-delete{background-position:-72px 50%} .ui-icon-arrow-r{background-position:-108px 50%} .ui-icon-arrow-l{background-position:-144px 50%} .ui-icon-arrow-u{background-position:-180px 50%} .ui-icon-arrow-d{background-position:-216px 50%} .ui-icon-check{background-position:-252px 50%} .ui-icon-gear{background-position:-288px 50%} .ui-icon-refresh{background-position:-324px 50%} .ui-icon-forward{background-position:-360px 50%} .ui-icon-back{background-position:-396px 50%} .ui-icon-grid{background-position:-432px 50%} .ui-icon-star{background-position:-468px 50%} .ui-icon-alert{background-position:-504px 50%} .ui-icon-info{background-position:-540px 50%} .ui-icon-home{background-position:-576px 50%} .ui-icon-search,.ui-icon-searchfield:after{background-position:-612px 50%} .ui-icon-checkbox-off{background-position:-684px 50%} .ui-icon-checkbox-on{background-position:-648px 50%} .ui-icon-radio-off{background-position:-756px 50%} .ui-icon-radio-on{background-position:-720px 50%} .ui-checkbox .ui-icon,.ui-selectmenu-list .ui-icon{-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px} .ui-icon-checkbox-off,.ui-icon-radio-off{background-color:transparent} .ui-checkbox-on .ui-icon,.ui-radio-on .ui-icon{background-color:#4596ce} .ui-icon-loading{background:url(../images/ajax-loader.gif);background-size:46px 46px} .ui-btn-corner-tl{-moz-border-radius-topleft:1em;-webkit-border-top-left-radius:1em;border-top-left-radius:1em} .ui-btn-corner-tr{-moz-border-radius-topright:1em;-webkit-border-top-right-radius:1em;border-top-right-radius:1em} .ui-btn-corner-bl{-moz-border-radius-bottomleft:1em;-webkit-border-bottom-left-radius:1em;border-bottom-left-radius:1em} .ui-btn-corner-br{-moz-border-radius-bottomright:1em;-webkit-border-bottom-right-radius:1em;border-bottom-right-radius:1em} .ui-btn-corner-top{-moz-border-radius-topleft:1em;-webkit-border-top-left-radius:1em;border-top-left-radius:1em;-moz-border-radius-topright:1em;-webkit-border-top-right-radius:1em;border-top-right-radius:1em} .ui-btn-corner-bottom{-moz-border-radius-bottomleft:1em;-webkit-border-bottom-left-radius:1em;border-bottom-left-radius:1em;-moz-border-radius-bottomright:1em;-webkit-border-bottom-right-radius:1em;border-bottom-right-radius:1em} .ui-btn-corner-right{-moz-border-radius-topright:1em;-webkit-border-top-right-radius:1em;border-top-right-radius:1em;-moz-border-radius-bottomright:1em;-webkit-border-bottom-right-radius:1em;border-bottom-right-radius:1em} .ui-btn-corner-left{-moz-border-radius-topleft:1em;-webkit-border-top-left-radius:1em;border-top-left-radius:1em;-moz-border-radius-bottomleft:1em;-webkit-border-bottom-left-radius:1em;border-bottom-left-radius:1em} .ui-btn-corner-all{-moz-border-radius:1em;-webkit-border-radius:1em;border-radius:1em} .ui-corner-tl,.ui-corner-tr,.ui-corner-bl,.ui-corner-br,.ui-corner-top,.ui-corner-bottom,.ui-corner-right,.ui-corner-left,.ui-corner-all,.ui-btn-corner-tl,.ui-btn-corner-tr,.ui-btn-corner-bl,.ui-btn-corner-br,.ui-btn-corner-top,.ui-btn-corner-bottom,.ui-btn-corner-right,.ui-btn-corner-left,.ui-btn-corner-all{-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box} .ui-overlay{background:#666;filter:Alpha(Opacity=50);opacity:.5;position:absolute;width:100%;height:100%} .ui-overlay-shadow{-moz-box-shadow:0 0 12px rgba(0,0,0,.6);-webkit-box-shadow:0 0 12px rgba(0,0,0,.6);box-shadow:0 0 12px rgba(0,0,0,.6)} .ui-shadow{-moz-box-shadow:0 1px 4px rgba(0,0,0,.3);-webkit-box-shadow:0 1px 4px rgba(0,0,0,.3);box-shadow:0 1px 4px rgba(0,0,0,.3)} .ui-bar-a .ui-shadow,.ui-bar-b .ui-shadow,.ui-bar-c .ui-shadow{-moz-box-shadow:0 1px 0 rgba(255,255,255,.3);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.3);box-shadow:0 1px 0 rgba(255,255,255,.3)} .ui-shadow-inset{-moz-box-shadow:inset 0 1px 4px rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 4px rgba(0,0,0,.2);box-shadow:inset 0 1px 4px rgba(0,0,0,.2)} .ui-icon-shadow{-moz-box-shadow:0 1px 0 rgba(255,255,255,.4);-webkit-box-shadow:0 1px 0 rgba(255,255,255,.4);box-shadow:0 1px 0 rgba(255,255,255,.4)} .ui-btn:focus,.ui-link-inherit:focus{outline:0} .ui-btn.ui-focus{z-index:1} .ui-focus,.ui-btn:focus{-moz-box-shadow:inset 0 0 3px #387bbe,0px 0 9px #387bbe;-webkit-box-shadow:inset 0 0 3px #387bbe,0px 0 9px #387bbe;box-shadow:inset 0 0 3px #387bbe,0px 0 9px #387bbe} .ui-input-text.ui-focus,.ui-input-search.ui-focus{-moz-box-shadow:0 0 12px #387bbe;-webkit-box-shadow:0 0 12px #387bbe;box-shadow:0 0 12px #387bbe} .ui-mobile-nosupport-boxshadow *{-moz-box-shadow:none!important;-webkit-box-shadow:none!important;box-shadow:none!important} .ui-mobile-nosupport-boxshadow .ui-focus,.ui-mobile-nosupport-boxshadow .ui-btn:focus,.ui-mobile-nosupport-boxshadow .ui-link-inherit:focus{outline-width:1px;outline-style:auto} .ui-mobile,.ui-mobile body{height:99.9%} .ui-mobile fieldset,.ui-page{padding:0;margin:0} .ui-mobile a img,.ui-mobile fieldset{border-width:0} .ui-mobile-viewport{margin:0;overflow-x:visible;-webkit-text-size-adjust:100%;-ms-text-size-adjust:none;-webkit-tap-highlight-color:rgba(0,0,0,0)} body.ui-mobile-viewport,div.ui-mobile-viewport{overflow-x:hidden} .ui-mobile [data-role=page],.ui-mobile [data-role=dialog],.ui-page{top:0;left:0;width:100%;min-height:100%;position:absolute;display:none;border:0} .ui-mobile .ui-page-active{display:block;overflow:visible} .ui-page{outline:none}@media screen and (orientation:portrait){.ui-mobile,.ui-mobile .ui-page{min-height:420px}}@media screen and (orientation:landscape){.ui-mobile,.ui-mobile .ui-page{min-height:300px}} .ui-loading .ui-loader{display:block} .ui-loader{display:none;z-index:9999999;position:fixed;top:50%;left:50%;border:0} .ui-loader-default{background:none;filter:Alpha(Opacity=18);opacity:.18;width:46px;height:46px;margin-left:-23px;margin-top:-23px} .ui-loader-verbose{width:200px;filter:Alpha(Opacity=88);opacity:.88;box-shadow:0 1px 1px -1px #fff;height:auto;margin-left:-110px;margin-top:-43px;padding:10px} .ui-loader-default h1{font-size:0;width:0;height:0;overflow:hidden} .ui-loader-verbose h1{font-size:16px;margin:0;text-align:center} .ui-loader .ui-icon{background-color:#000;display:block;margin:0;width:44px;height:44px;padding:1px;-webkit-border-radius:36px;-moz-border-radius:36px;border-radius:36px} .ui-loader-verbose .ui-icon{margin:0 auto 10px;filter:Alpha(Opacity=75);opacity:.75} .ui-loader-textonly{padding:15px;margin-left:-115px} .ui-loader-textonly .ui-icon{display:none} .ui-loader-fakefix{position:absolute} .ui-mobile-rendering > *{visibility:hidden} .ui-bar,.ui-body{position:relative;padding:.4em 15px;overflow:hidden;display:block;clear:both} .ui-bar{font-size:16px;margin:0} .ui-bar h1,.ui-bar h2,.ui-bar h3,.ui-bar h4,.ui-bar h5,.ui-bar h6{margin:0;padding:0;font-size:16px;display:inline-block} .ui-header,.ui-footer{position:relative;zoom:1} .ui-mobile .ui-header,.ui-mobile .ui-footer{border-left-width:0;border-right-width:0} .ui-header .ui-btn-left,.ui-header .ui-btn-right,.ui-footer .ui-btn-left,.ui-footer .ui-btn-right{position:absolute;top:3px} .ui-header .ui-btn-left,.ui-footer .ui-btn-left{left:5px} .ui-header .ui-btn-right,.ui-footer .ui-btn-right{right:5px} .ui-footer .ui-btn-icon-notext,.ui-header .ui-btn-icon-notext{top:6px} .ui-header .ui-title,.ui-footer .ui-title{min-height:1.1em;text-align:center;font-size:16px;display:block;margin:.6em 30% .8em;padding:0;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;outline:0!important} .ui-footer .ui-title{margin:.6em 15px .8em} .ui-content{border-width:0;overflow:visible;overflow-x:hidden;padding:15px} .ui-icon{width:18px;height:18px} .ui-nojs{position:absolute;left:-9999px} .ui-hide-label label.ui-input-text,.ui-hide-label label.ui-select,.ui-hide-label label.ui-slider,.ui-hide-label label.ui-submit,.ui-hide-label .ui-controlgroup-label,.ui-hidden-accessible{position:absolute!important;left:-9999px;clip:rect(1px);clip:rect(1px,1px,1px,1px)} .ui-mobile-viewport-transitioning,.ui-mobile-viewport-transitioning .ui-page{width:100%;height:100%;overflow:hidden;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box} .ui-page-pre-in{opacity:0} .in{-webkit-animation-timing-function:ease-out;-webkit-animation-duration:350ms;-moz-animation-timing-function:ease-out;-moz-animation-duration:350ms} .out{-webkit-animation-timing-function:ease-in;-webkit-animation-duration:225ms;-moz-animation-timing-function:ease-in;-moz-animation-duration:225ms} @-webkit-keyframes fadein{from{opacity:0}to{opacity:1}} @-moz-keyframes fadein{from{opacity:0}to{opacity:1}} @-webkit-keyframes fadeout{from{opacity:1}to{opacity:0}} @-moz-keyframes fadeout{from{opacity:1}to{opacity:0}} .fade.out{opacity:0;-webkit-animation-duration:125ms;-webkit-animation-name:fadeout;-moz-animation-duration:125ms;-moz-animation-name:fadeout} .fade.in{opacity:1;-webkit-animation-duration:225ms;-webkit-animation-name:fadein;-moz-animation-duration:225ms;-moz-animation-name:fadein} .pop{-webkit-transform-origin:50% 50%;-moz-transform-origin:50% 50%} .pop.in{-webkit-transform:scale(1);-moz-transform:scale(1);opacity:1;-webkit-animation-name:popin;-moz-animation-name:popin;-webkit-animation-duration:350ms;-moz-animation-duration:350ms} .pop.out{-webkit-animation-name:fadeout;-moz-animation-name:fadeout;opacity:0;-webkit-animation-duration:100ms;-moz-animation-duration:100ms} .pop.in.reverse{-webkit-animation-name:fadein;-moz-animation-name:fadein} .pop.out.reverse{-webkit-transform:scale(.8);-moz-transform:scale(.8);-webkit-animation-name:popout;-moz-animation-name:popout} @-webkit-keyframes popin{from{-webkit-transform:scale(.8);opacity:0}to{-webkit-transform:scale(1);opacity:1}} @-moz-keyframes popin{from{-moz-transform:scale(.8);opacity:0}to{-moz-transform:scale(1);opacity:1}} @-webkit-keyframes popout{from{-webkit-transform:scale(1);opacity:1}to{-webkit-transform:scale(.8);opacity:0}} @-moz-keyframes popout{from{-moz-transform:scale(1);opacity:1}to{-moz-transform:scale(.8);opacity:0}} @-webkit-keyframes slideinfromright{from{-webkit-transform:translateX(100%)}to{-webkit-transform:translateX(0)}} @-moz-keyframes slideinfromright{from{-moz-transform:translateX(100%)}to{-moz-transform:translateX(0)}} @-webkit-keyframes slideinfromleft{from{-webkit-transform:translateX(-100%)}to{-webkit-transform:translateX(0)}} @-moz-keyframes slideinfromleft{from{-moz-transform:translateX(-100%)}to{-moz-transform:translateX(0)}} @-webkit-keyframes slideouttoleft{from{-webkit-transform:translateX(0)}to{-webkit-transform:translateX(-100%)}} @-moz-keyframes slideouttoleft{from{-moz-transform:translateX(0)}to{-moz-transform:translateX(-100%)}} @-webkit-keyframes slideouttoright{from{-webkit-transform:translateX(0)}to{-webkit-transform:translateX(100%)}} @-moz-keyframes slideouttoright{from{-moz-transform:translateX(0)}to{-moz-transform:translateX(100%)}} .slide.out,.slide.in{-webkit-animation-timing-function:ease-out;-webkit-animation-duration:350ms;-moz-animation-timing-function:ease-out;-moz-animation-duration:350ms} .slide.out{-webkit-transform:translateX(-100%);-webkit-animation-name:slideouttoleft;-moz-transform:translateX(-100%);-moz-animation-name:slideouttoleft} .slide.in{-webkit-transform:translateX(0);-webkit-animation-name:slideinfromright;-moz-transform:translateX(0);-moz-animation-name:slideinfromright} .slide.out.reverse{-webkit-transform:translateX(100%);-webkit-animation-name:slideouttoright;-moz-transform:translateX(100%);-moz-animation-name:slideouttoright} .slide.in.reverse{-webkit-transform:translateX(0);-webkit-animation-name:slideinfromleft;-moz-transform:translateX(0);-moz-animation-name:slideinfromleft} .slidefade.out{-webkit-transform:translateX(-100%);-webkit-animation-name:slideouttoleft;-moz-transform:translateX(-100%);-moz-animation-name:slideouttoleft;-webkit-animation-duration:225ms;-moz-animation-duration:225ms} .slidefade.in{-webkit-transform:translateX(0);-webkit-animation-name:fadein;-moz-transform:translateX(0);-moz-animation-name:fadein;-webkit-animation-duration:200ms;-moz-animation-duration:200ms} .slidefade.out.reverse{-webkit-transform:translateX(100%);-webkit-animation-name:slideouttoright;-moz-transform:translateX(100%);-moz-animation-name:slideouttoright;-webkit-animation-duration:200ms;-moz-animation-duration:200ms} .slidefade.in.reverse{-webkit-transform:translateX(0);-webkit-animation-name:fadein;-moz-transform:translateX(0);-moz-animation-name:fadein;-webkit-animation-duration:200ms;-moz-animation-duration:200ms} .slidedown.out{-webkit-animation-name:fadeout;-moz-animation-name:fadeout;-webkit-animation-duration:100ms;-moz-animation-duration:100ms} .slidedown.in{-webkit-transform:translateY(0);-webkit-animation-name:slideinfromtop;-moz-transform:translateY(0);-moz-animation-name:slideinfromtop;-webkit-animation-duration:250ms;-moz-animation-duration:250ms} .slidedown.in.reverse{-webkit-animation-name:fadein;-moz-animation-name:fadein;-webkit-animation-duration:150ms;-moz-animation-duration:150ms} .slidedown.out.reverse{-webkit-transform:translateY(-100%);-moz-transform:translateY(-100%);-webkit-animation-name:slideouttotop;-moz-animation-name:slideouttotop;-webkit-animation-duration:200ms;-moz-animation-duration:200ms} @-webkit-keyframes slideinfromtop{from{-webkit-transform:translateY(-100%)}to{-webkit-transform:translateY(0)}} @-moz-keyframes slideinfromtop{from{-moz-transform:translateY(-100%)}to{-moz-transform:translateY(0)}} @-webkit-keyframes slideouttotop{from{-webkit-transform:translateY(0)}to{-webkit-transform:translateY(-100%)}} @-moz-keyframes slideouttotop{from{-moz-transform:translateY(0)}to{-moz-transform:translateY(-100%)}} .slideup.out{-webkit-animation-name:fadeout;-moz-animation-name:fadeout;-webkit-animation-duration:100ms;-moz-animation-duration:100ms} .slideup.in{-webkit-transform:translateY(0);-webkit-animation-name:slideinfrombottom;-moz-transform:translateY(0);-moz-animation-name:slideinfrombottom;-webkit-animation-duration:250ms;-moz-animation-duration:250ms} .slideup.in.reverse{-webkit-animation-name:fadein;-moz-animation-name:fadein;-webkit-animation-duration:150ms;-moz-animation-duration:150ms} .slideup.out.reverse{-webkit-transform:translateY(100%);-moz-transform:translateY(100%);-webkit-animation-name:slideouttobottom;-moz-animation-name:slideouttobottom;-webkit-animation-duration:200ms;-moz-animation-duration:200ms} @-webkit-keyframes slideinfrombottom{from{-webkit-transform:translateY(100%)}to{-webkit-transform:translateY(0)}} @-moz-keyframes slideinfrombottom{from{-moz-transform:translateY(100%)}to{-moz-transform:translateY(0)}} @-webkit-keyframes slideouttobottom{from{-webkit-transform:translateY(0)}to{-webkit-transform:translateY(100%)}} @-moz-keyframes slideouttobottom{from{-moz-transform:translateY(0)}to{-moz-transform:translateY(100%)}} .viewport-flip{-webkit-perspective:1000;-moz-perspective:1000;position:absolute} .flip{-webkit-backface-visibility:hidden;-webkit-transform:translateX(0);-moz-backface-visibility:hidden;-moz-transform:translateX(0)} .flip.out{-webkit-transform:rotateY(-90deg) scale(.9);-webkit-animation-name:flipouttoleft;-webkit-animation-duration:175ms;-moz-transform:rotateY(-90deg) scale(.9);-moz-animation-name:flipouttoleft;-moz-animation-duration:175ms} .flip.in{-webkit-animation-name:flipintoright;-webkit-animation-duration:225ms;-moz-animation-name:flipintoright;-moz-animation-duration:225ms} .flip.out.reverse{-webkit-transform:rotateY(90deg) scale(.9);-webkit-animation-name:flipouttoright;-moz-transform:rotateY(90deg) scale(.9);-moz-animation-name:flipouttoright} .flip.in.reverse{-webkit-animation-name:flipintoleft;-moz-animation-name:flipintoleft} @-webkit-keyframes flipouttoleft{from{-webkit-transform:rotateY(0)}to{-webkit-transform:rotateY(-90deg) scale(.9)}} @-moz-keyframes flipouttoleft{from{-moz-transform:rotateY(0)}to{-moz-transform:rotateY(-90deg) scale(.9)}} @-webkit-keyframes flipouttoright{from{-webkit-transform:rotateY(0)}to{-webkit-transform:rotateY(90deg) scale(.9)}} @-moz-keyframes flipouttoright{from{-moz-transform:rotateY(0)}to{-moz-transform:rotateY(90deg) scale(.9)}} @-webkit-keyframes flipintoleft{from{-webkit-transform:rotateY(-90deg) scale(.9)}to{-webkit-transform:rotateY(0)}} @-moz-keyframes flipintoleft{from{-moz-transform:rotateY(-90deg) scale(.9)}to{-moz-transform:rotateY(0)}} @-webkit-keyframes flipintoright{from{-webkit-transform:rotateY(90deg) scale(.9)}to{-webkit-transform:rotateY(0)}} @-moz-keyframes flipintoright{from{-moz-transform:rotateY(90deg) scale(.9)}to{-moz-transform:rotateY(0)}} .viewport-turn{-webkit-perspective:1000;-moz-perspective:1000;position:absolute} .turn{-webkit-backface-visibility:hidden;-webkit-transform:translateX(0);-webkit-transform-origin:0;-moz-backface-visibility:hidden;-moz-transform:translateX(0);-moz-transform-origin:0} .turn.out{-webkit-transform:rotateY(-90deg) scale(.9);-webkit-animation-name:flipouttoleft;-moz-transform:rotateY(-90deg) scale(.9);-moz-animation-name:flipouttoleft;-webkit-animation-duration:125ms;-moz-animation-duration:125ms} .turn.in{-webkit-animation-name:flipintoright;-moz-animation-name:flipintoright;-webkit-animation-duration:250ms;-moz-animation-duration:250ms} .turn.out.reverse{-webkit-transform:rotateY(90deg) scale(.9);-webkit-animation-name:flipouttoright;-moz-transform:rotateY(90deg) scale(.9);-moz-animation-name:flipouttoright} .turn.in.reverse{-webkit-animation-name:flipintoleft;-moz-animation-name:flipintoleft} @-webkit-keyframes flipouttoleft{from{-webkit-transform:rotateY(0)}to{-webkit-transform:rotateY(-90deg) scale(.9)}} @-moz-keyframes flipouttoleft{from{-moz-transform:rotateY(0)}to{-moz-transform:rotateY(-90deg) scale(.9)}} @-webkit-keyframes flipouttoright{from{-webkit-transform:rotateY(0)}to{-webkit-transform:rotateY(90deg) scale(.9)}} @-moz-keyframes flipouttoright{from{-moz-transform:rotateY(0)}to{-moz-transform:rotateY(90deg) scale(.9)}} @-webkit-keyframes flipintoleft{from{-webkit-transform:rotateY(-90deg) scale(.9)}to{-webkit-transform:rotateY(0)}} @-moz-keyframes flipintoleft{from{-moz-transform:rotateY(-90deg) scale(.9)}to{-moz-transform:rotateY(0)}} @-webkit-keyframes flipintoright{from{-webkit-transform:rotateY(90deg) scale(.9)}to{-webkit-transform:rotateY(0)}} @-moz-keyframes flipintoright{from{-moz-transform:rotateY(90deg) scale(.9)}to{-moz-transform:rotateY(0)}} .flow{-webkit-transform-origin:50% 30%;-moz-transform-origin:50% 30%;-webkit-box-shadow:0 0 20px rgba(0,0,0,.4);-moz-box-shadow:0 0 20px rgba(0,0,0,.4)} .ui-dialog.flow{-webkit-transform-origin:none;-moz-transform-origin:none;-webkit-box-shadow:none;-moz-box-shadow:none} .flow.out{-webkit-transform:translateX(-100%) scale(.7);-webkit-animation-name:flowouttoleft;-webkit-animation-timing-function:ease;-webkit-animation-duration:350ms;-moz-transform:translateX(-100%) scale(.7);-moz-animation-name:flowouttoleft;-moz-animation-timing-function:ease;-moz-animation-duration:350ms} .flow.in{-webkit-transform:translateX(0) scale(1);-webkit-animation-name:flowinfromright;-webkit-animation-timing-function:ease;-webkit-animation-duration:350ms;-moz-transform:translateX(0) scale(1);-moz-animation-name:flowinfromright;-moz-animation-timing-function:ease;-moz-animation-duration:350ms} .flow.out.reverse{-webkit-transform:translateX(100%);-webkit-animation-name:flowouttoright;-moz-transform:translateX(100%);-moz-animation-name:flowouttoright} .flow.in.reverse{-webkit-animation-name:flowinfromleft;-moz-animation-name:flowinfromleft} @-webkit-keyframes flowouttoleft{0%{-webkit-transform:translateX(0) scale(1)}60%,70%{-webkit-transform:translateX(0) scale(.7)}100%{-webkit-transform:translateX(-100%) scale(.7)}} @-moz-keyframes flowouttoleft{0%{-moz-transform:translateX(0) scale(1)}60%,70%{-moz-transform:translateX(0) scale(.7)}100%{-moz-transform:translateX(-100%) scale(.7)}} @-webkit-keyframes flowouttoright{0%{-webkit-transform:translateX(0) scale(1)}60%,70%{-webkit-transform:translateX(0) scale(.7)}100%{-webkit-transform:translateX(100%) scale(.7)}} @-moz-keyframes flowouttoright{0%{-moz-transform:translateX(0) scale(1)}60%,70%{-moz-transform:translateX(0) scale(.7)}100%{-moz-transform:translateX(100%) scale(.7)}} @-webkit-keyframes flowinfromleft{0%{-webkit-transform:translateX(-100%) scale(.7)}30%,40%{-webkit-transform:translateX(0) scale(.7)}100%{-webkit-transform:translateX(0) scale(1)}} @-moz-keyframes flowinfromleft{0%{-moz-transform:translateX(-100%) scale(.7)}30%,40%{-moz-transform:translateX(0) scale(.7)}100%{-moz-transform:translateX(0) scale(1)}} @-webkit-keyframes flowinfromright{0%{-webkit-transform:translateX(100%) scale(.7)}30%,40%{-webkit-transform:translateX(0) scale(.7)}100%{-webkit-transform:translateX(0) scale(1)}} @-moz-keyframes flowinfromright{0%{-moz-transform:translateX(100%) scale(.7)}30%,40%{-moz-transform:translateX(0) scale(.7)}100%{-moz-transform:translateX(0) scale(1)}} .ui-grid-a,.ui-grid-b,.ui-grid-c,.ui-grid-d{overflow:hidden} .ui-block-a,.ui-block-b,.ui-block-c,.ui-block-d,.ui-block-e{margin:0;padding:0;border:0;float:left;min-height:1px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box} .ui-grid-solo .ui-block-a{display:block;float:none} .ui-grid-a .ui-block-a,.ui-grid-a .ui-block-b{width:49.95%} .ui-grid-a >:nth-child(n){width:50%;margin-right:-.5px} .ui-grid-a .ui-block-a{clear:left} .ui-grid-b .ui-block-a,.ui-grid-b .ui-block-b,.ui-grid-b .ui-block-c{width:33.25%} .ui-grid-b >:nth-child(n){width:33.333%;margin-right:-.5px} .ui-grid-b .ui-block-a{clear:left} .ui-grid-c .ui-block-a,.ui-grid-c .ui-block-b,.ui-grid-c .ui-block-c,.ui-grid-c .ui-block-d{width:24.925%} .ui-grid-c >:nth-child(n){width:25%;margin-right:-.5px} .ui-grid-c .ui-block-a{clear:left} .ui-grid-d .ui-block-a,.ui-grid-d .ui-block-b,.ui-grid-d .ui-block-c,.ui-grid-d .ui-block-d,.ui-grid-d .ui-block-e{width:19.925%} .ui-grid-d >:nth-child(n){width:20%} .ui-grid-d .ui-block-a{clear:left} .ui-header-fixed,.ui-footer-fixed{left:0;right:0;width:100%;position:fixed;z-index:1000} .ui-header-fixed{top:0} .ui-footer-fixed{bottom:0} .ui-header-fullscreen,.ui-footer-fullscreen{filter:Alpha(Opacity=90);opacity:.9} .ui-page-header-fixed{padding-top:2.6875em} .ui-page-footer-fixed{padding-bottom:2.6875em} .ui-page-header-fullscreen .ui-content,.ui-page-footer-fullscreen .ui-content{padding:0} .ui-fixed-hidden{position:absolute} .ui-page-header-fullscreen .ui-fixed-hidden,.ui-page-footer-fullscreen .ui-fixed-hidden{left:-9999px} .ui-header-fixed .ui-btn,.ui-footer-fixed .ui-btn{z-index:10} .ui-navbar{max-width:100%} .ui-navbar.ui-mini{margin:0} .ui-navbar ul:before,.ui-navbar ul:after{content:" ";display:table} .ui-navbar ul:after{clear:both} .ui-navbar ul{list-style:none;margin:0;padding:0;position:relative;display:block;border:0;max-width:100%;overflow:visible;zoom:1} .ui-navbar li .ui-btn{display:block;text-align:center;margin:0 -1px 0 0;border-right-width:0} .ui-navbar li .ui-btn-icon-right .ui-icon{right:6px} .ui-navbar li:last-child .ui-btn,.ui-navbar .ui-grid-duo .ui-block-b .ui-btn{margin-right:0;border-right-width:1px} .ui-header .ui-navbar li:last-child .ui-btn,.ui-footer .ui-navbar li:last-child .ui-btn,.ui-header .ui-navbar .ui-grid-duo .ui-block-b .ui-btn,.ui-footer .ui-navbar .ui-grid-duo .ui-block-b .ui-btn{margin-right:-1px;border-right-width:0} .ui-navbar .ui-grid-duo li.ui-block-a:last-child .ui-btn{margin-right:-1px;border-right-width:1px} .ui-header .ui-navbar li .ui-btn,.ui-footer .ui-navbar li .ui-btn{border-top-width:0;border-bottom-width:0} .ui-header .ui-navbar .ui-grid-b li.ui-block-c .ui-btn,.ui-footer .ui-navbar .ui-grid-b li.ui-block-c .ui-btn{margin-right:-5px} .ui-header .ui-navbar .ui-grid-c li.ui-block-d .ui-btn,.ui-footer .ui-navbar .ui-grid-c li.ui-block-d .ui-btn,.ui-header .ui-navbar .ui-grid-d li.ui-block-e .ui-btn,.ui-footer .ui-navbar .ui-grid-d li.ui-block-e .ui-btn{margin-right:-4px} .ui-header .ui-navbar .ui-grid-b li.ui-block-c .ui-btn-icon-right .ui-icon,.ui-footer .ui-navbar .ui-grid-b li.ui-block-c .ui-btn-icon-right .ui-icon,.ui-header .ui-navbar .ui-grid-c li.ui-block-d .ui-btn-icon-right .ui-icon,.ui-footer .ui-navbar .ui-grid-c li.ui-block-d .ui-btn-icon-right .ui-icon,.ui-header .ui-navbar .ui-grid-d li.ui-block-e .ui-btn-icon-right .ui-icon,.ui-footer .ui-navbar .ui-grid-d li.ui-block-e .ui-btn-icon-right .ui-icon{right:8px} .ui-navbar li .ui-btn .ui-btn-inner{padding-top:.7em;padding-bottom:.8em} .ui-navbar li .ui-btn-icon-top .ui-btn-inner{padding-top:30px} .ui-navbar li .ui-btn-icon-bottom .ui-btn-inner{padding-bottom:30px} .ui-btn{display:block;text-align:center;cursor:pointer;position:relative;margin:.5em 0;padding:0} .ui-mini{margin-top:.25em;margin-bottom:.25em} .ui-btn-left,.ui-btn-right,.ui-input-clear,.ui-btn-inline,.ui-grid-a .ui-btn,.ui-grid-b .ui-btn,.ui-grid-c .ui-btn,.ui-grid-d .ui-btn,.ui-grid-e .ui-btn,.ui-grid-solo .ui-btn{margin-right:5px;margin-left:5px} .ui-btn-inner{font-size:16px;padding:.6em 20px;min-width:.75em;display:block;position:relative;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;zoom:1} .ui-btn input,.ui-btn button{z-index:2} .ui-btn-left,.ui-btn-right,.ui-btn-inline{display:inline-block;vertical-align:middle} .ui-mobile .ui-btn-left,.ui-mobile .ui-btn-right{margin:0} .ui-btn-block{display:block} .ui-header > .ui-btn,.ui-footer > .ui-btn{display:inline-block;margin:0} .ui-header .ui-btn-block,.ui-footer .ui-btn-block{display:block} .ui-header .ui-btn-inner,.ui-footer .ui-btn-inner,.ui-mini .ui-btn-inner{font-size:12.5px;padding:.55em 11px .5em} .ui-fullsize .ui-btn-inner,.ui-fullsize .ui-btn-inner{font-size:16px;padding:.6em 20px} .ui-btn-icon-notext{width:24px;height:24px} .ui-btn-icon-notext .ui-btn-inner{padding:0;height:100%} .ui-btn-icon-notext .ui-btn-inner .ui-icon{margin:2px 1px 2px 3px;float:left} .ui-btn-text{position:relative;z-index:1;width:100%;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none} div.ui-btn-text{width:auto} .ui-btn-icon-notext .ui-btn-text{position:absolute;left:-9999px} .ui-btn-icon-left .ui-btn-inner{padding-left:40px} .ui-btn-icon-right .ui-btn-inner{padding-right:40px} .ui-btn-icon-top .ui-btn-inner{padding-top:40px} .ui-btn-icon-bottom .ui-btn-inner{padding-bottom:40px} .ui-header .ui-btn-icon-left .ui-btn-inner,.ui-footer .ui-btn-icon-left .ui-btn-inner,.ui-mini.ui-btn-icon-left .ui-btn-inner,.ui-mini .ui-btn-icon-left .ui-btn-inner{padding-left:30px} .ui-header .ui-btn-icon-right .ui-btn-inner,.ui-footer .ui-btn-icon-right .ui-btn-inner,.ui-mini.ui-btn-icon-right .ui-btn-inner,.ui-mini .ui-btn-icon-right .ui-btn-inner{padding-right:30px} .ui-header .ui-btn-icon-top .ui-btn-inner,.ui-footer .ui-btn-icon-top .ui-btn-inner{padding:30px 3px .5em 3px} .ui-mini.ui-btn-icon-top .ui-btn-inner,.ui-mini .ui-btn-icon-top .ui-btn-inner{padding-top:30px} .ui-header .ui-btn-icon-bottom .ui-btn-inner,.ui-footer .ui-btn-icon-bottom .ui-btn-inner{padding:.55em 3px 30px 3px} .ui-mini.ui-btn-icon-bottom .ui-btn-inner,.ui-mini .ui-btn-icon-bottom .ui-btn-inner{padding-bottom:30px} .ui-btn-icon-notext .ui-icon{display:block;z-index:0} .ui-btn-icon-left > .ui-btn-inner > .ui-icon,.ui-btn-icon-right > .ui-btn-inner > .ui-icon{position:absolute;top:50%;margin-top:-9px} .ui-btn-icon-top .ui-btn-inner .ui-icon,.ui-btn-icon-bottom .ui-btn-inner .ui-icon{position:absolute;left:50%;margin-left:-9px} .ui-btn-icon-left .ui-icon{left:10px} .ui-btn-icon-right .ui-icon{right:10px} .ui-btn-icon-top .ui-icon{top:10px} .ui-btn-icon-bottom .ui-icon{top:auto;bottom:10px} .ui-header .ui-btn-icon-left .ui-icon,.ui-footer .ui-btn-icon-left .ui-icon,.ui-mini.ui-btn-icon-left .ui-icon,.ui-mini .ui-btn-icon-left .ui-icon{left:5px} .ui-header .ui-btn-icon-right .ui-icon,.ui-footer .ui-btn-icon-right .ui-icon,.ui-mini.ui-btn-icon-right .ui-icon,.ui-mini .ui-btn-icon-right .ui-icon{right:5px} .ui-header .ui-btn-icon-top .ui-icon,.ui-footer .ui-btn-icon-top .ui-icon,.ui-mini.ui-btn-icon-top .ui-icon,.ui-mini .ui-btn-icon-top .ui-icon{top:5px} .ui-header .ui-btn-icon-bottom .ui-icon,.ui-footer .ui-btn-icon-bottom .ui-icon,.ui-mini.ui-btn-icon-bottom .ui-icon,.ui-mini .ui-btn-icon-bottom .ui-icon{bottom:5px} .ui-btn-hidden{position:absolute;top:0;left:0;width:100%;height:100%;-webkit-appearance:none;cursor:pointer;background:#fff;background:rgba(255,255,255,0);filter:Alpha(Opacity=0);opacity:.1;font-size:1px;border:none;text-indent:-9999px} .ui-disabled .ui-btn-hidden{display:none} .ui-disabled{z-index:1} .ui-field-contain .ui-btn.ui-submit{margin:0} label.ui-submit{font-size:16px;line-height:1.4;font-weight:normal;margin:0 0 .3em;display:block}@media all and (min-width:450px){.ui-field-contain label.ui-submit{vertical-align:top;display:inline-block;width:20%;margin:0 2% 0 0}.ui-field-contain .ui-btn.ui-submit{width:78%;display:inline-block;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}.ui-hide-label .ui-btn.ui-submit{width:auto;display:block}} .ui-collapsible-inset{margin:.5em 0} .ui-collapsible-heading{font-size:16px;display:block;margin:0 -15px;padding:0;position:relative} .ui-collapsible-inset .ui-collapsible-heading{margin:0} .ui-collapsible-heading .ui-btn{text-align:left;margin:0;border-left-width:0;border-right-width:0} .ui-collapsible-inset .ui-collapsible-heading .ui-btn{border-right-width:1px;border-left-width:1px} .ui-collapsible-collapsed + .ui-collapsible:not(.ui-collapsible-inset) .ui-collapsible-heading .ui-btn{border-top-width:0} .ui-collapsible-set .ui-collapsible:not(.ui-collapsible-inset) .ui-collapsible-heading .ui-btn{border-top-width:1px} .ui-collapsible-heading .ui-btn-inner,.ui-collapsible-heading .ui-btn-icon-left .ui-btn-inner{padding-left:40px} .ui-collapsible-heading .ui-btn-icon-right .ui-btn-inner{padding-left:12px;padding-right:40px} .ui-collapsible-heading .ui-btn-icon-top .ui-btn-inner,.ui-collapsible-heading .ui-btn-icon-bottom .ui-btn-inner{padding-right:40px;text-align:center} .ui-collapsible-heading .ui-btn span.ui-btn{position:absolute;left:6px;top:50%;margin:-12px 0 0 0;width:20px;height:20px;padding:1px 0 1px 2px;text-indent:-9999px} .ui-collapsible-heading .ui-btn span.ui-btn .ui-btn-inner{padding:10px 0} .ui-collapsible-heading .ui-btn span.ui-btn .ui-icon{left:0;margin-top:-10px} .ui-collapsible-heading-status{position:absolute;top:-9999px;left:0} .ui-collapsible-content{display:block;margin:0 -15px;padding:10px 15px;border-left-width:0;border-right-width:0;border-top:none;background-image:none} .ui-collapsible-inset .ui-collapsible-content{margin:0;border-right-width:1px;border-left-width:1px} .ui-collapsible-content-collapsed{display:none} .ui-collapsible-set{margin:.5em 0} .ui-collapsible-set .ui-collapsible{margin:-1px 0 0} .ui-collapsible-set .ui-collapsible:first-child{margin-top:0} .ui-controlgroup,fieldset.ui-controlgroup{padding:0;margin:.5em 0;zoom:1} .ui-controlgroup.ui-mini,fieldset.ui-controlgroup.ui-mini{margin:.25em 0} .ui-field-contain .ui-controlgroup,.ui-field-contain fieldset.ui-controlgroup{margin:0} .ui-bar .ui-controlgroup{margin:0 5px} .ui-controlgroup-label{font-size:16px;line-height:1.4;font-weight:normal;margin:0 0 .4em} .ui-controlgroup li{list-style:none} .ui-controlgroup-vertical .ui-btn,.ui-controlgroup-vertical .ui-checkbox,.ui-controlgroup-vertical .ui-radio{margin:0;border-bottom-width:0} .ui-controlgroup-vertical .ui-controlgroup-last{border-bottom-width:1px} .ui-controlgroup-controls label.ui-select{position:absolute;left:-9999px} .ui-controlgroup .ui-btn-icon-notext{width:auto;height:auto;top:auto} .ui-controlgroup .ui-btn-icon-notext .ui-btn-inner{height:20px;padding:.6em 20px .6em 20px} .ui-controlgroup-horizontal .ui-btn-icon-notext .ui-btn-inner{width:18px} .ui-controlgroup.ui-mini .ui-btn-icon-notext .ui-btn-inner,.ui-header .ui-controlgroup .ui-btn-icon-notext .ui-btn-inner,.ui-footer .ui-controlgroup .ui-btn-icon-notext .ui-btn-inner{height:16px;padding:.55em 11px .5em 11px} .ui-controlgroup .ui-btn-icon-notext .ui-btn-inner .ui-icon{position:absolute;top:50%;right:50%;margin:-9px -9px 0 0} .ui-controlgroup-horizontal .ui-controlgroup-controls:before,.ui-controlgroup-horizontal .ui-controlgroup-controls:after{content:"";display:table} .ui-controlgroup-horizontal .ui-controlgroup-controls:after{clear:both} .ui-controlgroup-horizontal .ui-controlgroup-controls{display:inline-block;vertical-align:middle;zoom:1} .ui-controlgroup-horizontal .ui-btn-inner{text-align:center} .ui-controlgroup-horizontal.ui-mini .ui-btn-inner{height:16px;line-height:16px} .ui-controlgroup-horizontal .ui-btn,.ui-controlgroup-horizontal .ui-select,.ui-controlgroup-horizontal .ui-checkbox,.ui-controlgroup-horizontal .ui-radio{float:left;clear:none;margin:0 -1px 0 0} .ui-controlgroup-horizontal .ui-select .ui-btn,.ui-controlgroup-horizontal .ui-checkbox .ui-btn,.ui-controlgroup-horizontal .ui-radio .ui-btn{float:none;margin:0} .ui-controlgroup-horizontal .ui-controlgroup-last,.ui-controlgroup-horizontal .ui-select:last-child,.ui-controlgroup-horizontal .ui-checkbox:last-child,.ui-controlgroup-horizontal .ui-radio:last-child{margin-right:0} .ui-controlgroup .ui-checkbox label,.ui-controlgroup .ui-radio label{font-size:16px}@media all and (min-width:450px){.ui-field-contain .ui-controlgroup-label{vertical-align:top;display:inline-block;width:20%;margin:0 2% 0 0}.ui-field-contain .ui-controlgroup-controls{width:78%;display:inline-block}.ui-field-contain .ui-controlgroup .ui-select{width:100%;display:block}.ui-field-contain .ui-controlgroup-horizontal .ui-select{width:auto}.ui-hide-label .ui-controlgroup-controls{width:100%}} .ui-dialog{background:none!important} .ui-dialog-contain{width:92.5%;max-width:500px;margin:10% auto 15px auto;padding:0;position:relative;top:-15px} .ui-dialog-contain > .ui-header,.ui-dialog-contain > .ui-content,.ui-dialog-contain > .ui-footer{display:block;position:relative;width:auto;margin:0} .ui-dialog-contain > .ui-header{border:none;overflow:hidden;z-index:10;padding:0} .ui-dialog-contain > .ui-content{padding:15px} .ui-dialog-contain > .ui-footer{z-index:10;padding:0 15px} .ui-popup-open .ui-header-fixed,.ui-popup-open .ui-footer-fixed{position:absolute!important} .ui-popup-screen{background-image:url(data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==);top:0;left:0;right:0;bottom:1px;position:absolute;filter:Alpha(Opacity=0);opacity:0;z-index:1099} .ui-popup-screen.in{opacity:0.5;filter:Alpha(Opacity=50)} .ui-popup-screen.out{opacity:0;filter:Alpha(Opacity=0)} .ui-popup-container{z-index:1100;display:inline-block;position:absolute;padding:0;outline:0} .ui-popup{position:relative} .ui-popup.ui-content,.ui-popup .ui-content{overflow:visible} .ui-popup > p,.ui-popup > h1,.ui-popup > h2,.ui-popup > h3,.ui-popup > h4,.ui-popup > h5,.ui-popup > h6{margin:.5em 7px} .ui-popup > span{display:block;margin:.5em 7px} .ui-popup .ui-title{font-size:16px;font-weight:bold;margin-top:.5em;margin-bottom:.5em} .ui-popup-container .ui-content > p,.ui-popup-container .ui-content > h1,.ui-popup-container .ui-content > h2,.ui-popup-container .ui-content > h3,.ui-popup-container .ui-content > h4,.ui-popup-container .ui-content > h5,.ui-popup-container .ui-content > h6{margin:.5em 0} .ui-popup-container .ui-content > span{margin:0} .ui-popup-container .ui-content > p:first-child,.ui-popup-container .ui-content > h1:first-child,.ui-popup-container .ui-content > h2:first-child,.ui-popup-container .ui-content > h3:first-child,.ui-popup-container .ui-content > h4:first-child,.ui-popup-container .ui-content > h5:first-child,.ui-popup-container .ui-content > h6:first-child{margin-top:0} .ui-popup-container .ui-content > p:last-child,.ui-popup-container .ui-content > h1:last-child,.ui-popup-container .ui-content > h2:last-child,.ui-popup-container .ui-content > h3:last-child,.ui-popup-container .ui-content > h4:last-child,.ui-popup-container .ui-content > h5:last-child,.ui-popup-container .ui-content > h6:last-child{margin-bottom:0} .ui-popup > img{width:auto;height:auto;max-width:100%;max-height:100%;vertical-align:middle} .ui-popup iframe{vertical-align:middle}@media all and (min-width:450px){.ui-popup .ui-field-contain label.ui-submit,.ui-popup .ui-field-contain .ui-controlgroup-label,.ui-popup .ui-field-contain label.ui-select,.ui-popup .ui-field-contain label.ui-input-text{font-size:16px;line-height:1.4;display:block;font-weight:normal;margin:0 0 .3em}.ui-popup .ui-field-contain .ui-btn.ui-submit,.ui-popup .ui-field-contain .ui-controlgroup-controls,.ui-popup .ui-field-contain .ui-select,.ui-popup .ui-field-contain input.ui-input-text,.ui-popup .ui-field-contain textarea.ui-input-text,.ui-popup .ui-field-contain .ui-input-search{width:100%;display:block}} .ui-popup > .ui-btn-left,.ui-popup > .ui-btn-right{position:absolute;top:-9px;margin:0;z-index:1101} .ui-popup > .ui-btn-left{left:-9px} .ui-popup > .ui-btn-right{right:-9px} .ui-popup.ui-corner-all > .ui-header,.ui-popup.ui-corner-all ~ .ui-content,.ui-popup.ui-corner-all > .ui-content:first-child{-webkit-border-top-left-radius:inherit;border-top-left-radius:inherit;-webkit-border-top-right-radius:inherit;border-top-right-radius:inherit} .ui-popup.ui-corner-all > .ui-content,.ui-popup.ui-corner-all > .ui-footer,.ui-popup.ui-corner-all > .ui-header:nth-child(n):last-child{-webkit-border-bottom-left-radius:inherit;border-bottom-left-radius:inherit;-webkit-border-bottom-right-radius:inherit;border-bottom-right-radius:inherit} .ui-popup.ui-corner-all > .ui-content:nth-child(2),.ui-popup.ui-corner-all > .ui-header:nth-child(2){-webkit-border-top-left-radius:0;border-top-left-radius:0;-webkit-border-top-right-radius:0;border-top-right-radius:0} .ui-popup.ui-corner-all > .ui-content:nth-last-child(1n+2),.ui-popup.ui-corner-all > .ui-footer:nth-last-child(1n+2){-webkit-border-bottom-left-radius:0;border-bottom-left-radius:0;-webkit-border-bottom-right-radius:0;border-bottom-right-radius:0} .ui-popup.ui-corner-all > .ui-header:only-child,.ui-popup.ui-corner-all > .ui-footer:only-child{-webkit-border-radius:inherit;border-radius:inherit} .ui-popup-hidden{top:-99999px;left:-9999px} .ui-checkbox,.ui-radio{position:relative;clear:both;margin:0;z-index:1} .ui-checkbox .ui-btn,.ui-radio .ui-btn{margin-top:.5em;margin-bottom:.5em;text-align:left;z-index:2} .ui-checkbox .ui-btn.ui-mini,.ui-radio .ui-btn.ui-mini{margin:.25em 0} .ui-controlgroup .ui-checkbox .ui-btn,.ui-controlgroup .ui-radio .ui-btn{margin:0} .ui-checkbox .ui-btn-inner,.ui-radio .ui-btn-inner{white-space:normal} .ui-checkbox .ui-btn-icon-left .ui-btn-inner,.ui-radio .ui-btn-icon-left .ui-btn-inner{padding-left:45px} .ui-checkbox .ui-mini.ui-btn-icon-left .ui-btn-inner,.ui-radio .ui-mini.ui-btn-icon-left .ui-btn-inner{padding-left:36px} .ui-checkbox .ui-btn-icon-right .ui-btn-inner,.ui-radio .ui-btn-icon-right .ui-btn-inner{padding-right:45px} .ui-checkbox .ui-mini.ui-btn-icon-right .ui-btn-inner,.ui-radio .ui-mini.ui-btn-icon-right .ui-btn-inner{padding-right:36px} .ui-checkbox .ui-btn-icon-top .ui-btn-inner,.ui-radio .ui-btn-icon-top .ui-btn-inner{padding-right:0;padding-left:0;text-align:center} .ui-checkbox .ui-btn-icon-bottom .ui-btn-inner,.ui-radio .ui-btn-icon-bottom .ui-btn-inner{padding-right:0;padding-left:0;text-align:center} .ui-checkbox .ui-icon,.ui-radio .ui-icon{top:1.1em} .ui-checkbox .ui-btn-icon-left .ui-icon,.ui-radio .ui-btn-icon-left .ui-icon{left:15px} .ui-checkbox .ui-mini.ui-btn-icon-left .ui-icon,.ui-radio .ui-mini.ui-btn-icon-left .ui-icon{left:9px} .ui-checkbox .ui-btn-icon-right .ui-icon,.ui-radio .ui-btn-icon-right .ui-icon{right:15px} .ui-checkbox .ui-mini.ui-btn-icon-right .ui-icon,.ui-radio .ui-mini.ui-btn-icon-right .ui-icon{right:9px} .ui-checkbox .ui-btn-icon-top .ui-icon,.ui-radio .ui-btn-icon-top .ui-icon{top:10px} .ui-checkbox .ui-btn-icon-bottom .ui-icon,.ui-radio .ui-btn-icon-bottom .ui-icon{top:auto;bottom:10px} .ui-checkbox .ui-btn-icon-right .ui-icon,.ui-radio .ui-btn-icon-right .ui-icon{right:15px} .ui-checkbox .ui-mini.ui-btn-icon-right .ui-icon,.ui-radio .ui-mini.ui-btn-icon-right .ui-icon{right:9px} .ui-checkbox input,.ui-radio input{position:absolute;left:20px;top:50%;width:10px;height:10px;margin:-5px 0 0 0;outline:0!important;z-index:1} .ui-field-contain,fieldset.ui-field-contain{padding:.8em 0;margin:0;border-width:0 0 1px 0;overflow:visible} .ui-field-contain:last-child{border-bottom-width:0} .ui-field-contain{max-width:100%}@media all and (min-width:450px){.ui-field-contain,.ui-mobile fieldset.ui-field-contain{border-width:0;padding:0;margin:1em 0}} .ui-select{display:block;position:relative} .ui-select select{position:absolute;left:-9999px;top:-9999px} .ui-select .ui-btn{overflow:hidden;opacity:1} .ui-field-contain .ui-select .ui-btn{margin:0} .ui-select .ui-btn select{cursor:pointer;-webkit-appearance:none;left:0;top:0;width:100%;min-height:1.5em;min-height:100%;height:3em;max-height:100%;filter:Alpha(Opacity=0);opacity:0;z-index:2} .ui-select .ui-disabled{opacity:.3} .ui-select .ui-disabled select{display:none} @-moz-document url-prefix(){.ui-select .ui-btn select{opacity:0.0001}} .ui-select .ui-btn.ui-select-nativeonly{border-radius:0;border:0} .ui-select .ui-btn.ui-select-nativeonly select{opacity:1;text-indent:0;display:block} .ui-select .ui-disabled.ui-select-nativeonly .ui-btn-inner{opacity:0} .ui-select .ui-btn-icon-right .ui-btn-inner,.ui-select .ui-li-has-count .ui-btn-inner{padding-right:45px} .ui-select .ui-mini.ui-btn-icon-right .ui-btn-inner{padding-right:32px} .ui-select .ui-btn-icon-right.ui-li-has-count .ui-btn-inner{padding-right:80px} .ui-select .ui-mini.ui-btn-icon-right.ui-li-has-count .ui-btn-inner{padding-right:67px} .ui-select .ui-btn-icon-right .ui-icon{right:15px} .ui-select .ui-mini.ui-btn-icon-right .ui-icon{right:7px} .ui-select .ui-btn-icon-right.ui-li-has-count .ui-li-count{right:45px} .ui-select .ui-mini.ui-btn-icon-right.ui-li-has-count .ui-li-count{right:32px} label.ui-select{font-size:16px;line-height:1.4;font-weight:normal;margin:0 0 .3em;display:block} .ui-select .ui-btn-text,.ui-selectmenu .ui-btn-text{display:block;min-height:1em;overflow:hidden!important} .ui-select .ui-btn-text{text-overflow:ellipsis} .ui-selectmenu{padding:6px;min-width:160px} .ui-selectmenu .ui-listview{margin:0} .ui-selectmenu .ui-btn.ui-li-divider{cursor:default} .ui-screen-hidden,.ui-selectmenu-list .ui-li .ui-icon{display:none} .ui-selectmenu-list .ui-li .ui-icon{display:block} .ui-li.ui-selectmenu-placeholder{display:none} .ui-selectmenu .ui-header{margin:0;padding:0} .ui-selectmenu.ui-popup .ui-header{-webkit-border-top-left-radius:0;border-top-left-radius:0;-webkit-border-top-right-radius:0;border-top-right-radius:0} .ui-selectmenu .ui-header .ui-title{margin:0.6em 46px 0.8em}@media all and (min-width:450px){.ui-field-contain label.ui-select{vertical-align:top;display:inline-block;width:20%;margin:0 2% 0 0}.ui-field-contain .ui-select{width:78%;display:inline-block}.ui-hide-label .ui-select{width:100%}} .ui-selectmenu .ui-header h1:after{content:'.';visibility:hidden} label.ui-input-text{font-size:16px;line-height:1.4;display:block;font-weight:normal;margin:0 0 .3em} input.ui-input-text,textarea.ui-input-text{background-image:none;padding:.4em;margin:.5em 0;line-height:1.4;font-size:16px;display:block;width:100%;outline:0} input.ui-input-text.ui-mini,textarea.ui-input-text.ui-mini{margin:.25em 0} .ui-field-contain input.ui-input-text,.ui-field-contain textarea.ui-input-text{margin:0} input.ui-input-text,textarea.ui-input-text,.ui-input-search{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box} input.ui-input-text{-webkit-appearance:none} textarea.ui-input-text{height:50px;-webkit-transition:height 200ms linear;-moz-transition:height 200ms linear;-o-transition:height 200ms linear;transition:height 200ms linear} .ui-input-search{padding:0 30px;margin:.5em 0;background-image:none;position:relative} .ui-input-search.ui-mini{margin:.25em 0} .ui-field-contain .ui-input-search{margin:0} .ui-icon-searchfield:after{position:absolute;left:7px;top:50%;margin-top:-9px;content:"";width:18px;height:18px;opacity:.5} .ui-input-search input.ui-input-text{border:none;width:98%;padding:.4em 0;margin:0;display:block;background:transparent none;outline:0!important} .ui-input-search .ui-input-clear{position:absolute;right:0;top:50%;margin-top:-13px} .ui-mini .ui-input-clear{right:-3px} .ui-input-search .ui-input-clear-hidden{display:none} input.ui-mini,.ui-mini input,textarea.ui-mini{font-size:14px} textarea.ui-mini{height:45px} input:-moz-placeholder{color:#aaa} input[type=number]::-webkit-outer-spin-button{margin:0}@media all and (min-width:450px){.ui-field-contain label.ui-input-text{vertical-align:top;display:inline-block;width:20%;margin:0 2% 0 0}.ui-field-contain input.ui-input-text,.ui-field-contain textarea.ui-input-text,.ui-field-contain .ui-input-search{width:78%;display:inline-block}.ui-hide-label input.ui-input-text,.ui-hide-label textarea.ui-input-text,.ui-hide-label .ui-input-search{width:100%}.ui-input-search input.ui-input-text{width:98%}} .ui-listview{margin:0} ol.ui-listview,ol.ui-listview .ui-li-divider{counter-reset:listnumbering} .ui-content .ui-listview{margin:-15px} .ui-collapsible-content > .ui-listview{margin:-10px -15px} .ui-content .ui-listview-inset{margin:1em 0} .ui-collapsible-content .ui-listview-inset{margin:.5em 0} .ui-listview,.ui-li{list-style:none;padding:0} .ui-li,.ui-li.ui-field-contain{display:block;margin:0;position:relative;overflow:visible;text-align:left;border-width:0;border-top-width:1px} .ui-li.ui-btn{margin:0} .ui-li .ui-btn-text a.ui-link-inherit{text-overflow:ellipsis;overflow:hidden;white-space:nowrap} .ui-li-static{background-image:none} .ui-li-divider{padding:.5em 15px;font-size:14px;font-weight:bold} ol.ui-listview .ui-link-inherit:before,ol.ui-listview .ui-li-static:before,.ui-li-dec{font-size:.8em;display:inline-block;padding-right:.3em;font-weight:normal;counter-increment:listnumbering;content:counter(listnumbering) ". "} ol.ui-listview .ui-li-jsnumbering:before{content:""!important} .ui-listview-inset .ui-li{border-right-width:1px;border-left-width:1px} .ui-li-last,.ui-li.ui-field-contain.ui-li-last{border-bottom-width:1px} .ui-collapsible [class*="ui-body"] > .ui-listview:not(.ui-listview-inset) .ui-li-last{border-bottom-width:0} .ui-collapsible-content > .ui-listview:not(.ui-listview-inset) .ui-li:first-child{border-top-width:0} .ui-collapsible-content > .ui-listview:not(.ui-listview-inset),.ui-collapsible-content > .ui-listview:not(.ui-listview-inset) .ui-li-last{-webkit-border-bottom-left-radius:inherit;-webkit-border-bottom-right-radius:inherit;border-bottom-left-radius:inherit;border-bottom-right-radius:inherit} .ui-collapsible-content > .ui-listview:not(.ui-listview-inset) .ui-li-last .ui-li-link-alt{-webkit-border-bottom-right-radius:inherit;border-bottom-right-radius:inherit} .ui-li>.ui-btn-inner{display:block;position:relative;padding:0} .ui-li .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li{padding:.7em 15px;display:block} .ui-li-has-thumb .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-thumb{min-height:60px;padding-left:100px} .ui-li-has-icon .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-icon{min-height:20px;padding-left:40px} .ui-li-has-count .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-count,.ui-li-divider.ui-li-has-count{padding-right:45px} .ui-li-has-arrow .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-arrow{padding-right:40px} .ui-li-has-arrow.ui-li-has-count .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-arrow.ui-li-has-count{padding-right:75px} .ui-li-heading{font-size:16px;font-weight:bold;display:block;margin:.6em 0;text-overflow:ellipsis;overflow:hidden;white-space:nowrap} .ui-li-desc{font-size:12px;font-weight:normal;display:block;margin:-.5em 0 .6em;text-overflow:ellipsis;overflow:hidden;white-space:nowrap} .ui-li-thumb,.ui-listview .ui-li-icon{position:absolute;left:1px;top:0;max-height:80px;max-width:80px} .ui-listview .ui-li-icon{max-height:16px;max-width:16px;left:10px;top:.9em} .ui-li-thumb,.ui-listview .ui-li-icon,.ui-li-content{float:left;margin-right:10px} .ui-li-aside{float:right;width:50%;text-align:right;margin:.3em 0}@media all and (min-width:480px){.ui-li-aside{width:45%}} .ui-li-divider{cursor:default} .ui-li-has-alt .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-alt{padding-right:53px} .ui-li-has-alt.ui-li-has-count .ui-btn-inner a.ui-link-inherit,.ui-li-static.ui-li-has-alt.ui-li-has-count{padding-right:88px} .ui-li-has-count .ui-li-count{position:absolute;font-size:11px;font-weight:bold;padding:.2em .5em;top:50%;margin-top:-.9em;right:10px} .ui-li-has-count.ui-li-divider .ui-li-count,.ui-li-has-count .ui-link-inherit .ui-li-count{margin-top:-.95em} .ui-li-has-arrow.ui-li-has-count .ui-li-count{right:40px} .ui-li-has-alt.ui-li-has-count .ui-li-count{right:53px} .ui-li-link-alt{position:absolute;width:40px;height:100%;border-width:0;border-left-width:1px;top:0;right:0;margin:0;padding:0;z-index:2} .ui-li-link-alt .ui-btn{overflow:hidden;position:absolute;right:8px;top:50%;margin:-13px 0 0 0;border-bottom-width:1px;z-index:-1} .ui-li-link-alt .ui-btn-inner{padding:0;height:100%;position:absolute;width:100%;top:0;left:0} .ui-li-link-alt .ui-btn .ui-icon{right:50%;margin-right:-9px} .ui-li-link-alt .ui-btn-icon-notext .ui-btn-inner .ui-icon{position:absolute;top:50%;margin-top:-9px} .ui-listview * .ui-btn-inner > .ui-btn > .ui-btn-inner{border-top:0} .ui-listview-filter{border-width:0;overflow:hidden;margin:-15px -15px 15px -15px} .ui-collapsible-content .ui-listview-filter{margin:-10px -15px 10px -15px;border-bottom:inherit} .ui-listview-filter-inset{margin:-15px -5px;background:transparent} .ui-collapsible-content .ui-listview-filter-inset{margin:-5px;border-bottom-width:0} .ui-listview-filter .ui-input-search{margin:5px;width:auto;display:block} .ui-li.ui-screen-hidden{display:none}@media only screen and (min-device-width:768px) and (max-device-width:1024px){.ui-li .ui-btn-text{overflow:visible}} label.ui-slider{font-size:16px;line-height:1.4;font-weight:normal;margin:0 0 .3em;display:block} input.ui-slider-input,.ui-field-contain input.ui-slider-input{display:inline-block;width:50px;background-image:none;padding:.4em;margin:.5em 0;line-height:1.4;font-size:16px;outline:0} input.ui-slider-input.ui-mini,.ui-field-contain input.ui-slider-input.ui-mini{width:45px;margin:.25em 0;font-size:14px} .ui-field-contain input.ui-slider-input{margin:0} input.ui-slider-input,.ui-field-contain input.ui-slider-input{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;-ms-box-sizing:content-box;box-sizing:content-box} .ui-slider-input::-webkit-outer-spin-button{margin:0} select.ui-slider-switch{display:none} div.ui-slider{position:relative;display:inline-block;overflow:visible;height:15px;padding:0;margin:0 2% 0 20px;top:4px;width:65%} div.ui-slider-mini{height:12px;margin-left:10px;top:2px} div.ui-slider-bg{border:none;height:100%;padding-right:8px} .ui-controlgroup a.ui-slider-handle,a.ui-btn.ui-slider-handle{position:absolute;z-index:1;top:50%;width:28px;height:28px;margin:-15px 0 0 -15px;outline:0} a.ui-btn.ui-slider-handle .ui-btn-inner{padding:0;height:100%} div.ui-slider-mini a.ui-slider-handle{height:14px;width:14px;margin:-8px 0 0 -7px} div.ui-slider-mini a.ui-slider-handle .ui-btn-inner{height:30px;width:30px;padding:0;margin:-9px 0 0 -9px;border-top:none}@media all and (min-width:450px){.ui-field-contain label.ui-slider{vertical-align:top;display:inline-block;width:20%;margin:0 2% 0 0}.ui-field-contain div.ui-slider{width:43%}.ui-field-contain div.ui-slider-switch{width:5.5em}} div.ui-slider-switch{height:32px;margin-left:0;width:5.8em} a.ui-slider-handle-snapping{-webkit-transition:left 70ms linear;-moz-transition:left 70ms linear} div.ui-slider-switch a.ui-btn.ui-slider-handle{margin:1px 0 0 -15px;top:0} .ui-slider-inneroffset{margin:0 16px;position:relative;z-index:1} div.ui-slider-switch.ui-slider-mini{width:5em;height:29px} div.ui-slider-switch.ui-slider-mini .ui-slider-inneroffset{margin:0 15px 0 14px} div.ui-slider-switch.ui-slider-mini .ui-slider-handle{width:25px;height:25px;margin:1px 0 0 -13px} div.ui-slider-switch.ui-slider-mini a.ui-slider-handle .ui-btn-inner{height:30px;width:30px;padding:0;margin:0} span.ui-slider-label{position:absolute;text-align:center;width:100%;overflow:hidden;font-size:16px;top:0;line-height:2;min-height:100%;border-width:0;white-space:nowrap;cursor:pointer} .ui-slider-mini span.ui-slider-label{font-size:14px} span.ui-slider-label-a{z-index:1;left:0;text-indent:-1.5em} span.ui-slider-label-b{z-index:0;right:0;text-indent:1.5em} .ui-slider-inline{width:120px;display:inline-block} \ No newline at end of file diff --git a/openlp/plugins/remotes/html/assets/jquery.mobile.min.js b/openlp/plugins/remotes/html/assets/jquery.mobile.min.js new file mode 100644 index 000000000..41c25fefa --- /dev/null +++ b/openlp/plugins/remotes/html/assets/jquery.mobile.min.js @@ -0,0 +1,2 @@ +/*! jQuery Mobile vGit Build: SHA1: 27e3c18acfebab2d47ee7ed37bd50fc4942c8838 <> Date: Fri Mar 22 08:50:04 2013 -0600 jquerymobile.com | jquery.org/license !*/ +(function(e,t,n){typeof define=="function"&&define.amd?define(["jquery"],function(r){return n(r,e,t),r.mobile}):n(e.jQuery,e,t)})(this,document,function(e, t, n, r){(function(e, t, r){var i={};e.mobile=e.extend({},{version:"1.2.1",ns:"",subPageUrlKey:"ui-page",activePageClass:"ui-page-active",activeBtnClass:"ui-btn-active",focusClass:"ui-focus",ajaxEnabled:!0,hashListeningEnabled:!0,linkBindingEnabled:!0,defaultPageTransition:"fade",maxTransitionWidth:!1,minScrollBack:250,touchOverflowEnabled:!1,defaultDialogTransition:"pop",pageLoadErrorMessage:"Error Loading Page",pageLoadErrorMessageTheme:"e",phonegapNavigationEnabled:!1,autoInitializePage:!0,pushStateEnabled:!0,ignoreContentEnabled:!1,orientationChangeEnabled:!0,buttonMarkup:{hoverDelay:200},keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91},behaviors:{},silentScroll:function(r){e.type(r)!=="number"&&(r=e.mobile.defaultHomeScroll),e.event.special.scrollstart.enabled=!1,setTimeout(function(){t.scrollTo(0,r),e(n).trigger("silentscroll",{x:0,y:r})},20),setTimeout(function(){e.event.special.scrollstart.enabled=!0},150)},nsNormalizeDict:i,nsNormalize:function(t){if(!t)return;return i[t]||(i[t]=e.camelCase(e.mobile.ns+t))},getInheritedTheme:function(e,t){var n=e[0],r="",i=/ui-(bar|body|overlay)-([a-z])\b/,s,o;while(n){s=n.className||"";if(s&&(o=i.exec(s))&&(r=o[2]))break;n=n.parentNode}return r||t||"a"},closestPageData:function(e){return e.closest(':jqmData(role="page"), :jqmData(role="dialog")').data("page")},enhanceable:function(e){return this.haveParents(e,"enhance")},hijackable:function(e){return this.haveParents(e,"ajax")},haveParents:function(t,n){if(!e.mobile.ignoreContentEnabled)return t;var r=t.length,i=e(),s,o,u;for(var a=0;a").text(e(this).text()).html()},e.fn.jqmEnhanceable=function(){return e.mobile.enhanceable(this)},e.fn.jqmHijackable=function(){return e.mobile.hijackable(this)};var s=e.find,o=/:jqmData\(([^)]*)\)/g;e.find=function(t,n,r,i){return t=t.replace(o,"[data-"+(e.mobile.ns||"")+"$1]"),s.call(this,t,n,r,i)},e.extend(e.find,s),e.find.matches=function(t,n){return e.find(t,null,null,n)},e.find.matchesSelector=function(t,n){return e.find(n,null,null,[t]).length>0}})(e,this),function(e,t){var n=0,r=Array.prototype.slice,i=e.cleanData;e.cleanData=function(t){for(var n=0,r;(r=t[n])!=null;n++)try{e(r).triggerHandler("remove")}catch(s){}i(t)},e.widget=function(t,n,r){var i,s,o,u,a=t.split(".")[0];t=t.split(".")[1],i=a+"-"+t,r||(r=n,n=e.Widget),e.expr[":"][i]=function(t){return!!e.data(t,i)},e[a]=e[a]||{},s=e[a][t],o=e[a][t]=function(e,t){if(!this._createWidget)return new o(e,t);arguments.length&&this._createWidget(e,t)},e.extend(o,s,{version:r.version,_proto:e.extend({},r),_childConstructors:[]}),u=new n,u.options=e.widget.extend({},u.options),e.each(r,function(t,i){e.isFunction(i)&&(r[t]=function(){var e=function(){return n.prototype[t].apply(this,arguments)},r=function(e){return n.prototype[t].apply(this,e)};return function(){var t=this._super,n=this._superApply,s;return this._super=e,this._superApply=r,s=i.apply(this,arguments),this._super=t,this._superApply=n,s}}())}),o.prototype=e.widget.extend(u,{widgetEventPrefix:t},r,{constructor:o,namespace:a,widgetName:t,widgetBaseClass:i,widgetFullName:i}),s?(e.each(s._childConstructors,function(t,n){var r=n.prototype;e.widget(r.namespace+"."+r.widgetName,o,n._proto)}),delete s._childConstructors):n._childConstructors.push(o),e.widget.bridge(t,o)},e.widget.extend=function(n){var i=r.call(arguments,1),s=0,o=i.length,u,a;for(;s",options:{disabled:!1,create:null},_createWidget:function(t,r){r=e(r||this.defaultElement||this)[0],this.element=e(r),this.uuid=n++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=e.widget.extend({},this.options,this._getCreateOptions(),t),this.bindings=e(),this.hoverable=e(),this.focusable=e(),r!==this&&(e.data(r,this.widgetName,this),e.data(r,this.widgetFullName,this),this._on({remove:"destroy"}),this.document=e(r.style?r.ownerDocument:r.document||r),this.window=e(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:e.noop,_getCreateEventData:e.noop,_create:e.noop,_init:e.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(e.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:e.noop,widget:function(){return this.element},option:function(n,r){var i=n,s,o,u;if(arguments.length===0)return e.widget.extend({},this.options);if(typeof n=="string"){i={},s=n.split("."),n=s.shift();if(s.length){o=i[n]=e.widget.extend({},this.options[n]);for(u=0;u"+""+"

"+"",fakeFixLoader:function(){var t=e("."+e.mobile.activeBtnClass).first();this.element.css({top:e.support.scrollTop&&s.scrollTop()+s.height()/2||t.length&&t.offset().top||100})},checkLoaderPosition:function(){var t=this.element.offset(),n=s.scrollTop(),r=e.mobile.getScreenHeight();if(t.topr)this.element.addClass("ui-loader-fakefix"),this.fakeFixLoader(),s.unbind("scroll",this.checkLoaderPosition).bind("scroll",e.proxy(this.fakeFixLoader,this))},resetHtml:function(){this.element.html(e(this.defaultHtml).html())},show:function(t,o,u){var a,f,l,c;this.resetHtml(),e.type(t)==="object"?(c=e.extend({},this.options,t),t=c.theme||e.mobile.loadingMessageTheme):(c=this.options,t=t||e.mobile.loadingMessageTheme||c.theme),f=o||e.mobile.loadingMessage||c.text,i.addClass("ui-loading");if(e.mobile.loadingMessage!==!1||c.html)e.mobile.loadingMessageTextVisible!==r?a=e.mobile.loadingMessageTextVisible:a=c.textVisible,this.element.attr("class",n+" ui-corner-all ui-body-"+t+" ui-loader-"+(a||o||t.text?"verbose":"default")+(c.textonly||u?" ui-loader-textonly":"")),c.html?this.element.html(c.html):this.element.find("h1").text(f),this.element.appendTo(e.mobile.pageContainer),this.checkLoaderPosition(),s.bind("scroll",e.proxy(this.checkLoaderPosition,this))},hide:function(){i.removeClass("ui-loading"),e.mobile.loadingMessage&&this.element.removeClass("ui-loader-fakefix"),e(t).unbind("scroll",this.fakeFixLoader),e(t).unbind("scroll",this.checkLoaderPosition)}}),s.bind("pagecontainercreate",function(){e.mobile.loaderWidget=e.mobile.loaderWidget||e(e.mobile.loader.prototype.defaultHtml).loader()})}(e,this),function(e,t,n,r){function x(e){while(e&&typeof e.originalEvent!="undefined")e=e.originalEvent;return e}function T(t,n){var i=t.type,s,o,a,l,c,h,p,d,v;t=e.Event(t),t.type=n,s=t.originalEvent,o=e.event.props,i.search(/^(mouse|click)/)>-1&&(o=f);if(s)for(p=o.length,l;p;)l=o[--p],t[l]=s[l];i.search(/mouse(down|up)|click/)>-1&&!t.which&&(t.which=1);if(i.search(/^touch/)!==-1){a=x(s),i=a.touches,c=a.changedTouches,h=i&&i.length?i[0]:c&&c.length?c[0]:r;if(h)for(d=0,v=u.length;di||Math.abs(n.pageY-p)>i,d&&!r&&D("vmousecancel",t,s),D("vmousemove",t,s),M()}function F(e){if(g)return;L();var t=N(e.target),n;D("vmouseup",e,t);if(!d){var r=D("vclick",e,t);r&&r.isDefaultPrevented()&&(n=x(e).changedTouches[0],v.push({touchID:E,x:n.clientX,y:n.clientY}),m=!0)}D("vmouseout",e,t),d=!1,M()}function I(t){var n=e.data(t,i),r;if(n)for(r in n)if(n[r])return!0;return!1}function q(){}function R(t){var n=t.substr(1);return{setup:function(r,s){I(this)||e.data(this,i,{});var o=e.data(this,i);o[t]=!0,l[t]=(l[t]||0)+1,l[t]===1&&b.bind(n,P),e(this).bind(n,q),y&&(l.touchstart=(l.touchstart||0)+1,l.touchstart===1&&b.bind("touchstart",H).bind("touchend",F).bind("touchmove",j).bind("scroll",B))},teardown:function(r,s){--l[t],l[t]||b.unbind(n,P),y&&(--l.touchstart,l.touchstart||b.unbind("touchstart",H).unbind("touchmove",j).unbind("touchend",F).unbind("scroll",B));var o=e(this),u=e.data(this,i);u&&(u[t]=!1),o.unbind(n,q),I(this)||o.removeData(i)}}}var i="virtualMouseBindings",s="virtualTouchID",o="vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split(" "),u="clientX clientY pageX pageY screenX screenY".split(" "),a=e.event.mouseHooks?e.event.mouseHooks.props:[],f=e.event.props.concat(a),l={},c=0,h=0,p=0,d=!1,v=[],m=!1,g=!1,y="addEventListener"in n,b=e(n),w=1,E=0,S;e.vmouse={moveDistanceThreshold:10,clickDistanceThreshold:10,resetTimerDuration:1500};for(var U=0;Ue.event.special.swipe.scrollSupressionThreshold&&t.preventDefault()}var i=t.originalEvent.touches?t.originalEvent.touches[0]:t,s={time:(new Date).getTime(),coords:[i.pageX,i.pageY],origin:e(t.target)},o;n.bind(a,f).one(u,function(t){n.unbind(a,f),s&&o&&o.time-s.timee.event.special.swipe.horizontalDistanceThreshold&&Math.abs(s.coords[1]-o.coords[1])o.coords[0]?"swipeleft":"swiperight"),s=o=r})})}},e.each({scrollstop:"scrollstart",taphold:"tap",swipeleft:"swipe",swiperight:"swipe"},function(t,n){e.event.special[t]={setup:function(){e(this).bind(n,e.noop)}}})}(e,this),function(e,n){e.extend(e.support,{orientation:"orientation"in t&&"onorientationchange"in t})}(e),function(e){e.event.special.throttledresize={setup:function(){e(this).bind("resize",n)},teardown:function(){e(this).unbind("resize",n)}};var t=250,n=function(){s=(new Date).getTime(),o=s-r,o>=t?(r=s,e(this).trigger("throttledresize")):(i&&clearTimeout(i),i=setTimeout(n,t-o))},r=0,i,s,o}(e),function(e,t){function d(){var e=o();e!==u&&(u=e,r.trigger(i))}var r=e(t),i="orientationchange",s,o,u,a,f,l={0:!0,180:!0};if(e.support.orientation){var c=t.innerWidth||e(t).width(),h=t.innerHeight||e(t).height(),p=50;a=c>h&&c-h>p,f=l[t.orientation];if(a&&f||!a&&!f)l={"-90":!0,90:!0}}e.event.special.orientationchange=e.extend({},e.event.special.orientationchange,{setup:function(){if(e.support.orientation&&!e.event.special.orientationchange.disabled)return!1;u=o(),r.bind("throttledresize",d)},teardown:function(){if(e.support.orientation&&!e.event.special.orientationchange.disabled)return!1;r.unbind("throttledresize",d)},add:function(e){var t=e.handler;e.handler=function(e){return e.orientation=o(),t.apply(this,arguments)}}}),e.event.special.orientationchange.orientation=o=function(){var r=!0,i=n.documentElement;return e.support.orientation?r=l[t.orientation]:r=i&&i.clientWidth/i.clientHeight<1.1,r?"portrait":"landscape"},e.fn[i]=function(e){return e?this.bind(i,e):this.trigger(i)},e.attrFn&&(e.attrFn[i]=!0)}(e,this),function(e,r){var i=e(t),s=e("html");e.mobile.media=function(){var t={},r=e("
"),i=e("").append(r);return function(e){if(!(e in t)){var o=n.createElement("style"),u="@media "+e+" { #jquery-mediatest { position:absolute; } }";o.type="text/css",o.styleSheet?o.styleSheet.cssText=u:o.appendChild(n.createTextNode(u)),s.prepend(i).prepend(o),t[e]=r.css("position")==="absolute",i.add(o).remove()}return t[e]}}()}(e),function(e,r){function i(e){var t=e.charAt(0).toUpperCase()+e.substr(1),n=(e+" "+u.join(t+" ")+t).split(" ");for(var i in n)if(o[n[i]]!==r)return!0}function h(e,t,r){var i=n.createElement("div"),s=function(e){return e.charAt(0).toUpperCase()+e.substr(1)},o=function(e){return"-"+e.charAt(0).toLowerCase()+e.substr(1)+"-"},a=function(n){var r=o(n)+e+": "+t+";",u=s(n),a=u+s(e);i.setAttribute("style",r),!i.style[a]||(l=!0)},f=r?[r]:u,l;for(var c=0;c",{href:t}).appendTo("head"),o=e("").prependTo(s),u=o[0].href,n[0].href=i||location.pathname,r&&r.remove(),u.indexOf(t)===0}function v(){var e=n.createElement("x"),r=n.documentElement,i=t.getComputedStyle,s;return"pointerEvents"in e.style?(e.style.pointerEvents="auto",e.style.pointerEvents="x",r.appendChild(e),s=i&&i(e,"").pointerEvents==="auto",r.removeChild(e),!!s):!1}function m(){var e=n.createElement("div");return typeof e.getBoundingClientRect!="undefined"}var s=e("").prependTo("html"),o=s[0].style,u=["Webkit","Moz","O"],a="palmGetResource"in t,f=t.opera,l=t.operamini&&{}.toString.call(t.operamini)==="[object OperaMini]",c=t.blackberry&&!i("-webkit-transform");e.extend(e.mobile,{browser:{}}),e.mobile.browser.ie=function(){var e=3,t=n.createElement("div"),r=t.all||[];do t.innerHTML="";while(r[0]);return e>4?e:!e}(),e.extend(e.support,{cssTransitions:"WebKitTransitionEvent"in t||h("transition","height 100ms linear")&&!f,pushState:"pushState"in history&&"replaceState"in history,mediaquery:e.mobile.media("only all"),cssPseudoElement:!!i("content"),touchOverflow:!!i("overflowScrolling"),cssTransform3d:p(),boxShadow:!!i("boxShadow")&&!c,scrollTop:("pageXOffset"in t||"scrollTop"in n.documentElement||"scrollTop"in s[0])&&!a&&!l,dynamicBaseTag:d(),cssPointerEvents:v(),boundingRect:m()}),s.remove();var g=function(){var e=t.navigator.userAgent;return e.indexOf("Nokia")>-1&&(e.indexOf("Symbian/3")>-1||e.indexOf("Series60/5")>-1)&&e.indexOf("AppleWebKit")>-1&&e.match(/(BrowserNG|NokiaBrowser)\/7\.[0-3]/)}();e.mobile.gradeA=function(){return(e.support.mediaquery||e.mobile.browser.ie&&e.mobile.browser.ie>=7)&&(e.support.boundingRect||e.fn.jquery.match(/1\.[0-7+]\.[0-9+]?/)!==null)},e.mobile.ajaxBlacklist=t.blackberry&&!t.WebKitPoint||l||g,g&&e(function(){e("head link[rel='stylesheet']").attr("rel","alternate stylesheet").attr("rel","stylesheet")}),e.support.boxShadow||e("html").addClass("ui-mobile-nosupport-boxshadow")}(e),function(e,t){e.widget("mobile.page",e.mobile.widget,{options:{theme:"c",domCache:!1,keepNativeDefault:":jqmData(role='none'), :jqmData(role='nojs')"},_create:function(){var e=this;if(e._trigger("beforecreate")===!1)return!1;e.element.attr("tabindex","0").addClass("ui-page ui-body-"+e.options.theme).bind("pagebeforehide",function(){e.removeContainerBackground()}).bind("pagebeforeshow",function(){e.setContainerBackground()})},removeContainerBackground:function(){e.mobile.pageContainer.removeClass("ui-overlay-"+e.mobile.getInheritedTheme(this.element.parent()))},setContainerBackground:function(t){this.options.theme&&e.mobile.pageContainer.addClass("ui-overlay-"+(t||this.options.theme))},keepNativeSelector:function(){var t=this.options,n=t.keepNative&&e.trim(t.keepNative);return n&&t.keepNative!==t.keepNativeDefault?[t.keepNative,t.keepNativeDefault].join(", "):t.keepNativeDefault}})}(e),function(e,t,r){function l(e){return e=e||location.href,"#"+e.replace(/^[^#]*#?(.*)$/,"$1")}var i="hashchange",s=n,o,u=e.event.special,a=s.documentMode,f="on"+i in t&&(a===r||a>7);e.fn[i]=function(e){return e?this.bind(i,e):this.trigger(i)},e.fn[i].delay=50,u[i]=e.extend(u[i],{setup:function(){if(f)return!1;e(o.start)},teardown:function(){if(f)return!1;e(o.stop)}}),o=function(){function p(){var n=l(),r=h(u);n!==u?(c(u=n,r),e(t).trigger(i)):r!==u&&(location.href=location.href.replace(/#.*/,"")+r),o=setTimeout(p,e.fn[i].delay)}var n={},o,u=l(),a=function(e){return e},c=a,h=a;return n.start=function(){o||p()},n.stop=function(){o&&clearTimeout(o),o=r},e.browser.msie&&!f&&function(){var t,r;n.start=function(){t||(r=e.fn[i].src,r=r&&r+l(),t=e('