From 1b7469aad3ce605373be98f2cb87038d6d1a3a40 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Mon, 19 Jan 2015 21:30:41 +0000 Subject: [PATCH 01/44] 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 02/44] 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 03/44] 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 04/44] 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 05/44] 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 17d756514c9618d02c7a723776906d5e3a801ad8 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Fri, 13 Feb 2015 18:49:31 +0000 Subject: [PATCH 06/44] 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 07/44] 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 08/44] 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 c95ca007b57bd274036c1bd89e07a8abc50e57c6 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sat, 20 Jun 2015 23:35:22 +0100 Subject: [PATCH 09/44] 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 10/44] 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 11/44] 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 12/44] 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 13/44] 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 14/44] 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 76e7faf1aa0714691faf62a38dfdce2e5394c656 Mon Sep 17 00:00:00 2001 From: Chris Hill Date: Sun, 7 Feb 2016 09:27:28 +0000 Subject: [PATCH 15/44] 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 16/44] 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 d28ca7500e682f93a47d7edc126b405835a26242 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 29 Feb 2016 22:35:53 +0100 Subject: [PATCH 17/44] 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 18/44] 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 19/44] 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 e67ad21740a82be508a6e48680955240a15d3aae Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 7 Mar 2016 23:27:28 +0100 Subject: [PATCH 20/44] 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 21/44] 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 22/44] 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 23/44] 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 24/44] 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 25/44] 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 26/44] 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 27/44] 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 28/44] 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 29/44] 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 30/44] 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 31/44] 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 ec0379ea44130c879b3237fde3e8a456bc9da267 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sat, 19 Mar 2016 18:49:55 +0000 Subject: [PATCH 32/44] 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('