Merge branch 'imp/2014.11-performance_application-2014.09' into 2014.11
authorPhilipp Schüle <p.schuele@metaways.de>
Mon, 20 Apr 2015 08:46:53 +0000 (10:46 +0200)
committerPhilipp Schüle <p.schuele@metaways.de>
Mon, 20 Apr 2015 08:46:53 +0000 (10:46 +0200)
237 files changed:
scripts/backup/backup-config.sh [new file with mode: 0644]
scripts/backup/backup-data.sh [new file with mode: 0644]
scripts/backup/restore-config.sh [new file with mode: 0644]
scripts/backup/restore-data.sh [new file with mode: 0644]
scripts/vagrant/Vagrantfile
scripts/vagrant/bootstrap_vagrant.sh
scripts/vagrant/setup-tine20.sh
tests/setup/TestHelper.php
tests/tine20/ActiveSync/AllTests.php
tests/tine20/ActiveSync/Controller/ControllerTest.php [new file with mode: 0644]
tests/tine20/ActiveSync/Frontend/JsonTests.php [new file with mode: 0644]
tests/tine20/ActiveSync/TestCase.php
tests/tine20/Addressbook/Frontend/ActiveSyncTest.php
tests/tine20/Addressbook/JsonTest.php
tests/tine20/Admin/JsonTest.php
tests/tine20/Calendar/Controller/EventGrantsTests.php
tests/tine20/Calendar/Controller/EventNotificationsTests.php
tests/tine20/Calendar/Convert/Event/VCalendar/MacOSXTest.php
tests/tine20/Calendar/Export/ICalTest.php
tests/tine20/Calendar/Frontend/ActiveSyncTest.php
tests/tine20/Calendar/Frontend/iMIPTest.php
tests/tine20/Calendar/Import/files/ios_private.ics [new file with mode: 0644]
tests/tine20/Crm/AbstractTest.php
tests/tine20/Crm/Export/AbstractTest.php
tests/tine20/Crm/JsonTest.php
tests/tine20/Crm/NotificationsTests.php
tests/tine20/Felamimail/Controller/Cache/MessageTest.php
tests/tine20/Felamimail/Frontend/JsonTest.php
tests/tine20/Felamimail/Model/AccountTest.php
tests/tine20/Filemanager/Controller/DownloadLinkTests.php
tests/tine20/Sales/InvoiceControllerTests.php
tests/tine20/Sales/InvoiceExportTests.php
tests/tine20/Sales/InvoiceJsonTests.php
tests/tine20/Tasks/Frontend/ActiveSyncTest.php
tests/tine20/TestCase.php
tests/tine20/Timetracker/FilterTest.php
tests/tine20/Tinebase/AccountTest.php
tests/tine20/Tinebase/ConfigTest.php
tests/tine20/Tinebase/ContainerTest.php
tests/tine20/Tinebase/Frontend/CliTest.php
tests/tine20/Tinebase/Frontend/Json/PersistentFilterTest.php
tests/tine20/Tinebase/User/EmailUser/Imap/DovecotTest.php
tests/tine20/Tinebase/User/EmailUser/Smtp/PostfixTest.php
tests/tine20/Tinebase/User/SqlTest.php
tests/tine20/Tinebase/files/configtest.inc.php [new file with mode: 0644]
tine20/ActiveSync/Acl/Rights.php [new file with mode: 0644]
tine20/ActiveSync/ActiveSync.jsb2
tine20/ActiveSync/Controller/SyncDevices.php [new file with mode: 0644]
tine20/ActiveSync/Frontend/Json.php
tine20/ActiveSync/Model/Device.php
tine20/ActiveSync/Model/DeviceFilter.php
tine20/ActiveSync/Setup/Update/Release8.php [new file with mode: 0644]
tine20/ActiveSync/Setup/setup.xml
tine20/ActiveSync/js/AdminPanel.js [new file with mode: 0644]
tine20/ActiveSync/js/EditDialog.js [new file with mode: 0644]
tine20/ActiveSync/js/SyncDevices.js [new file with mode: 0644]
tine20/ActiveSync/js/SyncDevicesGridPanel.js [new file with mode: 0644]
tine20/Addressbook/Setup/DemoData.php
tine20/Admin/js/Admin.js
tine20/Calendar/Backend/Sql.php
tine20/Calendar/Config.php
tine20/Calendar/Controller.php
tine20/Calendar/Controller/Event.php
tine20/Calendar/Controller/EventNotifications.php
tine20/Calendar/Controller/MSEventFacade.php
tine20/Calendar/Controller/Resource.php
tine20/Calendar/Convert/Event/VCalendar/Abstract.php
tine20/Calendar/Convert/Event/VCalendar/Iphone.php
tine20/Calendar/Convert/Event/VCalendar/MacOSX.php
tine20/Calendar/Frontend/Json.php
tine20/Calendar/Frontend/WebDAV/Event.php
tine20/Calendar/Import/CalDAV.php
tine20/Calendar/Model/Attender.php
tine20/Calendar/Model/AttenderFilter.php
tine20/Calendar/Model/EventFilter.php
tine20/Calendar/Preference.php
tine20/Calendar/css/print.css
tine20/Calendar/js/AttendeeGridPanel.js
tine20/Calendar/js/DaysView.js
tine20/Calendar/js/EventEditDialog.js
tine20/Calendar/js/EventUI.js
tine20/Calendar/js/MainScreenCenterPanel.js
tine20/Calendar/js/Model.js
tine20/Calendar/js/Printer/DaysView.js
tine20/Calendar/js/RrulePanel.js
tine20/Calendar/translations/de.po
tine20/Courses/Controller/Course.php
tine20/Crm/Backend/Lead.php
tine20/Crm/Controller.php
tine20/Crm/Controller/Lead.php
tine20/Crm/Frontend/Json.php
tine20/Crm/Setup/DemoData.php
tine20/Crm/Setup/Update/Release8.php
tine20/Crm/Setup/setup.xml
tine20/Crm/js/Contact.js
tine20/Crm/js/LeadEditDialog.js
tine20/Crm/js/LeadGridDetailsPanel.js
tine20/Crm/js/LeadGridPanel.js
tine20/Crm/js/LeadSource.js
tine20/Crm/js/LeadState.js
tine20/Crm/js/Model.js
tine20/Crm/translations/de.po
tine20/Crm/translations/en.po
tine20/Crm/translations/template.pot
tine20/Crm/views/newLeadPlain.php
tine20/Felamimail/Controller/Account.php
tine20/Felamimail/Controller/Message.php
tine20/Felamimail/Frontend/ActiveSync.php
tine20/Felamimail/Model/Account.php
tine20/Felamimail/js/TreePanel.js
tine20/Filemanager/translations/template.pot
tine20/HumanResources/Frontend/Json.php
tine20/HumanResources/Setup/DemoData.php
tine20/HumanResources/js/ContractGridPanel.js
tine20/HumanResources/js/DatePicker.js
tine20/HumanResources/js/FreeTimeEditDialog.js
tine20/HumanResources/translations/de.po
tine20/HumanResources/translations/en.po
tine20/HumanResources/translations/template.pot
tine20/Inventory/Model/InventoryItem.php
tine20/Inventory/translations/de.po
tine20/Inventory/translations/en.po
tine20/Inventory/translations/template.pot
tine20/Sales/Acl/Rights.php
tine20/Sales/Config.php
tine20/Sales/Controller/Contract.php
tine20/Sales/Controller/Invoice.php
tine20/Sales/Frontend/Http.php
tine20/Sales/Frontend/Json.php
tine20/Sales/Model/CostCenter.php
tine20/Sales/Model/ProductAggregate.php
tine20/Sales/Setup/Update/Release8.php
tine20/Sales/Setup/setup.xml
tine20/Sales/js/BillingAddressGridPanel.js
tine20/Sales/js/ContractCostCenterFilterModel.js
tine20/Sales/js/CostCenterEditDialog.js
tine20/Sales/js/DeliveryAddressGridPanel.js
tine20/Sales/js/DivisionEditDialog.js
tine20/Sales/js/InvoiceDetailsPanel.js
tine20/Sales/js/InvoiceEditDialog.js
tine20/Sales/js/OrderConfirmationEditDialog.js
tine20/Sales/js/ProductEditDialog.js
tine20/Sales/translations/de.po
tine20/Sales/translations/en.po
tine20/Sales/translations/template.pot
tine20/Setup/Backend/Pgsql.php
tine20/Setup/Backend/Schema/Field/Abstract.php
tine20/Setup/Controller.php
tine20/Setup/Frontend/Cli.php
tine20/Setup/Server/Cli.php
tine20/Setup/Update/Abstract.php
tine20/Setup/js/EmailPanel.js
tine20/Timetracker/Model/Timeaccount.php
tine20/Timetracker/js/TimeaccountEditDialog.js
tine20/Timetracker/js/TimesheetEditDialog.js
tine20/Tinebase/Backend/Sql/Abstract.php
tine20/Tinebase/Backend/Sql/Command/Interface.php
tine20/Tinebase/Backend/Sql/Command/Mysql.php
tine20/Tinebase/Backend/Sql/Command/Oracle.php
tine20/Tinebase/Backend/Sql/Command/Pgsql.php
tine20/Tinebase/Cache/PerRequest.php [new file with mode: 0644]
tine20/Tinebase/Config/Abstract.php
tine20/Tinebase/Container.php
tine20/Tinebase/Controller.php
tine20/Tinebase/Controller/Record/Abstract.php
tine20/Tinebase/Controller/ScheduledImport.php
tine20/Tinebase/Convert/Interface.php
tine20/Tinebase/Convert/Json.php
tine20/Tinebase/Core.php
tine20/Tinebase/EmailUser.php
tine20/Tinebase/EmailUser/Imap/Cyrus.php
tine20/Tinebase/EmailUser/Imap/Dbmail.php
tine20/Tinebase/EmailUser/Imap/Dovecot.php
tine20/Tinebase/EmailUser/Imap/DovecotCombined.php [new file with mode: 0644]
tine20/Tinebase/EmailUser/Imap/Interface.php [new file with mode: 0644]
tine20/Tinebase/EmailUser/Imap/LdapDbmailSchema.php
tine20/Tinebase/EmailUser/Imap/LdapUniventionMailSchema.php [new file with mode: 0644]
tine20/Tinebase/EmailUser/Imap/Standard.php [new file with mode: 0644]
tine20/Tinebase/EmailUser/Ldap.php
tine20/Tinebase/EmailUser/Smtp/Interface.php [new file with mode: 0644]
tine20/Tinebase/EmailUser/Smtp/LdapDbmailSchema.php
tine20/Tinebase/EmailUser/Smtp/LdapMailSchema.php
tine20/Tinebase/EmailUser/Smtp/LdapQmailSchema.php
tine20/Tinebase/EmailUser/Smtp/LdapUniventionMailSchema.php [new file with mode: 0644]
tine20/Tinebase/EmailUser/Smtp/Postfix.php
tine20/Tinebase/EmailUser/Smtp/PostfixCombined.php [new file with mode: 0644]
tine20/Tinebase/EmailUser/Smtp/Standard.php [new file with mode: 0644]
tine20/Tinebase/EmailUser/Sql.php
tine20/Tinebase/Export/Abstract.php
tine20/Tinebase/Frontend/Cli.php
tine20/Tinebase/Frontend/Http.php
tine20/Tinebase/Frontend/Json.php
tine20/Tinebase/Frontend/Json/Abstract.php
tine20/Tinebase/Frontend/Json/PersistentFilter.php
tine20/Tinebase/Group/Ldap.php
tine20/Tinebase/Import/Abstract.php
tine20/Tinebase/ImportExportDefinition.php
tine20/Tinebase/Model/EmailUser.php
tine20/Tinebase/Model/Filter/ExplicitRelatedRecord.php
tine20/Tinebase/Model/Filter/Id.php
tine20/Tinebase/Model/Filter/Month.php
tine20/Tinebase/Model/FullUser.php
tine20/Tinebase/Model/FullUserFilter.php [new file with mode: 0644]
tine20/Tinebase/Record/Abstract.php
tine20/Tinebase/Record/Interface.php
tine20/Tinebase/Record/RecordSet.php
tine20/Tinebase/Relation/Backend/Sql.php
tine20/Tinebase/Server/Cli.php
tine20/Tinebase/Setup/Update/Release8.php
tine20/Tinebase/User.php
tine20/Tinebase/User/Abstract.php
tine20/Tinebase/User/Interface.php
tine20/Tinebase/User/Ldap.php
tine20/Tinebase/User/Plugin/Abstract.php
tine20/Tinebase/User/Plugin/LdapAbstract.php
tine20/Tinebase/User/Plugin/Samba.php
tine20/Tinebase/User/Plugin/SqlInterface.php
tine20/Tinebase/User/Sql.php
tine20/Tinebase/WebDav/Container/Abstract.php
tine20/Tinebase/WebDav/PrincipalBackend.php
tine20/Tinebase/js/Application.js
tine20/Tinebase/js/ApplicationStarter.js
tine20/Tinebase/js/PasswordChangeDialog.js
tine20/Tinebase/js/UserProfilePanel.js
tine20/Tinebase/js/tine20-loginbox.js
tine20/Tinebase/js/ux/Function.createBuffered.js
tine20/Tinebase/js/ux/Printer/renderers/Base.js
tine20/Tinebase/js/widgets/ContentTypeTreePanel.js
tine20/Tinebase/js/widgets/MainScreen.js
tine20/Tinebase/js/widgets/container/TreePanel.js
tine20/Tinebase/js/widgets/customfields/EditDialogPlugin.js
tine20/Tinebase/js/widgets/customfields/Field.js
tine20/Tinebase/js/widgets/dialog/EditDialog.js
tine20/Tinebase/js/widgets/dialog/PreferencesPanel.js
tine20/Tinebase/js/widgets/grid/ForeignRecordFilter.js
tine20/Tinebase/js/widgets/grid/GridPanel.js
tine20/Tinebase/js/widgets/relation/GenericPickerGridPanel.js

diff --git a/scripts/backup/backup-config.sh b/scripts/backup/backup-config.sh
new file mode 100644 (file)
index 0000000..6338487
--- /dev/null
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+BACKUP_PATH=$(mktemp --directory --tmpdir=/tmp/)
+TODAY=$(date +"%Y-%m-%d")
+
+test -d /var/lib/tine20/backup/$TODAY || mkdir -p /var/lib/tine20/backup/$TODAY
+
+cp -ra /etc/tine20/config.inc.php /var/lib/tine20/backup/$TODAY
\ No newline at end of file
diff --git a/scripts/backup/backup-data.sh b/scripts/backup/backup-data.sh
new file mode 100644 (file)
index 0000000..8282598
--- /dev/null
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+BACKUP_PATH=$(mktemp --directory --tmpdir=/tmp/)
+TODAY=$(date +"%Y-%m-%d")
+
+if [ ! -x /usr/bin/innobackupex ]; then
+    echo "innobackupex not found exiting"
+    exit 1
+fi
+
+if [ ! -x /usr/bin/xtrabackup ]; then
+    echo "xtrabackup not found exiting"
+    exit 1
+fi
+
+# MyISAM and Innodb tables
+innobackupex --defaults-extra-file /etc/tine20/xtrabackup.cnf --no-timestamp $BACKUP_PATH/mysql
+
+# prepare Innodb tables
+xtrabackup --prepare --target-dir=$BACKUP_PATH/mysql
+xtrabackup --prepare --target-dir=$BACKUP_PATH/mysql
+
+
+(cd $BACKUP_PATH/mysql/ && tar cjf ../full_mysql.tar.bz2 .)
+
+rm -rf $BACKUP_PATH/mysql
+
+(cd /var/lib/tine20/files; tar cjf $BACKUP_PATH/tine20_files.tar.bz2 .)
+
+test -d /var/lib/tine20/backup/$TODAY || mkdir -p /var/lib/tine20/backup/$TODAY
+
+mv $BACKUP_PATH/full_mysql.tar.bz2 /var/lib/tine20/backup/$TODAY
+mv $BACKUP_PATH/tine20_files.tar.bz2 /var/lib/tine20/backup/$TODAY
+
+rm -rf $BACKUP_PATH
+
diff --git a/scripts/backup/restore-config.sh b/scripts/backup/restore-config.sh
new file mode 100644 (file)
index 0000000..d157889
--- /dev/null
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+SOURCE_PATH=$1
+
+if [ -e $SOURCE_PATH/config.inc.php ]; then
+    cp $SOURCE_PATH/config.inc.php /etc/tine20/config.inc.php
+    chown root:www-data /etc/tine20/config.inc.php
+    chmod 0660 /etc/tine20/config.inc.php
+fi
\ No newline at end of file
diff --git a/scripts/backup/restore-data.sh b/scripts/backup/restore-data.sh
new file mode 100644 (file)
index 0000000..b7b788c
--- /dev/null
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+SOURCE_PATH=$1
+
+(cd /var/lib/mysql/; tar xf $SOURCE_PATH/full_mysql.tar.bz2 .)
+(cd /var/lib/tine20/files; tar xf $SOURCE_PATH/tine20_files.tar.bz2 .)
+
+chown -R mysql:mysql /var/lib/mysql/*
+chown -R www-daza:www-data /var/lib/tine20/files/*
\ No newline at end of file
index 9d07910..d1fd1f5 100644 (file)
@@ -6,7 +6,8 @@ VAGRANTFILE_API_VERSION = "2"
 
 Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
        # Name
-       config.vm.box = "precise32"
+       config.vm.box = "saucy-server-cloudimg-amd64-vagrant-disk1"
+    config.vm.box_url = "https://cloud-images.ubuntu.com/vagrant/saucy/current/saucy-server-cloudimg-amd64-vagrant-disk1.box"
 
        # vagrant-cachier caches packages for your vm local
        if Vagrant.has_plugin?("vagrant-cachier")
@@ -25,10 +26,10 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
        config.vm.provision :shell, :path => "bootstrap_vagrant.sh"
 
        # Configure NFS shared folder
-       config.vm.synced_folder "../../", "/vagrant/tine20.git", type: "nfs"
-       config.vm.synced_folder "", "/vagrant/", type: "nfs"
-
-        #config.vm.synced_folder "../../", "/vagrant/tine20.git", :nfs => false
+    config.vm.synced_folder "", "/vagrant/", type: "nfs"
+    # config.vm.synced_folder "../../", "/usr/local/share/tine20.git", type: "rsync", rsync__exclude: "/.git/"
+    # config.vm.synced_folder "../../", "/usr/local/share/tine20.git", :nfs => false
+    config.vm.synced_folder "../../", "/usr/local/share/tine20.git", type: "nfs"
 
        # Create a private network, which allows host-only access to the machine
        # using a specific IP.
index 7a04e24..ec4ed69 100644 (file)
@@ -76,7 +76,7 @@ phpmyadmin_vhost="
        ServerAlias     www.pma.local\n
 </VirtualHost>\n"
 
-echo -e $phpmyadmin_vhost > /etc/apache2/sites-available/pma-local
+echo -e $phpmyadmin_vhost > /etc/apache2/sites-available/pma-local.conf
 
 # Enable pma-local for apache
 a2ensite pma-local
@@ -101,9 +101,14 @@ if [ -e /etc/php5/conf.d/xdebug.ini ]; then
     echo xdebug.remote_log=/vagrant/logs/php5-xdebug.log >> /etc/php5/conf.d/xdebug.ini
 fi
 
-###################### INSTALL TINE20  ######################
-if [ -d /vagrant/tine20.git/tine20 ]; then
-    source /vagrant/setup-tine20.sh
+
+# Fix mcrypt
+ln -sf /etc/php5/conf.d/mcrypt.ini /etc/php5/mods-available/mcrypt.ini
+php5enmod mcrypt
+
+###################### INSTALL TINE20 ######################
+if [ -d /usr/local/share/tine20.git/tine20 ]; then
+    source /usr/local/share/tine20.git/scripts/vagrant/setup-tine20.sh
 fi
 
 ###################### FINALS ###########################
index fa804c9..32e80dc 100755 (executable)
@@ -3,10 +3,15 @@
 # Tine 2.0 Vhost
 tine20_vhost="
 <VirtualHost *:80>\n
-    DocumentRoot   \"/vagrant/tine20.git/tine20/\"\n
+    DocumentRoot   \"/usr/local/share/tine20.git/tine20/\"\n
     ServerName      tine20.vagrant\n
     ServerAlias     www.tine20.vagrant\n
     \n
+     <Directory /usr/local/share/tine20.git/tine20/>\n
+       AllowOverride None\n
+       Require all granted\n
+     </Directory>\n
+     \n
     # Active Sync\n
     RewriteEngine on\n
     RewriteRule ^/Microsoft-Server-ActiveSync /index.php?frontend=activesync [E=REMOTE_USER:%{HTTP:Authorization},L,QSA]\n
@@ -21,14 +26,14 @@ tine20_vhost="
     RewriteRule ^/webdav       /index.php?frontend=webdav [E=REMOTE_USER:%{HTTP:Authorization},L,QSA]\n
     RewriteRule ^/remote.php   /index.php?frontend=webdav [E=REMOTE_USER:%{HTTP:Authorization},L,QSA]\n
     \n
-    php_value include_path "/vagrant/conf:/vagrant/tine20.git/tine20/"\n
+    php_value include_path "/etc/tine20:/usr/local/share/tine20.git/tine20/"\n
     \n
-    ErrorLog "/vagrant/logs/error_log"\n
-    CustomLog "/vagrant/logs/access_log" common\n
+    ErrorLog "/var/log/tine20/error_log"\n
+    CustomLog "/var/log/tine20/access_log" common\n
     \n
 </VirtualHost>\n"
 
-echo -e $tine20_vhost > /etc/apache2/sites-available/tine20-vagrant
+echo -e $tine20_vhost > /etc/apache2/sites-available/tine20-vagrant.conf
 
 service apache2 restart
 
@@ -39,12 +44,15 @@ mysql -u root -p"vagrant" -e "CREATE DATABASE tine20;"
 a2ensite tine20-vagrant
 
 # update dependencies
-cd /vagrant/tine20.git/tine20
+cd /usr/local/share/tine20.git/tine20
 sudo -u vagrant composer install --dev --prefer-source --no-interaction
 
 # setup directories
-mkdir -p /vagrant/logs /vagrant/conf /vagrant/cache /vagrant/files /vagrant/tmp
-chown www-data /vagrant/logs /vagrant/conf /vagrant/cache /vagrant/files /vagrant/tmp
+mkdir -p /var/lib/tine20/cache /var/lib/tine20/files /var/lib/tine20/tmp
+mkdir -p /etc/tine20
+mkdir -p /var/log/tine20
+chown -R vagrant /var/lib/tine20
+chown -R vagrant /var/log/tine20
 
 # generate config.inc.php
 tine20_config="
@@ -68,21 +76,21 @@ tine20_config="
     \n
     'caching' => array (\n
         'active' => true,\n
-        'path' => '/vagrant/cache',\n
+        'path' => '/var/lib/tine20/cache',\n
         'lifetime' => 3600,\n
     ),\n
     \n
     'logger' => array (\n
         'active' => true,\n
-        'filename' => '/vagrant/logs/tine20.log',\n
+        'filename' => '/var/log/tine20/tine20.log',\n
         'priority' => '7',\n
     ),\n
-    'filesdir'  => '/vagrant/files',\n
-    'tmpdir' => '/vargrant/tmp',\n
+    'filesdir'  => '/var/lib/tine20/files',\n
+    'tmpdir' => '/var/lib/tine20/tmp',\n
   );\n"
 
-if [ ! -f /vagrant/conf/config.inc.php ]; then
-  echo -e $tine20_config > /vagrant/conf/config.inc.php
+if [ ! -f /etc/tine20/config.inc.php ]; then
+  echo -e $tine20_config > /etc/tine20/config.inc.php
 fi
 
 # generate install.properties
@@ -95,9 +103,30 @@ adminEmailAddress=vagrant@tine20.vagrant\n
 #imap=\n
 #smtp=\n"
 
-if [ ! -f /vagrant/conf/install.properties ]; then
-  echo -e $tine20_installprops > /vagrant/conf/install.properties
+if [ ! -f /etc/tine20/install.properties ]; then
+  echo -e $tine20_installprops > /etc/tine20/install.properties
 fi
 
-cd /vagrant/tine20.git/tine20
-sudo -u vagrant /vagrant/tine20.git/tine20/vendor/bin/phing -D configdir=/vagrant/conf/ tine-install
+cd /usr/local/share/tine20.git/tine20
+sudo -u vagrant /usr/local/share/tine20.git/tine20/vendor/bin/phing -D configdir=/etc/tine20/ tine-install
+
+echo "
+##################################################################################
+# Welcome to tine20.vagrant
+#
+# 1. make sure you have a tine20.vagrant entry in you /etc/hosts file
+#    with your vagrant machine ip (10.10.10.10 per default)
+# 2. navigate in your browser to
+#      http://tine20.vagrant (normal login)
+#      http://tine20.vagrant/setup.php (setup)
+#        username: vagrant
+#        password: vagrant
+# 3. to run phpunit tests:
+       vagrant ssh
+       cd /usr/local/share/tine20.git/tests/tine20
+       /usr/local/share/tine20.git/tine20/vendor/bin/phpunit \
+ -d include_path=/etc/tine20 -d memory_limit=-1 AllTests.php
+#
+# 4. Happy codeing! :-)
+##################################################################################
+"
\ No newline at end of file
index 896c46f..637332b 100644 (file)
@@ -5,8 +5,10 @@
  * @package     setup tests
  * @subpackage  test root
  * @license     http://www.gnu.org/licenses/agpl.html AGPL3
- * @copyright   Copyright (c) 2008 Metaways Infosystems GmbH (http://www.metaways.de)
- * @author      Philipp Schuele <p.schuele@metaways.de>
+ * @copyright   Copyright (c) 2008-2015 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Philipp Schüle <p.schuele@metaways.de>
+ * 
+ * @todo        refactor setup tests bootstrap
  */
 
 /*
@@ -51,11 +53,8 @@ if (version_compare($phpUnitVersion[1], "3.6.0") >= 0) {
 }
 
 // get config
-if(file_exists(dirname(__FILE__) . '/phpunitconfig.inc.php')) {
-    $config = new Zend_Config(require dirname(__FILE__) . '/phpunitconfig.inc.php');
-} else {
-    throw new Exception("Couldn't find phpunitconfig.inc.php! \n");
-}
+$configData = include('phpunitconfig.inc.php');
+$config = new Zend_Config($configData);
 
 $_SERVER['DOCUMENT_ROOT'] = $config->docroot;
 
index c0680cb..2a6bcac 100755 (executable)
@@ -4,7 +4,7 @@
  * 
  * @package     ActiveSync
  * @license     http://www.gnu.org/licenses/agpl.html
- * @copyright   Copyright (c) 2010-2010 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2010-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Jonas Fischer <j.fischer@metaways.de>
  */
 
@@ -30,6 +30,8 @@ class ActiveSync_AllTests
         
         $suite->addTestSuite('ActiveSync_TimezoneConverterTest');
         
+        $suite->addTestSuite('ActiveSync_Frontend_JsonTests');
+        
         return $suite;
     }
 }
diff --git a/tests/tine20/ActiveSync/Controller/ControllerTest.php b/tests/tine20/ActiveSync/Controller/ControllerTest.php
new file mode 100644 (file)
index 0000000..49b10e1
--- /dev/null
@@ -0,0 +1,230 @@
+<?php
+/**
+ * Tine 2.0 - http://www.tine20.org
+ * 
+ * @package     ActiveSync
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2010-2015 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * abstract test class for activesync controller tests
+ * 
+ * @package     ActiveSync
+ */
+abstract class ActiveSync_Controller_ControllerTest extends ActiveSync_TestCase
+{
+    /**
+     * name of the controller
+     *
+     * @var string
+     */
+    protected $_controllerName;
+    
+    /**
+     * @var ActiveSync_Controller_Abstract controller
+     */
+    protected $_controller;
+    
+    /**
+     *
+     * @return Syncroton_Model_Folder
+     */
+    public function testCreateFolder()
+    {
+        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE), new Tinebase_DateTime(null, null, 'de_DE'));
+        
+        $syncrotonFolder = $controller->createFolder(new Syncroton_Model_Folder(array(
+            'parentId' => 0, 
+            'displayName' => 'TestFolder'
+        )));
+    
+        $this->assertTrue(!empty($syncrotonFolder->serverId));
+        
+        return $syncrotonFolder;
+    }
+    
+    /**
+     * 
+     * @return Syncroton_Model_Folder
+     */
+    public function testUpdateFolder()
+    {
+        $syncrotonFolder = $this->testCreateFolder();
+        $syncrotonFolder->displayName = 'RenamedTestFolder';
+    
+        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE), new Tinebase_DateTime(null, null, 'de_DE'));
+    
+        $updatedSyncrotonFolder = $controller->updateFolder($syncrotonFolder);
+        
+        $allFolders = $controller->getAllFolders();
+        
+        $this->assertArrayHasKey($syncrotonFolder->serverId, $allFolders);
+        $this->assertEquals('RenamedTestFolder', $allFolders[$syncrotonFolder->serverId]->displayName);
+        
+        return $updatedSyncrotonFolder;
+    }
+    
+    /**
+     * @return Syncroton_Model_Folder
+     */
+    public function testUpdateFolderAndroid()
+    {
+        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_ANDROID), new Tinebase_DateTime(null, null, 'de_DE'));
+        
+        $syncrotonFolders = $controller->getAllFolders();
+    
+        #var_dump($syncrotonFolders); return;
+        
+        $this->setExpectedException('Syncroton_Exception_UnexpectedValue');
+        
+        $updatedSyncrotonFolder = $controller->updateFolder($syncrotonFolders[$this->_specialFolderName]);
+    }
+    
+    /**
+     * test if changed folders got returned
+     */
+    public function testGetChangedFolders()
+    {
+        $syncrotonFolder = $this->testUpdateFolder();
+        
+        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE), new Tinebase_DateTime(null, null, 'de_DE'));
+        
+        $changedFolders = $controller->getChangedFolders(Tinebase_DateTime::now()->subMinute(1), Tinebase_DateTime::now());
+        
+        //var_dump($changedFolders);
+        
+        $this->assertGreaterThanOrEqual(1, count($changedFolders));
+        $this->assertArrayHasKey($syncrotonFolder->serverId, $changedFolders);
+    }
+    
+    public function testDeleteFolder()
+    {
+        $this->markTestIncomplete('not yet implemented in controller');
+        
+        $syncrotonFolder = $this->testCreateFolder();
+    
+        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE), new Tinebase_DateTime(null, null, 'de_DE'));
+    
+        $controller->deleteFolder($syncrotonFolder);
+    }
+    
+    public function testGetAllFolders()
+    {
+        $syncrotonFolder = $this->testCreateFolder();
+    
+        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE), new Tinebase_DateTime(null, null, 'de_DE'));
+    
+        $allSyncrotonFolders = $controller->getAllFolders();
+        
+        $this->assertArrayHasKey($syncrotonFolder->serverId, $allSyncrotonFolders);
+        $this->assertArrayNotHasKey($this->_specialFolderName, $allSyncrotonFolders);
+        $this->assertTrue($allSyncrotonFolders[$syncrotonFolder->serverId] instanceof Syncroton_Model_Folder);
+        $this->assertEquals($syncrotonFolder->serverId, $allSyncrotonFolders[$syncrotonFolder->serverId]->serverId, 'serverId mismatch');
+        $this->assertEquals($syncrotonFolder->parentId, $allSyncrotonFolders[$syncrotonFolder->serverId]->parentId, 'parentId mismatch');
+        $this->assertEquals($syncrotonFolder->displayName, $allSyncrotonFolders[$syncrotonFolder->serverId]->displayName);
+        $this->assertTrue(!empty($allSyncrotonFolders[$syncrotonFolder->serverId]->type));
+    }
+    
+    public function testGetAllFoldersPalm()
+    {
+        $syncrotonFolder = $this->testCreateFolder();
+    
+        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_WEBOS), new Tinebase_DateTime(null, null, 'de_DE'));
+    
+        $allSyncrotonFolders = $controller->getAllFolders();
+        
+        $this->assertArrayHasKey($this->_specialFolderName, $allSyncrotonFolders, "key {$this->_specialFolderName} not found in " . print_r($allSyncrotonFolders, true));
+    }
+    
+    /**
+     * testDeleteEntry
+     */
+    public function testDeleteEntry()
+    {
+        $syncrotonFolder = $this->testCreateFolder();
+        
+        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE), new Tinebase_DateTime(null, null, 'de_DE'));
+        
+        list($serverId, $syncrotonContact) = $this->testCreateEntry($syncrotonFolder);
+        
+        $controller->deleteEntry($syncrotonFolder->serverId, $serverId, null);
+        
+        try {
+            $syncrotonContact = $controller->getEntry(new Syncroton_Model_SyncCollection(array('collectionId' => $syncrotonFolder->serverId)), $serverId);
+            $this->fail('should have thrown Syncroton_Exception_NotFound: '
+                . var_export($syncrotonContact, TRUE)
+                . ' tine contact: ' . print_r(Addressbook_Controller_Contact::getInstance()->get($serverId)->toArray(), TRUE));
+        } catch (Syncroton_Exception_NotFound $senf) {
+            $this->assertEquals('Syncroton_Exception_NotFound', get_class($senf));
+        }
+    }
+    
+    public function testGetInvalidEntry()
+    {
+        $syncrotonFolder = $this->testCreateFolder();
+    
+        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE), new Tinebase_DateTime(null, null, 'de_DE'));
+    
+        $this->setExpectedException('Syncroton_Exception_NotFound');
+    
+        $syncrotonContact = $controller->getEntry(new Syncroton_Model_SyncCollection(array('collectionId' => $syncrotonFolder->serverId)), 'jdszfegd63gfrk');
+    }
+    
+    /**
+     * test get changed entries
+     */
+    public function testGetChangedEntries()
+    {
+        $syncrotonFolder = $this->testCreateFolder();
+    
+        list($serverId, $syncrotonContact) = $this->testUpdateEntry($syncrotonFolder);
+    
+        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE), new Tinebase_DateTime(null, null, 'de_DE'));
+    
+        $changedEntries = $controller->getChangedEntries($syncrotonFolder->serverId, new DateTime('2000-01-01'));
+        
+        $this->assertContains($serverId, $changedEntries, 'did not get changed record id in ' . print_r($changedEntries, TRUE));
+    }
+    
+    /**
+     * test get changed entries for android
+     */
+    public function testGetChangedEntriesAndroid()
+    {
+        $syncrotonFolder = $this->testCreateFolder();
+    
+        list($serverId, $syncrotonContact) = $this->testUpdateEntry($syncrotonFolder);
+    
+        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_ANDROID), Tinebase_DateTime::now());
+    
+        $changedEntries = $controller->getChangedEntries($this->_specialFolderName, new Tinebase_DateTime('2000-01-01'));
+    
+        $this->assertContains($serverId, $changedEntries, 'did not get changed record id in ' . print_r($changedEntries, TRUE));
+    }
+    
+    /**
+     * test convert from XML to Tine 2.0 model
+     */
+    abstract public function testCreateEntry($syncrotonFolder = null);
+    
+    /**
+     * test xml generation for sync to client
+     */
+    abstract public function testUpdateEntry($syncrotonFolder = null);
+    
+    /**
+     * get application activesync controller
+     * 
+     * @param ActiveSync_Model_Device $_device
+     */
+    protected function _getController(ActiveSync_Model_Device $_device)
+    {
+        if ($this->_controller === null) {
+            $this->_controller = Syncroton_Data_Factory::factory($this->_class, $_device, new Tinebase_DateTime(null, null, 'de_DE'));
+        } 
+        
+        return $this->_controller;
+    }
+}
diff --git a/tests/tine20/ActiveSync/Frontend/JsonTests.php b/tests/tine20/ActiveSync/Frontend/JsonTests.php
new file mode 100644 (file)
index 0000000..8c0b2c6
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+/**
+ * Tine 2.0 - http://www.tine20.org
+ * 
+ * @package     ActiveSync
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2015 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Philipp Schüle <p.schuele@metaways.de>
+ */
+
+/**
+ * Test class for ActiveSync_Frontend_Json
+ * 
+ * @package     ActiveSync
+ */
+class ActiveSync_Frontend_JsonTests extends ActiveSync_TestCase
+{
+    /**
+     * lazy init of uit
+     *
+     * @return ActiveSync_Frontend_Json
+     *
+     * @todo fix ide object class detection for completions
+     */
+    protected function _getUit()
+    {
+        return parent::_getUit();
+    }
+    
+    /**
+     * Search for records matching given arguments
+     */
+    public function testSearchSyncDevices()
+    {
+        $device = $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE);
+        
+        $result = $this->_getUit()->searchSyncDevices(array(), array());
+        
+        $this->assertEquals(1, $result['totalcount']);
+        $this->assertEquals('iphone-abcd', $result['results'][0]['deviceid'], print_r($result['results'], true));
+    }
+    
+    /**
+     * deletes existing records
+     */
+    public function testDeleteSyncDevices()
+    {
+        $device = $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE);
+        $this->_getUit()->deleteSyncDevices(array($device->id));
+        $result = $this->_getUit()->searchSyncDevices(array(), array());
+        
+        $this->assertEquals(0, $result['totalcount']);
+    }
+    
+    /**
+     * Return a single record
+     * 
+     * @return array
+     */
+    public function testGetSyncDevice()
+    {
+        $device = $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE);
+        $fetchedDevice = $this->_getUit()->getSyncDevice($device->id);
+        
+        $this->assertTrue(is_array($fetchedDevice['owner_id']), print_r($fetchedDevice, true));
+        $this->assertEquals($this->_testUser->getId(), $fetchedDevice['owner_id']['accountId'], print_r($fetchedDevice['owner_id'], true));
+        
+        return $fetchedDevice;
+    }
+    
+    /**
+     * updates a record
+     */
+    public function testSaveSyncDevice()
+    {
+        $device = $this->testGetSyncDevice();
+        
+        $device['friendlyname'] = 'Very friendly name';
+        $device['owner_id'] = $device['owner_id']['accountId'];
+        $updatedDevice = $this->_getUit()->saveSyncDevice($device);
+        
+        $this->assertEquals($device['friendlyname'], $updatedDevice['friendlyname']);
+    }
+}
index 1764e00..f6acf86 100644 (file)
@@ -4,12 +4,12 @@
  * 
  * @package     ActiveSync
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
- * @copyright   Copyright (c) 2010-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2010-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Lars Kneschke <l.kneschke@metaways.de>
  */
 
 /**
- * abstract test class for activesync frontend tests
+ * abstract test class for activesync tests
  * 
  * @package     ActiveSync
  */
@@ -23,18 +23,6 @@ abstract class ActiveSync_TestCase extends TestCase
     protected $_applicationName;
     
     /**
-     * name of the controller
-     * 
-     * @var string
-     */
-    protected $_controllerName;
-    
-    /**
-     * @var ActiveSync_Frontend_Abstract controller
-     */
-    protected $_controller;
-    
-    /**
      * @var ActiveSync_Model_Device
      */
     protected $_device;
@@ -85,193 +73,6 @@ abstract class ActiveSync_TestCase extends TestCase
         Syncroton_Registry::setEmailDataClass('Felamimail_Frontend_ActiveSync');
         Syncroton_Registry::setTasksDataClass('Tasks_Frontend_ActiveSync');
     }
-
-    /**
-     *
-     * @return Syncroton_Model_Folder
-     */
-    public function testCreateFolder()
-    {
-        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE), new Tinebase_DateTime(null, null, 'de_DE'));
-        
-        $syncrotonFolder = $controller->createFolder(new Syncroton_Model_Folder(array(
-            'parentId' => 0, 
-            'displayName' => 'TestFolder'
-        )));
-    
-        $this->assertTrue(!empty($syncrotonFolder->serverId));
-        
-        return $syncrotonFolder;
-    }
-    
-    /**
-     * 
-     * @return Syncroton_Model_Folder
-     */
-    public function testUpdateFolder()
-    {
-        $syncrotonFolder = $this->testCreateFolder();
-        $syncrotonFolder->displayName = 'RenamedTestFolder';
-    
-        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE), new Tinebase_DateTime(null, null, 'de_DE'));
-    
-        $updatedSyncrotonFolder = $controller->updateFolder($syncrotonFolder);
-        
-        $allFolders = $controller->getAllFolders();
-        
-        $this->assertArrayHasKey($syncrotonFolder->serverId, $allFolders);
-        $this->assertEquals('RenamedTestFolder', $allFolders[$syncrotonFolder->serverId]->displayName);
-        
-        return $updatedSyncrotonFolder;
-    }
-    
-    /**
-     * @return Syncroton_Model_Folder
-     */
-    public function testUpdateFolderAndroid()
-    {
-        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_ANDROID), new Tinebase_DateTime(null, null, 'de_DE'));
-        
-        $syncrotonFolders = $controller->getAllFolders();
-    
-        #var_dump($syncrotonFolders); return;
-        
-        $this->setExpectedException('Syncroton_Exception_UnexpectedValue');
-        
-        $updatedSyncrotonFolder = $controller->updateFolder($syncrotonFolders[$this->_specialFolderName]);
-    }
-    
-    /**
-     * test if changed folders got returned
-     */
-    public function testGetChangedFolders()
-    {
-        $syncrotonFolder = $this->testUpdateFolder();
-        
-        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE), new Tinebase_DateTime(null, null, 'de_DE'));
-        
-        $changedFolders = $controller->getChangedFolders(Tinebase_DateTime::now()->subMinute(1), Tinebase_DateTime::now());
-        
-        //var_dump($changedFolders);
-        
-        $this->assertGreaterThanOrEqual(1, count($changedFolders));
-        $this->assertArrayHasKey($syncrotonFolder->serverId, $changedFolders);
-    }
-    
-    public function testDeleteFolder()
-    {
-        $this->markTestIncomplete('not yet implemented in controller');
-        
-        $syncrotonFolder = $this->testCreateFolder();
-    
-        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE), new Tinebase_DateTime(null, null, 'de_DE'));
-    
-        $controller->deleteFolder($syncrotonFolder);
-    }
-    
-    public function testGetAllFolders()
-    {
-        $syncrotonFolder = $this->testCreateFolder();
-    
-        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE), new Tinebase_DateTime(null, null, 'de_DE'));
-    
-        $allSyncrotonFolders = $controller->getAllFolders();
-        
-        $this->assertArrayHasKey($syncrotonFolder->serverId, $allSyncrotonFolders);
-        $this->assertArrayNotHasKey($this->_specialFolderName, $allSyncrotonFolders);
-        $this->assertTrue($allSyncrotonFolders[$syncrotonFolder->serverId] instanceof Syncroton_Model_Folder);
-        $this->assertEquals($syncrotonFolder->serverId, $allSyncrotonFolders[$syncrotonFolder->serverId]->serverId, 'serverId mismatch');
-        $this->assertEquals($syncrotonFolder->parentId, $allSyncrotonFolders[$syncrotonFolder->serverId]->parentId, 'parentId mismatch');
-        $this->assertEquals($syncrotonFolder->displayName, $allSyncrotonFolders[$syncrotonFolder->serverId]->displayName);
-        $this->assertTrue(!empty($allSyncrotonFolders[$syncrotonFolder->serverId]->type));
-    }
-    
-    public function testGetAllFoldersPalm()
-    {
-        $syncrotonFolder = $this->testCreateFolder();
-    
-        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_WEBOS), new Tinebase_DateTime(null, null, 'de_DE'));
-    
-        $allSyncrotonFolders = $controller->getAllFolders();
-        
-        $this->assertArrayHasKey($this->_specialFolderName, $allSyncrotonFolders, "key {$this->_specialFolderName} not found in " . print_r($allSyncrotonFolders, true));
-    }
-    
-    /**
-     * testDeleteEntry
-     */
-    public function testDeleteEntry()
-    {
-        $syncrotonFolder = $this->testCreateFolder();
-        
-        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE), new Tinebase_DateTime(null, null, 'de_DE'));
-        
-        list($serverId, $syncrotonContact) = $this->testCreateEntry($syncrotonFolder);
-        
-        $controller->deleteEntry($syncrotonFolder->serverId, $serverId, null);
-        
-        try {
-            $syncrotonContact = $controller->getEntry(new Syncroton_Model_SyncCollection(array('collectionId' => $syncrotonFolder->serverId)), $serverId);
-            $this->fail('should have thrown Syncroton_Exception_NotFound: '
-                . var_export($syncrotonContact, TRUE)
-                . ' tine contact: ' . print_r(Addressbook_Controller_Contact::getInstance()->get($serverId)->toArray(), TRUE));
-        } catch (Syncroton_Exception_NotFound $senf) {
-            $this->assertEquals('Syncroton_Exception_NotFound', get_class($senf));
-        }
-    }
-    
-    public function testGetInvalidEntry()
-    {
-        $syncrotonFolder = $this->testCreateFolder();
-    
-        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE), new Tinebase_DateTime(null, null, 'de_DE'));
-    
-        $this->setExpectedException('Syncroton_Exception_NotFound');
-    
-        $syncrotonContact = $controller->getEntry(new Syncroton_Model_SyncCollection(array('collectionId' => $syncrotonFolder->serverId)), 'jdszfegd63gfrk');
-    }
-    
-    /**
-     * test get changed entries
-     */
-    public function testGetChangedEntries()
-    {
-        $syncrotonFolder = $this->testCreateFolder();
-    
-        list($serverId, $syncrotonContact) = $this->testUpdateEntry($syncrotonFolder);
-    
-        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_IPHONE), new Tinebase_DateTime(null, null, 'de_DE'));
-    
-        $changedEntries = $controller->getChangedEntries($syncrotonFolder->serverId, new DateTime('2000-01-01'));
-        
-        $this->assertContains($serverId, $changedEntries, 'did not get changed record id in ' . print_r($changedEntries, TRUE));
-    }
-    
-    /**
-     * test get changed entries for android
-     */
-    public function testGetChangedEntriesAndroid()
-    {
-        $syncrotonFolder = $this->testCreateFolder();
-    
-        list($serverId, $syncrotonContact) = $this->testUpdateEntry($syncrotonFolder);
-    
-        $controller = Syncroton_Data_Factory::factory($this->_class, $this->_getDevice(Syncroton_Model_Device::TYPE_ANDROID), Tinebase_DateTime::now());
-    
-        $changedEntries = $controller->getChangedEntries($this->_specialFolderName, new Tinebase_DateTime('2000-01-01'));
-    
-        $this->assertContains($serverId, $changedEntries, 'did not get changed record id in ' . print_r($changedEntries, TRUE));
-    }
-    
-    /**
-     * test convert from XML to Tine 2.0 model
-     */
-    abstract public function testCreateEntry($syncrotonFolder = null);
-    
-    /**
-     * test xml generation for sync to client
-     */
-    abstract public function testUpdateEntry($syncrotonFolder = null);
     
     /**
      * create container with sync grant
@@ -382,20 +183,6 @@ abstract class ActiveSync_TestCase extends TestCase
     }
     
     /**
-     * get application activesync controller
-     * 
-     * @param ActiveSync_Model_Device $_device
-     */
-    protected function _getController(ActiveSync_Model_Device $_device)
-    {
-        if ($this->_controller === null) {
-            $this->_controller = Syncroton_Data_Factory::factory($this->_class, $_device, new Tinebase_DateTime(null, null, 'de_DE'));
-        } 
-        
-        return $this->_controller;
-    }
-    
-    /**
      * returns a test event
      * 
      * @param Tinebase_Model_Container $personalContainer
index 85f50b0..bcb1d1e 100644 (file)
@@ -13,7 +13,7 @@
  * 
  * @package     Addressbook
  */
-class Addressbook_Frontend_ActiveSyncTest extends ActiveSync_TestCase
+class Addressbook_Frontend_ActiveSyncTest extends ActiveSync_Controller_ControllerTest
 {
     /**
      * name of the application
index 33c8cd9..0d28d9f 100644 (file)
@@ -1007,6 +1007,38 @@ class Addressbook_JsonTest extends TestCase
     }
     
     /**
+     * testImportMergeTheirsWithTag
+     *
+     */
+    public function testImportMergeTheirsWithTag()
+    {
+        $result = $this->_importHelper(array('dryrun' => 0));
+        $this->assertTrue(count($result['results']) > 0, 'no record were imported');
+        $klaus = $result['results'][0];
+        
+        $klaus['tags'][] = $this->_getTag()->toArray();
+        $klaus['adr_one_postalcode'] = '12345';
+        
+        $clientRecords = array(array(
+            'recordData'        => $klaus,
+            'resolveStrategy'   => 'mergeTheirs',
+            'index'             => 0,
+        ));
+        
+        $options = array(
+            'dryrun'     => 0,
+            'duplicateResolveStrategy' => 'mergeTheirs',
+        );
+        
+        $result = $this->_importHelper($options, $clientRecords);
+        $this->assertEquals(2, count($result['results'][0]['tags']), 'klaus should have both tags: ' . print_r($result['results'][0], TRUE));
+        
+        $klaus = $this->_instance->getContact($klaus['id']);
+        $this->assertEquals(2, count($klaus['tags']), 'klaus should have both tags: ' . print_r($klaus, TRUE));
+        $this->assertEquals('12345', $klaus['adr_one_postalcode']);
+    }
+    
+    /**
      * helper for import with tags and keep/discard strategy
      * 
      * @param string $resolveStrategy
index 7bbd067..10817bc 100644 (file)
@@ -55,7 +55,7 @@ class Admin_JsonTest extends TestCase
             'accountPrimaryGroup'   => Tinebase_Group::getInstance()->getGroupByName('Users')->getId(),
             'accountLastName'       => 'Tine 2.0',
             'accountFirstName'      => 'PHPUnit',
-            'accountEmailAddress'   => 'phpunit@metaways.de'
+            'accountEmailAddress'   => 'phpunit@' . $this->_getMailDomain()
         ));
         
         if (Tinebase_Application::getInstance()->isInstalled('Addressbook') === true) {
@@ -398,7 +398,7 @@ class Admin_JsonTest extends TestCase
             'accountPrimaryGroup'   => Tinebase_Group::getInstance()->getGroupByName('Users')->getId(),
             'accountLastName'       => 'Tine 2.0',
             'accountFirstName'      => 'PHPUnit',
-            'accountEmailAddress'   => 'phpunit@metaways.de'
+            'accountEmailAddress'   => 'phpunit@' . $this->_getMailDomain()
         ));
         
         $contact = Addressbook_Controller_Contact::getInstance()->getContactByUserId($user['accountId']);
index 0bb4061..7bc3bfa 100644 (file)
@@ -341,7 +341,7 @@ class Calendar_Controller_EventGrantsTests extends Calendar_TestCase
      */
     public function testFreeBusyViaAttendee()
     {
-        // whipe grants from jmcblack
+        // wipe grants from jmcblack
         Tinebase_Container::getInstance()->setGrants($this->_getPersonasDefaultCals('jmcblack'), new Tinebase_Record_RecordSet('Tinebase_Model_Grants', array(array(
             'account_id'    => $this->_getPersona('jmcblack')->getId(),
             'account_type'  => 'user',
index dbd0c49..cd07b16 100644 (file)
@@ -95,9 +95,9 @@ class Calendar_Controller_EventNotificationsTests extends Calendar_TestCase
     /**
      * Test event creation with muted invitation
      */
-    public function testMuteToogle()
+    public function testMuteToogleCreation()
     {
-        $event = $this->_getEvent(false, /* $mute = */ 1);
+        $event = $this->_getEvent(TRUE, /* $mute = */ 1);
         $event->attendee = $this->_getPersonaAttendee('jsmith, pwulf, sclever, jmcblack, rwright');
 
         self::flushMailer();
@@ -106,6 +106,44 @@ class Calendar_Controller_EventNotificationsTests extends Calendar_TestCase
 
         $this->assertEquals($event->mute, 1);
     }
+    
+    /**
+     * Test event reschedul with muted invitation
+     */
+    public function testMuteToogleReschedul()
+    {
+        $event = $this->_getEvent(TRUE, /* $mute = */ 1);
+        $event->attendee = $this->_getPersonaAttendee('jsmith, pwulf, sclever, jmcblack, rwright');
+        $persistentEvent = $this->_eventController->create($event);
+        
+        $persistentEvent->summary = 'reschedule notification has precedence over normal update';
+        $persistentEvent->dtstart->addHour(1);
+        $persistentEvent->dtend->addHour(1);
+        $persistentEvent->mute = 1;
+        $this->assertEquals($persistentEvent->mute, 1);
+        
+        self::flushMailer();
+        $updatedEvent = $this->_eventController->update($persistentEvent);
+        $this->_assertMail('jsmith, pwulf, sclever, jmcblack, rwright', NULL);
+    }
+    
+    /**
+     * testMuteToogleUpdateAttendeeStatus
+     */
+    public function testMuteToogleUpdateAttendeeStatus()
+    {
+        $event = $this->_getEvent(TRUE, /* $mute = */ 1);
+        $event->attendee = $this->_getPersonaAttendee('jsmith, pwulf, sclever, jmcblack, rwright');
+        $persistentEvent = $this->_eventController->create($event);
+    
+        $persistentEvent->attendee[1]->status = Calendar_Model_Attender::STATUS_DECLINED;
+        $persistentEvent->mute = 1;
+        $this->assertEquals($persistentEvent->mute, 1);
+    
+        self::flushMailer();
+        $updatedEvent = $this->_eventController->update($persistentEvent);
+        $this->_assertMail('jsmith, pwulf, sclever, jmcblack, rwright', NULL);
+    }
 
     /**
      * testInvitationWithAttachment
@@ -1285,4 +1323,42 @@ class Calendar_Controller_EventNotificationsTests extends Calendar_TestCase
         
         $this->assertEquals(2, count($messages), 'two mails should be send to current user (resource + attender)');
     }
+
+    /**
+     * Enable by a preference which sends mails to every user who got permissions to edit the resource
+     */
+    public function testResourceNotificationForGrantedUsers()
+    {
+        // Enable feature, disabled by default!
+        Calendar_Config::getInstance()->set(Calendar_Config::RESOURCE_MAIL_FOR_EDITORS, true);
+
+        $resource = $this->_getResource();
+        $resource->email = Tinebase_Core::getUser()->accountEmailAddress;
+        $persistentResource = Calendar_Controller_Resource::getInstance()->create($resource);
+
+        $event = $this->_getEvent(/*now = */ true);
+        $event->attendee->addRecord($this->_createAttender($persistentResource->getId(), Calendar_Model_Attender::USERTYPE_RESOURCE));
+        $grants = Tinebase_Container::getInstance()->getGrantsOfContainer($resource->container_id);
+
+        $newGrants = array(
+                'account_id' => $this->_personas['sclever']->getId(),
+                'account_type' => 'user',
+                Tinebase_Model_Grants::GRANT_READ => true,
+                Tinebase_Model_Grants::GRANT_EDIT => true
+            );
+
+        Tinebase_Container::getInstance()->setGrants($resource->container_id, new Tinebase_Record_RecordSet('Tinebase_Model_Grants', array_merge(array($newGrants), $grants->toArray())), TRUE);
+
+        self::flushMailer();
+
+        $persistentEvent = $this->_eventController->create($event);
+
+        $messages = self::getMessages();
+
+        Tinebase_Container::getInstance()->setGrants($resource->container_id, $grants);
+
+        $this->assertContains('Meeting Room (Required, No response)', print_r($messages, true));
+        $this->assertEquals(4, count($messages), 'four mails should be send to current user (resource + attender + everybody whos allowed to edit this resource)');
+        $this->assertEquals(3, count($persistentEvent->attendee));
+    }
 }
index c5445b4..5f4c0d6 100644 (file)
@@ -85,7 +85,7 @@ class Calendar_Convert_Event_VCalendar_MacOSXTest extends PHPUnit_Framework_Test
 
     public function testConvertToTine20ModelXCalendarAccess()
     {
-        $converter = Calendar_Convert_Event_VCalendar_Factory::factory(Calendar_Convert_Event_VCalendar_Factory::CLIENT_MACOSX, '10.7.5');
+        $converter = Calendar_Convert_Event_VCalendar_Factory::factory(Calendar_Convert_Event_VCalendar_Factory::CLIENT_MACOSX, '10.10.2');
 
         $vcalendarStream = fopen(dirname(__FILE__) . '/../../../Import/files/apple_calendar_lion_access_private.ics', 'r');
         $event = $converter->toTine20Model($vcalendarStream);
@@ -94,5 +94,18 @@ class Calendar_Convert_Event_VCalendar_MacOSXTest extends PHPUnit_Framework_Test
         $vcalendarStream = fopen(dirname(__FILE__) . '/../../../Import/files/apple_calendar_lion_access_attendee.ics', 'r');
         $event = $converter->toTine20Model($vcalendarStream);
         $this->assertEquals(Calendar_Model_Event::CLASS_PUBLIC, $event->class);
+
+        $iosPrivateIcs = dirname(__FILE__) . '/../../../Import/files/ios_private.ics';
+        $vcalendarStream = fopen($iosPrivateIcs, 'r');
+        $event = $converter->toTine20Model($vcalendarStream);
+        $this->assertEquals(Calendar_Model_Event::CLASS_PRIVATE, $event->class);
+
+        // try again with ios user agent
+        $iosUserAgent = 'iOS/8.2 (12D508) dataaccessd/1.0';
+        list($backend, $version) = Calendar_Convert_Event_VCalendar_Factory::parseUserAgent($iosUserAgent);
+        $converter = Calendar_Convert_Event_VCalendar_Factory::factory($backend, $version);
+        $vcalendarStream = fopen($iosPrivateIcs, 'r');
+        $event = $converter->toTine20Model($vcalendarStream);
+        $this->assertEquals(Calendar_Model_Event::CLASS_PRIVATE, $event->class);
     }
 }
index c523ca5..8f49c0e 100644 (file)
  */
 class Calendar_Export_ICalTest extends Calendar_TestCase
 {
+    /**
+     * the test event
+     *
+     * @var Calendar_Model_Event
+     */
+    protected $_testEvent = null;
+
     public function setUp()
     {
         $this->_testEvent = new Calendar_Model_Event(array(
@@ -37,7 +44,6 @@ class Calendar_Export_ICalTest extends Calendar_TestCase
     {
         $exporter = new Calendar_Export_Ical();
         $ics = $exporter->eventToIcal($this->_testEvent);
-//        echo $ics;
 
         // assert basics
         $this->assertEquals(1, preg_match("/SUMMARY:{$this->_testEvent->summary}\r\n/", $ics), 'SUMMARY not correct');
@@ -61,8 +67,7 @@ class Calendar_Export_ICalTest extends Calendar_TestCase
         
         $exporter = new Calendar_Export_Ical();
         $ics = $exporter->eventToIcal($this->_testEvent);
-//        echo $ics;
-        
+
         // assert dtstart/dtend tz
         $this->assertEquals(1, preg_match("/DTSTART;VALUE=DATE:20101230\r\n/", $ics), 'DTSTART not correct');
         $this->assertEquals(1, preg_match("/DTEND;VALUE=DATE:20101231\r\n/", $ics), 'DTEND not correct');
@@ -76,7 +81,6 @@ class Calendar_Export_ICalTest extends Calendar_TestCase
         
         $exporter = new Calendar_Export_Ical();
         $ics = $exporter->eventToIcal($nextOccurance);
-//        echo $ics;
 
         // assert recurid
         $this->assertEquals(1, preg_match("/RECURRENCE-ID;TZID=Europe\/Berlin:20101231T130000\r\n/", $ics), 'RECURRENCE-ID broken');
@@ -94,10 +98,8 @@ class Calendar_Export_ICalTest extends Calendar_TestCase
 
         $exporter = new Calendar_Export_Ical();
         $ics = $exporter->eventToIcal($this->_testEvent);
-//        echo $ics;
 
         // assert exdate
-//        $this->assertEquals(1, preg_match("/EXDATE;TZID=Europe\/Berlin:20101231T130000,20110101T130000\r\n/", $ics), 'RECURRENCE-ID broken');
         $this->assertEquals(1, preg_match("/EXDATE;TZID=Europe\/Berlin:20101231T130000\r\n/", $ics), 'RECURRENCE-ID broken');
         $this->assertEquals(1, preg_match("/EXDATE;TZID=Europe\/Berlin:20110101T130000\r\n/", $ics), 'RECURRENCE-ID broken');
     }
@@ -119,7 +121,6 @@ class Calendar_Export_ICalTest extends Calendar_TestCase
 
         $exporter = new Calendar_Export_Ical();
         $ics = $exporter->eventToIcal($eventSet);
-//        echo $ics;
 
         $this->assertEquals(2, preg_match_all('/BEGIN:VEVENT\r\n/', $ics, $matches), 'There should be exactly 2 VEVENT compontents');
     }
@@ -129,12 +130,14 @@ class Calendar_Export_ICalTest extends Calendar_TestCase
      */
     public function testExportOrganizer()
     {
-        $this->_testEvent->organizer = Tinebase_Helper::array_value('pwulf', Zend_Registry::get('personas'))->contact_id;
+        $pwulf = Tinebase_Helper::array_value('pwulf', $this->_getPersonas());
+        $this->_testEvent->organizer = $pwulf->contact_id;
         
         $exporter = new Calendar_Export_Ical();
         $ics = $exporter->eventToIcal($this->_testEvent);
         
-        $this->assertEquals(1, preg_match("/ORGANIZER;CN=\"Wulf, Paul\":mailto:pwulf@" . $this->_getMailDomain() . "\r\n/", $ics), 'ORGANIZER missing/broken');
+        $this->assertContains("ORGANIZER;CN=\"Wulf, Paul\":mailto:" . $pwulf->accountEmailAddress,
+            (string) $ics, 'ORGANIZER missing/broken');
     }
     
     /**
@@ -142,19 +145,21 @@ class Calendar_Export_ICalTest extends Calendar_TestCase
      */
     public function testExportAttendee()
     {
+        $pwulf = Tinebase_Helper::array_value('pwulf', $this->_getPersonas());
         $this->_testEvent->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender', array(
             array(
                 'role'          => Calendar_Model_Attender::ROLE_REQUIRED,
                 'status'        => Calendar_Model_Attender::STATUS_ACCEPTED,
                 'user_type'     => Calendar_Model_Attender::USERTYPE_USER,
-                'user_id'       => Tinebase_Helper::array_value('pwulf', Zend_Registry::get('personas'))->contact_id,
+                'user_id'       => $pwulf->contact_id,
             )
         ));
         
         $exporter = new Calendar_Export_Ical();
         $ics = $exporter->eventToIcal($this->_testEvent);
         
-        $this->assertContains("ATTENDEE;CN=\"Wulf, Paul\";CUTYPE=INDIVIDUAL;EMAIL=pwulf@" . substr($this->_getMailDomain(), 0, -14), (string) $ics, 'ATTENDEE missing/broken');
+        $this->assertContains("ATTENDEE;CN=\"Wulf, Paul\";CUTYPE=INDIVIDUAL;EMAIL=" . substr($pwulf->accountEmailAddress, 0, -14),
+            (string) $ics, 'ATTENDEE missing/broken');
     }
     
     public function testExportAlarm()
@@ -173,7 +178,6 @@ class Calendar_Export_ICalTest extends Calendar_TestCase
         
         $exporter = new Calendar_Export_Ical();
         $ics = $exporter->eventToIcal($this->_testEvent);
-//        echo $ics;
 
         // assert organizer
         $this->assertEquals(1, preg_match("/TRIGGER:-PT15M\r\n/", $ics), 'TRIGGER missing/broken');
@@ -187,7 +191,7 @@ class Calendar_Export_ICalTest extends Calendar_TestCase
     {
         $eventData = $this->_getEvent(TRUE)->toArray();
         $this->_uit = new Calendar_Frontend_Json();
-        $persistentEventData = $this->_uit->saveEvent($eventData);
+         $this->_uit->saveEvent($eventData);
         
         $this->_testNeedsTransaction();
         $cmd = realpath(__DIR__ . "/../../../../tine20/tine20.php") . ' --method Calendar.exportICS ' .
@@ -198,7 +202,7 @@ class Calendar_Export_ICalTest extends Calendar_TestCase
         $result = implode(',', $output);
         
         $failMessage = print_r($output, TRUE);
-        $this->assertEquals(1, preg_match("/SUMMARY:{$eventData['summary']}/", $result), 'DESCRIPTION not correct');
+        $this->assertEquals(1, preg_match("/SUMMARY:{$eventData['summary']}/", $result), 'DESCRIPTION not correct: ' . $failMessage);
     }
     
     /**
index 688b25d..7b2802e 100644 (file)
@@ -13,7 +13,7 @@
  * 
  * @package     Calendar
  */
-class Calendar_Frontend_ActiveSyncTest extends ActiveSync_TestCase
+class Calendar_Frontend_ActiveSyncTest extends ActiveSync_Controller_ControllerTest
 {
     /**
      * name of the application
@@ -763,7 +763,28 @@ Zeile 3</AirSyncBase:Data>
         $this->assertEquals($event->seq + 1, $baseEventAfterExdateUpdate->seq, 'base sequence should be updated');
         $this->assertEquals($container->content_seq + 2, $containerAfterExdateUpdate->content_seq, 'container sequence should be updated');
     }
-    
+
+    /**
+     * testDeleteExdate
+     *
+     * @see : Exdate delete does not update seq of base event
+     */
+    public function testDeleteExdate()
+    {
+        list($serverId, $syncrotonEvent) = $this->testCreateEntry();
+
+        $event = Calendar_Controller_Event::getInstance()->get($serverId);
+        $container = Tinebase_Container::getInstance()->get($event->container_id);
+
+        $exception = Calendar_Controller_Event::getInstance()->getRecurExceptions($event)->getFirstRecord();
+        Calendar_Controller_Event::getInstance()->delete($exception);
+
+        $baseEventAfterExdateDelete = Calendar_Controller_Event::getInstance()->get($serverId);
+        $containerAfterExdateDelete = Tinebase_Container::getInstance()->get($event->container_id);
+        $this->assertGreaterThan($event->seq, $baseEventAfterExdateDelete->seq, 'base sequence should be updated');
+        $this->assertGreaterThan($container->content_seq, $containerAfterExdateDelete->content_seq, 'container sequence should be updated');
+    }
+
     public function testRecurEventExceptionFilters($syncrotonFolder = null)
     {
         if ($syncrotonFolder === null) {
index 2c6967b..61aaae2 100644 (file)
@@ -8,6 +8,7 @@
  * @author      Philipp Schüle <p.schuele@metaways.de>
  * 
  * @todo        add tests testInvitationCancel and testOrganizerSendBy
+ * @todo extend Calendar_TestCase
  */
 
 /**
diff --git a/tests/tine20/Calendar/Import/files/ios_private.ics b/tests/tine20/Calendar/Import/files/ios_private.ics
new file mode 100644 (file)
index 0000000..1ef52f3
--- /dev/null
@@ -0,0 +1,34 @@
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Apple Inc.//iOS 8.2//EN
+VERSION:2.0
+X-CALENDARSERVER-ACCESS:CONFIDENTIAL
+BEGIN:VTIMEZONE
+TZID:Europe/Berlin
+BEGIN:DAYLIGHT
+DTSTART:19810329T020000
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+TZNAME:MESZ
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19961027T030000
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+TZNAME:MEZ
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20150325T133444Z
+DTEND;TZID=Europe/Berlin:20150327T154500
+DTSTAMP:20150325T133500Z
+DTSTART;TZID=Europe/Berlin:20150327T144500
+LAST-MODIFIED:20150325T133444Z
+SEQUENCE:1
+SUMMARY:Privates
+TRANSP:OPAQUE
+UID:464683D1-15A8-4874-A6E4-1536C9669842
+END:VEVENT
+END:VCALENDAR
index af3b951..a8a80e9 100644 (file)
@@ -4,46 +4,17 @@
  * 
  * @package     Crm
  * @license     http://www.gnu.org/licenses/agpl.html
- * @copyright   Copyright (c) 2009-2013 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2009-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Philipp Schüle <p.schuele@metaways.de>
  * 
  */
 
 /**
- * Test helper
- */
-require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'TestHelper.php';
-
-/**
  * abstract crm test class
  */
-class Crm_AbstractTest extends PHPUnit_Framework_TestCase
+class Crm_AbstractTest extends TestCase
 {
     /**
-     * Sets up the fixture.
-     * This method is called before a test is executed.
-     *
-     * @access protected
-     */
-    protected function setUp()
-    {
-        Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
-        Addressbook_Controller_Contact::getInstance()->setGeoDataForContacts(FALSE);
-    }
-
-    /**
-     * Tears down the fixture
-     * This method is called after a test is executed.
-     *
-     * @access protected
-     */
-    protected function tearDown()
-    {
-        Addressbook_Controller_Contact::getInstance()->setGeoDataForContacts(TRUE);
-        Tinebase_TransactionManager::getInstance()->rollBack();
-    }
-    
-    /**
      * get contact
      * 
      * @return Addressbook_Model_Contact
index 2be9d76..4343f49 100644 (file)
@@ -36,6 +36,7 @@ abstract class Crm_Export_AbstractTest extends Crm_AbstractTest
     protected function setUp()
     {
         Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
+        Tinebase_Cache_PerRequest::getInstance()->resetCache();
         $this->_json = new Crm_Frontend_Json();
         
         $contact = $this->_getContact();
@@ -60,5 +61,6 @@ abstract class Crm_Export_AbstractTest extends Crm_AbstractTest
     protected function tearDown()
     {
         Tinebase_TransactionManager::getInstance()->rollBack();
+        Tinebase_Cache_PerRequest::getInstance()->resetCache();
     }
 }
index 00f63cf..6bbb3a1 100644 (file)
@@ -4,17 +4,12 @@
  * 
  * @package     Crm
  * @license     http://www.gnu.org/licenses/agpl.html
- * @copyright   Copyright (c) 2008-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2008-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Philipp Schüle <p.schuele@metaways.de>
  * 
  */
 
 /**
- * Test helper
- */
-require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'TestHelper.php';
-
-/**
  * Test class for Crm_Json
  */
 class Crm_JsonTest extends Crm_AbstractTest
@@ -104,7 +99,6 @@ class Crm_JsonTest extends Crm_AbstractTest
             Tinebase_Container::getInstance()->getDefaultContainer('Crm')->getId(),
             $registry['defaults']['container_id']['id']
         );
-        //print_r($registry);
     }
     
     /**
@@ -448,6 +442,9 @@ class Crm_JsonTest extends Crm_AbstractTest
         $taskJson->saveTask($taskData);
     }
     
+    /**
+     * testOtherRecordConstraintsConfig
+     */
     public function testOtherRecordConstraintsConfig()
     {
         $leadData1 = $this->_instance->saveLead($this->_getLead(FALSE, FALSE)->toArray());
@@ -573,11 +570,12 @@ class Crm_JsonTest extends Crm_AbstractTest
             $cfc = Tinebase_CustomFieldTest::getCustomField(array(
                 'application_id' => Tinebase_Application::getInstance()->getApplicationByName('Crm')->getId(),
                 'model'          => 'Crm_Model_Lead',
-                'name'           => 'crmcf',
+                'name'           => Tinebase_Record_Abstract::generateUID(),
             ));
+            $this->_cfcName = $cfc->name;
             
             $cfs = array(
-                'crmcf' => '1234'
+                $this->_cfcName => '1234'
             );
             
             Tinebase_CustomField::getInstance()->addCustomField($cfc);
@@ -655,7 +653,7 @@ class Crm_JsonTest extends Crm_AbstractTest
                 unset($savedLead['relations'][$key]);
             }
         }
-        $savedLead['customfields']['crmcf'] = '5678';
+        $savedLead['customfields'][$this->_cfcName] = '5678';
         $updatedLead = $this->_instance->saveLead($savedLead);
         
         // check modlog + history
@@ -666,7 +664,7 @@ class Crm_JsonTest extends Crm_AbstractTest
         foreach ($modifications as $modification) {
             switch ($modification->modified_attribute) {
                 case 'customfields':
-                    $this->assertEquals('{"crmcf":"5678"}', $modification->new_value);
+                    $this->assertEquals('{"' . $this->_cfcName . '":"5678"}', $modification->new_value);
                     break;
                 case 'relations':
                     $diff = new Tinebase_Record_RecordSetDiff(Zend_Json::decode($modification->new_value));
@@ -769,4 +767,25 @@ class Crm_JsonTest extends Crm_AbstractTest
         
         $this->assertContains(Tinebase_DateTime::now()->format('Y-m-d'), $newLead['start'], 'start should be set to now if missing');
     }
+    
+    /**
+     * testSortByLeadState
+     * 
+     * @see 0010792: Sort leads by status and source
+     */
+    public function testSortByLeadState()
+    {
+        $savedLead1 = $this->_saveLead();
+        $lead2 = $this->_getLead()->toArray();  // open
+        $lead2['leadstate_id'] = 2;             // contacted
+        $savedLead2 = $this->_instance->saveLead($lead2);
+        
+        $sort = array(
+            'sort' => 'leadstate_id',
+            'dir' => 'ASC'
+        );
+        $searchLeads = $this->_instance->searchLeads($this->_getLeadFilter(), $sort);
+        
+        $this->assertEquals(2, $searchLeads['results'][0]['leadstate_id'], 'leadstate "contacted" should come first');
+    }
 }
index 075efda..6a6586e 100644 (file)
@@ -4,16 +4,11 @@
  * 
  * @package     Crm
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
- * @copyright   Copyright (c) 2013 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2013-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Philipp Schüle <p.schuele@metaways.de>
  */
 
 /**
- * Test helper
- */
-require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'TestHelper.php';
-
-/**
  * Test class for Crm Notifications
  * 
  * @package     Crm
@@ -47,7 +42,7 @@ class Crm_NotificationsTests extends Crm_AbstractTest
     }
     
     /**
-     * testInvitation
+     * testNotification
      */
     public function testNotification()
     {
@@ -65,57 +60,69 @@ class Crm_NotificationsTests extends Crm_AbstractTest
         ), TRUE));
         $this->_leadController->create($lead);
         
-        $messages = $this->getMessages();
+        $messages = self::getMessages();
         $this->assertEquals(1, count($messages));
         $bodyText = $messages[0]->getBodyText()->getContent();
         $this->assertContains(' Lars Kneschke (Metaways', $bodyText);
     }
-    
+
     /**
-     * get messages
-     * 
-     * @return array
-     * 
-     * @todo move this to TestServer?
+     * testNotificationToResponsible
      */
-    public function getMessages()
+    public function testNotificationToResponsible()
     {
-        // make sure messages are sent if queue is activated
-        if (isset(Tinebase_Core::getConfig()->actionqueue)) {
-            Tinebase_ActionQueue::getInstance()->processQueue(100);
-        }
+        self::flushMailer();
         
-        return self::getMailer()->getMessages();
-    }
-    
-    /**
-     * get mailer
-     * 
-     * @return Zend_Mail_Transport_Abstract
-     * 
-     * @todo move this to TestServer?
-     */
-    public static function getMailer()
-    {
-        if (! self::$_mailer) {
-            self::$_mailer = Tinebase_Smtp::getDefaultTransport();
-        }
+        $lead = $this->_getLead();
+        
+        // give sclever access to lead container
+        $this->_setPersonaGrantsForTestContainer($lead['container_id'], 'sclever');
+        
+        $lead->relations = array(new Tinebase_Model_Relation(array(
+            'type'                   => 'RESPONSIBLE',
+            'related_record'         => Addressbook_Controller_Contact::getInstance()->getContactByUserId(Tinebase_Core::getUser()->getId()),
+            'own_model'              => 'Crm_Model_Lead',
+            'own_backend'            => 'Sql',
+            'own_degree'             => Tinebase_Model_Relation::DEGREE_SIBLING,
+            'related_model'          => 'Addressbook_Model_Contact',
+            'related_backend'        => Tasks_Backend_Factory::SQL,
+        ), TRUE));
+        $savedLead = $this->_leadController->create($lead);
         
-        return self::$_mailer;
+        $messages = self::getMessages();
+        $this->assertEquals(1, count($messages));
+        $bodyText = $messages[0]->getBodyText()->getContent();
+        $this->assertContains('**PHPUnit **', $bodyText);
     }
     
     /**
-     * flush mailer (send all remaining mails first)
-     * 
-     * @todo move this to TestServer?
+     * testNotificationToResponsible
      */
-    public static function flushMailer()
+    public function testNotificationOnDelete()
     {
-        // make sure all messages are sent if queue is activated
-        if (isset(Tinebase_Core::getConfig()->actionqueue)) {
-            Tinebase_ActionQueue::getInstance()->processQueue(10000);
-        }
+        $lead = $this->_getLead();
         
-        self::getMailer()->flush();
+        // give sclever access to lead container
+        $this->_setPersonaGrantsForTestContainer($lead['container_id'], 'sclever');
+        
+        $lead->relations = array(new Tinebase_Model_Relation(array(
+                'type'                   => 'RESPONSIBLE',
+                'related_record'         => Addressbook_Controller_Contact::getInstance()->getContactByUserId(Tinebase_Core::getUser()->getId()),
+                'own_model'              => 'Crm_Model_Lead',
+                'own_backend'            => 'Sql',
+                'own_degree'             => Tinebase_Model_Relation::DEGREE_SIBLING,
+                'related_model'          => 'Addressbook_Model_Contact',
+                'related_backend'        => Tasks_Backend_Factory::SQL,
+        ), TRUE));
+        $savedLead = $this->_leadController->create($lead);
+        
+        self::flushMailer();
+        
+        $this->_leadController->delete($savedLead->getId());
+        
+        $messages = self::getMessages();
+        $this->assertEquals(1, count($messages));
+        $bodyText = $messages[0]->getBodyText()->getContent();
+        $this->assertContains('**PHPUnit **', $bodyText);
     }
 }
index 6ba73ad..156c824 100644 (file)
@@ -118,7 +118,7 @@ class Felamimail_Controller_Cache_MessageTest extends PHPUnit_Framework_TestCase
         }
         
         // reset user if changed
-        if (Tinebase_Core::getUser()->getId() !== $this->_testUser->getId()) {
+        if (is_object($this->_testUser) && Tinebase_Core::getUser()->getId() !== $this->_testUser->getId()) {
             Tinebase_Core::set(Tinebase_Core::USER, $this->_testUser);
         }
     }
@@ -202,7 +202,6 @@ class Felamimail_Controller_Cache_MessageTest extends PHPUnit_Framework_TestCase
         $this->_headerValueToDelete = 'HEADER X-Tine20TestMessage multipart/alternative';
         
         // update message cache
-        $updatedFolder = $this->_controller->updateCache($this->_folder, 10, 1);
         $loopCount = 1;
         do {
             $updatedFolder = $this->_controller->updateCache($this->_folder, 10, 1);
@@ -239,7 +238,7 @@ class Felamimail_Controller_Cache_MessageTest extends PHPUnit_Framework_TestCase
     public function testUpdateCountersOnly()
     {
         // update message cache
-        $updatedFolder = $this->_controller->updateCache($this->_folder, 30, 1);
+        $this->_controller->updateCache($this->_folder, 30, 1);
         
         $this->_appendMessage('multipart_alternative.eml', $this->_testFolderName);
         $this->_headerValueToDelete = 'HEADER X-Tine20TestMessage multipart/alternative';
index 51f33ab..c824925 100644 (file)
@@ -679,6 +679,20 @@ class Felamimail_Frontend_JsonTest extends PHPUnit_Framework_TestCase
     }
     
     /**
+     * try search for a message with three cache filters to force a foreign relation join with at least 2 tables
+     */
+    public function testSearchMessageWithThreeCacheFilter()
+    {
+        $filter = array(
+            array('field' => 'flags',   'operator' => 'in',       'value' => Zend_Mail_Storage::FLAG_ANSWERED),
+            array('field' => 'to',      'operator' => 'contains', 'value' => 'testDOESNOTEXIST'),
+            array('field' => 'subject', 'operator' => 'contains', 'value' => 'testDOESNOTEXIST'),
+        );
+        $result = $this->_json->searchMessages($filter, '');
+        $this->assertEquals(0, $result['totalcount']);
+    }
+    
+    /**
      * try search for a message with empty path filter
      */
     public function testSearchMessageEmptyPath()
index e4bb5fb..34b3a49 100644 (file)
@@ -60,8 +60,18 @@ class Felamimail_Model_AccountTest extends PHPUnit_Framework_TestCase
     /**
      * test get smtp config
      */
+    public function testGetImapConfig()
+    {
+        $this->markTestSkipped('this test has to be implemented yet');
+    }
+    
+    /**
+     * test get smtp config
+     */
     public function testGetSmtpConfig()
     {
+        $this->markTestSkipped('this test has to be refactored');
+        
         $smtpConfig = Tinebase_Config::getInstance()->get(Tinebase_Config::SMTP, new Tinebase_Config_Struct())->toArray();
         
         $account = new Felamimail_Model_Account(array(
index 795cccb..5197732 100644 (file)
@@ -33,6 +33,7 @@ class Filemanager_Controller_DownloadLinkTests extends TestCase
         
         Tinebase_FileSystem::getInstance()->clearStatCache();
         Tinebase_FileSystem::getInstance()->clearDeletedFilesFromFilesystem();
+        Tinebase_Cache_PerRequest::getInstance()->resetCache();
     }
     
     /**
@@ -79,6 +80,9 @@ class Filemanager_Controller_DownloadLinkTests extends TestCase
             'value'    => '/' . Tinebase_Model_Container::TYPE_PERSONAL . '/' . Tinebase_Core::getUser()->accountLoginName
         )));
         $node = Filemanager_Controller_Node::getInstance()->search($filter)->getFirstRecord();
+        
+        $this->assertInstanceOf('Tinebase_Model_Tree_Node', $node);
+        
         return $node;
     }
     
index 480fbbd..2f3c0be 100644 (file)
@@ -270,14 +270,9 @@ class Sales_InvoiceControllerTests extends Sales_InvoiceTestCase
         $this->_createFullFixtures();
         
         $date = clone $this->_referenceDate;
-        $i = 0;
+        $date->addMonth(12);
         
-        // the whole year, 12 months
-        while ($i < 12) {
-            $result = $this->_invoiceController->createAutoInvoices($date);
-            $date->addMonth(1);
-            $i++;
-        }
+        $this->_invoiceController->createAutoInvoices($date);
         
         $paController = Sales_Controller_ProductAggregate::getInstance();
         $productAggregates = $paController->getAll();
@@ -520,24 +515,18 @@ class Sales_InvoiceControllerTests extends Sales_InvoiceTestCase
         )));
         
         $startDate = clone $this->_referenceDate;
-        // the whole year, 12 months
-        $i=0;
-        
         $startDate->addDay(5);
+        $startDate->addMonth(24);
         
-        while ($i < 24) {
-            $startDate->addMonth(1);
-            $result = $this->_invoiceController->createAutoInvoices($startDate);
-            $this->assertEquals(1, $result['created_count']);
-            $i++;
-        }
+        $result = $this->_invoiceController->createAutoInvoices($startDate);
+        $this->assertEquals(25, $result['created_count']);
         
         $invoices = $this->_invoiceController->getAll('start_date');
         $firstInvoice = $invoices->getFirstRecord();
         $this->assertInstanceOf('Tinebase_DateTime', $firstInvoice->start_date);
         $this->assertEquals('0101', $firstInvoice->start_date->format('md'));
         
-        $this->assertEquals(24, $invoices->count());
+        $this->assertEquals(25, $invoices->count());
         
         $filter = new Sales_Model_InvoicePositionFilter(array());
         $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'invoice_id', 'operator' => 'in', 'value' => $invoices->getArrayOfIds())));
@@ -561,13 +550,13 @@ class Sales_InvoiceControllerTests extends Sales_InvoiceTestCase
             $this->assertEquals($invoice->end_date->format('Y-m'), $pos->month);
         }
         
-        $this->assertEquals(24, $invoicePositions->count());
+        $this->assertEquals(25, $invoicePositions->count());
         
         $this->assertEquals($this->_referenceYear . '-01', $invoicePositions->getFirstRecord()->month);
         
         $invoicePositions->sort('month', 'DESC');
         
-        $this->assertEquals($this->_referenceYear + 1 . '-12', $invoicePositions->getFirstRecord()->month);
+        $this->assertEquals($this->_referenceYear + 2 . '-01', $invoicePositions->getFirstRecord()->month);
     }
     
     /**
@@ -673,12 +662,10 @@ class Sales_InvoiceControllerTests extends Sales_InvoiceTestCase
             $this->_invoiceController->delete($invoice);
         }
         
-        $testDate = clone $this->_referenceDate;
-        
         $productAggregate = $paController->get($productAggregate->getId());
         $productAggregate->setTimezone(Tinebase_Core::getUserTimezone());
         
-        $this->assertEquals(NULL, $productAggregate->last_autobill);
+        $this->assertEquals($this->_referenceDate, $productAggregate->last_autobill);
         
         // create 6 invoices again - each month one invoice - last autobill must be increased each month
         for ($i = 1; $i < 7; $i++) {
@@ -1023,7 +1010,7 @@ class Sales_InvoiceControllerTests extends Sales_InvoiceTestCase
     
         $result = $this->_invoiceController->createAutoInvoices($date);
     
-        $this->assertEquals(3, count($result['created']));
+        $this->assertEquals(5, count($result['created']));
         
         $tsController = Timetracker_Controller_Timesheet::getInstance();
     
@@ -1146,7 +1133,7 @@ class Sales_InvoiceControllerTests extends Sales_InvoiceTestCase
     
         $result = $this->_invoiceController->createAutoInvoices($date);
     
-        $this->assertEquals(3, count($result['created']));
+        $this->assertEquals(5, count($result['created']));
         
         $invoice = $this->_invoiceController->get($result['created'][0]);
         $invoice->cleared = 'CLEARED';
index 1a0b699..4f4e669 100644 (file)
@@ -4,7 +4,7 @@
  * 
  * @package     Sales
  * @license     http://www.gnu.org/licenses/agpl.html
- * @copyright   Copyright (c) 2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Alexander Stintzing <a.stintzing@metaways.de>
  * 
  */
@@ -28,9 +28,13 @@ class Sales_InvoiceExportTests extends Sales_InvoiceTestCase
     
     /**
      * tests auto invoice creation
+     *
+     * TODO should be refactored/fixed:  line 97: $this->assertEquals(6, $half); // $half is completely random
      */
     public function testExportInvoice()
     {
+        $this->markTestSkipped('FIXME: this test currently produces random results');
+
         $this->_createFullFixtures();
         $date = clone $this->_referenceDate;
         
@@ -38,8 +42,8 @@ class Sales_InvoiceExportTests extends Sales_InvoiceTestCase
         
         // until 1.7
         while ($i < 8) {
-            $result = $this->_invoiceController->createAutoInvoices($date);
             $date->addMonth(1);
+            $this->_invoiceController->createAutoInvoices($date);
             $i++;
         }
         
@@ -132,7 +136,7 @@ class Sales_InvoiceExportTests extends Sales_InvoiceTestCase
     
         // until 1.7
         while ($i < 8) {
-            $result = $this->_invoiceController->createAutoInvoices($date);
+            $this->_invoiceController->createAutoInvoices($date);
             $date->addMonth(1);
             $i++;
         }
@@ -149,9 +153,13 @@ class Sales_InvoiceExportTests extends Sales_InvoiceTestCase
         $spreadsheetXml = $xml->children($ns['office'])->{'body'}->{'spreadsheet'};
     
         // the product should be found here
-        $half = 0;
-        $quarter = 0;
-    
-        $this->assertEquals('Debitor', (string) $spreadsheetXml->children($ns['table'])->{'table'}->{'table-row'}->{1}->children($ns['table'])->{'table-cell'}->{3}->children($ns['text'])->{0});
+        $this->assertEquals('Debitor', (string) $spreadsheetXml->children(
+                $ns['table']
+            )->{'table'}->{'table-row'}->{1}->children(
+                $ns['table']
+            )->{'table-cell'}->{3}->children(
+                $ns['text']
+            )->{0}
+        );
     }
 }
index bf5d001..297e9c9 100644 (file)
@@ -79,6 +79,7 @@ class Sales_InvoiceJsonTests extends Sales_InvoiceTestCase
         
         $date = clone $this->_referenceDate;
         $date->addHour(3);
+        $date->addMonth(1);
         
         $this->_invoiceController->createAutoInvoices($date);
         
@@ -148,14 +149,9 @@ class Sales_InvoiceJsonTests extends Sales_InvoiceTestCase
         $this->_createFullFixtures();
         
         // the whole year, 12 months
-        $i = 0;
         $date = clone $this->_referenceDate;
-        
-        while ($i < 12) {
-            $result = $this->_invoiceController->createAutoInvoices($date);
-            $date->addMonth(1);
-            $i++;
-        }
+        $date->addMonth(12);
+        $this->_invoiceController->createAutoInvoices($date);
         
         $json = new Sales_Frontend_Json();
         
@@ -275,6 +271,7 @@ class Sales_InvoiceJsonTests extends Sales_InvoiceTestCase
         $i = 0;
         $date = clone $this->_referenceDate;
         $date->addHour(3);
+        $date->addMonth(1);
         
         $result = $this->_invoiceController->createAutoInvoices($date);
         
index f132685..5932393 100644 (file)
@@ -13,7 +13,7 @@
  * 
  * @package     Tasks
  */
-class Tasks_Frontend_ActiveSyncTest extends ActiveSync_TestCase
+class Tasks_Frontend_ActiveSyncTest extends ActiveSync_Controller_ControllerTest
 {
     /**
      * name of the application
index 77060b5..59efe10 100644 (file)
@@ -108,6 +108,8 @@ abstract class TestCase extends PHPUnit_Framework_TestCase
         Addressbook_Controller_Contact::getInstance()->setGeoDataForContacts(true);
         
         Tinebase_Core::set(Tinebase_Core::USER, $this->_originalTestUser);
+        
+        Tinebase_Cache_PerRequest::getInstance()->resetCache();
     }
     
     /**
@@ -242,7 +244,18 @@ abstract class TestCase extends PHPUnit_Framework_TestCase
     protected function _getMailDomain()
     {
         $testconfig = Zend_Registry::get('testConfig');
-        return ($testconfig && isset($testconfig->maildomain)) ? $testconfig->maildomain : 'tine20.org';
+        
+        if ($testconfig && isset($testconfig->maildomain)) {
+            return $testconfig->maildomain;
+        }
+        
+        if (!empty(Tinebase_Core::getUser()->accountEmailAddress)) {
+            list($user, $domain) = explode('@', Tinebase_Core::getUser()->accountEmailAddress, 2);
+            
+            return $domain;
+        }
+        
+        return 'tine20.org';
     }
     
     /**
@@ -358,4 +371,34 @@ abstract class TestCase extends PHPUnit_Framework_TestCase
         $tempFile = $tempFileBackend->createTempFile(dirname(__FILE__) . '/Filemanager/files/test.txt');
         return $tempFile;
     }
+    
+    /**
+     * set grants for a persona
+     * 
+     * @param integer $containerId
+     * @param string $persona
+     * @param string $adminGrant
+     */
+    protected function _setPersonaGrantsForTestContainer($containerId, $persona, $adminGrant = false)
+    {
+        $grants = new Tinebase_Record_RecordSet('Tinebase_Model_Grants', array(array(
+            'account_id'    => $this->_personas[$persona]->getId(),
+            'account_type'  => 'user',
+            Tinebase_Model_Grants::GRANT_READ     => true,
+            Tinebase_Model_Grants::GRANT_ADD      => true,
+            Tinebase_Model_Grants::GRANT_EDIT     => true,
+            Tinebase_Model_Grants::GRANT_DELETE   => true,
+            Tinebase_Model_Grants::GRANT_ADMIN    => $adminGrant,
+        ), array(
+            'account_id'    => Tinebase_Core::getUser()->getId(),
+            'account_type'  => 'user',
+            Tinebase_Model_Grants::GRANT_READ     => true,
+            Tinebase_Model_Grants::GRANT_ADD      => true,
+            Tinebase_Model_Grants::GRANT_EDIT     => true,
+            Tinebase_Model_Grants::GRANT_DELETE   => true,
+            Tinebase_Model_Grants::GRANT_ADMIN    => true,
+        )));
+        
+        Tinebase_Container::getInstance()->setGrants($containerId, $grants, TRUE);
+    }
 }
index f30f27d..53ce03c 100644 (file)
@@ -6,7 +6,7 @@
  * 
  * @package     Timetracker
  * @license     http://www.gnu.org/licenses/agpl.html
- * @copyright   Copyright (c) 2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2012-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Alexander Stintzing <a.stintzing@metaways.de>
  */
 
@@ -110,6 +110,19 @@ class Timetracker_FilterTest extends Timetracker_AbstractTest
         $result = $this->_timeaccountController->search($f);
         $this->assertEquals(1, $result->count());
         $this->assertEquals('TA1', $result->getFirstRecord()->title);
+        
+        // test "not" operator
+        $f = new Timetracker_Model_TimeaccountFilter(array(
+            array('field' => 'contract', 'operator' => 'AND', 'value' =>
+                array(array('field' => ':id', 'operator' => 'not', 'value' => $contract->getId()))
+            ),
+            array('field' => 'description', 'operator' => 'equals', 'value' => 'UnitTest')
+        ));
+        
+        $result = $this->_timeaccountController->search($f);
+        
+        // TODO is this correct? do we expect the timaccount without contract to be missing from results?
+        $this->assertEquals(0, $result->count(), 'No record should be found');
     }
 
     /**
index 91c8d46..35f0dc2 100644 (file)
@@ -41,7 +41,7 @@ class Tinebase_AccountTest extends TestCase
                 'accountPrimaryGroup' => Tinebase_Core::getUser()->accountPrimaryGroup,
                 'accountLastName'     => 'Tine 2.0',
                 'accountFirstName'    => 'PHPUnit',
-                'accountEmailAddress' => 'phpunit@metaways.de'
+                'accountEmailAddress' => 'phpunit@' . $this->_getMailDomain()
             )
         ));
         
index 95c79e4..a86e346 100644 (file)
@@ -29,6 +29,8 @@ class Tinebase_ConfigTest extends PHPUnit_Framework_TestCase
      */
     protected $objects = array();
 
+    protected $_filenamesToDelete = array();
+    
     /**
      * Runs the test methods of this class.
      *
@@ -60,6 +62,9 @@ class Tinebase_ConfigTest extends PHPUnit_Framework_TestCase
      */
     protected function tearDown()
     {
+        foreach ($this->_filenamesToDelete as $filename) {
+            unlink($filename);
+        }
     }
     
     /**
@@ -150,7 +155,8 @@ class Tinebase_ConfigTest extends PHPUnit_Framework_TestCase
     /**
      * test if config returns empty array if it's empty
      */
-    public function testReturnEmptyValue() {
+    public function testReturnEmptyValue()
+    {
         // Hold original value for further tests of sieve.
         $keepOriginalValue = $this->_instance->get("sieve");
         
@@ -166,4 +172,45 @@ class Tinebase_ConfigTest extends PHPUnit_Framework_TestCase
         // restore value
         $this->_instance->set("sieve", $keepOriginalValue);
     }
+    
+    /**
+     * testApplicationDefaultConfig
+     */
+    public function testApplicationDefaultConfig()
+    {
+        $ignoreBillablesConfig = Sales_Config::getInstance()->get(Sales_Config::IGNORE_BILLABLES_BEFORE);
+        $this->assertEquals('2000-01-01 22:00:00', $ignoreBillablesConfig);
+        
+        $dest = $this->_getSalesCustomDefaultConfig();
+        
+        if (! file_exists($dest)) {
+            copy(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'files' . DIRECTORY_SEPARATOR . 'configtest.inc.php', $dest);
+            $this->_filenamesToDelete[] = $dest;
+            
+            $ignoreBillablesConfigAppDefault = Sales_Config::getInstance()->get(Sales_Config::IGNORE_BILLABLES_BEFORE);
+            $this->assertEquals('1999-10-01 22:00:00', $ignoreBillablesConfigAppDefault);
+        }
+    }
+    
+    protected function _getSalesCustomDefaultConfig()
+    {
+        return dirname(dirname(dirname(dirname(__FILE__)))) . DIRECTORY_SEPARATOR . 'tine20' . DIRECTORY_SEPARATOR . 'Sales' . DIRECTORY_SEPARATOR . 'config.inc.php';
+    }
+    
+    /**
+     * testFeatureEnabled
+     * 
+     * @see 0010756: add feature switches for easy enabling/disabling of features
+     */
+    public function testFeatureEnabled()
+    {
+        $customConfigFilename = $this->_getSalesCustomDefaultConfig();
+        if (file_exists($customConfigFilename)) {
+            $this->markTestSkipped('do not test with existing custom config');
+        }
+        
+        $invoicesFeatureEnabled = Sales_Config::getInstance()->featureEnabled(Sales_Config::FEATURE_INVOICES_MODULE);
+        
+        $this->assertTrue($invoicesFeatureEnabled);
+    }
 }
index 43992f9..9507d52 100644 (file)
@@ -424,7 +424,6 @@ class Tinebase_ContainerTest extends PHPUnit_Framework_TestCase
     
     /**
      * try to get container by acl
-     *
      */
     public function testGetContainerByAcl()
     {
@@ -438,6 +437,18 @@ class Tinebase_ContainerTest extends PHPUnit_Framework_TestCase
             $this->_validatePath($container);
         }
     }
+
+    /**
+     * try to get container by acl with Zend_Cache
+     */
+    public function testGetContainerByAclWithPersistentCaching()
+    {
+        Tinebase_Cache_PerRequest::getInstance()->usePersistentCache(true);
+        $this->testGetContainerByAcl();
+        $oldValue = Tinebase_Cache_PerRequest::getInstance()->usePersistentCache(false);
+
+        $this->assertTrue($oldValue);
+    }
     
     /**
      * test getGrantsOfRecords
@@ -550,7 +561,9 @@ class Tinebase_ContainerTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(1, $otherUsers->filter('accountId', $user2->accountId)->count());
         
         Tinebase_User::getInstance()->setStatus($user2, 'disabled');
-        
+
+        Tinebase_Cache_PerRequest::getInstance()->resetCache();
+
         $otherUsers = Tinebase_Container::getInstance()->getOtherUsers($user1, 'Calendar', array(
             Tinebase_Model_Grants::GRANT_READ
         ));
index 6beca61..b0e18b1 100644 (file)
@@ -4,16 +4,11 @@
  * 
  * @package     Tinebase
  * @license     http://www.gnu.org/licenses/agpl.html
- * @copyright   Copyright (c) 2010-2013 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2010-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Philipp Schüle <p.schuele@metaways.de>
  */
 
 /**
- * Test helper
- */
-require_once dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
-
-/**
  * Test class for Tinebase_Frontend_Cli
  */
 class Tinebase_Frontend_CliTest extends TestCase
@@ -96,6 +91,7 @@ class Tinebase_Frontend_CliTest extends TestCase
         
         ob_start();
         $this->_cli->clearTable($opts);
+        // TODO check $out
         $out = ob_get_clean();
         
         $accessLogsAfter = Admin_Controller_AccessLog::getInstance()->search();
@@ -139,7 +135,7 @@ class Tinebase_Frontend_CliTest extends TestCase
 
         $contactBackend = Addressbook_Backend_Factory::factory(Addressbook_Backend_Factory::SQL);
         $this->setExpectedException('Tinebase_Exception_NotFound');
-        $deletedRecord = $contactBackend->get($deletedRecord->getId(), TRUE);
+        $contactBackend->get($deletedRecord->getId(), TRUE);
     }
 
     /**
@@ -295,4 +291,21 @@ class Tinebase_Frontend_CliTest extends TestCase
         $this->assertGreaterThan(1, count($matches));
         $this->assertGreaterThanOrEqual(0, $matches[1]);
     }
+
+    /**
+     * testMonitoringActiveUsers
+     *
+     * TODO generalize monitoring tests
+     */
+    public function testMonitoringActiveUsers()
+    {
+        ob_start();
+        $result = $this->_cli->monitoringActiveUsers();
+        $out = ob_get_clean();
+        $this->assertEquals(0, $result);
+
+        preg_match('/ACTIVE USERS OK \| count=(\d+);;;;/', $out, $matches);
+        $this->assertGreaterThan(1, count($matches));
+        $this->assertGreaterThanOrEqual(1, $matches[1], 'at least unittest user should have logged in once');
+    }
 }
index 254140e..ffa7910 100644 (file)
@@ -5,7 +5,7 @@
  * Test class for Tinebase_Frontend_Json_PersistentFilter
  * 
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
- * @copyright   Copyright (c) 2009-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2009-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Cornelius Weiss <c.weiss@metaways.de>
  * 
  */
@@ -376,4 +376,61 @@ class Tinebase_Frontend_Json_PersistentFilterTest extends TestCase
         
         $this->testOverwriteExistingFilter();
     }
+
+    /**
+     * testFilterResolving
+     * 
+     * -> relations should not be resolved here!
+     */
+    public function testFilterResolving()
+    {
+        $contact1 = Addressbook_Controller_Contact::getInstance()->create(new Addressbook_Model_Contact(array('n_family' => 'test heini')));
+        $contactWithRelations = array(
+            'n_family' => 'test typ',
+            'relations' => array(array(
+                'own_model'              => 'Addressbook_Model_Contact',
+                'own_backend'            => 'Sql',
+                'own_id'                 => 0,
+                'own_degree'             => Tinebase_Model_Relation::DEGREE_SIBLING,
+                'type'                   => '',
+                'related_backend'        => 'Sql',
+                'related_id'             => $contact1->getId(),
+                'related_model'          => 'Addressbook_Model_Contact',
+                'remark'                 => NULL,
+            ),
+            )
+        );
+        $contact2 = Addressbook_Controller_Contact::getInstance()->create(new Addressbook_Model_Contact($contactWithRelations));
+        $exampleFilterData = self::getPersistentFilterData();
+        $exampleFilterData['filters'] = array(
+            array('field' => 'foreignRecord', 'operator' => 'AND', 'value' => array(
+                'appName' => 'Addressbook',
+                'modelName' => 'Contact',
+                'linkType' => 'relation',
+                'filters' => array
+                (
+                    array
+                    (
+                        'field' => ':id',
+                        'operator' => 'equals',
+                        'value' => $contact2->getId()
+                    )
+                )
+            ))
+        );
+        $savedFilterData = $this->_uit->savePersistentFilter($exampleFilterData);
+        
+        $filterData = array(
+            array('field' => 'model',   'operator' => 'equals', 'value' => 'Tasks_Model_TaskFilter'),
+            array('field' => 'id',      'operator' => 'equals', 'value' => $savedFilterData['id'])
+        );
+        
+        $searchResult = $this->_uit->searchPersistentFilter($filterData, NULL);
+        
+        $this->assertEquals(1, $searchResult['totalcount']);
+        
+        $this->assertTrue(isset($searchResult['results'][0]['filters'][0]['value']['filters'][0]['value']));
+        $filterContact = $searchResult['results'][0]['filters'][0]['value']['filters'][0]['value'];
+        $this->assertTrue(! isset($filterContact['relations']), 'relations should not be resolved:' . print_r($filterContact, true));
+    }
 }
index 7d1adeb..048283a 100644 (file)
@@ -57,7 +57,7 @@ class Tinebase_User_EmailUser_Imap_DovecotTest extends PHPUnit_Framework_TestCas
     protected function setUp()
     {
         $this->_config = Tinebase_Config::getInstance()->get(Tinebase_Config::IMAP, new Tinebase_Config_Struct())->toArray();
-        if (!isset($this->_config['backend']) || !(ucfirst($this->_config['backend']) == Tinebase_EmailUser::DOVECOT_IMAP) || $this->_config['active'] != true) {
+        if (!isset($this->_config['backend']) || !('Imap_' . ucfirst($this->_config['backend']) == Tinebase_EmailUser::IMAP_DOVECOT) || $this->_config['active'] != true) {
             $this->markTestSkipped('Dovecot MySQL backend not configured or not enabled');
         }
         
@@ -113,7 +113,10 @@ class Tinebase_User_EmailUser_Imap_DovecotTest extends PHPUnit_Framework_TestCas
             'emailLastLogin'  => null,
             'emailMailSize'   => 0,
             #'emailSieveQuota' => 0,
-            'emailSieveSize'  => null
+            'emailSieveSize'  => null,
+            'emailPort'       => 143,
+            'emailSecure'     => 'tls',
+            'emailHost'       => 'localhost'
         ), $this->_objects['user']->imapUser->toArray());
         
         return $this->_objects['user'];
@@ -137,13 +140,16 @@ class Tinebase_User_EmailUser_Imap_DovecotTest extends PHPUnit_Framework_TestCas
         $this->assertEquals(array(
             'emailUserId'      => $this->_objects['user']->getId(),
             'emailUsername'    => $this->_objects['user']->imapUser->emailUsername,
-            'emailMailQuota'   => 600,
+            'emailMailQuota'   => '600',
             'emailUID'         => !empty($this->_config['dovecot']['uid']) ? $this->_config['dovecot']['uid'] : '1000',
             'emailGID'         => !empty($this->_config['dovecot']['gid']) ? $this->_config['dovecot']['gid'] : '1000',
             'emailLastLogin'   => null,
             'emailMailSize'    => 0,
             #'emailSieveQuota'  => 0,
-            'emailSieveSize'   => null
+            'emailSieveSize'   => null,
+            'emailPort'        => 143,
+            'emailSecure'      => 'tls',
+            'emailHost'        => 'localhost'
         ), $this->_objects['user']->imapUser->toArray());
     }
     
index 7c21364..0ad2669 100644 (file)
@@ -102,7 +102,7 @@ class Tinebase_User_EmailUser_Smtp_PostfixTest extends PHPUnit_Framework_TestCas
         
         $testUser = $this->_backend->addUser($user);
         $this->objects['users']['testUser'] = $testUser;
-
+        
         $this->assertTrue($testUser instanceof Tinebase_Model_FullUser);
         $this->assertTrue(isset($testUser->smtpUser), 'no smtpUser data found in ' . print_r($testUser->toArray(), TRUE));
         $this->assertEquals(array('unittest@' . $this->_mailDomain, 'test@' . $this->_mailDomain), $testUser->smtpUser->emailForwards, 'forwards not found');
index 2375e8a..1672e19 100644 (file)
@@ -360,6 +360,16 @@ class Tinebase_User_SqlTest extends PHPUnit_Framework_TestCase
      */
     public static function getTestRecord()
     {
+        $testconfig = Zend_Registry::get('testConfig');
+        
+        if ($testconfig && isset($testconfig->maildomain)) {
+            $domain = $testconfig->maildomain;
+        } else if (!empty(Tinebase_Core::getUser()->accountEmailAddress)) {
+            list($user, $domain) = explode('@', Tinebase_Core::getUser()->accountEmailAddress, 2);
+        } else {
+             $domain = 'tine20.org';
+        }
+        
         $user  = new Tinebase_Model_FullUser(array(
             'accountLoginName'      => 'tine20phpunituser',
             'accountStatus'         => 'enabled',
@@ -367,14 +377,9 @@ class Tinebase_User_SqlTest extends PHPUnit_Framework_TestCase
             'accountPrimaryGroup'   => Tinebase_Group::getInstance()->getDefaultGroup()->id,
             'accountLastName'       => 'Tine 2.0',
             'accountFirstName'      => 'PHPUnit User',
-            'accountEmailAddress'   => 'phpunit@tine20.org'
+            'accountEmailAddress'   => 'phpunit@' . $domain
         ));
         
         return $user;
     }
-}       
-    
-
-if (PHPUnit_MAIN_METHOD == 'Tinebase_User_SqlTest::main') {
-    Tinebase_User_SqlTest::main();
-}
+}
\ No newline at end of file
diff --git a/tests/tine20/Tinebase/files/configtest.inc.php b/tests/tine20/Tinebase/files/configtest.inc.php
new file mode 100644 (file)
index 0000000..032d471
--- /dev/null
@@ -0,0 +1,5 @@
+<?php
+
+return array(
+    'ignoreBillablesBefore' => '1999-10-01 22:00:00',
+);
diff --git a/tine20/ActiveSync/Acl/Rights.php b/tine20/ActiveSync/Acl/Rights.php
new file mode 100644 (file)
index 0000000..adb367b
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+/**
+ * Tine 2.0
+ * 
+ * @package     ActiveSync
+ * @subpackage  Acl
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2015 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Philipp Schüle <p.schuele@metaways.de>
+ * 
+ */
+
+/**
+ * this class handles the rights for the ActiveSync application
+ * 
+ * a right is always specific to an application and not to a record
+ * examples for rights are: admin, run
+ * 
+ * to add a new right you have to do these 3 steps:
+ * - add a constant for the right
+ * - add the constant to the $addRights in getAllApplicationRights() function
+ * . add getText identifier in getTranslatedRightDescriptions() function
+ * 
+ * @package     ActiveSync
+ * @subpackage  Acl
+ */
+class ActiveSync_Acl_Rights extends Tinebase_Acl_Rights_Abstract
+{
+    /**
+     * the right to manage shared contact favorites
+     * 
+     * @staticvar string
+     */
+    const MANAGE_DEVICES = 'manage_devices';
+    
+    /**
+     * holds the instance of the singleton
+     *
+     * @var ActiveSync_Acl_Rights
+     */
+    private static $_instance = NULL;
+    
+    /**
+     * the clone function
+     *
+     * disabled. use the singleton
+     */
+    private function __clone() 
+    {
+    }
+    
+    /**
+     * the constructor
+     *
+     */
+    private function __construct()
+    {
+        
+    }    
+    
+    /**
+     * the singleton pattern
+     *
+     * @return ActiveSync_Acl_Rights
+     */
+    public static function getInstance() 
+    {
+        if (self::$_instance === NULL) {
+            self::$_instance = new ActiveSync_Acl_Rights;
+        }
+        
+        return self::$_instance;
+    }
+    
+    /**
+     * get all possible application rights
+     *
+     * @return  array   all application rights
+     */
+    public function getAllApplicationRights()
+    {
+        
+        $allRights = parent::getAllApplicationRights();
+        
+        $addRights = array(
+            self::MANAGE_DEVICES,
+        );
+        $allRights = array_merge($allRights, $addRights);
+        
+        return $allRights;
+    }
+
+    /**
+     * get translated right descriptions
+     * 
+     * @return  array with translated descriptions for this applications rights
+     */
+    public static function getTranslatedRightDescriptions()
+    {
+        $translate = Tinebase_Translation::getTranslation('ActiveSync');
+        
+        $rightDescriptions = array(
+            self::MANAGE_DEVICES => array(
+                'text'          => $translate->_('Manage ActiveSync devices'),
+                'description'   => $translate->_('See, edit and delete ActiveSync devices'),
+            ),
+        );
+        
+        $rightDescriptions = array_merge($rightDescriptions, parent::getTranslatedRightDescriptions());
+        return $rightDescriptions;
+    }
+}
index 6b97072..d4dcdd2 100644 (file)
         {
           "text": "DeviceStore.js",
           "path": "js/"
+        },
+        {
+          "text": "SyncDevices.js",
+          "path": "js/"
+        },
+        {
+          "text": "EditDialog.js",
+          "path": "js/"
+        },
+        {
+          "text": "SyncDevicesGridPanel.js",
+          "path": "js/"
+        },
+        {
+          "text": "AdminPanel.js",
+          "path": "js/"
         }
       ]
     },
diff --git a/tine20/ActiveSync/Controller/SyncDevices.php b/tine20/ActiveSync/Controller/SyncDevices.php
new file mode 100644 (file)
index 0000000..6a266cb
--- /dev/null
@@ -0,0 +1,143 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     ActiveSync
+ * @subpackage  Controller
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ * @copyright   Copyright (c) 2014-2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ */
+
+/**
+ * Sync devices controller for ActiveSync application
+ *
+ * @package     ActiveSync
+ * @subpackage  Controller
+ */
+class ActiveSync_Controller_SyncDevices extends Tinebase_Controller_Record_Abstract
+{
+    /**
+     * application name (is needed in checkRight())
+     *
+     * @var string
+     */
+    protected $_applicationName = 'ActiveSync';
+    
+    /**
+     * Model name
+     *
+     * @var string
+     */
+    protected $_modelName = 'ActiveSync_Model_Device';
+    
+    /**
+     * check for container ACLs
+     *
+     * @var boolean
+     *
+     * @todo rename to containerACLChecks
+     */
+    protected $_doContainerACLChecks = false;
+    
+    /**
+     * holds the instance of the singleton
+     *
+     * @var ActiveSync_Controller_SyncDevices
+     */
+    private static $_instance = null;
+    
+    /**
+     * the constructor
+     *
+     * don't use the constructor. use the singleton 
+     */
+    private function __construct() 
+    {
+        $this->_backend = new ActiveSync_Backend_Device();
+    }
+
+    /**
+     * don't clone. Use the singleton.
+     *
+     */
+    private function __clone() 
+    {
+    }
+    
+    /**
+     * the singleton pattern
+     *
+     * @return ActiveSync_Controller_SyncDevices
+     */
+    public static function getInstance() 
+    {
+        if (self::$_instance === NULL) {
+            self::$_instance = new ActiveSync_Controller_SyncDevices;
+        }
+        
+        return self::$_instance;
+    }
+    
+    /**
+     * get list of access log entries
+     *
+     * @param Tinebase_Model_Filter_FilterGroup|optional $_filter
+     * @param Tinebase_Model_Pagination|optional $_pagination
+     * @param boolean $_getRelations
+     * @param boolean $_onlyIds
+     * @param string $_action for right/acl check
+     * @return Tinebase_Record_RecordSet|array
+     */
+    public function search(Tinebase_Model_Filter_FilterGroup $_filter = NULL, Tinebase_Record_Interface $_pagination = NULL, $_getRelations = FALSE, $_onlyIds = FALSE, $_action = 'get')
+    {
+        $this->checkRight('MANAGE_DEVICES');
+        
+        return parent::search($_filter, $_pagination, $_getRelations, $_onlyIds, $_action);
+    }
+    
+    /**
+     * returns the total number of access logs
+     * 
+     * @param Tinebase_Model_Filter_FilterGroup $_filter
+     * @param string $_action for right/acl check
+     * @return int
+     */
+    public function searchCount(Tinebase_Model_Filter_FilterGroup $_filter, $_action = 'get')
+    {
+        $this->checkRight('MANAGE_DEVICES');
+        
+        return parent::searchCount($_filter, $_action);
+    }
+    
+    /**
+     * delete access log entries
+     *
+     * @param   array $_ids list of logIds to delete
+     */
+    public function delete($_ids)
+    {
+        $this->checkRight('MANAGE_DEVICES');
+        
+        return parent::delete($_ids);
+    }
+
+    /**
+     * inspect update of one record (before update)
+     *
+     * @param   Tinebase_Record_Interface $_record      the update record
+     * @param   Tinebase_Record_Interface $_oldRecord   the current persistent record
+     * @return  void
+     */
+    protected function _inspectBeforeUpdate($_record, $_oldRecord)
+    {
+        // spoofing protection
+        $fieldsToUnset = array('id', 'deviceid', 'devicetype', 'policy_id', 'acsversion', 'useragent',
+            'model', 'os', 'oslanguage', 'pinglifetime', 'pingfolder', 'remotewipe', 'calendarfilter_id',
+            'contactsfilter_id', 'emailfilter_id', 'tasksfilter_id', 'lastping');
+        
+        foreach ($fieldsToUnset as $field) {
+            $_record->{$field} = $_oldRecord{$field};
+        }
+    }
+}
index c155145..317741e 100755 (executable)
@@ -6,7 +6,7 @@
  * @subpackage  Frontend
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Lars Kneschke <l.kneschke@metaways.de>
- * @copyright   Copyright (c) 2009 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2009-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  *
  */
 
@@ -58,4 +58,53 @@ class ActiveSync_Frontend_Json extends Tinebase_Frontend_Json_Abstract
             'userDevices' => $userDevices->toArray()
         );
     }
+    
+    /****************************** SyncDevices ******************************/
+    
+    /**
+     * Search for records matching given arguments
+     *
+     * @param array $filter
+     * @param array $paging
+     * @return array
+     */
+    public function searchSyncDevices($filter, $paging)
+    {
+        $result = $this->_search($filter, $paging, ActiveSync_Controller_SyncDevices::getInstance(), 'ActiveSync_Model_DeviceFilter');
+    
+        return $result;
+    }
+    
+    /**
+     * Return a single record
+     *
+     * @param   string $id
+     * @return  array record data
+     */
+    public function getSyncDevice($id)
+    {
+        return $this->_get($id, ActiveSync_Controller_SyncDevices::getInstance());
+    }
+    
+    /**
+     * creates/updates a record
+     *
+     * @param  array $recordData
+     * @return array created/updated record
+     */
+    public function saveSyncDevice($recordData)
+    {
+        return $this->_save($recordData, ActiveSync_Controller_SyncDevices::getInstance(), 'ActiveSync_Model_Device', 'id');
+    }
+    
+    /**
+     * deletes existing records
+     *
+     * @param  array  $ids
+     * @return string
+     */
+    public function deleteSyncDevices($ids)
+    {
+        return $this->_delete($ids, ActiveSync_Controller_SyncDevices::getInstance());
+    }
 }
index bd36f58..92034cb 100644 (file)
@@ -6,8 +6,7 @@
  * @subpackage  Model
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Lars Kneschke <l.kneschke@metaways.de>
- * @copyright   Copyright (c) 2008-2012 Metaways Infosystems GmbH (http://www.metaways.de)
- * 
+ * @copyright   Copyright (c) 2014-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  */
 
 /**
@@ -24,7 +23,7 @@
  * @property  string  $policykey          the current policykey
  * @property  string  $tasksfilter_id     the tasks filter id
  */
-class ActiveSync_Model_Device extends Tinebase_Record_Abstract #implements Syncroton_Model_IDevice
+class ActiveSync_Model_Device extends Tinebase_Record_Abstract
 {
     /**
      * key in $_validators/$_properties array for the filed which 
@@ -42,6 +41,20 @@ class ActiveSync_Model_Device extends Tinebase_Record_Abstract #implements Syncr
     protected $_application = 'ActiveSync';
     
     /**
+     * if foreign Id fields should be resolved on search and get from json
+     * should have this format:
+     *     array('Calendar_Model_Contact' => 'contact_id', ...)
+     * or for more fields:
+     *     array('Calendar_Model_Contact' => array('contact_id', 'customer_id), ...)
+     * (e.g. resolves contact_id with the corresponding Model)
+     *
+     * @var array
+     */
+    protected static $_resolveForeignIdFields = array(
+        'Tinebase_Model_User' => array('owner_id'),
+    );
+    
+    /**
      * list of zend inputfilter
      * 
      * this filter get used when validating user generated content with Zend_Input_Filter
@@ -65,7 +78,7 @@ class ActiveSync_Model_Device extends Tinebase_Record_Abstract #implements Syncr
         'devicetype'            => array(Zend_Filter_Input::ALLOW_EMPTY => false, 'presence'=>'required'),
         'owner_id'              => array(Zend_Filter_Input::ALLOW_EMPTY => false, 'presence'=>'required'),
         'policy_id'             => array(Zend_Filter_Input::ALLOW_EMPTY => true),
-        'policykey'             => array(Zend_Filter_Input::ALLOW_EMPTY => true),
+        //'policykey'             => array(Zend_Filter_Input::ALLOW_EMPTY => true),
         'acsversion'            => array(Zend_Filter_Input::ALLOW_EMPTY => false, 'presence'=>'required'),
         'useragent'             => array(Zend_Filter_Input::ALLOW_EMPTY => false, 'presence'=>'required'),
         'model'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true),
@@ -81,6 +94,16 @@ class ActiveSync_Model_Device extends Tinebase_Record_Abstract #implements Syncr
         'contactsfilter_id'     => array(Zend_Filter_Input::ALLOW_EMPTY => true),
         'emailfilter_id'        => array(Zend_Filter_Input::ALLOW_EMPTY => true),
         'tasksfilter_id'        => array(Zend_Filter_Input::ALLOW_EMPTY => true),
+        'lastping'              => array(Zend_Filter_Input::ALLOW_EMPTY => true)
+    );
+    
+    /**
+     * name of fields containing datetime or or an array of datetime information
+     *
+     * @var array list of datetime fields
+     */
+    protected $_datetimeFields = array(
+        'lastping'
     );
     
     /**
index 8993c3c..1df6340 100644 (file)
@@ -37,7 +37,7 @@ class ActiveSync_Model_DeviceFilter extends Tinebase_Model_Filter_FilterGroup
      */
     protected $_filterModel = array(
         'id'                   => array('filter' => 'Tinebase_Model_Filter_Id'),
-        #'query'                => array('filter' => 'Tinebase_Model_Filter_Query', 'options' => array('fields' => array('n_family', 'n_given', 'org_name', 'email', 'adr_one_locality',))),
+        'query'                => array('filter' => 'Tinebase_Model_Filter_Query', 'options' => array('fields' => array('deviceid', 'devicetype', 'friendlyname'))),
         'deviceid'             => array('filter' => 'Tinebase_Model_Filter_Text'),
         'owner_id'             => array('filter' => 'Tinebase_Model_Filter_Text'),
     );
diff --git a/tine20/ActiveSync/Setup/Update/Release8.php b/tine20/ActiveSync/Setup/Update/Release8.php
new file mode 100644 (file)
index 0000000..5fd136c
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     ActiveSync
+ * @subpackage  Setup
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2013 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Jan Evers <j.evers@metaways.de>
+ */
+
+/**
+ * updates for major release 8
+ *
+ * @package     ActiveSync
+ * @subpackage  Setup
+ */
+class ActiveSync_Setup_Update_Release8 extends Setup_Update_Abstract
+{
+    /**
+     * update to 8.1
+     *
+     * @see 0010752: update script for android 5.0 / lollipop devices
+     *
+     * @return void
+     */
+    public function update_0()
+    {
+        $from = SQL_TABLE_PREFIX . 'acsync_device';
+        $where = array($this->_db->quoteIdentifier('useragent') . ' LIKE ?' => 'Android/5%');
+        $this->_db->delete($from, $where);
+
+        $this->setApplicationVersion('ActiveSync', '8.1');
+    }
+}
index b1b2f7a..51b2238 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <application>
     <name>ActiveSync</name>
-    <version>8.0</version>
+    <version>8.1</version>
     <order>90</order>
     <depends>
         <application>Tinebase</application>
diff --git a/tine20/ActiveSync/js/AdminPanel.js b/tine20/ActiveSync/js/AdminPanel.js
new file mode 100644 (file)
index 0000000..94017e0
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Tine 2.0
+ * 
+ * @package     ActiveSync
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Philipp Schüle <p.schuele@metaways.de>
+ * @copyright   Copyright (c) 2015 Metaways Infosystems GmbH (http://www.metaways.de)
+ *
+ */
+
+Ext.namespace('Tine.ActiveSync');
+
+/**
+ * admin settings panel
+ * 
+ * @namespace   Tine.ActiveSync
+ * @class       Tine.ActiveSync.AdminPanel
+ * @extends     Ext.TabPanel
+ * 
+ * <p>ActiveSync Admin Panel</p>
+ * <p><pre>
+ * </pre></p>
+ * 
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Philipp Schuele <p.schuele@metaways.de>
+ * @copyright   Copyright (c) 2009 Metaways Infosystems GmbH (http://www.metaways.de)
+ * 
+ * @param       {Object} config
+ * @constructor
+ * Create a new Tine.ActiveSync.AdminPanel
+ */
+Tine.ActiveSync.AdminPanel = Ext.extend(Ext.TabPanel, {
+
+    border: false,
+    activeTab: 0,
+
+    /**
+     * @private
+     */
+    initComponent: function() {
+        
+        this.app = Tine.Tinebase.appMgr.get('ActiveSync');
+        
+        this.items = [new Tine.ActiveSync.SyncDevicesGridPanel({
+            title: this.app.i18n._('Sync Devices'),
+            // TODO make this work
+            disabled: ! Tine.Tinebase.common.hasRight('manage_devices', 'ActiveSync')
+        })];
+        
+        Tine.ActiveSync.AdminPanel.superclass.initComponent.call(this);
+    }
+});
+    
+/**
+ * ActiveSync Admin Panel Popup
+ * 
+ * @param   {Object} config
+ * @return  {Ext.ux.Window}
+ */
+Tine.ActiveSync.AdminPanel.openWindow = function (config) {
+    var window = Tine.WindowFactory.getWindow({
+        width: 700,
+        height: 470,
+        name: 'activesync-manage-syncdevice',
+        contentPanelConstructor: 'Tine.ActiveSync.AdminPanel',
+        contentPanelConstructorConfig: config
+    });
+};
diff --git a/tine20/ActiveSync/js/EditDialog.js b/tine20/ActiveSync/js/EditDialog.js
new file mode 100644 (file)
index 0000000..e686830
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * Tine 2.0
+ * 
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ * @copyright   Copyright (c) 2014-2015 Metaways Infosystems GmbH (http://www.metaways.de)
+ */
+/*global Ext, Tine*/
+
+Ext.ns('Tine.ActiveSync.syncdevices');
+
+/**
+ * @namespace   Tine.ActiveSync.syncdevices
+ * @class       Tine.ActiveSync.SyncDeviceEditDialog
+ * @extends     Tine.widgets.dialog.EditDialog
+ * 
+ * <p>Sync devices edit dialog</p>
+ * <p>
+ * </p>
+ * 
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ * 
+ * @param       {Object} config
+ * @constructor
+ * Create a new Tine.Admin.SyncDeviceEditDialog
+ */
+Tine.ActiveSync.SyncDeviceEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
+    
+    /**
+     * @private
+     */
+    windowNamePrefix: 'syncdeviceEditWindow_',
+    appName: 'ActiveSync',
+    recordClass: Tine.ActiveSync.Model.SyncDevice,
+    recordProxy: Tine.ActiveSync.syncdevicesBackend,
+    evalGrants: false,
+    
+    /**
+     * executed after record got updated from proxy
+     */
+    onRecordLoad: function () {
+        Tine.ActiveSync.SyncDeviceEditDialog.superclass.onRecordLoad.apply(this, arguments);
+    },
+    
+    /**
+     * executed when record gets updated from form
+     */
+    onRecordUpdate: function () {
+        Tine.ActiveSync.SyncDeviceEditDialog.superclass.onRecordUpdate.apply(this, arguments);
+    },
+    
+    /**
+     * returns dialog
+     */
+    getFormItems: function () {
+        return {
+            layout: 'vbox',
+            layoutConfig: {
+                align: 'stretch',
+                pack: 'start'
+            },
+            border: false,
+            items: [{
+                xtype: 'columnform',
+                border: false,
+                autoHeight: true,
+                items: [
+                    [{
+                        columnWidth: 0.33,
+                        fieldLabel: this.app.i18n._('Device ID'),
+                        name: 'deviceid',
+                        allowBlank: false,
+                        readOnly: true,
+                        maxLength: 40
+                    }, {
+                        columnWidth: 0.33,
+                        fieldLabel: this.app.i18n._('Devicetype'),
+                        name: 'devicetype',
+                        allowBlank: false,
+                        readOnly: true,
+                        maxLength: 40
+                    }, {
+                        columnWidth: 0.333,
+                        fieldLabel: this.app.i18n._('Owner'),
+                        name: 'owner_id',
+                        allowBlank: false,
+                        xtype: 'addressbookcontactpicker',
+                        userOnly: true,
+                        useAccountRecord: true,
+                        blurOnSelect: true,
+                        selectOnFocus: true,
+                        readOnly: true,
+                        maxLength: 40
+                    }], [{
+                        columnWidth: 0.33,
+                        fieldLabel: this.app.i18n._('Policy'),
+                        name: 'policy_id',
+                        readOnly: true,
+                        maxLength: 40
+                    }, {
+                        columnWidth: 0.33,
+                        fieldLabel: this.app.i18n._('AS Version'),
+                        name: 'acsversion',
+                        readOnly: true,
+                        maxLength: 40
+                    }, {
+                        columnWidth: 0.333,
+                        fieldLabel: this.app.i18n._('Useragent'),
+                        name: 'useragent',
+                        readOnly: true,
+                        maxLength: 40
+                    }], [{
+                        columnWidth: 0.33,
+                        fieldLabel: this.app.i18n._('Model'),
+                        name: 'model',
+                        readOnly: true,
+                        maxLength: 40
+                    }, {
+                        columnWidth: 0.33,
+                        fieldLabel: this.app.i18n._('IMEI'),
+                        name: 'imei',
+                        readOnly: true,
+                        maxLength: 40
+                    }, {
+                        columnWidth: 0.333,
+                        fieldLabel: this.app.i18n._('Friendly Name'),
+                        name: 'friendlyname',
+                        readOnly: true,
+                        maxLength: 40
+                    }], [{
+                        columnWidth: 0.33,
+                        fieldLabel: this.app.i18n._('OS'),
+                        name: 'os',
+                        readOnly: true,
+                        maxLength: 40
+                    }, {
+                        columnWidth: 0.33,
+                        fieldLabel: this.app.i18n._('OS Language'),
+                        name: 'oslanguage',
+                        readOnly: true,
+                        maxLength: 40
+                    }, {
+                        columnWidth: 0.333,
+                        fieldLabel: this.app.i18n._('Phonenumber'),
+                        name: 'phonenumber',
+                        readOnly: true,
+                        maxLength: 40
+                    }]
+                ]
+            }]
+        };
+    }
+});
+
+/**
+ * Container Edit Popup
+ * 
+ * @param   {Object} config
+ * @return  {Ext.ux.Window}
+ */
+Tine.ActiveSync.SyncDeviceEditDialog.openWindow = function (config) {
+    var window = Tine.WindowFactory.getWindow({
+        width: 600,
+        height: 400,
+        name: Tine.ActiveSync.SyncDeviceEditDialog.prototype.windowNamePrefix + Ext.id(),
+        contentPanelConstructor: 'Tine.ActiveSync.SyncDeviceEditDialog',
+        contentPanelConstructorConfig: config
+    });
+    return window;
+};
diff --git a/tine20/ActiveSync/js/SyncDevices.js b/tine20/ActiveSync/js/SyncDevices.js
new file mode 100644 (file)
index 0000000..61fd6c3
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * Tine 2.0
+ * 
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ * @copyright   Copyright (c) 2014-2015 Metaways Infosystems GmbH (http://www.metaways.de)
+ */
+
+/*global Ext, Tine*/ 
+
+Ext.ns('Tine.ActiveSync.syncdevices');
+
+/**
+ * Containers 'mainScreen'
+ * 
+ * @static
+ */
+Tine.ActiveSync.syncdevices.show = function () {
+    var app = Tine.Tinebase.appMgr.get('ActiveSync');
+    if (! Tine.ActiveSync.syncDevicesGridPanel) {
+        Tine.ActiveSync.syncDevicesGridPanel = new Tine.ActiveSync.SyncDevicesGridPanel({
+            app: app,
+            asAdminModule: true
+        });
+    } else {
+        Tine.ActiveSync.syncDevicesGridPanel.loadGridData.defer(100, Tine.ActiveSync.syncDevicesGridPanel, []);
+    }
+    
+    Tine.Tinebase.MainScreen.setActiveContentPanel(Tine.ActiveSync.syncDevicesGridPanel, true);
+    Tine.Tinebase.MainScreen.setActiveToolbar(Tine.ActiveSync.syncDevicesGridPanel.actionToolbar, true);
+};
+
+/************** models *****************/
+Ext.ns('Tine.ActiveSync.Model');
+
+/**
+ * Model of an account
+ */
+Tine.ActiveSync.Model.SyncDeviceArray = [
+    { name: 'id' },
+    { name: 'deviceid' },
+    { name: 'devicetype' },
+    { name: 'owner_id' },
+    { name: 'policy_id' },
+    //{ name: 'policykey' },
+    { name: 'acsversion' },
+    { name: 'useragent' },
+    { name: 'model' },
+    { name: 'imei' },
+    { name: 'friendlyname' },
+    { name: 'os' },
+    { name: 'oslanguage' },
+    { name: 'phonenumber' },
+    { name: 'pinglifetime' },
+    { name: 'pingfolder' },
+    { name: 'remotewipe' },
+    { name: 'calendarfilter_id' },
+    { name: 'contactsfilter_id' },
+    { name: 'emailfilter_id' },
+    { name: 'tasksfilter_id' },
+    { name: 'lastping', type: 'date', dateFormat: Date.patterns.ISO8601Long }
+];
+
+Tine.ActiveSync.Model.SyncDevice = Tine.Tinebase.data.Record.create(Tine.ActiveSync.Model.SyncDeviceArray, {
+    appName: 'ActiveSync',
+    modelName: 'SyncDevice',
+    idProperty: 'id',
+    //titleProperty: 'name',
+    titleProperty: 'deviceid',
+    // ngettext('SyncDevice', 'SyncDevices', n);
+    recordName: 'SyncDevice',
+    recordsName: 'SyncDevices'
+});
+
+/**
+ * returns default account data
+ * 
+ * @namespace Tine.ActiveSync.Model.SyncDevice
+ * @static
+ * @return {Object} default data
+ */
+Tine.ActiveSync.Model.SyncDevice.getDefaultData = function () {
+    return {};
+};
+
+/************** backend *****************/
+
+Tine.ActiveSync.syncdevicesBackend = new Tine.Tinebase.data.RecordProxy({
+    appName: 'ActiveSync',
+    modelName: 'SyncDevice',
+    recordClass: Tine.ActiveSync.Model.SyncDevice,
+    idProperty: 'id'
+});
diff --git a/tine20/ActiveSync/js/SyncDevicesGridPanel.js b/tine20/ActiveSync/js/SyncDevicesGridPanel.js
new file mode 100644 (file)
index 0000000..98c21af
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * Tine 2.0
+ * 
+ * @package     ActiveSync
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Stefanie Stamer <s.stamer@metaways.de>
+ * @copyright   Copyright (c) 2015 Metaways Infosystems GmbH (http://www.metaways.de)
+ *
+ */
+Ext.ns('Tine.ActiveSync');
+
+/**
+ * @namespace Tine.ActiveSync
+ * @class     Tine.ActiveSync.SyncDevicesGridPanel
+ * @extends   Tine.widgets.grid.GridPanel
+ * SyncDevicess Grid Panel <br>
+ * 
+ * @author      Cornelius Weiss <c.weiss@metaways.de>
+ */
+Tine.ActiveSync.SyncDevicesGridPanel = Ext.extend(Tine.widgets.grid.GridPanel, {
+    /**
+     * @cfg
+     */
+    recordClass: Tine.ActiveSync.Model.SyncDevice,
+    recordProxy: Tine.ActiveSync.syncdevicesBackend,
+    defaultSortInfo: {field: 'deviceid', direction: 'ASC'},
+    evalGrants: false,
+    gridConfig: {
+        autoExpandColumn: 'deviceid'
+    },
+    addButton: false,
+    asAdminModule: false,
+    
+    /**
+     * initComponent
+     */
+    initComponent: function() {
+        this.app = Tine.Tinebase.appMgr.get('ActiveSync');
+        this.gridConfig.cm = this.getColumnModel();
+        Tine.ActiveSync.SyncDevicesGridPanel.superclass.initComponent.call(this);
+    },
+    
+    /**
+     * returns column model
+     * 
+     * @return Ext.grid.ColumnModel
+     * @private
+     */
+    getColumnModel: function() {
+        return new Ext.grid.ColumnModel({
+            defaults: {
+                sortable: true,
+                hidden: true,
+                resizable: true
+            },
+            columns: this.getColumns()
+        });
+    },
+    
+    /**
+     * returns columns
+     * @private
+     * @return Array
+     */
+    getColumns: function(){
+        return [
+            { header: this.app.i18n._('ID'),             id: 'id',                dataIndex: 'id',                hidden: true,  width: 50},
+            { header: this.app.i18n._('Device ID'),      id: 'deviceid',          dataIndex: 'deviceid',          hidden: false, width: 200},
+            { header: this.app.i18n._('Devicetype'),     id: 'devicetype',        dataIndex: 'devicetype',        hidden: false, width: 100},
+            { header: this.app.i18n._('Owner'),          id: 'owner_id',          dataIndex: 'owner_id',          hidden: false, width: 80,  renderer: Tine.Tinebase.common.usernameRenderer},
+            { header: this.app.i18n._('Policy'),         id: 'policy_id',         dataIndex: 'policy_id',         hidden: false, width: 200},
+            { header: this.app.i18n._('AS Version'),     id: 'acsversion',        dataIndex: 'acsversion',        hidden: false, width: 100},
+            { header: this.app.i18n._('Useragent'),      id: 'useragent',         dataIndex: 'useragent',         hidden: true,  width: 200},
+            { header: this.app.i18n._('Model'),          id: 'model',             dataIndex: 'model',             hidden: false, width: 200},
+            { header: this.app.i18n._('IMEI'),           id: 'imei',              dataIndex: 'imei',              hidden: true,  width: 200},
+            { header: this.app.i18n._('Friendly Name'),  id: 'friendlyname',      dataIndex: 'friendlyname',      hidden: false, width: 200},
+            { header: this.app.i18n._('OS'),             id: 'os',                dataIndex: 'os',                hidden: false, width: 200},
+            { header: this.app.i18n._('OS Language'),    id: 'oslanguage',        dataIndex: 'oslanguage',        hidden: true,  width: 200},
+            { header: this.app.i18n._('Phonenumber'),    id: 'phonenumber',       dataIndex: 'phonenumber',       hidden: false, width: 200},
+            { header: this.app.i18n._('Ping Lifetime'),  id: 'pinglifetime',      dataIndex: 'pinglifetime',      hidden: true,  width: 200},
+            //{ header: this.app.i18n._('Ping Folder'),    id: 'pingfolder',        dataIndex: 'pingfolder',        hidden: false, width: 200},
+            { header: this.app.i18n._('Remote Wipe'),    id: 'remotewipe',        dataIndex: 'remotewipe',        hidden: false, width: 100},
+            { header: this.app.i18n._('Calendarfilter'), id: 'calendarfilter_id', dataIndex: 'calendarfilter_id', hidden: true,  width: 200},
+            { header: this.app.i18n._('Contactsfilter'), id: 'contactsfilter_id', dataIndex: 'contactsfilter_id', hidden: true,  width: 200},
+            { header: this.app.i18n._('Emailfilter'),    id: 'emailfilter_id',    dataIndex: 'emailfilter_id',    hidden: true,  width: 200},
+            { header: this.app.i18n._('Tasksfilter'),    id: 'tasksfilter_id',    dataIndex: 'tasksfilter_id',    hidden: true,  width: 200},
+            { header: this.app.i18n._('Last Ping'),      id: 'lastping',          dataIndex: 'lastping',          hidden: false, width: 200, renderer: Tine.Tinebase.common.dateTimeRenderer}
+        ];
+    },
+        /**
+     * initialises filter toolbar
+     */
+    initFilterPanel: function() {
+        this.filterToolbar = new Tine.widgets.grid.FilterToolbar({
+            filterModels: [
+                {label: this.app.i18n._('Quicksearch'),     field: 'query',    operators: ['contains']},
+                {label: this.app.i18n._('Device ID'),       field: 'deviceid', operators: ['contains']}
+            ],
+            defaultFilter: 'query',
+            /*filters: [
+                {field: 'deviceid', operator: 'equals', value: 'shared'}
+            ],*/
+            plugins: [
+                new Tine.widgets.grid.FilterToolbarQuickFilterPlugin()
+            ]
+        });
+        this.plugins = this.plugins || [];
+        this.plugins.push(this.filterToolbar);
+    },
+    
+    initLayout: function() {
+        this.supr().initLayout.call(this);
+        
+        if (! this.asAdminModule) {
+            this.items.push({
+                region : 'north',
+                height : 55,
+                border : false,
+                items  : this.actionToolbar
+            });
+        }
+    }
+});
index 573dbaa..1b96cc5 100644 (file)
@@ -147,7 +147,7 @@ class Addressbook_Setup_DemoData extends Tinebase_Setup_DemoData_Abstract
                         $this->_addresses[$i]['salutation'] = 'MR';
                     } else {
                         $isMan = false;
-                        $this->_addresses[$i]['salutation'] = 'MRS';
+                        $this->_addresses[$i]['salutation'] = 'MS';
                     }
                 } else {
                     $this->_addresses[$i][$indexes[$index]] = $field;
@@ -194,7 +194,8 @@ class Addressbook_Setup_DemoData extends Tinebase_Setup_DemoData_Abstract
         try {
             $record = $this->_controller->create($record);
             if ($imageData) {
-                $be->_saveImage($record->getId(), $imageData);
+                // @todo We should not use copyrighted/random pictures for demo data
+                //$be->_saveImage($record->getId(), $imageData);
             }
         } catch (Exception $e) {
             echo 'Skipping: ' . $data['n_given'] .' ' . $data['n_family'] . ($data['org_name'] ? ' ('.$data['org_name'].') ' : '') . $e->getMessage() . PHP_EOL;
index ee2edae..f2e8864 100644 (file)
@@ -19,7 +19,7 @@ Tine.Admin = function () {
      */
     var getInitialTree = function (translation) {
         
-        return [{
+        var tree = [{
             text: translation.ngettext('User', 'Users', 50),
             cls: 'treemain',
             iconCls: 'admin-node-user',
@@ -146,6 +146,24 @@ Tine.Admin = function () {
             dataPanelType: "serverinfo",
             viewRight: 'serverinfo'
         }];
+        
+        // TODO find a generic hooking mechanism
+        if (Tine.Tinebase.appMgr.get('ActiveSync') && Tine.Tinebase.common.hasRight('manage_devices', 'ActiveSync')) {
+            tree.push({
+                text: translation.gettext('ActiveSync Devices'),
+                cls: "treemain",
+                iconCls: 'activesync-device-standard',
+                allowDrag: false,
+                allowDrop: true,
+                id: "devices",
+                children: [],
+                leaf: null,
+                expanded: true,
+                dataPanelType: "devices"
+            });
+        }
+        
+        return tree;
     };
 
     /**
@@ -268,6 +286,10 @@ Tine.Admin = function () {
                    Tine.Tinebase.MainScreen.setActiveToolbar(this.infoPanelToolbar, true);
                }, this);
                break;
+           // TODO find a generic hooking mechanism
+            case 'devices':
+                Tine.ActiveSync.syncdevices.show();
+                break;
             }
         }, this);
 
@@ -291,13 +313,6 @@ Tine.Admin = function () {
 
         treePanel.on('contextmenu', function (node, event) {
             event.stopEvent();
-            //node.select();
-            //node.getOwnerTree().fireEvent('click', _node);
-            /* switch(node.attributes.contextMenuClass) {
-                case 'ctxMenuContactsTree':
-                    ctxMenuContactsTree.showAt(event.getXY());
-                    break;
-            } */
         });
 
         return treePanel;
index 3825945..ef00355 100644 (file)
@@ -143,7 +143,7 @@ class Calendar_Backend_Sql extends Tinebase_Backend_Sql_Abstract
      * Calendar optimized search function
      * 
      * 1. get all events neglecting grants filter
-     * 2. get all related container grants (via resolveing)
+     * 2. get all related container grants (via resolving)
      * 3. compute effective grants in PHP and only keep events 
      *    user has required grant for
      * 
@@ -191,8 +191,8 @@ class Calendar_Backend_Sql extends Tinebase_Backend_Sql_Abstract
             $_filter->removeFilter('grants');
         }
         
-        // clonde the filter, as the filter is also used in the json frontend
-        // and the calendarfilter is used in the UI to
+        // clone the filter, as the filter is also used in the json frontend
+        // and the calendar filter is used in the UI to
         $clonedFilters = clone $_filter;
         
         $calendarFilter = null;
@@ -209,14 +209,6 @@ class Calendar_Backend_Sql extends Tinebase_Backend_Sql_Abstract
         $select->group($this->_tableName . '.' . 'id');
         Tinebase_Backend_Sql_Abstract::traitGroup($select);
         
-        $period = $_filter->getFilter('period');
-
-        // filter out unnecessary YEARLY candidates for web client requests
-        // periods < 40 days should be client web client requests.
-        if ($period && $period->getFrom()->getClone()->addDay(40) > $period->getUntil()) {
-            $this->_removeNonMatchingBaseEvents($select, $period);
-        }
-        
         if ($calendarFilter) {
             $select1 = clone $select;
             $select2 = clone $select;
@@ -244,53 +236,7 @@ class Calendar_Backend_Sql extends Tinebase_Backend_Sql_Abstract
         
         return $_onlyIds ? $result->{is_bool($_onlyIds) ? $this->_getRecordIdentifier() : $_onlyIds} : $result;
     }
-    
-    /**
-     * removes non-matching rrule base events
-     * 
-     * @param Zend_Db_Select $select
-     * @param Calendar_Model_PeriodFilter $period
-     * 
-     * TODO improve this by moving the rrules to a separate table to simplify SQL statment
-     */
-    protected function _removeNonMatchingBaseEvents($select, $period)
-    {
-        $gs = new Tinebase_Backend_Sql_Filter_GroupSelect($select);
-        $from = $period->getFrom()->getClone()->subDay(1);
-        $fromMonth = $from->format('n');
-        $fromDay = $from->format('j');
-        $until = $period->getUntil()->getClone()->addDay(1);
-        $untilMonth = $until->format('n');
-        $untilDay = $until->format('j');
-        $quotedRrule = $this->_db->quoteIdentifier('rrule');
-        
-        $gs->where($quotedRrule . ' NOT LIKE ?', 'FREQ=YEARLY%')
-        ->orWhere($quotedRrule . ' IS NULL');
-        
-        // TODO improve view detection to handle year boundaries, too
-        if ($fromMonth > $untilMonth) {
-            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
-                    . ' Skipping performance optimization because we can\'t handle year boundaries yet');
-            return;
-        }
-        if ($fromMonth == $untilMonth && $untilDay-$fromDay <= 10) {
-            // day|week view
-            for ($day=$fromDay; $day<=$untilDay; $day++) {
-                $gs->orWhere($quotedRrule . ' LIKE ?', "FREQ=YEARLY;INTERVAL=1;BYMONTH={$fromMonth};BYMONTHDAY={$day}%");
-            }
-        } else {
-            // monthview
-            for ($month=$fromMonth; $month<=$untilMonth; $month++) {
-                $gs->orWhere($quotedRrule . ' LIKE ?', "FREQ=YEARLY;INTERVAL=1;BYMONTH={$month};%");
-            }
-        }
-        
-        $gs->appendWhere(Zend_Db_Select::SQL_AND);
-        
-        if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
-            . ' ' . $select);
-    }
-    
+
     /**
      * calculate event permissions and remove events that don't match
      * 
index 71f032f..daebc35 100644 (file)
@@ -42,6 +42,13 @@ class Calendar_Config extends Tinebase_Config_Abstract
      * @var string
      */
     const CROP_DAYS_VIEW = 'daysviewcroptime';
+
+    /**
+     * Days view mouse wheel increment
+     *
+     * @var integer
+     */
+    const DAYS_VIEW_MOUSE_WHEEL_INCREMENT = 'daysviewwheelincrement';
     
     /**
      * Allow events outside the definition created by the edit dialog
@@ -91,7 +98,12 @@ class Calendar_Config extends Tinebase_Config_Abstract
      * @var string
      */
     const SKIP_DOUBLE_EVENTS = 'skipdoubleevents';
-    
+
+    /**
+     * Send attendee mails to users with edit permissions to the added resource
+     */
+    const RESOURCE_MAIL_FOR_EDITORS = 'resourcemailforeditors';
+
     /**
      * (non-PHPdoc)
      * @see tine20/Tinebase/Config/Definition::$_properties
@@ -114,6 +126,8 @@ class Calendar_Config extends Tinebase_Config_Abstract
             'description'           => 'Crop calendar view configured start and endtime.',
             'type'                  => Tinebase_Config_Abstract::TYPE_BOOL,
             'clientRegistryInclude' => true,
+            'setByAdminModule'      => false,
+            'setBySetupModule'      => false,
             'default'               => false
         
         ),
@@ -124,9 +138,24 @@ class Calendar_Config extends Tinebase_Config_Abstract
             'description'           => 'Allow events outside start and endtime.',
             'type'                  => Tinebase_Config_Abstract::TYPE_BOOL,
             'clientRegistryInclude' => true,
+            'setByAdminModule'      => false,
+            'setBySetupModule'      => false,
             'default'               => false
         
         ),
+        self::DAYS_VIEW_MOUSE_WHEEL_INCREMENT => array(
+                                    //_('Week View Mouse Wheel Increment')
+            'label'                 => 'Week View Mouse Wheel Increment',
+            //_('Crop calendar view configured start and endtime.')
+            'description'           => 'Number of pixels to scroll per mouse wheel',
+            'type'                  => Tinebase_Config_Abstract::TYPE_INT,
+            'clientRegistryInclude' => true,
+            'setByAdminModule'      => false,
+            'setBySetupModule'      => false,
+            'default'               => 50
+
+        ),
+
         self::ATTENDEE_STATUS => array(
                                    //_('Attendee Status Available')
             'label'                 => 'Attendee Status Available',
@@ -213,6 +242,16 @@ class Calendar_Config extends Tinebase_Config_Abstract
                 'setBySetupModule'      => FALSE,
                 'default'               => '',
         ),
+        self::RESOURCE_MAIL_FOR_EDITORS => array(
+            //_('Send notifications to every user with edit permissions of the added resources')
+            'label'                 => 'Send notifications to every user with edit permissions of the added resources',
+            'description'           => 'Send notifications to every user with edit permissions of the added resources',
+            'type'                  => Tinebase_Config_Abstract::TYPE_BOOL,
+            'clientRegistryInclude' => false,
+            'setBySetupModule'      => false,
+            'setByAdminModule'      => false,
+            'default'               => false
+        )
     );
     
     /**
index 1abb260..0370711 100644 (file)
@@ -227,7 +227,7 @@ class Calendar_Controller extends Tinebase_Controller_Event implements Tinebase_
      * @param Calendar_Model_Event       $_oldEvent
      * @return void
      */
-    public function sendEventNotifications($_event, $_updater, $_action, $_oldEvent=NULL)
+    public function sendEventNotifications($_event, $_updater, $_action, $_oldEvent = NULL)
     {
         Calendar_Controller_EventNotifications::getInstance()->doSendNotifications($_event, $_updater, $_action, $_oldEvent);
     }
index c61cd44..d72fe68 100644 (file)
@@ -234,7 +234,7 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
         }
         
         // send notifications
-        if ($this->_sendNotifications && $_record['mute'] != 1) {
+        if ($this->_sendNotifications && $_record->mute != 1) {
             $this->doSendNotifications($createdEvent, Tinebase_Core::getUser(), 'created');
         }        
 
@@ -555,7 +555,7 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
         
         // send notifications
         $this->sendNotifications($sendNotifications);
-        if ($this->_sendNotifications) {
+        if ($this->_sendNotifications && $_record->mute != 1) {
             $this->doSendNotifications($updatedEvent, Tinebase_Core::getUser(), 'changed', $event);
         }
         return $updatedEvent;
@@ -614,6 +614,7 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
                 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
                     . ' ' . print_r($nextRegularRecurEvent->toArray(), TRUE));
                 
+                $nextRegularRecurEvent->mute = $exdate->mute;
                 $newBaseEvent = $this->createRecurException($nextRegularRecurEvent, FALSE, TRUE);
                 // @todo this should be done by createRecurException
                 $exdatesOfNewBaseEvent = $this->getRecurExceptions($newBaseEvent);
@@ -764,6 +765,17 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
                     // NOTE delete needs to update sequence otherwise iTIP based protocolls ignore the delete
                     $record->status = Calendar_Model_Event::STATUS_CANCELED;
                     $this->_touch($record);
+                    if ($record->isRecurException()) {
+                        try {
+                            $baseEvent = $this->getRecurBaseEvent($record);
+                            $this->_touch($baseEvent);
+                        } catch (Tinebase_Exception_NotFound $tnfe) {
+                            // base Event might be gone already
+                            if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
+                                . " BaseEvent of exdate {$record->uid} to delete not found ");
+
+                        }
+                    }
                     parent::delete($record);
                 }
                 
@@ -796,7 +808,10 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
     
     /**
      * delete range of events starting with given recur exception
-     * 
+     *
+     * NOTE: if exdate is persistent, it will not be deleted by this function
+     *       but by the original call of delete
+     *
      * @param Calendar_Model_Event $exdate
      * @param string $range
      */
@@ -1140,7 +1155,7 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
         $this->doContainerACLChecks($doContainerACLChecks);
         
         // send notifications
-        if ($this->_sendNotifications) {
+        if ($this->_sendNotifications && $_event->mute != 1) {
             // NOTE: recur exception is a fake event from client. 
             //       this might lead to problems, so we wrap the calls
             try {
@@ -1885,7 +1900,7 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
         }
         
         // send notifications
-        if ($currentAttender->status != $updatedAttender->status && $this->_sendNotifications) {
+        if ($currentAttender->status != $updatedAttender->status && $this->_sendNotifications && $_event->mute != 1) {
             $updatedEvent = $this->get($event->getId());
             $this->doSendNotifications($updatedEvent, Tinebase_Core::getUser(), 'changed', $event);
         }
@@ -2233,9 +2248,10 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
      * @param Tinebase_Model_FullAccount $_updater
      * @param Sting                      $_action
      * @param Calendar_Model_Event       $_oldEvent
+     * @param Array                      $_additionalRecipients
      * @return void
      */
-    public function doSendNotifications($_event, $_updater, $_action, $_oldEvent=NULL)
+    public function doSendNotifications($_event, $_updater, $_action, $_oldEvent = NULL)
     {
         Tinebase_ActionQueue::getInstance()->queueAction('Calendar.sendEventNotifications', 
             $_event, 
index 15fb5f8..ddd6804 100644 (file)
             $this->sendNotificationToAttender($organizer, $_event, $_updater, 'changed', self::NOTIFICATION_LEVEL_ATTENDEE_STATUS_UPDATE, $updates);
         }
     }
-    
+
     /**
      * send notification to a single attender
      * 
      * @param array                      $_updates
      * @return void
      */
-    public function sendNotificationToAttender($_attender, $_event, $_updater, $_action, $_notificationLevel, $_updates = NULL)
+    public function sendNotificationToAttender(Calendar_Model_Attender $_attender, $_event, $_updater, $_action, $_notificationLevel, $_updates = NULL)
     {
         try {
             $organizer = $_event->resolveOrganizer();
             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " receiver: '{$_attender->getEmail()}'");
             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " subject: '$messageSubject'");
             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " body: $messageBody");
-            
+
             $sender = $_action == 'alarm' ? $prefUser : $_updater;
-        
-            Tinebase_Notification::getInstance()->send($sender, array($attendee), $messageSubject, $messageBody, $calendarPart, $attachments);
+
+            $recipients = array($attendee);
+            if ($_attender->user_type == Calendar_Model_Attender::USERTYPE_RESOURCE
+                && Calendar_Config::getInstance()->get(Calendar_Config::RESOURCE_MAIL_FOR_EDITORS)
+            ) {
+                $recipients = array_merge($recipients,
+                    Calendar_Controller_Resource::getInstance()->getNotificationRecipients(
+                        Calendar_Controller_Resource::getInstance()->get($_attender->user_id)
+                    )
+                );
+            }
+            Tinebase_Notification::getInstance()->send($sender, $recipients, $messageSubject, $messageBody, $calendarPart, $attachments);
         } catch (Exception $e) {
             Tinebase_Exception::log($e);
             return;
index 888bd24..0b8e489 100644 (file)
@@ -539,7 +539,7 @@ class Calendar_Controller_MSEventFacade implements Tinebase_Controller_Record_In
                     if ($exceptionId) {
                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
                             . ' Found exdate to be deleted (id: ' . $exceptionId . ')');
-                        $ids[] = $exceptionId;
+                        array_unshift($ids, $exceptionId);
                     }
                 }
             }
@@ -941,14 +941,7 @@ class Calendar_Controller_MSEventFacade implements Tinebase_Controller_Record_In
         
         // get ids for toUpdate
         $idxIdMap = $this->_filterEventsByDTStarts($_currentPersistentExceptions, $toUpdateDtSTart)->getId();
-        try {
-            $migration['toUpdate']->setByIndices('id', $idxIdMap);
-        } catch (Tinebase_Exception_Record_NotDefined $ternd) {
-            // some debugging for 0008182: event with lots of exceptions breaks calendar sync
-            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($idxIdMap, TRUE));
-            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($migration['toUpdate']->toArray(), TRUE));
-            throw $ternd;
-        }
+        $migration['toUpdate']->setByIndices('id', $idxIdMap, /* $skipMissing = */ true);
         
         // filter exceptions marked as don't touch 
         foreach ($migration['toUpdate'] as $toUpdate) {
index 7d91d4d..b3937f6 100644 (file)
@@ -93,7 +93,7 @@ class Calendar_Controller_Resource extends Tinebase_Controller_Record_Abstract
         )), NULL, TRUE);
         
         if ($_record->grants instanceof Tinebase_Record_RecordSet) {
-            $grants = Tinebase_Container::getInstance()->setGrants($container->getId(), $_record->grants, TRUE, FALSE);
+            Tinebase_Container::getInstance()->setGrants($container->getId(), $_record->grants, TRUE, FALSE);
         }
         
         $_record->container_id = $container->getId();
@@ -165,4 +165,45 @@ class Calendar_Controller_Resource extends Tinebase_Controller_Record_Abstract
         }
         return parent::_deleteLinkedObjects($_record);
     }
+
+    /**
+     * returns recipients for a resource notification
+     *
+     *  users who are allowed to edit a resource, should receive a notification
+     *
+     * @param  Calendar_Model_Resource $_lead
+     * @return array          array of int|Addressbook_Model_Contact
+     */
+    public function getNotificationRecipients(Calendar_Model_Resource $resource)
+    {
+        $recipients = array();
+
+        $relations = Tinebase_Relations::getInstance()->getRelations('Calendar_Model_Resource', 'Sql', $resource->getId(), true);
+
+        foreach ($relations as $relation) {
+            if ($relation->related_model == 'Addressbook_Model_Contact' && $relation->type == 'RESPONSIBLE') {
+                $recipients[] = $relation->related_record;
+            }
+        }
+
+        if (empty($recipients)) {
+            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__CLASS__ . '::' . __METHOD__ . '::' . __LINE__ . ' no responsibles found for calendar resource: ' .
+                $resource->getId() . ' sending notification to all people having edit access to container ' . $resource->container_id);
+
+            $containerGrants = Tinebase_Container::getInstance()->getGrantsOfContainer($resource->container_id, TRUE);
+
+            foreach ($containerGrants as $grant) {
+                if ($grant['account_type'] == Tinebase_Acl_Rights::ACCOUNT_TYPE_USER && $grant[Tinebase_Model_Grants::GRANT_EDIT] == 1) {
+                    try {
+                        $recipients[] = Addressbook_Controller_Contact::getInstance()->getContactByUserId($grant['account_id'], TRUE);
+                    } catch (Addressbook_Exception_NotFound $aenf) {
+                        if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__CLASS__ . '::' . __METHOD__ . '::' . __LINE__
+                            . ' Do not send notification to non-existant user: ' . $aenf->getMessage());
+                    }
+                }
+            }
+        }
+
+        return $recipients;
+    }
 }
index 892ffe4..732fbf6 100644 (file)
@@ -183,13 +183,13 @@ class Calendar_Convert_Event_VCalendar_Abstract extends Tinebase_Convert_VCalend
 
             if ($organizerContact instanceof Addressbook_Model_Contact && !empty($organizerContact->email)) {
                 $organizer = $vevent->add(
-                    'ORGANIZER', 
-                    'mailto:' . $organizerContact->email, 
+                    'ORGANIZER',
+                    'mailto:' . $organizerContact->email,
                     array('CN' => $organizerContact->n_fileas, 'EMAIL' => $organizerContact->email)
                 );
             }
         }
-        
+
         $this->_addEventAttendee($vevent, $event);
         
         $optionalProperties = array(
@@ -210,9 +210,9 @@ class Calendar_Convert_Event_VCalendar_Abstract extends Tinebase_Convert_VCalend
             }
         }
 
-        $vevent->add('X-CALENDARSERVER-ACCESS',
-            $event->class == Calendar_Model_Event::CLASS_PUBLIC ? 'PUBLIC' : 'CONFIDENTIAL'
-        );
+        $class = $event->class == Calendar_Model_Event::CLASS_PUBLIC ? 'PUBLIC' : 'CONFIDENTIAL';
+        $vcalendar->add('X-CALENDARSERVER-ACCESS', $class);
+        $vevent->add('X-CALENDARSERVER-ACCESS', $class);
 
         // categories
         if (!isset($event->tags)) {
@@ -678,7 +678,7 @@ class Calendar_Convert_Event_VCalendar_Abstract extends Tinebase_Convert_VCalend
      */
     protected function _convertVevent(\Sabre\VObject\Component\VEvent $vevent, Calendar_Model_Event $event, $options)
     {
-        if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) 
+        if (Tinebase_Core::isLogLevel(Zend_Log::TRACE))
             Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' vevent ' . $vevent->serialize());
         
         $newAttendees = array();
@@ -1062,6 +1062,10 @@ class Calendar_Convert_Event_VCalendar_Abstract extends Tinebase_Convert_VCalend
         $this->_manageAttachmentsFromClient($event, $attachments);
         
         if (empty($event->dtend)) {
+            if (empty($event->dtstart)) {
+                throw new Tinebase_Exception_UnexpectedValue("Got event without dtstart and dtend");
+            }
+            
             // TODO find out duration (see TRIGGER DURATION)
 //             if (isset($vevent->DURATION)) {
 //             }
index 2de9fe2..a6be4c1 100644 (file)
  * @package     Calendar
  * @subpackage  Convert
  */
-class Calendar_Convert_Event_VCalendar_Iphone extends Calendar_Convert_Event_VCalendar_Abstract
+class Calendar_Convert_Event_VCalendar_Iphone extends Calendar_Convert_Event_VCalendar_MacOSX
 {
     // DAVKit/4.0 (728.4); iCalendar/1 (42.1); iPhone/3.1.3 7E18
     // DAVKit/5.0 (767); iCalendar/5.0 (79); iPhone/4.2.1 8C148
     // iOS/5.0.1 (9A405) dataaccessd/1.0
+    // iOS/7.1.2 (11D257) dataaccessd/1.0
+    // iOS/8.2 (12D508) dataaccessd/1.0
     const HEADER_MATCH = '/(iPhone|iOS)\/(?P<version>\S+)/';
-    
-    protected $_supportedFields = array(
-        'seq',
-        'dtend',
-        'transp',
-        'class',
-        'description',
-        #'geo',
-        'location',
-        'priority',
-        'summary',
-        'url',
-        'alarms',
-        #'tags',
-        'dtstart',
-        'exdate',
-        'rrule',
-        'recurid',
-        'is_all_day_event',
-        #'rrule_until',
-        'originator_tz'
-    );
 }
index f7379b3..88daafe 100644 (file)
@@ -79,12 +79,10 @@ class Calendar_Convert_Event_VCalendar_MacOSX extends Calendar_Convert_Event_VCa
     {
         $return = parent::_findMainEvent($vcalendar);
 
-        if (version_compare($this->_version, '10.8', '<')) {
-            // NOTE 10.7 sometimes writes access into calendar property
-            if (isset($vcalendar->{'X-CALENDARSERVER-ACCESS'})) {
-                foreach ($vcalendar->VEVENT as $vevent) {
-                    $vevent->{'X-CALENDARSERVER-ACCESS'} = $vcalendar->{'X-CALENDARSERVER-ACCESS'};
-                }
+        // NOTE 10.7 and 10.10 sometimes write access into calendar property
+        if (isset($vcalendar->{'X-CALENDARSERVER-ACCESS'})) {
+            foreach ($vcalendar->VEVENT as $vevent) {
+                $vevent->{'X-CALENDARSERVER-ACCESS'} = $vcalendar->{'X-CALENDARSERVER-ACCESS'};
             }
         }
 
@@ -102,13 +100,11 @@ class Calendar_Convert_Event_VCalendar_MacOSX extends Calendar_Convert_Event_VCa
     {
         $return = parent::_convertVevent($vevent, $event, $options);
 
-        if (version_compare($this->_version, '10.8', '<')) {
-            // NOTE: 10.7 sometimes uses (internal?) int's
-            if (isset($vevent->{'X-CALENDARSERVER-ACCESS'}) && (int) (string) $vevent->{'X-CALENDARSERVER-ACCESS'} > 0) {
-                $event->class = (int) (string) $vevent->{'X-CALENDARSERVER-ACCESS'} == 1 ?
-                    Calendar_Model_Event::CLASS_PUBLIC :
-                    Calendar_Model_Event::CLASS_PRIVATE;
-            }
+        // NOTE: 10.7 sometimes uses (internal?) int's
+        if (isset($vevent->{'X-CALENDARSERVER-ACCESS'}) && (int) (string) $vevent->{'X-CALENDARSERVER-ACCESS'} > 0) {
+            $event->class = (int) (string) $vevent->{'X-CALENDARSERVER-ACCESS'} == 1 ?
+                Calendar_Model_Event::CLASS_PUBLIC :
+                Calendar_Model_Event::CLASS_PRIVATE;
         }
 
         return $return;
index 8badcda..26bc770 100644 (file)
@@ -117,17 +117,12 @@ class Calendar_Frontend_Json extends Tinebase_Frontend_Json_Abstract
             $defaultCalendarArray = array();
         }
         
-        $definitionConverter = new Tinebase_Convert_ImportExportDefinition_Json();
         $importDefinitions = $this->_getImportDefinitions();
-        $defaultDefinition = $this->_getDefaultImportDefinition($importDefinitions);
         
         $registryData = array(
             'defaultContainer'          => $defaultCalendarArray,
-            'defaultImportDefinition'   => ($defaultDefinition) ? $definitionConverter->fromTine20Model($defaultDefinition) : array(),
-            'importDefinitions'         => array(
-                'results'               => $definitionConverter->fromTine20RecordSet($importDefinitions),
-                'totalcount'            => count($importDefinitions),
-            ),
+            'defaultImportDefinition'   => $importDefinitions['default'],
+            'importDefinitions'         => $importDefinitions
         );
         
         return $registryData;
@@ -212,7 +207,7 @@ class Calendar_Frontend_Json extends Tinebase_Frontend_Json_Abstract
     /**
      * get addressbook import definitions
      * 
-     * @return Tinebase_Record_RecordSet
+     * @return array
      * 
      * @todo generalize this
      */
@@ -223,9 +218,28 @@ class Calendar_Frontend_Json extends Tinebase_Frontend_Json_Abstract
             array('field' => 'type',            'operator' => 'equals', 'value' => 'import'),
         ));
         
-        $importDefinitions = Tinebase_ImportExportDefinition::getInstance()->search($filter);
+        $definitionConverter = new Tinebase_Convert_ImportExportDefinition_Json();
+        
+        try {
+            $importDefinitions = Tinebase_ImportExportDefinition::getInstance()->search($filter);
+            $defaultDefinition = $this->_getDefaultImportDefinition($importDefinitions);
+            $result = array(
+                'results'               => $definitionConverter->fromTine20RecordSet($importDefinitions),
+                'totalcount'            => count($importDefinitions),
+                'default'               => ($defaultDefinition) ? $definitionConverter->fromTine20Model($defaultDefinition) : array(),
+            );
+        } catch (Exception $e) {
+            Tinebase_Exception::log($zce);
+            $result = array(
+                array(
+                    'results'               => array(),
+                    'totalcount'            => 0,
+                    'default'               => array(),
+                )
+            );
+        }
         
-        return $importDefinitions;
+        return $result;
     }
     
     /**
index 7b70053..0f1a5d0 100644 (file)
@@ -445,6 +445,10 @@ class Calendar_Frontend_WebDAV_Event extends Sabre\DAV\File implements Sabre\Cal
             Calendar_Convert_Event_VCalendar_Abstract::OPTION_USE_SERVER_MODLOG => true,
         ));
 
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
+            Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " " . print_r($event->toArray(), true));
+
+
         $currentEvent = $this->getRecord();
         $currentContainer = Tinebase_Container::getInstance()->getContainerById($currentEvent->container_id);
 
index 44dd6ae..8c91cb5 100644 (file)
@@ -82,23 +82,23 @@ class Calendar_Import_CalDAV extends Tinebase_Import_Abstract
     public function import($_resource = NULL, $_clientRecordData = array())
     {
         $_resource['options'] = Zend_Json::decode($_resource['options']);
-
+        
         $credentials = new Tinebase_Model_CredentialCache(array(
             'id'    => $_resource['options']['cid'],
             'key'   => $_resource['options']['ckey']
         ));
         Tinebase_Auth_CredentialCache::getInstance()->getCachedCredentials($credentials);
-
+        
         $uri = $this->_splitUri($_resource['remoteUrl']);
-
+        
         $caldavClientOptions = array(
             'baseUri' => $uri['host'],
             'calenderUri' => $uri['path'],
             'userName' => $credentials->username,
             'password' => $credentials->password,
-            'allowDuplicateEvents' => $_resource['options']['allowDuplicateEvents'],
+            'allowDuplicateEvents' => isset($_resource['options']['allowDuplicateEvents']) ? $_resource['options']['allowDuplicateEvents'] : false,
         );
-
+        
         Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
             . ' Trying to get calendar container Name:' . $_resource['options']['container_id']);
         $container = Tinebase_Container::getInstance()->getContainerById($this->_getImportCalendarByName($_resource['options']['container_id']));
@@ -107,14 +107,17 @@ class Calendar_Import_CalDAV extends Tinebase_Import_Abstract
             throw new Tinebase_Exception('Could not import, aborting ..');
             return false;
         }
-
+        
         $credentialCache = Tinebase_Auth_CredentialCache::getInstance()->cacheCredentials($credentials->username, $credentials->password);
         Tinebase_Core::set(Tinebase_Core::USERCREDENTIALCACHE, $credentialCache);
-
+        
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
+            Tinebase_Core::getLogger()->debug(__METHOD__ . ' ' . __LINE__ . ' Trigger CalDAV client with URI ' . $_resource['remoteUrl']);
+        }
+        
         $this->_calDAVClient = new Calendar_Import_CalDav_Client($caldavClientOptions, 'Generic', $container->name);
         $this->_calDAVClient->setVerifyPeer(false);
         $this->_calDAVClient->getDecorator()->initCalendarImport();
-
         $this->_calDAVClient->updateAllCalendarData();
     }
     
index c882685..5150391 100644 (file)
@@ -170,7 +170,7 @@ class Calendar_Model_Attender extends Tinebase_Record_Abstract
                 break;
             case self::USERTYPE_GROUP:
             case self::USERTYPE_RESOURCE:
-                return $resolvedUser->name;
+                return $resolvedUser->name ?: $resolvedUser->n_fileas;
                 break;
             default:
                 throw new Exception("type $type not yet supported");
index 4382ab8..030ef9e 100644 (file)
@@ -6,7 +6,7 @@
  * @subpackage  Model
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Cornelius Weiss <c.weiss@metaways.de>
- * @copyright   Copyright (c) 2009 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2009-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  *
  */
 
@@ -151,7 +151,7 @@ class Calendar_Model_AttenderFilter extends Tinebase_Model_Filter_Abstract
                     /* on     */ $adapter->quoteIdentifier($dname . '.cal_event_id') . ' = ' . $adapter->quoteIdentifier($_backend->getTableName() . '.id') .
                                  ' AND ' .  $gs->getSQL(),
                     /* select */ array($dname => $_backend->getDbCommand()->getAggregate($dname . '.id')));
-            $_select->having($adapter->quoteIdentifier($dname) . ' IS NULL');
+            $_select->having($_backend->getDbCommand()->getAggregate($dname . '.id') . ' IS NULL');
         } else {
             $gs->appendWhere(Zend_Db_Select::SQL_OR);
         }
index 4b5f5a8..8b10d17 100644 (file)
@@ -25,14 +25,14 @@ class Calendar_Model_EventFilter extends Tinebase_Model_Filter_FilterGroup
      * @var string name of model this filter group is designed for
      */
     protected $_modelName = 'Calendar_Model_Event';
-    
+
     /**
      * @var string class name of this filter group
      *      this is needed to overcome the static late binding
      *      limitation in php < 5.3
      */
     protected $_className = 'Calendar_Model_EventFilter';
-    
+
     /**
      * @var array filter model fieldName => definition
      */
@@ -54,7 +54,7 @@ class Calendar_Model_EventFilter extends Tinebase_Model_Filter_FilterGroup
             'applicationName' => 'Calendar',
         )),
         'grants'                => array('filter' => 'Calendar_Model_GrantFilter'),
-        // NOTE using dtstart and dtend filters may not lead to the desired result. 
+        // NOTE using dtstart and dtend filters may not lead to the desired result.
         //      you need to use the period filter to filter for events in a given period
         'dtstart'               => array('filter' => 'Tinebase_Model_Filter_DateTime'),
         'dtend'                 => array('filter' => 'Tinebase_Model_Filter_DateTime'),
@@ -62,16 +62,15 @@ class Calendar_Model_EventFilter extends Tinebase_Model_Filter_FilterGroup
         'rrule'                 => array('filter' => 'Tinebase_Model_Filter_Text'),
         'recurid'               => array('filter' => 'Tinebase_Model_Filter_Text'),
         'rrule_until'           => array('filter' => 'Tinebase_Model_Filter_DateTime'),
-        'last_modified_time'    => array('filter' => 'Tinebase_Model_Filter_DateTime'),
         'summary'               => array('filter' => 'Tinebase_Model_Filter_Text'),
         'location'              => array('filter' => 'Tinebase_Model_Filter_Text'),
         'description'           => array('filter' => 'Tinebase_Model_Filter_Text'),
-        'last_modified_time'    => array('filter' => 'Tinebase_Model_Filter_Date'),
         'is_deleted'            => array('filter' => 'Tinebase_Model_Filter_Bool'),
         'deleted_by'            => array('filter' => 'Tinebase_Model_Filter_User'),
         'deleted_time'          => array('filter' => 'Tinebase_Model_Filter_DateTime'),
         'creation_time'         => array('filter' => 'Tinebase_Model_Filter_Date'),
         'last_modified_by'      => array('filter' => 'Tinebase_Model_Filter_User'),
+        'last_modified_time'    => array('filter' => 'Tinebase_Model_Filter_DateTime'),
         'created_by'            => array('filter' => 'Tinebase_Model_Filter_User'),
     );
 }
index 13e0d00..dc5e961 100644 (file)
@@ -25,6 +25,11 @@ class Calendar_Preference extends Tinebase_Preference_Abstract
      * where daysvoew should be scrolled maximum up to
      */
     const DAYSVIEW_ENDTIME = 'daysviewendtime';
+    
+    /**
+     * where daysview should be scrolled to
+     */
+    const DAYSVIEW_DEFAULT_STARTTIME = 'daysviewdefaultstarttime';
 
     /**
      * default calendar all newly created/invited events are placed in
@@ -74,6 +79,8 @@ class Calendar_Preference extends Tinebase_Preference_Abstract
      */
     public function getAllApplicationPreferences()
     {
+        $cropDays = Calendar_Config::getInstance()->get(Calendar_Config::CROP_DAYS_VIEW);
+        
         $allPrefs = array(
             self::DAYSVIEW_STARTTIME,
             self::DAYSVIEW_ENDTIME,
@@ -85,6 +92,10 @@ class Calendar_Preference extends Tinebase_Preference_Abstract
             self::DEFAULTALARM_MINUTESBEFORE,
             self::DEFAULTATTENDEE_STRATEGY
         );
+        
+        if ($cropDays) {
+            array_unshift($allPrefs, self::DAYSVIEW_DEFAULT_STARTTIME);
+        }
             
         return $allPrefs;
     }
@@ -107,6 +118,10 @@ class Calendar_Preference extends Tinebase_Preference_Abstract
                 'label'         => $translate->_('End Time'),
                 'description'   => $translate->_('Position on the left time axis, day and week view should end with'),
             ),
+            self::DAYSVIEW_DEFAULT_STARTTIME => array(
+                'label'         => $translate->_('Default Start Time'),
+                'description'   => $translate->_('Scroll position on the left time axis, day and week view should start with'),
+            ),
             self::DEFAULTCALENDAR  => array(
                 'label'         => $translate->_('Default Calendar'),
                 'description'   => $translate->_('The default calendar for invitations and new events'),
@@ -199,6 +214,10 @@ class Calendar_Preference extends Tinebase_Preference_Abstract
                 $preference->value      = '18:00';
                 $preference->options = $this->_createTimespanDataXML(1, 24);
                 break;
+            case self::DAYSVIEW_DEFAULT_STARTTIME:
+                $preference->value      = '08:00';
+                $preference->options = $this->_createTimespanDataXML(0, 23);
+                break;
             case self::DEFAULTCALENDAR:
                 $this->_getDefaultContainerPreferenceDefaults($preference, $_accountId);
                 break;
index 40b5f1d..a7364ed 100644 (file)
@@ -48,7 +48,7 @@ div.page
 }
 
 .cal-print-daysview-day-dayOfMonth {
-    font-size: 20px;
+    font-size: 15px;
     font-weight: bold;
 }
 
index 60c2dc1..e916de2 100644 (file)
@@ -189,6 +189,7 @@ Tine.Calendar.AttendeeGridPanel = Ext.extend(Ext.grid.EditorGridPanel, {
             editor: new Ext.form.ComboBox({
                 blurOnSelect  : true,
                 expandOnFocus : true,
+                listWidth     : 100,
                 mode          : 'local',
                 store         : [
                     ['user',     this.app.i18n._('User')   ],
index 5045341..59c2c93 100644 (file)
@@ -86,6 +86,11 @@ Ext.extend(Tine.Calendar.DaysView, Ext.Container, {
      */
     numOfDays: 4,
     /**
+     * @cfg {String} (H:i) defaultStart
+     * generic scroll start of the (work) day
+     */
+    defaultStart: '08:00',
+    /**
      * @cfg {String} (H:i) dayStart
      * generic start of the (work) day
      */
@@ -101,6 +106,11 @@ Ext.extend(Tine.Calendar.DaysView, Ext.Container, {
      */
     cropDayTime: false,
     /**
+     *  @cfg {Integer} wheelIncrement
+     *  the number of pixels to increment on mouse wheel scrolling (defaults to 50)
+     */
+    wheelIncrement: 50,
+    /**
      * @cfg {String} newEventSummary
      * _('New Event')
      */
@@ -217,19 +227,33 @@ Ext.extend(Tine.Calendar.DaysView, Ext.Container, {
         if (! this.selModel) {
             this.selModel = this.selModel || new Tine.Calendar.EventSelectionModel();
         }
-        
-        this.onLayout = Function.createBuffered(this.onLayout, 100, this);
+
+        this.onLayout = Function.createBuffered(this.unbufferedOnLayout, 100, this);
 
         // apply preferences
         var prefs = this.app.getRegistry().get('preferences'),
+            defaultStartTime = Date.parseDate(prefs.get('daysviewdefaultstarttime'), 'H:i'),
             startTime = Date.parseDate(prefs.get('daysviewstarttime'), 'H:i'),
             endTime = Date.parseDate(prefs.get('daysviewendtime'), 'H:i');
         
         this.dayStart = Ext.isDate(startTime) ? startTime : Date.parseDate(this.dayStart, 'H:i');
         this.dayEnd = Ext.isDate(endTime) ? endTime : Date.parseDate(this.dayEnd, 'H:i');
+        // 00:00 in users timezone is a spechial case where the user expects
+        // something like 24:00 and not 00:00
+        if (this.dayEnd.format('H:i') == '00:00') {
+            this.dayEnd = this.dayEnd.add(Date.MINUTE, -1);
+        }
         this.dayEndPx = this.getTimeOffset(this.dayEnd);
         
         this.cropDayTime = !! Tine.Tinebase.configManager.get('daysviewcroptime', 'Calendar') && !(!this.getTimeOffset(this.dayStart) && !this.getTimeOffset(this.dayEnd));
+
+        if (this.cropDayTime) {
+            this.defaultStart = Ext.isDate(defaultStartTime) ? defaultStartTime : Date.parseDate(this.defaultStart, 'H:i');
+        } else {
+            this.defaultStart = this.dayStart;
+        }
+
+        this.wheelIncrement = Tine.Tinebase.configManager.get('daysviewwheelincrement', 'Calendar') || this.wheelIncrement;
         
         Tine.Calendar.DaysView.superclass.initComponent.apply(this, arguments);
     },
@@ -306,34 +330,37 @@ Ext.extend(Tine.Calendar.DaysView, Ext.Container, {
             },
             
             notifyOut : function() {
-                //console.log('notifyOut');
                 //delete this.grid;
             },
             
             notifyDrop : function(dd, e, data) {
-                var v = data.scope;
-                
-                var targetDate = v.getTargetDateTime(e);
+                var v = data.scope,
+                    targetDate = v.getTargetDateTime(e);
                 
                 if (targetDate) {
-                    var event = data.event;
-                    
-                    var originalDuration = (event.get('dtend').getTime() - event.get('dtstart').getTime()) / Date.msMINUTE;
-                    
+                    var event = data.event,
+                        originalDuration = (event.get('dtend').getTime() - event.get('dtstart').getTime()) / Date.msMINUTE;
+
                     // Get the new endDate to ensure it's not out of croptimes
-                    var copyTargetDate = targetDate;
-                    var newEnd = copyTargetDate.add(Date.MINUTE, originalDuration);
-                    
-                    v.dayEnd.setDate(newEnd.getDate());
-                       
+                    var outOfCropBounds = false;
+                    if (v.cropDayTime == true) {
+                        var newEnd = new Date(),
+                            dayEndCompare = new Date();
+
+                        newEnd.setTime(targetDate.getTime());
+                        newEnd.add(Date.MINUTE, originalDuration);
+                        dayEndCompare.setTime(targetDate.getTime());
+                        dayEndCompare.setHours(v.dayEnd.getHours(), v.dayEnd.getMinutes());
+                        outOfCropBounds = (newEnd > dayEndCompare);
+                    }
+
                     // deny drop for missing edit grant or no time change
                     if (! event.get('editGrant') || Math.abs(targetDate.getTime() - event.get('dtstart').getTime()) < Date.msMINUTE
-                            || ((v.cropDayTime == true) && (newEnd > v.dayEnd))) {
+                            || outOfCropBounds) {
                         return false;
                     }
                     
                     event.beginEdit();
-                    
                     event.set('dtstart', targetDate);
                     
                     if (! event.get('is_all_day_event') && targetDate.is_all_day_event && event.duration < Date.msDAY) {
@@ -474,30 +501,50 @@ Ext.extend(Tine.Calendar.DaysView, Ext.Container, {
                 cropHeightPx = this.getTimeOffset(this.dayEnd) +2;
                 
             this.mainBody.setStyle('margin-top', '-' + cropStartPx + 'px');
-            if (this.getTimeOffset(this.dayEnd)) {
-                this.mainBody.setStyle('height', cropHeightPx + 'px');
-                this.mainBody.setStyle('overflow', 'hidden');
-            }
+            this.mainBody.setStyle('height', cropHeightPx + 'px');
+            this.mainBody.setStyle('overflow', 'hidden');
             this.scroller.addClass('cal-daysviewpanel-body-cropDayTime');
         }
-        
+
+        this.unbufferedOnLayout();
+
         // scrollTo initial position
         this.isScrolling = true;
+
         try {
-            this.scrollTo(this.dayStart);
+            this.scrollTo.defer(500, this, [this.defaultStart]);
         } catch (e) {
             this.scrollTo();
         }
-        
-        this.onLayout();
+
         this.rendered = true;
     },
     
     scrollTo: function(time) {
         time = Ext.isDate(time) ? time : new Date();
-        this.scroller.dom.scrollTop = this.getTimeOffset(time);
+        
+        var scrollTop = this.getTimeOffset(time);
+        if (this.cropDayTime) {
+            scrollTop = scrollTop - this.getTimeOffset(this.dayStart);
+        }
+
+        this.scroller.dom.scrollTop = scrollTop;
     },
-    
+
+    onMouseWheel: function(e) {
+        var d = e.getWheelDelta()*this.wheelIncrement*-1;
+        e.stopEvent();
+
+        var scrollTop = this.scroller.dom.scrollTop,
+            newTop = scrollTop + d,
+            sh = this.scroller.dom.scrollHeight-this.scroller.dom.clientHeight;
+
+        var s = Math.max(0, Math.min(sh, newTop));
+        if(s != scrollTop){
+            this.scroller.scrollTo('top', s);
+        }
+    },
+
     onBeforeScroll: function() {
         if (! this.isScrolling) {
             this.isScrolling = true;
@@ -524,7 +571,6 @@ Ext.extend(Tine.Calendar.DaysView, Ext.Container, {
             vStartMinutes = this.getHeightMinutes(visibleStart),
             vEndMinutes   = this.getHeightMinutes(visibleEnd);
             
-        
         Ext.each(this.dayCols, function(dayCol, idx) {
             var dayColEl    = Ext.get(dayCol),
                 dayStart    = this.startDate.add(Date.DAY, idx),
@@ -553,7 +599,7 @@ Ext.extend(Tine.Calendar.DaysView, Ext.Container, {
     
     onShow: function() {
         this.onLayout();
-        this.scroller.dom.scrollTop = this.lastScrollPos || this.getTimeOffset(new Date());
+        this.scroller.dom.scrollTop = this.lastScrollPos || this.getTimeOffset(this.defaultStart);
     },
     
     onBeforeHide: function() {
@@ -795,7 +841,8 @@ Ext.extend(Tine.Calendar.DaysView, Ext.Container, {
     
     redrawWholeDayEvents: function() {
         this.store.each(function(event) {
-            if (event.ui && event.get('is_all_day_event')) {
+            // check if event is currently visible by looking into ui.domIds
+            if (event.ui && event.ui.domIds.length > 0 && event.get('is_all_day_event')) {
                 this.removeEvent(event);
                 this.insertEvent(event);
             }
@@ -1241,9 +1288,12 @@ Ext.extend(Tine.Calendar.DaysView, Ext.Container, {
         
         this.scroller = new E(this.mainWrap.dom.childNodes[1]);
         this.scroller.setStyle('overflow-x', 'hidden');
+
+
+        this.mon(this.scroller, 'mousewheel', this.onMouseWheel, this);
         this.mon(this.scroller, 'scroll', this.onBeforeScroll, this);
         this.mon(this.scroller, 'scroll', this.onScroll, this, {buffer: 200});
-        
+
         this.mainBody = new E(this.scroller.dom.firstChild);
         this.dayCols = this.mainBody.dom.firstChild.lastChild.childNodes;
 
@@ -1283,11 +1333,17 @@ Ext.extend(Tine.Calendar.DaysView, Ext.Container, {
             Ext.fly(this.wholeDayArea.childNodes[freeIdxs[i]]).remove();
         }
     },
-    
+
+    /**
+     * buffered version of this.unbufferedOnLayout
+     * @see this.initComponent
+     */
+    onLayout: Ext.emptyFn,
+
     /**
      * layouts the view
      */
-    onLayout: function() {
+    unbufferedOnLayout: function() {
         Tine.Calendar.DaysView.superclass.onLayout.apply(this, arguments);
         if(!this.mainBody){
             return; // not rendered
index c638793..810ad8c 100644 (file)
@@ -166,7 +166,7 @@ Tine.Calendar.EventEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
                                     containerName: this.app.i18n.n_hidden(this.recordClass.getMeta('containerName'), this.recordClass.getMeta('containersName'), 1),
                                     containersName: this.app.i18n._hidden(this.recordClass.getMeta('containersName')),
                                     appName: this.app.appName,
-                                    requiredGrant: this.record.data.id ? ['editGrant'] : ['addGrant'],
+                                    requiredGrant: 'readGrant',
                                     disabled: true
                                 }), Ext.apply(this.perspectiveCombo.getAttendeeContainerField(), {
                                     columnWidth: 1
@@ -345,7 +345,9 @@ Tine.Calendar.EventEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
         
         this.on('render', function() {this.getForm().add(organizerCombo);}, this);
         
-        this.rrulePanel = new Tine.Calendar.RrulePanel({});
+        this.rrulePanel = new Tine.Calendar.RrulePanel({
+            eventEditDialog : this
+        });
         this.alarmPanel = new Tine.widgets.dialog.AlarmPanel({});
         this.attendeeStore = this.attendeeGridPanel.getStore();
         
@@ -471,7 +473,7 @@ Tine.Calendar.EventEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
         
         // Calendar is the only app with record based grants -> user gets edit grant for all fields when copying
         this.record.set('editGrant', true);
-        
+
         Tine.log.debug('Tine.Calendar.EventEditDialog::doCopyRecord() -> record:');
         Tine.log.debug(this.record);
     },
@@ -525,27 +527,22 @@ Tine.Calendar.EventEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
     },
     
     validateDtEnd: function() {
-        var dtStart = this.getForm().findField('dtstart').getValue();
-        
-        var dtEndField = this.getForm().findField('dtend');
-        var dtEnd = dtEndField.getValue();
+        var dtStart = this.getForm().findField('dtstart').getValue(),
+            dtEndField = this.getForm().findField('dtend'),
+            dtEnd = dtEndField.getValue(),
+            endTime = this.adjustTimeToUserPreference(dtEndField.getValue(), 'daysviewendtime');
         
-        var prefs = this.app.getRegistry().get('preferences'),
-            endTime = Date.parseDate(prefs.get('daysviewendtime'), 'H:i');
-      
-        // Update to the selected day
-        endTime.setDate(dtEnd.getDate());
-        endTime.setMonth(dtEnd.getMonth());
-        endTime.setYear(dtEnd.getYear() + 1900);
-
         if (! Ext.isDate(dtEnd)) {
             dtEndField.markInvalid(this.app.i18n._('End date is not valid'));
             return false;
         } else if (Ext.isDate(dtStart) && dtEnd.getTime() - dtStart.getTime() <= 0) {
             dtEndField.markInvalid(this.app.i18n._('End date must be after start date'));
             return false;
-        } else if  (! Tine.Tinebase.configManager.get('daysviewallowallevents', 'Calendar') && this.getForm().findField('is_all_day_event').checked === false && !! Tine.Tinebase.configManager.get('daysviewcroptime', 'Calendar') && dtEnd > endTime) {
-            dtEndField.markInvalid(this.app.i18n._('End date is not allowed to be be higher than the configured time range.'));
+        } else if (! Tine.Tinebase.configManager.get('daysviewallowallevents', 'Calendar')
+                && this.getForm().findField('is_all_day_event').checked === false
+                && !! Tine.Tinebase.configManager.get('daysviewcroptime', 'Calendar') && dtEnd > endTime)
+        {
+            dtEndField.markInvalid(this.app.i18n._('End time is not allowed to be after the configured time.'));
             return false;
         } else {
             dtEndField.clearInvalid();
@@ -553,23 +550,42 @@ Tine.Calendar.EventEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
         }
     },
     
+    /**
+     * adjusts given date (end/start) to user preference (hours)
+     * 
+     * @param {Date} dateValue
+     * @param {String} prefKey
+     * @return {Date}
+     */
+    adjustTimeToUserPreference: function(dateValue, prefKey) {
+        var userPreferenceDate = dateValue;
+            prefs = this.app.getRegistry().get('preferences'),
+            hour = prefs.get(prefKey).split(':')[0];
+        
+        // adjust date to user preference
+        userPreferenceDate.setHours(hour);
+        userPreferenceDate.setMinutes(0);
+        if (prefKey == 'daysviewendtime' && userPreferenceDate.format('H:i') == '00:00') {
+            userPreferenceDate = userPreferenceDate.add(Date.MINUTE, -1);
+        }
+        
+        return userPreferenceDate;
+    },
+    
     validateDtStart: function() {
-        var dtStartField = this.getForm().findField('dtstart');
-        var dtStart = dtStartField.getValue();
+        var dtStartField = this.getForm().findField('dtstart'),
+            dtStart = dtStartField.getValue(),
+            startTime = this.adjustTimeToUserPreference(dtStartField.getValue(), 'daysviewstarttime');
         
-        var prefs = this.app.getRegistry().get('preferences'),
-            startTime = Date.parseDate(prefs.get('daysviewstarttime'), 'H:i');
-      
-        // Update to the selected day
-        startTime.setDate(dtStart.getDate());
-        startTime.setMonth(dtStart.getMonth());
-        startTime.setYear(dtStart.getYear() + 1900);
-
         if (! Ext.isDate(dtStart)) {
             dtStartField.markInvalid(this.app.i18n._('Start date is not valid'));
             return false;
-        } else if  (! Tine.Tinebase.configManager.get('daysviewallowallevents', 'Calendar') && this.getForm().findField('is_all_day_event').checked === false && !! Tine.Tinebase.configManager.get('daysviewcroptime', 'Calendar') && dtStart < startTime) {
-            dtStartField.markInvalid(this.app.i18n._('End date is not allowed to be be lower than the configured time range.'));
+        } else if (! Tine.Tinebase.configManager.get('daysviewallowallevents', 'Calendar')
+                && this.getForm().findField('is_all_day_event').checked === false
+                && !! Tine.Tinebase.configManager.get('daysviewcroptime', 'Calendar')
+                && dtStart < startTime)
+        {
+            dtStartField.markInvalid(this.app.i18n._('Start date is not allowed to be before the configured time.'));
             return false;
         } else {
             dtStartField.clearInvalid();
index 827862e..bf2999f 100644 (file)
@@ -48,7 +48,7 @@ Tine.Calendar.EventUI.prototype = {
      */
     getEls: function() {
         var domEls = [];
-        for (var i=0; i<this.domIds.length; i++) {
+        for (var i=0; i < this.domIds.length; i++) {
             var el = Ext.get(this.domIds[i]);
             if (el) {
                 domEls.push(el);
@@ -309,9 +309,13 @@ Tine.Calendar.DaysViewEventUI = Ext.extend(Tine.Calendar.EventUI, {
         
         var offsetWidth = Ext.fly(view.wholeDayArea).getWidth();
         
-        var width = Math.round(offsetWidth * (this.dtEnd.getTime() - this.dtStart.getTime()) / (view.numOfDays * Date.msDAY)) -5;
-        var left = Math.round(offsetWidth * (this.dtStart.getTime() - view.startDate.getTime()) / (view.numOfDays * Date.msDAY));
-        
+        //var width = Math.round(offsetWidth * (this.dtEnd.getTime() - this.dtStart.getTime()) / (view.numOfDays * Date.msDAY)) -5;
+        //var left = Math.round(offsetWidth * (this.dtStart.getTime() - view.startDate.getTime()) / (view.numOfDays * Date.msDAY));
+
+        var width = Math.floor(1000 * (this.dtEnd.getTime() - this.dtStart.getTime()) / (view.numOfDays * Date.msDAY) -5) /10;
+        var left = 100 * (this.dtStart.getTime() - view.startDate.getTime()) / (view.numOfDays * Date.msDAY);
+
+
         if (left < 0) {
             width = width + left;
             left = 0;
@@ -336,9 +340,9 @@ Tine.Calendar.DaysViewEventUI = Ext.extend(Tine.Calendar.EventUI, {
             bgColor: this.colorSet.light,
             textColor: this.colorSet.text,
             zIndex: 100,
-            width: width  +'px',
+            width: width  +'%',
             height: '15px',
-            left: left + 'px',
+            left: left + '%',
             top: pos * 18 + 'px',//'1px'
             statusIcons: this.statusIcons
         }, true);
index 82b113b..4e87774 100644 (file)
@@ -153,10 +153,17 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
             handler: this.onCutEvent.createDelegate(this),
             iconCls: 'action_cut'
         });
+
+        this.action_copy_to = new Ext.Action({
+            requiredGrant: 'deleteGrant',
+            text: this.app.i18n._('Copy Event to clipboard'),
+            handler: this.onCopyToEvent.createDelegate(this),
+            iconCls: 'action_editcopy'
+        });
         
         this.action_cancelPasting = new Ext.Action({
             requiredGrant: 'deleteGrant',
-            text: this.app.i18n._('Stop cut & paste'),
+            text: this.app.i18n._('Stop cut / copy & paste'),
             handler: this.onCutCancelEvent.createDelegate(this),
             iconCls: 'action_cut_break'
         });
@@ -565,7 +572,7 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
      */
     onContextMenu: function (e) {
         e.stopEvent();
-        
+
         var view = this.getCalendarPanel(this.activeView).getView();
         var event = view.getTargetEvent(e);
         var datetime = view.getTargetDateTime(e);
@@ -577,6 +584,7 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
         }
         
         var addAction, responseAction, copyAction;
+
         if (datetime || event) {
             var dtStart = datetime || event.get('dtstart').clone();
             if (dtStart.format('H:i') === '00:00') {
@@ -594,7 +602,6 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
                 responseAction = this.getResponseAction(event);
                 copyAction = this.getCopyAction(event);
             }
-        
         } else {
             addAction = this.action_addInNewWindow;
         }
@@ -604,15 +611,16 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
         } else {
             view.getSelectionModel().clearSelections();
         }
-        
+
         var menuitems = this.recordActions.concat(addAction, responseAction || [], copyAction || []);
         
         if (event) {
-            menuitems = menuitems.concat(['-', this.action_cut, '-']);
+            this.action_copy_to.setDisabled(event.isRecurInstance() || event.isRecurException() || event.isRecurBase());
+            menuitems = menuitems.concat(['-', this.action_cut, this.action_copy_to, '-']);
         } else if (Tine.Tinebase.data.Clipboard.has('Calendar', 'Event')) {
             menuitems = menuitems.concat(['-', this.getPasteAction(datetime, Tine.Tinebase.data.Clipboard.pull('Calendar', 'Event', true)), this.action_cancelPasting, '-']);
         }
-        
+
         var ctxMenu = new Ext.menu.Menu({
             plugins: [{
                 ptype: 'ux.itemregistry',
@@ -676,14 +684,14 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
      */
     getCopyAction: function(event) {
         var copyAction = {
-            text: String.format(_('Copy {0}'), this.i18nRecordName),
+            text: String.format(this.app.i18n._('Copy {0}'), this.i18nRecordName),
             handler: this.onEditInNewWindow.createDelegate(this, ["copy", event]),
             iconCls: 'action_editcopy',
             // TODO allow to copy recurring events / exceptions
             disabled: event.isRecurInstance() || event.isRecurException() || event.isRecurBase()
         };
         
-        return copyAction;
+        return copyAction
     },
     
     checkPastEvent: function(event, checkBusyConflicts, actionType) {
@@ -1107,6 +1115,24 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
         }
         Tine.Tinebase.data.Clipboard.push(event);
     },
+
+    /**
+     * Is called on copy to clipboard
+     *
+     * @param action
+     * @param event
+     */
+    onCopyToEvent: function(action, event) {
+        var panel = this.getCalendarPanel(this.activeView);
+        var selection = panel.getSelectionModel().getSelectedEvents();
+        if (Ext.isArray(selection) && selection.length === 1) {
+            event = selection[0];
+        }
+
+        event.isCopy = true;
+
+        Tine.Tinebase.data.Clipboard.push(event);
+    },
     
     /**
      * is called on cancelling cut & paste
@@ -1117,7 +1143,7 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
         
         for (var index = 0; index < ids.length; index++) {
             var record = store.getAt(store.findExact('id', ids[index]));
-            if (record.ui) {
+            if (record && record.ui) {
                 record.ui.clearDirty();
             }
         }
@@ -1132,7 +1158,8 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
      */
     onPasteEvent: function(datetime) {
         var record = Tine.Tinebase.data.Clipboard.pull('Calendar', 'Event');
-        
+        var isCopy = record.isCopy;
+
         if (! record) {
             return;
         }
@@ -1140,18 +1167,40 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
         var dtend   = record.get('dtend');
         var dtstart = record.get('dtstart');
         var eventLength = dtend - dtstart;
-        
-        // remove before update
-        var store = this.getStore();
-        var oldRecord = store.getAt(store.findExact('id', record.getId()));
-        if (oldRecord && oldRecord.hasOwnProperty('ui')) {
-            oldRecord.ui.remove();
+
+        if (isCopy != true) {
+            // remove before update
+            var store = this.getStore();
+            var oldRecord = store.getAt(store.findExact('id', record.getId()));
+            if (oldRecord && oldRecord.hasOwnProperty('ui')) {
+                oldRecord.ui.remove();
+            }
+        } else {
+            record = Tine.Calendar.EventEditDialog.superclass.doCopyRecordToReturn.call(this, record);
+
+            record.set('editGrant', true);
+            record.set('id', '');
+
+            // remove attender ids
+            Ext.each(record.data.attendee, function(attender) {
+                delete attender.id;
+            }, this);
         }
-        
+
         record.set('dtstart', datetime);
         record.set('dtend', new Date(datetime.getTime() + eventLength));
-        
-        this.onUpdateEvent(record);
+
+        if (isCopy == true) {
+            record.isCopy = true;
+            Tine.Tinebase.data.Clipboard.push(record);
+            if (record.ui) {
+                record.ui.clearDirty();
+            }
+
+            this.onAddEvent(record);
+        } else {
+            this.onUpdateEvent(record);
+        }
     },
     
     /**
@@ -1192,7 +1241,7 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
         }
         
         Tine.log.debug('Tine.Calendar.MainScreenCenterPanel::onEditInNewWindow() - Opening event edit dialog with action: ' + action);
-        
+
         Tine.Calendar.EventEditDialog.openWindow({
             plugins: plugins ? Ext.encode(plugins) : null,
             record: Ext.encode(event.data),
index b30c634..8c39eab 100644 (file)
@@ -538,6 +538,12 @@ Tine.Calendar.Model.Attender = Tine.Tinebase.data.Record.create([
                 } else if (userData.hasOwnProperty('account_id')) {
                     // userData contains contact
                     return userData.id;
+                } else if (userData.group_id) {
+                    // userData contains list
+                    return userData.id;
+                } else if (userData.list_id) {
+                    // userData contains group
+                    return userData.list_id;
                 }
                 break;
             default:
index 867e0e4..0e2fa39 100644 (file)
@@ -31,17 +31,32 @@ Tine.Calendar.Printer.DaysViewRenderer = Ext.extend(Tine.Calendar.Printer.BaseRe
         
         return head;
     },
-    
+
+    onBeforePrint: function(document, view) {
+        //if (this.printMode == 'sheet') {
+        //    // FF has scale to page option but scrambles everything after the first page
+        //    // @TODO downscale to fit one page
+        //
+        //    // Chrome does not have a scale to page option
+        //    // A4 Landscape is about 1000px in my chrome ;-)
+        //    var zoom = 1000 / vw;
+        //    document.body.style.zoom = zoom;
+        //    document.body.style.MozTransform = 'scale(' + zoom + ')';
+        //    document.body.style.MozTransformOrigin = 'top left';
+        //}
+    },
+
     generateSheetHTML: function(view) {
         var node = view.el.dom.cloneNode(true),
             header = node.getElementsByClassName('cal-daysviewpanel-wholedayheader-scroller')[0],
-            scroller = node.getElementsByClassName('cal-daysviewpanel-scroller')[0];
+            scroller = node.getElementsByClassName('cal-daysviewpanel-scroller')[0],
+            fullHeight = view.dayEndPx - view.getTimeOffset(view.dayStart) + 20;
         
         // resize header/scroller to fullsize
         header.style.height = [header.firstChild.style.height, header.style.height].sort().pop();
-        scroller.style.height = view.dayEndPx - view.getTimeOffset(view.dayStart) + 20 + 'px';
+        scroller.style.height =  fullHeight + 'px';
         scroller.style.width = null;
-        
+
         return this.generateTitle(view) + node.innerHTML;
     },
     
index a779588..5fa44d5 100644 (file)
@@ -3,7 +3,7 @@
  * 
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Cornelius Weiss <c.weiss@metaways.de>
- * @copyright   Copyright (c) 2007-2008 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2007-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  */
 
 Ext.ns('Tine.Calendar');
@@ -19,6 +19,12 @@ Tine.Calendar.RrulePanel = Ext.extend(Ext.Panel, {
      */    
     activeRuleCard: null,
     
+    /**
+     * the event edit dialog (parent)
+     * @type Tine.Calendar.EventEditDialog
+     */
+    eventEditDialog: null,
+    
     layout: 'form',
     frame: true,
     
@@ -68,7 +74,6 @@ Tine.Calendar.RrulePanel = Ext.extend(Ext.Panel, {
             id: this.idPrefix + 'tglbtn' + 'NONE',
             xtype: 'tbbtnlockedtoggle',
             enableToggle: true,
-            //pressed: true,
             text: this.app.i18n._('None'),
             handler: this.onFreqChange.createDelegate(this, ['NONE']),
             toggleGroup: this.idPrefix + 'freqtglgroup'
@@ -374,12 +379,33 @@ Tine.Calendar.RrulePanel.AbstractCard = Ext.extend(Ext.Panel, {
     },
     
     isValid: function(record) {
-        var until = this.until.getValue();
+        var until = this.until.getValue(),
+            freq = this.freq;
+        
         if (Ext.isDate(until) && Ext.isDate(record.get('dtstart'))) {
             if (until.getTime() < record.get('dtstart').getTime()) {
                 this.until.markInvalid(this.app.i18n._('Until has to be after event start'));
                 return false;
             }
+        } 
+        
+        if (Ext.isDate(record.get('dtend')) && Ext.isDate(record.get('dtstart'))) {
+            var dayDifference = (record.get('dtend').getTime() - record.get('dtstart').getTime()) / 1000 / 60 / 60 / 24,
+                dtendField = this.rrulePanel.eventEditDialog.getForm().findField('dtend');
+            
+            if(freq == 'DAILY' && dayDifference >= 1) {
+                dtendField.markInvalid(this.app.i18n._('The event is longer than the recurring interval'));
+                return false;
+            } else if(freq == 'WEEKLY' && dayDifference >= 7) {
+                dtendField.markInvalid(this.app.i18n._('The event is longer than the recurring interval'));
+                return false;
+            } else if(freq == 'MONTHLY' && dayDifference >= 28) {
+                dtendField.markInvalid(this.app.i18n._('The event is longer than the recurring interval'));
+                return false;
+            } else if(freq == 'YEARLY' && dayDifference >= 365) {
+                dtendField.markInvalid(this.app.i18n._('The event is longer than the recurring interval'));
+                return false;
+            }
         }
         
         return true;
index 69ef1e4..7463db5 100644 (file)
@@ -1344,3 +1344,7 @@ msgstr "\"%2$s\" Antwort von %1$s"
 #: views/eventNotification.php:68
 msgid "Event details"
 msgstr "Termin Details"
+
+#: js/EventEditDialog.js:315
+msgid "Mute Alert"
+msgstr "Benachrichtigungen unterdrücken"
index 2ce6a2e..ccdceb1 100644 (file)
@@ -400,6 +400,9 @@ class Courses_Controller_Course extends Tinebase_Controller_Record_Abstract
             
             if ($this->_config->get(Courses_Config::STUDENT_LOGINNAME_PREFIX, FALSE) && ($position = strrpos($user->accountLoginName, '-')) !== false) {
                 $user->accountLoginName = $courseName . '-' . substr($user->accountLoginName, $position + 1);
+                
+                //short User name
+                $user->accountLoginName = $user->shortenUsername();
             }
             
             $user->accountPrimaryGroup  = $course->group_id;
index d5182ab..fdf5632 100644 (file)
@@ -6,7 +6,7 @@
  * @subpackage  Backend
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Philipp Schüle <p.schuele@metaways.de>
- * @copyright   Copyright (c) 2007-2011 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2007-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * 
  */
 
@@ -104,4 +104,37 @@ class Crm_Backend_Lead extends Tinebase_Backend_Sql_Abstract
         
         return $select;
     }
+    
+    /**
+     * (non-PHPdoc)
+     * @see Tinebase_Backend_Sql_Abstract::_appendForeignSort()
+     * 
+     * @todo generalize this: find a place (in model config?) for foreign record sorting information
+     * @todo maybe we can use a temp table with joins here
+     * @todo allow to to use it with keyfields, too (and/or switch those settings to keyfield configs)
+     */
+    protected function _appendForeignSort(Tinebase_Model_Pagination $pagination, Zend_Db_Select $select)
+    {
+        $virtualSortColumns = array(
+            'leadstate_id'  => Crm_Model_Config::LEADSTATES,
+            'leadsource_id' => Crm_Model_Config::LEADSOURCES,
+            'leadtype_id'   => Crm_Model_Config::LEADTYPES,
+        );
+        
+        $col = $pagination->sort;
+        if (isset($virtualSortColumns[$col])) {
+            $settings = Crm_Controller::getInstance()->getConfigSettings();
+            $setting = $settings->{$virtualSortColumns[$col]};
+            
+            // create cases (when => then) for sql switch (CASE) command
+            $cases = array();
+            foreach ($setting as $settingRecord) {
+                $cases[$settingRecord['id']] = $settingRecord[str_replace('_id', '', $col)];
+            }
+            
+            $foreignSortCase = $this->_dbCommand->getSwitch($col, $cases);
+            $select->columns(array('foreignSortCol' => $foreignSortCase));
+            $pagination->sort = 'foreignSortCol';
+        }
+    }
 }
index 83bcdaa..5b1b5c2 100644 (file)
@@ -138,7 +138,7 @@ class Crm_Controller extends Tinebase_Controller_Event implements Tinebase_Conta
      * @return  Crm_Model_Config
      * 
      * @todo check 'endslead' values
-     * @todo generalize this / adopt Tinebase_Controller_Abstract::getConfigSettings()
+     * @todo use keyfield configs here
      */
     public function getConfigSettings($_resolve = FALSE)
     {
index 2c58065..06277de 100644 (file)
@@ -8,7 +8,7 @@
  * @subpackage  Controller
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Philipp Schüle <p.schuele@metaways.de>
- * @copyright   Copyright (c) 2007-2011 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2007-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  *
  */
 
@@ -209,10 +209,12 @@ class Crm_Controller_Lead extends Tinebase_Controller_Record_Abstract
         
         if ($_action == 'changed') {
             $subject = sprintf($translate->_('Lead %s has been changed'), $_lead->lead_name);
+        } else if ($_action == 'deleted') {
+            $subject = sprintf($translate->_('Lead %s has been deleted'), $_lead->lead_name);
         } else {
             $subject = sprintf($translate->_('Lead %s has been created'), $_lead->lead_name);
         }
-
+        
         // create pdf
         try {
             $pdfGenerator = new Crm_Export_Pdf();
@@ -250,9 +252,10 @@ class Crm_Controller_Lead extends Tinebase_Controller_Record_Abstract
     {
         $recipients = array();
         
-        $relations = Tinebase_Relations::getInstance()->getRelations('Crm_Model_Lead', 'Sql', $_lead->getId(), true);
-        
-        foreach ($relations as $relation) {
+        if (! $_lead->relations instanceof Tinebase_Record_RecordSet) {
+            $_lead->relations = Tinebase_Relations::getInstance()->getRelations('Crm_Model_Lead', 'Sql', $_lead->getId(), true);
+        }
+        foreach ($_lead->relations as $relation) {
             if ($relation->related_model == 'Addressbook_Model_Contact' && $relation->type == 'RESPONSIBLE') {
                 $recipients[] = $relation->related_record;
             }
index df31532..25bea75 100644 (file)
@@ -5,7 +5,7 @@
  * @subpackage  Frontend
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Philipp Schüle <p.schuele@metaways.de>
- * @copyright   Copyright (c) 2007-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2007-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  *
  */
 
@@ -24,7 +24,7 @@ class Crm_Frontend_Json extends Tinebase_Frontend_Json_Abstract
      * @var Crm_Controller_Lead
      */
     protected $_controller = NULL;
-        
+    
     protected $_relatableModels = array('Crm_Model_Lead');
     
     /**
@@ -56,7 +56,7 @@ class Crm_Frontend_Json extends Tinebase_Frontend_Json_Abstract
         $result['totalleadtypes'] = $result['totalcount']['leadtypes'];
         
         $result['totalcount'] = $result['totalcount']['totalcount'];
-                
+        
         return $result;
     }
     
index cba770b..270dd03 100644 (file)
@@ -101,7 +101,7 @@ class Crm_Setup_DemoData extends Tinebase_Setup_DemoData_Abstract
         $currentUser = Tinebase_Core::getUser();
         
         $this->_getDays();
-        $this->_sharedTaskContainer = $this->_createSharedContainer('Tasks aus Leads', array('application_id' => Tinebase_Application::getInstance()->getApplicationByName('tasks')->getId()), false);
+        $this->_sharedTaskContainer = $this->_createSharedContainer('Tasks aus Leads', array('application_id' => Tinebase_Application::getInstance()->getApplicationByName('Tasks')->getId()), false);
         
         $fe = new Tinebase_Frontend_Json();
         $fe->savePreferences(array(
index 431e055..4ba8a04 100644 (file)
@@ -5,7 +5,7 @@
  * @package     Crm
  * @subpackage  Setup
  * @license     http://www.gnu.org/licenses/agpl.html AGPL3
- * @copyright   Copyright (c) 2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Alexander Stintzing <a.stintzing@metaways.de>
  */
 class Crm_Setup_Update_Release8 extends Setup_Update_Abstract
@@ -28,4 +28,19 @@ class Crm_Setup_Update_Release8 extends Setup_Update_Abstract
         
         $this->setApplicationVersion('Crm', '8.1');
     }
+
+    /**
+     * update to 8.2
+     * 
+     * convert relation type 'responsible' to uppercase
+     * 
+     * @see 0010822: contact relation is not saved correctly when new lead is created
+     */
+    public function update_1()
+    {
+        $where = array('type = ?' => 'responsible');
+        $this->_db->update(SQL_TABLE_PREFIX . 'relations', array('type' => 'RESPONSIBLE'), $where);
+        
+        $this->setApplicationVersion('Crm', '8.2');
+    }
 }
index 08a95a0..3c5342b 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <application>
     <name>Crm</name>
-    <version>8.1</version>
+    <version>8.2</version>
     <order>20</order>
     <depends>
         <application>Admin</application>
index 2057108..53a2c93 100644 (file)
@@ -52,7 +52,7 @@ Tine.Crm.Contact.Combo = Ext.extend(Tine.Addressbook.SearchCombo, {
             
         this.collapse();
         this.clearValue();
-    }    
+    }
 });
 
 /**
index cfc3cfb..55e60ee 100644 (file)
@@ -186,7 +186,7 @@ Tine.Crm.LeadEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
             delete relations[i]['related_record']['relation'];
             newLinkObject.relation = relations[i];
             newLinkObject.relation_type = relations[i]['type'].toLowerCase();
-    
+            
             if ((newLinkObject.relation_type === 'responsible' 
               || newLinkObject.relation_type === 'customer' 
               || newLinkObject.relation_type === 'partner')) {
@@ -200,7 +200,7 @@ Tine.Crm.LeadEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
                 products.push(newLinkObject);
             }
         }
-
+        
         return {
             contacts: contacts,
             tasks: tasks,
index f740fc6..90f6324 100644 (file)
@@ -59,6 +59,15 @@ Tine.Crm.LeadGridDetailsPanel = Ext.extend(Tine.widgets.grid.DetailsPanel, {
         }, {
             label: this.app.i18n._('Web'),
             dataField: 'url'
+        }, {
+            label: this.app.i18n._('Street'),
+            dataField: 'adr_one_street'
+        }, {
+            label: this.app.i18n._('Postalcode'),
+            dataField: 'adr_one_postalcode'
+        }, {
+            label: this.app.i18n._('City'),
+            dataField: 'adr_one_locality'
         }];
         var labelMarkup = '<label class="x-form-item x-form-item-label">';
         
@@ -113,7 +122,7 @@ Tine.Crm.LeadGridDetailsPanel = Ext.extend(Tine.widgets.grid.DetailsPanel, {
         if (record) {
             return record.data[definitionsLabel];
         } else {
-            return 'undefined';
+            return this.app.i18n._('undefined');
         }
     },
     
index f76aaba..207a71d 100644 (file)
@@ -142,8 +142,8 @@ Tine.Crm.LeadGridPanel = Ext.extend(Tine.widgets.grid.GridPanel, {
                 {header: this.app.i18n._('Responsible'), id: 'lead_responsible', dataIndex: 'relations', width: 175, sortable: false, hidden: true, renderer: this.responsibleRenderer},
                 {header: this.app.i18n._('Partner'), id: 'lead_partner', dataIndex: 'relations', width: 175, sortable: false, renderer: this.partnerRenderer},
                 {header: this.app.i18n._('Customer'), id: 'lead_customer', dataIndex: 'relations', width: 175, sortable: false, renderer: this.customerRenderer},
-                {header: this.app.i18n._('Leadstate'), id: 'leadstate_id', dataIndex: 'leadstate_id', sortable: false, width: 100, renderer: Tine.Crm.LeadState.Renderer},
-                {header: this.app.i18n._('Leadsource'), id: 'leadsource_id', dataIndex: 'leadsource_id', sortable: false, width: 100, renderer: Tine.Crm.LeadSource.Renderer},
+                {header: this.app.i18n._('Leadstate'), id: 'leadstate_id', dataIndex: 'leadstate_id', width: 100, renderer: Tine.Crm.LeadState.Renderer},
+                {header: this.app.i18n._('Leadsource'), id: 'leadsource_id', dataIndex: 'leadsource_id', width: 100, renderer: Tine.Crm.LeadSource.Renderer},
                 {header: this.app.i18n._('Probability'), id: 'probability', dataIndex: 'probability', width: 50, renderer: Ext.ux.PercentRenderer },
                 {header: this.app.i18n._('Turnover'), id: 'turnover', dataIndex: 'turnover', width: 100, renderer: Ext.util.Format.euMoney },
 
index 7293d21..a7ac0fe 100644 (file)
@@ -61,7 +61,7 @@ Tine.Crm.LeadSource.Renderer = function(_leadsourceId) {
     if (record) {
         return record.data.leadsource;
     } else {
-        return 'undefined';
+        return Tine.Tinebase.appMgr.get('Crm').i18n._('undefined');
     }
 };
 
index 4805933..1b9993c 100644 (file)
@@ -108,7 +108,7 @@ Tine.Crm.LeadState.Renderer = function(_leadstateId) {
     if (record) {
        return record.data.leadstate;
     } else {
-        return 'undefined';
+        return Tine.Tinebase.appMgr.get('Crm').i18n._('undefined');
     }
 };
 
index ec147f0..c203266 100644 (file)
@@ -85,7 +85,7 @@ Tine.Crm.Model.Lead.getDefaultData = function() {
         probability: 0,
         turnover: 0,
         relations: [{
-            type: 'responsible',
+            type: 'RESPONSIBLE',
             related_record: Tine.Tinebase.registry.get('userContact')
         }]
     };
index 1c05d78..61914da 100644 (file)
@@ -1,4 +1,4 @@
-# 
+#
 # Translators:
 # Alexander Stintzing <a.stintzing@metaways.de>, 2012
 # Michael Spahn <michael@spahn.me>, 2014
@@ -12,157 +12,80 @@ msgstr ""
 "PO-Revision-Date: 2014-09-10 18:54+0000\n"
 "Last-Translator: Michael Spahn <michael@spahn.me>\n"
 "Language-Team: German (http://www.transifex.com/projects/p/tine20/language/de/)\n"
+"Language: de\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Language: de\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 "X-Poedit-Country: GB\n"
 "X-Poedit-Language: en\n"
 "X-Poedit-SourceCharset: utf-8\n"
 
-#: Export/Pdf.php:75
-msgid "Lead Data"
-msgstr "Lead-Daten"
+#: Controller/Lead.php:193 js/LeadGridPanel.js:144 js/Contact.js:161
+#: js/Contact.js:356 js/Contact.js:396 js/LeadGridDetailsPanel.js:45
+#: Model/Lead.php:123 Controller.php:162
+msgid "Customer"
+msgstr "Kunde"
 
-#: Export/Pdf.php:78 js/AdminPanel.js:157 js/LeadGridPanel.js:145
-#: js/LeadStateFilterModel.js:32 js/LeadEditDialog.js:343
-msgid "Leadstate"
+#: Controller/Lead.php:194
+msgid "State"
 msgstr "Status"
 
-#: Export/Pdf.php:81 js/AdminPanel.js:171 js/LeadEditDialog.js:362
-#: js/LeadGridDetailsPanel.js:454
-msgid "Leadtype"
-msgstr "Typ"
+#: Controller/Lead.php:195 js/Contact.js:252 js/AddToLeadPanel.js:78
+msgid "Role"
+msgstr "Rolle"
 
-#: Export/Pdf.php:84 js/AdminPanel.js:164 js/LeadGridPanel.js:146
-#: js/LeadEditDialog.js:369 js/LeadGridDetailsPanel.js:459
-#: js/LeadSourceFilterModel.js:28
-msgid "Leadsource"
+#: Controller/Lead.php:196
+msgid "Source"
 msgstr "Quelle"
 
-#: Export/Pdf.php:87 js/LeadGridPanel.js:148 js/Model.js:112
-#: Controller/Lead.php:200
-msgid "Turnover"
-msgstr "Umsatz"
-
-#: Export/Pdf.php:90 js/LeadGridPanel.js:147 js/Model.js:110
-#: js/LeadEditDialog.js:222 Controller/Lead.php:201
-msgid "Probability"
-msgstr "Wahrscheinlichkeit"
-
-#: Export/Pdf.php:93 js/LeadEditDialog.js:408 js/LeadGridDetailsPanel.js:444
-#: Controller/Lead.php:197
+#: Controller/Lead.php:197 js/LeadEditDialog.js:384
+#: js/LeadGridDetailsPanel.js:453 Export/Pdf.php:93
 msgid "Start"
 msgstr "Start"
 
-#: Export/Pdf.php:96 js/LeadEditDialog.js:229 Controller/Lead.php:199
-msgid "End"
-msgstr "Ende"
-
-#: Export/Pdf.php:99
-msgid "End Scheduled"
+#: Controller/Lead.php:198
+msgid "Scheduled end"
 msgstr "geplantes Ende"
 
-#: Export/Pdf.php:184
-msgid "Contacts"
-msgstr "Kontakte"
-
-#: Export/Pdf.php:211 js/Contact.js:231
-msgid "Address"
-msgstr "Anschrift"
-
-#: Export/Pdf.php:218 Controller.php:177
-msgid "Telephone"
-msgstr "Telefon"
-
-#: Export/Pdf.php:219 Controller.php:176
-msgid "Email"
-msgstr "Email"
-
-#: Export/Pdf.php:232 js/Task.js:82 js/LeadEditDialog.js:245
-msgid "Tasks"
-msgstr "Aufgaben"
-
-#: Export/Pdf.php:252 js/Task.js:224
-msgid "Due Date"
-msgstr "Fälligkeitsdatum"
-
-#: Export/Pdf.php:259 js/Task.js:237
-msgid "Priority"
-msgstr "Priorität"
+#: Controller/Lead.php:199 js/LeadEditDialog.js:229 Export/Pdf.php:96
+msgid "End"
+msgstr "Ende"
 
-#: Export/Pdf.php:274 js/Product.js:85 js/LeadEditDialog.js:256
-msgid "Products"
-msgstr "Produkte"
+#: Controller/Lead.php:200 js/LeadGridPanel.js:147 Export/Pdf.php:87
+msgid "Turnover"
+msgstr "Umsatz"
 
-#: Export/Pdf.php:315
-msgid "low"
-msgstr "Niedrig"
+#: Controller/Lead.php:201 js/LeadEditDialog.js:222 Export/Pdf.php:90
+msgid "Probability"
+msgstr "Wahrscheinlichkeit"
 
-#: Export/Pdf.php:316
-msgid "normal"
-msgstr "Normal"
+#: Controller/Lead.php:202
+msgid "Folder"
+msgstr "Ordner"
 
-#: Export/Pdf.php:317
-msgid "high"
-msgstr "Hoch"
+#: Controller/Lead.php:203
+msgid "Updated by"
+msgstr "Aktualisiert von"
 
-#: Export/Pdf.php:318
-msgid "urgent"
-msgstr "Dringend"
+#: Controller/Lead.php:204
+msgid "Updated Fields:"
+msgstr "Geänderte Felder:"
 
-#: Controller.php:117
+#: Controller/Lead.php:205
 #, python-format
-msgid "%s's personal leads"
-msgstr "%ss persönliche Leads"
-
-#: Controller.php:162 Model/Lead.php:123 js/LeadGridPanel.js:144
-#: js/LeadGridDetailsPanel.js:45 js/Contact.js:161 js/Contact.js:356
-#: js/Contact.js:396 Controller/Lead.php:193
-msgid "Customer"
-msgstr "Kunde"
-
-#: Controller.php:163 js/LeadGridPanel.js:143 js/LeadGridDetailsPanel.js:45
-#: js/Contact.js:181 js/Contact.js:357 js/Contact.js:400
-msgid "Partner"
-msgstr "Partner"
-
-#: Controller.php:164
-msgid "Reseller"
-msgstr "Reseller"
-
-#: Controller.php:167
-msgid "open"
-msgstr "offen"
-
-#: Controller.php:168
-msgid "contacted"
-msgstr "kontaktiert"
-
-#: Controller.php:169
-msgid "waiting for feedback"
-msgstr "Wartet auf Feedback"
-
-#: Controller.php:170
-msgid "quote sent"
-msgstr "Quote gesendet"
-
-#: Controller.php:171
-msgid "accepted"
-msgstr "akzeptiert"
-
-#: Controller.php:172
-msgid "lost"
-msgstr "verloren"
+msgid "%s changed from %s to %s."
+msgstr "%s wurde von %s auf %s geändert."
 
-#: Controller.php:175
-msgid "Market"
-msgstr "Markt"
+#: Controller/Lead.php:211
+#, python-format
+msgid "Lead %s has been changed"
+msgstr "Lead %s wurde geändert"
 
-#: Controller.php:178
-msgid "Website"
-msgstr "Webseite"
+#: Controller/Lead.php:213
+#, python-format
+msgid "Lead %s has been created"
+msgstr "Lead %s wurde angelegt"
 
 #: Acl/Rights.php:106
 msgid "manage shared lead folders"
@@ -180,173 +103,48 @@ msgstr "Gemeinsame Lead-Favoriten verwalten"
 msgid "Create or update shared leads favorites"
 msgstr "Gemeinsame Lead-Favoriten erstellen oder bearbeiten"
 
-#: Model/Lead.php:122 js/LeadGridPanel.js:142 js/Contact.js:171
-#: js/Contact.js:355 js/Contact.js:392
-msgid "Responsible"
-msgstr "Verantwortlicher"
-
-#: Model/Lead.php:129
-msgid "Task"
-msgstr "Aufgabe"
-
-#: js/Task.js:105
-msgid "No Tasks to display"
-msgstr "Keine Aufgaben vorhanden"
-
-#: js/Task.js:216
-msgid "Summary"
-msgstr "Zusammenfassung"
-
-#: js/Task.js:220
-msgid "Add a task..."
-msgstr "Aufgabe hinzufügen..."
-
-#: js/Task.js:253
-msgid "Percent"
-msgstr "Prozent"
-
-#: js/Task.js:266 js/LeadGridDetailsPanel.js:439
-msgid "Status"
-msgstr "Status"
-
-#: js/Task.js:282
-msgid "Organizer"
-msgstr "Organisator"
-
-#: js/AdminPanel.js:122 js/LeadGridDetailsPanel.js:194
-#: js/LeadGridDetailsPanel.js:227
-msgid "Leadstates"
-msgstr "Lead-Status"
-
-#: js/AdminPanel.js:126 js/LeadGridDetailsPanel.js:216
-msgid "Leadtypes"
-msgstr "Lead-Typen"
-
-#: js/AdminPanel.js:130 js/LeadGridDetailsPanel.js:205
-msgid "Leadsources"
-msgstr "Lead-Quellen"
-
-#: js/AdminPanel.js:138
-msgid "Defaults"
-msgstr "Standardeinstellungen"
-
-#: js/LeadState.js:32
-msgid "Lead State"
-msgid_plural "Lead States"
-msgstr[0] "Lead Status"
-msgstr[1] "Lead Status"
-
-#: js/LeadState.js:166
-msgid "Add a Leadstate..."
-msgstr "Lead-Status hinzufügen..."
-
-#: js/LeadGridContactFilter.js:47
-msgid "Contact"
-msgstr "Kontakt"
-
-#: js/LeadGridContactFilter.js:54
-msgid "CRM Role"
-msgstr "CRM Rolle"
-
-#: js/LeadType.js:29
-msgid "Lead Type"
-msgid_plural "Lead Types"
-msgstr[0] "Lead Typ"
-msgstr[1] "Lead Typen"
-
-#: js/LeadType.js:134
-msgid "Add a Leadtype..."
-msgstr "Lead-Typ hinzufügen..."
-
-#: js/LinkGridPanel.js:34
-#, python-brace-format
-msgid "Add new {0}"
-msgstr "Neuen {0} hinzufügen"
-
-#: js/LinkGridPanel.js:56 js/Product.js:185
-#, python-brace-format
-msgid "Unlink {0}"
-msgstr "Entferne {0}"
-
-#: js/LinkGridPanel.js:57 js/Product.js:186
-#, python-brace-format
-msgid "Unlink selected {0}"
-msgstr "Ausgewählte {0} entfernen"
-
-#: js/LinkGridPanel.js:72
-#, python-brace-format
-msgid "Edit {0}"
-msgstr "{0} bearbeiten"
-
-#: js/LinkGridPanel.js:73
-#, python-brace-format
-msgid "Edit selected {0}"
-msgstr "Ausgewählten {0} bearbeiten"
-
-#: js/Product.js:134
-msgid "Product"
-msgstr "Produkt"
-
-#: js/Product.js:139 js/LeadEditDialog.js:446
-msgid "Description"
-msgstr "Beschreibung"
-
-#: js/Product.js:146
-msgid "Price"
-msgstr "Preis"
-
-#: js/Product.js:158
-msgid "Quantity"
-msgstr "Anzahl"
-
-#: js/Product.js:216
-msgid "Search for Products to add ..."
-msgstr "Produkte zum Hinzufügen suchen..."
-
-#: js/LeadGridPanel.js:139
-msgid "Lead id"
-msgstr "Lead-Bezeichner"
-
-#: js/LeadGridPanel.js:140
-msgid "Tags"
-msgstr "Tags"
+#: Setup/Initialize.php:36 Setup/Update/Release3.php:37
+msgid "All leads I have read grants for"
+msgstr "Alle Leads für die ich Lese-Rechte habe"
 
-#: js/LeadGridPanel.js:141 js/Model.js:108
-msgid "Lead name"
-msgstr "Lead-Name"
+#: Setup/Initialize.php:43
+msgid "Last modified by me"
+msgstr "Zuletzt von mir bearbeitet"
 
-#: js/LeadGridPanel.js:150 js/Model.js:119 js/LeadEditDialog.js:414
-#: js/LeadGridDetailsPanel.js:449
-msgid "Estimated end"
-msgstr "Geplantes Ende"
+#: Setup/Initialize.php:44
+msgid "All leads that I have last modified"
+msgstr "Alle Leads, die ich zuletzt bearbeitet habe"
 
-#: js/LeadGridPanel.js:151
-msgid "Probable Turnover"
-msgstr "Voraussichtlicher Umsatz"
+#: Setup/Initialize.php:53
+msgid "My leads"
+msgstr "Meine Leads"
 
-#: js/LeadGridPanel.js:152 js/Model.js:120 js/LeadEditDialog.js:395
-msgid "Resubmission Date"
-msgstr "Wiedervorlagedatum"
+#: Setup/Initialize.php:54
+msgid "All leads that I am responsible for"
+msgstr "Alle Leads, für die ich verantwortlich bin"
 
-#: js/LeadGridPanel.js:195
-msgid "Export Lead"
-msgstr "Lead exportieren"
+#: Setup/Initialize.php:67
+msgid "Leads with overdue tasks"
+msgstr "Leads mit überfälligen Aufgaben"
 
-#: js/LeadGridPanel.js:204
-msgid "Export as PDF"
-msgstr "Als PDF exportieren"
+#: js/Crm.js:24
+msgid "New Lead"
+msgstr "Neuer Lead"
 
-#: js/LeadGridPanel.js:211
-msgid "Export as CSV"
-msgstr "Als CSV exportieren"
+#: js/LeadSource.js:29
+msgid "Lead Source"
+msgid_plural "Lead Sources"
+msgstr[0] "Lead Quelle"
+msgstr[1] "Lead Quellen"
 
-#: js/LeadGridPanel.js:218
-msgid "Export as ODS"
-msgstr "Als ODS exportieren"
+#: js/LeadSource.js:153