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