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:
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);
+ }
+}