Added filters on the sources overview page
[ais.git] / bin / djais / views.py
1 # -*- coding: utf-8 -*-
2
3 import os
4 from datetime import *
5 import re
6 from time import time as get_timestamp
7 import logging
8 import crack
9 import struct
10 import rrdtool
11 from django.http import *
12 from django.template import loader, RequestContext
13 from django import forms
14 from django.shortcuts import render_to_response, get_object_or_404
15 from django.db import IntegrityError
16
17 from decoratedstr import remove_decoration
18
19 from ais.djais.basicauth import http_authenticate
20 from ais.djais.models import *
21 from ais.show_targets_ships import *
22 from ais.common import Nmea, NmeaFeeder, strmmsi_to_mmsi, SHIP_TYPES, STATUS_CODES, AIS_STATUS_NOT_AVAILABLE, AIS_ROT_NOT_AVAILABLE, AIS_LATLON_SCALE, AIS_LON_NOT_AVAILABLE, AIS_LAT_NOT_AVAILABLE, AIS_COG_SCALE, AIS_COG_NOT_AVAILABLE, AIS_NO_HEADING, AIS_SOG_SCALE, AIS_SOG_NOT_AVAILABLE, AIS_SOG_MAX_SPEED, add_nmea1, add_nmea5_partial, load_fleet_to_uset
23 from ais.ntools import datetime_to_timestamp, clean_ais_charset
24 from ais.inputs.common import is_id4_active
25 from ais.inputs.stats import STATS_DIR
26 from ais.inputs.config import peers_get_config
27
28 def auth(username, raw_password):
29     try:
30         user = User.objects.get(login=username)
31     except User.DoesNotExist:
32         return None
33     if not user.check_password(raw_password):
34         return None
35     return user
36
37
38 @http_authenticate(auth, 'ais')
39 def index(request):
40     return render_to_response('index.html', {}, RequestContext(request))
41
42
43 class VesselSearchForm(forms.Form):
44     mmsi = forms.CharField(max_length=9, required=False)
45     name = forms.CharField(max_length=20, required=False)
46     imo = forms.IntegerField(required=False)
47     callsign = forms.CharField(max_length=7, required=False)
48     destination = forms.CharField(max_length=20, required=False)
49     def clean(self):
50         cleaned_data = self.cleaned_data
51         for value in cleaned_data.values():
52             if value:
53                 return cleaned_data
54         raise forms.ValidationError("You must enter at least one criteria")
55
56
57
58 @http_authenticate(auth, 'ais')
59 def vessel_search(request):
60     if request.method == 'POST' or request.META['QUERY_STRING']:
61         form = VesselSearchForm(request.REQUEST)
62         if form.is_valid():
63             data = form.cleaned_data
64             vessels = Vessel.objects
65             if data['mmsi']:
66                 vessels = vessels.filter(mmsi=strmmsi_to_mmsi(data['mmsi']))
67             if data['name']:
68                 vessels = vessels.filter(name__contains=data['name'].upper())
69             if data['imo']:
70                 vessels = vessels.filter(imo=data['imo'])
71             if data['callsign']:
72                 vessels = vessels.filter(callsign__contains=data['callsign'].upper())
73             if data['destination']:
74                 vessels = vessels.filter(destination__contains=data['destination'].upper())
75             return render_to_response('vessels.html', {'vessels': vessels}, RequestContext(request))
76     else: # GET
77         form = VesselSearchForm()
78
79     return render_to_response('vessel_index.html', {'form': form}, RequestContext(request))
80
81 @http_authenticate(auth, 'ais')
82 def vessel(request, strmmsi):
83     mmsi = strmmsi_to_mmsi(strmmsi)
84     vessel = get_object_or_404(Vessel, pk=mmsi)
85     nmea = Nmea.new_from_lastinfo(strmmsi)
86     #if not nmea.timestamp_1 and not nmea.timestamp_5:
87     #    raise Http404
88     return render_to_response('vessel.html', {'nmea': nmea}, RequestContext(request))
89
90
91 class VesselManualInputForm(forms.Form):
92     timestamp = forms.DateTimeField(label=u'When', help_text=u'When was the observation made in GMT. Use YYYY-MM-DD HH:MM:SS format')
93     imo = forms.IntegerField(required=False, min_value=1000000, max_value=9999999)
94     name = forms.CharField(max_length=20, required=False)
95     callsign = forms.CharField(max_length=7, required=False)
96     type = forms.TypedChoiceField(required=False, choices = [ kv for kv in SHIP_TYPES.iteritems() if 'reserved' not in kv[1].lower()], coerce=int, empty_value=0, initial=0)
97     status = forms.TypedChoiceField(required=False, choices = [ kv for kv in STATUS_CODES.iteritems() if 'reserved' not in kv[1].lower()], coerce=int, empty_value=AIS_STATUS_NOT_AVAILABLE, initial=AIS_STATUS_NOT_AVAILABLE)
98     sog = forms.FloatField(label='Speed', help_text='Over ground, in knots', required=False, min_value=0, max_value=AIS_SOG_MAX_SPEED/AIS_SOG_SCALE)
99     latitude = forms.CharField(required=False)
100     longitude = forms.CharField(required=False)
101     cog = forms.FloatField(label='Course', help_text='Over ground', required=False, min_value=0.0, max_value=359.9)
102     heading = forms.IntegerField(required=False, min_value=0, max_value=359)
103
104     @staticmethod
105     def _clean_ais_charset(ustr):
106         ustr = remove_decoration(ustr) # benign cleaning, but can increase size (œ->oe)
107         ustr = ustr.upper() # benign cleaning
108         str = clean_ais_charset(ustr.encode('ascii', 'replace'))
109         if unicode(str) != ustr:
110             raise forms.ValidationError('Invalid character: AIS alphabet is @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^- !"#$%&\'()*+,-./0123456789:;<=>?')
111         return str
112
113     def clean_timestamp(self):
114         data = self.cleaned_data['timestamp']
115         if data is None:
116             return None
117         if data < datetime.utcnow() - timedelta(365):
118             raise forms.ValidationError('Date is too much is the past.')
119         if data > datetime.utcnow():
120             raise forms.ValidationError('Date is be in the future. This form is only for observed results.')
121         return datetime_to_timestamp(data)
122
123     def clean_imo(self):
124         data = self.cleaned_data['imo']
125         if data is None:
126             return 0
127         return data
128
129     def clean_name(self):
130         name = self.cleaned_data['name']
131         if name is None:
132             return ''
133         name = VesselManualInputForm._clean_ais_charset(name)
134         if len(name)>20:
135             raise forms.ValidationError('Ensure this value has at most 20 characters (it has %s).' % len(name))
136         return name
137
138     def clean_callsign(self):
139         callsign = self.cleaned_data['callsign']
140         if callsign is None:
141             return ''
142         callsign = VesselManualInputForm._clean_ais_charset(callsign)
143         if len(callsign)>7:
144             raise forms.ValidationError('Ensure this value has at most 7 characters (it has %s).' % len(callsign))
145         return callsign
146
147     def clean_sog(self):
148         sog = self.cleaned_data['sog']
149         if sog is None:
150             return AIS_SOG_NOT_AVAILABLE
151         return int(sog*AIS_SOG_SCALE)
152
153     def clean_latitude(self):
154         data = self.cleaned_data['latitude']
155         data = data.replace(u"''", u'"') # commong mistake
156         data = data.replace(u' ', u'') # remove spaces
157         sides = u'SN'
158         if not data:
159             return AIS_LAT_NOT_AVAILABLE
160         tmp, side = data[:-1], data[-1]
161         if side == sides[0]:
162             side = -1
163         elif side == sides[1]:
164             side = 1
165         else:
166             raise forms.ValidationError(u'Last character must be either %s or %s.' % (sides[0], sides[1]))
167         spl = tmp.split(u'°')
168         if len(spl) == 1:
169             raise forms.ValidationError(u'You need to use the ° character.')
170         d, tmp = spl
171         try:
172             d = float(d)
173         except ValueError:
174             raise forms.ValidationError(u'Degrees must be an number. It\'s %s.' % d)
175         spl = tmp.split(u"'", 1)
176         if len(spl) == 1:
177             # no ' sign: ok only if there is nothing but the side after °
178             # we don't accept seconds if there is no minutes:
179             # It might be an entry mistake
180             tmp = spl[0]
181             if len(tmp) == 0:
182                 m = s = 0
183             else:
184                 raise forms.ValidationError(u'You must use the \' character between ° and %s.' % data[-1])
185         else:
186             m, tmp = spl
187             try:
188                 m = float(m)
189             except ValueError:
190                 raise forms.ValidationError(u'Minutes must be an number. It\'s %s.' % m)
191             if len(tmp) == 0:
192                 s = 0
193             else:
194                 if tmp[-1] != '"':
195                     raise forms.ValidationError(u'You must use the " character between seconds and %s.' % data[-1])
196                 s = tmp[:-1]
197                 try:
198                     s = float(s)
199                 except ValueError:
200                     raise forms.ValidationError(u'Seconds must be an number. It\'s %s.' % s)
201         data = side * ( d + m / 60 + s / 3600)
202
203         if data < -90 or data > 90:
204             raise forms.ValidationError(u'%s in not in -90..90 range' % data)
205         return int(data * AIS_LATLON_SCALE)
206
207     def clean_longitude(self):
208         data = self.cleaned_data['longitude']
209         data = data.replace(u"''", u'"') # commong mistake
210         data = data.replace(u' ', u'') # remove spaces
211         sides = u'WE'
212         if not data:
213             return AIS_LON_NOT_AVAILABLE
214         tmp, side = data[:-1], data[-1]
215         if side == sides[0]:
216             side = -1
217         elif side == sides[1]:
218             side = 1
219         else:
220             raise forms.ValidationError(u'Last character must be either %s or %s.' % (sides[0], sides[1]))
221         spl = tmp.split(u'°')
222         if len(spl) == 1:
223             raise forms.ValidationError(u'You need to use the ° character.')
224         d, tmp = spl
225         try:
226             d = float(d)
227         except ValueError:
228             raise forms.ValidationError(u'Degrees must be an number. It\'s %s.' % d)
229         spl = tmp.split(u"'", 1)
230         if len(spl) == 1:
231             # no ' sign: ok only if there is nothing but the side after °
232             # we don't accept seconds if there is no minutes:
233             # It might be an entry mistake
234             tmp = spl[0]
235             if len(tmp) == 0:
236                 m = s = 0
237             else:
238                 raise forms.ValidationError(u'You must use the \' character between ° and %s.' % data[-1])
239         else:
240             m, tmp = spl
241             try:
242                 m = float(m)
243             except ValueError:
244                 raise forms.ValidationError(u'Minutes must be an number. It\'s %s.' % m)
245             if len(tmp) == 0:
246                 s = 0
247             else:
248                 if tmp[-1] != '"':
249                     raise forms.ValidationError(u'You must use the " character between seconds and %s.' % data[-1])
250                 s = tmp[:-1]
251                 try:
252                     s = float(s)
253                 except ValueError:
254                     raise forms.ValidationError(u'Seconds must be an number. It\'s %s.' % s)
255         data = side * ( d + m / 60 + s / 3600)
256
257         if data < -180 or data > 180:
258             raise forms.ValidationError(u'%s in not in -180..180 range' % data)
259         return int(data * AIS_LATLON_SCALE)
260
261     def clean_cog(self):
262         data = self.cleaned_data['cog']
263         if data is None:
264             return AIS_COG_NOT_AVAILABLE
265         return int(data * AIS_COG_SCALE)
266     
267     def clean_heading(self):
268         #raise forms.ValidationError(u'clean_heading called')
269         data = self.cleaned_data['heading']
270         if data is None:
271             return AIS_NO_HEADING
272         return data
273
274     def clean(self):
275         cleaned_data = self.cleaned_data
276         if (cleaned_data.get('latitude', AIS_LAT_NOT_AVAILABLE) == AIS_LAT_NOT_AVAILABLE ) ^ ( cleaned_data.get('longitude', AIS_LON_NOT_AVAILABLE) == AIS_LON_NOT_AVAILABLE):
277             raise forms.ValidationError('It makes no sense to enter just a latitude or a longitude. Enter both or none.')
278         if cleaned_data.get('latitude', AIS_LAT_NOT_AVAILABLE) == AIS_LAT_NOT_AVAILABLE:
279             if cleaned_data.get('status', AIS_STATUS_NOT_AVAILABLE) != AIS_STATUS_NOT_AVAILABLE:
280                 raise forms.ValidationError('It makes no sense to enter a status without coordinates. Please enter latitute and longitude too.')
281             if cleaned_data.get('sog', AIS_SOG_NOT_AVAILABLE) != AIS_SOG_NOT_AVAILABLE:
282                 raise forms.ValidationError('It makes no sense to enter a speed without coordinates. Please enter latitute and longitude too.')
283             if cleaned_data.get('cog', AIS_COG_NOT_AVAILABLE) != AIS_COG_NOT_AVAILABLE:
284                 raise forms.ValidationError('It makes no sense to enter a course without coordinates. Please enter latitute and longitude too.')
285             if cleaned_data.get('heading', AIS_NO_HEADING) != AIS_NO_HEADING:
286                 raise forms.ValidationError('It makes no sense to enter a heading without coordinates. Please enter latitute and longitude too.')
287
288         if cleaned_data.get('timestamp', None) \
289         and cleaned_data.get('imo', 0) == 0 \
290         and cleaned_data.get('name', '') == '' \
291         and cleaned_data.get('callsign', '') == '' \
292         and cleaned_data.get('type', 0) == 0 \
293         and cleaned_data.get('status', AIS_STATUS_NOT_AVAILABLE) == AIS_STATUS_NOT_AVAILABLE \
294         and cleaned_data.get('sog', AIS_SOG_NOT_AVAILABLE) == AIS_SOG_NOT_AVAILABLE \
295         and cleaned_data.get('latitude', AIS_LAT_NOT_AVAILABLE) == AIS_LAT_NOT_AVAILABLE \
296         and cleaned_data.get('longitude', AIS_LON_NOT_AVAILABLE) == AIS_LON_NOT_AVAILABLE \
297         and cleaned_data.get('cog', AIS_COG_NOT_AVAILABLE) == AIS_COG_NOT_AVAILABLE \
298         and cleaned_data.get('heading', AIS_NO_HEADING) == AIS_NO_HEADING:
299             raise forms.ValidationError("You must enter some data, beside when.")
300         return cleaned_data
301
302 @http_authenticate(auth, 'ais')
303 def vessel_manual_input(request, strmmsi):
304     strmmsi = strmmsi.encode('utf-8')
305     nmea = Nmea.new_from_lastinfo(strmmsi)
306     if request.method == 'POST' or request.META['QUERY_STRING']:
307         form = VesselManualInputForm(request.REQUEST)
308         if form.is_valid():
309             data = form.cleaned_data
310             source = 'U' +  struct.pack('<I', request.user.id)[0:3]
311             result = u''
312             if data['imo'] != 0 \
313             or data['name'] != '' \
314             or data['callsign'] != '' \
315             or data['type'] != 0:
316                 toto = (strmmsi, data['timestamp'], data['imo'], data['name'], data['callsign'], data['type'], 0,0,0,0, 0,0,24,60, 0, '', source)
317                 result += 'UPDATING NMEA 5: '+repr(toto)+'<br>'
318                 add_nmea5_partial(*toto)
319             if data['status'] != AIS_STATUS_NOT_AVAILABLE \
320             or data['sog'] != AIS_SOG_NOT_AVAILABLE \
321             or data['latitude'] != AIS_LAT_NOT_AVAILABLE \
322             or data['longitude'] != AIS_LON_NOT_AVAILABLE \
323             or data['cog'] != AIS_COG_NOT_AVAILABLE \
324             or data['heading'] != AIS_NO_HEADING:
325                 
326                 toto = (strmmsi, data['timestamp'], data['status'], AIS_ROT_NOT_AVAILABLE, data['sog'], data['latitude'], data['longitude'], data['cog'], data['heading'], source)
327                 result += 'UPDATING NMEA 1: '+repr(toto)+'<br>'
328                 add_nmea1(*toto)
329             return HttpResponse('Not fully implemented: '+repr(data) + '<br>' + result)
330     else: # GET
331         form = VesselManualInputForm()
332     return render_to_response('vessel_manual_input.html', {'form': form, 'nmea': nmea}, RequestContext(request))
333
334 @http_authenticate(auth, 'ais')
335 def vessel_track(request, strmmsi):
336     ndays = request.REQUEST.get('ndays', 90)
337     try:
338         ndays = int(ndays)
339     except ValueError:
340         ndays = 90
341     grain = request.REQUEST.get('grain', 3600)
342     try:
343         grain = int(grain)
344     except ValueError:
345         grain = 3600
346     date_end = datetime.utcnow()
347     date_start = date_end - timedelta(ndays)
348     nmea_iterator = NmeaFeeder(strmmsi, date_end, date_start, granularity=grain)
349     value = kml_to_kmz(format_boat_track(nmea_iterator))
350     response = HttpResponse(value, mimetype="application/vnd.google-earth.kml")
351     response['Content-Disposition'] = 'attachment; filename=%s.kmz' % strmmsi
352     return response
353
354
355 @http_authenticate(auth, 'ais')
356 def vessel_animation(request, strmmsi):
357     ndays = request.REQUEST.get('ndays', 90)
358     try:
359         ndays = int(ndays)
360     except ValueError:
361         ndays = 90
362     grain = request.REQUEST.get('grain', 3600)
363     try:
364         grain = int(grain)
365     except ValueError:
366         grain = 3600
367     date_end = datetime.utcnow()
368     date_start = date_end - timedelta(ndays)
369     nmea_iterator = NmeaFeeder(strmmsi, date_end, date_start, granularity=grain)
370     value = kml_to_kmz(format_boat_intime(nmea_iterator))
371     response = HttpResponse(value, mimetype="application/vnd.google-earth.kml")
372     response['Content-Disposition'] = 'attachment; filename=%s.kmz' % strmmsi
373     return response
374
375
376 @http_authenticate(auth, 'ais')
377 def fleets(request):
378     fleetusers = request.user.fleetuser_set.all()
379     return render_to_response('fleets.html', {'fleetusers':fleetusers}, RequestContext(request))
380
381
382 @http_authenticate(auth, 'ais')
383 def fleet(request, fleetname):
384     fleet = get_object_or_404(Fleet, pk=fleetname)
385     if not FleetUser.objects.filter(fleet=fleetname, user=request.user.id).all():
386         return HttpResponseForbidden('<h1>Forbidden</h1>')
387     return render_to_response('fleet.html', {'fleet':fleet}, RequestContext(request))
388
389
390 @http_authenticate(auth, 'ais')
391 def fleet_vessels(request, fleetname):
392     fleet = get_object_or_404(Fleet, pk=fleetname)
393     if not FleetUser.objects.filter(fleet=fleetname, user=request.user.id).all():
394         return HttpResponseForbidden('<h1>Forbidden</h1>')
395     vessels = fleet.vessel.all()
396     return render_to_response('fleet_vessels.html', {'fleet':fleet, 'vessels': vessels}, RequestContext(request))
397
398
399 @http_authenticate(auth, 'ais')
400 def fleet_vessel_add(request, fleetname):
401     fleet = get_object_or_404(Fleet, pk=fleetname)
402     if not FleetUser.objects.filter(fleet=fleetname, user=request.user.id).all():
403         return HttpResponseForbidden('<h1>Forbidden</h1>')
404     strmmsi = request.REQUEST['mmsi']
405     mmsi = strmmsi_to_mmsi(strmmsi)
406     try:
407         vessel = Vessel.objects.get(pk=mmsi)
408     except Vessel.DoesNotExist:
409         return HttpResponse('No such vessel')
410     try:
411         FleetVessel(fleet=fleet, vessel=vessel).save()
412     except IntegrityError:
413         return HttpResponse('Integrity error: Is the ship already in that fleet?')
414     return HttpResponse('Done')
415
416
417 class FleetAddVessel(forms.Form):
418     mmsi = forms.CharField(help_text=u'Enter one MMSI per line', required=False, widget=forms.Textarea)
419     #name = forms.CharField(max_length=20, required=False)
420     #imo = forms.IntegerField(required=False)
421     #callsign = forms.CharField(max_length=7, required=False)
422     #destination = forms.CharField(max_length=20, required=False)
423     def clean(self):
424         cleaned_data = self.cleaned_data
425         for value in cleaned_data.values():
426             if value:
427                 return cleaned_data
428         raise forms.ValidationError("You must enter at least one criteria")
429
430 @http_authenticate(auth, 'ais')
431 def fleet_vessel_add2(request, fleetname):
432     fleet = get_object_or_404(Fleet, pk=fleetname)
433     if not FleetUser.objects.filter(fleet=fleetname, user=request.user.id).all():
434         return HttpResponseForbidden('<h1>Forbidden</h1>')
435     if request.method == 'POST' or request.META['QUERY_STRING']:
436         form = FleetAddVessel(request.REQUEST)
437         if form.is_valid():
438             data = form.cleaned_data
439             result = []
440             a_strmmsi = request.REQUEST['mmsi']
441             if a_strmmsi:
442                 for strmmsi in a_strmmsi.split('\n'):
443                     strmmsi = strmmsi.strip('\r')
444                     if not strmmsi:
445                         continue
446                     try:
447                         sqlmmsi = strmmsi_to_mmsi(strmmsi)
448                     except AssertionError:
449                         result.append('Invalid mmsi %s' % strmmsi)
450                         continue
451                     try:
452                         vessel = Vessel.objects.get(pk=sqlmmsi)
453                     except Vessel.DoesNotExist:
454                         result.append('No vessel with MMSI '+strmmsi)
455                         continue
456                     try:
457                         fv = FleetVessel.objects.get(fleet=fleet, vessel=vessel)
458                         result.append('Vessel with MMSI %s is already in that fleet' % strmmsi)
459                     except FleetVessel.DoesNotExist:
460                         FleetVessel(fleet=fleet, vessel=vessel).save()
461                         result.append('Vessel with MMSI %s added' % strmmsi)
462
463             return HttpResponse('<br>'.join(result))
464     else: # GET
465         form = FleetAddVessel()
466
467     return render_to_response('fleet_vessel_add.html', {'form': form, 'fleet': fleet}, RequestContext(request))
468
469
470 @http_authenticate(auth, 'ais')
471 def fleet_users(request, fleetname):
472     fleet = get_object_or_404(Fleet, pk=fleetname)
473     if not FleetUser.objects.filter(fleet=fleetname, user=request.user.id).all():
474         return HttpResponseForbidden('<h1>Forbidden</h1>')
475
476     message = u''
477     if request.method == 'POST' or request.META['QUERY_STRING']:
478         action = request.REQUEST['action']
479         userlogin = request.REQUEST['user']
480         try:
481             user = User.objects.get(login=userlogin)
482         except User.DoesNotExist:
483             message = u'User %s does not exist.' % userlogin
484         else:
485             if action == u'add':
486                 try:
487                     fu = FleetUser.objects.get(fleet=fleet, user=user)
488                     message = u'User %s already has access.' % user.login
489                 except FleetUser.DoesNotExist:
490                     FleetUser(fleet=fleet, user=user).save()
491                     message = u'Granted access to user %s.' % user.login
492             elif action == u'revoke':
493                 try:
494                     fu = FleetUser.objects.get(fleet=fleet, user=user)
495                     fu.delete()
496                     message = u'Revoked access to user %s.' % user.login
497                 except FleetUser.DoesNotExist:
498                     message = u'User %s didn\'t have access.' % user.login
499             else:
500                 message = u'Unknown action %s' % action
501
502     fleetusers = fleet.fleetuser_set.all()
503     otherusers = User.objects.exclude(id__in=[fu.user.id for fu in fleetusers]).order_by('name')
504     return render_to_response('fleet_users.html', {'fleet':fleet, 'fleetusers': fleetusers, 'otherusers': otherusers, 'message': message }, RequestContext(request))
505
506
507 @http_authenticate(auth, 'ais')
508 def fleet_lastpos(request, fleetname):
509     fleet = get_object_or_404(Fleet, pk=fleetname)
510     if not FleetUser.objects.filter(fleet=fleetname, user=request.user.id).all():
511         return HttpResponseForbidden('<h1>Forbidden</h1>')
512     fleet_uset = load_fleet_to_uset(fleetname)
513     # = set([mmsi_to_strmmsi(vessel.mmsi) for vessel in fleet.vessel.all()])
514     value = kml_to_kmz(format_fleet(fleet_uset, document_name=fleetname+' fleet').encode('utf-8'))
515     response = HttpResponse(value, mimetype="application/vnd.google-earth.kml")
516     response['Content-Disposition'] = 'attachment; filename=%s.kmz' % fleetname
517     return response
518
519
520 @http_authenticate(auth, 'ais')
521 def users(request):
522     users = User.objects.all()
523     for user in users:
524         user.admin_ok = user.is_admin_by(request.user.id)
525     if request.REQUEST.has_key('showtree'):
526         local_users = {}
527         for user in users:
528             user.children = []
529             local_users[user.id] = user
530         for user in users:
531             if user.father_id == None:
532                 root = user
533             else:
534                 local_users[user.father_id].children.append(user)
535         assert root
536         return render_to_response('users_tree.html', {'root': root, 'auser': request.user.id}, RequestContext(request))
537     else:
538         return render_to_response('users.html', {'users':users}, RequestContext(request))
539
540 phone_re = re.compile('^\\+.+')
541
542 class UserEditForm(forms.Form):
543     login = forms.RegexField(regex=r'^[a-zA-Z0-9_]+$', max_length=16,
544         error_message ='Login must only contain letters, numbers and underscores')
545     name = forms.CharField(max_length=50)
546     email = forms.EmailField()
547     phone = forms.RegexField(regex='\\+.+', max_length=20, required=False,
548         error_message ='Phones must start with a \'+\'')
549     def __init__(self, *args, **kargs):
550         forms.Form.__init__(self, *args, **kargs)
551         self.old_login = kargs['initial']['login']
552     def clean_login(self):
553         new_login = self.cleaned_data['login']
554         if new_login != self.old_login:
555             if  User.objects.filter(login=new_login).count():
556                 raise forms.ValidationError("Sorry that login is already in use. Try another one.")
557         return new_login
558
559 @http_authenticate(auth, 'ais')
560 def user_detail(request, login):
561     user = get_object_or_404(User, login=login)
562     user.admin_ok = user.is_admin_by(request.user.id)
563     return render_to_response('user_detail.html', {'auser': user}, RequestContext(request))
564
565 @http_authenticate(auth, 'ais')
566 def user_edit(request, login):
567     initial = {}
568     if login:
569         user = get_object_or_404(User, login=login)
570         if not user.is_admin_by(request.user.id):
571             return HttpResponseForbidden('403 Forbidden')
572     else:
573         user = User()
574         user.father_id = request.user.id;
575     initial['login'] = user.login
576     initial['name'] = user.name
577     initial['email'] = user.email
578     initial['phone'] = user.phone
579     if request.method == 'POST':
580         form = UserEditForm(request.POST, initial=initial)
581         if form.is_valid():
582             user.login = form.cleaned_data['login']
583             user.name = form.cleaned_data['name']
584             user.email = form.cleaned_data['email']
585             user.phone = form.cleaned_data['phone']
586             user.save()
587             return HttpResponseRedirect('/user/')
588     else: # GET
589         form = UserEditForm(initial=initial)
590
591     return render_to_response('user_edit.html', {'form':form, 'auser': user}, RequestContext(request))
592
593
594 class ChangePasswordForm(forms.Form):
595     new_password = forms.CharField(max_length=16, widget=forms.PasswordInput())
596     new_password_check = forms.CharField(max_length=16, widget=forms.PasswordInput())
597     def clean_generic_password(self, field_name):
598         password = self.cleaned_data[field_name]
599         try:
600             crack.FascistCheck(password)
601         except ValueError, err:
602             raise forms.ValidationError(err.message)
603         return password
604
605     def clean_new_password(self):
606         return self.clean_generic_password('new_password')
607     def clean_new_password_check(self):
608         return self.clean_generic_password('new_password_check')
609     def clean(self):
610         cleaned_data = self.cleaned_data
611         pass1 = cleaned_data.get('new_password')
612         pass2 = cleaned_data.get('new_password_check')
613         if pass1 != pass2:
614             self._errors['new_password_check'] = forms.util.ErrorList(['Passwords check must match'])
615             del cleaned_data['new_password_check']
616         return cleaned_data
617
618
619 @http_authenticate(auth, 'ais')
620 def user_change_password(request, login):
621     user = get_object_or_404(User, login=login)
622     if not user.is_admin_by(request.user.id):
623         return HttpResponseForbidden('403 Forbidden')
624     if request.method == 'POST':
625         form = ChangePasswordForm(request.POST)
626         if form.is_valid():
627             user.set_password(form.cleaned_data['new_password'])
628             user.save()
629             return HttpResponseRedirect('/user/')
630     else: # GET
631         form = ChangePasswordForm()
632     return render_to_response('user_change_password.html', {'form':form, 'auser':user}, RequestContext(request))
633
634
635 @http_authenticate(auth, 'ais')
636 def user_delete(request, login):
637     user = get_object_or_404(User, login=login)
638     if not user.is_admin_by(request.user.id):
639         return HttpResponseForbidden('403 Forbidden')
640     if request.REQUEST.get('confirm', None):
641         user.delete()
642         return HttpResponseRedirect('/user/')
643     return render_to_response('user_delete.html', {'form':None, 'auser':user}, RequestContext(request))
644
645
646 def logout(request):
647     # TODO
648     return HttpResponse('Not implemented')
649     #response = render_to_response('logout.html', {}, RequestContext(request))
650     #return response
651
652 periods = ({
653     #'name_tiny': '2h',
654     #'name_long': '2 hours',
655     #'seconds': 2*60*60
656     #}, {
657     'name_tiny': '6h',
658     'name_long': '6 hours',
659     'seconds': 6*60*60,
660     'default': True,
661     }, {
662     #'name_tiny': '2d',
663     #'name_long': '2 days',
664     #'seconds': 2*24*60*60
665     #}, {
666     'name_tiny': '14d',
667     'name_long': '2 weeks',
668     'seconds': 14*24*60*60
669     }, {
670     'name_tiny': '90d',
671     'name_long': '3 monthes',
672     'seconds': 90*24*60*60
673     })
674         
675 @http_authenticate(auth, 'ais')
676 def sources_stats(request):
677     filter_peers = request.REQUEST.get('peers', None)
678     if filter_peers:
679         filter_peers = filter_peers.split(u',')
680
681     peers_config = peers_get_config()
682     peers_display = []
683
684     for id4, peer in peers_config.iteritems():
685         if filter_peers:
686             if unicode(id4) not in filter_peers:
687                 continue
688         peer['id4'] = id4
689         peers_display.append(peer)
690
691     peers_display = sorted(peers_display, key=lambda k: k['id4'])
692
693     filter_types = request.REQUEST.get('types', None)
694     if filter_types:
695         filter_types = filter_types.split(u',')
696     else:
697         filter_types = [ 'bytes', 'counts' ]
698     logging.error('filter_types=%s', filter_types)
699
700     filter_periods = request.REQUEST.get('periods', None)
701     if filter_periods:
702         display_periods = []
703         for period_name in filter_periods.split(u','):
704             for period in periods:
705                 if period['name_tiny'] == period_name:
706                     display_periods.append(period)
707     else:
708         display_periods = periods
709     #logging.error('display_periods=%s', display_periods)
710
711
712     now = int(get_timestamp())
713     for config in peers_display:
714         id4 = config['id4']
715         for period in periods:
716             args = os.path.join(STATS_DIR, id4+'-'+period['name_tiny']+'-bytes.png'), \
717                 '--lazy', \
718                 '-l', '0', \
719                 '--title', config['name'] + ' - Bandwidth usage - ' + period['name_long'], \
720                 '--start', '%d' % (now - period['seconds']), \
721                 '--end', '%d' % now, \
722                 '--vertical-label', 'bps', \
723                 'DEF:bytes=%s:bytes:AVERAGE' % os.path.join(STATS_DIR, id4+'.rrd'), \
724                 'DEF:rawbytes=%s:rawbytes:AVERAGE' % os.path.join(STATS_DIR, id4+'.rrd'), \
725                 'CDEF:bits=bytes,8,*', \
726                 'CDEF:rawbits=rawbytes,8,*', \
727                 'LINE:bits#00FF00:IP payload', \
728                 'LINE:rawbits#FF0000:IP with headers'
729             rrdtool.graph(*args)
730             args = os.path.join(STATS_DIR, id4+'-'+period['name_tiny']+'-counts.png'), \
731                 '--lazy', \
732                 '-l', '0', \
733                 '--title', config['name'] + ' - Packet\'izer stats - ' + period['name_long'], \
734                 '--start', '%d' % (now-period['seconds']), \
735                 '--end', '%d' % now, \
736                 '--vertical-label', 'Hz', \
737                 'DEF:packets=%s:packets:AVERAGE' % os.path.join(STATS_DIR, id4+'.rrd'), \
738                 'DEF:lines=%s:lines:AVERAGE' % os.path.join(STATS_DIR, id4+'.rrd'), \
739                 'LINE:packets#FF0000:input packets', \
740                 'LINE:lines#00FF00:AIVDM lines'
741             rrdtool.graph(*args)
742     return render_to_response('sources.html', {'sources':peers_display, 'show_bytes': 'bytes' in filter_types,  'show_counts': 'counts' in filter_types, 'periods': display_periods}, RequestContext(request))
743
744
745 @http_authenticate(auth, 'ais')
746 def sources_index(request):
747     peers_config = peers_get_config()
748     peers_display = []
749     for id4, peer in peers_config.iteritems():
750         peer['id4'] = id4
751         peer['id2'] = id4[2:]
752         peer['active'] = is_id4_active(id4)
753         peers_display.append(peer)
754         
755     peers_display = sorted(peers_display, key=lambda k: k['id4'])
756
757     return render_to_response('sources_index.html', {'sources':peers_display, 'periods': periods}, RequestContext(request))