Kalender
Eksempler >Kalender

Kalender

Hva
En enkel webbasert kalender

Et eksempel på en enkel webbasert tjeneste som er laget etter KISS (Keep It Simple Stupid) prinsippet. Du finner ikke noe her som ikke er bedre løst i "standard"-kalendere som f.eks. Googles kalender.

Kalenderen kan kanskje være nyttig for en enkeltperson som vil ha tilgang til sine avtaler hvor som helst, eller som et planlegggingsverktøy. Den kan også være nyttig for en gruppe personer ( prosjektgruppe, lite firma, fotballag, familie) som trenger å holde seg ajour med de andres avtaler eller felles avtaler. Kalenderen er ikke skalerbar til store organisasjoner eller grupper med behov for detaljert planlegging eller store datamengder, kategorisering av avtaler eller komplisert tilgangkontroll.

Løsningen er enkel fordi den betrakter notater for en dag som en tekst. Det er altså ikke noen inndeling i timer for hver dag. Det er heller ikke noen kategorisering av begivenheter eller brukere. Adgangskontroll kan gjøres på to nivåer:

  • Du kan sette adgangskontroll til kalenderen i sin helhet. Enten ved å lage en egen passordbeskyttet inngangsside eller ved å bruke .htaccess, slik som forklart i modulen: Autentisering eller ved å bruke et passord i et skript. Det første er sikrere enn det andre.
  • Du kan sette passord for å oppdatere kalenderen ved å bruke passord slik som beskrevet i skriptet som er sitert nedenfor.

Du kan teste en vidåpen versjon her, ingen adgangskontroll på noe nivå:

Test http://www.it.hiof.no/~borres/cgi-bin/demokalender/demokal.py

Denne tømmes med ujevne mellomrom for data, og det er lagt begrensninger på hvor langt fram og tilbake du kan gå i tid.

Løsningen er basert på tre filer:

  • kalender.py. Her ligger hele funksjonaliteten: presentasjon av års- og månedsoversikter, registrering av endringer etc.
  • month_template.html. Template for å lage hovedsiden for kalenderen
  • printmonth_template.html. Template for å lage en versjon av kalenderen som er egnet for utskrift

De to siste kunne vært lagt inn som tekstkonstanter i skriptet, men de er valgt holdt utenfor fordi jeg finner det enklere å vedlikeholde og modifisere løsningen på denne måten. Ut fra en ren effektivitetsvurdering burde de vært inkludert ( en fil mindre å åpne).

Datalagring

Data lagres som enkle XML-filer, en fil pr måned. Skriptet lager slike filer etter behov. Et utsnitt fra en månedsfil kan se slik ut:

<?xml version="1.0" encoding="utf-8"?>
<notes>
  <day dayno="7">
    kl 0915 Tannlegen#kl 1515 Forelesning#
  </day>
   <day dayno="10">
    kl 0915 Forelesning#Få ferdig evalueringsrapport
  </day>
  ...
</notes>

Linjeskift er lagret som #. Konverteringen skjer bak kulissene og brukeren trenger ikke tenke på dette. Det er nødvendig for å kunne vise teksten fram på en enhetlig måte i de aktuelle nettleserne som handterer linjeskift litt forskjellig.

Pythonskriptet

#! /usr/bin/python
# -*- coding: cp1252 -*-
import calendar,xml.dom.minidom,os,time,cgi,sys
import cgitb; cgitb.enable()
#--------------------------------------------------
# Manage a webbased calendar
# Module responds to 5 main commands:
#
# - prepareMonth(year,month,day)
#   Prepares and returns a calender for selected month
#
# - updateDay(year,month,day)
#   Updates data for the day and returns the monthpage
#
# - start()
#   Prepares and returns a calender for this month
#
# - preparePrintMonth(year,month,day)
#   Prepares and returns a calender for the month, suitable for print
#
# - prepareYear(year)
#   Prepares and returns a simple calender page for selected year
#
# Data is organized in one XML-file for each month.
# These files are generated as needed. Must be within a
# cataog with writepermission for everyone
# Data is simple:
# <notes>
#   <day dayno="1">
#      Velkommen til en ny måned
#   </day>
#   .....
# </notes>
# NOTE: linebreaks are stored as #
# The skeleton files:
# - MONTHFILE
# - PRINTMONTHFILE
# must be available with paths as stated relative to
# this script, see below. All datafiles are stored
# in catalog DATAPATH
#--------------------------------------------------
#--------------------------------------------------
# Adjust these values according to your installation
# and language
#--------------------------------------------------
# Absolute URL to this script
SCRIPTPATH='http://www.ia.hiof.no/~borres/cgi-bin/demokalender/demokal.py'
# Path to where XML-files will be stored, relative to this script
# Make sure that we can make files and write to them in this catalog:
DATAPATH='data/'
# Template for web page with monthly calendar, relative to script:
MONTHFILE='month_template.html'
# Template for web page with monthly calendar for print, relative to script:
PRINTMONTHFILE='printmonth_template.html'
# if you want to use a Password for update of the calendar
USEPASSWORD=False
# Password for update, effective when USEPASSWORD=True
PASSWD='pass'
#--------------------------------------------------
# language. These are strings generated by the script
# You may also change the templatefiles:
# MONTHFILE and PRINTMONTHFILE
#--------------------------------------------------
# month-names, dummy to handle 0 automatically
monthlist=['dummy','januar','februar','mars','april','mai',
           'juni','juli','august','september','oktober',
           'november','desember']
# day-names
daylist=['mandag','tirsdag','onsdag','torsdag','fredag',
         'lørdag','søndag']
# Password announcment: Passwd to update:
NEEDPWD='Passord for å oppdatere: '
# message : Bad Password
WRONGPWD='Galt passord'
# announcing an empty day, no notes
NONOTES='ingen notater'

#--------------------------------------------------
# helpers to read and write text-files
#--------------------------------------------------
def loadFile(fname):
    try:
        file=open(fname,'r')
        res=file.read()
        file.close()
        return res
    except:
        reportErrorAndExit('Cant read: '+fname)
def storeFile(fname,content):
    try:
        outf=open(fname,'w')
        outf.write(content)
        outf.close()
    except:
        reportErrorAndExit('Error writing: '+ fname)

#-------------------------------------------------------
# report error and quit
#-------------------------------------------------------
def reportErrorAndExit(msg):
    S="""<html><head><title>error</title></head>
<body><p>%s</p>
<p><a href="javascript:history.back()">&lt;&lt;&lt;</a></p>
</body></html>
"""
    print S%msg
    sys.exit()
#--------------------------------------------------
# utility to calculate correct filename
#--------------------------------------------------
def getMonthsXMLFilename(year,month):
    return DATAPATH+'data_'+str(year)+'_'+str(month)+'.xml'
#---------------------------------------------
# Prepare the content of a cell for one day in
# main calendar page
#---------------------------------------------
def getCellContent(day,notes):
    # three possible class attributes
    # nullday:days out of month
    # empty: no notes
    # content: with a note
    if day==0:
        return '\n<td  class="nullday">  </td>'
    classatt='content'
    if notes.has_key(day):
        if len(notes[day].strip())< 1:
            classatt='empty'
    else:
        classatt='empty'
    T="""\n<td id="t%s" class="%s" style="cursor:pointer"  
        onclick="javascript:showday(%s)"> %s </td>"""
    return T%(str(day),classatt,str(day),str(day))
#--------------------------------------------------
# Prepare a list of refs to (this and)neighbouring months
#--------------------------------------------------
def makeMonthRefList(year, month):
    S='[<a href="%s?command=show&month=12&year=%s">'\
    %(SCRIPTPATH,str(year-1))
    S+=str(year-1)+' <-</a>] \n'
    for i in range(1,13):
        S+='[<a href="%s?command=show&month=%s&year=%s">'\
        %(SCRIPTPATH,str(i),str(year))
        S+=monthlist[i][0:3]+'</a>] \n'
    S+='[<a href="%s?command=show&month=1&year=%s">'\
    %(SCRIPTPATH,str(year+1))
    S+=' -> '+str(year+1)+'</a>] \n'
    return S
#--------------------------------------------------
# Prepare a table for one month in main calendar page
#--------------------------------------------------
def makeTable(year,month,notes):
    weeklist=calendar.monthcalendar( year, month)
    # heading
    T='<tr>'
    for d in range(0,7):
        T+='<th width="14%">'+daylist[d][:3]+'</th>'
    T+='</tr>'
    # content
    for week in weeklist:
        T+='\n<tr>\n'
        for day in week:
            # prepare the content of this cell, including td-tags
            T+=getCellContent(day,notes)
        T+='\n</tr>\n'
    return T
#------------------------------------------------
# Prepare a dictionary with all entries for this month
# Produce a new file if necessary
#------------------------------------------------
def prepareDictionary(year,month):
    # first we must check if the appropriate XML-file exists
    filename=getMonthsXMLFilename(year,month)
    if not(os.path.exists(filename)):
        # we must make it
        xmlstr="""<?xml version="1.0" encoding="ISO-8859-1"?>
<notes>
<day dayno="1">
Velkommen til %s
</day>
</notes>"""
        storeFile(filename,xmlstr%monthlist[month])
    # we have what we want
    noteDict={}
    s=loadFile(filename)
    # dont bother about things that may look like entities
    s=s.replace('&','&amp;')
    dom=xml.dom.minidom.parseString(s)
    daylist=dom.getElementsByTagName('day')
    for day in daylist:
        dayno=int(day.getAttribute('dayno').encode('ISO-8859-1'))
        note=day.firstChild.data.encode('ISO-8859-1')
        if len(note.strip()) > 0:
            noteDict[dayno]=note
    return noteDict

#-------------------------------------------------
# prepare all  stuff to hidden fields:
# daily notes and the monthname
#-------------------------------------------------
def makeDayContent(year,month,notes):
    S=''
    for i in range(0,32):
        T=NONOTES
        if notes.has_key(i):
            T=notes[i]
        S+='\n<div id="%s" class="hiddenday">%s</div>'%(str(i),T)
    S+='\n<div id="monthname" class="hiddenmonth">%s</div>'%monthlist[month]
    return S

#--------------------------------------------------
# Prepare main calendar web page
#--------------------------------------------------
def prepareMonth(year,month,day):
    if day <= 0:
        day=1
    noteDictionary=prepareDictionary(year,month)
    S=loadFile(MONTHFILE)
    # shortcut to this day
    daytext=NONOTES
    if noteDictionary.has_key(day):
        daytext=noteDictionary[day].strip()
        daytext=daytext.replace('#','\n')
        daytext=daytext.replace('\n\n','\n')
    S=S.replace('#daytext#',daytext)
    S=S.replace('#day#',str(day))
    T=makeTable(year,month,noteDictionary)
    S=S.replace('#table#',T)
    T=makeMonthRefList(year, month)
    S=S.replace('#reflist#',T)
    S=S.replace('#month#',str(month))
    S=S.replace('#monthname#',monthlist[month])
    S=S.replace('#year#',str(year))
    S=S.replace('#next_year#',str(year+1))
    S=S.replace('#previous_year#',str(year-1))
    T=makeDayContent(year,month,noteDictionary)
    S=S.replace('#content#',T)
    S=S.replace('#scriptpath#',SCRIPTPATH)
    if USEPASSWORD:
        S=S.replace('#password',NEEDPWD+'<input type="password" name="password"/>')
    else:
        S=S.replace('#password','')
    print S
#--------------------------------------------------
# Update notes for one  day
#--------------------------------------------------
def updateDay(year,month,day,content):
    # fix content such that lf or cr-lf is replaced by #
    content=content.replace('\r\n','\n')
    content=content.replace('\n\n','\n')
    content=content.replace('\n','#')
    notes=prepareDictionary(year,month)
    notes[day]=content
    # dictionary to XML-string
    T='<?xml version="1.0" encoding="ISO-8859-1"?>\n<notes>%s</notes>'
    S=''
    for i in range(0,32):
        if notes.has_key(i):
            S+='\n<day dayno="%s">\n%s\n</day>'%(str(i),notes[i].strip())
    T=T%S
    # save updated XML-file
    storeFile(getMonthsXMLFilename(year,month),T)

#--------------------------------------------------
# Start, prepares for this month
#--------------------------------------------------
def start():
    # makes a calendar for the month we have just now
    t=time.localtime()
    prepareMonth(t[0],t[1],t[2])
#-----------------------------------------------------
# Prepare the printable HTML-page
#-----------------------------------------------------
def preparePrintMonth(year,month,theDay):
    # get the skeleton file
    htmlstr=loadFile(PRINTMONTHFILE)
    # get the data
    notes=prepareDictionary(year,month)
    # make the table, outer loop is week
    # inner is day
    weeklist=calendar.monthcalendar( year, month)
    # heading
    T='<tr>'
    for d in range(0,7):
        T+='<th>'+daylist[d]+'</th>'
    T+='</tr>'
    # content
    for week in weeklist:
        T+='\n<tr>\n'
        for day in week:
            if day==0:
                T+='<td></td>'
                continue
            # prepare the content of this cell
            T+='\n<td>'
            T+='<div class="verysmall">'+str(day)+'.</div>'
            S=''
            if notes.has_key(day):
                    S=notes[day].strip()
                    S=S.replace('#','<br/>')
            T+='\n<div>%s</div>'%S
        T+='\n</tr>\n'
    htmlstr=htmlstr.replace('#table#',T)
    htmlstr=htmlstr.replace('#monthname#',monthlist[month])
    htmlstr=htmlstr.replace('#month#',str(month))
    htmlstr=htmlstr.replace('#year#',str(year))
    htmlstr=htmlstr.replace('#day#',str(theDay))
    htmlstr=htmlstr.replace('#scriptpath#',SCRIPTPATH)
    print htmlstr
#-------------------------------------------------------
# print the calendar for this year
#-------------------------------------------------------
def prepareYear(y=2005):
    calendar.setfirstweekday(calendar.MONDAY)
    import cStringIO
    saveout = sys.stdout
    tmpF=cStringIO.StringIO()
    sys.stdout = tmpF
    calendar.prcal(y)
    sys.stdout = saveout
    T=tmpF.getvalue()
    org_monthlist=['dummy','January','February','March','April','May','June',
                   'July','August','September','October','November','December']
    org_daylist=['Mo','Tu','We','Th','Fr','Sa','Su']
    for i in range(1,len(monthlist)):
        T=T.replace(org_monthlist[i],'<strong>'+monthlist[i]+'</strong>')
    for i in range(0,len(daylist)):
        T=T.replace(org_daylist[i],'<span style="color:blue">'+daylist[i][0:2]+'</span>')
    T=T.replace(str(y),'<strong>'+str(y)+'</strong>')

    S='<html><head><title>aar</title></head><body><pre>'
    S+=T
    S+='</pre>'
    S+='<div><a href="javascript:history.back()">&lt;&lt;&lt;</a></div>'
    S+='</body></html>'
    print S
#-----------------------------------------------------
# The scriptpart finding out what to do
#
form=cgi.FieldStorage()
print 'Content-type: text/html; charset=iso-8859-1\n'
#-----------------------------------------
# Preparing some defaults

# Tell calender that monday is first day of week
calendar.setfirstweekday(calendar.MONDAY)
# Do we have a Password, only used when update
pwd='no-pass-wrd'
if form.has_key('password'):
    pwd=form['password'].value
# default Date is today: y,m,d
t=time.localtime()
this_y,this_m,this_d=t[0],t[1],t[2]
y,m,d=t[0],t[1],t[2]
if form.has_key('year'):
    # we allow only 4 years ahead and back
    y=min(max(int(form['year'].value),this_y-4),this_y+4)
if form.has_key('month'):
    m=min(max(int(form['month'].value),1),12)
if form.has_key('day'):
    d=min(max(int(form['day'].value),1),calendar.monthrange(y,m)[1])
#notes for the day (updating)
c=''
if form.has_key('daytext'):
    c=form['daytext'].value
#-------------------------------------------------
# what is the job ?
#-------------------------------------------------
if form.has_key('command'):
    cmd=form['command'].value
    if cmd=='show':
        # show the actual month
        prepareMonth(y,m,1)
    elif cmd=='update':
            # Password control ? and !cookie set
        if USEPASSWORD and pwd!=PASSWD:
            reportErrorAndExit(WRONGPWD)
        # update a certain day and then show the actual month
        # set cookie
        updateDay(y,m,d,c)
        prepareMonth(y,m,d)
    elif cmd=='print':
            # show printversion of the actual month
        preparePrintMonth(y,m,d)
    elif cmd=='printyear':
            # show printversion of the actual year
         prepareYear(y)
    else:
        # show this month
        start()
else:
    start()

Merk at skriptet gjør en rekke replace-kall for å befolke templatefilene med riktige verdier. Templatefilene inneholder en rekke stringer av typen #something# som skal erstattes av skriptet.

Javascript

Template-fila, month_template.html, som viser fram en månedsoversikt bruker et javascript for å la brukeren navigere på dagene i måneden. Dette baserer seg på at alle dagsnotatene er plasserte i HTML-file i skjulte div-elementer (display:none). Når brukeren trykker på en dag, hentes innholdet i det skjulte feltet fram og plasseres i en textboks, textarea, hvor det kan inspiseres og endres. Javascriptet som gjør dette ser slik ut:

  function showday(dayno)
  {
    //---------------------------
    // set header to daily note
    headerNode=document.getElementById("todayheader");
    monthNode=document.getElementById("monthname");
    while(headerNode.hasChildNodes())
      headerNode.removeChild(headerNode.lastChild);
    daystr="";
    if (dayno != 0)
      daystr=""+dayno+". "+monthNode.innerHTML;
    headerNode.appendChild(document.createTextNode(daystr));

    //---------------------------
    //mark hidden input field day input
    dayInputNode=document.getElementById("inputday");
    dayInputNode.setAttribute("value",dayno);

    //-----------------------------
    // display content for this day in textarea
    fromNode=document.getElementById(dayno);
    s=fromNode.firstChild.data;
    while (s.indexOf('\n')!=-1)
      s=s.replace('\n','')
    while (s.indexOf('#')!=-1)
      s=s.replace('#','\n')
    while (s.indexOf('\n\n')!=-1)
      s=s.replace('\n\n','\n')
    window.document.form1.daytext.value = s;
  }
  
Referanser

En pakke med de filene du trenger for å modifisere og implementere din egen kalender: kalenderfiler.zip.

Du må endre minst to ting i pythonskriptet (SCRIPTPATH og DATAPATH), og du kan endre mange ting i HTML-filene for å få utseendet slik du ønsker.

Vedlikehold
Børre Stenseth, endret okt 2009.
( Velkommen ) Eksempler >Kalender ( Vinkjeller )