summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsubstantialnoninfringinguser <substantialnoninfringinguser@gmail.com>2009-05-13 05:25:17 +0100
committersubstantialnoninfringinguser <substantialnoninfringinguser@gmail.com>2009-05-13 05:25:17 +0100
commit7aee3fb920a1477332d78c8f3fb546da428be6e8 (patch)
treecdfacfc1ef181881d2ac1c7f4c8d4bce2ec917eb
parent8c818906b5c1228a6fb411cb96d1bd5f1663b49a (diff)
downloadtroggle-7aee3fb920a1477332d78c8f3fb546da428be6e8.tar.gz
troggle-7aee3fb920a1477332d78c8f3fb546da428be6e8.tar.bz2
troggle-7aee3fb920a1477332d78c8f3fb546da428be6e8.zip
[svn] QM parser now parses Hauchhoehle QMs.py
Photo model added. Logbook parser now puts mugshots in as photo models, and descriptions from the old folk html pages in as "blurbs" on the person model. Experimented with eye candy and a random logbook quote generator. Copied from http://cucc@cucc.survex.com/svn/trunk/expoweb/troggle/, rev. 8094 by aaron @ 12/31/2008 2:59 AM
-rw-r--r--expo/admin.py3
-rw-r--r--expo/models.py30
-rw-r--r--expo/randomLogbookSentance.py24
-rw-r--r--expo/views_other.py8
-rw-r--r--media/SilkRoadsilouetteAndrew.pngbin0 -> 70596 bytes
-rw-r--r--media/css/main2.css33
-rw-r--r--media/js/base.js46
-rw-r--r--media/loserBanner.jpgbin0 -> 220305 bytes
-rw-r--r--media/open-quote.gifbin0 -> 187 bytes
-rw-r--r--parsers/QMs.py71
-rw-r--r--parsers/logbooks.py31
-rw-r--r--templates/base.html100
-rw-r--r--templates/logbookentry.html3
-rw-r--r--urls.py1
14 files changed, 259 insertions, 91 deletions
diff --git a/expo/admin.py b/expo/admin.py
index 4dc13e1..be517b8 100644
--- a/expo/admin.py
+++ b/expo/admin.py
@@ -8,7 +8,7 @@ class RoleInline(admin.TabularInline):
class SurvexBlockAdmin(admin.ModelAdmin):
inlines = (RoleInline,)
-
+admin.site.register(Photo)
admin.site.register(Cave)
admin.site.register(Area)
admin.site.register(OtherCaveName)
@@ -24,3 +24,4 @@ admin.site.register(Role)
admin.site.register(LogbookEntry)
admin.site.register(PersonTrip)
admin.site.register(QM)
+
diff --git a/expo/models.py b/expo/models.py
index 930028b..69f9549 100644
--- a/expo/models.py
+++ b/expo/models.py
@@ -27,8 +27,9 @@ class Expedition(models.Model):
class Person(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
- is_vfho = models.BooleanField()
+ is_vfho = models.BooleanField(help_text="VFHO is the Vereines f&uuml;r H&ouml;hlenkunde in Obersteier, a nearby Austrian caving club.")
mug_shot = models.CharField(max_length=100, blank=True,null=True)
+ blurb = models.TextField(blank=True,null=True)
def __unicode__(self):
return "%s %s" % (self.first_name, self.last_name)
@@ -55,7 +56,6 @@ class PersonExpedition(models.Model):
-
#class LogbookSentanceRating(models.Model):
# rating = models.IntegerField()
@@ -157,10 +157,7 @@ class LogbookEntry(models.Model):
author = models.ForeignKey(PersonExpedition,blank=True,null=True) # the person who writes it up doesn't have to have been on the trip
title = models.CharField(max_length=200)
cave = models.ForeignKey(Cave,blank=True,null=True)
- # this will be a foreign key of the place the logbook is describing - JT
place = models.CharField(max_length=100,blank=True,null=True)
- # adding optional cave reference
-# cave = models.ForeignKey(Cave,blank=True,null=True)
text = models.TextField()
# several PersonTrips point in to this object
@@ -273,4 +270,25 @@ class QM(models.Model):
#dateKilled = models.DateField(blank=True)
def __str__(self):
QMnumber=str(self.found_by.cave)+'-'+str(self.found_by.date.year)+"-"+str(self.number)+self.grade
- return str(QMnumber) \ No newline at end of file
+ return str(QMnumber)
+
+class Photo(models.Model):
+ caption = models.CharField(max_length=1000,blank=True,null=True)
+ contains_person_trip = models.ManyToManyField(PersonTrip,blank=True,null=True)
+ contains_person = models.ManyToManyField(Person,blank=True,null=True)
+ file = models.ImageField(upload_to='photos',)
+ is_mugshot = models.BooleanField(default=False)
+ contains_cave = models.ForeignKey(Cave,blank=True,null=True)
+ contains_entrance = models.ForeignKey(Entrance, related_name="photo_file",blank=True,null=True)
+ nearest_survey_point = models.ForeignKey(SurveyStation,blank=True,null=True)
+ nearest_QM = models.ForeignKey(QM,blank=True,null=True)
+
+
+ lon_utm = models.FloatField(blank=True,null=True)
+ lat_utm = models.FloatField(blank=True,null=True)
+ #content_type = models.ForeignKey(ContentType)
+ #object_id = models.PositiveIntegerField()
+ #location = generic.GenericForeignKey('content_type', 'object_id')
+
+ def __str__(self):
+ return self.caption
diff --git a/expo/randomLogbookSentance.py b/expo/randomLogbookSentance.py
new file mode 100644
index 0000000..d4d3f09
--- /dev/null
+++ b/expo/randomLogbookSentance.py
@@ -0,0 +1,24 @@
+import troggle.settings as settings
+from django import forms
+from expo.models import LogbookEntry
+import random
+import re
+
+def weighted_choice(lst):
+ n = random.uniform(0,1)
+ for item, weight in lst:
+ if n < weight:
+ break
+ n = n - weight
+ return item
+
+def randomLogbookSentence():
+ #Choose a random logbook entry
+ randSent={}
+ randSent['entry']=LogbookEntry.objects.order_by('?')[0]
+
+ #Choose a random sentence from that entry. Store the sentence as randSent['sentence'], and the number of that sentence in the entry as randSent['number']
+ sentenceList=re.findall('[A-Z].*?\.',randSent['entry'].text)
+ randSent['number']=random.randrange(0,len(sentenceList))
+ randSent['sentence']=sentenceList[randSent['number']]
+ return randSent \ No newline at end of file
diff --git a/expo/views_other.py b/expo/views_other.py
index 808238e..88a8559 100644
--- a/expo/views_other.py
+++ b/expo/views_other.py
@@ -3,6 +3,8 @@ from troggle.expo.models import Cave, Expedition, Person, LogbookEntry
import troggle.settings as settings
from django import forms
from django.db.models import Q
+import re
+import randSent
def stats(request):
statsDict={}
@@ -10,4 +12,8 @@ def stats(request):
statsDict['caveCount'] = int(Cave.objects.count())
statsDict['personCount'] = int(Person.objects.count())
statsDict['logbookEntryCount'] = int(LogbookEntry.objects.count())
- return render_to_response('statistics.html', statsDict) \ No newline at end of file
+ return render_to_response('statistics.html', statsDict)
+
+def frontPage(request):
+
+ return render_to_response('index.html', {'randSent':randSent.randomLogbookSentence(),'settings':settings}) \ No newline at end of file
diff --git a/media/SilkRoadsilouetteAndrew.png b/media/SilkRoadsilouetteAndrew.png
new file mode 100644
index 0000000..2ab357a
--- /dev/null
+++ b/media/SilkRoadsilouetteAndrew.png
Binary files differ
diff --git a/media/css/main2.css b/media/css/main2.css
index 523b1f7..b0810c6 100644
--- a/media/css/main2.css
+++ b/media/css/main2.css
@@ -2,11 +2,18 @@
.centre { text-align: center; }
.plus2pt { font-size: 160%; }
-body, td, center, ul, p, input { color: #000; font-family: sans-serif; }
+body, td, center, ul, p, input {
+ color: rgb(204, 204, 102);
+ font-family: sans-serif;
+}
+body {
+ background-color: #000;
+}
+
a:link, a:visited { text-decoration: none; }
div.centre img { vertical-align: middle; }
-h1 { text-align: center; font-size: 210%; line-height: 100%; }
+h1 { text-align: center; font-size: 210%; opacity:0.7; filter:alpha(opacity=70);}
h2 { color: #009900; }
h3 { color: #2c105e; }
h4 { color: #0d664c; }
@@ -19,6 +26,23 @@ img.onleft, div.onleft { vertical-align: top; float: left;
margin-left: 8pt; }
img.icon { vertical-align: middle; }
img.aligntop { vertical-align: top; }
+blockquote {
+ font: Georgia, "Times New Roman", Times, serif;
+ font-weight:bold;
+ font-variant:small-caps;
+ width: 400px;
+ background: url(../close-quote.gif) no-repeat right bottom;
+ padding-left: 25px;
+ text-indent: -25px;
+ text-align: right;
+ vertical-align:bottom;
+ color:#CCCC66;
+}
+blockquote:first-letter {
+ background: url(../open-quote.gif) no-repeat left top;
+ padding-left: 40px;
+ font: italic 1.4em Georgia, "Times New Roman", Times, serif;
+}
table.imgtable { margin-left: auto; margin-right: auto; }
table.imgtable td { vertical-align: middle; text-align: center;
padding: 10px; }
@@ -38,6 +62,7 @@ table.trad td, table.trad th { margin: 0pt; border: 1px solid #aaa;
html, body, div.contents {
min-height: 100%;
height: 100%;
+width:100%;
}
html>body, html>body div.contents {
height: auto;
@@ -47,12 +72,12 @@ body {
div.contents {
position: absolute;
top: 0;
-left: 0;
+right: 0;
}
div.footer {
position: fixed;
bottom: 2px;
-right: 2px;
+left: 2px;
}
div.main {
margin-bottom: 3em;
diff --git a/media/js/base.js b/media/js/base.js
new file mode 100644
index 0000000..17a664e
--- /dev/null
+++ b/media/js/base.js
@@ -0,0 +1,46 @@
+
+ function showDiv(collapsed,expanded){
+ document.getElementById(collapsed).style.display = 'none';
+ document.getElementById(expanded).style.display = 'block';
+ }
+
+ function hideDiv(collapsed,expanded){
+ document.getElementById(collapsed).style.display = 'block';
+ document.getElementById(expanded).style.display = 'none';
+ }
+
+ function makeDivTransparent(div){
+ document.getElementById(div).style.backgroundColor = 'transparent';
+ }
+
+
+
+hex=0 // Initial color value.
+leftPos=25
+year=1976
+currentDate= new Date()
+currentYear = currentDate.getFullYear()
+function fadeText(){
+if(hex<153) { //If color is not black yet
+ hex=hex+10; // increase color darkness
+ leftPos-=1;
+ document.getElementById("expoHeader").style.color="rgb("+0+","+hex+","+0+")";
+// document.getElementById("expoFinalDate").style.color="rgb("+0+","+hex+","+0+")";
+ document.getElementById("expoHeader").style.left=leftPos;
+ setTimeout("fadeText()",50)
+ setTimeout("countUpYear()",1000)
+}
+else {
+ hex=0;
+ leftPos=25;
+}
+}
+
+function countUpYear(){
+ if (year<currentYear) {
+// alert (year+''+currentYear)
+ year=year+1
+ document.getElementById("expoFinalDate").innerHTML="<h1>"+year+"</h1>"
+ setTimeout("countUpYear()",1000)
+ }
+} \ No newline at end of file
diff --git a/media/loserBanner.jpg b/media/loserBanner.jpg
new file mode 100644
index 0000000..abcdac3
--- /dev/null
+++ b/media/loserBanner.jpg
Binary files differ
diff --git a/media/open-quote.gif b/media/open-quote.gif
new file mode 100644
index 0000000..26330dc
--- /dev/null
+++ b/media/open-quote.gif
Binary files differ
diff --git a/parsers/QMs.py b/parsers/QMs.py
index 44c38c7..96b91fa 100644
--- a/parsers/QMs.py
+++ b/parsers/QMs.py
@@ -8,29 +8,56 @@ import re
QM.objects.all().delete()
-
-
-def parseSteinbrQMs():
- try:
- steinBr=Cave.objects.get(official_name="Steinbr&uuml;ckenh&ouml;hle")
- except Cave.DoesNotExist:
- print "Steinbruckenhoehle is not in the database. Please run parsers.cavetab first."
- return
-
+def parseCaveQMs(cave,pathToCSV):
+ if cave=='stein':
+ try:
+ steinBr=Cave.objects.get(official_name="Steinbr&uuml;ckenh&ouml;hle")
+ except Cave.DoesNotExist:
+ print "Steinbruckenhoehle is not in the database. Please run parsers.cavetab first."
+ return
+ elif cave=='hauch':
+ try:
+ hauchHl=Cave.objects.get(official_name="Hauchh&ouml;hle")
+ except Cave.DoesNotExist:
+ print "Steinbruckenhoehle is not in the database. Please run parsers.cavetab first."
+ return
- qmPath = settings.EXPOWEB+r"smkridge/204/qm.csv"
- qmReader = csv.reader(open(qmPath,'r'),dialect="excel-tab")
+ qmPath = settings.EXPOWEB+pathToCSV
+ qmCSVContents = open(qmPath,'r')
+ dialect=csv.Sniffer().sniff(qmCSVContents.read())
+ qmCSVContents.seek(0,0)
+ qmReader = csv.reader(qmCSVContents,dialect=dialect)
qmReader.next() # Skip header row
for line in qmReader:
- year=int(line[0][1:5])
-
- #check if placeholder exists for given year, create it if not
- placeholder, hadToCreate = LogbookEntry.objects.get_or_create(date__year=year, text="placeholder for QMs in 204", defaults={"date": date(year, 1, 1),"cave":steinBr})
- if hadToCreate:
- print "204 placeholder logbook entry for " + str(year) + " added to database"
- QMnum=re.match(r".*?-\d*?-X?(?P<numb>\d*)",line[0]).group("numb")
- newQM = QM(found_by=placeholder,number=QMnum,grade=line[1],area=line[2],location_description=line[3],nearest_station_description=line[4],completion_description=line[5],comment=line[6])
- newQM.save()
- print "QM "+str(newQM) + " added to database"
+ try:
+ year=int(line[0][1:5])
+ #check if placeholder exists for given year, create it if not
+ if cave=='stein':
+ placeholder, hadToCreate = LogbookEntry.objects.get_or_create(date__year=year, text="placeholder for QMs in 204", defaults={"date": date(year, 1, 1),"cave":steinBr})
+ elif cave=='hauch':
+ placeholder, hadToCreate = LogbookEntry.objects.get_or_create(date__year=year, text="placeholder for QMs in 234", defaults={"date": date(year, 1, 1),"cave":hauchHl})
+ if hadToCreate:
+ print cave+" placeholder logbook entry for " + str(year) + " added to database"
+ QMnum=re.match(r".*?-\d*?-X?(?P<numb>\d*)",line[0]).group("numb")
+ newQM = QM()
+ newQM.found_by=placeholder
+ newQM.number=QMnum
+ if line[1]=="Dig":
+ newQM.grade="D"
+ else:
+ newQM.grade=line[1]
+ newQM.area=line[2]
+ newQM.location_description=line[3]
+ newQM.nearest_station_description=line[4]
+ newQM.completion_description=line[5]
+ newQM.comment=line[6]
+ newQM.save()
+ print "QM "+str(newQM) + " added to database"
+ except KeyError:
+ continue
+# except IndexError:
+# print "Index error in " + str(line)
+# continue
-parseSteinbrQMs() \ No newline at end of file
+parseCaveQMs(cave='stein',pathToCSV=r"smkridge/204/qm.csv")
+parseCaveQMs(cave='hauch',pathToCSV=r"smkridge/234/qm.csv")
diff --git a/parsers/logbooks.py b/parsers/logbooks.py
index d0d4f4c..75caeaf 100644
--- a/parsers/logbooks.py
+++ b/parsers/logbooks.py
@@ -45,10 +45,37 @@ def LoadPersons():
pObject = models.Person(first_name = firstname,
last_name = lastname,
is_vfho = person[header["VfHO member"]],
- mug_shot = person[header["Mugshot"]])
- pObject.save()
+ )
+
is_guest = person[header["Guest"]] == "1" # this is really a per-expo catagory; not a permanent state
+ pObject.save()
+ #create mugshot Photo instance
+ mugShotPath = settings.EXPOWEB+"folk/"+person[header["Mugshot"]]
+ if mugShotPath[-3:]=='jpg': #if person just has an image, add it
+ mugShotObj = models.Photo(
+ caption="Mugshot for "+firstname+" "+lastname,
+ is_mugshot=True,
+ file=mugShotPath,
+ )
+ mugShotObj.save()
+ mugShotObj.contains_person.add(pObject)
+ mugShotObj.save()
+ elif mugShotPath[-3:]=='htm': #if person has an html page, find the image(s) and add it. Also, add the text from the html page to the "blurb" field in his model instance.
+ personPageOld=open(mugShotPath,'r').read()
+ pObject.blurb=re.search('<body>.*<hr',personPageOld,re.DOTALL).group() #this needs to be refined, take care of the HTML and make sure it doesn't match beyond the blurb
+ for photoFilename in re.findall('i/.*?jpg',personPageOld,re.DOTALL):
+ mugShotPath=settings.EXPOWEB+"folk/"+photoFilename
+ mugShotObj = models.Photo(
+ caption="Mugshot for "+firstname+" "+lastname,
+ is_mugshot=True,
+ file=mugShotPath,
+ )
+ mugShotObj.save()
+ mugShotObj.contains_person.add(pObject)
+ mugShotObj.save()
+ pObject.save()
+
for year, attended in zip(headers, person)[5:]:
yo = models.Expedition.objects.filter(year = year)[0]
if attended == "1" or attended == "-1":
diff --git a/templates/base.html b/templates/base.html
index 6acd6ac..66d0529 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -1,68 +1,60 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html lang="en">
<head>
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
- <link rel="stylesheet" type="text/css" href="{{ settings.MEDIA_URL }}css/main2.css" />
- <title>{% block title %}{% endblock %}</title>
-
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+<link rel="stylesheet" type="text/css" href="{{ settings.MEDIA_URL }}css/main2.css" />
+<title>{% block title %}{% endblock %}</title>
<!-- script to toggle navbar -->
-
-<script language="javascript">
-<!--
- function showFooter(){
- document.getElementById('footerHidden').style.display = 'none';
- document.getElementById('footerShowing').style.display = 'block';
- }
-
- function hideFooter(){
- document.getElementById('footerHidden').style.display = 'block';
- document.getElementById('footerShowing').style.display = 'none';
- }
-
- function makeTransparent(){
- document.getElementById('footerShowing').style.backgroundColor = 'transparent';
- }
--->
-</script>
-
+<script src="{{ settings.MEDIA_URL }}js/base.js" type="text/javascript"></script>
</head>
<body>
-<div class="contents">
-<div class="main">
- {% block content %}{% endblock %}
+<div id="editLink" style="right:0px; top:0px; text-align: right; position: absolute; top:0; right:0; z-index:1">
+ {% block editLink %}not editable
+ {% endblock %}
</div>
- {% block footer %}
-
-<div class="footer" id="footerHidden" style="display:none">
- <h4><a href="javascript:;" onMouseDown="showFooter();">[Show Troggle Navigation / Search]</a></h4>
+<div id="bottomRight" style="position:absolute;right:0;bottom:0">
+<img src="{{ settings.MEDIA_URL }}SilkRoadsilouetteAndrew.png" id="bridge" style="position:absolute; right:60px; bottom:0;z-index:-1;">
+{% block lgbkQuote %}{% endblock %}
</div>
-<div class="footer" id="footerShowing" style="background-color:#CCC">
- <h4 class="navbar"> Troggle navigation <a href="javascript:;" onMouseDown="hideFooter();">[Hide]</a> or <a href="javascript:;" onMouseDown="makeTransparent();">[Make transparent]</a></align></h4>
- <table class="normal">
- <tr>
- <td rowspan="2">
- <a href="{{ settings.URL_ROOT }}cave">list caves</a> or
- <form name="input" action="{{ settings.URL_ROOT }}cavesearch" method="get">
- <input type="text" name="q" value="search caves">
- <input type="submit" value="Submit">
- </form>
- </td>
- <td rowspan="2">
- <a href="{{ settings.URL_ROOT }}logbookentry">list logbook entries</a> or
- <form name="input" action="{{ settings.URL_ROOT }}logbooksearch" method="get">
- <input type="text" name="q" value="search logbooks">
- <input type="submit" value="Submit">
- </form>
- </td>
- <td><a href="{{ settings.URL_ROOT }}person">list cavers</a></td>
- <td><a href="{{ settings.URL_ROOT }}statistics"> statistics</a></td>
- <tr>
+<div id="headerBackBar" style="background-color:#CCCC66; position:absolute; left:0; right:0; z-index:-2; top:60px; opacity:0.7; filter:alpha(opacity=70);height:75px; color:#000000; text-indent:100px">
+ <p>
+ <br>
+ {% block currentLocation %}
+ Welcome to the website of the Cambridge University Caving Club's expeditions to Austria.
+ {% endblock %}
+ </p>
+</div>
+<div class="contents">
+<div class="main"> {% block content %}{% endblock %} </div>
+{% block footer %}
+<div class="footer" id="footerHidden">
+ <h4><a href="javascript:;" onMouseDown="showDiv('footerHidden','footerShowing');">[Show Troggle Navigation / Search]</a></h4>
+</div>
+<div class="footer" id="footerShowing" style="background-color:#CCC; display:none">
+ <h4 class="navbar"> Troggle navigation <a href="javascript:;" onMouseDown="hideDiv('footerHidden','footerShowing');">[Hide]</a> or <a href="javascript:;" onMouseDown="makeDivTransparent('footerShowing');">[Make transparent]</a>
+ </align>
+ </h4>
+ <table class="normal">
+ <tr>
+ <td rowspan="2"><a href="{{ settings.URL_ROOT }}cave">list caves</a> or
+ <form name="input" action="{{ settings.URL_ROOT }}cavesearch" method="get">
+ <input type="text" name="q" value="search caves">
+ <input type="submit" value="Submit">
+ </form></td>
+ <td rowspan="2"><a href="{{ settings.URL_ROOT }}logbookentry">list logbook entries</a> or
+ <form name="input" action="{{ settings.URL_ROOT }}logbooksearch" method="get">
+ <input type="text" name="q" value="search logbooks">
+ <input type="submit" value="Submit">
+ </form></td>
+ <td><a href="{{ settings.URL_ROOT }}person">list cavers</a></td>
+ <td><a href="{{ settings.URL_ROOT }}statistics"> statistics</a></td>
+ <tr>
<td><a href="{{ settings.URL_ROOT }}photos">photos</a></td>
<td><a href="{{ settings.URL_ROOT }}admin"> admin</a></td>
- </table>
+ </table>
</div>
- <p>{% endblock %}</p>
-</body>
+<p>{% endblock %}</p>
+</body>
</html> \ No newline at end of file
diff --git a/templates/logbookentry.html b/templates/logbookentry.html
index b76d9ab..2e34ba3 100644
--- a/templates/logbookentry.html
+++ b/templates/logbookentry.html
@@ -2,7 +2,7 @@
{% load wiki_markup %}
{% block title %}Logbook {{logbookentry.id}}{% endblock %}
-
+{% block editLink %}<a href="{{settings.URL_ROOT}}admin/expo/logbookentry/{{logbookentry.id}}">edit </a>{% endblock %}
{% block content %}
<div class="logbookblock">
<h2>{{logbookentry.title}} - {{logbookentry.date}}</h2>
@@ -11,6 +11,7 @@
{% for persontrip in logbookentry.persontrip_set.all %}
<li>
<a href="/person/{{persontrip.personexpedition.person.id}}">{{persontrip.personexpedition}}</a>
+ <a href="{{settings.URL_ROOT}}admin/expo/logbookentry/{{logbookentry.id}}">edit </a>
{% ifequal persontrip.personexpedition logbookentry.author %}
(author)
{% endifequal %}
diff --git a/urls.py b/urls.py
index 22e4336..787b1ff 100644
--- a/urls.py
+++ b/urls.py
@@ -6,6 +6,7 @@ admin.autodiscover()
urlpatterns = patterns('',
# Example:
+ (r'^$', frontPage),
(r'^cave/?$', caveindex),
(r'^cave/(?P<cave_id>[^/]+)/?$', cave),
(r'^cave/(?P<cave_id>[^/]+)/?(?P<ent_letter>[^/])$', ent),