Monthly calendar view.
authorrich <rich>
Thu, 7 Oct 2004 16:54:24 +0000 (16:54 +0000)
committerrich <rich>
Thu, 7 Oct 2004 16:54:24 +0000 (16:54 +0000)
12 files changed:
html/_css/standard.css
scripts/.depend
scripts/Makefile
scripts/cocanwiki.ml
scripts/cocanwiki_date.ml
scripts/cocanwiki_ext_calendar.ml [new file with mode: 0644]
scripts/page.ml
scripts/stats.ml
templates/calendar_day.html [new file with mode: 0644]
templates/calendar_month.html [new file with mode: 0644]
templates/calendar_year.html [new file with mode: 0644]
templates/page.html

index b2fa24e..70c16d7 100644 (file)
@@ -1,5 +1,5 @@
 /* Stylesheet for COCANWIKI.
- * $Id: standard.css,v 1.7 2004/10/05 14:51:11 rich Exp $
+ * $Id: standard.css,v 1.8 2004/10/07 16:54:24 rich Exp $
  */
 
 body {
@@ -275,3 +275,74 @@ div#search_div {
   top: 0.95em;
   text-align: right;
 }
+
+/* WikiForms - hide the edit link for the top section. */
+div#form_div p.edit_link {
+  display: none;
+}
+
+/* Calendar extension. */
+table.cal_month {
+  border-collapse: collapse;
+  border: 1px solid #eee;
+  width: 90%;
+  margin-left: 5%;
+  margin-right: 5%;
+}
+
+table.cal_month th.cal_month_header {
+  background-color: #eef;
+}
+
+table.cal_month th.cal_month_header a.cal_month_left {
+  margin-right: 2em;
+}
+
+table.cal_month th.cal_month_header a.cal_month_right {
+  margin-left: 2em;
+}
+
+table.cal_month td.cal_month_events {
+}
+
+table.cal_month td.cal_month_events ul {
+  list-style: none;
+  padding: 0px;
+  margin: 0px;
+}
+
+table.cal_month td.cal_month_events li {
+  display: inline;
+  margin-right: 2em;
+}
+
+table.cal_month tr.cal_month_row {
+  border: 1px solid #eef;
+  font-size: 0.7em;
+}
+
+table.cal_month tr.cal_month_weekend {
+  background-color: #fef;
+}
+
+table.cal_month tr.cal_month_row th {
+  text-align: right;
+  background-color: #eef;
+  border: 1px solid #fff;
+  padding-right: 1em;
+  width: 4em;
+}
+
+table.cal_month tr.cal_month_row td {
+}
+
+table.cal_month tr.cal_month_row ul {
+  list-style: none;
+  padding: 0px;
+  margin: 0px;
+}
+
+table.cal_month tr.cal_month_row li {
+  display: inline;
+  margin-right: 2em;
+}
index 87ebd07..dd737e6 100644 (file)
@@ -10,6 +10,10 @@ cocanwiki_create_host.cmo: cocanwiki_create_host.cmi
 cocanwiki_create_host.cmx: cocanwiki_create_host.cmi 
 cocanwiki_diff.cmo: cocanwiki_files.cmo 
 cocanwiki_diff.cmx: cocanwiki_files.cmx 
+cocanwiki_ext_calendar.cmo: cocanwiki.cmo cocanwiki_date.cmo \
+    cocanwiki_strings.cmo cocanwiki_template.cmi 
+cocanwiki_ext_calendar.cmx: cocanwiki.cmx cocanwiki_date.cmx \
+    cocanwiki_strings.cmx cocanwiki_template.cmx 
 cocanwiki_images.cmo: cocanwiki_files.cmo cocanwiki_strings.cmo \
     cocanwiki_images.cmi 
 cocanwiki_images.cmx: cocanwiki_files.cmx cocanwiki_strings.cmx \
index 8022749..bb56bd0 100644 (file)
@@ -1,10 +1,11 @@
 # Makefile for COCANWIKI.
-# $Id: Makefile,v 1.32 2004/10/06 10:34:29 rich Exp $
+# $Id: Makefile,v 1.33 2004/10/07 16:54:24 rich Exp $
 
 include ../Makefile.config
 
 OCAMLC := ocamlc
-OCAMLCFLAGS := -w s -I +apache -I +pcre -I +dbi -I +extlib
+# XXX Move GregorianDate out of +merjis.
+OCAMLCFLAGS := -w s -I +apache -I +pcre -I +dbi -I +extlib -I +merjis
 CPP := cpp
 
 LIB_OBJS := \
@@ -21,7 +22,8 @@ LIB_OBJS := \
        cocanwiki_emailnotify.cmo \
        wikilib.cmo \
        cocanwiki_links.cmo \
-       cocanwiki_create_host.cmo
+       cocanwiki_create_host.cmo \
+       cocanwiki_ext_calendar.cmo
 
 OBJS := change_password.cmo \
        change_password_form.cmo \
index 981379f..5664582 100644 (file)
@@ -1,7 +1,7 @@
 (* COCANWIKI - a wiki written in Objective CAML.
  * Written by Richard W.M. Jones <rich@merjis.com>.
  * Copyright (C) 2004 Merjis Ltd.
- * $Id: cocanwiki.ml,v 1.14 2004/10/04 15:19:56 rich Exp $
+ * $Id: cocanwiki.ml,v 1.15 2004/10/07 16:54:24 rich Exp $
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -218,3 +218,7 @@ let linkname_of_sectionname str =
     if not (isalnum str.[i]) then str.[i] <- '_'
   done;
   str
+
+(* List of extensions currently registered. *)
+type extension_t = Dbi.connection -> int -> string -> string
+let extensions = ref ([] : (string * extension_t) list)
index cd7fe01..42edc44 100644 (file)
@@ -1,7 +1,7 @@
 (* COCANWIKI - a wiki written in Objective CAML.
  * Written by Richard W.M. Jones <rich@merjis.com>.
  * Copyright (C) 2004 Merjis Ltd.
- * $Id: cocanwiki_date.ml,v 1.4 2004/09/09 12:21:22 rich Exp $
+ * $Id: cocanwiki_date.ml,v 1.5 2004/10/07 16:54:24 rich Exp $
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -35,6 +35,12 @@ let short_month = function
   | 9 -> "Sep" | 10 -> "Oct" | 11 -> "Nov" | 12 -> "Dec"
   | _ -> invalid_arg "short_month"
 
+let long_month = function
+  | 1 -> "January" | 2 -> "February" | 3 -> "March" | 4 -> "April"
+  | 5 -> "May" | 6 -> "June" | 7 -> "July" | 8 -> "August"
+  | 9 -> "September" | 10 -> "October" | 11 -> "November" | 12 -> "December"
+  | _ -> invalid_arg "short_month"
+
 (* Generate a printable datestamp for pages. *)
 let printable_date' date =
   sprintf "%d %s %04d" date.Dbi.day (short_month date.Dbi.month) date.Dbi.year
diff --git a/scripts/cocanwiki_ext_calendar.ml b/scripts/cocanwiki_ext_calendar.ml
new file mode 100644 (file)
index 0000000..2e35698
--- /dev/null
@@ -0,0 +1,199 @@
+(* COCANWIKI - a wiki written in Objective CAML.
+ * Written by Richard W.M. Jones <rich@merjis.com>.
+ * Copyright (C) 2004 Merjis Ltd.
+ * $Id: cocanwiki_ext_calendar.ml,v 1.1 2004/10/07 16:54:24 rich Exp $
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING.  If not, write to
+ * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *)
+
+open Apache
+open Registry
+open Cgi
+open Printf
+
+open ExtList
+
+open GregorianDate
+
+open Cocanwiki
+open Cocanwiki_template
+open Cocanwiki_strings
+open Cocanwiki_date
+
+let day_template = _get_template "calendar_day.html"
+let month_template = _get_template "calendar_month.html"
+let year_template = _get_template "calendar_year.html"
+
+let rec range a b =
+  if a <= b then
+    a :: range (a+1) b
+  else
+    []
+
+let extension (dbh : Dbi.connection) hostid url =
+  (* Validate a date in the form "yyyy[/mm[/dd]]".  Returns a (yyyy, mm, dd)
+   * tuple with missing fields set to 0.  If the string doesn't parse or the
+   * date isn't valid, then raises Not_found.
+   *)
+  let valid_date str =
+    if String.length str = 4 &&
+      isdigit str.[0] && isdigit str.[1] &&
+      isdigit str.[2] && isdigit str.[3] then (
+       let yyyy = int_of_string (String.sub str 0 4) in
+       (yyyy, 0, 0)
+      )
+    else if String.length str = 7 &&
+      isdigit str.[0] && isdigit str.[1] &&
+      isdigit str.[2] && isdigit str.[3] &&
+      str.[4] = '/' &&
+      isdigit str.[5] && isdigit str.[6] then (
+       let yyyy = int_of_string (String.sub str 0 4) in
+       let mm = int_of_string (String.sub str 5 2) in
+       if mm >= 1 && mm <= 12 then (yyyy, mm, 0) else raise Not_found
+      )
+    else if String.length str = 10 &&
+      isdigit str.[0] && isdigit str.[1] &&
+      isdigit str.[2] && isdigit str.[3] &&
+      str.[4] = '/' &&
+      isdigit str.[5] && isdigit str.[6] &&
+      str.[7] = '/' &&
+      isdigit str.[8] && isdigit str.[9] then (
+       let yyyy = int_of_string (String.sub str 0 4) in
+       let mm = int_of_string (String.sub str 5 2) in
+       let dd = int_of_string (String.sub str 8 2) in
+       let date = (yyyy, mm, dd) in
+       if GregorianDate.check_date date then date else raise Not_found
+      )
+    else
+      raise Not_found
+  in
+
+  (* From the links table, find all links to this page, or sub-calendar pages.
+   * This query overselects.  We then filter the real pages in OCaml.
+   *)
+  let sth =
+    dbh#prepare_cached
+      "select li.from_url, p.title, li.to_url
+         from links li, pages p
+        where li.hostid = ? and li.to_url like ?
+          and li.hostid = p.hostid and li.from_url = p.url" in
+  sth#execute [`Int hostid; `String (url ^ "%")];
+
+  let pages =
+    let results =
+      sth#map (function [`String from_url; `String title; `String to_url] ->
+                from_url, title, to_url
+                | _ -> assert false) in
+    List.filter_map
+      (fun (from_url, title, to_url) ->
+        try let date = valid_date to_url in Some (date, title, from_url)
+        with Not_found -> None) results in
+  let pages = List.sort pages in
+
+  (* Validate the date in the URL itself. *)
+  let date =
+    try Some (valid_date url)
+    with Not_found -> None in
+
+  match date with
+    | None ->                          (* Not a valid date. *)
+       "<p>" ^ url ^ " is not an actual date.</p>"
+    | Some (yyyy, 0, 0) ->             (* Year view. *)
+       let template = year_template in
+       failwith "not impl";
+
+
+
+
+
+
+
+       template#to_string
+
+    | Some (yyyy, mm, 0) ->            (* Month view. *)
+       let template = month_template in
+
+       template#set "month_name" (long_month mm);
+       template#set "yyyy" (string_of_int yyyy);
+       template#set "mm" (sprintf "%02d" mm);
+
+       let prev_yyyy, prev_mm =
+         if mm = 1 then yyyy - 1, 12
+         else yyyy, mm - 1 in
+       let next_yyyy, next_mm =
+         if mm = 12 then yyyy + 1, 1
+         else yyyy, mm + 1 in
+       template#set "prev_yyyy" (string_of_int prev_yyyy);
+       template#set "prev_mm" (sprintf "%02d" prev_mm);
+       template#set "next_yyyy" (string_of_int next_yyyy);
+       template#set "next_mm" (sprintf "%02d" next_mm);
+
+       (* Get all monthly events and all daily events. *)
+       let monthly_events, daily_events =
+         List.partition (function ((_, _, 0), _, _) -> true | _ -> false)
+           pages in
+
+       (* Table of monthly events. *)
+       let table =
+         List.map (fun (_, title, page) ->
+                     [ "title", Template.VarString title;
+                       "page", Template.VarString page ]) monthly_events in
+       template#table "monthly_events" table;
+
+       (* How many days in this month? *)
+       let max_dd = GregorianDate.days_in_month yyyy mm in
+       let days = range 1 max_dd in
+
+       let table =
+         List.map (fun dd ->
+                     let events =
+                       List.filter (fun ((_, _, d), _, _) -> d = dd)
+                         daily_events in
+                     let table =
+                       List.map (fun (_, title, page) ->
+                                   [ "title", Template.VarString title;
+                                     "page", Template.VarString page ])
+                         events in
+                     let is_weekend =
+                       GregorianDate.day_of_week (yyyy, mm, dd) >= 6 in
+                     [ "dd", Template.VarString (sprintf "%02d" dd);
+                       "is_weekend", Template.VarConditional is_weekend;
+                       "events", Template.VarTable table ])
+           days in
+       template#table "days" table;
+
+       template#to_string
+
+    | Some (yyyy, mm, dd) ->           (* Single day view. *)
+       let template = day_template in
+       failwith "not impl";
+
+
+
+
+
+
+
+
+
+
+
+
+       template#to_string
+
+(* Register the extension. *)
+let () =
+  extensions := ("calendar", extension) :: !extensions
index 52debcc..7e2f02d 100644 (file)
@@ -1,7 +1,7 @@
 (* COCANWIKI - a wiki written in Objective CAML.
  * Written by Richard W.M. Jones <rich@merjis.com>.
  * Copyright (C) 2004 Merjis Ltd.
- * $Id: page.ml,v 1.25 2004/10/04 15:19:56 rich Exp $
+ * $Id: page.ml,v 1.26 2004/10/07 16:54:24 rich Exp $
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -156,14 +156,33 @@ let run r (q : cgi) (dbh : Dbi.connection) hostid
     String.concat "" (List.concat [ head ; body ; tail ])
   in
 
+  (* Check the templates table for extensions. *)
+  let get_extension url =
+    let sth = dbh#prepare_cached "select extension from templates
+                                   where ? ~ url_regexp
+                                   order by ordering
+                                   limit 1" in
+    sth#execute [`String url];
+
+    try
+      let name = sth#fetch1string () in
+      Some (List.assoc name !extensions)
+    with
+       Not_found -> None
+  in
+
   (* This code generates ordinary pages. *)
   let make_page title description pageid last_modified_date has_page_css
-      version page page' =
+      version page page' extension =
     let t = template_page in
     t#set "title" title;
-    t#set "description" description;
-    t#set "pageid" (string_of_int pageid);
-    t#set "last_modified_date" (printable_date last_modified_date);
+    t#set "last_modified_date" last_modified_date;
+
+    (match description with
+        None -> t#conditional "has_description" false
+       | Some description ->
+          t#conditional "has_description" true;
+          t#set "description" description);
 
     if page <> page' then (* redirection *) (
       t#set "page" page';
@@ -190,38 +209,56 @@ let run r (q : cgi) (dbh : Dbi.connection) hostid
     t#conditional "has_stats" has_stats;
 
     (* Pull out the sections in this page. *)
-    let sth = dbh#prepare_cached
-               "select ordering, sectionname, content, divname
-                   from contents
-                  where pageid = ?
-                  order by ordering" in
-    sth#execute [`Int pageid];
-
     let sections =
-      sth#map
-       (function [`Int ordering;
-                  (`Null | `String _) as sectionname;
-                  `String content;
-                  (`Null | `String _) as divname] ->
-          let divname, has_divname =
-            match divname with
-                `Null -> "", false
-              | `String divname -> divname, true in
-          let sectionname, has_sectionname =
-            match sectionname with
-                `Null -> "", false
-              | `String sectionname -> sectionname, true in
-          let linkname = linkname_of_sectionname sectionname in
-          [ "ordering", Template.VarString (string_of_int ordering);
-            "has_sectionname", Template.VarConditional has_sectionname;
-            "sectionname", Template.VarString sectionname;
-            "linkname", Template.VarString linkname;
-            "content",
-              Template.VarString
-                (Wikilib.xhtml_of_content dbh hostid content);
-            "has_divname", Template.VarConditional has_divname;
-            "divname", Template.VarString divname ]
-          | _ -> assert false) in
+      match pageid with
+         None -> []
+       | Some pageid ->
+           let sth = dbh#prepare_cached
+                       "select ordering, sectionname, content, divname
+                           from contents where pageid = ? order by ordering" in
+           sth#execute [`Int pageid];
+
+           sth#map
+             (function [`Int ordering;
+                        (`Null | `String _) as sectionname;
+                        `String content;
+                        (`Null | `String _) as divname] ->
+                let divname, has_divname =
+                  match divname with
+                      `Null -> "", false
+                    | `String divname -> divname, true in
+                let sectionname, has_sectionname =
+                  match sectionname with
+                      `Null -> "", false
+                    | `String sectionname -> sectionname, true in
+                let linkname = linkname_of_sectionname sectionname in
+                [ "ordering", Template.VarString (string_of_int ordering);
+                  "has_sectionname",
+                    Template.VarConditional has_sectionname;
+                  "sectionname", Template.VarString sectionname;
+                  "linkname", Template.VarString linkname;
+                  "content",
+                    Template.VarString
+                      (Wikilib.xhtml_of_content dbh hostid content);
+                  "has_divname", Template.VarConditional has_divname;
+                  "divname", Template.VarString divname ]
+                | _ -> assert false) in
+
+    (* Call an extension to generate the first section in this page? *)
+    let sections =
+      match extension with
+         None -> sections
+       | Some extension ->
+           let content = extension dbh hostid page' in
+           let section = [
+             "ordering", Template.VarString "0";
+             "has_sectionname", Template.VarConditional false;
+             "linkname", Template.VarString "";
+             "content", Template.VarString content;
+             "has_divname", Template.VarConditional true;
+             "divname", Template.VarString "form_div";
+           ] in
+           section :: sections in
 
     t#table "sections" sections;
 
@@ -292,7 +329,7 @@ let run r (q : cgi) (dbh : Dbi.connection) hostid
   (* Fetch a page by name.  This function can give three answers:
    * (1) Page fetched OK (fetches some details of the page).
    * (2) Page is a redirect (fetches the name of the redirect page).
-   * (3) Page not found in database, ie. 404 error.
+   * (3) Page not found in database, could be template or 404 error.
    *)
   (* XXX Should do a case-insensitive matching of URLs, and if the URL differs
    * in case only should redirect to the lowercase version.
@@ -374,12 +411,24 @@ let run r (q : cgi) (dbh : Dbi.connection) hostid
     ) else
       match fetch_page page' version allow_redirect with
        | FPOK (pageid, title, description, last_modified_date, has_page_css)->
-           make_page title description pageid last_modified_date has_page_css
-             version page page'
+           (* Check if the page is also a template. *)
+           let extension = get_extension page' in
+           make_page title (Some description) (Some pageid)
+             (printable_date last_modified_date) has_page_css
+             version page page' extension
        | FPRedirect page' ->
            loop page' (i+1)
        | FPNotFound ->
-           make_404 ()
+           (* Might be a templated page with no content in it. *)
+           let extension = get_extension page' in
+           (match extension with
+              | (Some _) as extension ->
+                  let title = page' in
+                  make_page title None None
+                    "Now" false None page page'
+                    extension
+              | None ->
+                  make_404 ())
   in
   loop page 0
 
index e92d182..3d29afb 100644 (file)
@@ -1,7 +1,7 @@
 (* COCANWIKI - a wiki written in Objective CAML.
  * Written by Richard W.M. Jones <rich@merjis.com>.
  * Copyright (C) 2004 Merjis Ltd.
- * $Id: stats.ml,v 1.1 2004/09/23 15:16:21 rich Exp $
+ * $Id: stats.ml,v 1.2 2004/10/07 16:54:24 rich Exp $
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -24,6 +24,8 @@ open Registry
 open Cgi
 open Printf
 
+open GregorianDate
+
 open Cocanwiki
 open Cocanwiki_template
 open Cocanwiki_server_settings
@@ -41,20 +43,14 @@ let run r (q : cgi) (dbh : Dbi.connection) hostid { hostname = hostname } _ =
 
   template#set "stats_page" stats_page;
 
-  (* Get the current business week.  Tricky - because GregorianDate isn't
-   * around to help us, it's best to get the database to help us out.
-   *
-   * XXX This fails sometimes near New Year.  Better to get GD into extlib
-   * or this codebase.
-   *)
-  let sth =
-    dbh#prepare_cached "select extract (year from current_date) :: int,
-                               extract (week from current_date) :: int" in
-  sth#execute [];
+  (* Get the current business week. *)
   let year, week =
-    match sth#fetch1 () with
-       [ `Int year; `Int week ] -> year, week
-      | _ -> assert false in
+    let date =
+      let tm = Unix.gmtime (Unix.time ()) in
+      (tm.Unix.tm_year + 1900, tm.Unix.tm_mon + 1, tm.Unix.tm_mday)
+    in
+    fst (GregorianDate.business_of_standard date)
+  in
 
   template#set "year" (string_of_int year);
   template#set "week" (string_of_int week);
diff --git a/templates/calendar_day.html b/templates/calendar_day.html
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/templates/calendar_month.html b/templates/calendar_month.html
new file mode 100644 (file)
index 0000000..8694800
--- /dev/null
@@ -0,0 +1,27 @@
+<table class="cal_month">
+<tr>
+  <th colspan="2" class="cal_month_header">
+    <a href="/::prev_yyyy::/::prev_mm::" class="cal_month_left" title="Previous month">&lt;&lt;</a>
+    ::month_name_html:: <a href="/::yyyy::">::yyyy::</a>
+    <a href="/::next_yyyy::/::next_mm::" class="cal_month_right" title="Next month">&gt;&gt;</a>
+  </th>
+</tr>
+<tr>
+  <td></td>
+  <td class="cal_month_events">
+    <ul>
+    ::table(monthly_events)::<li><a href="/::page::">::title_html::</a></li>::end::
+    </ul>
+  </td>
+<tr>
+::table(days)::
+<tr class="cal_month_row::if(is_weekend):: cal_month_weekend::end::">
+  <th> <a href="/::yyyy::/::mm::/::dd::">::dd::</a> </th>
+  <td>
+    <ul>
+    ::table(events)::<li><a href="/::page::">::title_html::</a></li>::end::
+    </ul>
+  </td>
+</tr>
+::end::
+</table>
diff --git a/templates/calendar_year.html b/templates/calendar_year.html
new file mode 100644 (file)
index 0000000..e69de29
index 13f69c4..2755987 100644 (file)
@@ -3,7 +3,7 @@
 <head>
 <title>::title_html::</title>
 ::if(is_old_version)::<meta name="robots" content="noindex,nofollow"/>::end::
-<meta name="description" content="::description_html_tag::" />
+::if(has_description)::<meta name="description" content="::description_html_tag::" />::end::
 <meta name="author" content="http://www.merjis.com/" />
 <link rel="stylesheet" href="::theme_css_html_tag::" type="text/css" title="Standard"/>
 ::if(has_host_css)::<link rel="stylesheet" href="/_global.css" type="text/css" title="Standard"/>::end::