Attachment 'Blog_MoinMoin1.5.py'

Download

   1 """
   2 MoinMoin - Blog macro
   3 
   4 (c) 2003 Mark Proctor, athico.com
   5 
   6 (c) 2006 Carsten Grohmann
   7 
   8 Overview
   9 ========
  10 This is a simply Blog macro that utilises a javascript calendar and the
  11 Include macro to hack together a pseudo bwiki (blog/wiki).
  12 The calendar is used to choose the entries to show, the number of visible
  13 entries is controlled by the select control "max entries".
  14 The button to the left of "max entries" allows you to toggle between the
  15 two modes "Show All" and "Show Published".
  16  -  "Show Published" only shows those days that contain entries up to the
  17     given "max entries", from the chosen calendar date.
  18  -  "Show All" Show all the dates, previous to the chosen calendar date,
  19      up to a maximum of "max entries".
  20      
  21      This is the mode you will need to use to enter new blog entries
  22 
  23 Dependencies
  24 ============
  25  - C{Include}
  26 
  27 To install
  28 ==========
  29  - Save this macro in your macros directory
  30 
  31 To Use
  32 ======
  33  - C{<date>}       : in the format of yyyy-mm-dd
  34  - C{<showAll>}    : 1 or 0, where 1 shows all and 0 shows published
  35  - C{<entries>}    : the maximum visible number of entries
  36  - C{<maxEntriesInOptionList>} : the maximum value that C{<entries>} can be, this is used to restrict the web gui
  37  - C{<startDay>}   : The start Day for the calendar
  38    - values      : C{Su}, C{Mo}, C{Tu}, C{We}, C{Th}, C{Fr}, C{Sa}
  39 
  40 Defaults Values
  41 ===============
  42  - C{<date>}                    : C{today}
  43  - C{<showAll>}                 : C{0}
  44  - C{<entries>}                 : C{5}
  45  - C{<maxEntriesInOptionList>}  : C{20}
  46  - C{<startDay>}                : C{Mo}
  47 
  48 Example::
  49   [[Blog[<date>, <showAll>, <entries>, <maxEntriesInOptionList>]]
  50   [[Blog(, 1, 7)]] - Shows all days, up to 7 days, from todays date.
  51   [[Blog(2003-05-23, 0, 5)]] - Shows upto 5 published entries from the given date.
  52   [[Blog( , , , 10)]] - Shows upto 5(default) published(default) entries from todays date(default), but does not allow the user to speficy max entries to be more than 10.
  53   [[Blog( , , , , We)]] - Shows upto 5(default) published(default) entries from todays date(default), maxEntriesInOptionList(20) with start calendar  day Wednesday.
  54   [[Blog(2003-05-23, 0, 5, ,Sa)]] - Shows upto 5 published entries from the given date, with start calendar day Saturday
  55 
  56 $Id: Blog.py,v 1.4 2006/06/15 17:39:04 carsten Exp $
  57 
  58 @license: Licensed under GNU GPL - see COPYING for details.
  59 @version: 1.1
  60 @var re_entries: Precompiled regular expression to match the entries option
  61 @var re_date:    Precompiled regular expression to match the date option
  62 """
  63 
  64 from MoinMoin.Page import Page
  65 import datetime, re
  66 import MoinMoin.macro.Include
  67 
  68 Dependencies = []
  69 
  70 # compile regular expressions once at start time
  71 re_entries=re.compile(r'(?P<entries>\d+)')
  72 re_date=re.compile(r'(?P<year>\d\d\d\d)-(?P<month>\d?\d)-(?P<day>\d?\d)')
  73 
  74 class Blog:
  75     """
  76     This macro provides an html based blog for MoinMoin.
  77 
  78     Blog entries are per day possible and named like the day
  79     
  80     @cvar calendarHTML:   HTML code of the calendar part
  81     @cvar cssStyle:       The CSS declaration
  82     @cvar errorMessage:   Preformatted html error message
  83     @cvar headingLevel:   The level of depth of the heading of missing pages
  84     @cvar inputHeader:    Header of the html imput form
  85     @cvar javaScriptFunctions: Set of java script function to have a
  86                                more comfortable use of the blog calendar
  87     @cvar oneDay:         Timedelta object with one day length
  88 
  89     @ivar macro:          Reference to the macro instance
  90     @ivar maxEntriesInOptionList: Maximum numbers of entries in the option list
  91     @ivar re_blogEntries: Page specific regular expression to catch all blog
  92                           entries (subpages)
  93     @ivar startDay:       First day of the week; valid values are
  94                           "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"
  95     @ivar showAll:        Integer flag to show all entries or the existing only
  96     @ivar text:           The macro arguments
  97     @ivar thisPageName:   The of the current page
  98     """
  99 
 100     calendarHTML = """
 101         <TABLE CELLPADDING=0 CELLSPACING=0 BORDER=0 >
 102             <TR><TD>
 103               <CENTER>
 104               <FORM NAME="calControl" onsubmit="return false;" >
 105                 <SELECT class="calendarButton" NAME="month" onchange="selectDate()">
 106                   <OPTION>January</OPTION>
 107                   <OPTION>February</OPTION>
 108                   <OPTION>March</OPTION>
 109                   <OPTION>April</OPTION>
 110                   <OPTION>May</OPTION>
 111                   <OPTION>June</OPTION>
 112                   <OPTION>July</OPTION>
 113                   <OPTION>August</OPTION>
 114                   <OPTION>September</OPTION>
 115                   <OPTION>October</OPTION>
 116                   <OPTION>November</OPTION>
 117                   <OPTION>December</OPTION>
 118                 </SELECT>
 119                 <INPUT NAME="year" class="calendarButton" TYPE=TEXT SIZE=4 MAXLENGTH=4>
 120                 <INPUT TYPE="button" class="calendarButton" NAME="Go" value="Update Year" onClick="selectDate()">
 121               </FORM>
 122               </CENTER>
 123             </TD></TR>
 124             <TR><TD id="calendar" align="center" onsubmit="return false;" ></TD></TR>
 125 	    <TR><TD>
 126 	      <CENTER>
 127               <FORM NAME="calButtons">
 128                 <INPUT class='calendarButton' TYPE=BUTTON NAME="previousYear" VALUE=" <<  "    onClick="setPreviousYear()">
 129                 <INPUT class='calendarButton' TYPE=BUTTON NAME="previousYear" VALUE="  <  "    onClick="setPreviousMonth()">
 130                 <INPUT class='calendarButton' TYPE=BUTTON NAME="previousYear" VALUE="Today"    onClick="setToday()">
 131                 <INPUT class='calendarButton' TYPE=BUTTON NAME="previousYear" VALUE="  >  "    onClick="setNextMonth()">
 132                 <INPUT class='calendarButton' TYPE=BUTTON NAME="previousYear" VALUE="  >> "    onClick="setNextYear()">
 133 	      </FORM>
 134               </CENTER>
 135 	    </TD></TR>
 136         </TABLE>
 137         <script>
 138          onload=function() {
 139             var date = global.date.split("-");
 140             document.calControl.month.selectedIndex = date[1]-1;
 141             document.calControl.year.value = date[0];
 142             makeCalendar();
 143             updateCalendar(date[1]-1, date[0]);
 144          };
 145         </script>
 146         """
 147 
 148     cssStyle = """
 149         <style type="text/css">
 150             .calendarButton {
 151                 font-size:10;
 152                 cursor:pointer;
 153                 cursor:hand;
 154             }
 155 
 156             .calendarHeader {
 157                 background-color:#C0DED1;
 158                 font-size:12;
 159                 text-decoration:none;
 160                 cursor:pointer;
 161                 cursor:hand;
 162             }
 163         
 164             .calendarValue {
 165                 background-color:#FDFAD1;
 166                 font-size:10;
 167                 font-color:black;
 168                 cursor:pointer;
 169                 cursor:hand;
 170             }
 171         
 172             .calendarValueSelected {
 173                 background-color:#FD0000;
 174                 font-size:10;
 175                 font-color:black;
 176                 cursor:pointer;
 177                 cursor:hand;
 178             }
 179         </style>
 180         """
 181         
 182     errorMessage = '<p><strong class="error">%s</strong></p>'
 183 
 184     headingLevel = 1
 185 
 186     inputHeader =  """
 187     <input class='calendarButton' type=button value='%s' onclick='global.showAll=%s;updateBlog();'>
 188     max entries:
 189     <select class='calendarButton'
 190     onchange='if (this.value&&(this.value != "")) {global.entries=this.value;  updateBlog();}'>
 191     """
 192 
 193     javaScriptFunctions  = """
 194 <script>
 195 
 196 var global = {};
 197 global.page = "%s";
 198 global.date = "%s";
 199 global.entries = "%s";
 200 global.showAll = "%s";
 201 
 202 global.daysLookup = {"Su":0, "Mo":1, "Tu":2, "We":3, "Th":4, "Fr":5, "Sa":6};
 203 global.days = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
 204 global.startDay = "%s";
 205 
 206 function gotoDate() {
 207     if (!this.date) return;
 208     global.date = this.date;
 209     updateBlog();
 210 }
 211 
 212 function updateBlog() {
 213     window.location.href = "/"+global.page+"?date="+global.date+"&entries="+global.entries+"&showAll="+global.showAll
 214 }
 215 
 216 function setToday() {
 217     var now   = new Date();
 218     var day   = now.getDate();
 219     var month = now.getMonth();
 220     var year  = now.getYear();
 221     if (year < 2000)    // Y2K Fix, Isaac Powell
 222     year = year + 1900; // http://onyx.idbsu.edu/~ipowell
 223     this.focusDay = day;
 224     document.calControl.month.selectedIndex = month;
 225     document.calControl.year.value = year;
 226     updateCalendar(month, year);
 227 }
 228 
 229 function isFourDigitYear(year) {
 230     if (year.length != 4) {
 231         alert ("Sorry, the year must be four-digits in length.");
 232         document.calControl.year.select();
 233         document.calControl.year.focus();
 234         return false;
 235     } else {
 236         return true;
 237     }
 238 }
 239 
 240 function selectDate() {
 241     var year  = document.calControl.year.value;
 242     if (isFourDigitYear(year)) {
 243         var day   = 0;
 244         var month = document.calControl.month.selectedIndex;
 245         updateCalendar(month, year);
 246     }
 247 }
 248 
 249 function setPreviousYear() {
 250     var year  = document.calControl.year.value;
 251     if (isFourDigitYear(year)) {
 252         var day   = 0;
 253         var month = document.calControl.month.selectedIndex;
 254         year--;
 255         document.calControl.year.value = year;
 256         updateCalendar(month, year);
 257    }
 258 }
 259 function setPreviousMonth() {
 260     var year  = document.calControl.year.value;
 261     if (isFourDigitYear(year)) {
 262         var day   = 0;
 263         var month = document.calControl.month.selectedIndex;
 264         if (month == 0) {
 265             month = 11;
 266             if (year > 1000) {
 267                 year--;
 268                 document.calControl.year.value = year;
 269             }
 270         } else {
 271             month--;
 272         }
 273         document.calControl.month.selectedIndex = month;
 274         updateCalendar(month, year);
 275    }
 276 }
 277 function setNextMonth() {
 278     var year  = document.calControl.year.value;
 279     if (isFourDigitYear(year)) {
 280         var day   = 0;
 281         var month = document.calControl.month.selectedIndex;
 282         if (month == 11) {
 283             month = 0;
 284             year++;
 285             document.calControl.year.value = year;
 286         } else {
 287             month++;
 288         }
 289         document.calControl.month.selectedIndex = month;
 290         updateCalendar(month, year);
 291    }
 292 }
 293 function setNextYear() {
 294     var year = document.calControl.year.value;
 295     if (isFourDigitYear(year)) {
 296         var day = 0;
 297         var month = document.calControl.month.selectedIndex;
 298         year++;
 299         document.calControl.year.value = year;
 300         updateCalendar(month, year);
 301    }
 302 }
 303 
 304 function makeCalendar() {
 305     var cal = document.getElementById("calendarTbody");
 306     if (cal) return;
 307     var i;
 308     var table = document.createElement("table");
 309     table.style.cssText = "border-left:1px solid black; border-top:1px solid black";
 310     table.cellSpacing = 0;
 311     table.cellPadding = 2;
 312     var thead = document.createElement("thead");
 313 
 314     var cell;
 315     var row = document.createElement("tr");
 316 
 317     var titleDays = [];
 318     var startDay = global.daysLookup[global.startDay];
 319 
 320     for (i=startDay;i<7;i++) {
 321         titleDays.push(global.days[i]);
 322     }
 323 
 324     for (i=0;i<startDay;i++) {
 325         titleDays.push(global.days[i]);
 326     }
 327 
 328     for  (i=0;i<titleDays.length;i++) {
 329         cell = document.createElement("td");
 330         cell.style.cssText = "border-right:1px solid black; border-bottom:1px solid black";
 331         cell.className = "calendarHeader";
 332         cell.innerHTML =  titleDays[i];
 333         row.appendChild(cell);
 334     }
 335     thead.appendChild(row);
 336     table.appendChild(thead);
 337 
 338     var tbody = document.createElement("tbody");
 339     tbody.id = "calendarTbody";
 340     row = document.createElement("tr");
 341     for (i=0; i<42; i++)  {
 342         if ( i%%7 == 0 ) { //start new line
 343             tbody.appendChild(row);
 344             row = document.createElement("tr");
 345         }
 346         cell = document.createElement("td");
 347         cell.className = "calendarValue";
 348         cell.style.cssText = "border-right:1px solid black; border-bottom:1px solid black";
 349         cell.innerHTML = "&nbsp;";
 350         cell.date = null;
 351         cell.onclick = gotoDate
 352         row.appendChild(cell);
 353     }
 354     tbody.appendChild(row);
 355     table.appendChild(tbody);
 356 
 357     var  calendar = document.getElementById("calendar");
 358     calendar.appendChild(table);
 359 }
 360 
 361 function updateCalendar(month, year) {
 362     month = parseInt(month);
 363     year = parseInt(year);
 364     var i = 0;
 365     var days = getDaysInMonth(month+1,year);
 366     var startDay = global.daysLookup[global.startDay];
 367     var firstOfMonth = new Date (year, month, 1).getDay();
 368     var startingPos =  (firstOfMonth >= startDay) ? firstOfMonth - startDay : 7 - (startDay - firstOfMonth);
 369     days += startingPos;
 370 
 371     var cal = document.getElementById("calendarTbody");
 372 
 373     var cells = cal.getElementsByTagName("td");
 374     var cell;
 375     for (i = 0; i < startingPos; i++) {
 376         cell = cells[i];
 377         cell.innerHTML = "&nbsp;";
 378         cell.date = null;
 379         cell.className = "calendarValue";
 380     }
 381 
 382     var value;
 383     month++;
 384     if (month<10) month = "0" + month;
 385     date = year+"-"+month+"-";
 386     for (i = startingPos; i < days; i++) {
 387         cell = cells[i];
 388         value = "";
 389         value = i-startingPos+1;
 390         if (value < 10) value = "0" + value;
 391         cell.date = year+'-'+(month)+'-'+value;
 392         cell.innerHTML = value;
 393         if (global.date != date+value) cell.className = "calendarValue";
 394         else cell.className = "calendarValueSelected";
 395     }
 396 
 397     for (i = days; i < 42; i++) {
 398         cell = cells[i];
 399         cell.date = null;
 400         cell.className = "calendarValue";
 401         cell.innerHTML = "&nbsp;";
 402     }
 403 }
 404 
 405 function getDaysInMonth(month,year)  {
 406     var days;
 407     if (month==1 || month==3 || month==5 || month==7 || month==8 || month==10 || month==12)  days=31;
 408     else if (month==4 || month==6 || month==9 || month==11) days=30;
 409     else if (month==2)  {
 410         if (isLeapYear(year)) { days=29; }
 411         else { days=28; }
 412     }
 413     return (days);
 414 }
 415 
 416 function isLeapYear (Year) {
 417     if (((Year %% 4)==0) && ((Year %% 100)!=0) || ((Year %% 400)==0)) {
 418     return (true);
 419     } else { return (false); }
 420 }
 421 </script>
 422 """
 423 
 424     oneDay = datetime.timedelta(days = 1)
 425 
 426     def __init__(self, macro, text):
 427         """
 428         Constructor to initialise this class with default values
 429 
 430         @param macro: Instance of the class Macro
 431         @param text:  The macro arguments
 432         """
 433         self.macro = macro
 434         self.text = text
 435         self.thisPageName = self.macro.formatter.page.page_name
 436 
 437         # set default values
 438         self.maxEntriesInOptionList = 20
 439         self.startDay = "Mo"
 440         self.showAll = "0"
 441 
 442         # compile regular expression to catch own sub pages
 443         self.re_blogEntries = re.compile(
 444             r'^%s/'                                                  # parent page
 445             r'BlogEntry-'
 446             r'(?P<year>\d{4,4})-(?P<month>\d{2,2})-(?P<day>\d{2,2})' # date of the entry
 447             % self.thisPageName ).match
 448         
 449     def dispatch(self):
 450         """
 451         Main function
 452 
 453         Process all stuff and format the content
 454         """
 455         
 456         #get incoming macro args, else set to []
 457         if self.text:
 458             args = self.text.split(",")
 459         else:
 460             args = []
 461     
 462         #remove all leading and trailing spaces
 463         args = map(lambda line: line.strip(), args)
 464     
 465         #set date
 466         if self.macro.form.has_key('date'):
 467             date = self.macro.form['date'][0]
 468         elif (len(args) > 0) and (args[0]):
 469             date = args[0]
 470         else:
 471             date = ""
 472     
 473         #set showAll
 474         if self.macro.form.has_key('showAll'):
 475             self.showAll = self.macro.form['showAll'][0]
 476         elif (len(args) > 1) and (args[1]):
 477             self.showAll = args[1]
 478     
 479         #set entries
 480         if self.macro.form.has_key('entries'):
 481             self.entries = self.macro.form['entries'][0]
 482         elif (len(args) > 2) and (args[2]):
 483             self.entries = args[2]
 484         else:
 485             self.entries = None
 486     
 487         #set max entries
 488         if (len(args) > 3) and (args[3]):
 489             self.maxEntriesInOptionList = int(args[3])
 490             if self.maxEntriesInOptionList < 0:
 491                 self.maxEntriesInOptionList = 20
 492 
 493         #set start day
 494         if (len(args) > 4) and (args[4]):
 495             self.startDay = args[4]
 496     
 497         #set the number of visible entries
 498         if self.entries:
 499             args = re_entries.match(self.entries)
 500             if not args:
 501                 return (self.errorMessage %('Invalid entries "%s"!')) % (self.macro.form['beforeDate'][0])
 502             self.entries = int(self.macro.form['entries'][0])
 503             if self.entries > self.maxEntriesInOptionList:
 504                 self.entries = self.maxEntriesInOptionList
 505             if self.entries < 0:
 506                 self.entries = 5
 507         else:
 508             self.entries = 5
 509     
 510         # get the date
 511         if not date == "":
 512             args = re_date.match(date)
 513             if not args:
 514                 return (self.errorMessage %('Invalid date "%s"!')) % (self.macro.form['date'][0])
 515             try:
 516                 self.blogDate = datetime.date(
 517                        int(args.group('year')),
 518                       int(args.group('month')),
 519                       int(args.group('day'))
 520                       )
 521             except ValueError:
 522                 self.blogDate = datetime.date.today()
 523         else:
 524             self.blogDate = datetime.date.today()
 525     
 526         content  = self.getCSSStyle()
 527         content += self.getJavaScript()
 528         content += self.getCalendar()
 529     
 530         # get the entries to display
 531         if (self.showAll == '1'):
 532             content += self.getShowAll()
 533         else:
 534             content += self.getShowEntered()
 535     
 536         return content
 537 
 538     def getCalendar(self):
 539         """"
 540         Returns the calendar HTML block
 541 
 542         @see: L{self.calendarHTML}
 543         """
 544         return self.calendarHTML
 545 
 546     def getCSSStyle(self):
 547         """
 548         Returns the CSS declaration
 549 
 550         @see: L{self.cssStyle}
 551         """
 552         return self.cssStyle
 553 
 554     def getJavaScript(self):
 555         """
 556         Returns the java script code of all calendar functions
 557 
 558         @see: L{self.javaScriptFunctions}
 559         """
 560 
 561         return self.javaScriptFunctions %  (
 562             self.thisPageName,
 563             "%d-%02d-%02d" % (
 564                 self.blogDate.year,
 565                 self.blogDate.month,
 566                 self.blogDate.day
 567                 ),
 568             self.entries,
 569             self.showAll,
 570             self.startDay)
 571 
 572     def getShowAll(self):
 573         """
 574         Shows the existing entries and link to non-existing pages
 575         """
 576 
 577         entryDate = self.blogDate
 578         formatter = self.macro.formatter
 579         
 580         content = []
 581         content.append(self.inputHeader % ("Show Published", "0"))
 582         content.append(self._createMaxEntriesOptionList())
 583 
 584         for i in range(self.entries):
 585             entryTitle = '%d-%02d-%02d' % (
 586                 entryDate.year,
 587                 entryDate.month,
 588                 entryDate.day
 589                 )
 590             fullEntryPath = '%s/BlogEntry-%s' % (
 591                 self.thisPageName,
 592                 entryTitle
 593                 )
 594 
 595             # we need this only to check the existence of a page
 596             dummyPage = Page(self.macro.request, fullEntryPath, formatter = formatter)
 597 
 598             # include existing pages using the Include() macro
 599             if dummyPage.exists():
 600                 content.append(MoinMoin.macro.Include.execute(
 601                     self.macro,
 602                     '%s, "%s", 1' % (
 603                         fullEntryPath,
 604                         entryTitle
 605                     )))
 606             else:
 607                 # create an heading for the empty page
 608                 content.append(
 609                     formatter.heading(1, self.headingLevel) +
 610                     formatter.pagelink(1, fullEntryPath, generated=1) +
 611                     formatter.text(entryTitle) +
 612                     formatter.pagelink(0,fullEntryPath) +
 613                     formatter.heading(0, self.headingLevel)
 614                 )
 615 
 616             # decrease date
 617             entryDate -= self.oneDay
 618             
 619         return "\n".join(content)
 620 
 621     def getShowEntered(self):
 622         """
 623         Shows the existing entries only.
 624 
 625         @note: This function uses the Include macro.
 626         @return: HTML page as string
 627         """
 628         content = []
 629         content.append(self.inputHeader % ("Show All", "1"))
 630         content.append(self._createMaxEntriesOptionList())
 631     
 632         # use precompiled regular expression to found all children
 633         child_page_list = self.macro.request.rootpage.getPageList(
 634             filter = self.re_blogEntries
 635             )
 636         
 637         child_page_list.sort()
 638         child_page_list.reverse()
 639         selectedDate = int( "%d%02d%02d" % (
 640             self.blogDate.year,
 641             self.blogDate.month,
 642             self.blogDate.day))
 643         pageCount = 0
 644 
 645         # process all child pages
 646         for pageName in child_page_list:
 647 
 648             # extract date from pagename
 649             formattedDate = pageName[-10:]
 650 
 651             pageDate = int("%s%s%s" % (
 652                 formattedDate[:4],
 653                 formattedDate[5:7],
 654                 formattedDate[8:10]
 655                 ))
 656 
 657             # skip too young pages
 658             if (pageDate > selectedDate):
 659                 continue
 660 
 661             # include child page
 662             includeParams = """%s, "%s", 1""" % (pageName, formattedDate)
 663             content.append(MoinMoin.macro.Include.execute(self.macro, includeParams))
 664 
 665             # leave loop on maximum number of pages to show
 666             pageCount += 1
 667             if (pageCount  >= self.entries):
 668                 break
 669                 
 670         return "\n".join(content)
 671 
 672     def _createMaxEntriesOptionList(self):
 673         """
 674         Creates an html option list to select the number of entries that will be shown
 675 
 676         @return: Option list as string
 677         """
 678         # open option list
 679         content = """<option value=''>--Entries--</option>"""
 680 
 681         # create an entry for each value till maxEntriesInOptionList and make the current as selected
 682         for i in xrange(1, self.maxEntriesInOptionList +1):
 683             if i == self.entries:
 684                 selected = "selected"
 685             else:
 686                 selected = ""
 687             content += '<option %s value="%d">%d</option>' % (selected, i, i)
 688 
 689         # add closing tag
 690         content += "</select>"
 691 
 692         return content
 693 
 694 def execute(macro, args):
 695     """
 696     Execute macro
 697     """
 698 
 699     return Blog(macro, args).dispatch()

Attached Files

To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.
  • [get | view] (2006-06-13 16:24:51, 4.2 KB) [[attachment:Blog.py_adapt_to_1.3.5.diff]]
  • [get | view] (2006-06-15 17:55:01, 22.3 KB) [[attachment:Blog_MoinMoin1.5.py]]
 All files | Selected Files: delete move to page copy to page

You are not allowed to attach a file to this page.