New Peiod widget
authorJean-Michel Nirgal Vourgère <jmv@nirgal.com>
Fri, 12 Nov 2010 23:13:01 +0000 (23:13 +0000)
committerJean-Michel Nirgal Vourgère <jmv@nirgal.com>
Fri, 12 Nov 2010 23:13:01 +0000 (23:13 +0000)
Imported & patched calendar widget from django admin
Added HistoryForm for vessel history download (start/end date)
More detailed info on running jobs

14 files changed:
bin/djais/models.py
bin/djais/settings.py
bin/djais/views.py
bin/djais/widgets.py [new file with mode: 0644]
html_templates/base.html
html_templates/fragment_vessel_history.html [new file with mode: 0644]
html_templates/job.html
html_templates/vessel.html
html_templates/vessel_history.html [new file with mode: 0644]
www/DateTimeShortcuts.js [new file with mode: 0644]
www/calendar.css [new file with mode: 0644]
www/calendar.js [new file with mode: 0644]
www/global.css
www/global.js

index 938c6fdce394f81d28de2192f4375b4e3f08d024..ebdc864c20125dfc4b36def72d918481ad1aa800 100644 (file)
@@ -230,6 +230,10 @@ class Job(models.Model):
         dt = self.finish_time - self.start_time
         return nice_timedelta_str(dt)
 
+    def running_time(self):
+        dt = datetime.utcnow() - self.start_time
+        return nice_timedelta_str(dt)
+
     def get_stats(self):
         result = {}
         try:
@@ -237,6 +241,8 @@ class Job(models.Model):
         except:
             return
         # man 5 proc
+        # TODO:
+        # "getconf CLK_TCK" = 100 -> 1 tick = 1/100 seconds
         strstats = strstats.rstrip('\n').split(' ')
         for i, key in enumerate(('pid', 'comm', 'state', 'ppid', 'pgrp', 'session', 'tty_nr', 'tpgid', 'flags', 'minflt', 'cminflt', 'majflt', 'cmajflt', 'utime', 'stime', 'cutime', 'cstime', 'priority', 'nice', 'num_threads', 'itrealvalue', 'starttime', 'vsize', 'rss', 'rsslim', 'startcode', 'endcode', 'startstack', 'kstkesp', 'kstkeip', 'signal', 'blocked', 'sigignore', 'sigcatch', 'wchan', 'nswap', 'cnswap', 'exit_signal', 'processor', 'rt_priority', 'policy', 'delayacct_blkio_ticks', 'guest_time', 'cguest_time')):
             result[key] = strstats[i]
index 31936d1d2dbc69b19cb91e564d6be6e5ff099c46..db690b8813fe7b721c0763a200f5965947d7785d 100644 (file)
@@ -40,12 +40,12 @@ USE_I18N = True
 
 # Absolute path to the directory that holds media.
 # Example: "/home/media/media.lawrence.com/"
-MEDIA_ROOT = ''
+MEDIA_ROOT = '/usr/lib/ais/www/'
 
 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
 # trailing slash if there is a path component (optional in other cases).
 # Examples: "http://media.lawrence.com", "http://example.com/media/"
-MEDIA_URL = ''
+MEDIA_URL = '/'
 
 # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
 # trailing slash.
index a28bd2c7a977276f6cb1178ae5aceacbf108f84b..c1e0744b9aff4689c28081c6048c5ed9bdda009d 100644 (file)
@@ -21,6 +21,7 @@ from django.http import *
 from django.template import loader, RequestContext
 from django import forms
 from django.shortcuts import render_to_response, get_object_or_404
+from django.utils.safestring import mark_safe
 
 from decoratedstr import remove_decoration
 
@@ -33,6 +34,7 @@ from ais.inputs.common import is_id4_active
 from ais.inputs.stats import STATS_DIR
 from ais.inputs.config import peers_get_config
 from ais import jobrunner
+from ais.djais.widgets import *
 
 def auth(username, raw_password):
     try:
@@ -113,6 +115,106 @@ def vessel_search(request):
 
     return render_to_response('vessel_index.html', {'form': form}, RequestContext(request))
 
+class PeriodWidget(forms.MultiWidget):
+    '''
+    A widget that splits period number and period type in a integer and a choice widgets
+    '''
+    __periods = (
+        (u'1', u'second(s)'),
+        (u'60', u'minute(s)'),
+        (u'3600', u'hour(s)'),
+        (u'86400', u'day(s)'),
+        (u'2592000', u'month(es)'))
+
+    def __init__(self, attrs=None):
+        textattrs = { 'size': 3 }
+        if attrs:
+            textattrs.update(attrs)
+        widgets = (
+            forms.TextInput(attrs=textattrs),
+            forms.Select(choices=(self.__periods), attrs=attrs))
+        super(PeriodWidget, self).__init__(widgets, attrs)
+
+    def decompress(self, value):
+        if value:
+            if value >= 2592000 and not value % 2592000:
+                return [ unicode(value // 2592000), u'2592000' ]
+            if value >= 86400 and not value % 86400:
+                return [ unicode(value // 86400), u'86400' ]
+            if value >= 3600 and not value % 3600:
+                return [ unicode(value // 3600), u'3600' ]
+            if value >= 60 and not value % 60:
+                return [ unicode(value // 60), u'60' ]
+            return [unicode(value), u'1']
+        return [None, None]
+
+class PeriodField(forms.MultiValueField):
+    widget = PeriodWidget
+
+    def __init__(self, *args, **kargs):
+        fields = (
+            forms.IntegerField(),
+            forms.CharField(),
+        )
+        forms.MultiValueField.__init__(self, fields=fields, *args, **kargs)
+
+    def compress(self, data_list):
+        #print 'data_list=', data_list
+        if data_list and data_list[0] is not None and data_list[1] is not None:
+            try:
+                return data_list[0] * int(data_list[1])
+            except ValueError:
+                pass
+        return None
+
+
+class HistoryForm(forms.Form):
+    format = forms.ChoiceField(choices=(('track', 'Track line (Google Earth)'), ('animation', 'Animation (Google Earth)'), ('csv', 'Coma separated values (SpreadSheet)')), widget=forms.Select(attrs={'onchange': mark_safe("if (this.value=='csv') $('#csvhint').show(); else $('#csvhint').hide();")}))
+    period_type = forms.ChoiceField(choices=(('duration', 'Duration until now'), ('date_date','Between two dates'), ('start_duration', 'Start date and duration')), widget=forms.RadioSelect(attrs={'onchange': mark_safe("show_hide_start_end_time(this.value);")}))
+    start_date = forms.DateTimeField(required=False, widget=AisCalendarWidget(attrs={'class':'vDateField'}))
+    duration = PeriodField(required=False, label='Period length', initial=7*86400)
+    end_date = forms.DateTimeField(required=False, widget=AisCalendarWidget(attrs={'class':'vDateField'}))
+    grain = PeriodField(label='One position every', initial=3600)
+
+    def clean_start_date(self):
+        period_type = self.cleaned_data.get('period_type', None)
+        start_date = self.cleaned_data.get('start_date', None)
+        if period_type in (u'date_date', u'start_duration') and start_date is None:
+            raise forms.ValidationError('That is field is required.')
+        return start_date
+
+    def clean_duration(self):
+        period_type = self.cleaned_data.get('period_type', None)
+        duration = self.cleaned_data.get('duration', None)
+        print 'duration=', duration
+        if period_type in (u'duration', u'start_duration') and duration is None:
+            raise forms.ValidationError('That is field is required.')
+        return duration
+    
+    def clean_end_date(self):
+        period_type = self.cleaned_data.get('period_type', None)
+        end_date = self.cleaned_data.get('end_date', None)
+        if period_type in (u'date_date',) and end_date is None:
+            raise forms.ValidationError('That is field is required.')
+        return end_date
+
+    def clean(self):
+        cleaned_data = self.cleaned_data
+        period_type = self.cleaned_data.get('period_type', None)
+        start_date = self.cleaned_data.get('start_date', None)
+        #duration = self.cleaned_data.get('duration', None)
+        end_date = self.cleaned_data.get('end_date', None)
+        #if period_type in (u'date_date', u'start_duration') and start_date is None:
+        #    self._errors["start_date"] = self.error_class(['That field is required.'])
+        #if period_type in (u'duration', u'start_duration') and duration is None:
+        #    self._errors["duration"] = self.error_class(['That field is required.'])
+        #if period_type in (u'date_date',) and end_date is None:
+        #    self._errors["end_date"] = self.error_class(['That field is required.'])
+        if period_type == u'date_date' and start_date is not None and end_date is not None:
+            if start_date <= end_date:
+                self._errors["start_date"] = self.error_class(['Start date must be before end date.'])
+        return cleaned_data
+
 @http_authenticate(auth, 'ais')
 def vessel(request, strmmsi):
     mmsi = strmmsi_to_mmsi(strmmsi)
@@ -120,7 +222,7 @@ def vessel(request, strmmsi):
     nmea = Nmea.new_from_lastinfo(strmmsi)
     #if not nmea.timestamp_1 and not nmea.timestamp_5:
     #    raise Http404
-    return render_to_response('vessel.html', {'nmea': nmea}, RequestContext(request))
+    return render_to_response('vessel.html', {'nmea': nmea, 'form': HistoryForm()}, RequestContext(request))
 
 
 class VesselManualInputForm(forms.Form):
@@ -368,68 +470,66 @@ def vessel_manual_input(request, strmmsi):
 
 
 @http_authenticate(auth, 'ais')
-def vessel_history(request, strmmsi, format=None):
+def vessel_history(request, strmmsi):
     """
+    TODO
     That view is called from Google Earth, so that it must support GET method.  
     """
-    ndays = request.REQUEST.get('ndays', None)
-    if ndays is not None:
-        try:
-            ndays = int(ndays)
-        except ValueError:
-            ndays = 90
-        period = ndays * 86400
-    else:
-        period = request.REQUEST.get('period', u'1')
-        try:
-            period = int(period)
-        except ValueError:
-            period = 1
-        period_type = request.REQUEST.get('period_type', u'86400')
-        try:
-            period_type = int(period_type)
-        except ValueError:
-            period_type = 86400
-
-    grain = request.REQUEST.get('grain', 1)
-    try:
-        grain = int(grain)
-    except ValueError:
-        grain = 1
-    grain_type = request.REQUEST.get('grain_type', 3600)
-    try:
-        grain_type = int(grain_type)
-    except ValueError:
-        grain_type = 3600
-
-    date_end = datetime.utcnow()
-    date_start = date_end - timedelta(0,period*period_type)
-    nmea_iterator = NmeaFeeder(strmmsi, date_end, date_start, granularity=grain*grain_type)
-    
-    format = request.REQUEST.get('format', u'track')
-
-    if format == u'track':
-        command = u'show_targets_ships --start=\'' + date_start.strftime('%Y%m%d %H%M%S') + u'\' --granularity=' + unicode(grain*grain_type) + ' --format=track '+ strmmsi
-        extension = u'kmz'
-
-    elif format == u'animation':
-        command = u'show_targets_ships --start=\'' + date_start.strftime('%Y%m%d %H%M%S') + u'\' --granularity=' + unicode(grain*grain_type) + ' --format=animation '+ strmmsi
-        extension = u'kmz'
-    
-    elif format == u'csv':
-        command = u'common --start=\'' + date_start.strftime('%Y%m%d %H%M%S') + u'\' --granularity=' + unicode(grain*grain_type) + ' ' + strmmsi
-        extension = u'csv'
-    else:
-        raise Http404(u'Invalid archive format')
-
-    job = Job()
-    job.friendly_filename = u'%s.%s' % (strmmsi, extension)
-    job.user = request.user
-    job.command = command
-    job.save()
-    if not jobrunner.wakeup_daemon():
-        return HttpResponseServerError(jobrunner.DAEMON_WAKEUP_ERROR)
-    return HttpResponseRedirect('/job/%s/download' % job.id)
+    initial = {}
+    if request.method == 'POST':
+        form = HistoryForm(request.POST, initial=initial)
+        if form.is_valid():
+            data = form.cleaned_data
+            if data['period_type'] == 'duration':
+                date_start = datetime.utcnow() - timedelta(0, data['duration'])
+                date_end = None # Now
+            elif data['period_type'] == 'date_date':
+                date_start = data['start_date']
+                date_end = data['end_date']
+            else:
+                assert data['period_type'] == 'start_duration', ('Invalid period type %s' % data['period_type'])
+                date_start = data['start_date']
+                date_end = date_start + timedelta(0, data['duration'])
+
+            grain = data['grain']
+            
+            format = data['format']
+
+            if format == u'track':
+                command = u'show_targets_ships'
+                command += u' --format=track'
+                extension = u'kmz'
+
+            elif format == u'animation':
+                command = u'show_targets_ships'
+                command += u' --format=animation'
+                extension = u'kmz'
+            
+            elif format == u'csv':
+                command = u'common'
+                extension = u'csv'
+            else:
+                raise Http404(u'Invalid archive format')
+            
+            command += u' --start=\'' + date_start.strftime('%Y%m%d %H%M%S') + u'\''
+            if date_end:
+                command += u' --end=\'' + date_end.strftime('%Y%m%d %H%M%S') + u'\''
+            command += u' --granularity=' + unicode(grain)
+            command += u' ' + strmmsi
+
+            job = Job()
+            job.friendly_filename = u'%s.%s' % (strmmsi, extension)
+            job.user = request.user
+            job.command = command
+            job.save()
+            if not jobrunner.wakeup_daemon():
+                return HttpResponseServerError(jobrunner.DAEMON_WAKEUP_ERROR)
+            return HttpResponseRedirect('/job/%s/download' % job.id)
+    else: # GET
+        form = HistoryForm(initial=initial)
+    strmmsi = strmmsi.encode('utf-8')
+    nmea = Nmea.new_from_lastinfo(strmmsi)
+    return render_to_response('vessel_history.html', {'nmea': nmea, 'form':form}, RequestContext(request))
 
 
 @http_authenticate(auth, 'ais')
diff --git a/bin/djais/widgets.py b/bin/djais/widgets.py
new file mode 100644 (file)
index 0000000..bc092d8
--- /dev/null
@@ -0,0 +1,12 @@
+# -*- encoding: utf-8 -*-
+
+from django import forms
+
+class AisCalendarWidget(forms.TextInput):
+    class Media:
+        css = {
+            'all': ('calendar.css',)
+        }
+        js = ('calendar.js', 'DateTimeShortcuts.js')
+
+
index 01415943fb0ccf0733e3dcc657970977949fe8b1..1f7a498a5468f57d5874a7d57ab1796a1b63ff3e 100644 (file)
@@ -4,9 +4,9 @@
 <link rel=stylesheet href='/global.css'>
 <link rel=alternate type='application/rss+xml' title='All the news about AIS ship monitoring' href='https://ais.nirgal.com/news/feed'>
 <title>{% block title %}AIS{% endblock %}</title>
-{% block style_extra %}{% endblock %}
 <script src='/javascript/jquery/jquery.js' type='text/javascript'></script>
 <script src='/global.js' type='text/javascript'></script>
+{% block style_extra %}{% endblock %}
 
 <div id=header>
     <span id=bannertitle>AIS ship monitoring</span>
diff --git a/html_templates/fragment_vessel_history.html b/html_templates/fragment_vessel_history.html
new file mode 100644 (file)
index 0000000..54c53e1
--- /dev/null
@@ -0,0 +1,38 @@
+<span id=csvhint style="display:none;">Make sure you select "Charset: UTF-8" and "Separated by: Coma" when you <a href="/oocalc_howto.png">choose import options</a>.<br></span>
+
+<script type='text/javascript'>
+function show_hide_start_end_time(value) {
+    if (value=='duration') {
+        $('#id_start_date').attr('disabled', 'disabled');
+        $('#id_duration_0').removeAttr('disabled');
+        $('#id_duration_1').removeAttr('disabled');
+        $('#id_end_date').attr('disabled', 'disabled');
+    } else if (value=='date_date') {
+        $('#id_start_date').removeAttr('disabled');
+        $('#id_duration_0').attr('disabled', 'disabled');
+        $('#id_duration_1').attr('disabled', 'disabled');
+        $('#id_end_date').removeAttr('disabled');
+    } else if (value=='start_duration') {
+        $('#id_start_date').removeAttr('disabled');
+        $('#id_duration_0').removeAttr('disabled');
+        $('#id_duration_1').removeAttr('disabled');
+        $('#id_end_date').attr('disabled', 'disabled');
+    } else {
+        $('#id_start_date').attr('disabled', 'disabled');
+        $('#id_duration_0').attr('disabled', 'disabled');
+        $('#id_duration_1').attr('disabled', 'disabled');
+        $('#id_end_date').attr('disabled', 'disabled');
+    }
+}
+$(document).ready(function () {
+    show_hide_start_end_time($('input:radio[name=period_type]:checked').val());
+});
+</script>
+<form method=post action=history>
+{% include "fragment_formerror.html" %}
+<table>
+{{ form.as_table }}
+<tr><th><td>
+<input type=submit value=Get>
+</table>
+</form>
index 098568eb1c9d42f9cf6ec6a3d41a9ee4657c6610..6300602e157e0cd3524321d1c47b7aeb349a18b8 100644 (file)
@@ -16,15 +16,20 @@ Result size: {{ job.get_sucess_size|filesizeformat }}<br>
 <a href="/job/{{ job.id }}/download" class=button>download</a>{% endif %}<br>
 {% else %}
     {% if job.start_time %}
-    Status: <b>Running</b> since {{ job.start_time }}.<br>
+    Status: <b>Running</b> since {{ job.start_time|date:"Y-m-d H:i:s" }} UTC ( {{ job.running_time}} ) <br>
     {% with job.get_stats as stats %}
     Process ID: {{ stats.pid }}<br>
     CPU ID: {{ stats.processor }}<br>
     Nice: {{ stats.nice }}<br>
     State: {{ stats.state }}<br>
     Virtual size: {{ stats.vsize|filesizeformat }}<br>
-    User time: {{ stats.utime }} ticks<br>
-    System time: {{ stats.stime }} ticks<br>
+    {% comment %}
+    TODO: 
+    "getconf CLK_TCK" = 100 -> 1 tick = 1/100 seconds
+    see Job.get_stat
+    {% endcomment %}
+    Time spent scheduled in user mode: {{ stats.utime }}00 ms<br>
+    Time spent scheduled in system mode: {{ stats.stime }}00 ms<br>
     {% endwith %}
     {% else %}
     Status: <b>Queued</b> since {{ job.queue_time }}.<br>
index 94c97fbb096968f87dd192c7ea256de30e72b2bf..dbf281dfc66e6dc60212f5e514325650c073b762 100644 (file)
@@ -1,5 +1,10 @@
 {% extends "vessel_index.html" %}
 
+{% block style_extra %}
+{{ block.super }}
+{{ form.media }}
+{% endblock %}
+
 {% block breadcrumbs %}
 {{ block.super }}
 / <a href="/vessel/{{nmea.strmmsi}}/">{{nmea.strmmsi}}</a>
@@ -38,29 +43,6 @@ Sources: position by {{ nmea.get_source_1_str }}, voyage by {{ nmea.get_source_5
 
 
 <h2>Get archive data</h2>
-<form action='history'>
-Format: <select name=format onchange="if (this.value=='csv') $('#csvhint').show(); else $('#csvhint').hide();">
-<option value=track>Track line (Google Earth)</option>
-<option value=animation>Animation (Google Earth)</option>
-<option value=csv>Coma separated values (SpreadSheet)</option>
-</select><br>
-
-For the last <input name=period size=3 value=7><select name=period_type>
-<option value=3600>hour(s)</option>
-<option value=86400 selected>day(s)</option>
-<option value=2592000>month(es)</option>
-</select>
-<br>
-One position every <input name=grain size=3 value=1><select name=grain_type>
-<option value=1>second(s)</option>
-<option value=60>minute(s)</option>
-<option value=3600 selected>hour(s)</option>
-<option value=86400>day(s)</option>
-</select>
-<br>
-<span id=csvhint style="display:none;">Make sure you select "Charset: UTF-8" and "Separated by: Coma" when you <a href="/oocalc_howto.png">choose import options</a>.<br></span>
-<input type=submit value=Get>
-</form>
-
+{% include "fragment_vessel_history.html" %}
 
 {% endblock %}
diff --git a/html_templates/vessel_history.html b/html_templates/vessel_history.html
new file mode 100644 (file)
index 0000000..355ee2f
--- /dev/null
@@ -0,0 +1,14 @@
+{% extends "vessel.html" %}
+
+{% block breadcrumbs %}
+{{ block.super }}
+/ <a href="/vessel/{{nmea.strmmsi}}/history">history</a>
+{% endblock %}
+
+{% block content %}
+
+<h2>Download archive data for {{ nmea.get_name }}</h2>
+
+{% include "fragment_vessel_history.html" %}
+
+{% endblock %}
diff --git a/www/DateTimeShortcuts.js b/www/DateTimeShortcuts.js
new file mode 100644 (file)
index 0000000..c12dde2
--- /dev/null
@@ -0,0 +1,257 @@
+// Inserts shortcut buttons after all of the following:
+//     <input type="text" class="vDateField">
+//     <input type="text" class="vTimeField">
+
+var DateTimeShortcuts = {
+    calendars: [],
+    calendarInputs: [],
+    clockInputs: [],
+    calendarDivName1: 'calendarbox', // name of calendar <div> that gets toggled
+    calendarDivName2: 'calendarin',  // name of <div> that contains calendar
+    calendarLinkName: 'calendarlink',// name of the link that is used to toggle
+    clockDivName: 'clockbox',        // name of clock <div> that gets toggled
+    clockLinkName: 'clocklink',      // name of the link that is used to toggle
+    admin_media_prefix: '',
+    init: function() {
+        // Deduce admin_media_prefix by looking at the <script>s in the
+        // current document and finding the URL of *this* module.
+        var scripts = document.getElementsByTagName('script');
+        for (var i=0; i<scripts.length; i++) {
+            if (scripts[i].src.match(/DateTimeShortcuts/)) {
+                //var idx = scripts[i].src.indexOf('js/admin/DateTimeShortcuts');
+                var idx = scripts[i].src.indexOf('DateTimeShortcuts');
+                DateTimeShortcuts.admin_media_prefix = scripts[i].src.substring(0, idx);
+                // NIRGAL WAS HERE:
+                DateTimeShortcuts.admin_media_prefix += 'media/'
+                break;
+            }
+        }
+
+        var inputs = document.getElementsByTagName('input');
+        for (i=0; i<inputs.length; i++) {
+            var inp = inputs[i];
+            if (inp.getAttribute('type') == 'text' && inp.className.match(/vTimeField/)) {
+                DateTimeShortcuts.addClock(inp);
+            }
+            else if (inp.getAttribute('type') == 'text' && inp.className.match(/vDateField/)) {
+                DateTimeShortcuts.addCalendar(inp);
+            }
+        }
+    },
+    // Add clock widget to a given field
+    addClock: function(inp) {
+        var num = DateTimeShortcuts.clockInputs.length;
+        DateTimeShortcuts.clockInputs[num] = inp;
+
+        // Shortcut links (clock icon and "Now" link)
+        var shortcuts_span = document.createElement('span');
+        inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
+        var now_link = document.createElement('a');
+        now_link.setAttribute('href', "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().getHourMinuteSecond());");
+        now_link.appendChild(document.createTextNode(gettext('Now')));
+        var clock_link = document.createElement('a');
+        clock_link.setAttribute('href', 'javascript:DateTimeShortcuts.openClock(' + num + ');');
+        clock_link.id = DateTimeShortcuts.clockLinkName + num;
+        quickElement('img', clock_link, '', 'src', DateTimeShortcuts.admin_media_prefix + 'img/admin/icon_clock.gif', 'alt', gettext('Clock'));
+        shortcuts_span.appendChild(document.createTextNode('\240'));
+        shortcuts_span.appendChild(now_link);
+        shortcuts_span.appendChild(document.createTextNode('\240|\240'));
+        shortcuts_span.appendChild(clock_link);
+
+        // Create clock link div
+        //
+        // Markup looks like:
+        // <div id="clockbox1" class="clockbox module">
+        //     <h2>Choose a time</h2>
+        //     <ul class="timelist">
+        //         <li><a href="#">Now</a></li>
+        //         <li><a href="#">Midnight</a></li>
+        //         <li><a href="#">6 a.m.</a></li>
+        //         <li><a href="#">Noon</a></li>
+        //     </ul>
+        //     <p class="calendar-cancel"><a href="#">Cancel</a></p>
+        // </div>
+
+        var clock_box = document.createElement('div');
+        clock_box.style.display = 'none';
+        clock_box.style.position = 'absolute';
+        clock_box.className = 'clockbox module';
+        clock_box.setAttribute('id', DateTimeShortcuts.clockDivName + num);
+        document.body.appendChild(clock_box);
+        addEvent(clock_box, 'click', DateTimeShortcuts.cancelEventPropagation);
+
+        quickElement('h2', clock_box, gettext('Choose a time'));
+        time_list = quickElement('ul', clock_box, '');
+        time_list.className = 'timelist';
+        quickElement("a", quickElement("li", time_list, ""), gettext("Now"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", new Date().getHourMinuteSecond());")
+        quickElement("a", quickElement("li", time_list, ""), gettext("Midnight"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", '00:00:00');")
+        quickElement("a", quickElement("li", time_list, ""), gettext("6 a.m."), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", '06:00:00');")
+        quickElement("a", quickElement("li", time_list, ""), gettext("Noon"), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", '12:00:00');")
+
+        cancel_p = quickElement('p', clock_box, '');
+        cancel_p.className = 'calendar-cancel';
+        quickElement('a', cancel_p, gettext('Cancel'), 'href', 'javascript:DateTimeShortcuts.dismissClock(' + num + ');');
+    },
+    openClock: function(num) {
+        var clock_box = document.getElementById(DateTimeShortcuts.clockDivName+num)
+        var clock_link = document.getElementById(DateTimeShortcuts.clockLinkName+num)
+    
+        // Recalculate the clockbox position
+        // is it left-to-right or right-to-left layout ?
+        if (getStyle(document.body,'direction')!='rtl') {
+            clock_box.style.left = findPosX(clock_link) + 17 + 'px';
+        }
+        else {
+            // since style's width is in em, it'd be tough to calculate
+            // px value of it. let's use an estimated px for now
+            // TODO: IE returns wrong value for findPosX when in rtl mode
+            //       (it returns as it was left aligned), needs to be fixed.
+            clock_box.style.left = findPosX(clock_link) - 110 + 'px';
+        }
+        clock_box.style.top = findPosY(clock_link) - 30 + 'px';
+    
+        // Show the clock box
+        clock_box.style.display = 'block';
+        addEvent(window, 'click', function() { DateTimeShortcuts.dismissClock(num); return true; });
+    },
+    dismissClock: function(num) {
+       document.getElementById(DateTimeShortcuts.clockDivName + num).style.display = 'none';
+       window.onclick = null;
+    },
+    handleClockQuicklink: function(num, val) {
+       DateTimeShortcuts.clockInputs[num].value = val;
+       DateTimeShortcuts.dismissClock(num);
+    },
+    // Add calendar widget to a given field.
+    addCalendar: function(inp) {
+        var num = DateTimeShortcuts.calendars.length;
+
+        DateTimeShortcuts.calendarInputs[num] = inp;
+
+        // Shortcut links (calendar icon and "Today" link)
+        var shortcuts_span = document.createElement('span');
+        inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
+        var today_link = document.createElement('a');
+        today_link.setAttribute('href', 'javascript:DateTimeShortcuts.handleCalendarQuickLink(' + num + ', 0);');
+        today_link.appendChild(document.createTextNode(gettext('Today')));
+        var cal_link = document.createElement('a');
+        cal_link.setAttribute('href', 'javascript:DateTimeShortcuts.openCalendar(' + num + ');');
+        cal_link.id = DateTimeShortcuts.calendarLinkName + num;
+        quickElement('img', cal_link, '', 'src', DateTimeShortcuts.admin_media_prefix + 'img/admin/icon_calendar.gif', 'alt', gettext('Calendar'));
+        shortcuts_span.appendChild(document.createTextNode('\240'));
+        shortcuts_span.appendChild(today_link);
+        shortcuts_span.appendChild(document.createTextNode('\240|\240'));
+        shortcuts_span.appendChild(cal_link);
+
+        // Create calendarbox div.
+        //
+        // Markup looks like:
+        //
+        // <div id="calendarbox3" class="calendarbox module">
+        //     <h2>
+        //           <a href="#" class="link-previous">&lsaquo;</a>
+        //           <a href="#" class="link-next">&rsaquo;</a> February 2003
+        //     </h2>
+        //     <div class="calendar" id="calendarin3">
+        //         <!-- (cal) -->
+        //     </div>
+        //     <div class="calendar-shortcuts">
+        //          <a href="#">Yesterday</a> | <a href="#">Today</a> | <a href="#">Tomorrow</a>
+        //     </div>
+        //     <p class="calendar-cancel"><a href="#">Cancel</a></p>
+        // </div>
+        var cal_box = document.createElement('div');
+        cal_box.style.display = 'none';
+        cal_box.style.position = 'absolute';
+        cal_box.className = 'calendarbox module';
+        cal_box.setAttribute('id', DateTimeShortcuts.calendarDivName1 + num);
+        document.body.appendChild(cal_box);
+        addEvent(cal_box, 'click', DateTimeShortcuts.cancelEventPropagation);
+
+        // next-prev links
+        var cal_nav = quickElement('div', cal_box, '');
+        var cal_nav_prev = quickElement('a', cal_nav, '<', 'href', 'javascript:DateTimeShortcuts.drawPrev('+num+');');
+        cal_nav_prev.className = 'calendarnav-previous';
+        var cal_nav_next = quickElement('a', cal_nav, '>', 'href', 'javascript:DateTimeShortcuts.drawNext('+num+');');
+        cal_nav_next.className = 'calendarnav-next';
+
+        // main box
+        var cal_main = quickElement('div', cal_box, '', 'id', DateTimeShortcuts.calendarDivName2 + num);
+        cal_main.className = 'calendar';
+        DateTimeShortcuts.calendars[num] = new Calendar(DateTimeShortcuts.calendarDivName2 + num, DateTimeShortcuts.handleCalendarCallback(num));
+        DateTimeShortcuts.calendars[num].drawCurrent();
+
+        // calendar shortcuts
+        var shortcuts = quickElement('div', cal_box, '');
+        shortcuts.className = 'calendar-shortcuts';
+        quickElement('a', shortcuts, gettext('Yesterday'), 'href', 'javascript:DateTimeShortcuts.handleCalendarQuickLink(' + num + ', -1);');
+        shortcuts.appendChild(document.createTextNode('\240|\240'));
+        quickElement('a', shortcuts, gettext('Today'), 'href', 'javascript:DateTimeShortcuts.handleCalendarQuickLink(' + num + ', 0);');
+        shortcuts.appendChild(document.createTextNode('\240|\240'));
+        quickElement('a', shortcuts, gettext('Tomorrow'), 'href', 'javascript:DateTimeShortcuts.handleCalendarQuickLink(' + num + ', +1);');
+
+        // cancel bar
+        var cancel_p = quickElement('p', cal_box, '');
+        cancel_p.className = 'calendar-cancel';
+        quickElement('a', cancel_p, gettext('Cancel'), 'href', 'javascript:DateTimeShortcuts.dismissCalendar(' + num + ');');
+    },
+    openCalendar: function(num) {
+        var cal_box = document.getElementById(DateTimeShortcuts.calendarDivName1+num)
+        var cal_link = document.getElementById(DateTimeShortcuts.calendarLinkName+num)
+       var inp = DateTimeShortcuts.calendarInputs[num];
+
+       // Determine if the current value in the input has a valid date.
+       // If so, draw the calendar with that date's year and month.
+       if (inp.value) {
+           var date_parts = inp.value.split('-');
+           var year = date_parts[0];
+           var month = parseFloat(date_parts[1]);
+           if (year.match(/\d\d\d\d/) && month >= 1 && month <= 12) {
+               DateTimeShortcuts.calendars[num].drawDate(month, year);
+           }
+       }
+
+    
+        // Recalculate the clockbox position
+        // is it left-to-right or right-to-left layout ?
+        if (getStyle(document.body,'direction')!='rtl') {
+            cal_box.style.left = findPosX(cal_link) + 17 + 'px';
+        }
+        else {
+            // since style's width is in em, it'd be tough to calculate
+            // px value of it. let's use an estimated px for now
+            // TODO: IE returns wrong value for findPosX when in rtl mode
+            //       (it returns as it was left aligned), needs to be fixed.
+            cal_box.style.left = findPosX(cal_link) - 180 + 'px';
+        }
+        cal_box.style.top = findPosY(cal_link) - 75 + 'px';
+    
+        cal_box.style.display = 'block';
+        addEvent(window, 'click', function() { DateTimeShortcuts.dismissCalendar(num); return true; });
+    },
+    dismissCalendar: function(num) {
+        document.getElementById(DateTimeShortcuts.calendarDivName1+num).style.display = 'none';
+    },
+    drawPrev: function(num) {
+        DateTimeShortcuts.calendars[num].drawPreviousMonth();
+    },
+    drawNext: function(num) {
+        DateTimeShortcuts.calendars[num].drawNextMonth();
+    },
+    handleCalendarCallback: function(num) {
+        return "function(y, m, d) { DateTimeShortcuts.calendarInputs["+num+"].value = y+'-'+(m<10?'0':'')+m+'-'+(d<10?'0':'')+d; document.getElementById(DateTimeShortcuts.calendarDivName1+"+num+").style.display='none';}";
+    },
+    handleCalendarQuickLink: function(num, offset) {
+       var d = new Date();
+       d.setDate(d.getDate() + offset)
+       DateTimeShortcuts.calendarInputs[num].value = d.getISODate();
+       DateTimeShortcuts.dismissCalendar(num);
+    },
+    cancelEventPropagation: function(e) {
+        if (!e) e = window.event;
+        e.cancelBubble = true;
+        if (e.stopPropagation) e.stopPropagation();
+    }
+}
+
+addEvent(window, 'load', DateTimeShortcuts.init);
diff --git a/www/calendar.css b/www/calendar.css
new file mode 100644 (file)
index 0000000..f3be9b8
--- /dev/null
@@ -0,0 +1,32 @@
+/* DATE AND TIME */
+p.datetime { line-height:20px; margin:0; padding:0; color:#666; font-size:11px; font-weight:bold; }
+.datetime span { font-size:11px; color:#ccc; font-weight:normal; white-space:nowrap; }
+table p.datetime { font-size:10px; margin-left:0; padding-left:0; }
+
+/* CALENDARS & CLOCKS */
+.calendarbox, .clockbox { margin:5px auto; font-size:11px; width:16em; text-align:center; background:white; position:relative; }
+.clockbox { width:auto; }
+.calendar { margin:0; padding: 0; }
+.calendar table { margin:0; padding:0; border-collapse:collapse; background:white; width:99%; }
+.calendar caption, .calendarbox h2 { margin: 0; font-size:11px; text-align:center; border-top:none; }
+.calendar th { font-size:10px; color:#666; padding:2px 3px; text-align:center; background:#e1e1e1 url(/media/img/admin/nav-bg.gif) 0 50% repeat-x; border-bottom:1px solid #ddd; }
+.calendar td { font-size:11px; text-align: center; padding: 0; border-top:1px solid #eee; border-bottom:none; }
+.calendar td.selected a { background: #C9DBED; }
+.calendar td.nonday { background:#efefef; }
+.calendar td.today a { background:#ffc; }
+.calendar td a, .timelist a { display: block; font-weight:bold; padding:4px; text-decoration: none; color:#444; }
+.calendar td a:hover, .timelist a:hover { background: #5b80b2; color:white; }
+.calendar td a:active, .timelist a:active { background: #036; color:white; }
+.calendarnav { font-size:10px; text-align: center; color:#ccc; margin:0; padding:1px 3px; }
+.calendarnav a:link, #calendarnav a:visited, #calendarnav a:hover { color: #999; }
+.calendar-shortcuts { background:white; font-size:10px; line-height:11px; border-top:1px solid #eee; padding:3px 0 4px; color:#ccc; }
+.calendarbox .calendarnav-previous, .calendarbox .calendarnav-next { display:block; position:absolute; font-weight:bold; font-size:12px; background:#C9DBED url(/media/img/admin/default-bg.gif) bottom left repeat-x; padding:1px 4px 2px 4px; color:white; }
+.calendarnav-previous:hover, .calendarnav-next:hover { background:#036; }
+.calendarnav-previous { top:0; left:0; }
+.calendarnav-next { top:0; right:0; }
+.calendar-cancel { margin:0 !important; padding:0; font-size:10px; background:#e1e1e1 url(/media/img/admin/nav-bg.gif) 0 50% repeat-x;  border-top:1px solid #ddd; }
+.calendar-cancel a { padding:2px; color:#999; }
+ul.timelist, .timelist li { list-style-type:none; margin:0; padding:0; }
+.timelist a { padding:2px; }
+
+
diff --git a/www/calendar.js b/www/calendar.js
new file mode 100644 (file)
index 0000000..9035176
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+calendar.js - Calendar functions by Adrian Holovaty
+*/
+
+function removeChildren(a) { // "a" is reference to an object
+    while (a.hasChildNodes()) a.removeChild(a.lastChild);
+}
+
+// quickElement(tagType, parentReference, textInChildNode, [, attribute, attributeValue ...]);
+function quickElement() {
+    var obj = document.createElement(arguments[0]);
+    if (arguments[2] != '' && arguments[2] != null) {
+        var textNode = document.createTextNode(arguments[2]);
+        obj.appendChild(textNode);
+    }
+    var len = arguments.length;
+    for (var i = 3; i < len; i += 2) {
+        obj.setAttribute(arguments[i], arguments[i+1]);
+    }
+    arguments[1].appendChild(obj);
+    return obj;
+}
+
+// CalendarNamespace -- Provides a collection of HTML calendar-related helper functions
+var CalendarNamespace = {
+    monthsOfYear: gettext('January February March April May June July August September October November December').split(' '),
+    daysOfWeek: gettext('S M T W T F S').split(' '),
+    isLeapYear: function(year) {
+        return (((year % 4)==0) && ((year % 100)!=0) || ((year % 400)==0));
+    },
+    getDaysInMonth: function(month,year) {
+        var days;
+        if (month==1 || month==3 || month==5 || month==7 || month==8 || month==10 || month==12) {
+            days = 31;
+        }
+        else if (month==4 || month==6 || month==9 || month==11) {
+            days = 30;
+        }
+        else if (month==2 && CalendarNamespace.isLeapYear(year)) {
+            days = 29;
+        }
+        else {
+            days = 28;
+        }
+        return days;
+    },
+    draw: function(month, year, div_id, callback) { // month = 1-12, year = 1-9999
+        month = parseInt(month);
+        year = parseInt(year);
+        var calDiv = document.getElementById(div_id);
+        removeChildren(calDiv);
+        var calTable = document.createElement('table');
+        quickElement('caption', calTable, CalendarNamespace.monthsOfYear[month-1] + ' ' + year);
+        var tableBody = quickElement('tbody', calTable);
+
+        // Draw days-of-week header
+        var tableRow = quickElement('tr', tableBody);
+        for (var i = 0; i < 7; i++) {
+            quickElement('th', tableRow, CalendarNamespace.daysOfWeek[i]);
+        }
+
+        var startingPos = new Date(year, month-1, 1).getDay();
+        var days = CalendarNamespace.getDaysInMonth(month, year);
+
+        // Draw blanks before first of month
+        tableRow = quickElement('tr', tableBody);
+        for (var i = 0; i < startingPos; i++) {
+            var _cell = quickElement('td', tableRow, ' ');
+            _cell.style.backgroundColor = '#f3f3f3';
+        }
+
+        // Draw days of month
+        var currentDay = 1;
+        for (var i = startingPos; currentDay <= days; i++) {
+            if (i%7 == 0 && currentDay != 1) {
+                tableRow = quickElement('tr', tableBody);
+            }
+            var cell = quickElement('td', tableRow, '');
+            quickElement('a', cell, currentDay, 'href', 'javascript:void(' + callback + '('+year+','+month+','+currentDay+'));');
+            currentDay++;
+        }
+
+        // Draw blanks after end of month (optional, but makes for valid code)
+        while (tableRow.childNodes.length < 7) {
+            var _cell = quickElement('td', tableRow, ' ');
+            _cell.style.backgroundColor = '#f3f3f3';
+        }
+
+        calDiv.appendChild(calTable);
+    }
+}
+
+// Calendar -- A calendar instance
+function Calendar(div_id, callback) {
+    // div_id (string) is the ID of the element in which the calendar will
+    //     be displayed
+    // callback (string) is the name of a JavaScript function that will be
+    //     called with the parameters (year, month, day) when a day in the
+    //     calendar is clicked
+    this.div_id = div_id;
+    this.callback = callback;
+    this.today = new Date();
+    this.currentMonth = this.today.getMonth() + 1;
+    this.currentYear = this.today.getFullYear();
+}
+Calendar.prototype = {
+    drawCurrent: function() {
+        CalendarNamespace.draw(this.currentMonth, this.currentYear, this.div_id, this.callback);
+    },
+    drawDate: function(month, year) {
+        this.currentMonth = month;
+        this.currentYear = year;
+        this.drawCurrent();
+    },
+    drawPreviousMonth: function() {
+        if (this.currentMonth == 1) {
+            this.currentMonth = 12;
+            this.currentYear--;
+        }
+        else {
+            this.currentMonth--;
+        }
+        this.drawCurrent();
+    },
+    drawNextMonth: function() {
+        if (this.currentMonth == 12) {
+            this.currentMonth = 1;
+            this.currentYear++;
+        }
+        else {
+            this.currentMonth++;
+        }
+        this.drawCurrent();
+    },
+    drawPreviousYear: function() {
+        this.currentYear--;
+        this.drawCurrent();
+    },
+    drawNextYear: function() {
+        this.currentYear++;
+        this.drawCurrent();
+    }
+}
index c2074dd61090eb1007078f880cb772cfcfd5bf69..848c6bb00ce5136d6fc9d84ca0077f5ed00f627b 100644 (file)
@@ -55,6 +55,10 @@ body {
     font-size: 70%;
 }
 
+a img { 
+    border:none;
+}
+
 a[href] {
     text-decoration: none;
     /*color: #4444bb;*/
@@ -115,6 +119,10 @@ ul.errorlist li {
     list-style-image: url('/errorbullet.png');
 }
 
+td li {
+    list-style: none;
+}
+
 div.message {
     background:#ffffd0;
     border: 1px solid yellow;
index 771629184baf0d4a608648d62e61f9dd7f6e058f..f1efbee6bcf0185781f72f50c6a4cc26325d98f4 100644 (file)
@@ -7,3 +7,166 @@ function check_footer_bottom() {
 
 $(document).ready(check_footer_bottom);
 $(window).resize(check_footer_bottom);
+
+
+
+
+//-------- dummy i18n.js
+function gettext(txt) {
+    return txt;
+}
+
+//-------- core.js
+// Core javascript helper functions
+
+// basic browser identification & version
+var isOpera = (navigator.userAgent.indexOf("Opera")>=0) && parseFloat(navigator.appVersion);
+var isIE = ((document.all) && (!isOpera)) && parseFloat(navigator.appVersion.split("MSIE ")[1].split(";")[0]);
+
+// Cross-browser event handlers.
+function addEvent(obj, evType, fn) {
+    if (obj.addEventListener) {
+        obj.addEventListener(evType, fn, false);
+        return true;
+    } else if (obj.attachEvent) {
+        var r = obj.attachEvent("on" + evType, fn);
+        return r;
+    } else {
+        return false;
+    }
+}
+
+function removeEvent(obj, evType, fn) {
+    if (obj.removeEventListener) {
+        obj.removeEventListener(evType, fn, false);
+        return true;
+    } else if (obj.detachEvent) {
+        obj.detachEvent("on" + evType, fn);
+        return true;
+    } else {
+        return false;
+    }
+}
+
+// quickElement(tagType, parentReference, textInChildNode, [, attribute, attributeValue ...]);
+function quickElement() {
+    var obj = document.createElement(arguments[0]);
+    if (arguments[2] != '' && arguments[2] != null) {
+        var textNode = document.createTextNode(arguments[2]);
+        obj.appendChild(textNode);
+    }
+    var len = arguments.length;
+    for (var i = 3; i < len; i += 2) {
+        obj.setAttribute(arguments[i], arguments[i+1]);
+    }
+    arguments[1].appendChild(obj);
+    return obj;
+}
+
+// ----------------------------------------------------------------------------
+// Find-position functions by PPK
+// See http://www.quirksmode.org/js/findpos.html
+// ----------------------------------------------------------------------------
+function findPosX(obj) {
+    var curleft = 0;
+    if (obj.offsetParent) {
+        while (obj.offsetParent) {
+            curleft += obj.offsetLeft - ((isOpera) ? 0 : obj.scrollLeft);
+            obj = obj.offsetParent;
+        }
+        // IE offsetParent does not include the top-level 
+        if (isIE && obj.parentElement){
+            curleft += obj.offsetLeft - obj.scrollLeft;
+        }
+    } else if (obj.x) {
+        curleft += obj.x;
+    }
+    return curleft;
+}
+
+function findPosY(obj) {
+    var curtop = 0;
+    if (obj.offsetParent) {
+        while (obj.offsetParent) {
+            curtop += obj.offsetTop - ((isOpera) ? 0 : obj.scrollTop);
+            obj = obj.offsetParent;
+        }
+        // IE offsetParent does not include the top-level 
+        if (isIE && obj.parentElement){
+            curtop += obj.offsetTop - obj.scrollTop;
+        }
+    } else if (obj.y) {
+        curtop += obj.y;
+    }
+    return curtop;
+}
+
+//-----------------------------------------------------------------------------
+// Date object extensions
+// ----------------------------------------------------------------------------
+Date.prototype.getCorrectYear = function() {
+    // Date.getYear() is unreliable --
+    // see http://www.quirksmode.org/js/introdate.html#year
+    var y = this.getYear() % 100;
+    return (y < 38) ? y + 2000 : y + 1900;
+}
+
+Date.prototype.getTwoDigitMonth = function() {
+    return (this.getMonth() < 9) ? '0' + (this.getMonth()+1) : (this.getMonth()+1);
+}
+
+Date.prototype.getTwoDigitDate = function() {
+    return (this.getDate() < 10) ? '0' + this.getDate() : this.getDate();
+}
+
+Date.prototype.getTwoDigitHour = function() {
+    return (this.getHours() < 10) ? '0' + this.getHours() : this.getHours();
+}
+
+Date.prototype.getTwoDigitMinute = function() {
+    return (this.getMinutes() < 10) ? '0' + this.getMinutes() : this.getMinutes();
+}
+
+Date.prototype.getTwoDigitSecond = function() {
+    return (this.getSeconds() < 10) ? '0' + this.getSeconds() : this.getSeconds();
+}
+
+Date.prototype.getISODate = function() {
+    return this.getCorrectYear() + '-' + this.getTwoDigitMonth() + '-' + this.getTwoDigitDate();
+}
+
+Date.prototype.getHourMinute = function() {
+    return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute();
+}
+
+Date.prototype.getHourMinuteSecond = function() {
+    return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute() + ':' + this.getTwoDigitSecond();
+}
+
+// ----------------------------------------------------------------------------
+// String object extensions
+// ----------------------------------------------------------------------------
+String.prototype.pad_left = function(pad_length, pad_string) {
+    var new_string = this;
+    for (var i = 0; new_string.length < pad_length; i++) {
+        new_string = pad_string + new_string;
+    }
+    return new_string;
+}
+
+// ----------------------------------------------------------------------------
+// Get the computed style for and element
+// ----------------------------------------------------------------------------
+function getStyle(oElm, strCssRule){
+    var strValue = "";
+    if(document.defaultView && document.defaultView.getComputedStyle){
+        strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule);
+    }
+    else if(oElm.currentStyle){
+        strCssRule = strCssRule.replace(/\-(\w)/g, function (strMatch, p1){
+            return p1.toUpperCase();
+        });
+        strValue = oElm.currentStyle[strCssRule];
+    }
+    return strValue;
+}