/* Monolith calendar widget. * - by Richard W.M. Jones * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, write to the Free * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * * $Id: ml_calendar_day.c,v 1.7 2003/02/22 15:34:25 rich Exp $ */ #include "config.h" #include #include #include #ifdef HAVE_TIME_H #include #endif #ifdef HAVE_STRING_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ml_calendar_lib.h" #include "ml_calendar_day.h" static void repaint (void *, ml_session, const char *, io_handle); struct ml_widget_operations calendar_day_ops = { repaint: repaint }; struct ml_calendar_day { struct ml_widget_operations *ops; pool pool; /* Pool for allocations. */ ml_session session; /* Current session. */ const char *conninfo; /* Database connection. */ ml_calendar calendar; /* Parent calendar. */ int yyyy, mm, dd; /* Current date. */ ml_iframe iframe; /* The iframe widget. */ ml_table_layout tbl; /* Top-level layout. */ ml_button b[24][2]; /* One button for each half hour of the day. */ /* This is the new event form: */ ml_form_select f_resid; /* Which calendar. */ vector f_resids; /* List of calendars. */ ml_form_select f_start; /* Start time. */ ml_form_select f_length; /* Length. */ ml_form_text f_subject; /* Subject (title). */ ml_form_textarea f_body; /* Body / note. */ /* The above fields are also used on the edit event form, plus these: */ int eventid; /* Event ID we are editing. */ ml_form_submit edit, delete; /* Which button was pressed. */ }; /* This is the parameter to the time_button_press function. */ struct time_button_data { int hh, mm; ml_calendar_day w; }; /* This is the parameter to the event_button_press function. */ struct event_button_data { int eventid; ml_calendar_day w; }; static pool cal_day_pool; /* Pool for global allocations. */ static vector start_times; /* List of start times (for forms). */ static vector lengths; /* List of lengths (for forms). */ static ml_text_label spacer; /* Used for spacing things out. */ /* The global start_times vector contains these. */ struct start_times_entry { const char *text; /* Text label. */ int hh, mm; /* Actual start time. */ }; /* The global lengths vector contains these. */ struct lengths_entry { const char *text; /* Text label. */ int hh, mm; /* Actual length in hours, mins. */ }; static void calendar_day_init (void) __attribute__ ((constructor)); static void calendar_day_stop (void) __attribute__ ((destructor)); static void make_window (ml_session session, void *vw); static void update_table (ml_calendar_day w); static void time_button_press (ml_session session, void *vdata); static void add_event (ml_session session, void *vw); static void event_button_press (ml_session session, void *vdata); static void edit_event (ml_session session, void *vw); static void calendar_day_init () { struct start_times_entry se; struct lengths_entry le; int hh, mm; cal_day_pool = new_subpool (global_pool); start_times = new_vector (cal_day_pool, struct start_times_entry); lengths = new_vector (cal_day_pool, struct lengths_entry); /* Populate the start_times array. */ for (hh = 0; hh < 24; ++hh) for (mm = 0; mm < 60; mm += 15) { se.text = psprintf (cal_day_pool, "%02d:%02d", hh, mm); se.hh = hh; se.mm = mm; vector_push_back (start_times, se); } /* Populate the lengths array. */ le.text = "¼ hour"; le.hh = 0; le.mm = 15; vector_push_back (lengths, le); le.text = "½ hour"; le.hh = 0; le.mm = 30; vector_push_back (lengths, le); le.text = "1 hour"; le.hh = 1; le.mm = 0; vector_push_back (lengths, le); le.text = "1½ hours"; le.hh = 1; le.mm = 30; vector_push_back (lengths, le); le.text = "2 hours"; le.hh = 2; le.mm = 0; vector_push_back (lengths, le); le.text = "3 hours"; le.hh = 3; le.mm = 0; vector_push_back (lengths, le); le.text = "4 hours"; le.hh = 4; le.mm = 0; vector_push_back (lengths, le); le.text = "5 hours"; le.hh = 5; le.mm = 0; vector_push_back (lengths, le); le.text = "6 hours"; le.hh = 6; le.mm = 0; vector_push_back (lengths, le); le.text = "7 hours"; le.hh = 7; le.mm = 0; vector_push_back (lengths, le); le.text = "8 hours"; le.hh = 8; le.mm = 0; vector_push_back (lengths, le); le.text = "9 hours"; le.hh = 9; le.mm = 0; vector_push_back (lengths, le); le.text = "10 hours"; le.hh = 10; le.mm = 0; vector_push_back (lengths, le); le.text = "11 hours"; le.hh = 11; le.mm = 0; vector_push_back (lengths, le); le.text = "12 hours"; le.hh = 12; le.mm = 0; vector_push_back (lengths, le); /* This is just used to space events out. */ spacer = new_ml_text_label (cal_day_pool, " "); } static void calendar_day_stop () { delete_pool (cal_day_pool); } ml_calendar_day new_ml_calendar_day (pool pool, ml_session session, const char *conninfo, ml_calendar calendar) { ml_calendar_day w = pmalloc (pool, sizeof *w); struct time_button_data *data; int hh, mm, i; w->ops = &calendar_day_ops; w->pool = pool; w->session = session; w->conninfo = conninfo; w->calendar = calendar; /* The parent calendar must call ml_calendar_day_set_date to set these * before using this widget. */ w->yyyy = w->mm = w->dd = -1; /* The widget is really an iframe. */ w->iframe = new_ml_iframe (pool, make_window, session, w, 0); ml_widget_set_property (w->iframe, "height", 500); ml_widget_set_property (w->iframe, "width", 400); /* Create the top-level widgets. */ w->tbl = new_ml_table_layout (pool, 48, 2); /* Create the buttons and populate them in the table widget. */ for (hh = 0; hh < 24; ++hh) for (mm = i = 0; mm <= 30; mm += 30, i++) { data = pmalloc (pool, sizeof *data); data->hh = hh; data->mm = mm; data->w = w; if (i == 0) w->b[hh][0] = new_ml_button (pool, psprintf (pool, "%02d:00", hh)); else w->b[hh][1] = new_ml_button (pool, "---"); ml_button_set_callback (w->b[hh][i], time_button_press, session, data); ml_button_set_popup (w->b[hh][i], "ml_calendar_day_add_event"); ml_button_set_popup_size (w->b[hh][i], 640, 300); ml_widget_set_property (w->b[hh][i], "button.style", "link"); if (hh >= 9 && hh < 18) ml_widget_set_property (w->b[hh][i], "class", "ml_calendar_hour"); else ml_widget_set_property (w->b[hh][i], "class", "ml_calendar_hour_off_peak"); ml_table_layout_pack (w->tbl, w->b[hh][i], hh*2+i, 0); } return w; } void ml_calendar_day_set_date (ml_calendar_day w, int yyyy, int mm, int dd) { w->yyyy = yyyy; w->mm = mm; w->dd = dd; update_table (w); } /* Create the internal window. * XXX This could be more efficiently written. It creates a window each * time the widget is redrawn. */ static void make_window (ml_session session, void *vw) { ml_calendar_day w = (ml_calendar_day) vw; ml_window win; win = new_ml_window (session, w->pool); ml_window_pack (win, w->tbl); ml_window_scroll_to (win, 0, 360); } /* This form is used for adding events to the calendar. */ static void time_button_press (ml_session session, void *vdata) { struct time_button_data *data = (struct time_button_data *) vdata; ml_calendar_day w = data->w; db_handle dbh; st_handle sth; ml_window win; ml_form form; ml_form_layout tbl; ml_form_submit submit; int i, resid; const char *resname; win = new_ml_window (w->session, w->pool); form = new_ml_form (w->pool); tbl = new_ml_form_layout (w->pool); /* Set the callback on the form. */ ml_form_set_callback (form, add_event, session, w); ml_widget_set_property (form, "method", "GET"); /* Pull out the list of calendar names from the database, and * use this to populate the calendar drop-down menu. */ dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS); w->f_resid = new_ml_form_select (w->pool, form); w->f_resids = new_vector (w->pool, int); sth = st_prepare_cached (dbh, "select c.resid, r.name from ml_calendar c, ml_resources r " "where c.resid in (@) and c.resid = r.resid order by 2", DBI_VECTOR_INT); st_execute (sth, _ml_calendar_get_calendars (w->calendar)); st_bind (sth, 0, resid, DBI_INT); st_bind (sth, 1, resname, DBI_STRING); while (st_fetch (sth)) { vector_push_back (w->f_resids, resid); ml_form_select_push_back (w->f_resid, resname); /* XXX Colours. */ } ml_form_layout_pack (tbl, "Calendar: ", w->f_resid); put_db_handle (dbh); /* Start time. */ w->f_start = new_ml_form_select (w->pool, form); for (i = 0; i < vector_size (start_times); ++i) { struct start_times_entry se; vector_get (start_times, i, se); ml_form_select_push_back (w->f_start, se.text); if (se.hh == data->hh && se.mm == data->mm) ml_form_select_set_selection (w->f_start, ml_form_select_size (w->f_start) - 1); } ml_form_layout_pack (tbl, "Start time: ", w->f_start); /* Length. */ w->f_length = new_ml_form_select (w->pool, form); for (i = 0; i < vector_size (lengths); ++i) { struct lengths_entry le; vector_get (lengths, i, le); ml_form_select_push_back (w->f_length, le.text); } ml_form_select_set_selection (w->f_length, 1); ml_form_layout_pack (tbl, "Length: ", w->f_length); /* Subject/title. */ w->f_subject = new_ml_form_text (w->pool, form); ml_widget_set_property (w->f_subject, "form.text.size", 50); ml_form_layout_pack (tbl, "Title: ", w->f_subject); /* Body. */ w->f_body = new_ml_form_textarea (w->pool, form, 5, 50); ml_form_layout_pack (tbl, "Notes: ", w->f_body); /* Submit button. */ /* XXX Cancel button. */ submit = new_ml_form_submit (w->pool, form, "Save"); ml_form_layout_pack (tbl, 0, submit); /* Pack everything into the window. */ ml_form_pack (form, tbl); ml_window_pack (win, form); } /* When the form is submitted to save a new event, this function is called. */ static void add_event (ml_session session, void *vw) { ml_calendar_day w = (ml_calendar_day) vw; int userid; const char *subject, *body; int si, li; int i, resid; struct start_times_entry se; struct lengths_entry le; db_handle dbh; st_handle sth; /* Only logged-in users can post. */ /* XXX Resource-level security - later. */ userid = ml_session_userid (session); if (!userid) { ml_error_window (w->pool, session, "You must be logged in to add events to the calendar.", ML_DIALOG_CLOSE_BUTTON); return; } /* Verify that the user has at least given a subject line. If not, just * return, which redisplays the form. */ subject = ml_form_input_get_value (w->f_subject); if (!subject || strlen (subject) == 0) return; /* Verify the remaining fields are in range. */ i = ml_form_select_get_selection (w->f_resid); si = ml_form_select_get_selection (w->f_start); li = ml_form_select_get_selection (w->f_length); if (i < 0 || i >= vector_size (w->f_resids) || si < 0 || si >= vector_size (start_times) || li < 0 || li >= vector_size (lengths)) return; /* Pull the other fields from the form. */ body = ml_form_input_get_value (w->f_body); vector_get (w->f_resids, i, resid); vector_get (start_times, si, se); vector_get (lengths, li, le); /* Save it in the database. */ dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS); sth = st_prepare_cached (dbh, "insert into ml_calendar_events (resid, start_time, length, subject, " "body, author, original_ip) " "values (?, ?, ?, ?, ?, ?, ?)", DBI_INT, DBI_STRING, DBI_STRING, DBI_STRING, DBI_STRING, DBI_INT, DBI_STRING); st_execute (sth, resid, psprintf (w->pool, "%04d/%02d/%02d %02d:%02d:00", w->yyyy, w->mm, w->dd, se.hh, se.mm), psprintf (w->pool, "%d hours %d mins", le.hh, le.mm), subject, body, userid, ml_session_get_peernamestr (session)); /* Commit change to the database. */ db_commit (dbh); put_db_handle (dbh); /* Print confirmation page. */ ml_ok_window (w->pool, session, "That event was added to the calendar.", ML_DIALOG_CLOSE_BUTTON|ML_DIALOG_CLOSE_RELOAD_OPENER); update_table (w); } static void event_button_press (ml_session session, void *vdata) { struct event_button_data *data = (struct event_button_data *) vdata; ml_calendar_day w = data->w; db_handle dbh; st_handle sth; ml_window win; ml_form form; ml_form_layout tbl; ml_flow_layout buttons; int i, t, resid; const char *resname, *subject, *body; struct dbi_timestamp start_time; struct dbi_interval length; dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS); /* Pull out the current values of the fields from the database. */ sth = st_prepare_cached (dbh, "select resid, start_time, length, subject, body " " from ml_calendar_events " " where id = ? and resid in (@)", DBI_INT, DBI_VECTOR_INT); st_execute (sth, data->eventid, _ml_calendar_get_calendars (w->calendar)); st_bind (sth, 0, resid, DBI_INT); st_bind (sth, 1, start_time, DBI_TIMESTAMP); st_bind (sth, 2, length, DBI_INTERVAL); st_bind (sth, 3, subject, DBI_STRING); st_bind (sth, 4, body, DBI_STRING); if (!st_fetch (sth)) { ml_error_window (w->pool, session, "I cannot find that calendar event. Perhaps it has " "been deleted by another user, or it is in a calendar " "which you can no longer see.", ML_DIALOG_CLOSE_BUTTON); return; } /* Save the event ID in the widget structure. We need it in edit_event. */ w->eventid = data->eventid; win = new_ml_window (session, w->pool); form = new_ml_form (w->pool); tbl = new_ml_form_layout (w->pool); /* Set the callback on the form. */ ml_form_set_callback (form, edit_event, session, w); ml_widget_set_property (form, "method", "GET"); /* Pull out the list of calendar names from the database, and * use this to populate the calendar drop-down menu. */ w->f_resid = new_ml_form_select (w->pool, form); w->f_resids = new_vector (w->pool, int); sth = st_prepare_cached (dbh, "select c.resid, r.name from ml_calendar c, ml_resources r " "where c.resid in (@) and c.resid = r.resid order by 2", DBI_VECTOR_INT); st_execute (sth, _ml_calendar_get_calendars (w->calendar)); st_bind (sth, 0, t, DBI_INT); st_bind (sth, 1, resname, DBI_STRING); while (st_fetch (sth)) { vector_push_back (w->f_resids, resid); ml_form_select_push_back (w->f_resid, resname); /* XXX Colours. */ if (t == resid) ml_form_select_set_selection (w->f_resid, ml_form_select_size (w->f_resid) - 1); } ml_form_layout_pack (tbl, "Calendar: ", w->f_resid); /* Start time. */ w->f_start = new_ml_form_select (w->pool, form); for (i = 0; i < vector_size (start_times); ++i) { struct start_times_entry se; vector_get (start_times, i, se); ml_form_select_push_back (w->f_start, se.text); /* XXX This doesn't work for events spanning across day boundaries. */ if (se.hh == start_time.hour && se.mm == start_time.min) ml_form_select_set_selection (w->f_start, ml_form_select_size (w->f_start) - 1); } ml_form_layout_pack (tbl, "Start time: ", w->f_start); /* Length. */ w->f_length = new_ml_form_select (w->pool, form); for (i = 0; i < vector_size (lengths); ++i) { struct lengths_entry le; vector_get (lengths, i, le); ml_form_select_push_back (w->f_length, le.text); if (le.hh == length.hours && le.mm == length.mins) ml_form_select_set_selection (w->f_length, ml_form_select_size (w->f_length) - 1); } ml_form_layout_pack (tbl, "Length: ", w->f_length); /* Subject/title. */ w->f_subject = new_ml_form_text (w->pool, form); ml_widget_set_property (w->f_subject, "form.text.size", 50); if (subject) ml_form_input_set_value (w->f_subject, subject); ml_form_layout_pack (tbl, "Title: ", w->f_subject); /* Body. */ w->f_body = new_ml_form_textarea (w->pool, form, 5, 50); if (body) ml_form_input_set_value (w->f_body, body); ml_form_layout_pack (tbl, "Notes: ", w->f_body); /* Submit buttons. */ buttons = new_ml_flow_layout (w->pool); w->edit = new_ml_form_submit (w->pool, form, "Edit"); ml_flow_layout_pack (buttons, w->edit); w->delete = new_ml_form_submit (w->pool, form, "Delete"); ml_flow_layout_pack (buttons, w->delete); ml_form_layout_pack (tbl, 0, buttons); /* Pack everything into the window. */ ml_form_pack (form, tbl); ml_window_pack (win, form); } /* When the edit or delete button is pressed, this function is called. */ static void edit_event (ml_session session, void *vw) { ml_calendar_day w = (ml_calendar_day) vw; int userid; int edit_pressed, delete_pressed; const char *s, *subject, *body; int si, li; int i, resid; struct start_times_entry se; struct lengths_entry le; db_handle dbh; st_handle sth; /* Only logged-in users can post. */ /* XXX Resource-level security - later. */ userid = ml_session_userid (session); if (!userid) { ml_error_window (w->pool, session, "You must be logged in to change " "events in the calendar.", ML_DIALOG_CLOSE_BUTTON); return; } dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS); /* Which submit button was pressed? */ s = ml_form_input_get_value (w->edit); edit_pressed = s && strlen (s) > 0; s = ml_form_input_get_value (w->delete); delete_pressed = s && strlen (s) > 0; assert ((edit_pressed || delete_pressed) && !(edit_pressed && delete_pressed)); /* Delete button pressed? */ if (delete_pressed) { sth = st_prepare_cached (dbh, "delete from ml_calendar_events " "where id = ? and resid in (@)", DBI_INT, DBI_VECTOR_INT); st_execute (sth, w->eventid, _ml_calendar_get_calendars (w->calendar)); /* Commit change to the database. */ db_commit (dbh); put_db_handle (dbh); /* Print confirmation page. */ ml_ok_window (w->pool, session, "That event was deleted.", ML_DIALOG_CLOSE_BUTTON|ML_DIALOG_CLOSE_RELOAD_OPENER); update_table (w); return; } /* Verify that the user has at least given a subject line. If not, just * return, which redisplays the form. */ subject = ml_form_input_get_value (w->f_subject); if (!subject || strlen (subject) == 0) return; /* Verify the remaining fields are in range. */ i = ml_form_select_get_selection (w->f_resid); si = ml_form_select_get_selection (w->f_start); li = ml_form_select_get_selection (w->f_length); if (i < 0 || i >= vector_size (w->f_resids) || si < 0 || si >= vector_size (start_times) || li < 0 || li >= vector_size (lengths)) return; /* Pull the other fields from the form. */ body = ml_form_input_get_value (w->f_body); vector_get (w->f_resids, i, resid); vector_get (start_times, si, se); vector_get (lengths, li, le); /* Update the database. */ sth = st_prepare_cached (dbh, "update ml_calendar_events " "set resid = ?, start_time = ?, length = ?, subject = ?, body = ?, " "last_modified_date = current_timestamp " "where id = ? and resid in (@)", DBI_INT, DBI_STRING, DBI_STRING, DBI_STRING, DBI_STRING, DBI_INT, DBI_VECTOR_INT); st_execute (sth, resid, psprintf (w->pool, "%04d/%02d/%02d %02d:%02d:00", w->yyyy, w->mm, w->dd, se.hh, se.mm), psprintf (w->pool, "%d hours %d mins", le.hh, le.mm), subject, body, w->eventid, _ml_calendar_get_calendars (w->calendar)); /* Commit change to the database. */ db_commit (dbh); put_db_handle (dbh); /* Print confirmation page. */ ml_ok_window (w->pool, session, "That event was edited.", ML_DIALOG_CLOSE_BUTTON|ML_DIALOG_CLOSE_RELOAD_OPENER); update_table (w); } /* Update the table from the database. */ static void update_table (ml_calendar_day w) { db_handle dbh; st_handle sth; const vector resids = _ml_calendar_get_calendars (w->calendar); int eventid, resid, start_time_hh, start_time_mm, end_time_hh, end_time_mm; const char *subject, *body, *start_of_day; ml_button b; ml_flow_layout flow[48] = { 0 }; int start, end, i; struct event_button_data *data; const char *title; dbh = get_db_handle (w->conninfo, DBI_THROW_ERRORS); /* Read the events from the database. */ /* SQL/PostgreSQL could do with having some way to name subexpressions * in a query ... */ start_of_day = psprintf (w->pool, "%04d/%02d/%02d 00:00:00", w->yyyy, w->mm, w->dd); sth = st_prepare_cached (dbh, "select id, resid, subject, body, " " date_part ('hour', start_time - date ?), " " date_part ('min', start_time - date ?), " " date_part ('hour', start_time + length - date ?), " " date_part ('min', start_time + length - date ?) " " from ml_calendar_events " " where resid in (@) " " and length <> '1 day' " " and start_time + length >= date ? " " and start_time < date ? + interval '1 day'" " order by 1", DBI_STRING, DBI_STRING, DBI_STRING, DBI_STRING, DBI_VECTOR_INT, DBI_STRING, DBI_STRING); st_execute (sth, start_of_day, start_of_day, start_of_day, start_of_day, resids, start_of_day, start_of_day); st_bind (sth, 0, eventid, DBI_INT); st_bind (sth, 1, resid, DBI_INT); st_bind (sth, 2, subject, DBI_STRING); st_bind (sth, 3, body, DBI_STRING); st_bind (sth, 4, start_time_hh, DBI_INT); st_bind (sth, 5, start_time_mm, DBI_INT); st_bind (sth, 6, end_time_hh, DBI_INT); st_bind (sth, 7, end_time_mm, DBI_INT); while (st_fetch (sth)) { data = pmalloc (w->pool, sizeof *data); data->eventid = eventid; data->w = w; /* Create the tooltip (title) for this button. */ title = psprintf (w->pool, "%02d:%02d - %02d:%02d: %s", start_time_hh, start_time_mm, end_time_hh, end_time_mm, body ? body : ""); /* Create the button. */ b = new_ml_button (w->pool, subject); ml_widget_set_property (b, "button.style", "link"); ml_widget_set_property (b, "color", _ml_calendar_colour (resid)); ml_widget_set_property (b, "title", title); ml_button_set_callback (b, event_button_press, w->session, data); ml_button_set_popup (b, "ml_calendar_day_edit_event"); ml_button_set_popup_size (b, 640, 300); /* Work out the start / end indexes. */ if (start_time_hh < 0) start = 0; else { assert (start_time_hh <= 24); assert (0 <= start_time_mm && start_time_mm < 60); start = start_time_hh * 2 + start_time_mm / 30; } if (end_time_hh >= 24) end = 48; else { assert (end_time_hh >= 0); assert (0 <= end_time_mm && end_time_mm < 60); end = end_time_hh * 2 + end_time_mm / 30; } /* Make sure that short events are displayed. */ if (start == end) { assert (end < 48); end++; } #if 0 fprintf (stderr, "fetched event #%d for day %04d/%02d/%02d: starts at %02d:%02d " "ends at %02d:%02d startbox %d endbox %d\n", eventid, w->yyyy, w->mm, w->dd, start_time_hh, start_time_mm, end_time_hh, end_time_mm, start, end); #endif /* Pack it into the appropriate flow widget. */ for (i = start; i < end; ++i) { if (flow[i] == 0) flow[i] = new_ml_flow_layout (w->pool); ml_flow_layout_push_back (flow[i], b); ml_flow_layout_push_back (flow[i], spacer); } } /* Copy the flow widgets into the table's second column. */ for (i = 0; i < 48; ++i) ml_table_layout_pack (w->tbl, flow[i], i, 1); put_db_handle (dbh); } static void repaint (void *vw, ml_session session, const char *windowid, io_handle io) { ml_calendar_day w = (ml_calendar_day) vw; ml_widget_repaint (w->iframe, session, windowid, io); }