partkeepr

fork of partkeepr
git clone https://git.e1e0.net/partkeepr.git
Log | Files | Refs | Submodules | README | LICENSE

commit ed09701f5f9d7f02810f9852cdaf5164115db570
parent 894f8b5e5ec3d8be9913e8e491f3163d4afdea7e
Author: Felicitus <felicitus@felicitus.org>
Date:   Thu, 18 Aug 2011 08:02:01 +0200

Added tip of the day and user preferences as well as migration testing.

Diffstat:
Mcli-config.php | 18++++--------------
Acronjobs/UpdateTipsOfTheDay.php | 22++++++++++++++++++++++
Adoctrine.php | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afrontend/js/Components/TipOfTheDay/TipOfTheDayWindow.js | 312+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afrontend/js/Ext.ux/Iframe.js | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afrontend/js/Models/TipOfTheDay.js | 11+++++++++++
Afrontend/js/Models/UserPreference.js | 9+++++++++
Mfrontend/js/PartKeepr.js | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amigrations.yml | 6++++++
Msrc/de/RaumZeitLabor/PartKeepr/PartKeepr.php | 11++++++++++-
Msrc/de/RaumZeitLabor/PartKeepr/Service/Service.php | 11+++++++++++
Asrc/de/RaumZeitLabor/PartKeepr/TipOfTheDay/TipOfTheDay.php | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/de/RaumZeitLabor/PartKeepr/TipOfTheDay/TipOfTheDayHistory.php | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/de/RaumZeitLabor/PartKeepr/TipOfTheDay/TipOfTheDayService.php | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/de/RaumZeitLabor/PartKeepr/UserPreference/UserPreference.php | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/de/RaumZeitLabor/PartKeepr/UserPreference/UserPreferenceService.php | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/de/RaumZeitLabor/PartKeepr/Versions/Version20110817235003.php | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/de/RaumZeitLabor/PartKeepr/Versions/Version20110818051810.php | 48++++++++++++++++++++++++++++++++++++++++++++++++
18 files changed, 1202 insertions(+), 15 deletions(-)

diff --git a/cli-config.php b/cli-config.php @@ -1,17 +1,8 @@ <?php -use de\RaumZeitLabor\PartKeepr\Service\ServiceManager; use de\RaumZeitLabor\PartKeepr\PartKeepr; -use Doctrine\Common\ClassLoader; -include("src/de/RaumZeitLabor/PartKeepr/PartKeepr.php"); -PartKeepr::initialize(""); +$helpers = array( + 'db' => new \Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper(PartKeepr::getEM()->getConnection()), + 'em' => new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper(PartKeepr::getEM()) +); -$em = PartKeepr::getEM(); - -$classes = PartKeepr::getEntityClasses(); - - -$helperSet = new \Symfony\Component\Console\Helper\HelperSet(array( - 'db' => new \Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper($em->getConnection()), - 'em' => new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($em) -));- \ No newline at end of file diff --git a/cronjobs/UpdateTipsOfTheDay.php b/cronjobs/UpdateTipsOfTheDay.php @@ -0,0 +1,21 @@ +<?php +/** + * Updates the tip of the day title index. + * Typically scheduled once or twice a day. + * + * The API to retrieve tips has an upper limit of 500 requests/day, so don't schedule this each minute. + * + * @author felicitus + * + */ +namespace de\RaumZeitLabor\PartKeepr\Cronjobs; + +declare(encoding = 'UTF-8'); + +include(__DIR__."/../src/de/RaumZeitLabor/PartKeepr/PartKeepr.php"); + +use de\RaumZeitLabor\PartKeepr\PartKeepr; +use de\RaumZeitLabor\PartKeepr\TipOfTheDay\TipOfTheDay; +PartKeepr::initialize(); + +TipOfTheDay::syncTips();+ \ No newline at end of file diff --git a/doctrine.php b/doctrine.php @@ -0,0 +1,55 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\Foo; +declare(encoding = 'UTF-8'); + +use de\RaumZeitLabor\PartKeepr\PartKeepr; + +include("src/de/RaumZeitLabor/PartKeepr/PartKeepr.php"); + +PartKeepr::initialize(""); +// Variable $helperSet is defined inside cli-config.php +$helpers = array(); + +require __DIR__ . '/cli-config.php'; + +$cli = new \Symfony\Component\Console\Application('Doctrine Command Line Interface', \Doctrine\Common\Version::VERSION); +$cli->setCatchExceptions(true); +$helperSet = $cli->getHelperSet(); +foreach ($helpers as $name => $helper) { + $helperSet->set($helper, $name); +} +$cli->addCommands(array( + // DBAL Commands + new \Doctrine\DBAL\Tools\Console\Command\RunSqlCommand(), + new \Doctrine\DBAL\Tools\Console\Command\ImportCommand(), + + // ORM Commands + new \Doctrine\ORM\Tools\Console\Command\ClearCache\MetadataCommand(), + new \Doctrine\ORM\Tools\Console\Command\ClearCache\ResultCommand(), + new \Doctrine\ORM\Tools\Console\Command\ClearCache\QueryCommand(), + new \Doctrine\ORM\Tools\Console\Command\SchemaTool\CreateCommand(), + new \Doctrine\ORM\Tools\Console\Command\SchemaTool\UpdateCommand(), + new \Doctrine\ORM\Tools\Console\Command\SchemaTool\DropCommand(), + new \Doctrine\ORM\Tools\Console\Command\EnsureProductionSettingsCommand(), + new \Doctrine\ORM\Tools\Console\Command\ConvertDoctrine1SchemaCommand(), + new \Doctrine\ORM\Tools\Console\Command\GenerateRepositoriesCommand(), + new \Doctrine\ORM\Tools\Console\Command\GenerateEntitiesCommand(), + new \Doctrine\ORM\Tools\Console\Command\GenerateProxiesCommand(), + new \Doctrine\ORM\Tools\Console\Command\ConvertMappingCommand(), + new \Doctrine\ORM\Tools\Console\Command\RunDqlCommand(), + new \Doctrine\ORM\Tools\Console\Command\ValidateSchemaCommand(), + +)); + +$cli->addCommands(array( +// ... + +// Migrations Commands +new \Doctrine\DBAL\Migrations\Tools\Console\Command\DiffCommand(), +new \Doctrine\DBAL\Migrations\Tools\Console\Command\ExecuteCommand(), +new \Doctrine\DBAL\Migrations\Tools\Console\Command\GenerateCommand(), +new \Doctrine\DBAL\Migrations\Tools\Console\Command\MigrateCommand(), +new \Doctrine\DBAL\Migrations\Tools\Console\Command\StatusCommand(), +new \Doctrine\DBAL\Migrations\Tools\Console\Command\VersionCommand() +)); +$cli->run(); diff --git a/frontend/js/Components/TipOfTheDay/TipOfTheDayWindow.js b/frontend/js/Components/TipOfTheDay/TipOfTheDayWindow.js @@ -0,0 +1,311 @@ +/** + * This class represents the tip of the day window and its logic. + */ +Ext.define("PartKeepr.TipOfTheDayWindow", { + extend: 'Ext.window.Window', + + /* Defines the title template. */ + titleTemplate: i18n("Tip of the Day"), + + /* Cosmetic settings */ + width: 600, + height: 300, + + minWidth: 600, + minHeight: 300, + + layout: 'fit', + + /** + * Stores the currently displayed tip, or null if none is displayed + * @var Ext.data.Record + */ + currentTip: null, + + /** + * Holds an instance of the TipOfTheDay store. + */ + tipStore: null, + + /** + * Initializes the window. Adds the iframe used for displaying tips, as well + * as the user controls (prev/next buttons, config checkboxes). + */ + initComponent: function () { + // Initialize the window with the title template + this.title = this.titleTemplate; + + // Set the tip store + this.tipStore = PartKeepr.getApplication().getTipOfTheDayStore(); + + // Set the tip display iframe and add it to the items + this.tipDisplay = Ext.create("Ext.ux.SimpleIFrame", { + border: false + }); + + this.items = this.tipDisplay; + + // Initialize previous and next buttons + this.previousButton = Ext.create("Ext.button.Button", { + text: i18n("Previous Tip"), + handler: Ext.bind(this.displayPreviousTip, this), + disabled: true + }); + + this.nextButton = Ext.create("Ext.button.Button", { + text: i18n("Next Tip"), + handler: Ext.bind(this.displayNextTip, this) + }); + + // Initializes the "show tips on login" checkbox as well as the "show read tips" checkbox + this.showTipsCheckbox = Ext.create("Ext.form.field.Checkbox", { + boxLabel: i18n("Show Tips on login"), + handler: Ext.bind(this.showTipsHandler, this) + }); + + this.displayReadTipsCheckbox = Ext.create("Ext.form.field.Checkbox", { + boxLabel: i18n("Show read tips"), + handler: Ext.bind(this.showReadTipsHandler, this) + }); + + // Initialize the "show tips" checkbox with the user preference + if (PartKeepr.getApplication().getUserPreference("partkeepr.tipoftheday.showtips") === false) { + this.showTipsCheckbox.setValue(false); + } else { + this.showTipsCheckbox.setValue(true); + } + + // Append the controls to the bottom toolbar + this.dockedItems = [{ + xtype: 'toolbar', + dock: 'bottom', + items: [ + this.showTipsCheckbox, + this.displayReadTipsCheckbox, + '->', + this.previousButton, + this.nextButton + ] + }]; + + // Auto-load the next unread tip on window display + this.on("show", this.displayNextTip, this); + + // Window destroy handler + this.on("destroy", this.onDestroy, this); + this.callParent(); + }, + /** + * If the "show read tips" checkbox was clicked, update the buttons + * to reflect the tip navigation. + */ + showReadTipsHandler: function () { + this.updateButtons(this.currentTip); + }, + /** + * Destroy handler. Removes the "read tip" timer. + */ + onDestroy: function () { + this.cancelReadTimer(); + }, + /** + * Cancels the read timer. + */ + cancelReadTimer: function () { + if (this.markAsReadTask) { + this.markAsReadTask.cancel(); + } + }, + /** + * Handler when the "show tips" checkbox was clicked. + */ + showTipsHandler: function (checkbox, checked) { + PartKeepr.getApplication().setUserPreference("partkeepr.tipoftheday.showtips", checked); + }, + /** + * Displays a specific tip of the day. + * @param record The record which contains the information regarding the tip + */ + displayTip: function (record) { + // Cancel the old read timer + this.cancelReadTimer(); + + // Update buttons to reflect position + this.updateButtons(record); + + // Set the title to the tip name + this.setTitle(this.titleTemplate+ ": " + record.get("name")); + + // Set iframe to the tip url + this.tipDisplay.setSrc(record.get("url")); + + // Fire up delayed task to mark the tip as read + this.markAsReadTask = new Ext.util.DelayedTask(this.markTipRead, this); + this.markAsReadTask.delay(5000); + + }, + /** + * Updates the navigation buttons. + * + * This method has two modes, depending on which state the "show read tips" checkbox is in. + * @param record The currently displayed tip + */ + updateButtons: function (record) { + if (this.displayReadTipsCheckbox.getValue() == true) { + if (this.tipStore.indexOf(record) > 0) { + this.previousButton.enable(); + } else { + this.previousButton.disable(); + } + + if (this.tipStore.indexOf(record) == this.tipStore.getTotalCount() - 1) { + this.nextButton.disable(); + } else { + this.nextButton.enable(); + } + } else { + if (this.tipStore.indexOf(record) > this.getFirstUnreadTip()) { + this.previousButton.enable(); + } else { + this.previousButton.disable(); + } + + + if (this.tipStore.indexOf(record) >= this.getLastUnreadTip()) { + this.nextButton.disable(); + } else { + this.nextButton.enable(); + } + } + + }, + /** + * Returns the index of the first unread tip, or null if there's no unread tip. + * @returns int The index of the first unread tip, or null + */ + getFirstUnreadTip: function () { + for (var i=0;i<this.tipStore.getTotalCount();i++) { + if (this.tipStore.getAt(i).get("read") == false) { + return i; + } + } + + return null; + }, + /** + * Returns the index of the last unread tip, or null if there's no unread tip. + * @returns int The index of the last unread tip, or null + */ + getLastUnreadTip: function () { + for (var i=this.tipStore.getTotalCount()-1;i>-1;i--) { + if (this.tipStore.getAt(i).get("read") == false) { + return i; + } + } + + return null; + }, + /** + * Marks the current tip as read. Commits the information to the server. + */ + markTipRead: function () { + this.currentTip.set("read", true); + this.currentTip.commit(); + + var call = new PartKeepr.ServiceCall("TipOfTheDay", "markTipAsRead"); + call.setLoadMessage(sprintf(i18n("Marking tip %s as read..."), this.currentTip.get("name"))); + call.setParameter("name", this.currentTip.get("name")); + call.doCall(); + }, + /** + * Displays the next tip + */ + displayNextTip: function () { + this.retrieveTip("ASC"); + }, + /** + * Displays the previous tip + */ + displayPreviousTip: function () { + this.retrieveTip("DESC"); + }, + /** + * Displays the next or previous tip. + * + * @param dir string Either "ASC" or "DESC", which denotes the direction to search for the next tip + */ + retrieveTip: function (dir) { + var startIdx = -1, record = null; + + if (this.currentTip) { + startIdx = this.tipStore.indexOf(this.currentTip); + } + + if (dir == "ASC") { + record = this.extractNextTip(startIdx); + } else { + record = this.extractPreviousTip(startIdx); + } + + if (record) { + this.currentTip = record; + this.displayTip(record); + } + }, + /** + * Returns the record with the next tip + * @param startIdx The index to start searching from + * @returns record The record with the next tip + */ + extractNextTip: function (startIdx) { + var record = null, foundRecord = null; + if (this.displayReadTipsCheckbox.getValue() == true) { + var tmpIdx = startIdx + 1; + if (tmpIdx > this.tipStore.getTotalCount() - 1) { + tmpIdx = this.tipStore.getTotalCount() - 1; + } + + foundRecord = this.tipStore.getAt(tmpIdx); + } else { + for (var i = startIdx+1; i < this.tipStore.getTotalCount();i++) { + record = this.tipStore.getAt(i); + if (record.get("read") === false) { + foundRecord = record; + break; + } + } + } + + return foundRecord; + }, + /** + * Returns the record with the previous tip + * @param startIdx The index to start searching from + * @returns record The record with the previous tip + */ + extractPreviousTip: function (startIdx) { + var record = null, foundRecord = null; + if (this.displayReadTipsCheckbox.getValue() == true) { + var tmpIdx = startIdx - 1; + if (tmpIdx < 0) { + tmpIdx = 0; + } + + foundRecord = this.tipStore.getAt(tmpIdx); + } else { + for (var i = startIdx - 1; i > -1;i--) { + record = this.tipStore.getAt(i); + + if (record.get("read") === false) { + foundRecord = record; + break; + } + } + } + + + return foundRecord; + } + + +});+ \ No newline at end of file diff --git a/frontend/js/Ext.ux/Iframe.js b/frontend/js/Ext.ux/Iframe.js @@ -0,0 +1,117 @@ +// vim: sw=2:ts=2:nu:nospell:fdc=2:expandtab +/** +* @class Ext.ux.SimpleIFrame +* @extends Ext.Panel +* +* A simple ExtJS 4 implementaton of an iframe providing basic functionality. +* For example: +* +* var panel=Ext.create('Ext.ux.SimpleIFrame', { +* border: false, +* src: 'http://localhost' +* }); +* panel.setSrc('http://www.sencha.com'); +* panel.reset(); +* panel.reload(); +* panel.getSrc(); +* panel.update('<div><b>Some Content....</b></div>'); +* panel.destroy(); +* +* @author Conor Armstrong +* @copyright (c) 2011 Conor Armstrong +* @date 12 April 2011 +* @version 0.1 +* +* @license Ext.ux.SimpleIFrame.js is licensed under the terms of the Open Source +* LGPL 3.0 license. Commercial use is permitted to the extent that the +* code/component(s) do NOT become part of another Open Source or Commercially +* licensed development library or toolkit without explicit permission. +* +* <p>License details: <a href="http://www.gnu.org/licenses/lgpl.html" +* target="_blank">http://www.gnu.org/licenses/lgpl.html</a></p> +* +*/ + +Ext.require([ + 'Ext.panel.*' +]); + +Ext.define('Ext.ux.SimpleIFrame', { + extend: 'Ext.Panel', + alias: 'widget.simpleiframe', + src: 'about:blank', + loadingText: 'Loading ...', + initComponent: function(){ + this.updateHTML(); + this.callParent(arguments); + }, + updateHTML: function() { + this.html='<iframe id="iframe-'+this.id+'"'+ + ' style="overflow:auto;width:100%;height:100%;"'+ + ' frameborder="0" '+ + ' src="'+this.src+'"'+ + '></iframe>'; + }, + reload: function() { + this.setSrc(this.src); + }, + reset: function() { + var iframe=this.getDOM(); + var iframeParent=iframe.parentNode; + if (iframe && iframeParent) { + iframe.src='about:blank'; + iframe.parentNode.removeChild(iframe); + } + + iframe=document.createElement('iframe'); + iframe.frameBorder=0; + iframe.src=this.src; + iframe.id='iframe-'+this.id; + iframe.style.overflow='auto'; + iframe.style.width='100%'; + iframe.style.height='100%'; + iframeParent.appendChild(iframe); + }, + setSrc: function(src, loadingText) { + this.src=src; + var iframe=this.getDOM(); + if (iframe) { + iframe.src=src; + } + }, + getSrc: function() { + return this.src; + }, + getDOM: function() { + return document.getElementById('iframe-'+this.id); + }, + getDocument: function() { + var iframe=this.getDOM(); + iframe = (iframe.contentWindow) ? iframe.contentWindow : (iframe.contentDocument.document) ? iframe.contentDocument.document : iframe.contentDocument; + return iframe.document; + }, + destroy: function() { + var iframe=this.getDOM(); + if (iframe && iframe.parentNode) { + iframe.src='about:blank'; + iframe.parentNode.removeChild(iframe); + } + this.callParent(arguments); + }, + update: function(content) { + this.setSrc('about:blank'); + try { + var doc=this.getDocument(); + doc.open(); + doc.write(content); + doc.close(); + } catch(err) { + // reset if any permission issues + this.reset(); + var doc=this.getDocument(); + doc.open(); + doc.write(content); + doc.close(); + } + } +});+ \ No newline at end of file diff --git a/frontend/js/Models/TipOfTheDay.js b/frontend/js/Models/TipOfTheDay.js @@ -0,0 +1,10 @@ +Ext.define("PartKeepr.TipOfTheDay", { + extend: "Ext.data.Model", + fields: [ + { id: 'id', name: 'id', type: 'int' }, + { name: 'name', type: 'string'}, + { name: 'url', type: 'string'}, + { name: 'read', type: 'boolean' } + ], + proxy: PartKeepr.getRESTProxy("TipOfTheDay"), +});+ \ No newline at end of file diff --git a/frontend/js/Models/UserPreference.js b/frontend/js/Models/UserPreference.js @@ -0,0 +1,8 @@ +Ext.define("PartKeepr.UserPreference", { + extend: "Ext.data.Model", + fields: [ + { name: 'key', type: 'string'}, + { name: 'value'} + ], + proxy: PartKeepr.getRESTProxy("UserPreference") +});+ \ No newline at end of file diff --git a/frontend/js/PartKeepr.js b/frontend/js/PartKeepr.js @@ -33,6 +33,10 @@ Ext.application({ onContextMenu: function (e, target) { e.preventDefault(); }, + /** + * Handles the login function. Initializes the part manager window, + * enables the menu bar and creates the stores+loads them. + */ login: function () { this.createGlobalStores(); this.reloadStores(); @@ -44,6 +48,23 @@ Ext.application({ this.addItem(j); this.menuBar.enable(); + + /* Give the user preference stuff enough time to load */ + /* @todo Load user preferences directly on login and not via delayed task */ + this.displayTipWindowTask = new Ext.util.DelayedTask(this.displayTipOfTheDayWindow, this); + this.displayTipWindowTask.delay(1000); + + }, + /** + * Displays the tip of the day window. + * + * This method checks if the user has disabled tips, and if so, this method + * avoids showing the window. + */ + displayTipOfTheDayWindow: function () { + if (PartKeepr.getApplication().setUserPreference("partkeepr.tipoftheday.showtips") !== false) { + Ext.create("PartKeepr.TipOfTheDayWindow").show(); + } }, logout: function () { this.menuBar.disable(); @@ -106,6 +127,20 @@ Ext.application({ pageSize: -1, autoLoad: false }); + + this.tipOfTheDayStore = Ext.create("Ext.data.Store", + { + model: 'PartKeepr.TipOfTheDay', + pageSize: -1, + autoLoad: true + }); + + this.userPreferenceStore = Ext.create("Ext.data.Store", + { + model: 'PartKeepr.UserPreference', + pageSize: -1, + autoLoad: true + }); }, setAdmin: function (admin) { this.admin = admin; @@ -113,6 +148,45 @@ Ext.application({ isAdmin: function () { return this.admin; }, + getTipOfTheDayStore: function () { + return this.tipOfTheDayStore; + }, + /** + * Queries for a specific user preference. Returns either the value or null if the + * preference was not found. + * @param key The key to query + * @returns the key value, or null if nothing was found + */ + getUserPreference: function (key) { + var record = this.userPreferenceStore.findRecord("key", key); + + if (record) { + return record.get("value"); + } else { + return null; + } + }, + /** + * Sets a specific user preference. Directly commits the change to the server. + * + * @param key The key to set + * @param value The value to set + */ + setUserPreference: function (key, value) { + var record = this.userPreferenceStore.findRecord("key", key); + + if (record) { + record.set("value", value); + } else { + var j = new PartKeepr.UserPreference(); + j.set("key", key); + j.set("value", value); + + this.userPreferenceStore.add(j); + } + + this.userPreferenceStore.sync(); + }, getUnitStore: function () { return this.unitStore; }, diff --git a/migrations.yml b/migrations.yml @@ -0,0 +1,5 @@ +--- +name: PartKeepr +migrations_namespace: DoctrineMigrations +table_name: SchemaVersions +migrations_directory: src/de/RaumZeitLabor/PartKeepr/Versions+ \ No newline at end of file diff --git a/src/de/RaumZeitLabor/PartKeepr/PartKeepr.php b/src/de/RaumZeitLabor/PartKeepr/PartKeepr.php @@ -50,6 +50,9 @@ class PartKeepr { $classLoader = new ClassLoader('Doctrine\ORM'); $classLoader->register(); // register on SPL autoload stack + $classLoader = new ClassLoader("Doctrine\DBAL\Migrations", dirname(dirname(dirname(dirname(__DIR__)))) ."/3rdparty/doctrine-migrations/lib"); + $classLoader->register(); + $classLoader = new ClassLoader('Doctrine\DBAL'); $classLoader->register(); // register on SPL autoload stack @@ -61,6 +64,8 @@ class PartKeepr { $classLoader = new ClassLoader("DoctrineExtensions\NestedSet", dirname(dirname(dirname(dirname(__DIR__)))) ."/3rdparty/doctrine2-nestedset/lib"); $classLoader->register(); + + } @@ -205,7 +210,11 @@ class PartKeepr { 'de\RaumZeitLabor\PartKeepr\Statistic\StatisticSnapshotUnit', 'de\RaumZeitLabor\PartKeepr\SiPrefix\SiPrefix', 'de\RaumZeitLabor\PartKeepr\Unit\Unit', - 'de\RaumZeitLabor\PartKeepr\PartParameter\PartParameter' + 'de\RaumZeitLabor\PartKeepr\PartParameter\PartParameter', + + 'de\RaumZeitLabor\PartKeepr\TipOfTheDay\TipOfTheDay', + 'de\RaumZeitLabor\PartKeepr\TipOfTheDay\TipOfTheDayHistory', + 'de\RaumZeitLabor\PartKeepr\UserPreference\UserPreference', ); } diff --git a/src/de/RaumZeitLabor/PartKeepr/Service/Service.php b/src/de/RaumZeitLabor/PartKeepr/Service/Service.php @@ -1,5 +1,7 @@ <?php namespace de\RaumZeitLabor\PartKeepr\Service; +use de\RaumZeitLabor\PartKeepr\User\User; + use de\RaumZeitLabor\PartKeepr\Session\Session; declare(encoding = 'UTF-8'); @@ -47,6 +49,15 @@ class Service { return $this->params; } + /** + * Returns the current user for this session + * + * @return User The user + */ + public function getUser () { + return SessionManager::getCurrentSession()->getUser(); + } + public function hasParameter ($name) { if (array_key_exists($name, $this->params)) { return true; diff --git a/src/de/RaumZeitLabor/PartKeepr/TipOfTheDay/TipOfTheDay.php b/src/de/RaumZeitLabor/PartKeepr/TipOfTheDay/TipOfTheDay.php @@ -0,0 +1,114 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\TipOfTheDay; + +use de\RaumZeitLabor\PartKeepr\Util\Serializable; + +use de\RaumZeitLabor\PartKeepr\PartKeepr; + +use de\RaumZeitLabor\PartKeepr\Util\Configuration; + +use de\RaumZeitLabor\PartKeepr\Util\BaseEntity; + +/** + * Represents a tip of the day. + * + * Tips are stored on the central PartKeepr server in a wiki. However, we need to know a list of all tip pages + * because the API has a limit per day. So basically, we sync the tip names from the wiki to the local system several + * times a day and not each time an user logs in. + * + * Note: If you wish to link against a tip of the day, do it by name and not by id! + * + * @Entity + **/ +class TipOfTheDay extends BaseEntity implements Serializable { + /** + * @Column(type="string") + * @var string + */ + private $name; + + /** + * Sets the name for this tip + * @param string $name The name + */ + public function setName ($name) { + $this->name = $name; + } + + /** + * Returns the name for this tip + * @return string The name + */ + public function getName () { + return $this->name; + } + + /** + * Syncronizes the tip database against the master wiki. + * @throws \Exception + */ + public static function syncTips () { + if (ini_get("allow_url_fopen") == 0) { + throw new \Exception("allow_url_fopen is disabled, but required to query the TipOfTheDay database."); + } + + $url = Configuration::getOption("partkeepr.tipoftheday.api", "http://partkeepr.org/wiki/api.php?action=query&list=categorymembers&cmtitle=Category:TipOfTheDay&format=json"); + + $tipsString = file_get_contents($url); + + + $aPageNames = self::extractPageNames($tipsString); + + self::updateTipDatabase($aPageNames); + } + + /** + * Updates the tip database. Expects an array of page names. + * + * This method clears all page names and re-creates them. This saves + * alot of engineering, because we don't need to match contents + * within the database against contents in an array. + * + * @param array $aPageNames The page names as array. Page names are stored as string. + */ + private static function updateTipDatabase (array $aPageNames) { + $dql = "DELETE FROM de\RaumZeitLabor\PartKeepr\TipOfTheDay\TipOfTheDay"; + $query = PartKeepr::getEM()->createQuery($dql); + + $query->execute(); + + foreach ($aPageNames as $pageName) { + $tip = new TipOfTheDay(); + $tip->setName($pageName); + PartKeepr::getEM()->persist($tip); + } + + PartKeepr::getEM()->flush(); + } + + /** + * Extracts the page names from the mediawiki JSON returned. + * @param string $response The encoded json string + * @return array An array with the titles of each page + */ + private static function extractPageNames ($response) { + $aTipsStructure = json_decode($response, true); + $aTips = $aTipsStructure["query"]["categorymembers"]; + + $aPageNames = array(); + + foreach ($aTips as $tip) { + $aPageNames[] = $tip["title"]; + } + + return $aPageNames; + } + + /** + * (non-PHPdoc) + * @see de\RaumZeitLabor\PartKeepr\Util.Serializable::serialize() + */ + public function serialize () { + return array( "name" => $this->getName() ); + } +}+ \ No newline at end of file diff --git a/src/de/RaumZeitLabor/PartKeepr/TipOfTheDay/TipOfTheDayHistory.php b/src/de/RaumZeitLabor/PartKeepr/TipOfTheDay/TipOfTheDayHistory.php @@ -0,0 +1,51 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\TipOfTheDay; + +use de\RaumZeitLabor\PartKeepr\User\User; + +use de\RaumZeitLabor\PartKeepr\Util\Serializable; + +use de\RaumZeitLabor\PartKeepr\PartKeepr; + +use de\RaumZeitLabor\PartKeepr\Util\Configuration; + +use de\RaumZeitLabor\PartKeepr\Util\BaseEntity; + +/** + * Represents a tip of the day history entry. + * + * This entity stores each tip of the day the user has already seen. + * + * @Entity + **/ +class TipOfTheDayHistory extends BaseEntity { + /** + * @Column(type="string") + * @var string + */ + private $name; + + /** + * Defines the user + * @ManyToOne(targetEntity="de\RaumZeitLabor\PartKeepr\User\User") + * @var StorageLocation + */ + private $user; + + /** + * Sets the user for this entry + * @param User $user + */ + public function setUser (User $user) { + $this->user = $user; + } + + /** + * Sets the tip of the day name the user already has seen + * @param string $name The tip name + */ + public function setName ($name) { + $this->name = $name; + } + +}+ \ No newline at end of file diff --git a/src/de/RaumZeitLabor/PartKeepr/TipOfTheDay/TipOfTheDayService.php b/src/de/RaumZeitLabor/PartKeepr/TipOfTheDay/TipOfTheDayService.php @@ -0,0 +1,73 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\TipOfTheDay; +use de\RaumZeitLabor\PartKeepr\Util\Configuration; + +use de\RaumZeitLabor\PartKeepr\User\User; + +use de\RaumZeitLabor\PartKeepr\Service\RestfulService; +use de\RaumZeitLabor\PartKeepr\Session\SessionManager; + +declare(encoding = 'UTF-8'); + +use de\RaumZeitLabor\PartKeepr\Service\Service; +use de\RaumZeitLabor\PartKeepr\PartKeepr; + +class TipOfTheDayService extends Service implements RestfulService { + /** + * Returns all tips along with the information wether they are read or not. + * (non-PHPdoc) + * @see de\RaumZeitLabor\PartKeepr\Service.RestfulService::get() + */ + public function get () { + $aTips = array(); + $url = Configuration::getOption("partkeepr.tipoftheday.wiki", "http://partkeepr.org/wiki/index.php/"); + + /* Extract all tips which aren't read */ + $dql = "SELECT d FROM de\RaumZeitLabor\PartKeepr\TipOfTheDay\TipOfTheDay d WHERE d.name NOT IN "; + $dql .= "(SELECT dh.name FROM de\RaumZeitLabor\PartKeepr\TipOfTheDay\TipOfTheDayHistory dh WHERE dh.user = :user)"; + + $query = PartKeepr::getEM()->createQuery($dql); + $query->setParameter("user", SessionManager::getCurrentSession()->getUser()); + + foreach ($query->getResult() as $result) { + $aTips[] = array ( + "name" => $result->getName(), + "read" => false, + "url" => $url.$result->getName() . "?useskin=monobookplain"); + } + + /* Extract all tips which are read */ + $dql = "SELECT d FROM de\RaumZeitLabor\PartKeepr\TipOfTheDay\TipOfTheDay d WHERE d.name IN "; + $dql .= "(SELECT dh.name FROM de\RaumZeitLabor\PartKeepr\TipOfTheDay\TipOfTheDayHistory dh WHERE dh.user = :user)"; + + $query = PartKeepr::getEM()->createQuery($dql); + $query->setParameter("user", SessionManager::getCurrentSession()->getUser()); + + foreach ($query->getResult() as $result) { + $aTips[] = array ( + "name" => $result->getName(), + "read" => true, + "url" => $url.$result->getName() . "?useskin=monobookplain"); + } + + return array("data" => $aTips); + } + + public function create() {} + public function update () {} + public function destroy () {} + + /** + * Marks a specific tip as read. + * + * Uses the parameter "name" to identify the tip. + */ + public function markTipAsRead () { + $th = new TipOfTheDayHistory; + $th->setUser($this->getUser()); + $th->setName($this->getParameter("name")); + + PartKeepr::getEM()->persist($th); + PartKeepr::getEM()->flush(); + } +}+ \ No newline at end of file diff --git a/src/de/RaumZeitLabor/PartKeepr/UserPreference/UserPreference.php b/src/de/RaumZeitLabor/PartKeepr/UserPreference/UserPreference.php @@ -0,0 +1,153 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\UserPreference; + +use de\RaumZeitLabor\PartKeepr\Util\Serializable; +use de\RaumZeitLabor\PartKeepr\PartKeepr; +use de\RaumZeitLabor\PartKeepr\User\User; +use de\RaumZeitLabor\PartKeepr\Util\Configuration; +use de\RaumZeitLabor\PartKeepr\Util\BaseEntity; + +/** + * Represents a user preference entry. + * + * User preferences are a simple key => value mechanism, where the developer can + * specify the key and value himself. + * + * Note that values are stored internally as JSON to keep their type. + * + * @Entity + **/ +class UserPreference implements Serializable { + /** + * Defines the key of the user preference + * @Column(type="string",length=255) + * @Id + * @var string + */ + private $preferenceKey; + + /** + * Defines the value. Note that the value is internally stored as a serialized string. + * @Column(type="text") + * @var mixed + */ + private $preferenceValue; + + /** + * Defines the user + * @ManyToOne(targetEntity="de\RaumZeitLabor\PartKeepr\User\User") + * @Id + * @var User + */ + private $user; + + + /** + * Sets the user for this entry + * @param User $user + */ + public function setUser (User $user) { + $this->user = $user; + } + + /** + * Returns the user associated with this entry + * @return \de\RaumZeitLabor\PartKeepr\User\User + */ + public function getUser () { + return $this->user; + } + + /** + * Sets the key for this user preference + * @param string $key The key name + */ + public function setKey ($key) { + $this->preferenceKey = $key; + } + + /** + * Returns the key of this entry + * @return string + */ + public function getKey () { + return $this->preferenceKey; + } + + /** + * Sets the value for this entry + * @param mixed $value + */ + public function setValue ($value) { + $this->preferenceValue = serialize($value); + } + + /** + * Returns the value for this entry + * @return mixed The value + */ + public function getValue () { + return unserialize($this->preferenceValue); + } + + /** + * (non-PHPdoc) + * @see de\RaumZeitLabor\PartKeepr\Util.Serializable::serialize() + */ + public function serialize () { + return array( + "key" => $this->getKey(), + "value" => $this->getValue() + ); + } + + /** + * Creates or updates a preference for a given user. + * + * @param User $user The user to set the preference for + * @param string $key The key to set + * @param string $value The value to set + */ + public static function setPreference (User $user, $key, $value) { + $dql = "SELECT up FROM de\RaumZeitLabor\PartKeepr\UserPreference\UserPreference up WHERE up.user = :user AND "; + $dql .= "up.preferenceKey = :key"; + + $query = PartKeepr::getEM()->createQuery($dql); + $query->setParameter("user", $user); + $query->setParameter("key", $key); + + try { + $up = $query->getSingleResult(); + } catch (\Exception $e) { + $up = new UserPreference(); + $up->setUser($user); + $up->setKey($key); + + PartKeepr::getEM()->persist($up); + } + + $up->setValue($value); + + PartKeepr::getEM()->flush(); + + return $up; + } + + /** + * Removes a specific setting for a specific user. + * + * @param User $user The user to delete the preference for + * @param string $key The key to delete + */ + public static function deletePreference (User $user, $key) { + $dql = "DELETE FROM de\RaumZeitLabor\PartKeepr\UserPreference\UserPreference up WHERE up.user = :user AND "; + $dql .= "up.preferenceKey = :key"; + + $query = PartKeepr::getEM()->createQuery($dql); + $query->setParameter("user", $user); + $query->setParameter("key", $key); + + $query->execute(); + } + +}+ \ No newline at end of file diff --git a/src/de/RaumZeitLabor/PartKeepr/UserPreference/UserPreferenceService.php b/src/de/RaumZeitLabor/PartKeepr/UserPreference/UserPreferenceService.php @@ -0,0 +1,77 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\UserPreference; +use de\RaumZeitLabor\PartKeepr\User\User; + +use de\RaumZeitLabor\PartKeepr\Util\Configuration; + +use de\RaumZeitLabor\PartKeepr\Service\RestfulService; +use de\RaumZeitLabor\PartKeepr\Session\SessionManager; + +declare(encoding = 'UTF-8'); + +use de\RaumZeitLabor\PartKeepr\Service\Service; +use de\RaumZeitLabor\PartKeepr\PartKeepr; + +/** + * Represents the user preference service. This service is implemented as a RestfulService, however, + * only setting and deleting properties is supported, as we don't want to have duplicate values per key. + * + * For convinience, create() and update() perform the exact same function. + * @author felicitus + * + */ +class UserPreferenceService extends Service implements RestfulService { + /** + * Returns the preferences for the current user. + * + * (non-PHPdoc) + * @see de\RaumZeitLabor\PartKeepr\Service.RestfulService::get() + */ + public function get () { + $aPreferences = array(); + + /* Extract all preferences */ + $dql = "SELECT up FROM de\RaumZeitLabor\PartKeepr\UserPreference\UserPreference up WHERE up.user = :user"; + + $query = PartKeepr::getEM()->createQuery($dql); + $query->setParameter("user", SessionManager::getCurrentSession()->getUser()); + + foreach ($query->getResult() as $result) { + $aPreferences[] = $result->serialize(); + } + + return array("data" => $aPreferences); + } + + /** + * Creates or updates a value for a specific key. + * + * (non-PHPdoc) + * @see de\RaumZeitLabor\PartKeepr\Service.RestfulService::create() + */ + public function create() { + $userPreference = UserPreference::setPreference($this->getUser(), $this->getParameter("key"), $this->getParameter("value")); + + return array("data" => $userPreference->serialize()); + } + + /** + * Creates or updates a value for a specific key. + * + * (non-PHPdoc) + * @see de\RaumZeitLabor\PartKeepr\Service.RestfulService::update() + */ + public function update () { + return $this->create(); + } + + /** + * Deletes a key-value combination from the database. + * + * (non-PHPdoc) + * @see de\RaumZeitLabor\PartKeepr\Service.RestfulService::destroy() + */ + public function destroy () { + UserPreference::deletePreference($this->getUser(), $this->getParameter("key")); + } +}+ \ No newline at end of file diff --git a/src/de/RaumZeitLabor/PartKeepr/Versions/Version20110817235003.php b/src/de/RaumZeitLabor/PartKeepr/Versions/Version20110817235003.php @@ -0,0 +1,49 @@ +<?php + +namespace DoctrineMigrations; +use de\RaumZeitLabor\PartKeepr\PartKeepr; + +use Doctrine\DBAL\Migrations\AbstractMigration, + Doctrine\DBAL\Schema\Schema; + +/** + * Auto-generated Migration: Please modify to your need! + */ +class Version20110817235003 extends AbstractMigration +{ + public function up(Schema $schema) + { + $tool = new \Doctrine\ORM\Tools\SchemaTool(PartKeepr::getEM()); + + $classes = array( + 'de\RaumZeitLabor\PartKeepr\TipOfTheDay\TipOfTheDay', + 'de\RaumZeitLabor\PartKeepr\TipOfTheDay\TipOfTheDayHistory' + ); + + $aClasses = array(); + + foreach ($classes as $class) { + $aClasses[] = PartKeepr::getEM()->getClassMetadata($class); + } + + $tool->updateSchema($aClasses, true); + } + + public function down(Schema $schema) + { + $tool = new \Doctrine\ORM\Tools\SchemaTool(PartKeepr::getEM()); + + $classes = array( + 'de\RaumZeitLabor\PartKeepr\TipOfTheDay\TipOfTheDay', + 'de\RaumZeitLabor\PartKeepr\TipOfTheDay\TipOfTheDayHistory' + ); + + $aClasses = array(); + + foreach ($classes as $class) { + $aClasses[] = PartKeepr::getEM()->getClassMetadata($class); + } + + $tool->dropSchema($aClasses); + } +} diff --git a/src/de/RaumZeitLabor/PartKeepr/Versions/Version20110818051810.php b/src/de/RaumZeitLabor/PartKeepr/Versions/Version20110818051810.php @@ -0,0 +1,48 @@ +<?php + +namespace DoctrineMigrations; + +use de\RaumZeitLabor\PartKeepr\PartKeepr; + +use Doctrine\DBAL\Migrations\AbstractMigration, + Doctrine\DBAL\Schema\Schema; + +/** + * Auto-generated Migration: Please modify to your need! + */ +class Version20110818051810 extends AbstractMigration +{ + public function up(Schema $schema) + { + $tool = new \Doctrine\ORM\Tools\SchemaTool(PartKeepr::getEM()); + + $classes = array( + 'de\RaumZeitLabor\PartKeepr\UserPreference\UserPreference' + ); + + $aClasses = array(); + + foreach ($classes as $class) { + $aClasses[] = PartKeepr::getEM()->getClassMetadata($class); + } + + $tool->updateSchema($aClasses, true); + } + + public function down(Schema $schema) + { + $tool = new \Doctrine\ORM\Tools\SchemaTool(PartKeepr::getEM()); + + $classes = array( + 'de\RaumZeitLabor\PartKeepr\UserPreference\UserPreference' + ); + + $aClasses = array(); + + foreach ($classes as $class) { + $aClasses[] = PartKeepr::getEM()->getClassMetadata($class); + } + + $tool->dropSchema($aClasses); + } +}