· 7 years ago · Jun 07, 2018, 05:12 PM
1import os
2
3# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
4BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
5
6
7# Quick-start development settings - unsuitable for production
8# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
9
10# SECURITY WARNING: keep the secret key used in production secret!
11SECRET_KEY = 'o0-0sp)e7qbr*6(v((xbv0q89&p#g=312_q=q+2uq4%ros(2+0'
12
13# SECURITY WARNING: don't run with debug turned on in production!
14DEBUG = True
15
16ALLOWED_HOSTS = []
17
18EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
19
20# Application definition
21
22INSTALLED_APPS = [
23 'django.contrib.admin',
24 'django.contrib.auth',
25 'django.contrib.contenttypes',
26 'django.contrib.sessions',
27 'django.contrib.messages',
28 'django.contrib.staticfiles',
29 'drivers', # my apps
30]
31
32MIDDLEWARE = [
33 'django.middleware.security.SecurityMiddleware',
34 'django.contrib.sessions.middleware.SessionMiddleware',
35 'django.middleware.common.CommonMiddleware',
36 'django.middleware.csrf.CsrfViewMiddleware',
37 'django.contrib.auth.middleware.AuthenticationMiddleware',
38 'django.contrib.messages.middleware.MessageMiddleware',
39 'django.middleware.clickjacking.XFrameOptionsMiddleware',
40]
41
42ROOT_URLCONF = 'buster.urls'
43
44TEMPLATES = [
45 {
46 'BACKEND': 'django.template.backends.django.DjangoTemplates',
47 'DIRS': [os.path.join(BASE_DIR, 'templates')],
48 'APP_DIRS': True,
49 'OPTIONS': {
50 'context_processors': [
51 'django.template.context_processors.debug',
52 'django.template.context_processors.request',
53 'django.contrib.auth.context_processors.auth',
54 'django.contrib.messages.context_processors.messages',
55 ],
56 },
57 },
58]
59
60WSGI_APPLICATION = 'buster.wsgi.application'
61
62
63# Database
64# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
65
66DATABASES = {
67 'default': {
68 'ENGINE': 'django.db.backends.postgresql_psycopg2',
69 'NAME': 'buster_db',
70 'USER': 'postgres',
71 'PASSWORD': 'qwerty',
72 'HOST': 'localhost',
73 'PORT': '5432',
74 }
75}
76
77
78# Password validation
79# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
80
81AUTH_PASSWORD_VALIDATORS = [
82 {
83 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
84 },
85 {
86 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
87 },
88 {
89 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
90 },
91 {
92 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
93 },
94]
95
96
97# Internationalization
98# https://docs.djangoproject.com/en/2.0/topics/i18n/
99
100LANGUAGE_CODE = 'ru-ru'
101
102TIME_ZONE = 'UTC'
103
104USE_I18N = True
105
106USE_L10N = True
107
108USE_TZ = True
109
110
111# Static files (CSS, JavaScript, Images)
112# https://docs.djangoproject.com/en/2.0/howto/static-files/
113
114STATIC_URL = '/static/'
115STATICFILES_DIRS = [
116 os.path.join(BASE_DIR, 'static'),
117]
118from django.contrib import admin
119from django.urls import path, include
120
121urlpatterns = [
122 path('admin/', admin.site.urls),
123 path('accounts/', include('drivers.urls')), # application url's
124]
125from django.forms import ModelForm
126from django.core.exceptions import ValidationError
127from .models import Organization, Car, Profile, OrganizationRoute, Schedule
128from django.contrib.auth.models import User
129from django.contrib.auth.forms import UserChangeForm
130
131import requests, json
132
133# Your forms here.
134class OrganizationForm(ModelForm):
135
136 def clean_unp(self):
137 EGR_API = 'http://egr.gov.by/egrn/API.jsp?NM='
138 unp = self.cleaned_data['unp']
139 try:
140 r = requests.get(EGR_API + unp)
141 json_data = r.json()
142 except json.decoder.JSONDecodeError:
143 raise ValidationError('UNP is not register')
144
145 if json_data[0]['ACT'] is False:
146 raise ValidationError('UNP number in not active')
147
148 return unp
149
150 class Meta:
151 model = Organization
152 fields = ('name', 'unp', )
153
154
155class ProfileForm(UserChangeForm):
156 class Meta:
157 model = User
158 fields = ('email', 'username', 'first_name', 'last_name', 'password')
159
160class OrganizationRoutesForm(ModelForm):
161 class Meta:
162 model = OrganizationRoute
163 fields = ('start_place', 'final_place', 'start_time', 'final_time', 'price')
164 exclude = ('organization', )
165
166class OrganizationCarsForm(ModelForm):
167 class Meta:
168 model = Car
169 fields = ('model', 'seats', 'number')
170 labels = {'model': 'Car model',
171 'seats': 'Number of seats',
172 'number': 'Car number',
173 }
174 exclude = ('organization', )
175
176class ScheduleForm(ModelForm):
177 class Meta:
178 model = Schedule
179 fields = ('date', 'org_route', 'driver', 'car')
180from django.db import models
181from django.contrib.auth.models import User
182
183# Create your models here.
184
185class Profile(models.Model):
186 mobile_number = models.CharField(max_length=12, unique=True)
187 user = models.OneToOneField(User, on_delete=models.CASCADE)
188
189class Organization(models.Model):
190 name = models.CharField(max_length=255)
191 unp = models.CharField(max_length=9, unique=True)
192
193class Car(models.Model):
194 model = models.CharField(max_length=255)
195 seats = models.IntegerField()
196 number = models.CharField(max_length=9, unique=True)
197 organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
198
199class Employee(models.Model):
200 STATUS = (
201 ('OP', 'Operator'),
202 ('DR', 'Driver')
203 )
204 name = models.OneToOneField(User, on_delete=models.CASCADE)
205 status = models.CharField(max_length=8, choices=STATUS)
206 hire_date = models.DateField()
207 hire_end_date = models.DateField(null=True)
208 organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
209
210class OrganizationInvite(models.Model):
211 code = models.CharField(unique=True, max_length=10)
212 organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
213
214class OrganizationRoute(models.Model):
215 start_place = models.CharField(max_length=255)
216 final_place = models.CharField(max_length=255)
217 #route_distance = models.IntegerField() # distance rout value
218 start_time = models.TimeField()
219 #route_time = models.CharField(max_length=4) # time in rout distance
220 final_time = models.TimeField()
221 price = models.IntegerField()
222 organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
223
224class Schedule(models.Model):
225 date = models.DateField()
226 org_route = models.ForeignKey(OrganizationRoute, on_delete=models.CASCADE)
227 driver = models.ForeignKey(Employee, on_delete=models.CASCADE, related_name='schedule_driver')
228 car = models.ForeignKey(Car, on_delete=models.CASCADE, related_name='schedule_car')
229
230class Flight(models.Model):
231 STATUS = (
232 ('True', 'True'),
233 ('False', 'False')
234 )
235 schedule = models.ForeignKey(Schedule, on_delete=models.CASCADE, related_name='flight_schedule')
236 user = models.OneToOneField(User, on_delete=models.CASCADE)
237 status = models.CharField(max_length=5, choices=STATUS)
238
239from django.conf.urls import url, include, re_path
240from .views import signup, new_organization, send_invite_code, get_employees, edit_profile, add_route, add_car, add_schedule, get_cars, get_routes, get_main, get_search, get_user_routes, join_invite, flight_activate, get_stats
241from django.contrib.auth.views import login, logout
242
243urlpatterns = [
244 # url(r'^profile/$', home),
245 url(r'^signup/$', signup, name='signup'),
246 url(r'^logout/', logout, name='logout'),
247 url(r'^login/$', login, {'template_name': 'accounts/login.html'}, name='login'),
248 url(r'^setting/$', edit_profile, name='settings_page'),
249 # url(r'^organization/$', get_organization, name='organization'),
250 url(r'^organization/(?P<invite>[a-zA-Z0-9]+)$', join_invite, name='invite_join'),
251 #url(r'^organization/new/$', new_organization, name='new_org'),
252 url(r'^organization/sho/$', send_invite_code, name='invite'),
253
254 url(r'^organization/activate/$', flight_activate, name='flight_activate'),
255
256 # organization pages
257 url(r'^main/$', get_main, name='main_page'),
258 url(r'^search$', get_search, name='search_page'),
259 url(r'^routes', get_user_routes, name='user_routes'),
260 # add
261 url(r'^organization/add/route$', add_route, name='add_route'),
262 url(r'^organization/add/car$', add_car, name='add_car'),
263 url(r'^organization/add/schedule$', add_schedule, name='add_schedule'),
264 # get
265 url(r'^organization/employees/$', get_employees, name='employees'),
266 url(r'^organization/cars/$', get_cars, name='cars'),
267 url(r'^organization/routes/$', get_routes, name='routes'),
268 url(r'^organization/stats/$', get_stats, name='stats'),
269from django.shortcuts import render, redirect
270from django.db.models import Q
271from django.contrib.auth.forms import UserCreationForm, User
272from .forms import OrganizationForm, ProfileForm, OrganizationRoutesForm, OrganizationCarsForm, ScheduleForm
273from django.contrib.auth import authenticate, login, logout
274from .models import Profile, Organization, Schedule, Car, Employee, OrganizationInvite, OrganizationRoute, Flight
275from django.core.mail import send_mail, BadHeaderError
276from django.http import HttpResponse, HttpResponseRedirect
277from django.contrib import messages
278
279import string
280import random
281import googlemaps
282import datetime
283
284GOOGLE_API_KEY = 'AIzaSyBhMPu5yfGH1CdqFXO7yiksGlgNR4T07J0'
285# Create your views here.
286
287def signup(request):
288 if request.method == 'POST':
289 form = UserCreationForm(request.POST)
290 if form.is_valid():
291 form.save()
292 username = form.cleaned_data.get('username')
293 password = form.cleaned_data.get('password1')
294 user = authenticate(username=username, password=password)
295 login(request, user)
296 return redirect('/accounts/profile')
297 else:
298 form = UserCreationForm()
299 return render(request, 'accounts/signup.html', {'form': form})
300
301
302def edit_profile(request):
303 if request.method == 'POST':
304 form = ProfileForm(request.POST, instance=request.user)
305 if form.is_valid():
306 form.save()
307 return render(request, 'accounts/settings.html')
308 else:
309 form = ProfileForm(instance=request.user)
310 return render(request, 'accounts/settings.html', {'form': form})
311
312# user organization information
313
314def get_main(request):
315 return render(request, 'search/main_page.html')
316
317def error_msg(request):
318 messages.error(request, '⚠ПожалуйÑта выберите начальную и конечную точку маршрута')
319 return redirect('main_page')
320
321def reg_msg(request):
322 messages.error(request, 'âš Ð’Ñ‹ уже запиÑаны на данный рейÑ')
323 return redirect('main_page')
324
325def ok_msg(request):
326 messages.error(request, '✔ Ð’Ñ‹ уÑпешно зарегеÑтрированы на рейÑ')
327 return redirect('user_routes')
328
329def get_search(request):
330 queryset_list = Schedule.objects.all()
331 if request.method == 'GET':
332 #query = request.GET.get('start', 'final')
333 start = request.GET['start']
334 final = request.GET['final']
335 date = request.GET['date']
336 time = request.GET['time']
337 if start and final:
338 queryset_list = queryset_list.filter(
339 Q(org_route__start_place__icontains=start) & Q(org_route__final_place__icontains=final)
340 )
341 elif start and final and date:
342 queryset_list = queryset_list.filter(
343 Q(org_route__start_place__icontains=start) & Q(org_route__final_place__icontains=final) & Q(date__icontains=date)
344 )
345 else:
346 error_msg(request)
347 context = {
348 'queryset': queryset_list,
349 }
350 return render(request, 'search/main_page.html', context)
351
352def check_operator_status(user):
353 employee = Employee.objects.get(name=user)
354 if employee.status == 'OP':
355 return True
356
357def get_employees(request):
358 user = request.user
359 organization = user.employee.organization
360 if organization:
361 employees = Employee.objects.filter(organization=organization)
362 status = check_operator_status(user)
363 return render(request, 'organization/employees_page.html', {'employees': employees, 'status': status})
364
365def get_cars(request):
366 user = request.user
367 organization = user.employee.organization
368 if organization:
369 cars = Car.objects.filter(organization=organization)
370 status = check_operator_status(user)
371 return render(request, 'organization/cars_page.html', {'cars': cars, 'status': status})
372
373def get_routes(request):
374 user = request.user
375 organization = user.employee.organization
376 if organization:
377 routes = OrganizationRoute.objects.filter(organization=organization)
378 status = check_operator_status(user)
379 return render(request, 'organization/routes_page.html', {'routes': routes, 'status': status})
380
381def get_stats(request):
382 user = request.user
383 organization = user.employee.organization
384 if organization:
385 routes = OrganizationRoute.objects.filter(organization=organization)
386 return render(request, 'organization/statistic_page.html', {'routes': routes})
387
388def add_route(request):
389 gmaps = googlemaps.Client(key=GOOGLE_API_KEY)
390 if request.method == 'POST':
391 organization = request.user.employee.organization
392 form = OrganizationRoutesForm(request.POST)
393 if form.is_valid():
394 car = form.save(commit=False)
395 car.organization = organization
396 car.save()
397 return render(request, 'organization/add_page.html')
398 else:
399 form = OrganizationRoutesForm()
400 return render(request, 'organization/add_page.html', {'form': form})
401
402def add_car(request):
403 if request.method == 'POST':
404 organization = request.user.employee.organization
405 form = OrganizationCarsForm(request.POST)
406 if form.is_valid():
407 route = form.save(commit=False)
408 route.organization = organization
409 route.save()
410 return render(request, 'organization/add_page.html')
411 else:
412 form = OrganizationCarsForm()
413 return render(request, 'organization/add_page.html', {'form': form})
414
415def add_schedule(request):
416 if request.method == 'POST':
417 organization = request.user.employee.organization
418 form = ScheduleForm(request.POST)
419 if form.is_valid():
420 car = form.save(commit=False)
421 car.organization = organization
422 car.save()
423 return render(request, 'organization/add_page.html')
424 else:
425 form = ScheduleForm()
426 return render(request, 'organization/add_page.html', {'form': form})
427
428def new_organization(request):
429 if request.method == 'POST':
430 form = OrganizationForm(request.POST)
431 if form.is_valid():
432 form.save()
433 name = form.cleaned_data.get('name')
434 unp = form.cleaned_data.get('unp')
435 return redirect('login')
436 else:
437 form = OrganizationForm()
438 return render(request, 'accounts/new_org.html', {'form': form})
439
440def get_user_routes(request):
441 user = request.user
442 # driver = user.employee.schedule
443 if user:
444 flight = Flight.objects.filter(user=user)
445 return render(request, 'accounts/user_routes.html', {'flight': flight})
446
447def flight_activate(request):
448 if request.method == 'POST':
449 user = request.user
450 schdule = request.POST.get('route')
451 kek = Schedule.objects.get(id=schdule)
452 flight = Flight.objects.filter(user=user)
453 if schdule:
454 if flight:
455 reg_msg(request)
456 else:
457 Flight.objects.create(schedule=kek, user=user, status=False)
458 ok_msg(request)
459 return redirect('main_page')
460
461
462# invite 'system'
463def send_invite_code(request):
464 # code settings
465 user = request.user
466 organization = user.employee.organization
467 code = ''.join(random.choice(string.ascii_uppercase + string.digits + string.ascii_lowercase) for _ in range(10))
468
469 # email settings
470 subject = 'Buster invite code'
471 message = 'Hi! You invited to drivers company. If you not Roman - welcome to service. Invite url - http://localhost:8000/accounts/organization/{}'.format(code)
472 from_message = request.POST.get('from_invite', '')
473
474 if from_message and check_operator_status(user):
475 try:
476 send_mail(subject, message, 'invites@buster.io', [from_message])
477 OrganizationInvite.objects.create(code=code, organization=organization)
478 except BadHeaderError:
479 return HttpResponse('invalid header')
480 return redirect('/accounts/organization')
481 else:
482 return HttpResponse('Make sure all fields are entered and valid.')
483
484def join_invite(request, invite):
485 user = request.user
486 invite_code = OrganizationInvite.objects.get(code=invite)
487 if invite_code:
488 Employee.objects.create(
489 name=user,
490 status='DR',
491 hire_date=datetime.date.today(),
492 hire_end_date=datetime.date.today(),
493 organization=invite_code.organization
494 )
495 invite_code.delete()
496var gulp = require('gulp'),
497 gutil = require('gulp-util' ),
498 sass = require('gulp-sass'),
499 browserSync = require('browser-sync'),
500 concat = require('gulp-concat'),
501 uglify = require('gulp-uglify'),
502 cleanCSS = require('gulp-clean-css'),
503 rename = require('gulp-rename'),
504 del = require('del'),
505 imagemin = require('gulp-imagemin'),
506 cache = require('gulp-cache'),
507 autoprefixer = require('gulp-autoprefixer'),
508 ftp = require('vinyl-ftp'),
509 notify = require("gulp-notify"),
510 rsync = require('gulp-rsync');
511
512// ПользовательÑкие Ñкрипты проекта
513
514gulp.task('common-js', function() {
515 return gulp.src([
516 'app/js/common.js',
517 ])
518 .pipe(concat('common.min.js'))
519 .pipe(uglify())
520 .pipe(gulp.dest('app/js'));
521});
522
523gulp.task('js', ['common-js'], function() {
524 return gulp.src([
525 'app/libs/jquery/dist/jquery.min.js',
526 'app/js/common.min.js', // Ð’Ñегда в конце
527 ])
528 .pipe(concat('scripts.min.js'))
529 // .pipe(uglify()) // Минимизировать веÑÑŒ js (на выбор)
530 .pipe(gulp.dest('app/js'))
531 .pipe(browserSync.reload({stream: true}));
532});
533
534gulp.task('browser-sync', function() {
535 browserSync({
536 server: {
537 baseDir: 'app'
538 },
539 notify: false,
540 // tunnel: true,
541 // tunnel: "projectmane", //Demonstration page: http://projectmane.localtunnel.me
542 });
543});
544
545gulp.task('sass', function() {
546 return gulp.src('app/sass/**/*.sass')
547 .pipe(sass({outputStyle: 'expand'}).on("error", notify.onError()))
548 .pipe(rename({suffix: '.min', prefix : ''}))
549 .pipe(autoprefixer(['last 15 versions']))
550 .pipe(cleanCSS()) // Опционально, закомментировать при отладке
551 .pipe(gulp.dest('app/css'))
552 .pipe(browserSync.reload({stream: true}));
553});
554
555gulp.task('watch', ['sass', 'js', 'browser-sync'], function() {
556 gulp.watch('app/sass/**/*.sass', ['sass']);
557 gulp.watch(['libs/**/*.js', 'app/js/common.js'], ['js']);
558 gulp.watch('app/*.html', browserSync.reload);
559});
560
561gulp.task('imagemin', function() {
562 return gulp.src('app/img/**/*')
563 .pipe(cache(imagemin())) // Cache Images
564 .pipe(gulp.dest('dist/img'));
565});
566
567gulp.task('build', ['removedist', 'imagemin', 'sass', 'js'], function() {
568
569 var buildFiles = gulp.src([
570 'app/*.html',
571 'app/.htaccess',
572 ]).pipe(gulp.dest('dist'));
573
574 var buildCss = gulp.src([
575 'app/css/main.min.css',
576 ]).pipe(gulp.dest('dist/css'));
577
578 var buildJs = gulp.src([
579 'app/js/scripts.min.js',
580 ]).pipe(gulp.dest('dist/js'));
581
582 var buildFonts = gulp.src([
583 'app/fonts/**/*',
584 ]).pipe(gulp.dest('dist/fonts'));
585
586});
587
588gulp.task('deploy', function() {
589
590 var conn = ftp.create({
591 host: 'hostname.com',
592 user: 'username',
593 password: 'userpassword',
594 parallel: 10,
595 log: gutil.log
596 });
597
598 var globs = [
599 'dist/**',
600 'dist/.htaccess',
601 ];
602 return gulp.src(globs, {buffer: false})
603 .pipe(conn.dest('/path/to/folder/on/server'));
604
605});
606
607gulp.task('rsync', function() {
608 return gulp.src('dist/**')
609 .pipe(rsync({
610 root: 'dist/',
611 hostname: 'username@yousite.com',
612 destination: 'yousite/public_html/',
613 // include: ['*.htaccess'], // Скрытые файлы, которые необходимо включить в деплой
614 recursive: true,
615 archive: true,
616 silent: false,
617 compress: true
618 }));
619});
620
621gulp.task('removedist', function() { return del.sync('dist'); });
622gulp.task('clearcache', function () { return cache.clearAll(); });
623
624gulp.task('default', ['watch']);
625bal gettext, interpolate, ngettext*/
626(function($) {
627 'use strict';
628 var lastChecked;
629
630 $.fn.actions = function(opts) {
631 var options = $.extend({}, $.fn.actions.defaults, opts);
632 var actionCheckboxes = $(this);
633 var list_editable_changed = false;
634 var showQuestion = function() {
635 $(options.acrossClears).hide();
636 $(options.acrossQuestions).show();
637 $(options.allContainer).hide();
638 },
639 showClear = function() {
640 $(options.acrossClears).show();
641 $(options.acrossQuestions).hide();
642 $(options.actionContainer).toggleClass(options.selectedClass);
643 $(options.allContainer).show();
644 $(options.counterContainer).hide();
645 },
646 reset = function() {
647 $(options.acrossClears).hide();
648 $(options.acrossQuestions).hide();
649 $(options.allContainer).hide();
650 $(options.counterContainer).show();
651 },
652 clearAcross = function() {
653 reset();
654 $(options.acrossInput).val(0);
655 $(options.actionContainer).removeClass(options.selectedClass);
656 },
657 checker = function(checked) {
658 if (checked) {
659 showQuestion();
660 } else {
661 reset();
662 }
663 $(actionCheckboxes).prop("checked", checked)
664 .parent().parent().toggleClass(options.selectedClass, checked);
665 },
666 updateCounter = function() {
667 var sel = $(actionCheckboxes).filter(":checked").length;
668 // data-actions-icnt is defined in the generated HTML
669 // and contains the total amount of objects in the queryset
670 var actions_icnt = $('.action-counter').data('actionsIcnt');
671 $(options.counterContainer).html(interpolate(
672 ngettext('%(sel)s of %(cnt)s selected', '%(sel)s of %(cnt)s selected', sel), {
673 sel: sel,
674 cnt: actions_icnt
675 }, true));
676 $(options.allToggle).prop("checked", function() {
677 var value;
678 if (sel === actionCheckboxes.length) {
679 value = true;
680 showQuestion();
681 } else {
682 value = false;
683 clearAcross();
684 }
685 return value;
686 });
687 };
688 // Show counter by default
689 $(options.counterContainer).show();
690 // Check state of checkboxes and reinit state if needed
691 $(this).filter(":checked").each(function(i) {
692 $(this).parent().parent().toggleClass(options.selectedClass);
693 updateCounter();
694 if ($(options.acrossInput).val() === 1) {
695 showClear();
696 }
697 });
698 $(options.allToggle).show().click(function() {
699 checker($(this).prop("checked"));
700 updateCounter();
701 });
702 $("a", options.acrossQuestions).click(function(event) {
703 event.preventDefault();
704 $(options.acrossInput).val(1);
705 showClear();
706 });
707 $("a", options.acrossClears).click(function(event) {
708 event.preventDefault();
709 $(options.allToggle).prop("checked", false);
710 clearAcross();
711 checker(0);
712 updateCounter();
713 });
714 lastChecked = null;
715 $(actionCheckboxes).click(function(event) {
716 if (!event) { event = window.event; }
717 var target = event.target ? event.target : event.srcElement;
718 if (lastChecked && $.data(lastChecked) !== $.data(target) && event.shiftKey === true) {
719 var inrange = false;
720 $(lastChecked).prop("checked", target.checked)
721 .parent().parent().toggleClass(options.selectedClass, target.checked);
722 $(actionCheckboxes).each(function() {
723 if ($.data(this) === $.data(lastChecked) || $.data(this) === $.data(target)) {
724 inrange = (inrange) ? false : true;
725 }
726 if (inrange) {
727 $(this).prop("checked", target.checked)
728 .parent().parent().toggleClass(options.selectedClass, target.checked);
729 }
730 });
731 }
732 $(target).parent().parent().toggleClass(options.selectedClass, target.checked);
733 lastChecked = target;
734 updateCounter();
735 });
736 $('form#changelist-form table#result_list tr').on('change', 'td:gt(0) :input', function() {
737 list_editable_changed = true;
738 });
739 $('form#changelist-form button[name="index"]').click(function(event) {
740 if (list_editable_changed) {
741 return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."));
742 }
743 });
744 $('form#changelist-form input[name="_save"]').click(function(event) {
745 var action_changed = false;
746 $('select option:selected', options.actionContainer).each(function() {
747 if ($(this).val()) {
748 action_changed = true;
749 }
750 });
751 if (action_changed) {
752 if (list_editable_changed) {
753 return confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action."));
754 } else {
755 return confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."));
756 }
757 }
758 });
759 };
760 /* Setup plugin defaults */
761 $.fn.actions.defaults = {
762 actionContainer: "div.actions",
763 counterContainer: "span.action-counter",
764 allContainer: "div.actions span.all",
765 acrossInput: "div.actions input.select-across",
766 acrossQuestions: "div.actions span.question",
767 acrossClears: "div.actions span.clear",
768 allToggle: "#action-toggle",
769 selectedClass: "selected"
770 };
771 $(document).ready(function() {
772 var $actionsEls = $('tr input.action-select');
773 if ($actionsEls.length > 0) {
774 $actionsEls.actions();
775 }
776 });
777})(django.jQuery);
778
779var isOpera = (navigator.userAgent.indexOf("Opera") >= 0) && parseFloat(navigator.appVersion);
780var isIE = ((document.all) && (!isOpera)) && parseFloat(navigator.appVersion.split("MSIE ")[1].split(";")[0]);
781
782// quickElement(tagType, parentReference [, textInChildNode, attribute, attributeValue ...]);
783function quickElement() {
784 'use strict';
785 var obj = document.createElement(arguments[0]);
786 if (arguments[2]) {
787 var textNode = document.createTextNode(arguments[2]);
788 obj.appendChild(textNode);
789 }
790 var len = arguments.length;
791 for (var i = 3; i < len; i += 2) {
792 obj.setAttribute(arguments[i], arguments[i + 1]);
793 }
794 arguments[1].appendChild(obj);
795 return obj;
796}
797
798// "a" is reference to an object
799function removeChildren(a) {
800 'use strict';
801 while (a.hasChildNodes()) {
802 a.removeChild(a.lastChild);
803 }
804}
805
806// ----------------------------------------------------------------------------
807// Find-position functions by PPK
808// See http://www.quirksmode.org/js/findpos.html
809// ----------------------------------------------------------------------------
810function findPosX(obj) {
811 'use strict';
812 var curleft = 0;
813 if (obj.offsetParent) {
814 while (obj.offsetParent) {
815 curleft += obj.offsetLeft - ((isOpera) ? 0 : obj.scrollLeft);
816 obj = obj.offsetParent;
817 }
818 // IE offsetParent does not include the top-level
819 if (isIE && obj.parentElement) {
820 curleft += obj.offsetLeft - obj.scrollLeft;
821 }
822 } else if (obj.x) {
823 curleft += obj.x;
824 }
825 return curleft;
826}
827
828function findPosY(obj) {
829 'use strict';
830 var curtop = 0;
831 if (obj.offsetParent) {
832 while (obj.offsetParent) {
833 curtop += obj.offsetTop - ((isOpera) ? 0 : obj.scrollTop);
834 obj = obj.offsetParent;
835 }
836 // IE offsetParent does not include the top-level
837 if (isIE && obj.parentElement) {
838 curtop += obj.offsetTop - obj.scrollTop;
839 }
840 } else if (obj.y) {
841 curtop += obj.y;
842 }
843 return curtop;
844}
845
846//-----------------------------------------------------------------------------
847// Date object extensions
848// ----------------------------------------------------------------------------
849(function() {
850 'use strict';
851 Date.prototype.getTwelveHours = function() {
852 var hours = this.getHours();
853 if (hours === 0) {
854 return 12;
855 }
856 else {
857 return hours <= 12 ? hours : hours - 12;
858 }
859 };
860
861 Date.prototype.getTwoDigitMonth = function() {
862 return (this.getMonth() < 9) ? '0' + (this.getMonth() + 1) : (this.getMonth() + 1);
863 };
864
865 Date.prototype.getTwoDigitDate = function() {
866 return (this.getDate() < 10) ? '0' + this.getDate() : this.getDate();
867 };
868
869 Date.prototype.getTwoDigitTwelveHour = function() {
870 return (this.getTwelveHours() < 10) ? '0' + this.getTwelveHours() : this.getTwelveHours();
871 };
872
873 Date.prototype.getTwoDigitHour = function() {
874 return (this.getHours() < 10) ? '0' + this.getHours() : this.getHours();
875 };
876
877 Date.prototype.getTwoDigitMinute = function() {
878 return (this.getMinutes() < 10) ? '0' + this.getMinutes() : this.getMinutes();
879 };
880
881 Date.prototype.getTwoDigitSecond = function() {
882 return (this.getSeconds() < 10) ? '0' + this.getSeconds() : this.getSeconds();
883 };
884
885 Date.prototype.getHourMinute = function() {
886 return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute();
887 };
888
889 Date.prototype.getHourMinuteSecond = function() {
890 return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute() + ':' + this.getTwoDigitSecond();
891 };
892
893 Date.prototype.getFullMonthName = function() {
894 return typeof window.CalendarNamespace === "undefined"
895 ? this.getTwoDigitMonth()
896 : window.CalendarNamespace.monthsOfYear[this.getMonth()];
897 };
898
899 Date.prototype.strftime = function(format) {
900 var fields = {
901 B: this.getFullMonthName(),
902 c: this.toString(),
903 d: this.getTwoDigitDate(),
904 H: this.getTwoDigitHour(),
905 I: this.getTwoDigitTwelveHour(),
906 m: this.getTwoDigitMonth(),
907 M: this.getTwoDigitMinute(),
908 p: (this.getHours() >= 12) ? 'PM' : 'AM',
909 S: this.getTwoDigitSecond(),
910 w: '0' + this.getDay(),
911 x: this.toLocaleDateString(),
912 X: this.toLocaleTimeString(),
913 y: ('' + this.getFullYear()).substr(2, 4),
914 Y: '' + this.getFullYear(),
915 '%': '%'
916 };
917 var result = '', i = 0;
918 while (i < format.length) {
919 if (format.charAt(i) === '%') {
920 result = result + fields[format.charAt(i + 1)];
921 ++i;
922 }
923 else {
924 result = result + format.charAt(i);
925 }
926 ++i;
927 }
928 return result;
929 };
930
931// ----------------------------------------------------------------------------
932// String object extensions
933// ----------------------------------------------------------------------------
934 String.prototype.pad_left = function(pad_length, pad_string) {
935 var new_string = this;
936 for (var i = 0; new_string.length < pad_length; i++) {
937 new_string = pad_string + new_string;
938 }
939 return new_string;
940 };
941
942 String.prototype.strptime = function(format) {
943 var split_format = format.split(/[.\-/]/);
944 var date = this.split(/[.\-/]/);
945 var i = 0;
946 var day, month, year;
947 while (i < split_format.length) {
948 switch (split_format[i]) {
949 case "%d":
950 day = date[i];
951 break;
952 case "%m":
953 month = date[i] - 1;
954 break;
955 case "%Y":
956 year = date[i];
957 break;
958 case "%y":
959 year = date[i];
960 break;
961 }
962 ++i;
963 }
964 // Create Date object from UTC since the parsed value is supposed to be
965 // in UTC, not local time. Also, the calendar uses UTC functions for
966 // date extraction.
967 return new Date(Date.UTC(year, month, day));
968 };
969
970})();
971
972function getStyle(oElm, strCssRule) {
973 'use strict';
974 var strValue = "";
975 if(document.defaultView && document.defaultView.getComputedStyle) {
976 strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule);
977 }
978 else if(oElm.currentStyle) {
979 strCssRule = strCssRule.replace(/\-(\w)/g, function(strMatch, p1) {
980 return p1.toUpperCase();
981 });
982 strValue = oElm.currentStyle[strCssRule];
983 }
984 return strValue;
985}
986(function($) {
987 'use strict';
988 $.fn.formset = function(opts) {
989 var options = $.extend({}, $.fn.formset.defaults, opts);
990 var $this = $(this);
991 var $parent = $this.parent();
992 var updateElementIndex = function(el, prefix, ndx) {
993 var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))");
994 var replacement = prefix + "-" + ndx;
995 if ($(el).prop("for")) {
996 $(el).prop("for", $(el).prop("for").replace(id_regex, replacement));
997 }
998 if (el.id) {
999 el.id = el.id.replace(id_regex, replacement);
1000 }
1001 if (el.name) {
1002 el.name = el.name.replace(id_regex, replacement);
1003 }
1004 };
1005 var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").prop("autocomplete", "off");
1006 var nextIndex = parseInt(totalForms.val(), 10);
1007 var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").prop("autocomplete", "off");
1008 // only show the add button if we are allowed to add more items,
1009 // note that max_num = None translates to a blank string.
1010 var showAddButton = maxForms.val() === '' || (maxForms.val() - totalForms.val()) > 0;
1011 $this.each(function(i) {
1012 $(this).not("." + options.emptyCssClass).addClass(options.formCssClass);
1013 });
1014 if ($this.length && showAddButton) {
1015 var addButton = options.addButton;
1016 if (addButton === null) {
1017 if ($this.prop("tagName") === "TR") {
1018 // If forms are laid out as table rows, insert the
1019 // "add" button in a new table row:
1020 var numCols = this.eq(-1).children().length;
1021 $parent.append('<tr class="' + options.addCssClass + '"><td colspan="' + numCols + '"><a href="#">' + options.addText + "</a></tr>");
1022 addButton = $parent.find("tr:last a");
1023 } else {
1024 // Otherwise, insert it immediately after the last form:
1025 $this.filter(":last").after('<div class="' + options.addCssClass + '"><a href="#">' + options.addText + "</a></div>");
1026 addButton = $this.filter(":last").next().find("a");
1027 }
1028 }
1029 addButton.click(function(e) {
1030 e.preventDefault();
1031 var template = $("#" + options.prefix + "-empty");
1032 var row = template.clone(true);
1033 row.removeClass(options.emptyCssClass)
1034 .addClass(options.formCssClass)
1035 .attr("id", options.prefix + "-" + nextIndex);
1036 if (row.is("tr")) {
1037 // If the forms are laid out in table rows, insert
1038 // the remove button into the last table cell:
1039 row.children(":last").append('<div><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></div>");
1040 } else if (row.is("ul") || row.is("ol")) {
1041 // If they're laid out as an ordered/unordered list,
1042 // insert an <li> after the last list item:
1043 row.append('<li><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></li>");
1044 } else {
1045 // Otherwise, just insert the remove button as the
1046 // last child element of the form's container:
1047 row.children(":first").append('<span><a class="' + options.deleteCssClass + '" href="#">' + options.deleteText + "</a></span>");
1048 }
1049 row.find("*").each(function() {
1050 updateElementIndex(this, options.prefix, totalForms.val());
1051 });
1052 // Insert the new form when it has been fully edited
1053 row.insertBefore($(template));
1054 // Update number of total forms
1055 $(totalForms).val(parseInt(totalForms.val(), 10) + 1);
1056 nextIndex += 1;
1057 // Hide add button in case we've hit the max, except we want to add infinitely
1058 if ((maxForms.val() !== '') && (maxForms.val() - totalForms.val()) <= 0) {
1059 addButton.parent().hide();
1060 }
1061 // The delete button of each row triggers a bunch of other things
1062 row.find("a." + options.deleteCssClass).click(function(e1) {
1063 e1.preventDefault();
1064 // Remove the parent form containing this button:
1065 row.remove();
1066 nextIndex -= 1;
1067 // If a post-delete callback was provided, call it with the deleted form:
1068 if (options.removed) {
1069 options.removed(row);
1070 }
1071 $(document).trigger('formset:removed', [row, options.prefix]);
1072 // Update the TOTAL_FORMS form count.
1073 var forms = $("." + options.formCssClass);
1074 $("#id_" + options.prefix + "-TOTAL_FORMS").val(forms.length);
1075 // Show add button again once we drop below max
1076 if ((maxForms.val() === '') || (maxForms.val() - forms.length) > 0) {
1077 addButton.parent().show();
1078 }
1079 // Also, update names and ids for all remaining form controls
1080 // so they remain in sequence:
1081 var i, formCount;
1082 var updateElementCallback = function() {
1083 updateElementIndex(this, options.prefix, i);
1084 };
1085 for (i = 0, formCount = forms.length; i < formCount; i++) {
1086 updateElementIndex($(forms).get(i), options.prefix, i);
1087 $(forms.get(i)).find("*").each(updateElementCallback);
1088 }
1089 });
1090 // If a post-add callback was supplied, call it with the added form:
1091 if (options.added) {
1092 options.added(row);
1093 }
1094 $(document).trigger('formset:added', [row, options.prefix]);
1095 });
1096 }
1097 return this;
1098 };
1099
1100 /* Setup plugin defaults */
1101 $.fn.formset.defaults = {
1102 prefix: "form", // The form prefix for your django formset
1103 addText: "add another", // Text for the add link
1104 deleteText: "remove", // Text for the delete link
1105 addCssClass: "add-row", // CSS class applied to the add link
1106 deleteCssClass: "delete-row", // CSS class applied to the delete link
1107 emptyCssClass: "empty-row", // CSS class applied to the empty row
1108 formCssClass: "dynamic-form", // CSS class applied to each form in a formset
1109 added: null, // Function called each time a new form is added
1110 removed: null, // Function called each time a form is deleted
1111 addButton: null // Existing add button to use
1112 };
1113
1114
1115 // Tabular inlines ---------------------------------------------------------
1116 $.fn.tabularFormset = function(options) {
1117 var $rows = $(this);
1118 var alternatingRows = function(row) {
1119 $($rows.selector).not(".add-row").removeClass("row1 row2")
1120 .filter(":even").addClass("row1").end()
1121 .filter(":odd").addClass("row2");
1122 };
1123
1124 var reinitDateTimeShortCuts = function() {
1125 // Reinitialize the calendar and clock widgets by force
1126 if (typeof DateTimeShortcuts !== "undefined") {
1127 $(".datetimeshortcuts").remove();
1128 DateTimeShortcuts.init();
1129 }
1130 };
1131
1132 var updateSelectFilter = function() {
1133 // If any SelectFilter widgets are a part of the new form,
1134 // instantiate a new SelectFilter instance for it.
1135 if (typeof SelectFilter !== 'undefined') {
1136 $('.selectfilter').each(function(index, value) {
1137 var namearr = value.name.split('-');
1138 SelectFilter.init(value.id, namearr[namearr.length - 1], false);
1139 });
1140 $('.selectfilterstacked').each(function(index, value) {
1141 var namearr = value.name.split('-');
1142 SelectFilter.init(value.id, namearr[namearr.length - 1], true);
1143 });
1144 }
1145 };
1146
1147 var initPrepopulatedFields = function(row) {
1148 row.find('.prepopulated_field').each(function() {
1149 var field = $(this),
1150 input = field.find('input, select, textarea'),
1151 dependency_list = input.data('dependency_list') || [],
1152 dependencies = [];
1153 $.each(dependency_list, function(i, field_name) {
1154 dependencies.push('#' + row.find('.field-' + field_name).find('input, select, textarea').attr('id'));
1155 });
1156 if (dependencies.length) {
1157 input.prepopulate(dependencies, input.attr('maxlength'));
1158 }
1159 });
1160 };
1161
1162 $rows.formset({
1163 prefix: options.prefix,
1164 addText: options.addText,
1165 formCssClass: "dynamic-" + options.prefix,
1166 deleteCssClass: "inline-deletelink",
1167 deleteText: options.deleteText,
1168 emptyCssClass: "empty-form",
1169 removed: alternatingRows,
1170 added: function(row) {
1171 initPrepopulatedFields(row);
1172 reinitDateTimeShortCuts();
1173 updateSelectFilter();
1174 alternatingRows(row);
1175 },
1176 addButton: options.addButton
1177 });
1178
1179 return $rows;
1180 };
1181
1182 // Stacked inlines ---------------------------------------------------------
1183 $.fn.stackedFormset = function(options) {
1184 var $rows = $(this);
1185 var updateInlineLabel = function(row) {
1186 $($rows.selector).find(".inline_label").each(function(i) {
1187 var count = i + 1;
1188 $(this).html($(this).html().replace(/(#\d+)/g, "#" + count));
1189 });
1190 };
1191
1192 var reinitDateTimeShortCuts = function() {
1193 // Reinitialize the calendar and clock widgets by force, yuck.
1194 if (typeof DateTimeShortcuts !== "undefined") {
1195 $(".datetimeshortcuts").remove();
1196 DateTimeShortcuts.init();
1197 }
1198 };
1199
1200 var updateSelectFilter = function() {
1201 // If any SelectFilter widgets were added, instantiate a new instance.
1202 if (typeof SelectFilter !== "undefined") {
1203 $(".selectfilter").each(function(index, value) {
1204 var namearr = value.name.split('-');
1205 SelectFilter.init(value.id, namearr[namearr.length - 1], false);
1206 });
1207 $(".selectfilterstacked").each(function(index, value) {
1208 var namearr = value.name.split('-');
1209 SelectFilter.init(value.id, namearr[namearr.length - 1], true);
1210 });
1211 }
1212 };
1213
1214 var initPrepopulatedFields = function(row) {
1215 row.find('.prepopulated_field').each(function() {
1216 var field = $(this),
1217 input = field.find('input, select, textarea'),
1218 dependency_list = input.data('dependency_list') || [],
1219 dependencies = [];
1220 $.each(dependency_list, function(i, field_name) {
1221 dependencies.push('#' + row.find('.form-row .field-' + field_name).find('input, select, textarea').attr('id'));
1222 });
1223 if (dependencies.length) {
1224 input.prepopulate(dependencies, input.attr('maxlength'));
1225 }
1226 });
1227 };
1228
1229 $rows.formset({
1230 prefix: options.prefix,
1231 addText: options.addText,
1232 formCssClass: "dynamic-" + options.prefix,
1233 deleteCssClass: "inline-deletelink",
1234 deleteText: options.deleteText,
1235 emptyCssClass: "empty-form",
1236 removed: updateInlineLabel,
1237 added: function(row) {
1238 initPrepopulatedFields(row);
1239 reinitDateTimeShortCuts();
1240 updateSelectFilter();
1241 updateInlineLabel(row);
1242 },
1243 addButton: options.addButton
1244 });
1245
1246 return $rows;
1247 };
1248
1249 $(document).ready(function() {
1250 $(".js-inline-admin-formset").each(function() {
1251 var data = $(this).data(),
1252 inlineOptions = data.inlineFormset;
1253 switch(data.inlineType) {
1254 case "stacked":
1255 $(inlineOptions.name + "-group .inline-related").stackedFormset(inlineOptions.options);
1256 break;
1257 case "tabular":
1258 $(inlineOptions.name + "-group .tabular.inline-related tbody:first > tr").tabularFormset(inlineOptions.options);
1259 break;
1260 }
1261 });
1262 });
1263})(django.jQuery);
1264
1265(function($) {
1266 'use strict';
1267 function findForm(node) {
1268 // returns the node of the form containing the given node
1269 if (node.tagName.toLowerCase() !== 'form') {
1270 return findForm(node.parentNode);
1271 }
1272 return node;
1273 }
1274
1275 window.SelectFilter = {
1276 init: function(field_id, field_name, is_stacked) {
1277 if (field_id.match(/__prefix__/)) {
1278 // Don't initialize on empty forms.
1279 return;
1280 }
1281 var from_box = document.getElementById(field_id);
1282 from_box.id += '_from'; // change its ID
1283 from_box.className = 'filtered';
1284
1285 var ps = from_box.parentNode.getElementsByTagName('p');
1286 for (var i = 0; i < ps.length; i++) {
1287 if (ps[i].className.indexOf("info") !== -1) {
1288 // Remove <p class="info">, because it just gets in the way.
1289 from_box.parentNode.removeChild(ps[i]);
1290 } else if (ps[i].className.indexOf("help") !== -1) {
1291 // Move help text up to the top so it isn't below the select
1292 // boxes or wrapped off on the side to the right of the add
1293 // button:
1294 from_box.parentNode.insertBefore(ps[i], from_box.parentNode.firstChild);
1295 }
1296 }
1297
1298 // <div class="selector"> or <div class="selector stacked">
1299 var selector_div = quickElement('div', from_box.parentNode);
1300 selector_div.className = is_stacked ? 'selector stacked' : 'selector';
1301
1302 // <div class="selector-available">
1303 var selector_available = quickElement('div', selector_div);
1304 selector_available.className = 'selector-available';
1305 var title_available = quickElement('h2', selector_available, interpolate(gettext('Available %s') + ' ', [field_name]));
1306 quickElement(
1307 'span', title_available, '',
1308 'class', 'help help-tooltip help-icon',
1309 'title', interpolate(
1310 gettext(
1311 'This is the list of available %s. You may choose some by ' +
1312 'selecting them in the box below and then clicking the ' +
1313 '"Choose" arrow between the two boxes.'
1314 ),
1315 [field_name]
1316 )
1317 );
1318
1319 var filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter');
1320 filter_p.className = 'selector-filter';
1321
1322 var search_filter_label = quickElement('label', filter_p, '', 'for', field_id + '_input');
1323
1324 quickElement(
1325 'span', search_filter_label, '',
1326 'class', 'help-tooltip search-label-icon',
1327 'title', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name])
1328 );
1329
1330 filter_p.appendChild(document.createTextNode(' '));
1331
1332 var filter_input = quickElement('input', filter_p, '', 'type', 'text', 'placeholder', gettext("Filter"));
1333 filter_input.id = field_id + '_input';
1334
1335 selector_available.appendChild(from_box);
1336 var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_add_all_link');
1337 choose_all.className = 'selector-chooseall';
1338
1339 // <ul class="selector-chooser">
1340 var selector_chooser = quickElement('ul', selector_div);
1341 selector_chooser.className = 'selector-chooser';
1342 var add_link = quickElement('a', quickElement('li', selector_chooser), gettext('Choose'), 'title', gettext('Choose'), 'href', '#', 'id', field_id + '_add_link');
1343 add_link.className = 'selector-add';
1344 var remove_link = quickElement('a', quickElement('li', selector_chooser), gettext('Remove'), 'title', gettext('Remove'), 'href', '#', 'id', field_id + '_remove_link');
1345 remove_link.className = 'selector-remove';
1346
1347 // <div class="selector-chosen">
1348 var selector_chosen = quickElement('div', selector_div);
1349 selector_chosen.className = 'selector-chosen';
1350 var title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s') + ' ', [field_name]));
1351 quickElement(
1352 'span', title_chosen, '',
1353 'class', 'help help-tooltip help-icon',
1354 'title', interpolate(
1355 gettext(
1356 'This is the list of chosen %s. You may remove some by ' +
1357 'selecting them in the box below and then clicking the ' +
1358 '"Remove" arrow between the two boxes.'
1359 ),
1360 [field_name]
1361 )
1362 );
1363
1364 var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name'));
1365 to_box.className = 'filtered';
1366 var clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', '#', 'id', field_id + '_remove_all_link');
1367 clear_all.className = 'selector-clearall';
1368
1369 from_box.setAttribute('name', from_box.getAttribute('name') + '_old');
1370
1371 // Set up the JavaScript event handlers for the select box filter interface
1372 var move_selection = function(e, elem, move_func, from, to) {
1373 if (elem.className.indexOf('active') !== -1) {
1374 move_func(from, to);
1375 SelectFilter.refresh_icons(field_id);
1376 }
1377 e.preventDefault();
1378 };
1379 choose_all.addEventListener('click', function(e) {
1380 move_selection(e, this, SelectBox.move_all, field_id + '_from', field_id + '_to');
1381 });
1382 add_link.addEventListener('click', function(e) {
1383 move_selection(e, this, SelectBox.move, field_id + '_from', field_id + '_to');
1384 });
1385 remove_link.addEventListener('click', function(e) {
1386 move_selection(e, this, SelectBox.move, field_id + '_to', field_id + '_from');
1387 });
1388 clear_all.addEventListener('click', function(e) {
1389 move_selection(e, this, SelectBox.move_all, field_id + '_to', field_id + '_from');
1390 });
1391 filter_input.addEventListener('keypress', function(e) {
1392 SelectFilter.filter_key_press(e, field_id);
1393 });
1394 filter_input.addEventListener('keyup', function(e) {
1395 SelectFilter.filter_key_up(e, field_id);
1396 });
1397 filter_input.addEventListener('keydown', function(e) {
1398 SelectFilter.filter_key_down(e, field_id);
1399 });
1400 selector_div.addEventListener('change', function(e) {
1401 if (e.target.tagName === 'SELECT') {
1402 SelectFilter.refresh_icons(field_id);
1403 }
1404 });
1405 selector_div.addEventListener('dblclick', function(e) {
1406 if (e.target.tagName === 'OPTION') {
1407 if (e.target.closest('select').id === field_id + '_to') {
1408 SelectBox.move(field_id + '_to', field_id + '_from');
1409 } else {
1410 SelectBox.move(field_id + '_from', field_id + '_to');
1411 }
1412 SelectFilter.refresh_icons(field_id);
1413 }
1414 });
1415 findForm(from_box).addEventListener('submit', function() {
1416 SelectBox.select_all(field_id + '_to');
1417 });
1418 SelectBox.init(field_id + '_from');
1419 SelectBox.init(field_id + '_to');
1420 // Move selected from_box options to to_box
1421 SelectBox.move(field_id + '_from', field_id + '_to');
1422
1423 if (!is_stacked) {
1424 // In horizontal mode, give the same height to the two boxes.
1425 var j_from_box = $(from_box);
1426 var j_to_box = $(to_box);
1427 var resize_filters = function() { j_to_box.height($(filter_p).outerHeight() + j_from_box.outerHeight()); };
1428 if (j_from_box.outerHeight() > 0) {
1429 resize_filters(); // This fieldset is already open. Resize now.
1430 } else {
1431 // This fieldset is probably collapsed. Wait for its 'show' event.
1432 j_to_box.closest('fieldset').one('show.fieldset', resize_filters);
1433 }
1434 }
1435
1436 // Initial icon refresh
1437 SelectFilter.refresh_icons(field_id);
1438 },
1439 any_selected: function(field) {
1440 var any_selected = false;
1441 try {
1442 // Temporarily add the required attribute and check validity.
1443 // This is much faster in WebKit browsers than the fallback.
1444 field.attr('required', 'required');
1445 any_selected = field.is(':valid');
1446 field.removeAttr('required');
1447 } catch (e) {
1448 // Browsers that don't support :valid (IE < 10)
1449 any_selected = field.find('option:selected').length > 0;
1450 }
1451 return any_selected;
1452 },
1453 refresh_icons: function(field_id) {
1454 var from = $('#' + field_id + '_from');
1455 var to = $('#' + field_id + '_to');
1456 // Active if at least one item is selected
1457 $('#' + field_id + '_add_link').toggleClass('active', SelectFilter.any_selected(from));
1458 $('#' + field_id + '_remove_link').toggleClass('active', SelectFilter.any_selected(to));
1459 // Active if the corresponding box isn't empty
1460 $('#' + field_id + '_add_all_link').toggleClass('active', from.find('option').length > 0);
1461 $('#' + field_id + '_remove_all_link').toggleClass('active', to.find('option').length > 0);
1462 },
1463 filter_key_press: function(event, field_id) {
1464 var from = document.getElementById(field_id + '_from');
1465 // don't submit form if user pressed Enter
1466 if ((event.which && event.which === 13) || (event.keyCode && event.keyCode === 13)) {
1467 from.selectedIndex = 0;
1468 SelectBox.move(field_id + '_from', field_id + '_to');
1469 from.selectedIndex = 0;
1470 event.preventDefault();
1471 return false;
1472 }
1473 },
1474 filter_key_up: function(event, field_id) {
1475 var from = document.getElementById(field_id + '_from');
1476 var temp = from.selectedIndex;
1477 SelectBox.filter(field_id + '_from', document.getElementById(field_id + '_input').value);
1478 from.selectedIndex = temp;
1479 return true;
1480 },
1481 filter_key_down: function(event, field_id) {
1482 var from = document.getElementById(field_id + '_from');
1483 // right arrow -- move across
1484 if ((event.which && event.which === 39) || (event.keyCode && event.keyCode === 39)) {
1485 var old_index = from.selectedIndex;
1486 SelectBox.move(field_id + '_from', field_id + '_to');
1487 from.selectedIndex = (old_index === from.length) ? from.length - 1 : old_index;
1488 return false;
1489 }
1490 // down arrow -- wrap around
1491 if ((event.which && event.which === 40) || (event.keyCode && event.keyCode === 40)) {
1492 from.selectedIndex = (from.length === from.selectedIndex + 1) ? 0 : from.selectedIndex + 1;
1493 }
1494 // up arrow -- wrap around
1495 if ((event.which && event.which === 38) || (event.keyCode && event.keyCode === 38)) {
1496 from.selectedIndex = (from.selectedIndex === 0) ? from.length - 1 : from.selectedIndex - 1;
1497 }
1498 return true;
1499 }
1500 };
1501
1502 window.addEventListener('load', function(e) {
1503 $('select.selectfilter, select.selectfilterstacked').each(function() {
1504 var $el = $(this),
1505 data = $el.data();
1506 SelectFilter.init($el.attr('id'), data.fieldName, parseInt(data.isStacked, 10));
1507 });
1508 });
1509
1510})(django.jQuery);
1511(function() {
1512 'use strict';
1513 var timeParsePatterns = [
1514 // 9
1515 {
1516 re: /^\d{1,2}$/i,
1517 handler: function(bits) {
1518 if (bits[0].length === 1) {
1519 return '0' + bits[0] + ':00';
1520 } else {
1521 return bits[0] + ':00';
1522 }
1523 }
1524 },
1525 // 13:00
1526 {
1527 re: /^\d{2}[:.]\d{2}$/i,
1528 handler: function(bits) {
1529 return bits[0].replace('.', ':');
1530 }
1531 },
1532 // 9:00
1533 {
1534 re: /^\d[:.]\d{2}$/i,
1535 handler: function(bits) {
1536 return '0' + bits[0].replace('.', ':');
1537 }
1538 },
1539 // 3 am / 3 a.m. / 3am
1540 {
1541 re: /^(\d+)\s*([ap])(?:.?m.?)?$/i,
1542 handler: function(bits) {
1543 var hour = parseInt(bits[1]);
1544 if (hour === 12) {
1545 hour = 0;
1546 }
1547 if (bits[2].toLowerCase() === 'p') {
1548 if (hour === 12) {
1549 hour = 0;
1550 }
1551 return (hour + 12) + ':00';
1552 } else {
1553 if (hour < 10) {
1554 return '0' + hour + ':00';
1555 } else {
1556 return hour + ':00';
1557 }
1558 }
1559 }
1560 },
1561 // 3.30 am / 3:15 a.m. / 3.00am
1562 {
1563 re: /^(\d+)[.:](\d{2})\s*([ap]).?m.?$/i,
1564 handler: function(bits) {
1565 var hour = parseInt(bits[1]);
1566 var mins = parseInt(bits[2]);
1567 if (mins < 10) {
1568 mins = '0' + mins;
1569 }
1570 if (hour === 12) {
1571 hour = 0;
1572 }
1573 if (bits[3].toLowerCase() === 'p') {
1574 if (hour === 12) {
1575 hour = 0;
1576 }
1577 return (hour + 12) + ':' + mins;
1578 } else {
1579 if (hour < 10) {
1580 return '0' + hour + ':' + mins;
1581 } else {
1582 return hour + ':' + mins;
1583 }
1584 }
1585 }
1586 },
1587 // noon
1588 {
1589 re: /^no/i,
1590 handler: function(bits) {
1591 return '12:00';
1592 }
1593 },
1594 // midnight
1595 {
1596 re: /^mid/i,
1597 handler: function(bits) {
1598 return '00:00';
1599 }
1600 }
1601 ];
1602
1603 function parseTimeString(s) {
1604 for (var i = 0; i < timeParsePatterns.length; i++) {
1605 var re = timeParsePatterns[i].re;
1606 var handler = timeParsePatterns[i].handler;
1607 var bits = re.exec(s);
1608 if (bits) {
1609 return handler(bits);
1610 }
1611 }
1612 return s;
1613 }
1614
1615 window.parseTimeString = parseTimeString;
1616})();