See ChangeLog.

Monotone-Parent: 4822220bb92ce7ebcbd47af90d5aad37bb750ed0
Monotone-Revision: a7e7282f1911ceaff2a7f4f96320407cba1085cc

Monotone-Author: flachapelle@inverse.ca
Monotone-Date: 2011-10-03T19:32:37
Monotone-Branch: ca.inverse.sogo
maint-2.0.2
Francis Lachapelle 2011-10-03 19:32:37 +00:00
parent 3234ee9a92
commit 10e3850d49
11 changed files with 234 additions and 34 deletions

View File

@ -1,3 +1,20 @@
2011-10-03 Francis Lachapelle <flachapelle@inverse.ca>
* UI/WebServerResources/generic.js (snoozeAlarm): new function to
snooze an alarm for a specific number of minutes.
(showAlarmCallback): offer the user to snooze the alarm right
after triggering the browser-native alert.
(showSelectDialog): new function that construct a dialog box with
a popup menu.
* UI/Scheduler/UIxAppointmentEditor.m (-viewAction): accepts the
new form parameter snoozeAlarm, which delays the alarm to the
specified number of minutes.
* SoObjects/Appointments/SOGoCalendarComponent.m (-snoozeAlarm:):
new method to set the next alarm of the event in a specified
number of minutes. Only affect the quick table, not the original vCalendar.
2011-09-30 Wolfgang Sourdeau <wsourdeau@inverse.ca>
* OpenChange/MAPIStoreMailMessage.m (-getMessageData:inMemCtx:):

2
NEWS
View File

@ -5,6 +5,8 @@ New Features
creating an event or a task (selected, personal, first enabled)
- new user defaults SOGoBusyOffHours to specify if off-hours should be
automatically added to the free-busy information
- new indicator in the link banner when a vacation message (auto-reply) is active
- new snooze function for events alarms in Web interface
Enhancements
- phone numbers in the contacts web module are now links (tel:)

View File

@ -1,3 +1,10 @@
2011-10-03 Francis Lachapelle <flachapelle@inverse.ca>
* GCSFolder.m (-updateQuickFields:whereColumn:isEqualTo:): new
method to update some fields of the quick table matching the
single specified condition.
(-_quickTableEntity): the method was not returning all the fields.
2011-05-30 Wolfgang Sourdeau <wsourdeau@inverse.ca>
* GCSFolder.m (-lastModificationDate): new method that returns the

View File

@ -126,6 +126,10 @@
- (NSException *) deleteFolder;
- (NSException *) updateQuickFields: (NSDictionary *) _fields
whereColumn: (NSString *) _colname
isEqualTo: (id) _value;
- (NSArray *) fetchFields: (NSArray *) _flds
fetchSpecification: (EOFetchSpecification *) _fs;
- (NSArray *) fetchFields: (NSArray *) fields

View File

@ -139,10 +139,13 @@ static NSArray *contentFieldNames = nil;
folderManager:nil];
}
- (id)initWithPath:(NSString *)_path primaryKey:(id)_folderId
folderTypeName:(NSString *)_ftname folderType:(GCSFolderType *)_ftype
location:(NSURL *)_loc quickLocation:(NSURL *)_qloc
folderManager:(GCSFolderManager *)_fm
- (id)initWithPath:(NSString *)_path
primaryKey:(id)_folderId
folderTypeName:(NSString *)_ftname
folderType:(GCSFolderType *)_ftype
location:(NSURL *)_loc
quickLocation:(NSURL *)_qloc
folderManager:(GCSFolderManager *)_fm
{
return [self initWithPath:_path primaryKey:_folderId folderTypeName:_ftname
folderType:_ftype location:_loc quickLocation:_qloc
@ -664,9 +667,9 @@ static NSArray *contentFieldNames = nil;
}
- (NSString *)_generateUpdateStatementForRow:(NSDictionary *)_row
tableName:(NSString *)_table
whereColumn:(NSString *)_colname isEqualTo:(id)_value
andColumn:(NSString *)_colname2 isEqualTo:(id)_value2
tableName:(NSString *)_table
whereColumn:(NSString *)_colname isEqualTo:(id)_value
andColumn:(NSString *)_colname2 isEqualTo:(id)_value2
{
// TODO: move to NSDictionary category?
NSMutableString *sql;
@ -744,7 +747,23 @@ static NSArray *contentFieldNames = nil;
- (EOEntity *) _quickTableEntity
{
return [self _entityWithName: [self quickTableName]];
EOEntity *entity;
EOAttribute *attribute;
GCSFieldInfo *field;
NSEnumerator *fields;
NSString *fieldName;
entity = [self _entityWithName: [self quickTableName]];
fields = [quickFieldNames objectEnumerator];
while ((fieldName = [fields nextObject]))
{
attribute = AUTORELEASE([[EOAttribute alloc] init]);
[attribute setName: fieldName];
[attribute setColumnName: fieldName];
[entity addAttribute: attribute];
}
return entity;
}
- (EOSQLQualifier *) _qualifierUsingWhereColumn:(NSString *)_colname
@ -1171,6 +1190,36 @@ static NSArray *contentFieldNames = nil;
return nil;
}
- (NSException *) updateQuickFields: (NSDictionary *) _fields
whereColumn: (NSString *) _colname
isEqualTo: (id) _value
{
EOAdaptorChannel *quickChannel;
NSException *error;
quickChannel = [self acquireQuickChannel];
[[quickChannel adaptorContext] beginTransaction];
error = [quickChannel updateRowX: _fields
describedByQualifier: [self _qualifierUsingWhereColumn: _colname
isEqualTo: _value andColumn: nil isEqualTo: nil
entity: [self _quickTableEntity]]];
if (error)
{
[[quickChannel adaptorContext] rollbackTransaction];
[self logWithFormat:
@"ERROR(%s): cannot update content : %@", __PRETTY_FUNCTION__, error];
}
else
{
[[quickChannel adaptorContext] commitTransaction];
}
[self releaseChannel: quickChannel];
return error;
}
/* SQL generation */
/* fetching */

View File

@ -88,6 +88,8 @@
- (SOGoComponentOccurence *) occurence: (iCalRepeatableEntityObject *) component;
- (iCalRepeatableEntityObject *) newOccurenceWithID: (NSString *) recID;
- (void) snoozeAlarm: (unsigned int) minutes;
@end
#endif /* SOGOCALENDARCOMPONENT_H */

View File

@ -24,6 +24,7 @@
#import <Foundation/NSDictionary.h>
#import <Foundation/NSEnumerator.h>
#import <Foundation/NSString.h>
#import <Foundation/NSValue.h>
#import <NGObjWeb/NSException+HTTP.h>
#import <NGObjWeb/SoSecurityManager.h>
@ -41,6 +42,7 @@
#import <NGMime/NGMimeBodyPart.h>
#import <NGMime/NGMimeMultipartBody.h>
#import <NGMail/NGMimeMessage.h>
#import <GDLContentStore/GCSFolder.h>
#import <SOGo/NSCalendarDate+SOGo.h>
#import <SOGo/NSDictionary+Utilities.h>
@ -1211,6 +1213,29 @@ static inline BOOL _occurenceHasID (iCalRepeatableEntityObject *occurence,
return roles;
}
- (void) snoozeAlarm: (unsigned int) minutes
{
NSDictionary *quickFields;
GCSFolder *folder;
unsigned int nextAlarm;
folder = [[self container] ocsFolder];
if (!folder)
{
[self errorWithFormat:@"(%s): missing folder for fetch!",
__PRETTY_FUNCTION__];
return;
}
nextAlarm = [[NSCalendarDate calendarDate] timeIntervalSince1970] + minutes * 60;
quickFields = [NSDictionary dictionaryWithObject: [NSNumber numberWithInt: nextAlarm]
forKey: @"c_nextalarm"];
[folder updateQuickFields: quickFields
whereColumn: @"c_name"
isEqualTo: nameInContainer];
}
/* SOGoComponentOccurence protocol */
- (iCalRepeatableEntityObject *) occurence

View File

@ -242,8 +242,7 @@
NSString *webstatus;
anAlarm = [[self alarms] objectAtIndex: 0];
if ([[anAlarm action] caseInsensitiveCompare: @"DISPLAY"]
== NSOrderedSame)
if ([[anAlarm action] caseInsensitiveCompare: @"DISPLAY"] == NSOrderedSame)
{
webstatus = [[anAlarm trigger] value: 0 ofAttribute: @"x-webstatus"];
if (!webstatus

View File

@ -461,7 +461,8 @@
NSTimeZone *timeZone;
SOGoUserDefaults *ud;
SOGoCalendarComponent *co;
BOOL resetAlarm;
BOOL resetAlarm;
unsigned int snoozeAlarm;
[self event];
@ -480,17 +481,27 @@
[componentCalendar retain];
}
resetAlarm = [[[context request] formValueForKey: @"resetAlarm"] boolValue];
if (resetAlarm && [event hasAlarms] && ![event hasRecurrenceRules])
if ([event hasAlarms] && ![event hasRecurrenceRules])
{
iCalAlarm *anAlarm;
iCalTrigger *aTrigger;
anAlarm = [[event alarms] objectAtIndex: 0];
aTrigger = [anAlarm trigger];
[aTrigger setValue: 0 ofAttribute: @"x-webstatus" to: @"triggered"];
[co saveComponent: event];
resetAlarm = [[[context request] formValueForKey: @"resetAlarm"] boolValue];
snoozeAlarm = [[[context request] formValueForKey: @"snoozeAlarm"] intValue];
if (resetAlarm)
{
iCalTrigger *aTrigger;
anAlarm = [[event alarms] objectAtIndex: 0];
aTrigger = [anAlarm trigger];
[aTrigger setValue: 0 ofAttribute: @"x-webstatus" to: @"triggered"];
[co saveComponent: event];
}
else if (snoozeAlarm)
{
anAlarm = [[event alarms] objectAtIndex: 0];
if ([[anAlarm action] caseInsensitiveCompare: @"DISPLAY"] == NSOrderedSame)
[co snoozeAlarm: snoozeAlarm];
}
}
data = [NSDictionary dictionaryWithObjectsAndKeys:

View File

@ -291,12 +291,13 @@ UL.choiceMenu LI._chosen:hover
.menu LI.submenu
{ background-image: url('arrow-right.png'); }
.menu LI[class~="disabled"].submenu
{ background-image: url('submenu-disabled.gif') !important; }
.menu LI.submenu:hover, .menu LI.submenu-selected
{ background-image: url('arrow-right.png') !important; }
.menu LI[class~="disabled"].submenu,
.menu LI[class~="disabled"].submenu:hover
{ background-image: url('submenu-disabled.gif') !important; }
DIV#logConsole
{ position: absolute;
overflow: auto;

View File

@ -28,7 +28,6 @@ var menus = new Array();
var search = {};
var sorting = {};
var dialogs = {};
var dialogActive = false;
var dialogsStack = new Array();
var lastClickedRow = -1;
@ -1291,19 +1290,33 @@ function triggerNextAlarm() {
}
}
function snoozeAlarm(url) {
url += "?snoozeAlarm=" + this.value;
triggerAjaxRequest(url, snoozeAlarmCallback);
disposeDialog();
}
function snoozeAlarmCallback(http) {
if (http.readyState == 4
&& http.status == 200) {
refreshAlarms();
}
}
function showAlarm(url) {
url = UserFolderURL + "Calendar/" + url + "/view?resetAlarm=yes";
url = UserFolderURL + "Calendar/" + url + "/view";
if (document.viewAlarmAjaxRequest) {
document.viewAlarmAjaxRequest.aborted = true;
document.viewAlarmAjaxRequest.abort();
}
document.viewAlarmAjaxRequest = triggerAjaxRequest(url, showAlarmCallback);
document.viewAlarmAjaxRequest = triggerAjaxRequest(url + "?resetAlarm=yes", showAlarmCallback, url);
}
function showAlarmCallback(http) {
if (http.readyState == 4
&& http.status == 200) {
if (http.responseText.length) {
var url = http.callbackData;
var data = http.responseText.evalJSON(true);
var msg = _("Reminder:") + " " + data["summary"] + "\n";
if (data["startDate"]) {
@ -1324,6 +1337,15 @@ function showAlarmCallback(http) {
msg += "\n\n" + data["description"];
window.alert(msg);
showSelectDialog(data["summary"], _('Snooze for '),
{ '5': _('5 minutes'),
'10': _('10 minutes'),
'15': _('15 minutes'),
'30': _('30 minutes'),
'45': _('45 minutes'),
'60': _('1 hour') }, _('OK'),
snoozeAlarm, url,
'10');
}
else
log("showAlarmCallback ajax error: no data received");
@ -1801,9 +1823,8 @@ function createButton(id, caption, action) {
function showAlertDialog(label) {
var div = $("bgDialogDiv");
if (div && div.visible() && div.getOpacity() > 0) {
dialogsStack.push(label);
}
if (div && div.visible() && div.getOpacity() > 0)
dialogsStack.push(_showAlertDialog.bind(this, label));
else
_showAlertDialog(label);
}
@ -1830,6 +1851,14 @@ function _showAlertDialog(label) {
}
function showConfirmDialog(title, label, callbackYes, callbackNo) {
var div = $("bgDialogDiv");
if (div && div.visible() && div.getOpacity() > 0)
dialogsStack.push(_showConfirmDialog.bind(this, title, label, callbackYes, callbackNo));
else
_showConfirmDialog(title, label, callbackYes, callbackNo);
}
function _showConfirmDialog(title, label, callbackYes, callbackNo) {
var key = title;
if (Object.isElement(label)) key += label.allTextContent();
else key += label;
@ -1860,6 +1889,14 @@ function showConfirmDialog(title, label, callbackYes, callbackNo) {
}
function showPromptDialog(title, label, callback, defaultValue) {
var div = $("bgDialogDiv");
if (div && div.visible() && div.getOpacity() > 0)
dialogsStack.push(_showPromptDialog.bind(this, title, label, callback, defaultValue));
else
_showPromptDialog(title, label, callback, defaultValue);
}
function _showPromptDialog(title, label, callback, defaultValue) {
var dialog = dialogs[title+label];
v = defaultValue?defaultValue:"";
if (dialog) {
@ -1887,8 +1924,54 @@ function showPromptDialog(title, label, callback, defaultValue) {
document.body.appendChild(dialog);
dialogs[title+label] = dialog;
}
dialog.appear({ duration: 0.2,
afterFinish: function () { dialog.down("input").focus(); } });
}
function showSelectDialog(title, label, options, button, callbackFcn, callbackArg, defaultValue) {
var div = $("bgDialogDiv");
if (div && div.visible() && div.getOpacity() > 0) {
dialogsStack.push(_showSelectDialog.bind(this, title, label, options, button, callbackFcn, callbackArg, defaultValue));
}
else
_showSelectDialog(title, label, options, button, callbackFcn, callbackArg, defaultValue);
}
function _showSelectDialog(title, label, options, button, callbackFcn, callbackArg, defaultValue) {
var dialog = dialogs[title+label];
if (dialog) {
$("bgDialogDiv").show();
}
else {
var fields = createElement("p", null, []);
fields.appendChild(document.createTextNode(label));
var select = createElement("select"); //, null, null, { cname: name } );
fields.appendChild(select);
var values = $H(options).keys();
for (var i = 0; i < values.length; i++) {
var option = createElement("option", null, null,
{ value: values[i] }, null, select);
option.appendChild(document.createTextNode(options[values[i]]));
}
fields.appendChild(createElement("br"));
fields.appendChild(createButton(null,
button,
callbackFcn.bind(select, callbackArg)));
fields.appendChild(createButton(null,
_("Cancel"),
disposeDialog));
dialog = createDialog(null,
title,
null,
fields,
"none");
document.body.appendChild(dialog);
dialogs[title+label] = dialog;
}
if (defaultValue)
defaultOption = dialog.down('option[value="'+defaultValue+'"]').selected = true;
dialog.appear({ duration: 0.2 });
dialog.down("input").focus();
}
function disposeDialog() {
@ -1898,9 +1981,9 @@ function disposeDialog() {
});
if (dialogsStack.length > 0) {
// Show the next dialog box
var label = dialogsStack.first();
var dialogFcn = dialogsStack.first();
dialogsStack.splice(0, 1);
_showAlertDialog.delay(0.2, label);
dialogFcn.delay(0.2);
}
else {
var bgFade = Effect.Fade('bgDialogDiv', { duration: 0.2 });
@ -1916,9 +1999,9 @@ function _disposeDialog(bgEffect) {
bgEffect.cancel();
div.show();
div.appear({ duration: 0.2, to: 0.3 });
var label = dialogsStack.first();
var dialogFcn = dialogsStack.first();
dialogsStack.splice(0, 1);
_showAlertDialog(label);
dialogFcn();
}
}