From 42e4caa5edea4ba9379168bd269325e82e43884b Mon Sep 17 00:00:00 2001 From: Robert Sparks <rjsparks@nostrum.com> Date: Tue, 28 Jan 2014 20:19:19 +0000 Subject: [PATCH 1/5] Attempt to bring trunk fixes and branch updates together - Legacy-Id: 7176 --- ietf/meeting/ajax.py | 303 +++++++++++++++++++++++++++++++------------ 1 file changed, 221 insertions(+), 82 deletions(-) diff --git a/ietf/meeting/ajax.py b/ietf/meeting/ajax.py index a5794a790..0922b5c3f 100644 --- a/ietf/meeting/ajax.py +++ b/ietf/meeting/ajax.py @@ -1,4 +1,5 @@ import json +import datetime from django.core.urlresolvers import reverse from django.shortcuts import get_object_or_404, redirect @@ -9,10 +10,11 @@ from ietf.ietfauth.utils import role_required, has_role, user_is_person from ietf.name.models import TimeSlotTypeName from ietf.meeting.helpers import get_meeting, get_schedule, get_schedule_by_id, agenda_permissions +from ietf.meeting.models import ScheduledSession from ietf.meeting.views import edit_timeslots, edit_agenda from ietf.meeting.models import TimeSlot, Session, Schedule, Room, Constraint -import debug +#import debug def dajaxice_core_js(request): # this is a slightly weird hack to get, we seem to need this because @@ -64,83 +66,48 @@ def update_timeslot_pinned(request, schedule_id, scheduledsession_id, pinned=Fal if scheduledsession_id is not None: ss_id = int(scheduledsession_id) - if ss_id != 0: - ss = get_object_or_404(schedule.scheduledsession_set, pk=ss_id) + if ss_id == 0: + return json.dumps({'error':'no permission'}) + ss = get_object_or_404(schedule.scheduledsession_set, pk=ss_id) ss.pinned = pinned ss.save() return json.dumps({'message':'valid'}) - - -@role_required('Area Director','Secretariat') -@dajaxice_register -def update_timeslot(request, schedule_id, session_id, scheduledsession_id=None, extended_from_id=None, duplicate=False): - schedule = get_object_or_404(Schedule, pk = int(schedule_id)) - meeting = schedule.meeting - ss_id = 0 - ess_id = 0 - ess = None - ss = None - - #print "duplicate: %s schedule.owner: %s user: %s" % (duplicate, schedule.owner, request.user.person) - cansee,canedit = agenda_permissions(meeting, schedule, request.user) - - if not canedit: - #raise Exception("Not permitted") - return json.dumps({'error':'no permission'}) - - session_id = int(session_id) - session = get_object_or_404(meeting.session_set, pk=session_id) - - if scheduledsession_id is not None: - ss_id = int(scheduledsession_id) - - if extended_from_id is not None: - ess_id = int(extended_from_id) - - if ss_id != 0: - ss = get_object_or_404(schedule.scheduledsession_set, pk=ss_id) - - # this cleans up up two sessions in one slot situation, the - # ... extra scheduledsessions need to be cleaned up. - - if ess_id == 0: - # if this is None, then we must be moving. - for ssO in schedule.scheduledsession_set.filter(session=session): - #print "sched(%s): removing session %s from slot %u" % ( schedule, session, ssO.pk ) - #if ssO.extendedfrom is not None: - # ssO.extendedfrom.session = None - # ssO.extendedfrom.save() - ssO.session = None - ssO.extendedfrom = None - ssO.save() - else: - ess = get_object_or_404(schedule.scheduledsession_set, pk = ess_id) - ss.extendedfrom = ess - - try: - # find the scheduledsession, assign the Session to it. - if ss: - #print "ss.session: %s session:%s duplicate=%s"%(ss, session, duplicate) - ss.session = session - if duplicate: - ss.id = None - ss.save() - except Exception: - return json.dumps({'error':'invalid scheduledsession'}) - - return json.dumps({'message':'valid'}) - @role_required('Secretariat') @dajaxice_register -def update_timeslot_purpose(request, timeslot_id=None, purpose=None): +def update_timeslot_purpose(request, + meeting_num, + timeslot_id=None, + purpose =None, + room_id = None, + duration= None, + time = None): + + meeting = get_meeting(meeting_num) ts_id = int(timeslot_id) - try: - timeslot = TimeSlot.objects.get(pk=ts_id) - except: - return json.dumps({'error':'invalid timeslot'}) + time_str = time + if ts_id == 0: + try: + time = datetime.datetime.strptime(time_str, '%Y-%m-%d %H:%M:%S') + except: + return json.dumps({'error':'invalid time: %s' % (time_str)}) + + try: + room = meeting.room_set.get(pk = int(room_id)) + except Room.DoesNotExist: + return json.dumps({'error':'invalid room id'}) + + timeslot = TimeSlot(meeting=meeting, + location = room, + time = time, + duration = duration) + else: + try: + timeslot = TimeSlot.objects.get(pk=ts_id) + except: + return json.dumps({'error':'invalid timeslot'}) try: timeslottypename = TimeSlotTypeName.objects.get(pk = purpose) @@ -149,9 +116,17 @@ def update_timeslot_purpose(request, timeslot_id=None, purpose=None): 'extra': purpose}) timeslot.type = timeslottypename - timeslot.save() + try: + timeslot.save() + except: + return json.dumps({'error':'failed to save'}) - return json.dumps(timeslot.json_dict(request.build_absolute_uri('/'))) + try: + # really should return 201 created, but dajaxice sucks. + json_dict = timeslot.json_dict(request.build_absolute_uri('/')) + return json.dumps(json_dict) + except: + return json.dumps({'error':'failed to save'}) ############################################################################# ## ROOM API @@ -192,6 +167,25 @@ def timeslot_delroom(request, meeting, roomid): room.delete() return HttpResponse('{"error":"none"}', status = 200) +@role_required('Secretariat') +def timeslot_updroom(request, meeting, roomid): + room = get_object_or_404(meeting.room_set, pk=roomid) + + if "name" in request.POST: + room.name = request.POST["name"] + + if "capacity" in request.POST: + room.capacity = request.POST["capacity"] + + if "resources" in request.POST: + new_resource_ids = request.POST["resources"] + new_resources = [ ResourceAssociation.objects.get(pk=a) + for a in new_resource_ids] + room.resources = new_resources + + room.save() + return HttpResponse('{"error":"none"}', status = 200) + def timeslot_roomsurl(request, num=None): meeting = get_meeting(num) @@ -210,14 +204,14 @@ def timeslot_roomurl(request, num=None, roomid=None): room = get_object_or_404(meeting.room_set, pk=roomid) return HttpResponse(json.dumps(room.json_dict(request.build_absolute_uri('/'))), content_type="application/json") -# XXX FIXME: timeslot_updroom() doesn't exist -# elif request.method == 'POST': -# return timeslot_updroom(request, meeting) + elif request.method == 'PUT': + return timeslot_updroom(request, meeting, roomid) elif request.method == 'DELETE': return timeslot_delroom(request, meeting, roomid) ############################################################################# ## DAY/SLOT API +## -- this creates groups of timeslots, and associated scheduledsessions. ############################################################################# AddSlotForm = modelform_factory(TimeSlot, exclude=('meeting','name','location','sessions', 'modified')) @@ -227,7 +221,7 @@ def timeslot_slotlist(request, mtg): json_array=[] for slot in slots: json_array.append(slot.json_dict(request.build_absolute_uri('/'))) - return HttpResponse(json.dumps(json_array), + return HttpResponse(json.dumps(json_array, sort_keys=True, indent=2), content_type="application/json") @role_required('Secretariat') @@ -326,6 +320,10 @@ def agenda_update(request, meeting, schedule): # (schedule, update_dict, request.body)) user = request.user + + cansee,canedit = agenda_permissions(meeting, schedule, request.user) + read_only = not canedit + if has_role(user, "Secretariat"): if "public" in request.POST: value1 = True @@ -335,13 +333,20 @@ def agenda_update(request, meeting, schedule): #debug.log("setting public for %s to %s" % (schedule, value1)) schedule.public = value1 - if "visible" in request.POST: + if "visible" in request.POST and cansee: value1 = True value = request.POST["visible"] if value == "0" or value == 0 or value=="false": value1 = False #debug.log("setting visible for %s to %s" % (schedule, value1)) schedule.visible = value1 + if has_role(user, "Secretariat") and canedit: + if "name" in request.POST: + value = request.POST["name"] + #log.debug("setting name for %s to %s" % (schedule, value)) + schedule.name = value + else: + return HttpResponse({'error':'no permission'}, status=401) if "name" in request.POST: value = request.POST["name"] @@ -382,11 +387,11 @@ def agenda_infosurl(request, num=None): # unacceptable action return HttpResponse(status=406) -def agenda_infourl(request, num=None, schedule_name=None): +def agenda_infourl(request, num=None, name=None): meeting = get_meeting(num) - #debug.log("agenda: %s / %s" % (meeting, schedule_name)) + #log.debug("agenda: %s / %s" % (meeting, name)) - schedule = get_schedule(meeting, schedule_name) + schedule = get_schedule(meeting, name) #debug.log("results in agenda: %u / %s" % (schedule.id, request.method)) if request.method == 'GET': @@ -417,13 +422,13 @@ def meeting_update(request, meeting): value = request.POST["agenda"] #debug.log("4 meeting.agenda: %s" % (value)) if not value or value == "None": # value == "None" is just weird, better with empty string - meeting.agenda = None + meeting.set_official_agenda(None) else: schedule = get_schedule(meeting, value) if not schedule.public: return HttpResponse(status = 406) #debug.log("3 meeting.agenda: %s" % (schedule)) - meeting.agenda = schedule + meeting.set_official_agenda(schedule) #debug.log("2 meeting.agenda: %s" % (meeting.agenda)) meeting.save() @@ -441,7 +446,7 @@ def meeting_json(request, num): ############################################################################# -## Agenda Editing API functions +## Session details API functions ############################################################################# def session_json(request, num, sessionid): @@ -459,6 +464,140 @@ def session_json(request, num, sessionid): return HttpResponse(json.dumps(sess1, sort_keys=True, indent=2), content_type="application/json") +# get group of all sessions. +def sessions_json(request, num): + meeting = get_meeting(num) + + sessions = meeting.sessions_that_can_meet.all() + + sess1_dict = [ x.json_dict(request.build_absolute_uri('/')) for x in sessions ] + return HttpResponse(json.dumps(sess1_dict, sort_keys=True, indent=2), + content_type="application/json") + +############################################################################# +## Scheduledsesion +############################################################################# + +def scheduledsessions_post(request, meeting, schedule): + cansee,canedit = agenda_permissions(meeting, schedule, request.user) + if not canedit: + return HttpResponse(json.dumps({'error':'no permission to modify this agenda'}), + status = 403, + content_type="application/json") + + # get JSON out of raw body. XXX should check Content-Type! + newvalues = json.loads(request.raw_post_data) + if not ("session_id" in newvalues) or not ("timeslot_id" in newvalues): + return HttpResponse(json.dumps({'error':'missing values, timeslot_id and session_id required'}), + status = 406, + content_type="application/json") + + ss1 = ScheduledSession(schedule = schedule, + session_id = newvalues["session_id"], + timeslot_id = newvalues["timeslot_id"]) + if("extendedfrom_id" in newvalues): + val = int(newvalues["extendedfrom_id"]) + try: + ss2 = schedule.scheduledsessions_set.get(pk = val) + ss1.extendedfrom = ss2 + except ScheduledSession.DoesNotExist: + return HttpResponse(json.dumps({'error':'invalid extendedfrom value: %u' % val}), + status = 406, + content_type="application/json") + + ss1.save() + ss1_dict = ss1.json_dict(request.build_absolute_uri('/')) + response = HttpResponse(json.dumps(ss1_dict), + status = 201, + content_type="application/json") + # 201 code needs a Location: header. + response['Location'] = ss1_dict["href"], + return response + +def scheduledsessions_get(request, num, schedule): + scheduledsessions = schedule.scheduledsession_set.all() + + sess1_dict = [ x.json_dict(request.build_absolute_uri('/')) for x in scheduledsessions ] + return HttpResponse(json.dumps(sess1_dict, sort_keys=True, indent=2), + content_type="application/json") + +# this returns the list of scheduled sessions for the given named agenda +def scheduledsessions_json(request, num, name): + meeting = get_meeting(num) + schedule = get_schedule(meeting, name) + + if request.method == 'GET': + return scheduledsessions_get(request, meeting, schedule) + elif request.method == 'POST': + return scheduledsessions_post(request, meeting, schedule) + else: + return HttpResponse(json.dumps({'error':'inappropriate action: %s' % (request.method)}), + status = 406, + content_type="application/json") + + +def scheduledsession_update(request, meeting, schedule, scheduledsession_id): + cansee,canedit = agenda_permissions(meeting, schedule, request.user) + if not canedit or True: + return HttpResponse(json.dumps({'error':'no permission to update this agenda'}), + status = 403, + content_type="application/json") + + +def scheduledsession_delete(request, meeting, schedule, scheduledsession_id): + cansee,canedit = agenda_permissions(meeting, schedule, request.user) + if not canedit: + return HttpResponse(json.dumps({'error':'no permission to update this agenda'}), + status = 403, + content_type="application/json") + + scheduledsessions = schedule.scheduledsession_set.filter(pk = scheduledsession_id) + if len(scheduledsessions) == 0: + return HttpResponse(json.dumps({'error':'no such object'}), + status = 404, + content_type="application/json") + + count=0 + for ss in scheduledsessions: + ss.delete() + count =+ 1 + + return HttpResponse(json.dumps({'result':"%u objects deleted"%(count)}), + status = 200, + content_type="application/json") + +def scheduledsession_get(request, meeting, schedule, scheduledsession_id): + cansee,canedit = agenda_permissions(meeting, schedule, request.user) + + if not cansee: + return HttpResponse(json.dumps({'error':'no permission to see this agenda'}), + status = 403, + content_type="application/json") + + scheduledsessions = schedule.scheduledsession_set.filter(pk = scheduledsession_id) + if len(scheduledsessions) == 0: + return HttpResponse(json.dumps({'error':'no such object'}), + status = 404, + content_type="application/json") + + sess1_dict = scheduledsessions[0].json_dict(request.build_absolute_uri('/')) + return HttpResponse(json.dumps(sess1_dict, sort_keys=True, indent=2), + content_type="application/json") + +# this returns the list of scheduled sessions for the given named agenda +def scheduledsession_json(request, num, name, scheduledsession_id): + meeting = get_meeting(num) + schedule = get_schedule(meeting, name) + + scheduledsession_id = int(scheduledsession_id) + + if request.method == 'GET': + return scheduledsession_get(request, meeting, schedule, scheduledsession_id) + elif request.method == 'PUT': + return scheduledsession_update(request, meeting, schedule, scheduledsession_id) + elif request.method == 'DELETE': + return scheduledsession_delete(request, meeting, schedule, scheduledsession_id) + # Would like to cache for 1 day, but there are invalidation issues. #@cache_page(86400) def constraint_json(request, num, constraintid): From 5cdfe6adbfb5a1eb294de4af09e2c3dc30088889 Mon Sep 17 00:00:00 2001 From: Robert Sparks <rjsparks@nostrum.com> Date: Fri, 31 Jan 2014 01:13:53 +0000 Subject: [PATCH 2/5] reasonably complete merge, but some of the newer-style tests are failing because the js API changed - Legacy-Id: 7188 --- ietf/group/models.py | 3 + ietf/meeting/ajax.py | 2 +- ietf/meeting/management/__init__.py | 0 ietf/meeting/management/commands/__init__.py | 0 ietf/meeting/management/commands/autoplace.py | 124 + ietf/meeting/migrations/0014_add_room.py | 350 +++ .../migrations/0015_add_basic_resources.py | 339 +++ .../0016_add_resource_to_session.py | 341 +++ ..._remove_empty_session_scheduledsessions.py | 335 +++ ietf/meeting/models.py | 256 +- ietf/meeting/placement.py | 738 ++++++ ietf/meeting/tests_api.py | 6 +- ietf/meeting/tests_views.py | 2 +- ietf/meeting/urls.py | 10 +- ietf/meeting/views.py | 69 +- .../migrations/0020_add_default_penalties.py | 205 ++ ietf/name/migrations/0021_add_room.py | 200 ++ .../migrations/0022_add_room_resources.py | 201 ++ ietf/name/models.py | 4 +- ietf/secr/meetings/views.py | 7 +- ietf/secr/sreq/forms.py | 25 +- ietf/secr/sreq/urls.py | 2 + ietf/secr/sreq/views.py | 125 +- .../includes/sessions_request_form.html | 34 +- .../includes/sessions_request_view.html | 6 +- ietf/secr/templates/sreq/main.html | 2 +- ietf/secr/templates/sreq/view.html | 4 +- ietf/settings.py | 24 + ietf/templates/meeting/agenda-utc.html | 6 +- ietf/templates/meeting/landscape_edit.html | 349 +-- ietf/templates/meeting/requests.html | 3 +- ietf/templates/meeting/room_edit.html | 60 + ietf/templates/meeting/timeslot_edit.html | 63 +- static/css/agenda.css | 93 +- static/css/jquery-ui-timepicker-addon.css | 11 + static/css/lib/qunit-1.12.0.css | 244 ++ ...84425806666249pin_blue_nicu_buculei_01.svg | 70 + .../12161797831849255690jcartier_board.svg | 158 ++ ...97831849255690jcartier_board.svg.thumb.png | Bin 0 -> 4072 bytes static/images/meetecho-logo.png | Bin 0 -> 3005 bytes static/images/meetecho-mini.png | Bin 0 -> 1567 bytes static/images/meetecho-mini.xcf | Bin 0 -> 3415 bytes static/images/projector2.png | Bin 0 -> 2421 bytes static/images/projector2.svg | 175 ++ static/js/agenda/agenda_edit.js | 56 +- static/js/agenda/agenda_helpers.js | 295 +-- static/js/agenda/agenda_listeners.js | 725 +++--- static/js/agenda/agenda_objects.js | 577 ++++- static/js/agenda/timeslot_edit.js | 216 +- .../jquery-ui-sliderAccess.js | 91 + .../jquery-ui-timepicker-addon.js | 2134 ++++++++++++++++ static/js/lib/qunit-1.12.0.js | 2212 +++++++++++++++++ static/js/moment.min.js | 6 + static/js/test/agenda_funcs.js | 316 +++ static/js/test/agenda_tests.js | 370 +++ static/secretariat/css/custom.css | 5 + static/test/agenda_tests.html | 27 + static/test/agenda_ui.html | 603 +++++ 58 files changed, 11267 insertions(+), 1012 deletions(-) create mode 100644 ietf/meeting/management/__init__.py create mode 100644 ietf/meeting/management/commands/__init__.py create mode 100644 ietf/meeting/management/commands/autoplace.py create mode 100644 ietf/meeting/migrations/0014_add_room.py create mode 100644 ietf/meeting/migrations/0015_add_basic_resources.py create mode 100644 ietf/meeting/migrations/0016_add_resource_to_session.py create mode 100644 ietf/meeting/migrations/0017_remove_empty_session_scheduledsessions.py create mode 100644 ietf/meeting/placement.py create mode 100644 ietf/name/migrations/0020_add_default_penalties.py create mode 100644 ietf/name/migrations/0021_add_room.py create mode 100644 ietf/name/migrations/0022_add_room_resources.py create mode 100644 ietf/templates/meeting/room_edit.html create mode 100644 static/css/jquery-ui-timepicker-addon.css create mode 100644 static/css/lib/qunit-1.12.0.css create mode 100644 static/images/1194984425806666249pin_blue_nicu_buculei_01.svg create mode 100644 static/images/12161797831849255690jcartier_board.svg create mode 100644 static/images/12161797831849255690jcartier_board.svg.thumb.png create mode 100644 static/images/meetecho-logo.png create mode 100644 static/images/meetecho-mini.png create mode 100644 static/images/meetecho-mini.xcf create mode 100644 static/images/projector2.png create mode 100644 static/images/projector2.svg create mode 100644 static/js/jquery-ui-timepicker/jquery-ui-sliderAccess.js create mode 100644 static/js/jquery-ui-timepicker/jquery-ui-timepicker-addon.js create mode 100644 static/js/lib/qunit-1.12.0.js create mode 100644 static/js/moment.min.js create mode 100644 static/js/test/agenda_funcs.js create mode 100644 static/js/test/agenda_tests.js create mode 100644 static/test/agenda_tests.html create mode 100644 static/test/agenda_ui.html diff --git a/ietf/group/models.py b/ietf/group/models.py index 79e136512..2f5386c16 100644 --- a/ietf/group/models.py +++ b/ietf/group/models.py @@ -63,6 +63,9 @@ class Group(GroupInfo): role_names = [role_names] return user.is_authenticated() and self.role_set.filter(name__in=role_names, person__user=user).exists() + def is_bof(self): + return (self.state.slug in ["bof", "bof-conc"]) + def get_chair(self): chair = self.role_set.filter(name__slug='chair')[:1] return chair and chair[0] or None diff --git a/ietf/meeting/ajax.py b/ietf/meeting/ajax.py index 0922b5c3f..de84aa811 100644 --- a/ietf/meeting/ajax.py +++ b/ietf/meeting/ajax.py @@ -560,7 +560,7 @@ def scheduledsession_delete(request, meeting, schedule, scheduledsession_id): count=0 for ss in scheduledsessions: ss.delete() - count =+ 1 + count += 1 return HttpResponse(json.dumps({'result':"%u objects deleted"%(count)}), status = 200, diff --git a/ietf/meeting/management/__init__.py b/ietf/meeting/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ietf/meeting/management/commands/__init__.py b/ietf/meeting/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ietf/meeting/management/commands/autoplace.py b/ietf/meeting/management/commands/autoplace.py new file mode 100644 index 000000000..8a96616b4 --- /dev/null +++ b/ietf/meeting/management/commands/autoplace.py @@ -0,0 +1,124 @@ +""" +Runs the automatic placement code (simulated annealing of glass) +for a given meeting number, using a schedule given by the schedule database ID. + +for help on this file: +https://docs.djangoproject.com/en/dev/howto/custom-management-commands/ + +""" + +from django.core.management.base import BaseCommand, CommandError +from optparse import make_option +#import cProfile, pstats, io +import os +import sys +import gzip +import time +import csv +import codecs +from ietf.meeting.models import Schedule, Meeting + +class Command(BaseCommand): + args = '<meeting> <schedulename>' + help = 'perform automatic placement' + stderr = sys.stderr + stdout = sys.stdout + + verbose = False + profile = False + permit_movement = False + maxstep = 20000 + seed = None + recordsteps = False + + option_list = BaseCommand.option_list + ( + make_option('--profile', + action='store_true', + dest='profile', + default=False, + help='Enable verbose mode'), + make_option('--recordsteps', + action='store_true', + dest='recordsteps', + default=False, + help='Enable recording progress to table'), + make_option('--verbose', + action='count', + dest='verbose', + default=False, + help='Enable verbose mode'), + make_option('--maxstep', + action="store", type="int", + dest='maxstep', + default=20000, + help='Maximum number of steps'), + make_option('--seed', + action="store", type="int", + dest='seed', + default=None, + help='Seed to use for calculation'), + ) + + def handle(self, *labels, **options): + self.verbose = options.get('verbose', 1) + + meetingname = labels[0] + schedname = labels[1] + targetname = None + if labels[2] is not None: + targetname = labels[2] + + seed = options.get('seed', None) + maxstep = options.get('maxstep', 20000) + verbose = options.get('verbose', False) + profile = options.get('profile', False) + recordsteps = options.get('recordsteps', False) + + from ietf.meeting.helpers import get_meeting,get_schedule + try: + meeting = get_meeting(meetingname) + except Meeting.DoesNotExist: + print "No such meeting: %s" % (meetingname) + return + + try: + schedule = meeting.schedule_set.get(name = schedname) + except Schedule.DoesNotExist: + print "No such schedule: %s in meeting: %s" % (schedname, meeting) + return + + if targetname is not None: + try: + targetsched = meeting.schedule_set.get(name=targetname) + except Schedule.DoesNotExist: + print "Creating new schedule %s" % (targetname) + targetsched = Schedule(meeting = meeting, + owner = schedule.owner, + name = targetname) + targetsched.save() + else: + targetsched = schedule + + print "Saving results to %s" % (targetsched.name) + + from ietf.meeting.placement import CurrentScheduleState + css = CurrentScheduleState(schedule, seed) + css.recordsteps = recordsteps + css.verbose = verbose + + if profile: + import cProfile + import re + cProfile.runctx('css.do_placement(maxstep, targetsched)', + vars(), + vars(), + 'placestats.pyprof') + + import pstats + p = pstats.Stats('placestats.pyprof') + p.strip_dirs().sort_stats(-1).print_stats() + else: + css.do_placement(maxstep, targetsched) + + + diff --git a/ietf/meeting/migrations/0014_add_room.py b/ietf/meeting/migrations/0014_add_room.py new file mode 100644 index 000000000..eaa54b0b8 --- /dev/null +++ b/ietf/meeting/migrations/0014_add_room.py @@ -0,0 +1,350 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'ResourceAssociation' + db.create_table('meeting_resourceassociation', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['name.RoomResourceName'])), + ('icon', self.gf('django.db.models.fields.CharField')(max_length=64)), + ('desc', self.gf('django.db.models.fields.CharField')(max_length=256)), + )) + db.send_create_signal('meeting', ['ResourceAssociation']) + + # Adding M2M table for field resources on 'Room' + m2m_table_name = db.shorten_name('meeting_room_resources') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('room', models.ForeignKey(orm['meeting.room'], null=False)), + ('resourceassociation', models.ForeignKey(orm['meeting.resourceassociation'], null=False)) + )) + db.create_unique(m2m_table_name, ['room_id', 'resourceassociation_id']) + + def backwards(self, orm): + # Deleting model 'ResourceAssociation' + db.delete_table('meeting_resourceassociation') + + # Removing M2M table for field resources on 'Room' + db.delete_table(db.shorten_name('meeting_room_resources')) + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'doc.document': { + 'Meta': {'object_name': 'Document'}, + 'abstract': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'ad_document_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['person.Email']", 'symmetrical': 'False', 'through': "orm['doc.DocumentAuthor']", 'blank': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'external_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'intended_std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.IntendedStdLevelName']", 'null': 'True', 'blank': 'True'}), + 'internal_comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}), + 'note': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'notify': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1', 'blank': 'True'}), + 'pages': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'rev': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'shepherd': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shepherd_document_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'states': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StdLevelName']", 'null': 'True', 'blank': 'True'}), + 'stream': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StreamName']", 'null': 'True', 'blank': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['name.DocTagName']", 'null': 'True', 'blank': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'}) + }, + 'doc.documentauthor': { + 'Meta': {'ordering': "['document', 'order']", 'object_name': 'DocumentAuthor'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1'}) + }, + 'doc.state': { + 'Meta': {'ordering': "['type', 'order']", 'object_name': 'State'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'previous_states'", 'blank': 'True', 'to': "orm['doc.State']"}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.StateType']"}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'doc.statetype': { + 'Meta': {'object_name': 'StateType'}, + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}) + }, + 'group.group': { + 'Meta': {'object_name': 'Group'}, + 'acronym': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '40'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'charter': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'chartered_group'", 'unique': 'True', 'null': 'True', 'to': "orm['doc.Document']"}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_archive': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'list_email': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'list_subscribe': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupStateName']", 'null': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupTypeName']", 'null': 'True'}), + 'unused_states': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'unused_tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['name.DocTagName']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'meeting.constraint': { + 'Meta': {'object_name': 'Constraint'}, + 'day': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Meeting']"}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.ConstraintName']"}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'constraint_source_set'", 'to': "orm['group.Group']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'constraint_target_set'", 'null': 'True', 'to': "orm['group.Group']"}) + }, + 'meeting.meeting': { + 'Meta': {'ordering': "['-date']", 'object_name': 'Meeting'}, + 'agenda': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['meeting.Schedule']"}), + 'agenda_note': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'break_area': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '2', 'blank': 'True'}), + 'date': ('django.db.models.fields.DateField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'number': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}), + 'reg_area': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'time_zone': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.MeetingTypeName']"}), + 'venue_addr': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'venue_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}) + }, + 'meeting.resourceassociation': { + 'Meta': {'object_name': 'ResourceAssociation'}, + 'desc': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'icon': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.RoomResourceName']"}) + }, + 'meeting.room': { + 'Meta': {'object_name': 'Room'}, + 'capacity': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Meeting']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'resources': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['meeting.ResourceAssociation']", 'symmetrical': 'False'}) + }, + 'meeting.schedule': { + 'Meta': {'object_name': 'Schedule'}, + 'badness': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Meeting']", 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'meeting.scheduledsession': { + 'Meta': {'ordering': "['timeslot__time', 'session__group__parent__name', 'session__group__acronym', 'session__name']", 'object_name': 'ScheduledSession'}, + 'badness': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), + 'extendedfrom': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['meeting.ScheduledSession']", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'notes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'pinned': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'schedule': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'assignments'", 'to': "orm['meeting.Schedule']"}), + 'session': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['meeting.Session']", 'null': 'True'}), + 'timeslot': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.TimeSlot']"}) + }, + 'meeting.session': { + 'Meta': {'object_name': 'Session'}, + 'agenda_note': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'attendees': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'materials': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.Document']", 'symmetrical': 'False', 'blank': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Meeting']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'requested': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'requested_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'requested_duration': ('ietf.meeting.timedeltafield.TimedeltaField', [], {'default': '0'}), + 'scheduled': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'short': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'status': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.SessionStatusName']"}) + }, + 'meeting.timeslot': { + 'Meta': {'object_name': 'TimeSlot'}, + 'duration': ('ietf.meeting.timedeltafield.TimedeltaField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Room']", 'null': 'True', 'blank': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Meeting']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'sessions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'slots'", 'to': "orm['meeting.Session']", 'through': "orm['meeting.ScheduledSession']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True'}), + 'show_location': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.TimeSlotTypeName']"}) + }, + 'name.constraintname': { + 'Meta': {'ordering': "['order']", 'object_name': 'ConstraintName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'penalty': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctagname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTagName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.groupstatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupStateName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.grouptypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.intendedstdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'IntendedStdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.meetingtypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'MeetingTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.roomresourcename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoomResourceName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.sessionstatusname': { + 'Meta': {'ordering': "['order']", 'object_name': 'SessionStatusName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.stdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.streamname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StreamName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.timeslottypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'TimeSlotTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'person.email': { + 'Meta': {'object_name': 'Email'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'person.person': { + 'Meta': {'object_name': 'Person'}, + 'address': ('django.db.models.fields.TextField', [], {'max_length': '255', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'ascii': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ascii_short': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['meeting'] diff --git a/ietf/meeting/migrations/0015_add_basic_resources.py b/ietf/meeting/migrations/0015_add_basic_resources.py new file mode 100644 index 000000000..a1c87e780 --- /dev/null +++ b/ietf/meeting/migrations/0015_add_basic_resources.py @@ -0,0 +1,339 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + orm.ResourceAssociation(name_id = "project", + icon = "12161797831849255690jcartier_board.svg", + desc = "Projector in room").save() + orm.ResourceAssociation(name_id = "proj2", + icon = "projector2.svg", + desc = "Second projector in room").save() + orm.ResourceAssociation(name_id = "meetecho", + icon = "meetecho-mini.png", + desc = "Meetecho support in room").save() + + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'doc.document': { + 'Meta': {'object_name': 'Document'}, + 'abstract': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'ad_document_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['person.Email']", 'symmetrical': 'False', 'through': "orm['doc.DocumentAuthor']", 'blank': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'external_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'intended_std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.IntendedStdLevelName']", 'null': 'True', 'blank': 'True'}), + 'internal_comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}), + 'note': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'notify': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1', 'blank': 'True'}), + 'pages': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'rev': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'shepherd': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shepherd_document_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'states': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StdLevelName']", 'null': 'True', 'blank': 'True'}), + 'stream': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StreamName']", 'null': 'True', 'blank': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['name.DocTagName']", 'null': 'True', 'blank': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'}) + }, + 'doc.documentauthor': { + 'Meta': {'ordering': "['document', 'order']", 'object_name': 'DocumentAuthor'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1'}) + }, + 'doc.state': { + 'Meta': {'ordering': "['type', 'order']", 'object_name': 'State'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'previous_states'", 'blank': 'True', 'to': "orm['doc.State']"}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.StateType']"}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'doc.statetype': { + 'Meta': {'object_name': 'StateType'}, + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}) + }, + 'group.group': { + 'Meta': {'object_name': 'Group'}, + 'acronym': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '40'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'charter': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'chartered_group'", 'unique': 'True', 'null': 'True', 'to': "orm['doc.Document']"}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_archive': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'list_email': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'list_subscribe': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupStateName']", 'null': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupTypeName']", 'null': 'True'}), + 'unused_states': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'unused_tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['name.DocTagName']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'meeting.constraint': { + 'Meta': {'object_name': 'Constraint'}, + 'day': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Meeting']"}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.ConstraintName']"}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'constraint_source_set'", 'to': "orm['group.Group']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'constraint_target_set'", 'null': 'True', 'to': "orm['group.Group']"}) + }, + 'meeting.meeting': { + 'Meta': {'ordering': "['-date']", 'object_name': 'Meeting'}, + 'agenda': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['meeting.Schedule']"}), + 'agenda_note': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'break_area': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '2', 'blank': 'True'}), + 'date': ('django.db.models.fields.DateField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'number': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}), + 'reg_area': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'time_zone': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.MeetingTypeName']"}), + 'venue_addr': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'venue_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}) + }, + 'meeting.resourceassociation': { + 'Meta': {'object_name': 'ResourceAssociation'}, + 'desc': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'icon': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.RoomResourceName']"}) + }, + 'meeting.room': { + 'Meta': {'object_name': 'Room'}, + 'capacity': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Meeting']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'resources': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['meeting.ResourceAssociation']", 'symmetrical': 'False'}) + }, + 'meeting.schedule': { + 'Meta': {'object_name': 'Schedule'}, + 'badness': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Meeting']", 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'meeting.scheduledsession': { + 'Meta': {'ordering': "['timeslot__time', 'session__group__parent__name', 'session__group__acronym', 'session__name']", 'object_name': 'ScheduledSession'}, + 'badness': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), + 'extendedfrom': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['meeting.ScheduledSession']", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'notes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'pinned': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'schedule': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'assignments'", 'to': "orm['meeting.Schedule']"}), + 'session': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['meeting.Session']", 'null': 'True'}), + 'timeslot': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.TimeSlot']"}) + }, + 'meeting.session': { + 'Meta': {'object_name': 'Session'}, + 'agenda_note': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'attendees': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'materials': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.Document']", 'symmetrical': 'False', 'blank': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Meeting']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'requested': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'requested_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'requested_duration': ('ietf.meeting.timedeltafield.TimedeltaField', [], {'default': '0'}), + 'scheduled': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'short': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'status': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.SessionStatusName']"}) + }, + 'meeting.timeslot': { + 'Meta': {'object_name': 'TimeSlot'}, + 'duration': ('ietf.meeting.timedeltafield.TimedeltaField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Room']", 'null': 'True', 'blank': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Meeting']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'sessions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'slots'", 'to': "orm['meeting.Session']", 'through': "orm['meeting.ScheduledSession']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True'}), + 'show_location': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.TimeSlotTypeName']"}) + }, + 'name.constraintname': { + 'Meta': {'ordering': "['order']", 'object_name': 'ConstraintName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'penalty': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctagname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTagName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.groupstatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupStateName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.grouptypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.intendedstdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'IntendedStdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.meetingtypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'MeetingTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.roomresourcename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoomResourceName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.sessionstatusname': { + 'Meta': {'ordering': "['order']", 'object_name': 'SessionStatusName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.stdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.streamname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StreamName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.timeslottypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'TimeSlotTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'person.email': { + 'Meta': {'object_name': 'Email'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'person.person': { + 'Meta': {'object_name': 'Person'}, + 'address': ('django.db.models.fields.TextField', [], {'max_length': '255', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'ascii': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ascii_short': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['meeting'] + symmetrical = True diff --git a/ietf/meeting/migrations/0016_add_resource_to_session.py b/ietf/meeting/migrations/0016_add_resource_to_session.py new file mode 100644 index 000000000..c3a4a5eb3 --- /dev/null +++ b/ietf/meeting/migrations/0016_add_resource_to_session.py @@ -0,0 +1,341 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding M2M table for field resources on 'Session' + m2m_table_name = db.shorten_name('meeting_session_resources') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('session', models.ForeignKey(orm['meeting.session'], null=False)), + ('resourceassociation', models.ForeignKey(orm['meeting.resourceassociation'], null=False)) + )) + db.create_unique(m2m_table_name, ['session_id', 'resourceassociation_id']) + + + def backwards(self, orm): + # Removing M2M table for field resources on 'Session' + db.delete_table(db.shorten_name('meeting_session_resources')) + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'doc.document': { + 'Meta': {'object_name': 'Document'}, + 'abstract': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'ad_document_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['person.Email']", 'symmetrical': 'False', 'through': "orm['doc.DocumentAuthor']", 'blank': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'external_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'intended_std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.IntendedStdLevelName']", 'null': 'True', 'blank': 'True'}), + 'internal_comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}), + 'note': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'notify': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1', 'blank': 'True'}), + 'pages': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'rev': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'shepherd': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shepherd_document_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'states': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StdLevelName']", 'null': 'True', 'blank': 'True'}), + 'stream': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StreamName']", 'null': 'True', 'blank': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['name.DocTagName']", 'null': 'True', 'blank': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'}) + }, + 'doc.documentauthor': { + 'Meta': {'ordering': "['document', 'order']", 'object_name': 'DocumentAuthor'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1'}) + }, + 'doc.state': { + 'Meta': {'ordering': "['type', 'order']", 'object_name': 'State'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'previous_states'", 'blank': 'True', 'to': "orm['doc.State']"}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.StateType']"}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'doc.statetype': { + 'Meta': {'object_name': 'StateType'}, + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}) + }, + 'group.group': { + 'Meta': {'object_name': 'Group'}, + 'acronym': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '40'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'charter': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'chartered_group'", 'unique': 'True', 'null': 'True', 'to': "orm['doc.Document']"}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_archive': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'list_email': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'list_subscribe': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupStateName']", 'null': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupTypeName']", 'null': 'True'}), + 'unused_states': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'unused_tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['name.DocTagName']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'meeting.constraint': { + 'Meta': {'object_name': 'Constraint'}, + 'day': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Meeting']"}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.ConstraintName']"}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'constraint_source_set'", 'to': "orm['group.Group']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'constraint_target_set'", 'null': 'True', 'to': "orm['group.Group']"}) + }, + 'meeting.meeting': { + 'Meta': {'ordering': "['-date']", 'object_name': 'Meeting'}, + 'agenda': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['meeting.Schedule']"}), + 'agenda_note': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'break_area': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '2', 'blank': 'True'}), + 'date': ('django.db.models.fields.DateField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'number': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}), + 'reg_area': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'time_zone': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.MeetingTypeName']"}), + 'venue_addr': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'venue_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}) + }, + 'meeting.resourceassociation': { + 'Meta': {'object_name': 'ResourceAssociation'}, + 'desc': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'icon': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.RoomResourceName']"}) + }, + 'meeting.room': { + 'Meta': {'object_name': 'Room'}, + 'capacity': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Meeting']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'resources': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['meeting.ResourceAssociation']", 'symmetrical': 'False'}) + }, + 'meeting.schedule': { + 'Meta': {'object_name': 'Schedule'}, + 'badness': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Meeting']", 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'meeting.scheduledsession': { + 'Meta': {'ordering': "['timeslot__time', 'session__group__parent__name', 'session__group__acronym', 'session__name']", 'object_name': 'ScheduledSession'}, + 'badness': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), + 'extendedfrom': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['meeting.ScheduledSession']", 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'notes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'pinned': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'schedule': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'assignments'", 'to': "orm['meeting.Schedule']"}), + 'session': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['meeting.Session']", 'null': 'True'}), + 'timeslot': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.TimeSlot']"}) + }, + 'meeting.session': { + 'Meta': {'object_name': 'Session'}, + 'agenda_note': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'attendees': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'materials': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.Document']", 'symmetrical': 'False', 'blank': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Meeting']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'requested': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'requested_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'requested_duration': ('ietf.meeting.timedeltafield.TimedeltaField', [], {'default': '0'}), + 'resources': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['meeting.ResourceAssociation']", 'symmetrical': 'False'}), + 'scheduled': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'short': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'status': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.SessionStatusName']"}) + }, + 'meeting.timeslot': { + 'Meta': {'object_name': 'TimeSlot'}, + 'duration': ('ietf.meeting.timedeltafield.TimedeltaField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Room']", 'null': 'True', 'blank': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['meeting.Meeting']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'sessions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'slots'", 'to': "orm['meeting.Session']", 'through': "orm['meeting.ScheduledSession']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True'}), + 'show_location': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.TimeSlotTypeName']"}) + }, + 'name.constraintname': { + 'Meta': {'ordering': "['order']", 'object_name': 'ConstraintName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'penalty': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctagname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTagName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.groupstatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupStateName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.grouptypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.intendedstdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'IntendedStdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.meetingtypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'MeetingTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.roomresourcename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoomResourceName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.sessionstatusname': { + 'Meta': {'ordering': "['order']", 'object_name': 'SessionStatusName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.stdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.streamname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StreamName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.timeslottypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'TimeSlotTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'person.email': { + 'Meta': {'object_name': 'Email'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'person.person': { + 'Meta': {'object_name': 'Person'}, + 'address': ('django.db.models.fields.TextField', [], {'max_length': '255', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'ascii': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ascii_short': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['meeting'] \ No newline at end of file diff --git a/ietf/meeting/migrations/0017_remove_empty_session_scheduledsessions.py b/ietf/meeting/migrations/0017_remove_empty_session_scheduledsessions.py new file mode 100644 index 000000000..d6427cec7 --- /dev/null +++ b/ietf/meeting/migrations/0017_remove_empty_session_scheduledsessions.py @@ -0,0 +1,335 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + # Note: Don't use "from appname.models import ModelName". + # Use orm.ModelName to refer to models in this application, + # and orm['appname.ModelName'] for models in other applications. + orm.ScheduledSession.objects.exclude(session__isnull=False).delete() + + def backwards(self, orm): + "Write your backwards methods here." + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'doc.document': { + 'Meta': {'object_name': 'Document'}, + 'abstract': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'ad_document_set'", 'null': 'True', 'to': u"orm['person.Person']"}), + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['person.Email']", 'symmetrical': 'False', 'through': u"orm['doc.DocumentAuthor']", 'blank': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'external_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'intended_std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.IntendedStdLevelName']", 'null': 'True', 'blank': 'True'}), + 'internal_comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}), + 'note': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'notify': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1', 'blank': 'True'}), + 'pages': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'rev': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'shepherd': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shepherd_document_set'", 'null': 'True', 'to': u"orm['person.Person']"}), + 'states': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.StdLevelName']", 'null': 'True', 'blank': 'True'}), + 'stream': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.StreamName']", 'null': 'True', 'blank': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['name.DocTagName']", 'null': 'True', 'blank': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'}) + }, + u'doc.documentauthor': { + 'Meta': {'ordering': "['document', 'order']", 'object_name': 'DocumentAuthor'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Email']"}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.Document']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1'}) + }, + u'doc.state': { + 'Meta': {'ordering': "['type', 'order']", 'object_name': 'State'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'previous_states'", 'blank': 'True', 'to': u"orm['doc.State']"}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['doc.StateType']"}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'doc.statetype': { + 'Meta': {'object_name': 'StateType'}, + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}) + }, + u'group.group': { + 'Meta': {'object_name': 'Group'}, + 'acronym': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '40'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'charter': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'chartered_group'", 'unique': 'True', 'null': 'True', 'to': u"orm['doc.Document']"}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_archive': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'list_email': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'list_subscribe': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.GroupStateName']", 'null': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.GroupTypeName']", 'null': 'True'}), + 'unused_states': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'unused_tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['name.DocTagName']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'meeting.constraint': { + 'Meta': {'object_name': 'Constraint'}, + 'day': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['meeting.Meeting']"}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.ConstraintName']"}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'constraint_source_set'", 'to': u"orm['group.Group']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'constraint_target_set'", 'null': 'True', 'to': u"orm['group.Group']"}) + }, + u'meeting.meeting': { + 'Meta': {'ordering': "['-date']", 'object_name': 'Meeting'}, + 'agenda': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': u"orm['meeting.Schedule']"}), + 'agenda_note': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'break_area': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '2', 'blank': 'True'}), + 'date': ('django.db.models.fields.DateField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'number': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}), + 'reg_area': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'time_zone': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.MeetingTypeName']"}), + 'venue_addr': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'venue_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}) + }, + u'meeting.resourceassociation': { + 'Meta': {'object_name': 'ResourceAssociation'}, + 'desc': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'icon': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.RoomResourceName']"}) + }, + u'meeting.room': { + 'Meta': {'object_name': 'Room'}, + 'capacity': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['meeting.Meeting']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'resources': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['meeting.ResourceAssociation']", 'symmetrical': 'False'}) + }, + u'meeting.schedule': { + 'Meta': {'object_name': 'Schedule'}, + 'badness': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['meeting.Meeting']", 'null': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']"}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'meeting.scheduledsession': { + 'Meta': {'ordering': "['timeslot__time', 'session__group__parent__name', 'session__group__acronym', 'session__name']", 'object_name': 'ScheduledSession'}, + 'badness': ('django.db.models.fields.IntegerField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), + 'extendedfrom': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['meeting.ScheduledSession']", 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'notes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'pinned': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'schedule': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'assignments'", 'to': u"orm['meeting.Schedule']"}), + 'session': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['meeting.Session']", 'null': 'True'}), + 'timeslot': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['meeting.TimeSlot']"}) + }, + u'meeting.session': { + 'Meta': {'object_name': 'Session'}, + 'agenda_note': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'attendees': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['group.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'materials': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['doc.Document']", 'symmetrical': 'False', 'blank': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['meeting.Meeting']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'requested': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'requested_by': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']"}), + 'requested_duration': ('ietf.meeting.timedeltafield.TimedeltaField', [], {'default': '0'}), + 'resources': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['meeting.ResourceAssociation']", 'symmetrical': 'False'}), + 'scheduled': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'short': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'status': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.SessionStatusName']"}) + }, + u'meeting.timeslot': { + 'Meta': {'object_name': 'TimeSlot'}, + 'duration': ('ietf.meeting.timedeltafield.TimedeltaField', [], {}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['meeting.Room']", 'null': 'True', 'blank': 'True'}), + 'meeting': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['meeting.Meeting']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'sessions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'slots'", 'to': u"orm['meeting.Session']", 'through': u"orm['meeting.ScheduledSession']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True'}), + 'show_location': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['name.TimeSlotTypeName']"}) + }, + u'name.constraintname': { + 'Meta': {'ordering': "['order']", 'object_name': 'ConstraintName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'penalty': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'name.doctagname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTagName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'name.doctypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'name.groupstatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupStateName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'name.grouptypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'name.intendedstdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'IntendedStdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'name.meetingtypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'MeetingTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'name.roomresourcename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoomResourceName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'name.sessionstatusname': { + 'Meta': {'ordering': "['order']", 'object_name': 'SessionStatusName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'name.stdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'name.streamname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StreamName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'name.timeslottypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'TimeSlotTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + u'person.email': { + 'Meta': {'object_name': 'Email'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['person.Person']", 'null': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + u'person.person': { + 'Meta': {'object_name': 'Person'}, + 'address': ('django.db.models.fields.TextField', [], {'max_length': '255', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'ascii': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ascii_short': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['meeting'] + symmetrical = True diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 363d3ae32..8c0364cca 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -4,6 +4,7 @@ import pytz, datetime from urlparse import urljoin import copy import os +import sys import re import debug @@ -18,7 +19,7 @@ from django.template.defaultfilters import slugify, date as date_format, time as from ietf.group.models import Group from ietf.person.models import Person from ietf.doc.models import Document -from ietf.name.models import MeetingTypeName, TimeSlotTypeName, SessionStatusName, ConstraintName +from ietf.name.models import MeetingTypeName, TimeSlotTypeName, SessionStatusName, ConstraintName, RoomResourceName countries = pytz.country_names.items() countries.sort(lambda x,y: cmp(x[1], y[1])) @@ -111,6 +112,15 @@ class Meeting(models.Model): def sessions_that_can_meet(self): return self.session_set.exclude(status__slug='notmeet').exclude(status__slug='disappr').exclude(status__slug='deleted').exclude(status__slug='apprw') + def sessions_that_can_be_placed(self): + from django.db.models import Q + donotplace_groups = Q(group__acronym="edu") + donotplace_groups |= Q(group__acronym="tools") + donotplace_groups |= Q(group__acronym="iesg") + donotplace_groups |= Q(group__acronym="ietf") + donotplace_groups |= Q(group__acronym="iepg") + donotplace_groups |= Q(group__acronym="iab") + return self.sessions_that_can_meet.exclude(donotplace_groups) def json_url(self): return "/meeting/%s.json" % (self.number, ) @@ -157,8 +167,8 @@ class Meeting(models.Model): if ymd in time_slices: # only keep unique entries - if [ts.time, ts.time + ts.duration] not in time_slices[ymd]: - time_slices[ymd].append([ts.time, ts.time + ts.duration]) + if [ts.time, ts.time + ts.duration, ts.duration.seconds] not in time_slices[ymd]: + time_slices[ymd].append([ts.time, ts.time + ts.duration, ts.duration.seconds]) slots[ymd].append(ts) days.sort() @@ -195,13 +205,38 @@ class Meeting(models.Model): pass return '' + def set_official_agenda(self, agenda): + if self.agenda != agenda: + self.agenda = agenda + if self.agenda is not None: + self.agenda.sendEmail() + self.save() + class Meta: ordering = ["-date", ] +class ResourceAssociation(models.Model): + name = models.ForeignKey(RoomResourceName) + #url = models.UrlField() # not sure what this was for. + icon = models.CharField(max_length=64) # icon to be found in /static/img + desc = models.CharField(max_length=256) + + def __unicode__(self): + return self.desc + + def json_dict(self, host_scheme): + res1 = dict() + res1['name'] = self.name.slug + res1['icon'] = "/images/%s" % (self.icon) + res1['desc'] = self.desc + res1['resource_id'] = self.pk + return res1 + class Room(models.Model): meeting = models.ForeignKey(Meeting) name = models.CharField(max_length=255) capacity = models.IntegerField(null=True, blank=True) + resources = models.ManyToManyField(ResourceAssociation) def __unicode__(self): return "%s size: %u" % (self.name, self.capacity) @@ -223,6 +258,9 @@ class Room(models.Model): duration=ts.duration) self.meeting.create_all_timeslots() + def domid(self): + return "room%u" % (self.pk) + def json_url(self): return "/meeting/%s/room/%s.json" % (self.meeting.number, self.id) @@ -325,16 +363,25 @@ class TimeSlot(models.Model): # {{s.timeslot.time|date:'Y-m-d'}}_{{ s.timeslot.time|date:'Hi' }}" # also must match: # {{r|slugify}}_{{day}}_{{slot.0|date:'Hi'}} - return "%s_%s_%s" % (slugify(self.get_location()), self.time.strftime('%Y-%m-%d'), self.time.strftime('%H%M')) + domid="ts%u" % (self.pk) + if self.location is not None: + domid = self.location.domid() + return "%s_%s_%s" % (domid, self.time.strftime('%Y-%m-%d'), self.time.strftime('%H%M')) - def json_dict(self, selfurl): + def json_dict(self, host_scheme): ts = dict() ts['timeslot_id'] = self.id - ts['room'] = slugify(self.location) + ts['href'] = urljoin(host_scheme, self.json_url()) + ts['room'] = self.get_location() ts['roomtype'] = self.type.slug + if self.location is not None: + ts['capacity'] = self.location.capacity ts["time"] = date_format(self.time, 'Hi') - ts["date"] = time_format(self.time, 'Y-m-d') + ts["date"] = fmt_date(self.time) ts["domid"] = self.js_identifier + following = self.slot_to_the_right + if following is not None: + ts["following_timeslot_id"] = following.id return ts def json_url(self): @@ -418,11 +465,18 @@ class Schedule(models.Model): # def url_edit(self): # return "/meeting/%s/agenda/%s/edit" % (self.meeting.number, self.name) -# +# # @property # def relurl_edit(self): # return self.url_edit("") + def owner_email(self): + emails = self.owner.email_set.all() + if len(emails)>0: + return emails[0].address + else: + return "noemail" + @property def visible_token(self): if self.visible: @@ -512,12 +566,11 @@ class Schedule(models.Model): scheduled += 1 return assignments,sessions,total,scheduled - cached_sessions_that_can_meet = None @property def sessions_that_can_meet(self): - if self.cached_sessions_that_can_meet is None: - self.cached_sessions_that_can_meet = self.meeting.sessions_that_can_meet.all() - return self.cached_sessions_that_can_meet + if not hasattr(self, "_cached_sessions_that_can_meet"): + self._cached_sessions_that_can_meet = self.meeting.sessions_that_can_meet.all() + return self._cached_sessions_that_can_meet def area_list(self): return ( self.assignments.filter(session__group__type__slug__in=['wg', 'rg', 'ag'], @@ -529,6 +582,41 @@ class Schedule(models.Model): def groups(self): return Group.objects.filter(type__slug__in=['wg', 'rg', 'ag'], session__scheduledsession__schedule=self).distinct().order_by('parent__acronym', 'acronym') + @property + def qs_scheduledsessions_with_assignments(self): + return self.scheduledsession_set.filter(session__isnull=False) + + # calculate badness of entire schedule + def calc_badness(self): + # now calculate badness + assignments = self.group_mapping + return self.calc_badness1(assignments) + + # calculate badness of entire schedule + def calc_badness1(self, assignments): + badness = 0 + for sess in self.sessions_that_can_meet: + badness += sess.badness(assignments) + self.badness = badness + return badness + + def delete_schedule(self): + self.scheduledsession_set.all().delete() + self.delete() + + # send email to every session requester whose session is now scheduled. + # mark the sessions as now state scheduled, rather than waiting. + def sendEmail(self): + for ss in self.scheduledsession_set.all(): + session = ss.session + if session.status.slug == "schedw": + session.status_id = "sched" + session.scheduled = datetime.datetime.now() + import ietf.secr.meetings.views + ietf.secr.meetings.views.send_notification(None, [session]) + session.save() + +# to be renamed ScheduleTimeslotSessionAssignments (stsa) class ScheduledSession(models.Model): """ This model provides an N:M relationship between Session and TimeSlot. @@ -601,27 +689,31 @@ class ScheduledSession(models.Model): else: return "" - @property - def empty_str(self): - # return JS happy value - if self.session: - return "False" - else: - return "True" + def json_url(self): + return "/meeting/%s/schedule/%s/session/%u.json" % (self.schedule.meeting.number, + self.schedule.name, self.id) - def json_dict(self, selfurl): + def json_dict(self, host_scheme): ss = dict() ss['scheduledsession_id'] = self.id - #ss['href'] = self.url(host_scheme) - ss['empty'] = self.empty_str + ss['href'] = urljoin(host_scheme, self.json_url()) ss['timeslot_id'] = self.timeslot.id + + efset = self.session.scheduledsession_set.filter(schedule=self.schedule).order_by("timeslot__time") + if efset.count() > 1: + # now we know that there is some work to do finding the extendedfrom_id. + # loop through the list of items + previous = None + for efss in efset: + if efss.pk == self.pk: + extendedfrom = previous + break + previous = efss + if extendedfrom is not None: + ss['extendedfrom_id'] = extendedfrom.id + if self.session: ss['session_id'] = self.session.id - ss['room'] = slugify(self.timeslot.location) - ss['roomtype'] = self.timeslot.type.slug - ss["time"] = date_format(self.timeslot.time, 'Hi') - ss["date"] = time_format(self.timeslot.time, 'Y-m-d') - ss["domid"] = self.timeslot.js_identifier ss["pinned"] = self.pinned return ss @@ -670,9 +762,8 @@ class Constraint(models.Model): return True return False - @property def constraint_cost(self): - return self.name.cost(); + return self.name.penalty; def json_url(self): return "/meeting/%s/constraint/%s.json" % (self.meeting.number, self.id) @@ -715,6 +806,7 @@ class Session(models.Model): modified = models.DateTimeField(default=datetime.datetime.now) materials = models.ManyToManyField(Document, blank=True) + resources = models.ManyToManyField(ResourceAssociation) unique_constraints_dict = None @@ -747,6 +839,9 @@ class Session(models.Model): ss0name = ss[0].timeslot.time.strftime("%H%M") return u"%s: %s %s[%u]" % (self.meeting, self.group.acronym, ss0name, self.pk) + def is_bof(self): + return self.group.is_bof(); + @property def short_name(self): if self.name: @@ -817,18 +912,20 @@ class Session(models.Model): sess1['href'] = urljoin(host_scheme, self.json_url()) if self.group is not None: sess1['group'] = self.group.json_dict(host_scheme) - # nuke rest of these as soon as JS cleaned up. sess1['group_href'] = urljoin(host_scheme, self.group.json_url()) - sess1['group_acronym'] = str(self.group.acronym) if self.group.parent is not None: sess1['area'] = str(self.group.parent.acronym).upper() - sess1['GroupInfo_state']= str(self.group.state) sess1['description'] = str(self.group.name) sess1['group_id'] = str(self.group.pk) + reslist = [] + for r in self.resources.all(): + reslist.append(r.json_dict(host_scheme)) + sess1['resources'] = reslist sess1['session_id'] = str(self.pk) sess1['name'] = str(self.name) sess1['title'] = str(self.short_name) sess1['short_name'] = str(self.short_name) + sess1['bof'] = str(self.is_bof()) sess1['agenda_note'] = str(self.agenda_note) sess1['attendees'] = str(self.attendees) sess1['status'] = str(self.status) @@ -843,17 +940,57 @@ class Session(models.Model): pass sess1['requested_duration']= "%.1f" % (float(self.requested_duration.seconds) / 3600) - sess1['duration'] = sess1['requested_duration'] sess1['special_request'] = str(self.special_request_token) return sess1 + def agenda_text(self): + doc = self.agenda() + if doc: + path = os.path.join(settings.AGENDA_PATH, self.meeting.number, "agenda", doc.external_url) + if os.path.exists(path): + with open(path) as f: + return f.read() + else: + return "No agenda file found" + else: + return "The agenda has not been uploaded yet." + + def type(self): + if self.group.type.slug in [ "wg" ]: + return "BOF" if self.group.state.slug in ["bof", "bof-conc"] else "WG" + else: + return "" + + def ical_status(self): + if self.status.slug == 'canceled': # sic + return "CANCELLED" + elif (datetime.date.today() - self.meeting.date) > datetime.timedelta(days=5): + # this is a bit simpleminded, better would be to look at the + # time(s) of the timeslot(s) of the official meeting schedule. + return "CONFIRMED" + else: + return "TENTATIVE" + + def agenda_file(self): + if not hasattr(self, '_agenda_file'): + self._agenda_file = "" + + docs = self.materials.filter(type="agenda", states__type="agenda", states__slug="active") + if not docs: + return "" + + # we use external_url at the moment, should probably regularize + # the filenames to match the document name instead + filename = docs[0].external_url + self._agenda_file = "%s/agenda/%s" % (self.meeting.number, filename) + + return self._agenda_file def badness_test(self, num): from settings import BADNESS_CALC_LOG #sys.stdout.write("num: %u / BAD: %u\n" % (num, BADNESS_CALC_LOG)) return BADNESS_CALC_LOG >= num def badness_log(self, num, msg): - import sys if self.badness_test(num): sys.stdout.write(msg) @@ -878,7 +1015,7 @@ class Session(models.Model): conflicts = self.unique_constraints() if self.badness_test(2): - self.badness_log(2, "badgroup: %s badness calculation has %u constraints\n" % (self.group.acronym, len(conflicts))) + self.badness_log(2, "badness for group: %s has %u constraints\n" % (self.group.acronym, len(conflicts))) from settings import BADNESS_UNPLACED, BADNESS_TOOSMALL_50, BADNESS_TOOSMALL_100, BADNESS_TOOBIG, BADNESS_MUCHTOOBIG count = 0 myss_list = assignments[self.group] @@ -946,9 +1083,9 @@ class Session(models.Model): if self.badness_test(3): self.badness_log(3, " [%u] 4group: %s my_sessions: %s vs %s\n" % (count, group.acronym, myss.timeslot.time, ss.timeslot.time)) if ss.timeslot.time == myss.timeslot.time: - newcost = constraint.constraint_cost + newcost = constraint.constraint_cost() if self.badness_test(2): - self.badness_log(2, " [%u] 5group: %s conflicts: %s on %s cost %u\n" % (count, self.group.acronym, ss.session.group.acronym, ss.timeslot.time, newcost)) + self.badness_log(2, " [%u] 5group: %s conflict(%s): %s on %s cost %u\n" % (count, self.group.acronym, constraint.name_id, ss.session.group.acronym, ss.timeslot.time, newcost)) # yes accumulate badness. conflictbadness += newcost ss.badness = conflictbadness @@ -1035,51 +1172,8 @@ class Session(models.Model): if ss.timeslot is not None and ss.timeslot.location == timeslot.location: continue # ignore conflicts when two sessions in the same room constraint = conflict[1] - badness += constraint.constraint_cost + badness += constraint.constraint_cost() if self.badness_test(1): self.badness_log(1, "badgroup: %s badness = %u\n" % (self.group.acronym, badness)) return badness - - def agenda_text(self): - doc = self.agenda() - if doc: - path = os.path.join(settings.AGENDA_PATH, self.meeting.number, "agenda", doc.external_url) - if os.path.exists(path): - with open(path) as f: - return f.read() - else: - return "No agenda file found" - else: - return "The agenda has not been uploaded yet." - - def type(self): - if self.group.type.slug in [ "wg" ]: - return "BOF" if self.group.state.slug in ["bof", "bof-conc"] else "WG" - else: - return "" - - def ical_status(self): - if self.status.slug == 'canceled': # sic - return "CANCELLED" - elif (datetime.date.today() - self.meeting.date) > datetime.timedelta(days=5): - # this is a bit simpleminded, better would be to look at the - # time(s) of the timeslot(s) of the official meeting schedule. - return "CONFIRMED" - else: - return "TENTATIVE" - - def agenda_file(self): - if not hasattr(self, '_agenda_file'): - self._agenda_file = "" - - docs = self.materials.filter(type="agenda", states__type="agenda", states__slug="active") - if not docs: - return "" - - # we use external_url at the moment, should probably regularize - # the filenames to match the document name instead - filename = docs[0].external_url - self._agenda_file = "%s/agenda/%s" % (self.meeting.number, filename) - - return self._agenda_file diff --git a/ietf/meeting/placement.py b/ietf/meeting/placement.py new file mode 100644 index 000000000..bcd8d47b3 --- /dev/null +++ b/ietf/meeting/placement.py @@ -0,0 +1,738 @@ +# FILE: ietf/meeting/placement.py +# +# Copyright (c) 2013, The IETF Trust. See ../../../LICENSE. +# +# This file contains a model that encapsulates the progress of the automatic placer. +# Each step of placement is stored as a row in a table, not because this is necessary, +# but because it helps to debug things. +# +# A production run of the placer would do the same work, but simply not save anything. +# + +import sys +import datetime + +from random import Random +from datetime import datetime + +from django.db import models +from settings import BADNESS_UNPLACED, BADNESS_TOOSMALL_50, BADNESS_TOOSMALL_100, BADNESS_TOOBIG, BADNESS_MUCHTOOBIG +from ietf.meeting.models import Schedule, ScheduledSession,TimeSlot,Room + +def do_prompt(): + print "waiting:" + sys.stdin.readline() + +class PlacementException(Exception): + pass + +# ScheduleSlot really represents a single column of time. +# The TimeSlot object would work here, but it associates a room. +# There is a special Schedule slot (subclass) which corresponds to unscheduled items. +class ScheduleSlot(object): + def __init__(self, daytime): + self.daytime = daytime + self.badness = None + self.slotgroups = {} + + # this is a partial copy of ScheduledSession's methods. Prune later. + #def __unicode__(self): + # return u"%s [%s<->%s]" % (self.schedule, self.session, self.timeslot) + # + #def __str__(self): + # return self.__unicode__() + + def add_scheduledsession(self,fs): + self.slotgroups[fs] = fs + + def scheduled_session_pk(self, assignments): + things = [] + slot1 = assignments.slot1 + slot2 = assignments.slot2 + for fs in self.slotgroups.iterkeys(): + session = fs.session + if slot1 is not None and fs == slot1: + session = slot2.session + if slot2 is not None and fs == slot2: + session = slot1.session + if session is not None: + things.append((session.pk,fs)) + return things + + def recalc_badness1(self, assignments): + badness = 0 + for fs,fs2 in self.slotgroups.iteritems(): + if fs.session is not None: + num = fs.session.badness2(self) + #print "rc,,,,%s,%s,%u,recalc1" % (self.daytime, fs.session.short_name, num) + badness += num + self.badness = badness + + def recalc_badness(self, assignments): + badness = 0 + session_pk_list = self.scheduled_session_pk(assignments) + #print "rc,,,%u,slot_recalc" % (len(session_pk_list)) + for pk,fs in session_pk_list: + #print "rc,,,,%u,%s,list" % (pk,fs.session) + if fs.session is not None: + num = fs.session.badness_fast(fs.timeslot, self, session_pk_list) + #print "rc,,,,%s,%s,%u,recalc0" % (self.daytime, fs.session.short_name, num) + badness += num + self.badness = badness + + def calc_badness(self, assignments): + if self.badness is None: + self.recalc_badness(assignments) + return self.badness + +# +# this subclass does everything a ScheduleSlot does, in particular it knows how to +# maintain and recalculate badness, but it also maintains a list of slots which +# are unplaced so as to accelerate finding things to place at the beginning of automatic placement. +# +# XXX perhaps this should be in the form an iterator? +# +class UnplacedScheduleSlot(ScheduleSlot): + def __init__(self): + super(UnplacedScheduleSlot, self).__init__(None) + self.unplaced_slot_numbers = [] + self.unplaced_slots_finishcount = 0 + + def shuffle(self, generator): + generator.shuffle(self.unplaced_slot_numbers) + self.unplaced_slots_finishcount = self.count / 10 + + def finished(self): + if len(self.unplaced_slot_numbers) <= self.unplaced_slots_finishcount: + return True + else: + return False + + @property + def count(self): + return len(self.unplaced_slot_numbers) + + def add_scheduledsession(self,fs): + super(UnplacedScheduleSlot, self).add_scheduledsession(fs) + #print "unplaced add: %s" % (fs.available_slot) + self.unplaced_slot_numbers.append(fs.available_slot) + + def get_unplaced_slot_number(self): + #print "unplaced slots: %s" % (self.unplaced_slot_numbers) + return self.unplaced_slot_numbers[0] + + def delete_first(self): + del self.unplaced_slot_numbers[0] + + +class FakeScheduledSession(object): + """ + This model provides a fake (not-backed by database) N:M relationship between + Session and TimeSlot, but in this case TimeSlot is always None, because the + Session is not scheduled. + """ + faked = "fake" + + def __init__(self, schedule): + self.extendedfrom = None + self.modified = None + self.notes = None + self.badness = None + self.available_slot = None + self.origss = None + self.timeslot = None + self.session = None + self.schedule = schedule + self.pinned = False + self.scheduleslot = None + + def fromScheduledSession(self, ss): # or from another FakeScheduledSession + self.session = ss.session + self.schedule = ss.schedule + self.timeslot = ss.timeslot + self.modified = ss.modified + self.pinned = ss.pinned + self.origss = ss + + def save(self): + pass + + # this is a partial copy of ScheduledSession's methods. Prune later. + def __unicode__(self): + return u"%s [%s<->%s]" % (self.schedule, self.session, self.timeslot) + + def __str__(self): + return self.__unicode__() + + @property + def room_name(self): + return "noroom" + + @property + def special_agenda_note(self): + return self.session.agenda_note if self.session else "" + + @property + def acronym(self): + if self.session and self.session.group: + return self.session.group.acronym + + @property + def slot_to_the_right(self): + return None + + @property + def acronym_name(self): + if not self.session: + return self.notes + if hasattr(self, "interim"): + return self.session.group.name + " (interim)" + elif self.session.name: + return self.session.name + else: + return self.session.group.name + + @property + def session_name(self): + return self.session.name + + @property + def area(self): + if not self.session or not self.session.group: + return "" + if self.session.group.type_id == "irtf": + return "irtf" + if self.timeslot.type_id == "plenary": + return "1plenary" + if not self.session.group.parent or not self.session.group.parent.type_id in ["area","irtf"]: + return "" + return self.session.group.parent.acronym + + @property + def break_info(self): + return None + + @property + def area_name(self): + if self.session and self.session.group and self.session.group.acronym == "edu": + return "Training" + elif not self.session or not self.session.group or not self.session.group.parent or not self.session.group.parent.type_id == "area": + return "" + return self.session.group.parent.name + + @property + def isWG(self): + if not self.session or not self.session.group: + return False + if self.session.group.type_id == "wg" and self.session.group.state_id != "bof": + return True + + @property + def group_type_str(self): + if not self.session or not self.session.group: + return "" + if self.session.group and self.session.group.type_id == "wg": + if self.session.group.state_id == "bof": + return "BOF" + else: + return "WG" + + return "" + + @property + def slottype(self): + return "" + + @property + def empty_str(self): + # return JS happy value + if self.session: + return "False" + else: + return "True" + + def json_dict(self, selfurl): + ss = dict() + ss['scheduledsession_id'] = self.id + #ss['href'] = self.url(sitefqdn) + ss['empty'] = self.empty_str + ss['timeslot_id'] = self.timeslot.id + if self.session: + ss['session_id'] = self.session.id + ss['room'] = slugify(self.timeslot.location) + ss['roomtype'] = self.timeslot.type.slug + ss["time"] = date_format(self.timeslot.time, 'Hi') + ss["date"] = time_format(self.timeslot.time, 'Y-m-d') + ss["domid"] = self.timeslot.js_identifier + return ss + +# this object maintains the current state of the placement tool. +# the assignments hash says where the sessions would go. +class CurrentScheduleState: + def __getitem__(self, key): + if key in self.tempdict: + return self.tempdict[key] + return self.current_assignments[key] + + def __iter__(self): + return self.current_assignments.__iter__() + def iterkeys(self): + return self.current_assignments.__iter__() + + def add_to_available_slot(self, fs): + size = len(self.available_slots) + if fs.session is not None: + fs.session.setup_conflicts() + + time_column = None + needs_to_be_added = True + #print "adding fs for slot: %s" % (fs.timeslot) + if fs.timeslot is not None: + if fs.timeslot in self.fs_by_timeslot: + ofs = self.fs_by_timeslot[fs.timeslot] + #print " duplicate timeslot[%s], updating old one: %s" % (ofs.available_slot, fs.timeslot) + if ofs.session is None: + # keep the one with the assignment. + self.fs_by_timeslot[fs.timeslot] = fs + # get rid of old item + fs.available_slot = ofs.available_slot + self.available_slots[ofs.available_slot] = fs + needs_to_be_added = False + else: + self.fs_by_timeslot[fs.timeslot] = fs + + # add the slot to the list of vertical slices. + time_column = self.timeslots[fs.timeslot.time] + group_name = "empty" + if fs.session is not None: + group_name = fs.session.group.acronym + #print " inserting fs %s / %s to slot: %s" % (fs.timeslot.location.name, + # group_name, + # time_column.daytime) + fs.scheduleslot = time_column + if fs.session is None: + self.placed_scheduleslots.append(fs) + else: + time_column = self.unplaced_scheduledslots + fs.scheduleslot = self.unplaced_scheduledslots + + if needs_to_be_added: + self.total_slots = size + self.available_slots.append(fs) + fs.available_slot = size + + if time_column is not None: + # needs available_slot to be filled in + time_column.add_scheduledsession(fs) + #print "adding item: %u to unplaced slots (pinned: %s)" % (fs.available_slot, fs.pinned) + + def __init__(self, schedule, seed=None): + # initialize available_slots with the places that a session can go based upon the + # scheduledsession objects of the provided schedule. + # for each session which is not initially scheduled, also create a scheduledsession + # that has a session, but no timeslot. + + self.recordsteps = True + self.debug_badness = False + self.lastSaveTime = datetime.now() + self.lastSaveStep = 0 + self.verbose = False + + # this maps a *group* to a list of (session,location) pairs, using FakeScheduledSession + self.current_assignments = {} + self.tempdict = {} # used when calculating badness. + + # this contains an entry for each location, and each un-location in the form of + # (session,location) with the appropriate part None. + self.fs_by_timeslot = {} + self.available_slots = [] + self.unplaced_scheduledslots = UnplacedScheduleSlot() + self.placed_scheduleslots = [] + self.sessions = {} + self.total_slots = 0 + + self.schedule = schedule + self.meeting = schedule.meeting + self.seed = seed + self.badness = schedule.badness + self.random_generator=Random() + self.random_generator.seed(seed) + self.temperature = 10000000 + self.stepnum = 1 + self.timeslots = {} + self.slot1 = None + self.slot2 = None + + # setup up array of timeslots objects + for timeslot in schedule.meeting.timeslot_set.filter(type = "session").all(): + if not timeslot.time in self.timeslots: + self.timeslots[timeslot.time] = ScheduleSlot(timeslot.time) + fs = FakeScheduledSession(self.schedule) + fs.timeslot = timeslot + self.add_to_available_slot(fs) + self.timeslots[None] = self.unplaced_scheduledslots + + # make list of things that need placement. + for sess in self.meeting.sessions_that_can_be_placed().all(): + fs = FakeScheduledSession(self.schedule) + fs.session = sess + self.sessions[sess] = fs + self.current_assignments[sess.group] = [] + + #print "Then had %u" % (self.total_slots) + # now find slots that are not empty. + # loop here and the one for useableslots could be merged into one loop + allschedsessions = self.schedule.qs_scheduledsessions_with_assignments.filter(timeslot__type = "session").all() + for ss in allschedsessions: + # do not need to check for ss.session is not none, because filter above only returns those ones. + sess = ss.session + if not (sess in self.sessions): + #print "Had to create sess for %s" % (sess) + self.sessions[sess] = FakeScheduledSession(self.schedule) + fs = self.sessions[sess] + #print "Updating %s from %s(%s)" % (fs.session.group.acronym, ss.timeslot.location.name, ss.timeslot.time) + fs.fromScheduledSession(ss) + + # if pinned, then do not consider it when selecting, but it needs to be in + # current_assignments so that conflicts are calculated. + if not ss.pinned: + self.add_to_available_slot(fs) + else: + del self.sessions[sess] + self.current_assignments[ss.session.group].append(fs) + + # XXX can not deal with a session in two slots yet?! + + # need to remove any sessions that might have gotten through above, but are in non-session + # places, otherwise these could otherwise appear to be unplaced. + allspecialsessions = self.schedule.qs_scheduledsessions_with_assignments.exclude(timeslot__type = "session").all() + for ss in allspecialsessions: + sess = ss.session + if sess is None: + continue + if (sess in self.sessions): + del self.sessions[sess] + + # now need to add entries for those sessions which are currently unscheduled (and yet not pinned) + for sess,fs in self.sessions.iteritems(): + if fs.timeslot is None: + #print "Considering sess: %s, and loc: %s" % (sess, str(fs.timeslot)) + self.add_to_available_slot(fs) + + #import pdb; pdb.set_trace() + # do initial badness calculation for placement that has been done + for daytime,scheduleslot in self.timeslots.iteritems(): + scheduleslot.recalc_badness(self) + + def dump_available_slot_state(self): + for fs in self.available_slots: + shortname="unplaced" + sessid = 0 + if fs.session is not None: + shortname=fs.session.short_name + sessid = fs.session.id + pinned = "unplaced" + ssid=0 + if fs.origss is not None: + pinned = fs.origss.pinned + ssid = fs.origss.id + print "%s: %s[%u] pinned: %s ssid=%u" % (fs.available_slot, shortname, sessid, pinned, ssid) + + def pick_initial_slot(self): + if self.unplaced_scheduledslots.finished(): + self.initial_stage = False + if self.initial_stage: + item = self.unplaced_scheduledslots.get_unplaced_slot_number() + slot1 = self.available_slots[item] + #print "item: %u points to %s" % (item, slot1) + else: + slot1 = self.random_generator.choice(self.available_slots) + return slot1 + + def pick_second_slot(self): + if self.initial_stage and len(self.placed_scheduleslots)>0: + self.random_generator.shuffle(self.placed_scheduleslots) + slot2 = self.placed_scheduleslots[0] + del self.placed_scheduleslots[0] + else: + slot2 = self.random_generator.choice(self.available_slots) + return slot2 + + def pick_two_slots(self): + slot1 = self.pick_initial_slot() + slot2 = self.pick_second_slot() + tries = 100 + self.repicking = 0 + # 1) no point in picking two slots which are the same. + # 2) no point in picking two slots which have no session (already empty) + # 3) no point in picking two slots which are both unscheduled sessions + # 4) limit outselves to ten tries. + while (slot1 == slot2 or slot1 is None or slot2 is None or + (slot1.session is None and slot2.session is None) or + (slot1.timeslot is None and slot2.timeslot is None) + ) and tries > 0: + self.repicking += 1 + #print "%u: .. repicking slots, had: %s and %s" % (self.stepnum, slot1, slot2) + slot1 = self.pick_initial_slot() + slot2 = self.pick_second_slot() + tries -= 1 + if tries == 0: + raise PlacementException("How can it pick the same slot ten times in a row") + + if slot1.pinned: + raise PlacementException("Should never attempt to move pinned slot1") + + if slot2.pinned: + raise PlacementException("Should never attempt to move pinned slot2") + + return slot1, slot2 + + # this assigns a session to a particular slot. + def assign_session(self, session, fslot, doubleup=False): + import copy + if session is None: + # we need to unschedule the session + session = fslot.session + self.tempdict[session.group] = [] + return + + if not session in self.sessions: + raise PlacementException("Is there a legit case where session is not in sessions here?") + + oldfs = self.sessions[session] + # find the group mapping. + pairs = copy.copy(self.current_assignments[session.group]) + #print "pairs is: %s" % (pairs) + if oldfs in pairs: + which = pairs.index(oldfs) + del pairs[which] + #print "new pairs is: %s" % (pairs) + + self.sessions[session] = fslot + # now fix up the other things. + pairs.append(fslot) + self.tempdict[session.group] = pairs + + def commit_tempdict(self): + for key,value in self.tempdict.iteritems(): + self.current_assignments[key] = value + self.tempdict = dict() + + # calculate badness of the columns which have changed + def calc_badness(self, slot1, slot2): + badness = 0 + for daytime,scheduleslot in self.timeslots.iteritems(): + oldbadness = scheduleslot.badness + if oldbadness is None: + oldbadness = 0 + recalc="" + if slot1 is not None and slot1.scheduleslot == scheduleslot: + recalc="recalc slot1" + scheduleslot.recalc_badness(self) + if slot2 is not None and slot2.scheduleslot == scheduleslot: + recalc="recalc slot2" + scheduleslot.recalc_badness(self) + + newbadness = scheduleslot.calc_badness(self) + if self.debug_badness: + print " calc: %s %u %u %s" % (scheduleslot.daytime, oldbadness, newbadness, recalc) + badness += newbadness + return badness + + def try_swap(self): + badness = self.badness + slot1,slot2 = self.pick_two_slots() + if self.debug_badness: + print "start\n slot1: %s.\n slot2: %s.\n badness: %s" % (slot1, slot2,badness) + self.slot1 = slot1 + self.slot2 = slot2 + #import pdb; pdb.set_trace() + #self.assign_session(slot2.session, slot1, False) + #self.assign_session(slot1.session, slot2, False) + # self can substitute for current_assignments thanks to getitem() above. + newbadness = self.calc_badness(slot1, slot2) + if self.debug_badness: + print "end\n slot1: %s.\n slot2: %s.\n badness: %s" % (slot1, slot2, newbadness) + return newbadness + + def log_step(self, accepted_str, change, dice, prob): + acronym1 = "empty" + if self.slot1.session is not None: + acronym1 = self.slot1.session.group.acronym + place1 = "nowhere" + if self.slot1.timeslot is not None: + place1 = str(self.slot1.timeslot.location.name) + + acronym2= "empty" + if self.slot2.session is not None: + acronym2 = self.slot2.session.group.acronym + place2 = "nowhere" + if self.slot2.timeslot is not None: + place2 = str(self.slot2.timeslot.location.name) + initial = " " + if self.initial_stage: + initial = "init" + + # note in logging: the swap has already occured, but the values were set before + if self.verbose: + print "% 5u:%s %s temp=%9u delta=%+9d badness=%10d dice=%.4f <=> prob=%.4f (repicking=%u) %9s:[%8s->%8s], %9s:[%8s->%8s]" % (self.stepnum, initial, + accepted_str, self.temperature, + change, self.badness, dice, prob, + self.repicking, acronym1, place2, place1, acronym2, place1, place2) + + def do_step(self): + self.stepnum += 1 + newbadness = self.try_swap() + if self.badness is None: + self.commit_tempdict + self.badness = newbadness + return True, 0 + + change = newbadness - self.badness + prob = self.calc_probability(change) + dice = self.random_generator.random() + + #self.log_step("consider", change, dice, prob) + + if dice < prob: + accepted_str = "accepted" + accepted = True + # swap things as planned + self.commit_tempdict + + # actually do the swap in the FS + tmp = self.slot1.session + self.slot1.session = self.slot2.session + self.slot2.session = tmp + self.badness = newbadness + # save state object + else: + accepted_str = "rejected" + accepted = False + self.tempdict = dict() + + self.log_step(accepted_str, change, dice, prob) + + if accepted and not self.initial_stage: + self.temperature = self.temperature * 0.9995 + + return accepted, change + + def calc_probability(self, change): + import math + return 1/(1 + math.exp(float(change)/self.temperature)) + + def delete_available_slot(self, number): + # because the numbers matter, we just None things out, and let repicking + # work on things. + #last = len(self.available_slots)-1 + #if number < last: + # self.available_slots[number] = self.available_slots[last] + # self.available_slots[last].available_slot = number + # + #del self.available_slots[last] + self.available_slots[number] = None + + def do_steps(self, limit=None, monitorSchedule=None): + print "do_steps(%s,%s)" % (limit, monitorSchedule) + if self.badness is None or self.badness == 0: + self.badness = self.schedule.calc_badness1(self) + self.oldbadness = self.badness + while (limit is None or self.stepnum < limit) and self.temperature > 1000: + accepted,change = self.do_step() + #set_prompt_wait(True) + if not accepted and self.initial_stage: + # randomize again! + self.unplaced_scheduledslots.shuffle(self.random_generator) + + if accepted and self.initial_stage and self.unplaced_scheduledslots.count>0: + # delete it from available slots, so as not to leave unplaced slots + self.delete_available_slot(self.slot1.available_slot) + # remove initial slot from list. + self.unplaced_scheduledslots.delete_first() + + if False and accepted and self.recordsteps: + ass1 = AutomaticScheduleStep() + ass1.schedule = self.schedule + if self.slot1.session is not None: + ass1.session = self.slot1.session + if self.slot1.origss is not None: + ass1.moved_to = self.slot1.origss + ass1.stepnum = self.stepnum + ass1.save() + ass2 = AutomaticScheduleStep() + ass2.schedule = self.schedule + if self.slot2.session is not None: + ass2.session = self.slot2.session + if self.slot2.origss is not None: + ass2.moved_to = self.slot2.origss + ass2.stepnum = self.stepnum + ass2.save() + #print "%u: accepted: %s change %d temp: %d" % (self.stepnum, accepted, change, self.temperature) + if (self.stepnum % 1000) == 0 and monitorSchedule is not None: + self.saveToSchedule(monitorSchedule) + print "Finished after %u steps, badness = %u->%u" % (self.stepnum, self.oldbadness, self.badness) + + def saveToSchedule(self, targetSchedule): + when = datetime.now() + since = 0 + rate = 0 + if targetSchedule is None: + targetSchedule = self.schedule + else: + # XXX more stuff to do here, setup mapping, copy pinned items + pass + + if self.lastSaveTime is not None: + since = when - self.lastSaveTime + if since.microseconds > 0: + rate = 1000 * float(self.stepnum - self.lastSaveStep) / (1000*since.seconds + since.microseconds / 1000) + print "%u: saved to schedule: %s %s elapsed=%s rate=%.2f" % (self.stepnum, targetSchedule.name, when, since, rate) + self.lastSaveTime = datetime.now() + self.lastSaveStep = self.stepnum + + # first, remove all assignments in the schedule. + for ss in targetSchedule.scheduledsession_set.all(): + if ss.pinned: + continue + ss.delete() + + # then, add new items for new placements. + for fs in self.available_slots: + if fs is None: + continue + ss = ScheduledSession(timeslot = fs.timeslot, + schedule = targetSchedule, + session = fs.session) + ss.save() + + def do_placement(self, limit=None, targetSchedule=None): + self.badness = self.schedule.calc_badness1(self) + if limit is None: + limitstr = "unlimited " + else: + limitstr = "%u" % (limit) + print "Initial stage (limit=%s) starting with: %u items to place" % (limitstr, self.unplaced_scheduledslots.count) + + # permute the unplaced sessions + self.unplaced_scheduledslots.shuffle(self.random_generator) + + self.initial_stage = True + monitorSchedule = targetSchedule + if monitorSchedule is None: + monitorSchedule = self.schedule + self.do_steps(limit, monitorSchedule) + self.saveToSchedule(targetSchedule) + +# +# this does not clearly have value at this point. +# Not worth a migration/table yet. +# +if False: + class AutomaticScheduleStep(models.Model): + schedule = models.ForeignKey('Schedule', null=False, blank=False, help_text=u"Who made this agenda") + session = models.ForeignKey('Session', null=True, default=None, help_text=u"Scheduled session involved") + moved_from = models.ForeignKey('ScheduledSession', related_name="+", null=True, default=None, help_text=u"Where session was") + moved_to = models.ForeignKey('ScheduledSession', related_name="+", null=True, default=None, help_text=u"Where session went") + stepnum = models.IntegerField(default=0, blank=True, null=True) + diff --git a/ietf/meeting/tests_api.py b/ietf/meeting/tests_api.py index 01699928f..f1e3cb566 100644 --- a/ietf/meeting/tests_api.py +++ b/ietf/meeting/tests_api.py @@ -213,7 +213,7 @@ class ApiTests(TestCase): meeting = make_meeting_test_data() url = urlreverse("ietf.meeting.ajax.agenda_infourl", - kwargs=dict(num=meeting.number, schedule_name=meeting.agenda.name)) + kwargs=dict(num=meeting.number, name=meeting.agenda.name)) r = self.client.get(url) info = json.loads(r.content) @@ -247,7 +247,7 @@ class ApiTests(TestCase): url = urlreverse("ietf.meeting.ajax.agenda_infourl", kwargs=dict(num=meeting.number, - schedule_name=meeting.agenda.name)) + name=meeting.agenda.name)) post_data = { 'visible': 'false', @@ -272,7 +272,7 @@ class ApiTests(TestCase): url = urlreverse("ietf.meeting.ajax.agenda_infourl", kwargs=dict(num=meeting.number, - schedule_name=meeting.agenda.name)) + name=meeting.agenda.name)) # unauthorized delete self.client.login(remote_user="plain") r = self.client.delete(url) diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 6b78c4ae2..5573e482e 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -174,7 +174,7 @@ class EditTests(TestCase): self.assertEqual(r.status_code, 302) # get - url = urlreverse("ietf.meeting.views.edit_agenda", kwargs=dict(num=meeting.number, schedule_name="foo")) + url = urlreverse("ietf.meeting.views.edit_agenda", kwargs=dict(num=meeting.number, name="foo")) r = self.client.get(url) self.assertEqual(r.status_code, 200) diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py index c3743bdd5..6152ab8b8 100644 --- a/ietf/meeting/urls.py +++ b/ietf/meeting/urls.py @@ -20,11 +20,13 @@ urlpatterns = patterns('', (r'^agenda/week-view.html$', views.week_view), (r'^week-view.html$', views.week_view), (r'^(?P<num>\d+)/schedule/edit$', views.edit_agenda), - (r'^(?P<num>\d+)/schedule/(?P<schedule_name>[A-Za-z0-9-:_]+)/edit$', views.edit_agenda), - (r'^(?P<num>\d+)/schedule/(?P<schedule_name>[A-Za-z0-9-:_]+)/details$', views.edit_agenda_properties), + (r'^(?P<num>\d+)/schedule/(?P<name>[A-Za-z0-9-:_]+)/edit$', views.edit_agenda), + (r'^(?P<num>\d+)/schedule/(?P<name>[A-Za-z0-9-:_]+)/details$', views.edit_agenda_properties), (r'^(?P<num>\d+)/schedule/(?P<name>[A-Za-z0-9-:_]+)(?P<ext>.html)?/?$', views.agenda), (r'^(?P<num>\d+)/agenda(?P<ext>.html)?/?$', views.agenda), (r'^(?P<num>\d+)/(?P<base>agenda-utc)(?P<ext>.html)?/?$', views.agenda), + (r'^(?P<num>\d+)/schedule/(?P<name>[A-Za-z0-9-:_]+)/sessions.json$', ajax.scheduledsessions_json), + (r'^(?P<num>\d+)/schedule/(?P<name>[A-Za-z0-9-:_]+)/session/(?P<scheduledsession_id>\d+).json$', ajax.scheduledsession_json), (r'^(?P<num>\d+)/requests.html$', RedirectView.as_view(url='/meeting/%(num)s/requests', permanent=True)), (r'^(?P<num>\d+)/requests$', views.meeting_requests), (r'^(?P<num>\d+)/agenda(?P<ext>.txt)$', views.agenda), @@ -34,10 +36,11 @@ urlpatterns = patterns('', (r'^(?P<num>\d+)/timeslots/edit$', views.edit_timeslots), (r'^(?P<num>\d+)/rooms$', ajax.timeslot_roomsurl), (r'^(?P<num>\d+)/room/(?P<roomid>\d+).json$', ajax.timeslot_roomurl), + (r'^(?P<num>\d+)/room/(?P<roomid>\d+).html$', views.edit_roomurl), (r'^(?P<num>\d+)/timeslots$', ajax.timeslot_slotsurl), (r'^(?P<num>\d+)/timeslots.json$', ajax.timeslot_slotsurl), (r'^(?P<num>\d+)/timeslot/(?P<slotid>\d+).json$', ajax.timeslot_sloturl), - (r'^(?P<num>\d+)/agendas/(?P<schedule_name>[A-Za-z0-9-:_]+).json$', ajax.agenda_infourl), + (r'^(?P<num>\d+)/agendas/(?P<name>[A-Za-z0-9-:_]+).json$', ajax.agenda_infourl), (r'^(?P<num>\d+)/agendas$', ajax.agenda_infosurl), (r'^(?P<num>\d+)/agendas.json$', ajax.agenda_infosurl), (r'^(?P<num>\d+)/week-view.html$', views.week_view), @@ -45,6 +48,7 @@ urlpatterns = patterns('', (r'^(?P<num>\d+)/agenda/(?P<session>[A-Za-z0-9-]+)-drafts.pdf$', views.session_draft_pdf), (r'^(?P<num>\d+)/agenda/(?P<session>[A-Za-z0-9-]+)-drafts.tgz$', views.session_draft_tarfile), (r'^(?P<num>\d+)/agenda/(?P<session>[A-Za-z0-9-]+)/?$', views.session_agenda), + (r'^(?P<num>\d+)/sessions.json', ajax.sessions_json), (r'^(?P<num>\d+)/session/(?P<sessionid>\d+).json', ajax.session_json), (r'^(?P<num>\d+)/session/(?P<sessionid>\d+)/constraints.json', ajax.session_constraints), (r'^(?P<num>\d+)/constraint/(?P<constraintid>\d+).json', ajax.constraint_json), diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index f2bd733d1..44a0a0d30 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -23,12 +23,13 @@ from django.middleware.gzip import GZipMiddleware from django.db.models import Max from django.forms.models import modelform_factory from django.views.decorators.csrf import ensure_csrf_cookie +from django.forms import ModelForm from ietf.utils.pipe import pipe from ietf.ietfauth.utils import role_required, has_role from ietf.doc.models import Document, State from ietf.person.models import Person -from ietf.meeting.models import Meeting, TimeSlot, Session, Schedule +from ietf.meeting.models import Meeting, TimeSlot, Session, Schedule, Room from ietf.group.models import Group from ietf.meeting.helpers import get_areas @@ -97,13 +98,13 @@ class SaveAsForm(forms.Form): savename = forms.CharField(max_length=100) @role_required('Area Director','Secretariat') -def agenda_create(request, num=None, schedule_name=None): +def agenda_create(request, num=None, name=None): meeting = get_meeting(num) - schedule = get_schedule(meeting, schedule_name) + schedule = get_schedule(meeting, name) if schedule is None: # here we have to return some ajax to display an error. - raise Http404("No meeting information for meeting %s schedule %s available" % (num,schedule_name)) + raise Http404("No meeting information for meeting %s schedule %s available" % (num,name)) # authorization was enforced by the @group_require decorator above. @@ -160,6 +161,7 @@ def agenda_create(request, num=None, schedule_name=None): return redirect(edit_agenda, meeting.number, newschedule.name) +@role_required('Secretariat') @decorator_from_middleware(GZipMiddleware) @ensure_csrf_cookie def edit_timeslots(request, num=None): @@ -174,8 +176,8 @@ def edit_timeslots(request, num=None): rooms = meeting.room_set.order_by("capacity") + # this import locate here to break cyclic loop. from ietf.meeting.ajax import timeslot_roomsurl, AddRoomForm, timeslot_slotsurl, AddSlotForm - roomsurl = reverse(timeslot_roomsurl, args=[meeting.number]) adddayurl = reverse(timeslot_slotsurl, args=[meeting.number]) @@ -194,15 +196,47 @@ def edit_timeslots(request, num=None): "meeting":meeting}, RequestContext(request)), content_type="text/html") +class RoomForm(ModelForm): + class Meta: + model = Room + exclude = ('meeting',) + +@role_required('Secretariat') +def edit_roomurl(request, num, roomid): + meeting = get_meeting(num) + + try: + room = meeting.room_set.get(pk=roomid) + except Room.DoesNotExist: + raise Http404("No room %u for meeting %s" % (roomid, meeting.name)) + + if request.POST: + roomform = RoomForm(request.POST, instance=room) + new_room = roomform.save(commit=False) + new_room.meeting = meeting + new_room.save() + roomform.save_m2m() + return HttpResponseRedirect( reverse(edit_timeslots, args=[meeting.number]) ) + + roomform = RoomForm(instance=room) + meeting_base_url = request.build_absolute_uri(meeting.base_url()) + site_base_url = request.build_absolute_uri('/')[:-1] # skip the trailing slash + return HttpResponse(render_to_string("meeting/room_edit.html", + {"meeting_base_url": meeting_base_url, + "site_base_url": site_base_url, + "editroom": roomform, + "meeting":meeting}, + RequestContext(request)), content_type="text/html") + ############################################################################## #@role_required('Area Director','Secretariat') # disable the above security for now, check it below. @decorator_from_middleware(GZipMiddleware) @ensure_csrf_cookie -def edit_agenda(request, num=None, schedule_name=None): +def edit_agenda(request, num=None, name=None): if request.method == 'POST': - return agenda_create(request, num, schedule_name) + return agenda_create(request, num, name) user = request.user requestor = "AnonymousUser" @@ -215,8 +249,8 @@ def edit_agenda(request, num=None, schedule_name=None): pass meeting = get_meeting(num) - #sys.stdout.write("requestor: %s for sched_name: %s \n" % ( requestor, schedule_name )) - schedule = get_schedule(meeting, schedule_name) + #sys.stdout.write("requestor: %s for sched_name: %s \n" % ( requestor, name )) + schedule = get_schedule(meeting, name) #sys.stdout.write("2 requestor: %u for sched owned by: %u \n" % ( requestor.id, schedule.owner.id )) meeting_base_url = request.build_absolute_uri(meeting.base_url()) @@ -239,14 +273,8 @@ def edit_agenda(request, num=None, schedule_name=None): "meeting_base_url":meeting_base_url}, RequestContext(request)), status=403, content_type="text/html") - sessions = meeting.sessions_that_can_meet.order_by("id", "group", "requested_by") scheduledsessions = get_all_scheduledsessions_from_schedule(schedule) - session_jsons = [ json.dumps(s.json_dict(site_base_url)) for s in sessions ] - - # useful when debugging javascript - #session_jsons = session_jsons[1:20] - # get_modified_from needs the query set, not the list modified = get_modified_from_scheduledsessions(scheduledsessions) @@ -275,7 +303,6 @@ def edit_agenda(request, num=None, schedule_name=None): "area_list": area_list, "area_directors" : ads, "wg_list": wg_list , - "session_jsons": session_jsons, "scheduledsessions": scheduledsessions, "show_inline": set(["txt","htm","html"]) }, RequestContext(request)), content_type="text/html") @@ -289,10 +316,10 @@ AgendaPropertiesForm = modelform_factory(Schedule, fields=('name','visible', 'pu @role_required('Area Director','Secretariat') @decorator_from_middleware(GZipMiddleware) @ensure_csrf_cookie -def edit_agenda_properties(request, num=None, schedule_name=None): +def edit_agenda_properties(request, num=None, name=None): meeting = get_meeting(num) - schedule = get_schedule(meeting, schedule_name) + schedule = get_schedule(meeting, name) form = AgendaPropertiesForm(instance=schedule) return HttpResponse(render_to_string("meeting/properties_edit.html", @@ -311,7 +338,7 @@ def edit_agenda_properties(request, num=None, schedule_name=None): def edit_agendas(request, num=None, order=None): #if request.method == 'POST': - # return agenda_create(request, num, schedule_name) + # return agenda_create(request, num, name) meeting = get_meeting(num) user = request.user @@ -546,7 +573,7 @@ def ical_agenda(request, num=None, name=None, ext=None): # Process the special flags. # "-wgname" will remove a working group from the output. - # "~Type" will add that type to the output. + # "~Type" will add that type to the output. # "-~Type" will remove that type from the output # Current types are: # Session, Other (default on), Break, Plenary (default on) @@ -568,7 +595,7 @@ def ical_agenda(request, num=None, name=None, ext=None): Q(session__group__parent__acronym__in = include) ).exclude(session__group__acronym__in = exclude).distinct() #.exclude(Q(session__group__isnull = False), - #Q(session__group__acronym__in = exclude) | + #Q(session__group__acronym__in = exclude) | #Q(session__group__parent__acronym__in = exclude)) return HttpResponse(render_to_string("meeting/agenda.ics", diff --git a/ietf/name/migrations/0020_add_default_penalties.py b/ietf/name/migrations/0020_add_default_penalties.py new file mode 100644 index 000000000..53ac9c0d8 --- /dev/null +++ b/ietf/name/migrations/0020_add_default_penalties.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models +from settings import BADNESS_TOOBIG, BADNESS_MUCHTOOBIG +from settings import BADNESS_CONFLICT_1, BADNESS_CONFLICT_2, BADNESS_CONFLICT_3, BADNESS_BETHERE + +class Migration(DataMigration): + + def forwards(self, orm): + bethere = orm.ConstraintName.objects.get(slug="bethere") + if bethere.penalty is None or bethere.penalty == 0: + bethere.penalty = BADNESS_BETHERE + bethere.save() + + # + conflict = orm.ConstraintName.objects.get(slug="conflict") + if conflict.penalty is None or conflict.penalty == 0: + conflict.penalty = BADNESS_CONFLICT_1 + conflict.save() + + # + conflic2 = orm.ConstraintName.objects.get(slug="conflic2") + if conflic2.penalty is None or conflic2.penalty == 0: + conflic2.penalty = BADNESS_CONFLICT_2 + conflic2.save() + + # + conflic3 = orm.ConstraintName.objects.get(slug="conflic3") + if conflic3.penalty is None or conflic3.penalty == 0: + conflic3.penalty = BADNESS_CONFLICT_3 + conflic3.save() + + def backwards(self, orm): + pass + + models = { + 'name.ballotpositionname': { + 'Meta': {'ordering': "['order']", 'object_name': 'BallotPositionName'}, + 'blocking': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.constraintname': { + 'Meta': {'ordering': "['order']", 'object_name': 'ConstraintName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'penalty': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.dbtemplatetypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DBTemplateTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.docrelationshipname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocRelationshipName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'revname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.docremindertypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocReminderTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctagname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTagName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.feedbacktype': { + 'Meta': {'ordering': "['order']", 'object_name': 'FeedbackType'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.groupmilestonestatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupMilestoneStateName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.groupstatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupStateName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.grouptypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.intendedstdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'IntendedStdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.liaisonstatementpurposename': { + 'Meta': {'ordering': "['order']", 'object_name': 'LiaisonStatementPurposeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.meetingtypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'MeetingTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.nomineepositionstate': { + 'Meta': {'ordering': "['order']", 'object_name': 'NomineePositionState'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.rolename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoleName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.sessionstatusname': { + 'Meta': {'ordering': "['order']", 'object_name': 'SessionStatusName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.stdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.streamname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StreamName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.timeslottypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'TimeSlotTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + } + } + + complete_apps = ['name'] + symmetrical = True diff --git a/ietf/name/migrations/0021_add_room.py b/ietf/name/migrations/0021_add_room.py new file mode 100644 index 000000000..f7189b53f --- /dev/null +++ b/ietf/name/migrations/0021_add_room.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'RoomResourceName' + db.create_table('name_roomresourcename', ( + ('slug', self.gf('django.db.models.fields.CharField')(max_length=8, primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('desc', self.gf('django.db.models.fields.TextField')(blank=True)), + ('used', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('order', self.gf('django.db.models.fields.IntegerField')(default=0)), + )) + db.send_create_signal('name', ['RoomResourceName']) + + def backwards(self, orm): + # Deleting model 'RoomResourceName' + db.delete_table('name_roomresourcename') + + + models = { + 'name.ballotpositionname': { + 'Meta': {'ordering': "['order']", 'object_name': 'BallotPositionName'}, + 'blocking': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.constraintname': { + 'Meta': {'ordering': "['order']", 'object_name': 'ConstraintName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'penalty': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.dbtemplatetypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DBTemplateTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.docrelationshipname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocRelationshipName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'revname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.docremindertypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocReminderTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctagname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTagName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.feedbacktype': { + 'Meta': {'ordering': "['order']", 'object_name': 'FeedbackType'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.groupmilestonestatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupMilestoneStateName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.groupstatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupStateName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.grouptypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.intendedstdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'IntendedStdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.liaisonstatementpurposename': { + 'Meta': {'ordering': "['order']", 'object_name': 'LiaisonStatementPurposeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.meetingtypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'MeetingTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.nomineepositionstate': { + 'Meta': {'ordering': "['order']", 'object_name': 'NomineePositionState'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.rolename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoleName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.roomresourcename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoomResourceName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.sessionstatusname': { + 'Meta': {'ordering': "['order']", 'object_name': 'SessionStatusName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.stdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.streamname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StreamName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.timeslottypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'TimeSlotTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + } + } + + complete_apps = ['name'] diff --git a/ietf/name/migrations/0022_add_room_resources.py b/ietf/name/migrations/0022_add_room_resources.py new file mode 100644 index 000000000..d4db287d3 --- /dev/null +++ b/ietf/name/migrations/0022_add_room_resources.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + RoomResourceName = orm['name.RoomResourceName'] + RoomResourceName(slug='project', name='LCD projector', + desc='The room will have a computer projector', + used=True).save() + RoomResourceName(slug='proj2', name='second LCD projector', + desc='The room will have a second computer projector', + used=True).save() + RoomResourceName(slug='meetecho', name='Meetecho Remote Partition Support', + desc='The room will have a meetecho wrangler', + used=True).save() + + + def backwards(self, orm): + pass + + + models = { + 'name.ballotpositionname': { + 'Meta': {'ordering': "['order']", 'object_name': 'BallotPositionName'}, + 'blocking': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.constraintname': { + 'Meta': {'ordering': "['order']", 'object_name': 'ConstraintName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'penalty': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.dbtemplatetypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DBTemplateTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.docrelationshipname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocRelationshipName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'revname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.docremindertypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocReminderTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctagname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTagName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.feedbacktype': { + 'Meta': {'ordering': "['order']", 'object_name': 'FeedbackType'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.groupmilestonestatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupMilestoneStateName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.groupstatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupStateName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.grouptypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.intendedstdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'IntendedStdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.liaisonstatementpurposename': { + 'Meta': {'ordering': "['order']", 'object_name': 'LiaisonStatementPurposeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.meetingtypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'MeetingTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.nomineepositionstate': { + 'Meta': {'ordering': "['order']", 'object_name': 'NomineePositionState'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.rolename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoleName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.roomresourcename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoomResourceName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.sessionstatusname': { + 'Meta': {'ordering': "['order']", 'object_name': 'SessionStatusName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.stdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.streamname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StreamName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.timeslottypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'TimeSlotTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + } + } + + complete_apps = ['name'] diff --git a/ietf/name/models.py b/ietf/name/models.py index 7403256d1..b18e9415b 100644 --- a/ietf/name/models.py +++ b/ietf/name/models.py @@ -31,7 +31,7 @@ class DocRelationshipName(NameModel): """Updates, Replaces, Obsoletes, Reviews, ... The relationship is always recorded in one direction.""" revname = models.CharField(max_length=255) - + class DocTypeName(NameModel): """Draft, Agenda, Minutes, Charter, Discuss, Guideline, Email, Review, Issue, Wiki""" @@ -71,3 +71,5 @@ class DraftSubmissionStateName(NameModel): Previous Version Authors, Awaiting Initial Version Approval, Awaiting Manual Post, Cancelled, Posted""" next_states = models.ManyToManyField('DraftSubmissionStateName', related_name="previous_states", blank=True) +class RoomResourceName(NameModel): + "Room resources: Audio Stream, Meetecho, . . ." diff --git a/ietf/secr/meetings/views.py b/ietf/secr/meetings/views.py index 1fd1cb05e..0e0fa7d1b 100644 --- a/ietf/secr/meetings/views.py +++ b/ietf/secr/meetings/views.py @@ -163,9 +163,14 @@ def send_notification(request, sessions): Room Name: {6} --------------------------------------------- ''' + + requestinguser = None + if request is not None: + requestinguser = request.user.person + group = sessions[0].group to_email = sessions[0].requested_by.role_email('chair').address - cc_list = get_cc_list(group, request.user.person) + cc_list = get_cc_list(group, requestinguser) from_email = ('"IETF Secretariat"','agenda@ietf.org') if sessions.count() == 1: subject = '%s - Requested session has been scheduled for IETF %s' % (group.acronym, sessions[0].meeting.number) diff --git a/ietf/secr/sreq/forms.py b/ietf/secr/sreq/forms.py index 95c4231d5..20c90aad2 100644 --- a/ietf/secr/sreq/forms.py +++ b/ietf/secr/sreq/forms.py @@ -1,6 +1,9 @@ from django import forms from ietf.group.models import Group +from ietf.meeting.models import ResourceAssociation +from django.forms.formsets import formset_factory +from ietf.person.forms import EmailsField import os # ------------------------------------------------- @@ -44,12 +47,25 @@ def join_conflicts(data): class GroupSelectForm(forms.Form): group = forms.ChoiceField() - + def __init__(self,*args,**kwargs): choices = kwargs.pop('choices') super(GroupSelectForm, self).__init__(*args,**kwargs) self.fields['group'].widget.choices = choices +BETHERE_CHOICES = ((False , 'No'), (True , 'Yes')) +# not using the ModelFormset, too complex. +class MustBePresentForm(forms.Form): + from ietf.person.models import Person + + #person = forms.ModelChoiceField(queryset= Person.objects.all(), required=False) + person = EmailsField(required=False) + bethere = forms.ChoiceField(required = False, choices = BETHERE_CHOICES) + # something like this is desired to make pre-existing items read-only + #self.fields['person'].widget.attrs['readonly'] = True + + +MustBePresentFormSet = formset_factory(MustBePresentForm, extra = 1) class SessionForm(forms.Form): num_session = forms.ChoiceField(choices=NUM_SESSION_CHOICES) @@ -65,6 +81,7 @@ class SessionForm(forms.Form): wg_selector2 = forms.ChoiceField(choices=WG_CHOICES,required=False) wg_selector3 = forms.ChoiceField(choices=WG_CHOICES,required=False) third_session = forms.BooleanField(required=False) + resources = forms.MultipleChoiceField(choices=[(x.pk,x.desc) for x in ResourceAssociation.objects.all()], widget=forms.CheckboxSelectMultiple) def __init__(self, *args, **kwargs): super(SessionForm, self).__init__(*args, **kwargs) @@ -85,7 +102,11 @@ class SessionForm(forms.Form): if self.initial and 'length_session3' in self.initial: if self.initial['length_session3'] != '0' and self.initial['length_session3'] != None: self.fields['third_session'].initial = True - + + resources = self.initial['resources'] + resource_choices = [r.pk for r in resources] + self.initial['resources'] = resource_choices + def clean_conflict1(self): conflict = self.cleaned_data['conflict1'] check_conflict(conflict) diff --git a/ietf/secr/sreq/urls.py b/ietf/secr/sreq/urls.py index 88fe5c1f2..32bd3a719 100644 --- a/ietf/secr/sreq/urls.py +++ b/ietf/secr/sreq/urls.py @@ -4,10 +4,12 @@ urlpatterns = patterns('ietf.secr.sreq.views', url(r'^$', 'main', name='sessions'), url(r'^status/$', 'tool_status', name='sessions_tool_status'), url(r'^(?P<acronym>[A-Za-z0-9_\-\+]+)/$', 'view', name='sessions_view'), + url(r'^(?P<num>[A-Za-z0-9_\-\+]+)/(?P<acronym>[A-Za-z0-9_\-\+]+)/$', 'view', name='sessions_view'), url(r'^(?P<acronym>[A-Za-z0-9_\-\+]+)/approve/$', 'approve', name='sessions_approve'), url(r'^(?P<acronym>[A-Za-z0-9_\-\+]+)/cancel/$', 'cancel', name='sessions_cancel'), url(r'^(?P<acronym>[A-Za-z0-9_\-\+]+)/confirm/$', 'confirm', name='sessions_confirm'), url(r'^(?P<acronym>[A-Za-z0-9_\-\+]+)/edit/$', 'edit', name='sessions_edit'), url(r'^(?P<acronym>[A-Za-z0-9_\-\+]+)/new/$', 'new', name='sessions_new'), url(r'^(?P<acronym>[A-Za-z0-9_\-\+]+)/no_session/$', 'no_session', name='sessions_no_session'), + url(r'^(?P<num>[A-Za-z0-9_\-\+]+)/(?P<acronym>[A-Za-z0-9_\-\+]+)/edit/$', 'edit_mtg', name='sessions_edit'), ) diff --git a/ietf/secr/sreq/views.py b/ietf/secr/sreq/views.py index 7ff24576e..c17c184bf 100644 --- a/ietf/secr/sreq/views.py +++ b/ietf/secr/sreq/views.py @@ -14,6 +14,7 @@ from ietf.secr.utils.group import get_my_groups, groups_by_session from ietf.ietfauth.utils import has_role from ietf.utils.mail import send_mail from ietf.meeting.models import Meeting, Session, Constraint +from ietf.meeting.helpers import get_meeting from ietf.group.models import Group, Role from ietf.name.models import SessionStatusName, ConstraintName @@ -44,10 +45,13 @@ def get_initial_session(sessions): This function takes a queryset of sessions ordered by 'id' for consistency. It returns a dictionary to be used as the initial for a legacy session form ''' + initial = {} + if(len(sessions) == 0): + return initial + meeting = sessions[0].meeting group = sessions[0].group conflicts = group.constraint_source_set.filter(meeting=meeting) - initial = {} # even if there are three sessions requested, the old form has 2 in this field initial['num_session'] = sessions.count() if sessions.count() <= 2 else 2 @@ -64,6 +68,7 @@ def get_initial_session(sessions): initial['conflict2'] = ' '.join([ c.target.acronym for c in conflicts.filter(name__slug='conflic2') ]) initial['conflict3'] = ' '.join([ c.target.acronym for c in conflicts.filter(name__slug='conflic3') ]) initial['comments'] = sessions[0].comments + initial['resources'] = sessions[0].resources.all() return initial def get_lock_message(): @@ -78,12 +83,6 @@ def get_lock_message(): message = "This application is currently locked." return message -def get_meeting(): - ''' - Function to get the current IETF regular meeting. Simply returns the meeting with the most recent date - ''' - return Meeting.objects.filter(type='ietf').order_by('-date')[0] - def get_requester_text(person,group): ''' This function takes a Person object and a Group object and returns the text to use in the @@ -181,7 +180,7 @@ def approve(request, acronym): if has_role(request.user,'Secretariat') or group.parent.role_set.filter(name='ad',person=request.user.person): session.status = SessionStatusName.objects.get(slug='appr') - session.save() + session_save(session) messages.success(request, 'Third session approved') return redirect('sessions_view', acronym=acronym) @@ -212,7 +211,7 @@ def cancel(request, acronym): # mark sessions as deleted for session in sessions: session.status_id = 'deleted' - session.save() + session_save(session) # clear schedule assignments if already scheduled session.scheduledsession_set.all().delete() @@ -271,7 +270,7 @@ def confirm(request, acronym): requested_duration=datetime.timedelta(0,int(duration)), comments=form['comments'], status=SessionStatusName.objects.get(slug=slug)) - new_session.save() + session_save(new_session) # write constraint records save_conflicts(group,meeting,form['conflict1'],'conflict') @@ -302,12 +301,61 @@ def confirm(request, acronym): RequestContext(request, {}), ) +def make_essential_person(pk, person, required): + essential_person = dict() + essential_person["person"] = person.email_set.all()[0].pk + essential_person["bethere"] = required + return essential_person + +def make_bepresent_formset(group, session, default=True): + dict_of_essential_people = {} + + for x in session.people_constraints.all(): + #print "add db: %u %s" % (x.person.pk, x.person) + dict_of_essential_people[x.person.pk] = make_essential_person(x.person.pk, x.person, True) + + # now, add the co-chairs if they were not already present + chairs = group.role_set.filter(name='chair') + for chairrole in chairs: + chair = chairrole.person + if not chair.pk in dict_of_essential_people: + #print "add chair: %u" % (chair.pk) + dict_of_essential_people[chair.pk] = make_essential_person(chair.pk, chair, default) + + # add the responsible AD + if not group.ad.pk in dict_of_essential_people: + #print "add ad: %u" % (chair.pk) + dict_of_essential_people[group.ad.pk] = make_essential_person(group.ad.pk, group.ad, default) + + # make the form set of these people + list_of_essential_people = [] + for k,x in dict_of_essential_people.iteritems(): + #print "k: %s x: %s" % (k,x) + list_of_essential_people.append(x) + + list_of_essential_people.reverse() + #for t in list_of_essential_people: + # print "t: %s" % (t) + + formset = MustBePresentFormSet(initial=list_of_essential_people) + return formset + @check_permissions def edit(request, acronym): + return edit_mtg(request, None, acronym) + +def session_save(session): + session.save() + if session.status_id == "schedw" and session.meeting.agenda != None: + # send an email to iesg-secretariat to alert to change + pass + +@check_permissions +def edit_mtg(request, num, acronym): ''' This view allows the user to edit details of the session request ''' - meeting = get_meeting() + meeting = get_meeting(num) group = get_object_or_404(Group, acronym=acronym) sessions = Session.objects.filter(meeting=meeting,group=group).order_by('id') sessions_count = sessions.count() @@ -315,13 +363,18 @@ def edit(request, acronym): session_conflicts = session_conflicts_as_string(group, meeting) login = request.user.person + session = Session() + if(len(sessions) > 0): + session = sessions[0] + if request.method == 'POST': button_text = request.POST.get('submit', '') if button_text == 'Cancel': return redirect('sessions_view', acronym=acronym) form = SessionForm(request.POST,initial=initial) - if form.is_valid(): + bepresent_formset = MustBePresentFormSet(request.POST) + if form.is_valid() or bepresent_formset.is_valid(): if form.has_changed(): # might be cleaner to simply delete and rewrite all records (but maintain submitter?) # adjust duration or add sessions @@ -329,7 +382,7 @@ def edit(request, acronym): if 'length_session1' in form.changed_data: session = sessions[0] session.requested_duration = datetime.timedelta(0,int(form.cleaned_data['length_session1'])) - session.save() + session_save(session) # session 2 if 'length_session2' in form.changed_data: @@ -351,7 +404,7 @@ def edit(request, acronym): duration = datetime.timedelta(0,int(form.cleaned_data['length_session2'])) session = sessions[1] session.requested_duration = duration - session.save() + session_save(session) # session 3 if 'length_session3' in form.changed_data: @@ -373,7 +426,7 @@ def edit(request, acronym): duration = datetime.timedelta(0,int(form.cleaned_data['length_session3'])) session = sessions[2] session.requested_duration = duration - session.save() + session_save(session) if 'attendees' in form.changed_data: @@ -390,6 +443,12 @@ def edit(request, acronym): Constraint.objects.filter(meeting=meeting,source=group,name='conflic3').delete() save_conflicts(group,meeting,form.cleaned_data['conflict3'],'conflic3') + if 'resources' in form.changed_data: + new_resource_ids = form.cleaned_data['resources'] + new_resources = [ ResourceAssociation.objects.get(pk=a) + for a in new_resource_ids] + session.resources = new_resources + # deprecated # log activity #add_session_activity(group,'Session Request was updated',meeting,user) @@ -397,16 +456,46 @@ def edit(request, acronym): # send notification send_notification(group,meeting,login,form.cleaned_data,'update') + for bepresent in bepresent_formset.forms: + if bepresent.is_valid() and 'person' in bepresent.cleaned_data: + persons_cleaned = bepresent.cleaned_data['person'] + if(len(persons_cleaned) == 0): + continue + + person = bepresent.cleaned_data['person'][0].person + if 'bethere' in bepresent.changed_data and bepresent.cleaned_data['bethere']=='True': + #print "Maybe adding bethere constraint for %s" % (person) + if session.people_constraints.filter(person = person).count()==0: + # need to create new constraint. + #print " yes" + nc = session.people_constraints.create(person = person, + meeting = meeting, + name_id = 'bethere', + source = session.group) + nc.save() + else: + #print "Maybe deleting bethere constraint for %s" % (person) + if session.people_constraints.filter(person = person).count() > 0: + #print " yes" + session.people_constraints.filter(person = person).delete() + + # nuke any cache that might be lingering around. + from ietf.meeting.helpers import session_constraint_expire + session_constraint_expire(session) + messages.success(request, 'Session Request updated') return redirect('sessions_view', acronym=acronym) else: form = SessionForm(initial=initial) + bepresent_formset = make_bepresent_formset(group, session, False) + return render_to_response('sreq/edit.html', { 'meeting': meeting, 'form': form, 'group': group, + 'bepresent_formset' : bepresent_formset, 'session_conflicts': session_conflicts}, RequestContext(request, {}), ) @@ -546,7 +635,7 @@ def no_session(request, acronym): requested_by=login, requested_duration=0, status=SessionStatusName.objects.get(slug='notmeet')) - session.save() + session_save(session) # send notification to_email = SESSION_REQUEST_EMAIL @@ -610,11 +699,11 @@ def tool_status(request): RequestContext(request, {}), ) -def view(request, acronym): +def view(request, acronym, num = None): ''' This view displays the session request info ''' - meeting = get_meeting() + meeting = get_meeting(num) group = get_object_or_404(Group, acronym=acronym) sessions = Session.objects.filter(~Q(status__in=('canceled','notmeet','deleted')),meeting=meeting,group=group).order_by('id') diff --git a/ietf/secr/templates/includes/sessions_request_form.html b/ietf/secr/templates/includes/sessions_request_form.html index 629acc3e3..ab85655bb 100755 --- a/ietf/secr/templates/includes/sessions_request_form.html +++ b/ietf/secr/templates/includes/sessions_request_form.html @@ -1,4 +1,4 @@ - <span class="required">*</span> Required Field + <span class="required">*</span> Required Field <form id="session-request-form" action="." method="post" name="form_post">{% csrf_token %} {% if form.non_field_errors %}{{ form.non_field_errors }}{% endif %} <table id="sessions-new-table" cellspacing="1" cellpadding="1" border="0"> @@ -50,12 +50,38 @@ </table> </td> </tr> - <tr bgcolor="#cccccc"> + <tr class="bg1"><td>People who need to be present:</td> + <td> + {{ bepresent_formset.management_form }} + <table style="width: 100%;"> + <tr> + <th style="width: 70%;">Person</th> + <th>Must be present?</th> + </tr> + {% for essential_person_form in bepresent_formset.forms %} + <tr class="{% cycle 'bg3' 'bg2'%}"> + <td>{{ essential_person_form.person }}</td> + <td>{{ essential_person_form.bethere }}</td> + </tr> + {% endfor %} + </table> + </td> + </tr> + <tr class="bg2"><td>Resources requested:</td> + <td> + {{ form.resources }} + </td> + </tr> + <tr class="bg1"> <td valign="top">Special Requests:<br /> <br />i.e. Meetecho, WebEx (please state which, and the reason needed), restrictions on meeting times / days, etc.</td> <td>{{ form.comments.errors }}{{ form.comments }}</td> </tr> </table> - + {% include "includes/buttons_save_cancel.html" %} - + </form> +{% block content_end %} +<script type="text/javascript" src="/js/lib/jquery.tokeninput.js"></script> +<script type="text/javascript" src="/js/tokenized-field.js"></script> +{% endblock %} diff --git a/ietf/secr/templates/includes/sessions_request_view.html b/ietf/secr/templates/includes/sessions_request_view.html index 5c761cc34..44614564c 100644 --- a/ietf/secr/templates/includes/sessions_request_view.html +++ b/ietf/secr/templates/includes/sessions_request_view.html @@ -26,7 +26,11 @@ <td>Other WGs that included {{ group }} in their conflict list:</td> <td>{% if session_conflicts %}{{ session_conflicts }}{% else %}<i>None so far</i>{% endif %}</td> </tr> + <tr class="row2"> + <td>Resources requested:</td> + <td>{% if session.resources %}<ul>{% for resource in session.resources %}<li>{{ resource.desc }}{% endfor %}</ul>{% else %}<i>None so far</i>{% endif %}</td> + </tr> {% autoescape off %} - <tr class="row2"><td>Special Requests:</td><td>{{ session.comments }}</td></tr> + <tr class="row1"><td>Special Requests:</td><td>{{ session.comments }}</td></tr> {% endautoescape %} </table> diff --git a/ietf/secr/templates/sreq/main.html b/ietf/secr/templates/sreq/main.html index 79e5a3206..e095c2cb8 100755 --- a/ietf/secr/templates/sreq/main.html +++ b/ietf/secr/templates/sreq/main.html @@ -14,7 +14,7 @@ {% endblock %} {% block content %} -<p>» <a href="http://datatracker.ietf.org/meeting/requests">View list of timeslot requests</a></p> +<p>» <a href="/meeting/requests">View list of timeslot requests</a></p> <div class="module interim-container"> <h2> Sessions Request Tool: IETF {{ meeting.number }} diff --git a/ietf/secr/templates/sreq/view.html b/ietf/secr/templates/sreq/view.html index 56c783f02..4c4fdf97b 100644 --- a/ietf/secr/templates/sreq/view.html +++ b/ietf/secr/templates/sreq/view.html @@ -18,8 +18,8 @@ {% block content %} <div class="module interim-container"> - <h2>Sessions - View</h2> - + <h2>Sessions - View (meeting: {{ meeting.number }})</h2> + {% include "includes/sessions_request_view.html" %} <br> diff --git a/ietf/settings.py b/ietf/settings.py index 30fc92fb3..63b11566d 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -383,6 +383,30 @@ USE_ETAGS=True PRODUCTION_TIMEZONE = "America/Los_Angeles" +# Automatic Scheduling +# +# how much to login while running, bigger numbers make it more verbose. +BADNESS_CALC_LOG = 0 +# +# these penalties affect the calculation of how bad the assignments are. +BADNESS_UNPLACED = 1000000 + +# following four are used only during migrations to setup up ConstraintName +# and penalties are taken from the database afterwards. +BADNESS_BETHERE = 200000 +BADNESS_CONFLICT_1 = 100000 +BADNESS_CONFLICT_2 = 10000 +BADNESS_CONFLICT_3 = 1000 + +BADNESS_TOOSMALL_50 = 5000 +BADNESS_TOOSMALL_100 = 50000 +BADNESS_TOOBIG = 100 +BADNESS_MUCHTOOBIG = 500 + +# do not run SELENIUM tests by default +SELENIUM_TESTS = False +SELENIUM_TESTS_ONLY = False + # Put SECRET_KEY in here, or any other sensitive or site-specific # changes. DO NOT commit settings_local.py to svn. from settings_local import * diff --git a/ietf/templates/meeting/agenda-utc.html b/ietf/templates/meeting/agenda-utc.html index 98c9295d0..7dd300b74 100644 --- a/ietf/templates/meeting/agenda-utc.html +++ b/ietf/templates/meeting/agenda-utc.html @@ -121,7 +121,7 @@ You can customize the agenda below to show only selected working group sessions. {% for wg in schedule.groups %}{% ifchanged wg.parent.acronym %}{% if forloop.counter > 1 %} </td>{% endif %} <td valign="top" id="{{wg.parent.acronym|upper}}-groups">{% endifchanged %} - <div id='selector-{{wg.acronym}}' class="unselected" onclick="toggle(this)">{% if wg.state.name = "BOF" %}<i>{{wg.acronym}}</i>{% else %}{{wg.acronym}}{% endif %}</div>{% endfor %} + <div id='selector-{{wg.acronym}}' class="unselected" onclick="toggle(this)">{% if wg.is_bof %}<i>{{wg.acronym}}</i>{% else %}{{wg.acronym}}{% endif %}</div>{% endfor %} </td> </tr> <tr><td align="center" colspan="{{schedule.area_list|length}}"> @@ -182,7 +182,7 @@ You can customize the agenda below to show only selected working group sessions. </td> <td colspan="5"> {{item.timeslot.name}} - - + - {% if item.timeslot.show_location %}<a href="http://tools.ietf.org/agenda/{{schedule.meeting.number}}/venue/?room={{ item.timeslot.get_location|slugify }}">{{item.timeslot.get_location}}</a>{% endif %} </td> </tr> @@ -200,7 +200,7 @@ You can customize the agenda below to show only selected working group sessions. <img src="/images/color-palette-4x4.gif" alt="" onclick="pickAgendaColor('{{schedule.meeting.number}}-{{item.timeslot.time|date:"D-Hi"|lower}}-{{item.session.group.parent.acronym|upper}}-{{item.session.group.acronym|lower}}',this);" title="color tag this line"/ class="noprint"> {% if item.session.agenda %}<a href="/meeting/{{ schedule.meeting.number }}/agenda/{{ item.session.group.acronym }}/">{{item.session.group.name}}</a> {% else %}{{item.session.group.name}}{% endif %} - {% if item.session.group.state.name = "BOF" %} BOF {% endif %} + {% if item.session.is_bof %} BOF {% endif %} {% if item.session.agenda_note %} <br/><span class="note">{{item.session.agenda_note}}</span>{% endif %}</td> <td class="materials">{% if item.session.agenda %}drafts: <a href="/meeting/{{schedule.meeting.number}}/agenda/{{item.session.group.acronym}}-drafts.tgz">tar</a>|<a href="/meeting/{{ schedule.meeting.number }}/agenda/{{item.session.group.acronym}}-drafts.pdf">pdf</a>{%endif%}</td> diff --git a/ietf/templates/meeting/landscape_edit.html b/ietf/templates/meeting/landscape_edit.html index e2c296c29..f8b4d3833 100644 --- a/ietf/templates/meeting/landscape_edit.html +++ b/ietf/templates/meeting/landscape_edit.html @@ -30,7 +30,6 @@ <script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/minified/jquery.ui.sortable.min.js'></script> <script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/minified/jquery.ui.accordion.min.js'></script> <script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/minified/jquery.ui.draggable.min.js'></script> -<!-- <script type='text/javascript' src='/js/jquery-ui-1.8.11.custom.min.js'></script> --> {% dajaxice_js_import %} @@ -44,14 +43,17 @@ <script type='text/javascript'> -meeting_number = {{ meeting.number }}; -schedule_id = {{ schedule.id }}; +var meeting_number = "{{ meeting.number }}"; +var schedule_id = {{ schedule.id }}; +var schedule_owner_href = "{{ schedule.owner_email }}"; +var schedule_name = "{{ schedule.name }}"; var meeting_base_url = "{{ meeting_base_url }}"; var site_base_url = "{{ site_base_url }}"; -total_days = {{time_slices|length}}; -total_rooms = {{rooms|length}}; +var scheduledsession_post_href = "{% url "ietf.meeting.ajax.scheduledsessions_json" meeting.number schedule.name %}"; +var total_days = {{time_slices|length}}; +var total_rooms = {{rooms|length}}; -function setup_slots(){ +function setup_slots(promiselist){ {% for day in time_slices %} days.push("{{day}}"); {% endfor %} @@ -63,40 +65,16 @@ area_directors["{{ad.group.acronym}}"] = []; area_directors["{{ad.group.acronym}}"].push(find_person_by_href("{{ad.person.defurl}}")); {% endfor %} -{% autoescape off %} -{% for s in session_jsons %} - session_obj({{s}}); -{% endfor %} -{% endautoescape %} +var ts_promise = load_timeslots("{% url "ietf.meeting.ajax.timeslot_slotsurl" meeting.number %}"); +var sess_promise = load_sessions("{% url "ietf.meeting.ajax.sessions_json" meeting.number %}"); +promiselist.push(ts_promise); +promiselist.push(sess_promise); -{% for s in scheduledsessions %} - make_ss({ "scheduledsession_id": "{{s.id}}", - "empty": "{{s.empty_str}}", - "timeslot_id":"{{s.timeslot.id}}", - "session_id" :"{{s.session.id}}", - "pinned" :"{{s.pinned}}", - {% if s.session %}{% if s.extendedfrom %}"extendedfrom_id":"{{s.extendedfrom.id}}", - {% endif %}{% endif %}"room" :"{{s.timeslot.location.name|slugify}}", - {% if s.slot_to_the_right %}"following_timeslot_id":"{{s.slot_to_the_right.id}}", - {% endif %}"roomtype" :"{{s.slottype}}", - "time" :"{{s.timeslot.time|date:'Hi' }}", - "date" :"{{s.timeslot.time|date:'Y-m-d'}}", - "domid" :"{{s.timeslot.js_identifier}}"}); -{% endfor %} - - make_ss({ "scheduledsession_id": 0, - "empty": true, - "timeslot_id":"Unassigned", - "session_id" :null, - "room" :"unassigned", - "time" :null, - "date" :null, - "domid" :"sortable-list"}); +var ss_promise = load_scheduledsessions(ts_promise, sess_promise, scheduledsession_post_href); +promiselist.push(ss_promise); console.log("setup_slots run"); - calculate_room_select_box(); - {% for area in area_list %} legend_status["{{area.upcase_acronym}}"] = true; {% endfor %} @@ -165,7 +143,6 @@ area_directors["{{ad.group.acronym}}"].push(find_person_by_href("{{ad.person.def <div class="agenda_div"> - <div id="dialog-confirm" title="" style="display:none"> <p> <span class="ui-icon ui-icon-alert" style="background: white; float: left; margin: 0 7px 20px 0;"></span> @@ -191,135 +168,6 @@ area_directors["{{ad.group.acronym}}"].push(find_person_by_href("{{ad.person.def </p> </div> -<table id="meetings" class="ietf-navbar" style="width:100%"> -<tr> - <th class="schedule_title"><div id="spinner"><!-- spinney goes here --></div></th> -{% for day in time_slices %} - <th colspan="{{date_slices|colWidth:day}}" id="{{day|date:'Y-m-d'}}-btn" class=" day_{{day}} agenda_slot_title agenda_slot_unavailable"> - <div id="close_{{day|date:'Y-m-d'}}" class="close top_left very_small close_day">x</div> - {{day|date:'D'}} ({{day}}) - - </th> - <th class="day_{{day}} spacer {{day|date:'Y-m-d'}}-spacer" id=""> - <div class="ui-widget-content ui-resizable" id="resize-{{day|date:'Y-m-d'}}-spacer"> - <div class="spacer_grip ui-resizable-handle ui-resizable-e"></div> - </div> -</th> -{% endfor %} -</tr> - <tr> - <th class="th_column"><button id="show_all_button" class="styled_button">show all</button></th> - {% for day in time_slices %} - {% for slot in date_slices|lookup:day %} - <th class="day_{{day}}-{{slot.0|date:'Hi'}} day_{{day}} room_title ">{{slot.0|date:'Hi'}}-{{slot.1|date:'Hi'}} </th> - {% endfor %} - - <th class="day_{{day}} spacer {{day|date:'Y-m-d'}}-spacer"></th> - {% endfor %} - - - - - - {% for r in rooms %} - <tr id="{{r.name|to_acceptable_id}}" class="{% cycle 'agenda_row_alt' '' %} agenda_slot"> - <th class="vert_time"> - <div class="close very_small close_room top_left small_button" id="close_{{r.name|to_acceptable_id}}">X</div> - <div class="right room_name">{{r.name}} <span class="capacity">({{r.capacity}})</span></div> - - <!-- <span class="hide_room light_blue_border">X</span><span class="left">{{r.name}}</span>--> - </th> - {% for day in time_slices %} - {% for slot in date_slices|lookup:day %} - <td id="{{r.name|slugify}}_{{day}}_{{slot.0|date:'Hi'}}" class="day_{{day}} agenda-column-{{day}}-{{slot.0|date:'Hi'}} agenda_slot agenda_slot_unavailable" capacity="{{r.capacity}}" ></td> - {% endfor %} - <td class="day_{{day}} spacer {{day|date:'Y-m-d'}}-spacer"></td> - {% endfor %} - </tr> - {% endfor %} -</table> - - - -</div> - - -<div id="session-info" class="ui-droppable bucket-list room_title"> - <div class="agenda_slot_title"><b>Session Information:</b></div> - - <div class="ss_info_box"> - <div class="ss_info ss_info_left"> - <table> - <tr><td class="ss_info_name_short">Group:</td><td><span id="info_grp"></span> - <!-- <button id="agenda_sreq_button" class="right">Edit Request</button> --></tr> - <tr><td class="ss_info_name_short">Name:</td> <td d="info_name"></td></tr> - <tr><td class="ss_info_name_short">Area:</td> <td><span id="info_area"></span><button id="show_all_area" class="right">Show All</button></td></tr> - </table> - </div> - - <div class="ss_info ss_info_right"> - <table> - <tr><td class="ss_info_name_long">Duration/Capacity:</td><td class="info_split" id="info_duration"></td> <td class="info_split" id="info_capacity"></td></tr> - <tr><td class="ss_info_name_long">Location:</td><td colspan=2 id="info_location"></td></tr> - <tr><td class="ss_info_name_long">Responsible AD:</td><td colspan=2 id="info_responsible"></td></tr> - <tr><td class="ss_info_name_long">Requested By:</td><td colspan=2 id="info_requestedby"></td></tr> - </table> - </div> - - <div id="conflict_table"> - <div id="special_requests">Special Requests</div> - <table> - <tbody id="conflict_table_body"> - <tr class="conflict_list_row"> - <td class="conflict_list_title"> - Group conflicts - </td> - <td id="conflict_group_list"> - <ul> - </ul> - </td> - </tr> - <tr class="conflict_list_row"> - <td class="conflict_list_title"> - <b>be present</b> - </td> - <td id="conflict_people_list"> - <ul> - </ul> - </td> - </tr> - </tbody> - </table> - </div> - <div class="agenda_find_free"><button class="agenda_selected_buttons small_button" id="find_free">Find Free</button></div> - <div class="agenda_double_slot button_disabled"> - <button class="agenda_selected_buttons small_button" disabled - id="double_slot">Extend</button> - </div> - <div id="agenda_pin_slot" class="button_disabled"> - <button class="agenda_selected_buttons small_button" disabled - id="pin_slot">Pin</button> - </div> - <div class="color_legend"> - {% for area in area_list %} - <span class="{{area.upcase_acronym}}-scheme"><input class='color_checkboxes' type="checkbox" id="{{area.upcase_acronym}}" value="{{area.upcase_acronym}}-value" checked>{{area.upcase_acronym}}</span> - {% endfor %} - </div> - </div> - - <div class="agenda_save_box"> - - <div id="agenda_title"><b>Agenda name: </b><span>{{schedule.name}}</span></div> - <div id="agenda_saveas"> - <form action="{{saveasurl}}" method="post">{% csrf_token %} - {{ saveas.as_p }} - <input type="submit" name="saveas" value="saveas"> - </form> - </div> - </div> - -</div> - <!-- some boxes for dialogues --> <div id="dialog-confirm-two" title="" style="display:none"> <p> @@ -349,14 +197,171 @@ area_directors["{{ad.group.acronym}}"].push(find_person_by_href("{{ad.person.def </p> </div> -<div id="can-extend-dialog" title="" class="ui-dialog dialog" style="display:none"> + +<table id="meetings" class="ietf-navbar" style="width:100%"> +<tr> + <th class="schedule_title"><div id="pageloaded" style="display:none">loaded</div><div id="spinner"><!-- spinney goes here --></div></th> + <th></th> +{% for day in time_slices %} + <th colspan="{{date_slices|colWidth:day}}" id="{{day|date:'Y-m-d'}}-btn" class=" day_{{day}} agenda_slot_title agenda_slot_unavailable"> + <div id="close_{{day|date:'Y-m-d'}}" class="close top_left very_small close_day">x</div> + {{day|date:'D'}} ({{day}}) + + </th> + <th class="day_{{day}} spacer {{day|date:'Y-m-d'}}-spacer" id=""> + <div class="ui-widget-content ui-resizable" id="resize-{{day|date:'Y-m-d'}}-spacer"> + <div class="spacer_grip ui-resizable-handle ui-resizable-e"></div> + </div> + </th> +{% endfor %} +</tr> + +<tr> + <th class="th_column"><button id="show_all_button" class="styled_button">show all</button></th> + <th><!-- resources --></th> + {% for day in time_slices %} + {% for slot in date_slices|lookup:day %} + <th class="day_{{day}}-{{slot.0|date:'Hi'}} day_{{day}} room_title ">{{slot.0|date:'Hi'}}-{{slot.1|date:'Hi'}} </th> + + {% endfor %} + <th class="day_{{day}} spacer {{day|date:'Y-m-d'}}-spacer"></th> + + {% endfor %} +</tr> + + + + + {% for r in rooms %} + <tr id="{{r.name|to_acceptable_id}}" class="{% cycle 'agenda_row_alt' '' %} agenda_slot"> + <th class="vert_time"> + <div class="close very_small close_room top_left small_button" id="close_{{r.name|to_acceptable_id}}">X</div> + <div class="right room_name">{{r.name}} <span class="capacity">({{r.capacity}})</span></div> + </th> + <th class="room_features"> + <div class="resource_list"> + {% for resource in r.resources.all %} + <span class="resource_image"> + <img src="/images/{{ resource.icon }}" height=24 alt="{{resource.desc}}" title="{{resource.desc}}"/> + </span> + {% endfor %} + </div> + </th> + {% for day in time_slices %} + {% for slot in date_slices|lookup:day %} + <td id="{{r.domid}}_{{day}}_{{slot.0|date:'Hi'}}" class="day_{{day}} agenda-column-{{day}}-{{slot.0|date:'Hi'}} agenda_slot agenda_slot_unavailable" capacity="{{r.capacity}}" ></td> + {% endfor %} + <td class="day_{{day}} spacer {{day|date:'Y-m-d'}}-spacer"></td> + {% endfor %} + </tr> + {% endfor %} +</table> + + + </div> -<div id="can-not-extend-dialog" title="" class="ui-dialog dialog" style="display:none"> - <p> - <span class="ui-icon ui-icon-alert" style="background: white; float: left; margin: 0 7px 20px 0;"></span> - You can not extend this session. The slot is not available. - </p> + +<div id="session-info" class="ui-droppable bucket-list room_title"> + <div class="agenda_slot_title"><b>Session Information:</b></div> + + <div class="ss_info_box"> + <div class="ss_info ss_info_left"> + <table> + <tr><td class="ss_info_name_short">Group:</td><td><span id="info_grp"></span> + <!-- <button id="agenda_sreq_button" class="right">Edit Request</button> --></tr> + <tr><td class="ss_info_name_short">Name:</td> <td id="info_name"></td></tr> + <tr><td class="ss_info_name_short">Area:</td> <td><span id="info_area"></span> + <button id="show_all_area" class="right">Show All</button></td></tr> + <tr> + <td colspan=2> + <div class="agenda_nice_button" id="agenda_find_free"> + <button class="agenda_selected_buttons small_button" id="find_free">Find Free</button> + </div> + <div class="agenda_nice_button button_disabled" id="agenda_double_slot"> + <button class="agenda_selected_buttons small_button" disabled id="double_slot">Extend</button> + </div> + <div id="agenda_pin_slot" class="agenda_nice_button button_disabled"> + <button class="agenda_selected_buttons small_button" disabled id="pin_slot">Pin</button> + </div> + </td> + </tr> + + </table> + </div> + + <div class="ss_info ss_info_right"> + <table> + <tr><td class="ss_info_name_long">Duration/Capacity:</td> + <td class="info_split"><span id="info_duration"></span> + <span style="right" id="grp_type"></span></td> + <td class="info_split" id="info_capacity"></td></tr> + <tr><td class="ss_info_name_long">Location:</td><td colspan=2 id="info_location"></td></tr> + <tr><td class="ss_info_name_long">Responsible AD:</td><td colspan=2 id="info_responsible"></td></tr> + <tr><td class="ss_info_name_long">Requested By:</td><td colspan=2 id="info_requestedby"></td></tr> + <tr> + <td colspan=3> + <div class="agenda_nice_button button_disabled" id="agenda_prev_session"> + <button class="agenda_selected_buttons small_button" disabled id="prev_session">Prev</button> + </div> + <div class="agenda_nice_button button_disabled" id="agenda_show"> + <button class="agenda_selected_buttons small_button" disabled id="show_session">Show</button> + </div> + <div class="agenda_nice_button button_disabled" id="agenda_next_session"> + <button class="agenda_selected_buttons small_button" disabled id="next_session">Next</button> + </div> + + <div class="request_features" id="agenda_requested_features"> + + </div> + </td> + </tr> + </table> + </div> + + <div id="conflict_table"> + <div id="special_requests">Special Requests</div> + <table> + <tbody id="conflict_table_body"> + <tr class="conflict_list_row"> + <td class="conflict_list_title"> + Group conflicts + </td> + <td id="conflict_group_list"> + <ul> + </ul> + </td> + </tr> + <tr class="conflict_list_row"> + <td class="conflict_list_title"> + <b>be present</b> + </td> + <td id="conflict_people_list"> + <ul> + </ul> + </td> + </tr> + </tbody> + </table> + </div> + <div class="color_legend"> + {% for area in area_list %} + <span class="{{area.upcase_acronym}}-scheme"><input class='color_checkboxes' type="checkbox" id="{{area.upcase_acronym}}" value="{{area.upcase_acronym}}-value" checked>{{area.upcase_acronym}}</span> + {% endfor %} + </div> + </div> + + <div class="agenda_save_box"> + + <div id="agenda_title"><b>Agenda name: </b><span>{{schedule.name}}</span></div> + <div id="agenda_saveas"> + <form action="{{saveasurl}}" method="post">{% csrf_token %} + {{ saveas.as_p }} + <input id="saveasbutton" type="submit" name="saveas" value="saveas"> + </form> + </div> + </div> + </div> {% endblock %} diff --git a/ietf/templates/meeting/requests.html b/ietf/templates/meeting/requests.html index 160a8e743..255a3420b 100644 --- a/ietf/templates/meeting/requests.html +++ b/ietf/templates/meeting/requests.html @@ -45,9 +45,8 @@ th { text-align: right; vertical-align: text-top; } {%ifchanged%} <tr><td class="status" colspan="7"><!-- {{session.group.parent.id}} -->{{session.status}}</td></tr> {%endifchanged%} -yes <tr class="{% if forloop.counter|divisibleby:2 %}even{% else %}odd{% endif %}"> - <th valign="top">{{session.group.acronym|upper}}</th> + <th valign="top"><a href="{% url "ietf.secr.sreq.views.edit_mtg" num=meeting.number acronym=session.group.acronym %}">{{session.group.acronym|upper}}</a></th> <td valign="top" align="center">{% if not session.requested_duration %}<i>{{session.status}}</i>{%else%} {{session.requested_duration|stringformat:"s"|slice:"0:4"}} {% endif %}</td> <td valign="top" align="center">{{session.attendees}}</td> <td valign="top"> diff --git a/ietf/templates/meeting/room_edit.html b/ietf/templates/meeting/room_edit.html new file mode 100644 index 000000000..c63a7a478 --- /dev/null +++ b/ietf/templates/meeting/room_edit.html @@ -0,0 +1,60 @@ +{% extends "base.html" %} +{% load ietf_filters %} +{# Copyright The IETF Trust 2007, All Rights Reserved #} +{% load humanize %} + +{% load dajaxice_templatetags %} + +{% block title %}IETF {{ meeting.number }} Meeting Agenda: Timeslot/Room Availability{% endblock %} +{% load agenda_custom_tags %} +{% block pagehead %} +<link rel='stylesheet' type='text/css' href='/css/jquery-ui-themes/jquery-ui-1.8.11.custom.css' /> +<link rel='stylesheet' type='text/css' href='/css/base2.css' /> +<link rel='stylesheet' type='text/css' href='/css/agenda.css' /> +{% endblock pagehead %} + +{% block js %} +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/jquery-ui.custom.js'></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/jquery.ui.widget.js'></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/jquery.ui.droppable.js'></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/jquery.ui.sortable.js'></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/jquery.ui.accordion.js'></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/jquery.ui.draggable.js'></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/jquery.ui.datetime.js'></script> + +<!-- source (MIT License) http://momentjs.com/ https://github.com/moment/moment/ --> +<script type='text/javascript' src='/js/moment.min.js'></script> + +<!-- source (MIT License) : https://github.com/trentrichardson/jQuery-Timepicker-Addon --> +<script type='text/javascript' src='/js/jquery-ui-timepicker/jquery-ui-timepicker-addon.js'></script> +<script type='text/javascript' src='/js/jquery-ui-timepicker/jquery-ui-sliderAccess.js.js'></script> +<link rel='stylesheet' type='text/css' href='/css/jquery-ui-timepicker-addon.css' /> + +{% dajaxice_js_import %} + +<script type='text/javascript' src='/js/spin/dist/spin.min.js'></script> + +{% endblock js %} + + +{% block start_content_table %}{% endblock %} +{% block end_content_table %}{% endblock %} + +{% block content %} +<div class="wrapper custom_text_stuff"> +<div style="ui-icon ui-icon-arrow-1-w" id="close_ietf_menubar"> +< +</div> + +<div class="room_div"> + +<div id="edit_room_dialog"> + <form action="{{roomsurl}}" method="post">{% csrf_token %} + <table> + {{ editroom.as_table }} + <tr><td><input type="submit" name="editroom" value="editroom"></td></tr> + </table> + </form> +</div> + +{% endblock %} diff --git a/ietf/templates/meeting/timeslot_edit.html b/ietf/templates/meeting/timeslot_edit.html index 2697cd672..591b3271c 100644 --- a/ietf/templates/meeting/timeslot_edit.html +++ b/ietf/templates/meeting/timeslot_edit.html @@ -20,17 +20,22 @@ <script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/jquery.ui.sortable.js'></script> <script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/jquery.ui.accordion.js'></script> <script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/jquery.ui.draggable.js'></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/jquery.ui.datetime.js'></script> -{% if server_mode and server_mode != "production" %} - <script src="https://towtruck.mozillalabs.com/towtruck.js"></script> -{% endif %} +<!-- source (MIT License) http://momentjs.com/ https://github.com/moment/moment/ --> +<script type='text/javascript' src='/js/moment.min.js'></script> + +<!-- source (MIT License) : https://github.com/trentrichardson/jQuery-Timepicker-Addon --> +<script type='text/javascript' src='/js/jquery-ui-timepicker/jquery-ui-timepicker-addon.js'></script> +<script type='text/javascript' src='/js/jquery-ui-timepicker/jquery-ui-sliderAccess.js.js'></script> +<link rel='stylesheet' type='text/css' href='/css/jquery-ui-timepicker-addon.css' /> {% dajaxice_js_import %} <script type='text/javascript' src='/js/spin/dist/spin.min.js'></script> <script type='text/javascript' src='/js/agenda/timeslot_edit.js'></script> -<script type='text/javascript' src='/js/agenda/agenda_helpers.js'></script> <script type='text/javascript' src='/js/agenda/agenda_objects.js'></script> +<script type='text/javascript' src='/js/agenda/agenda_helpers.js'></script> <script type='text/javascript' src='/js/agenda/agenda_listeners.js'></script> @@ -41,29 +46,25 @@ function my_js_callback(data){ alert(data.message); }<!-- dajaxice example --> {% endcomment %} +var meeting_number = "{{ meeting.number }}"; var meeting_base_url = "{{ meeting_base_url }}"; var site_base_url = "{{ site_base_url }}"; total_days = {{time_slices|length}}; total_rooms = {{rooms|length}}; -function setup_slots(){ -{% for ts in timeslots %} - make_ss({ - "timeslot_id":"{{ts.id}}", - "room" :"{{ts.location|slugify}}", - "roomtype" :"{{ts.type.slug}}", - "time" :"{{ts.time|date:'Hi' }}", - "date" :"{{ts.time|date:'Y-m-d'}}", - "domid" :"{{ts.js_identifier}}"}); +first_day = new Date("{% with timeslots|first as day %} {{ day.time }} {% endwith %}"); /* needed for the datepicker */ + +function setup_slots(promiselist){ +var ts_promise = load_timeslots("{% url "ietf.meeting.ajax.timeslot_slotsurl" meeting.number %}"); +promiselist.push(ts_promise); + +{% for day in time_slices %} + days.push("{{day}}"); {% endfor %} - console.log("setup_slots run"); } </script> -<style type='text/css'> - -</style> {% endblock js %} @@ -79,9 +80,10 @@ function setup_slots(){ <div class="agenda_div"> <table id="meetings" class="ietf-navbar" style="width:100%"> - <th class="schedule_title"><span id="schedule_name">name: {{meeting.number}}</span> + <th class="schedule_title"><div id="pageloaded" style="display:none"><span id="schedule_name">name: {{meeting.number}}</span></div> <div id="spinner"><!-- spinney goes here --></div> </th> + <th><!-- resources --></th> {% for day in time_slices %} <th colspan="{{date_slices|colWidth:day}}" id="{{day|date:'Y-m-d'}}-btn" class=" day_{{day}} agenda_slot_title"> <div style="display: none;" class="delete delete_day bottom_left" id="delete_{{day|date:'Y-m-d'}}">X</div> @@ -99,6 +101,7 @@ function setup_slots(){ <div class="addbutton" id="add_room">+ROOM</div> <div class="addbutton" id="add_day">+DAY</div> </th> + <th><!-- resources --></th> {% for day in time_slices %} {% for slot in slot_slices|lookup:day %} <th class="day_{{day}} room_title"> @@ -117,18 +120,28 @@ function setup_slots(){ {% endfor %} {% for r in rooms %} - <tr id="{{r|to_acceptable_id}}" class="agenda_slot"> + <tr id="{{r.name|to_acceptable_id}}" class="agenda_slot_row"> <th class="vert_time"> <div class="delete delete_room bottom_left" - id="delete_{{r|to_acceptable_id}}" + id="delete_{{r.name|to_acceptable_id}}" href="{{r.json_url}}" roomid="{{r.pk}}">X</div> - <div class="right room_name">{{r}} <span class="capacity">({{r.capacity}})</span></div> + <div class="right room_name"><a class="edit_room editlink" + href="/meeting/{{ meeting.number }}/room/{{r.pk}}.html" >{{r.name}} <span class="capacity">({{r.capacity}})</span></a></div> - <!-- <span class="hide_room light_blue_border">X</span><span class="left">{{r}}</span></th> --> + <!-- <span class="hide_room light_blue_border">X</span><span class="left">{{r.name}}</span></th> --> + <th class="room_features"> + <div class="resource_list"> + {% for resource in r.resources.all %} + <span class="resource_image"> + <img src="/images/{{ resource.icon }}" height=24 alt="{{resource.desc}}" title="{{resource.desc}}"/> + </span> + {% endfor %} + </div> + </th> {% for day in time_slices %} {% for slot in date_slices|lookup:day %} - <td id="{{r|slugify}}_{{day}}_{{slot.0|date:'Hi'}}" class="day_{{day}} agenda-column-{{day}}-{{slot.0|date:'Hi'}} agenda_slot {% cycle 'agenda_slot_alt' '' %} agenda_slot_unavailable" ></td> + <td slot_time="{{day}} {{slot.0|date:'H:i:s'}}" slot_duration="{{slot.2}}" slot_room="{{r.pk}}" id="{{r.domid}}_{{day}}_{{slot.0|date:'Hi'}}" class="day_{{day}} agenda-column-{{day}}-{{slot.0|date:'Hi'}} agenda_slot {% cycle 'agenda_slot_alt' '' %} agenda_slot_unavailable" ></td> {% endfor %} <td class="day_{{day}} spacer {{day|date:'Y-m-d'}}-spacer"></td> {% endfor %} @@ -148,7 +161,9 @@ function setup_slots(){ <div class="dialog" id="add_day_dialog"> <table> <form action="{{adddayurl}}" method="post">{% csrf_token %} - {{ addday.as_table }} + {{ addday }} + <tr><th><label>Duration</label></th><td><input type="text" id="duration_time"></td></tr> + <tr><th></th><td><div id="timespan"></div></td></tr> <tr><td><input type="submit" name="addday" value="addday"></td></tr> </form> </table> diff --git a/static/css/agenda.css b/static/css/agenda.css index 388962c39..d24213b72 100644 --- a/static/css/agenda.css +++ b/static/css/agenda.css @@ -251,8 +251,9 @@ div.agenda_slot{ #session-info{ - min-height: 150px; + min-height: 160px; position: relative; + width:100%; } .ss_info { @@ -315,6 +316,18 @@ div.agenda_slot{ } +.wg_style { + font-style:normal; +} + +.bof_style { + font-style:italic; +} +#grp_type { + display: inline; + float: right; +} + .ss_info tr{ /* border-right: 1px solid #89D; */ /* border-top: 1px solid #89D; */ @@ -354,36 +367,36 @@ td.ss_info_name_short{ min-width:50px; } -.agenda_find_free{ +.agenda_nice_button { border: 1px solid #89D; background-color: #EDF5FF; padding: 5px; max-width: 150px; float:left; - margin-top:70px; - margin-left:3px; + margin-top: 5px; + margin-bottom: 10px; + margin-left:10px; +} + +.agenda_find_free{ } .agenda_double_slot { - border: 1px solid #89D; - background-color: #EDF5FF; - padding: 5px; - max-width: 150px; - float:left; - margin-top:70px; - margin-left:3px; } #agenda_pin_slot { - border: 1px solid #89D; - background-color: #EDF5FF; - padding: 5px; - max-width: 150px; - float:left; - margin-top:70px; - margin-left:3px; } +#agenda_prev_session { +} + +#agenda_show { +} + +#agenda_next_session { +} + + .button_disabled { color: #EDF4FF; text-color: #EDF4FF; @@ -1045,3 +1058,47 @@ div.line{ padding-top: 0px; padding-bottom: 0px; } + + +.ui-resizable-s { +height:6px; +/* background-color:aqua; */ +/* background: #99beff; /\* Old browsers *\/ */ +/* background: -moz-linear-gradient(top, #99beff 0%, #1e5799 100%); /\* FF3.6+ *\/ */ + +/* expect error on firefox */ +background: -webkit-gradient(linear, top, bottom, color-stop(0%,#99beff), color-stop(100%,#1e5799)); /* Chrome,Safari4+ */ +background: -webkit-linear-gradient(top, #99beff 0%,#1e5799 100%); /* Chrome10+,Safari5.1+ */ +/* background: -o-linear-gradient(left, #99beff 0%,#1e5799 100%); /\* Opera 11.10+ *\/ */ +/* background: -ms-linear-gradient(left, #99beff 0%,#1e5799 100%); /\* IE10+ *\/ */ +/* background: linear-gradient(to right, #99beff 0%,#1e5799 100%); /\* W3C *\/ */ + +/* expect error on firefox */ +/* filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#99beff', endColorstr='#1e5799',GradientType=1 ); /\* IE6-9 *\/ */ + +} + +.resource_image { + float: left; +} +.resource_list { + width: 75px; +} +.room_features { + background-color:#2647A0; +} +.agenda_requested_feature { + float: right; +} + +/* copied from doc.css, maybe with some modifications. */ +a.editlink { + background-image: url("/images/pencil.png"); + background-size: 10px; + background-position: right top; + background-attachment: scroll; + background-repeat: no-repeat; + padding-right: 12px; +} +a.editlink:link {text-decoration:none; color:inherit;} +a.editlink:visited {text-decoration:none; color:inherit;} diff --git a/static/css/jquery-ui-timepicker-addon.css b/static/css/jquery-ui-timepicker-addon.css new file mode 100644 index 000000000..da12d9833 --- /dev/null +++ b/static/css/jquery-ui-timepicker-addon.css @@ -0,0 +1,11 @@ +.ui-timepicker-div .ui-widget-header { margin-bottom: 8px; } +.ui-timepicker-div dl { text-align: left; } +.ui-timepicker-div dl dt { float: left; clear:left; padding: 0 0 0 5px; } +.ui-timepicker-div dl dd { margin: 0 10px 10px 40%; } +.ui-timepicker-div td { font-size: 90%; } +.ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; } + +.ui-timepicker-rtl{ direction: rtl; } +.ui-timepicker-rtl dl { text-align: right; padding: 0 5px 0 0; } +.ui-timepicker-rtl dl dt{ float: right; clear: right; } +.ui-timepicker-rtl dl dd { margin: 0 40% 10px 10px; } \ No newline at end of file diff --git a/static/css/lib/qunit-1.12.0.css b/static/css/lib/qunit-1.12.0.css new file mode 100644 index 000000000..7ba3f9a30 --- /dev/null +++ b/static/css/lib/qunit-1.12.0.css @@ -0,0 +1,244 @@ +/** + * QUnit v1.12.0 - A JavaScript Unit Testing Framework + * + * http://qunitjs.com + * + * Copyright 2012 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + +/** Font Family and Sizes */ + +#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { + font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; +} + +#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } +#qunit-tests { font-size: smaller; } + + +/** Resets */ + +#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { + margin: 0; + padding: 0; +} + + +/** Header */ + +#qunit-header { + padding: 0.5em 0 0.5em 1em; + + color: #8699a4; + background-color: #0d3349; + + font-size: 1.5em; + line-height: 1em; + font-weight: normal; + + border-radius: 5px 5px 0 0; + -moz-border-radius: 5px 5px 0 0; + -webkit-border-top-right-radius: 5px; + -webkit-border-top-left-radius: 5px; +} + +#qunit-header a { + text-decoration: none; + color: #c2ccd1; +} + +#qunit-header a:hover, +#qunit-header a:focus { + color: #fff; +} + +#qunit-testrunner-toolbar label { + display: inline-block; + padding: 0 .5em 0 .1em; +} + +#qunit-banner { + height: 5px; +} + +#qunit-testrunner-toolbar { + padding: 0.5em 0 0.5em 2em; + color: #5E740B; + background-color: #eee; + overflow: hidden; +} + +#qunit-userAgent { + padding: 0.5em 0 0.5em 2.5em; + background-color: #2b81af; + color: #fff; + text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; +} + +#qunit-modulefilter-container { + float: right; +} + +/** Tests: Pass/Fail */ + +#qunit-tests { + list-style-position: inside; +} + +#qunit-tests li { + padding: 0.4em 0.5em 0.4em 2.5em; + border-bottom: 1px solid #fff; + list-style-position: inside; +} + +#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { + display: none; +} + +#qunit-tests li strong { + cursor: pointer; +} + +#qunit-tests li a { + padding: 0.5em; + color: #c2ccd1; + text-decoration: none; +} +#qunit-tests li a:hover, +#qunit-tests li a:focus { + color: #000; +} + +#qunit-tests li .runtime { + float: right; + font-size: smaller; +} + +.qunit-assert-list { + margin-top: 0.5em; + padding: 0.5em; + + background-color: #fff; + + border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; +} + +.qunit-collapsed { + display: none; +} + +#qunit-tests table { + border-collapse: collapse; + margin-top: .2em; +} + +#qunit-tests th { + text-align: right; + vertical-align: top; + padding: 0 .5em 0 0; +} + +#qunit-tests td { + vertical-align: top; +} + +#qunit-tests pre { + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; +} + +#qunit-tests del { + background-color: #e0f2be; + color: #374e0c; + text-decoration: none; +} + +#qunit-tests ins { + background-color: #ffcaca; + color: #500; + text-decoration: none; +} + +/*** Test Counts */ + +#qunit-tests b.counts { color: black; } +#qunit-tests b.passed { color: #5E740B; } +#qunit-tests b.failed { color: #710909; } + +#qunit-tests li li { + padding: 5px; + background-color: #fff; + border-bottom: none; + list-style-position: inside; +} + +/*** Passing Styles */ + +#qunit-tests li li.pass { + color: #3c510c; + background-color: #fff; + border-left: 10px solid #C6E746; +} + +#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } +#qunit-tests .pass .test-name { color: #366097; } + +#qunit-tests .pass .test-actual, +#qunit-tests .pass .test-expected { color: #999999; } + +#qunit-banner.qunit-pass { background-color: #C6E746; } + +/*** Failing Styles */ + +#qunit-tests li li.fail { + color: #710909; + background-color: #fff; + border-left: 10px solid #EE5757; + white-space: pre; +} + +#qunit-tests > li:last-child { + border-radius: 0 0 5px 5px; + -moz-border-radius: 0 0 5px 5px; + -webkit-border-bottom-right-radius: 5px; + -webkit-border-bottom-left-radius: 5px; +} + +#qunit-tests .fail { color: #000000; background-color: #EE5757; } +#qunit-tests .fail .test-name, +#qunit-tests .fail .module-name { color: #000000; } + +#qunit-tests .fail .test-actual { color: #EE5757; } +#qunit-tests .fail .test-expected { color: green; } + +#qunit-banner.qunit-fail { background-color: #EE5757; } + + +/** Result */ + +#qunit-testresult { + padding: 0.5em 0.5em 0.5em 2.5em; + + color: #2b81af; + background-color: #D2E0E6; + + border-bottom: 1px solid white; +} +#qunit-testresult .module-name { + font-weight: bold; +} + +/** Fixture */ + +#qunit-fixture { + position: absolute; + top: -10000px; + left: -10000px; + width: 1000px; + height: 1000px; +} diff --git a/static/images/1194984425806666249pin_blue_nicu_buculei_01.svg b/static/images/1194984425806666249pin_blue_nicu_buculei_01.svg new file mode 100644 index 000000000..42d962fe8 --- /dev/null +++ b/static/images/1194984425806666249pin_blue_nicu_buculei_01.svg @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" +"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> +<!-- Created with Inkscape (http://www.inkscape.org/) --><svg height="80.000000pt" id="svg1" inkscape:version="0.40" sodipodi:docbase="/home/nicu/Desktop/pins" sodipodi:docname="bluepin.svg" sodipodi:version="0.32" width="80.000000pt" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink"> + <metadata> + <rdf:RDF xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> + <cc:Work rdf:about=""> + <dc:title>pin - blue</dc:title> + <dc:description></dc:description> + <dc:subject> + <rdf:Bag> + <rdf:li>office</rdf:li> + <rdf:li></rdf:li> + </rdf:Bag> + </dc:subject> + <dc:publisher> + <cc:Agent rdf:about="http://www.openclipart.org"> + <dc:title>Nicu Buculei</dc:title> + </cc:Agent> + </dc:publisher> + <dc:creator> + <cc:Agent> + <dc:title>Nicu Buculei</dc:title> + </cc:Agent> + </dc:creator> + <dc:rights> + <cc:Agent> + <dc:title>Nicu Buculei</dc:title> + </cc:Agent> + </dc:rights> + <dc:date></dc:date> + <dc:format>image/svg+xml</dc:format> + <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/> + <cc:license rdf:resource="http://web.resource.org/cc/PublicDomain"/> + <dc:language>en</dc:language> + </cc:Work> + <cc:License rdf:about="http://web.resource.org/cc/PublicDomain"> + <cc:permits rdf:resource="http://web.resource.org/cc/Reproduction"/> + <cc:permits rdf:resource="http://web.resource.org/cc/Distribution"/> + <cc:permits rdf:resource="http://web.resource.org/cc/DerivativeWorks"/> + </cc:License> + </rdf:RDF> + </metadata> + <defs id="defs3"> + <linearGradient id="linearGradient4810"> + <stop id="stop4811" offset="0.0000000" style="stop-color:#ffffff;stop-opacity:0.0000000;"/> + <stop id="stop4812" offset="1.0000000" style="stop-color:#ffffff;stop-opacity:0.68041235;"/> + </linearGradient> + <linearGradient id="linearGradient2931"> + <stop id="stop2932" offset="0.0000000" style="stop-color:#ffffff;stop-opacity:1.0000000;"/> + <stop id="stop2933" offset="1.0000000" style="stop-color:#ffffff;stop-opacity:0.0000000;"/> + </linearGradient> + <linearGradient gradientTransform="scale(1.806421,0.553581)" gradientUnits="userSpaceOnUse" id="linearGradient5443" inkscape:collect="always" x1="145.63139" x2="145.63139" xlink:href="#linearGradient2931" y1="712.78033" y2="772.31848"/> + <radialGradient cx="124.36100" cy="1082.2069" fx="125.14262" fy="1084.0195" gradientTransform="scale(2.175919,0.459576)" gradientUnits="userSpaceOnUse" id="radialGradient5444" inkscape:collect="always" r="54.577564" xlink:href="#linearGradient4810"/> + </defs> + <sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="layer1" inkscape:cx="40.000000" inkscape:cy="40.000000" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="753" inkscape:window-width="958" inkscape:window-x="26" inkscape:window-y="25" inkscape:zoom="7.1625000" pagecolor="#ffffff"/> + <g id="layer1" inkscape:groupmode="layer" inkscape:label="Layer 1"> + <g id="g8582"> + <path d="M 263.75000,501.11218 L 267.50000,707.36218 L 278.75000,761.11218 L 286.25000,706.11218 L 282.50000,502.36218 L 263.75000,501.11218 z " id="path1684" sodipodi:nodetypes="cccccc" style="fill:#9e9d9b;fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:4.8927894;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4.0000000;stroke-opacity:1.0000000;" transform="matrix(0.228884,-0.113495,0.113495,0.228884,-68.50043,-44.07814)"/> + <path d="M 265.37352,538.01031 L 268.16112,707.31019 L 278.15032,752.49956 L 270.68998,537.71983 L 265.37352,538.01031 z " id="path4813" sodipodi:nodetypes="ccccc" style="fill:#ffffff;fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:1.0000000pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1.0000000;" transform="matrix(0.228884,-0.113495,0.113495,0.228884,-68.50043,-44.07814)"/> + <path d="M 47.853127,40.988541 C 56.075476,56.812966 64.270603,72.652676 72.495918,88.474884 C 75.387683,92.150116 83.137425,100.52810 82.532330,99.736721 C 81.066331,95.446317 79.849393,89.204867 78.337002,84.933957 C 70.273127,69.392997 62.377631,54.378767 54.317596,38.835824 C 52.452933,39.609814 49.713423,40.204129 47.853127,40.988541 z M 49.593395,41.622707 C 50.683882,41.168118 51.774370,40.713528 52.864857,40.258938 C 60.765236,55.502991 68.882678,69.332768 76.756478,84.588139 C 77.866557,87.881796 78.976635,92.222575 80.086713,95.516233 C 77.928546,92.728974 75.501521,90.351573 73.402278,87.526326 C 65.465984,72.225120 57.529690,56.923913 49.593395,41.622707 z " id="path6692" sodipodi:nodetypes="cccccccccccc" style="fill:#000000;fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:4.8927894;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4.0000000;stroke-opacity:1.0000000"/> + <path d="M 266.25000,361.26166 C 215.88000,361.26166 175.00000,378.34505 175.00000,399.39423 C 175.00000,403.53049 176.74525,407.46554 179.68750,411.18066 C 181.43767,420.67385 183.55201,434.39840 183.31250,446.85194 C 164.43916,456.32637 152.90625,468.84725 152.90625,482.59255 C 152.90625,512.12465 205.92967,536.08211 271.25000,536.08214 C 336.57032,536.08214 389.59376,512.12466 389.59375,482.59255 C 389.59375,467.81002 376.14852,454.57147 354.62500,444.94531 C 351.71542,434.28130 350.02631,422.82075 351.87500,412.25530 C 355.39117,408.23677 357.50000,403.93193 357.50000,399.39423 C 357.50000,398.81798 357.34188,398.26562 357.28125,397.69559 C 357.36116,397.57026 357.41834,397.43882 357.50000,397.31427 C 357.44366,397.13929 357.27081,396.99722 357.18750,396.82895 C 354.00151,376.98464 314.53966,361.26166 266.25000,361.26166 z " id="path1061" style="fill:#0000d3;fill-opacity:1.0000000;stroke:none;stroke-width:4.8927894;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4.0000000;stroke-opacity:1.0000000;" transform="matrix(0.228884,-0.113495,0.113495,0.228884,-68.50043,-44.07814)"/> + <path d="M 336.25000 417.36218 A 77.500000 23.750000 0 1 1 181.25000,417.36218 A 77.500000 23.750000 0 1 1 336.25000 417.36218 z" id="path2309" sodipodi:cx="258.75000" sodipodi:cy="417.36218" sodipodi:rx="77.500000" sodipodi:ry="23.750000" sodipodi:type="arc" style="fill:url(#linearGradient5443);fill-opacity:1.0000000;stroke:none;stroke-width:2.0521364;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4.0000000;stroke-opacity:1.0000000;" transform="matrix(0.256132,-0.127006,0.153507,0.309576,-92.66068,-79.75977)"/> + <path d="M 348.54157,411.25942 C 326.15065,427.59874 289.69574,431.77062 257.71875,430.84375 C 241.73026,430.38032 226.55820,428.15413 214.25000,424.59375 C 201.94180,421.03337 190.72405,418.18039 186.29912,412.92579 L 184.37500,413.93750 C 189.95007,420.55790 200.09726,425.57601 212.90625,429.28125 C 225.71524,432.98649 241.23849,435.27593 257.59375,435.75000 C 290.30426,436.69813 326.32842,430.50443 350.18750,413.09375 L 348.54157,411.25942 z " id="path2935" sodipodi:nodetypes="ccccccccc" style="fill:#000000;fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:4.8927894;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4.0000000;stroke-opacity:1.0000000;" transform="matrix(0.228884,-0.113495,0.113495,0.228884,-68.50043,-44.07814)"/> + <path d="M 351.45088,447.13671 C 332.11482,464.05577 298.58377,475.22314 267.15625,475.53125 C 235.72873,475.83936 204.38333,466.48027 186.34772,449.64703 L 185.16457,450.48423 C 204.62896,468.65099 234.89627,480.75439 267.21875,480.43750 C 299.54123,480.12061 331.89240,467.07926 352.55634,448.99832 L 351.45088,447.13671 z " id="path3557" sodipodi:nodetypes="ccccccc" style="fill:#000000;fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:4.8927894;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4.0000000;stroke-opacity:1.0000000;" transform="matrix(0.228884,-0.113495,0.113495,0.228884,-68.50043,-44.07814)"/> + <path d="M 157.50000,487.36218 C 204.01668,522.54648 331.42487,527.56909 386.25000,482.36218 C 378.33176,545.59131 185.36558,554.84125 157.50000,487.36218 z " id="path4179" sodipodi:nodetypes="ccc" style="fill:url(#radialGradient5444);fill-opacity:1.0000000;fill-rule:evenodd;stroke:none;stroke-width:1.0000000pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1.0000000;" transform="matrix(0.228884,-0.113495,0.113495,0.228884,-68.50043,-44.07814)"/> + <path d="M 33.318230,7.9252710 C 27.215884,11.044142 21.205920,15.038942 17.459858,20.916861 C 15.909772,23.341527 14.735120,28.452360 19.100117,29.562094 C 21.103833,30.646438 20.541915,32.411847 21.795175,34.234322 C 22.939480,35.958635 24.866038,37.092702 22.791698,38.750178 C 20.825671,41.934310 19.044130,47.092568 21.465680,50.387341 C 24.305049,54.044199 29.350987,54.417249 33.674482,54.296096 C 43.510136,53.782469 52.915078,50.093273 61.188863,44.917575 C 67.017920,41.080696 72.737539,36.400227 75.792011,29.971363 C 77.375191,26.756307 77.463894,22.323064 74.512347,19.846313 C 71.528442,17.299724 67.393066,16.903866 63.619015,16.987519 C 61.976677,14.783236 58.759800,12.489943 59.104992,9.5691842 C 59.411426,7.4278307 58.985334,5.3889748 57.202566,4.3823833 C 53.135243,2.2487783 48.275091,3.0623370 43.984172,3.9816103 C 40.290194,4.8714406 36.720836,6.2420975 33.318230,7.9252710 z M 33.875065,9.0482330 C 39.754580,6.2254347 46.216107,3.9323746 52.827216,4.3959927 C 55.250918,4.5884597 58.492905,5.7149230 58.598104,8.5748892 C 58.028039,10.364457 58.765097,12.064180 59.652578,13.610363 C 60.617852,15.220044 61.833115,16.576598 63.261167,17.777540 C 65.134419,17.993188 66.958374,18.117125 68.836337,18.602079 C 71.983508,19.092749 75.452803,21.186266 75.630017,24.692122 C 75.778639,29.281869 72.721638,33.694705 69.762569,36.904845 C 62.726075,44.072040 53.616500,48.844901 44.055597,51.752455 C 38.282749,53.350356 31.978353,54.163016 26.137532,52.246241 C 23.306342,51.377078 21.003388,48.628354 21.396869,45.566280 C 21.612112,42.644524 23.270903,40.083437 24.838068,37.736859 C 24.837824,36.153761 23.443240,34.843731 22.784323,33.444992 C 21.811946,31.989412 21.065589,30.284732 19.886800,28.996132 C 17.485785,28.162991 16.655344,25.408810 17.688898,23.282068 C 19.887216,18.285800 24.409437,14.813081 28.851983,11.877822 C 30.473423,10.844502 32.152697,9.9025167 33.875065,9.0482330 z " id="path6693" sodipodi:nodetypes="cccccccccccccccccccccccccccccccc" style="fill:#000000;fill-opacity:1.0000000;stroke:none;stroke-width:4.8927894;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4.0000000;stroke-opacity:1.0000000"/> + </g> + </g> +</svg> diff --git a/static/images/12161797831849255690jcartier_board.svg b/static/images/12161797831849255690jcartier_board.svg new file mode 100644 index 000000000..241a822e3 --- /dev/null +++ b/static/images/12161797831849255690jcartier_board.svg @@ -0,0 +1,158 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="30" + height="46" + id="svg7594" + sodipodi:version="0.32" + inkscape:version="0.46" + version="1.0" + sodipodi:docname="hw_board.svg" + inkscape:output_extension="org.inkscape.output.svg.inkscape"> + <defs + id="defs7596" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="14.818182" + inkscape:cx="15.982682" + inkscape:cy="20.30673" + inkscape:document-units="px" + inkscape:current-layer="g11262" + showgrid="false" + inkscape:window-width="1680" + inkscape:window-height="1003" + inkscape:window-x="-4" + inkscape:window-y="-4" + showguides="true" + inkscape:guide-bbox="true" /> + <metadata + id="metadata7599"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title>hw_board</dc:title> + <dc:date>2008-05-15</dc:date> + <dc:creator> + <cc:Agent> + <dc:title>Jean Cartier</dc:title> + </cc:Agent> + </dc:creator> + <cc:license + rdf:resource="http://creativecommons.org/licenses/publicdomain/" /> + <dc:language>fr-FR</dc:language> + <dc:subject> + <rdf:Bag> + <rdf:li>hardware</rdf:li> + <rdf:li>computer</rdf:li> + <rdf:li>desk</rdf:li> + <rdf:li>business</rdf:li> + </rdf:Bag> + </dc:subject> + <dc:contributor> + <cc:Agent> + <dc:title>Jean-Victor Balin (jean.victor.balin@gmail.com)</dc:title> + </cc:Agent> + </dc:contributor> + <dc:description>http://www.jcartier.net</dc:description> + </cc:Work> + <cc:License + rdf:about="http://creativecommons.org/licenses/publicdomain/"> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Reproduction" /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Distribution" /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /> + </cc:License> + </rdf:RDF> + </metadata> + <g + inkscape:label="Calque 1" + inkscape:groupmode="layer" + id="layer1"> + <path + style="opacity:1;fill:#888888;fill-opacity:1;stroke:none;stroke-width:0.69999999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + d="" + id="path8460" /> + <path + style="opacity:1;fill:#888888;fill-opacity:1;stroke:none;stroke-width:0.69999999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + d="" + id="path8462" /> + <path + style="opacity:1;fill:#888888;fill-opacity:1;stroke:none;stroke-width:0.69999999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + d="" + id="path8464" /> + <g + id="g11262" + transform="translate(-279.1135,-276.31506)"> + <g + id="g9079"> + <path + style="fill:#edd400;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + id="path4277" + sodipodi:nodetypes="cccccc" + d="M 305.1135,316.85015 L 281.1135,304.85015 L 281.1135,302.85015 L 281.12856,302.82044 L 305.1135,314.85015 L 305.1135,316.85015 z" /> + <path + style="fill:#c4a000;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + id="path4273" + sodipodi:nodetypes="czzzz" + d="M 281.1135,302.85016 C 281.1135,301.85017 282.1135,301.85015 283.1135,301.85015 C 284.1135,301.85015 307.1135,313.28194 307.1135,313.85015 C 307.1135,314.41836 306.1135,314.85015 305.1135,314.85015 C 304.1135,314.85015 281.1135,303.85015 281.1135,302.85016 z" /> + <path + style="fill:#fce94f;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + id="path4275" + d="M 307.1135,313.85015 L 307.1135,315.85015 L 305.1135,316.85015 L 305.1135,314.85015 L 307.1135,313.85015 z" /> + <path + id="path6219" + d="M 283.125,301.34375 C 282.625,301.34375 282.09524,301.33284 281.59375,301.5 C 281.343,301.58358 281.09119,301.73097 280.90625,301.96875 C 280.72131,302.20653 280.625,302.51042 280.625,302.84375 C 280.625,303.84375 280.62501,303.84374 280.625,304.84375 C 280.625,305.11458 280.73624,305.2247 280.8125,305.3125 C 280.88876,305.4003 280.96438,305.45244 281.0625,305.53125 C 281.25873,305.68886 281.51944,305.85868 281.84375,306.0625 C 282.49237,306.47015 283.40886,307.00501 284.5,307.59375 C 286.68227,308.77123 289.58992,310.24813 292.53125,311.6875 C 295.47258,313.12687 298.41606,314.51298 300.71875,315.5625 C 301.87009,316.08726 302.86048,316.5374 303.59375,316.84375 C 303.96038,316.99693 304.2633,317.10187 304.5,317.1875 C 304.61835,317.23031 304.72198,317.2866 304.8125,317.3125 C 304.90302,317.3384 304.97917,317.34375 305.125,317.34375 C 305.625,317.34375 306.15476,317.35466 306.65625,317.1875 C 306.907,317.10392 307.15881,316.98778 307.34375,316.75 C 307.52869,316.51222 307.625,316.17708 307.625,315.84375 C 307.625,314.84375 307.62501,314.84374 307.625,313.84375 C 307.625,313.57292 307.51376,313.49405 307.4375,313.40625 C 307.36124,313.31845 307.25437,313.23506 307.15625,313.15625 C 306.96002,312.99864 306.69931,312.82882 306.375,312.625 C 305.72638,312.21735 304.84114,311.71374 303.75,311.125 C 301.56773,309.94752 298.66008,308.47062 295.71875,307.03125 C 292.77742,305.59188 289.80269,304.17452 287.5,303.125 C 286.34866,302.60024 285.35827,302.18135 284.625,301.875 C 284.25836,301.72182 283.95545,301.58563 283.71875,301.5 C 283.6004,301.45719 283.49677,301.43215 283.40625,301.40625 C 283.31573,301.38035 283.27084,301.34375 283.125,301.34375 z M 283.125,302.34375 C 283.04167,302.34375 283.06983,302.35921 283.125,302.375 C 283.18017,302.39079 283.26996,302.3995 283.375,302.4375 C 283.58508,302.51349 283.89203,302.63169 284.25,302.78125 C 284.96595,303.08037 285.91697,303.50914 287.0625,304.03125 C 289.35356,305.07548 292.34758,306.50187 295.28125,307.9375 C 298.21492,309.37313 301.11977,310.83373 303.28125,312 C 304.36199,312.58313 305.23065,313.08343 305.84375,313.46875 C 306.1503,313.66141 306.39008,313.82412 306.53125,313.9375 C 306.58419,313.98002 306.60398,314.01038 306.625,314.03125 C 306.625,314.7835 306.625,314.9375 306.625,315.84375 C 306.625,316.01041 306.59631,316.08153 306.5625,316.125 C 306.52869,316.16847 306.46801,316.20858 306.34375,316.25 C 306.09524,316.33284 305.625,316.34375 305.125,316.34375 C 305.20833,316.34375 305.14892,316.35954 305.09375,316.34375 C 305.03858,316.32796 304.94879,316.288 304.84375,316.25 C 304.63367,316.174 304.35797,316.05581 304,315.90625 C 303.28405,315.60713 302.30178,315.17836 301.15625,314.65625 C 298.86519,313.61202 295.90242,312.21688 292.96875,310.78125 C 290.03508,309.34562 287.13023,307.88502 284.96875,306.71875 C 283.88801,306.13562 282.9881,305.60407 282.375,305.21875 C 282.06845,305.02609 281.82867,304.86338 281.6875,304.75 C 281.64797,304.71825 281.64745,304.70854 281.625,304.6875 C 281.625,303.89841 281.625,303.76562 281.625,302.84375 C 281.625,302.67708 281.65369,302.63722 281.6875,302.59375 C 281.72131,302.55028 281.782,302.47892 281.90625,302.4375 C 282.15476,302.35466 282.625,302.34375 283.125,302.34375 z" + style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + sodipodi:nodetypes="ccccc" + id="path9069" + d="M 284.2086,277.50524 L 304.25155,287.56045 L 304.04909,313.06966 L 284.14111,303.2169 L 284.2086,277.50524 z" + style="fill:#edd400;fill-opacity:1;stroke:none;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4" /> + <path + id="path4269" + d="M 284.40625,277.28125 C 284.30032,277.26783 284.18881,277.2693 284.03125,277.3125 C 283.95247,277.3341 283.84394,277.3678 283.75,277.46875 C 283.65606,277.5697 283.625,277.72396 283.625,277.84375 C 283.625,278.84375 283.62501,301.84378 283.625,302.84375 C 283.625,303.08333 283.712,303.19301 283.78125,303.28125 C 283.8505,303.36949 283.91245,303.42354 284,303.5 C 284.1751,303.65292 284.40433,303.84112 284.6875,304.03125 C 285.25384,304.41151 286.0564,304.87154 287,305.40625 C 288.8872,306.47566 291.36904,307.80952 293.875,309.0625 C 296.38096,310.31548 298.89194,311.4938 300.78125,312.3125 C 301.7259,312.72185 302.51771,313.02948 303.09375,313.21875 C 303.38177,313.31339 303.60063,313.37941 303.8125,313.40625 C 303.91843,313.41967 304.02994,313.44945 304.1875,313.40625 C 304.26628,313.38465 304.37481,313.3197 304.46875,313.21875 C 304.56269,313.1178 304.625,312.96354 304.625,312.84375 C 304.625,311.84375 304.62501,288.84378 304.625,287.84375 C 304.625,287.60417 304.50675,287.52574 304.4375,287.4375 C 304.36825,287.34926 304.3063,287.26396 304.21875,287.1875 C 304.04365,287.03458 303.81442,286.87763 303.53125,286.6875 C 302.96491,286.30724 302.1936,285.81596 301.25,285.28125 C 299.3628,284.21184 296.84971,282.90923 294.34375,281.65625 C 291.83779,280.40327 289.32681,279.22495 287.4375,278.40625 C 286.49285,277.9969 285.70104,277.65802 285.125,277.46875 C 284.83698,277.37412 284.61812,277.30809 284.40625,277.28125 z M 284.625,278.375 C 284.69989,278.39599 284.72017,278.40716 284.8125,278.4375 C 285.33021,278.6076 286.1009,278.90935 287.03125,279.3125 C 288.89195,280.1188 291.38096,281.31548 293.875,282.5625 C 296.36904,283.80952 298.8872,285.10066 300.75,286.15625 C 301.6814,286.68404 302.44134,287.14588 302.96875,287.5 C 303.23246,287.67706 303.44463,287.83456 303.5625,287.9375 C 303.60179,287.97181 303.60764,287.98161 303.625,288 C 303.625,289.23981 303.625,310.53023 303.625,312.34375 C 303.54164,312.32109 303.51404,312.31667 303.40625,312.28125 C 302.88854,312.11114 302.11785,311.8094 301.1875,311.40625 C 299.32681,310.59995 296.83779,309.40327 294.34375,308.15625 C 291.84971,306.90923 289.3628,305.58684 287.5,304.53125 C 286.5686,304.00346 285.77741,303.54162 285.25,303.1875 C 284.98629,303.01044 284.77412,302.85294 284.65625,302.75 C 284.6366,302.73284 284.63952,302.73275 284.625,302.71875 C 284.625,301.5318 284.625,280.23986 284.625,278.375 z" + style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + sodipodi:nodetypes="ccccc" + id="rect9054" + d="M 6.9509201,4.1595091 L 23.01227,12.122699 L 23.079754,33.852761 L 6.8159508,25.754602 L 6.9509201,4.1595091 z" + style="fill:#d3d7cf;fill-opacity:1;stroke:none;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4" + transform="translate(279.1135,276.31506)" /> + <path + id="path4271" + d="M 286.3125,280.25 C 286.21212,280.24665 286.09177,280.27159 285.9375,280.34375 C 285.78323,280.41591 285.625,280.64583 285.625,280.84375 C 285.625,281.92814 285.62501,300.84378 285.625,301.84375 C 285.625,302.04167 285.68994,302.16185 285.75,302.25 C 285.81006,302.33815 285.86335,302.39707 285.9375,302.46875 C 286.0858,302.6121 286.2698,302.77307 286.5,302.9375 C 286.96041,303.26636 287.61754,303.6519 288.375,304.09375 C 289.88992,304.97745 291.86755,306.05877 293.875,307.0625 C 295.88245,308.06623 297.88804,308.99241 299.40625,309.625 C 300.16535,309.94129 300.80595,310.1767 301.28125,310.3125 C 301.5189,310.3804 301.7055,310.43081 301.90625,310.4375 C 302.00663,310.44085 302.12698,310.44716 302.28125,310.375 C 302.43552,310.30284 302.625,310.04167 302.625,309.84375 C 302.625,308.84375 302.62501,289.84378 302.625,288.84375 C 302.625,288.64583 302.52881,288.5569 302.46875,288.46875 C 302.40869,288.3806 302.3554,288.29043 302.28125,288.21875 C 302.13295,288.0754 301.94895,287.94568 301.71875,287.78125 C 301.25834,287.45239 300.63246,287.0356 299.875,286.59375 C 298.36008,285.71005 296.3512,284.65998 294.34375,283.65625 C 292.3363,282.65252 290.33071,281.69508 288.8125,281.0625 C 288.0534,280.74621 287.4128,280.5108 286.9375,280.375 C 286.69985,280.3071 286.51325,280.25669 286.3125,280.25 z M 286.625,281.34375 C 286.64007,281.34783 286.64058,281.33927 286.65625,281.34375 C 287.05595,281.45795 287.66535,281.69129 288.40625,282 C 289.88804,282.61741 291.88245,283.56623 293.875,284.5625 C 295.86755,285.55877 297.88992,286.60245 299.375,287.46875 C 300.11754,287.9019 300.71041,288.29761 301.125,288.59375 C 301.3323,288.74182 301.50767,288.85429 301.59375,288.9375 C 301.62024,288.9631 301.61381,288.98685 301.625,289 C 301.625,290.22815 301.625,307.64251 301.625,309.375 C 301.59302,309.36691 301.59777,309.35383 301.5625,309.34375 C 301.1628,309.22955 300.5534,308.99621 299.8125,308.6875 C 298.33071,308.07009 296.3363,307.15252 294.34375,306.15625 C 292.3512,305.15998 290.36008,304.08505 288.875,303.21875 C 288.13246,302.7856 287.50834,302.42114 287.09375,302.125 C 286.88645,301.97693 286.74233,301.83321 286.65625,301.75 C 286.6397,301.734 286.63577,301.7306 286.625,301.71875 C 286.625,300.54277 286.625,283.20004 286.625,281.34375 z" + style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + id="path4279" + d="M 294,281.34375 L 293.875,281.40625 L 291.875,282.40625 L 292.34375,283.3125 L 294.28125,282.34375 L 294.625,282.34375 L 294.625,283.4375 L 295.625,283.4375 L 295.625,281.84375 L 295.625,281.34375 L 295.125,281.34375 L 294.125,281.34375 L 294,281.34375 z" + style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + id="path4281" + d="M 293.625,311.84375 L 293.625,315.6875 L 290.6875,320.59375 L 291.53125,321.09375 L 294.53125,316.09375 L 294.625,316 L 294.625,315.84375 L 294.625,311.84375 L 293.625,311.84375 z" + style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + sodipodi:nodetypes="cccccccc" + id="path4283" + d="M 293.6875,316.09375 L 296.6875,321.09375 L 297.53125,320.59375 L 295.21875,316.71875 L 296.96875,317.3125 L 297.28125,316.375 L 294.28125,315.375 L 293.6875,316.09375 z" + style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + </g> + </g> + </g> +</svg> diff --git a/static/images/12161797831849255690jcartier_board.svg.thumb.png b/static/images/12161797831849255690jcartier_board.svg.thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..06b2c69cc83ece07a6d43bd14957ec379328077a GIT binary patch literal 4072 zcmV<E4;S!>P)<h;3K|Lk000e1NJLTq002Dz003hM1^@s6DCP)m00004b3#c}2nYxW zd<bNS00009a7bBm0019F0019F0lpGGQ2+n{8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H14`oS2K~#90?VNddP1pLzKkH;T8DvD7N(eC*<(h{YYKV{qC2Hzb zl(vGZxzyOYEv+hj)KsmsDCL%_3Kd0FD@qb$@l&oEB2>hfNpjxbAM2bGiI~#J(B%6( z`$^8)?;1X5?e*@p_gU{!B0_djO4$QDY;CBSmQEzF9#}3S*Rst}h@|cVT_4z+B{>mT z1uPVitmg`i4EH%_Yx$>hdoUUSZ~_JZzbU1nlu|F7uZY4W5y|!d8Uq4IY!q^8H!^bq zQlpAayW=<yi~>sBtE_@{-0$*k4D1Gojk5MM0MUnF>1tTL0ZB_knUfQMdBDdaa``dz zPp&CP&=y^|1fPV#(yvKRPQk{UoCqudW{Jqz$I<A&t4D@Dd>kB&;O_^@+{ah0!r~RM z;7ihO-m?0F+rU@AOc6QySScP=4?Cvg4<id^WQ=8RFJxGMWY;=<F8(m`=N@G0D8$p9 z6f(xV30Ma-%<r*mWqTCpHqYGU!~~G1t7|8BcCWUJjYeioM!da9%wqc^(5_HHw{E6C z2efZ1ue>%;yuF{u1n=pNj15Ij?9%p^4k8O@Amx3zkp=z?a1z*WKEew;^oxCa%b}w& za^OgmELt*O0z0%98ykZa%FzLNr4Mo-Lg%}D2wD0e;#HEQ?5-5xbY4PRCm=bI!C;_i zvnDic)`ZyeuM_t9m#ka6mZap{EM5VhuYztt(6A27_yqon#{oc@N$-%iQtDX|+5UIh z^U&?it%Az<lwrc8H`(&z54`f)KwMoMNlS;Xzk|{5z~w7oFhKX`Ao6<%Uj{zj+%ji3 z0^5{Q5lX2Bxz(n7Dc7Ja^6>Ov*zh52*&e~DF(dHuazhUzFqz<NESx%{dw=o>oShuO z-VT#Fdt3Arcdb(DsoZXxMY#@bk+ZWi{RZ@9^VSHaO`k&f@=pK|84bNcp+#3P+QZqs za3Kam*hf&d3^&c$p9c;prM^%~RmknOJvL~I?Ck95+N}%gH-<BN?ksB7^aJ4c6VPu2 zwCxEyBO$mQL~o|V{CDArQY4w%))zRgl$xiM^3JU`J$7hoMFs=y0@|>0&9^LBx`=vp zD*+I57@i*p-3Gy-W6+}$9N3EE`xC*#jTFER7y_JBO1-C)^7z|r`MU+=M2#9ZBy7bR zK3VuN)vA^SV8?D~(*p*Kf-~pf<(_bO8^$Rkp@cKH(T}xn0;iNxZz-jmb6S7CKwH$J zWph@qThGi{@8MU@3lUiV12pXnBd5XDYw-F2IP@cSqhA9DBWZv;Fdg_qDb+J4_2w%y zfKrN%L4m9f4`=e!xA5|ECoLVmSOfJsz}p{buRne$9N3EO;J#p|y}dWEQYp1XDOKrF z>dbd&i)?Ic=-wlQ@E<laa`YRxxjB=Z0&_ouy6s`k5-8yW)5pTzjTkz#wq|XI_Vkk; z_AO3<fwsu#V5I-cFR?jdGp`OFgrlR8Yd7GXInXc&B6dRAGO+w(SUDfc`vTwqybT;s zO4WN%9R&{Bq7p7882WlBn<K*M^Y32R+1YYF4tj>dYi~k)0tB>xU)RH!!TO#LR8xvb zrBuKBm0e(=E%Ges$(s|#vTnmSG-y~KfHmJklTNU9lYY7J<`CHNU#MOgM6fj&Sge#P zb+0lDJhVlAe&zTqY%x>bo<s?k62vFKpfNCfGNhZJioc%E1DH(M0rzxng$5e5rw{4g zmG$4RrDdxY04!e%pRLsAjdl-LPN9Of$jhrV^A^k{Ag~<(TYrAE{Dl(QA}_DffPR8| z6cuXdoT?bQsHhmasHhmasHhmasHhmasHhmasHhmasHhmasHhmasHhmasHhmasHhnF z!BP)4DFiB#J#7o1l(Gdn0KUMJKq;`U>H5dXRw-2x9f4E-LB>vwdR1rC-e?jMuHxb0 z{@5G%IAj3E0`=|fpjriJR12Ed1t-V5jeB)Ap%ffFdW;vk_F~8OpYpoBxgrD531D1* zSTY4Z90l*auK!we{FMG1_$(})4UP^F7Z=B{&^H)9bR=<carw~noXdb-nuJ#=>xx_e zs+9+K7yW<Wi?HBJ{fmEaJNS7WJlj;C+xFvjI(6*A($BsiH8nLa+n!@ty?5D)IJ<aQ zSL0%53v<VT?-P)c0u$ed*4<%G3{)r!|6L4=--psAAt@<^*|X*l+#!hd-)_KUGG$*| z0g)BjU;y8W82qY&gOgr^UeOmmn+hGD(UCcak3&#j81j~0zum1P?D<|VX|%P0*x1WV z96y<mE}i*l$Ig6%dXVnAPdm9lh3cp${lLZ+adw0W17ZGKP`RA`)qmA`Xb_~sk{li2 z{fT;MQ?OnQ!HE;+7&>@3{rbE>O!WS|W_q9u=&(UXOv4m&*qo}sy);y+g(~T-d%0F6 z-RmR&tyk_{zX9XlgGNEHdIMA_2Voz<mam|x`Q{fDeS*Gy`ZGMVKY#pjGG73*lK~h9 zq@6sCA)q&=asMIp+70tb8}Ralina8YU}FPa+rX-s5ZqD^i8HY<csw+J9yV`>dQZcK zFuj_udSzYCw(a}r);WYJ6NYl(LjHStgNUp^N9G(cnJ_F_iG9u1B!0e%WQ#wtvxg`B zz`qu_l>!eJeT!T6A82Ln)ng~2&oBt+1;6amtNC_*1M{YXkGEdbx^~?!JQvuJ*)zv+ z^=kfwVH*HMBnqgDUQc}^F$t$J({ZTXj<{d<W!%c_?V(J0@UIDOp5RvorVoWrr$9Y( z#6{F0=+qBF25HCFs|)Pi2-8M`yDOxo3QNPb(6L<`=FgePmG~?90}UV|sUk8Hr~)hn zB=#cS!ToR!=*^K+e`d_Z?H!<uKUAy%t{za`ALfpQIb-y!4q*FkX#E289|7m%U}!&x z{z0$bHX7l^&2$zl+DJf~cDy@vIA_nC%X?^xL?li`1^^8)2be}3q(*}v9D-kB|DWeF z!~0YO|EIvk1M1g+Pbb4WL!g|u_VnLHK;up@d=exk!IY7(FB}H-(Vjmg#l*_5ck*0^ zV8)IZ$nQrUe=O=fA1y^>pNKRC`T(Z@+h3!oQ#TM}=K(~Yjm>~JI)ZOSs9=VFwgD`E zSC4xyPe@OP<?EncAWWR8VP}kogIi%-D3tWjV}8@-D0+nSW>Ej0{QAr8yobI6{wts= zFbMb)u-o+;b?XFR-*q7SV&k}Bv2}2QvXwNvtEcwxt7gKG9^mQ>DXFkv8PsV9Gw12$ z%40*}Km^R30DeBYoXE)IygD$H7oHC$eB)+J>A5?@d#INPB9bN|OVHyz6gUUi{}f66 zT5Yipd4;`km+poyUmZM3Laz?6dKSDC3=T%PnF#MMgqp2k`bUtI3<G<^-c5RH`*d|( z&e3D%7&m%6L4g6RT(OGe<mB9;&QVmV4ig*(#9|H-Rgm1aIg!8b;JPKA+H#zYM54bz zc7-9|eT;N%gV@=UDIj%FFXZsHj6C0binM5)IS|#w#ZkKV2$2<Ee<_EK#>k6(dkg5m z*Z9La5cUrH#*Z9T`;hRCz)0Yt6<(3pz9qjN+nEjgZX{-{hW~aB(lby4Tk_i3BE32x z`@%EwY+sE$ABfl(GJgU76)Q-!YE^Rteb4Oy9!2IDsiD9aprlSCvBR^(%$dg1B|UJt z(?0nYT#kbqSK-D@_~u7g69Lwlpn{+_FnTc5tg6$_UC_a*tG<I%e`c<~b9?ywL;W;Z zCL@BQiHJmI+uq!Owic$83osnL1&F&&<7QAB_RpC{Z4bB1@TtiV9|zasAt?nm`~<5u z!^QXvo7PQWbSO0XS4P@z`(e!n9k-d3V#BKW=~OD8Ptevvm2yL0_HO{LI*sd{1K2-n z3UxeOb(%$Kw;}!lBwU8uw;^ITtO$qGu^BcEYwMnG^US^L%~xn^;YxV|qtQ;UgihmT zKuZqJo=Q#cQd!{BAmI`uT-5ug?23V9-^1^xGHhy8fl;sO$5=y_dryIYwpOAeXU3v$ zca|8*l;({%JZBnJ%KK)%>`m#A@E2UUh@{;{MIY7hdyiz;`1wHJ?$Emnc$wpJvOo_O zk%if|CoiC_l_^yc-RtAf0b~H_^`GYV*;A=dql&&7EHat&c#n_6bUOvZ@zbz;BmA@v zOy*|W+CckO&^H8{H2~ktFS6GFV@2e2rgr2dw6#*DmZPV)iRfrq0I5-x|NCGPK8@;R zh8N(v89pTm!?}y_-459F3nX06uq)-MUt?uaGVmero`~Fn{DTeyUILZ@Kcg>wYaCTY zKTgkhi_)!|a#y(UjjIrUf%K$X*rcVy&(YfJ?>&+^fMrNeSh<PxE7z>s3!MV`i^$IW zhW<iC`YELhKzH=>d>xCOk2kT?$Kf8_7AKYEx5K1ckZ_6H*RNm@!6q&qA`imx(@?1_ zbZZOADX?rKtk{StEgb`Tcb!260y@jN4#0Szxh1W1NiI)%1Lq!{aJ01{lhV@Q>R+T> zi6c2Z9k=Z2``>wZdl7Qt3<~(G!2N0|BAY~{1@H{85#X<QO1(Y_$12UawrDv?$tfB3 zwzg2}NsN{Mg`00B5{-_WBFi=^HJ@_hgQ?XmA4?ZFXp2PT7ZG_5sEz)CG5y+g+$PMx zu}X7peXx+^D_1k@72x8IlYb5T{cCZvge!ZT-P}*tXwv+At){JT@$j?`kOB*Bk%$}= zkzVL$`o%!Xtwfw>%)_C2E7FEdBK6oGne!NpxRk9zqZ*AHUUG5w^vPOgenDGCW^1KX zAElJ*eXRizIVmEq0u_J{fNN=K*nYJR`(|CB%gdxi?0mQo#5{*qO8EfY%pc1F%YjQu zsr5>!7a#Nmh{y#I8H-*(@CNWdfS>nZ+cOjz24PyXJmZPYq9aGITXw-an`b*i-SG^* z676Wx$$rPGWZ>0DD9;86LBGVbmg8W=RIj${^=g$}5}h1vF5gk#CwUFr`i_Que#igQ z%sqbH{JB*Q%WndF33qM&W;wP2<?`%4t(3AuZ@BgpuoVadqR`%XkGa7{bNU7mN&g#l zxuPdK<<M_+E&%7mvcYQ}_VQ@-x19=9Gp98}&yoZfkY{blBVZq}Zn-xj%egRf`X#{q zp9F1jUS|O3Ea$w?8?6d3?;in8X6F@h51XOp^JzfsLJf`HR_llvXv=-<Y=9WE-7d_J zyoD9o;=EFTst>SjdWTc%TPUH;W7Xok-hIeCmV5UFz^Twdn;~8>U#<@Y0F(wIf%cEH aUH=aX>8ZhCMN)GB0000<MNUMnLSTYqlEzX1 literal 0 HcmV?d00001 diff --git a/static/images/meetecho-logo.png b/static/images/meetecho-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..59eda57a4c6a63d737b9c343eeba6bba40d62f5d GIT binary patch literal 3005 zcmV;u3qtgXP)<h;3K|Lk000e1NJLTq005By001Wl1^@s64@;^`00009a7bBm000XU z000XU0RWnu7ytkO8FWQhbW?9;ba!ELWdK2BZ(?O2No`?gWm08fWO;GPWjp`?3ra~u zK~#9!>|9H18`l-R!#}CtNY>9uEzw1hO@WF=fwrr9gbK*g3i?R{v=+4}nr>>UK+yt4 zVH!ozMHiwL1zL0w%Fd=J3ebB29w$K3Hg!$GNRT)POv;WFT9!1_hvILBy>DhnK8|L{ z8IrOkbsjvXI5Y3Q&pq$nbMJeS1MFg#4vWK{0=sltT+JR{`q}j<cW*yiDVM?Fa;N_F z$6us7ZBUlk)5l8X5;)yn0cEb*YB=46(YrbIliyr+`uy?0$oZgea0FaFKZwNw6!(O9 zA-f&<=O2Eawb~HMoN~C`G5@(S4li&8dI2N}WoG%q?F^A=gXye*2$g*Wm7VArzW{;Z z^FTo4=^FsX;e>KN2g0L|lZVeXI4eLR0K)owU}T&PjJ^!Mp%L)*4}!z(k-@pYo)&i3 zKILp6ogH@;clmo{>*Ma}g@MsAcxAXBD)?Y+x0gH*m2!I#{JdeIotV?>Wo5a69_U3& z^vd`U_?!+{&y`?<aj>P_ehlB6wpDV<i8Zl8z%VSaIt=4L9^`>9vAuAy&jnw`Yogx^ zPOlHG$WX+B6`Dk09C9kOW+>C?hT(o9(~XxCg81q22hiHF+&6y}o3vGO`lGXu{6BIP z3CXlrD#BjA1bMLnMG2sQpB1r;FH~3yZ3LmB)FWhCEEV7&Ur=O<Aj_3RLfl>CouI9f zv*6B9;bJk5{O16Q*=<;R@)+`^aqzgD@ND-03cCV``9sSM5df7Bb5PpL!shxDcprh< z=l6n;E5M<!1?Aj6NC;ZHoPH1`@QNyt*se^bD-{Wz?Cy{dA1#Fl$Xy`6^DJqh4f$QM zRKmJ@;P7~nUsdFvpSiuVd;+e6%;!*%q@=Bq(^s7Im&||P&5VEj+Z<{(D2XLhs;F2H zv^>O86!)``d$xff+Wd9q-aDouDU?}2nZ*5li^w85CvwTw?vTjbg~HAz<hH(u-T%|| z+qO#1Mu<OvAH2+u7)G{0PPZG#&&1t`#bSK*ufLyfBTsY(KR8*Y(}m%^TefsEPbwCY z2wV>3W^I+60T59@A`w_vCdPHQ6h%e1Z7oploGLd&$t0j$vQ^PWSy&mM<}w9~GwB!6 z4c%>zRp%(E)<|1ecynnA{(Q}PQt-{CE2|111&!<={4_QPe~B#HsIXi@)LcfvTJG+6 zK?M6d3GnrDfNqDlpIY$-kRfAS{^Q?;;MQ-g0cj_bfc|qpuHK;$m>LN{3B&sbTY2sf zc@Y*HL}w<%Ck4ZMIYrH)MH)H}Fnj^PhXBf<vNI`w>?UiuB_!&AL>j>f`$ZAnlSbsk zW!d`$z*ffA#3?T3kAg5v_d>$KQ5a(T!Q)^~$1Ew_!!DLAkxudXd{9JdB6ZZ72izCG zipK3F81yi(RygtCxuymegvZ(YdYxJ#pOb;%Ar{=@lWhaY_l;L!^uh#;`5n;XW<ac* z`1y%Z(T`m^1rdWJY1GHS$V)GS|JAE8$IY~96Ysp4o(g>D4Hz38fia&0#{CRP6|%!p zu{H4j<jycAgMPFgZsaj8M<ujfkoeqkIxNjiHjUC;H`7;oTul62&j1V~2#39lEa$U5 zN#6c2=~jsO<Dj0i;$d;-u0_{0rOIGyX@<p_RfD#IM^YOLE0?M7f^OY9f8wAq7e?C3 zTIg#v{}WRCo08=iwW(i!Rvkx5Z9i>szAjVkC)M`Toq-6+kQ2%ExF0Jq<Yz}{t5D%{ zH`BA_tCPA4_1sEd>vb_P0-kdSo)NS{z1X5qtbmXgp;RcEnT(jqOSI1f`TpX}Z4=;$ zsfyJ;OR6$x<%HFoi%~uX)Ssty<Xs0Pnab)si()%BOe~hD?s%2wDR~^t0I{&LsP;j7 zKZf-dOw4zr$ryO~nFh?J-I;{0=b&7X3GujJaY<u7hV64RJc4EJ2#30rev9=mG37mn zT&EAwmn#F@Ti=7N9fE;Ms{!%Ugr?N*$(nT_8^#MOb1eYGQvgb;-%}0XP~Pj5gukT% zpl<0JffrTZX}0o{8UShg3oQV2M;Aa!iKqZ;HQ!MRKuOIQIQgDt&P{_TpU=a_en~$6 zL);Mo#h41F0XGx(B4}iR$d&wWr|%DWnQ--;F?=V0#GHWey-yxP=Cg<Lk&_lBp(<IB zP4nW+MZBi*x<uVq$gf@qUZ0ezIS$En6&V*PWwENIsw(LQUW|OvmZy48xu$h2P`Cz` z#hF31ooT9l#mY6hMH=i0N_mPR8~YnHv`|%$C`wMKV`bDjF%6KWTFiI#=U}aUrC&5S z<Fp*pAR;>7$ZWy~>-$hZR!l&FmZ)ZB2xt&IcLGk}(QSof@ZfVo#z|ziTdKgjs}JGn zM@ciQdXBOlM_ra;PG>0)L<}M;mIC3bM#eJbwm4non&)fLTANyzrEnpR0)13RN}K{v zSOs!cBPZR02ePu9#hKS?V^!)bXxa?7nD45Lbi)`HXO=W|W7d<6GLj@nfA|3Y{a6sl z$wWW8r%~dr<jX-wGbaPc$H*3na3Mm(k%YrL|9Jpg_e^u)NX?uYaUNwtle!b5SfMaA zDd{F5yz%5(>#Gq!sz8cUfSuGKu+_e{yZu8pV2w^!#5Md*TcWjq!Pp&E7amWIW6C;+ zWQJXWwfFCY9|&TcxF?czCK1=TkEyvgVwnhcB1HUYUV{7EVl?}~ij|ce(iyyw2?p8p zHoCM~g9+4AF5hHw&6L?%Uz$f?2?IrjTY}JBNi-Q}np$vNlV~(yB8rS_IFkA3tfA~} z3lCqrFkkw|WBA&jGmPXK-5Cla;Xiuf9`Yal+sH}q$*vgQ_`$?7eD9ZLCn(r3*$Uq< zDx5|!cXedUqKsJ+?X?S{Ae9EVZhBk*w>9^L4v%sOZ9WfaqRv)@*Vc7)&}n$|-3#-D zzpue11k#|_fdsn()gwxuA4u>~Rt!IVeZs1F=DOfwxGt@3Gr1JHO|FB!zNn+Yp$3=O zst}+rtTZePSEoH#|Ng|h__wv>SNb6~(Zk4D=tujK@ae7;-hN}E)8np56DzFMO5>lC zGE%0LU{+aYF=O4VWL}?G%D$aWe4J&%=*d!>g-SxYI@yh*t`W<b>7y&>d2O7=TP72Y z!O!X@_nuD?G!dQ|&f2gTy7pH=@;{nPZ~buM1_EliLjdtDA_;3Ux{*jU2I$=`^fgKG zzZY@jBxyhcOuN66_@`x(UEoCBkYTm0*)~DI8>9=KPib5TX67oQjR;R3h#GS$p{Cm! z#xibGyd&I!H4=0pt7f^TSX*w8R?u-;&enZs9$_kh1XlyUn$$e0f8;6#joTX6Oh!nc z6O~o>*e<srBUuBHF_;t3xUI2hXpsn?Xyg>t98QetD{t5zZQbsbD9{;oz4=~PxuVHM zMY>9?kHH$PVy7ce-wueW-+5@<JPoM|W_7j+mhnCZ^<R79)m?j@nlMh4L1bPs00>uG ztqUvR+I?jkK(U<4eIDvtFkyAaJ4soxYJW-2Dy^f$YF}8YuL-I?r}q2OG0b;b9Ycaz zxDoZcp!T^jK}u~So!mdSr=-|tDbNI=eoP|@_46{u?nc=LpjTN)RdSS49NVP+k`D3Y zjzlragmM(WVb%Us+X+)j2pYI>4vub5(Dj4IO1>2e5Yo(db*@4bk><2pGcH52SCN7s ziH!q3+yfBHJh(mW<T+v>Nnw;*R{;6-qe4yq#=$ok*lpF6hA690&B?Sr@I}IIa+apP znnIc!RPzIFWG7oO?^W<5K`$|=0BKs~G_#ad5!Cln(Cl!SGKP8i+);gjgf#WF+Y%|t zZMVvY#aq6kjNzhYz6GcsO0$2>-7d#d{wKfy3XzjDM;Z-D00000NkvXXu0mjfbiAuj literal 0 HcmV?d00001 diff --git a/static/images/meetecho-mini.png b/static/images/meetecho-mini.png new file mode 100644 index 0000000000000000000000000000000000000000..c9c6fdeb9b8ca686217b3f9e71cc61f0e3b6446f GIT binary patch literal 1567 zcmV+)2H^RLP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru-T@B*Gb7e5kZb?|02y>e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{00oChL_t(o!|j*NZyVPY#eeV3 z3^_wl<cJbQ+KE2A2@qsKZIUj-qKhouqDX@RZ6YrOWaVhrg-{?tAb3l!+Cq!SlK%j8 zUm)7lG2jNFqP9rf51Wo7Rk0|3kB0N{UKV5=qp=cF$uf6+=A85HJ#*iT04S}jy=>Ud zW-5PS(Vog<+75BhC+IXc`>m7J+t;qP7_&+%Yb9w}6=%9ocBTuMsWd1>&}r?So*b>- zzIJsFK$KS2E=$YWnJCVYnz?{Il_xQoMM#6l_h{bxq<VUMSRThjX=SY>q_LaI7v0qC zGuXL2$;=eeN)QKqx(Bsp`@z4;x368@Gl5OhnZUFitcf()*#ghaWYEBab{b$Q^g64+ zJ7YAUVkDEU=}cgzCP+^gc&?bkHY5jL$f1yK=yfZ=Pfdhyg)|UCBFzNPx@j&=nva>9 zZJT5!H?o1!%Gv@#l%;7Qq(mAPGnq7(CM|3O5(d;eDH4-z`6s`tEJy&9LPZgA7|`)U z{8$qx=*JozM}T(6IQ=4e@SKW5;;>K8?+-X*2o$I|LYgKzjuuU{CJuZezemtI;labt z@WUbr)8M$>qu*{4`={e(5EX?)rygOa#iN6Ve4rGQPLihA=d@WT^17%nzyyYgZzH6E z){2Mi2GX?9aZKoS@f*k0F|?}yaTpSM9b_VjMiY7+j;$nGE22}6-qGi1rK%>dtCcE; zUI);qC?x2%k%oy@G5vM}zj0g{Ref(`y;WLStDyC2==DH@3i?D|cW@}JiM%de{iss? z!#gceT3O2iyTB4cAWaLyvOow_7!U`+=HABom2>QY_kl7(APo~~SqNziauM{aXkFgh zSZ|3TI18)-%fR9jt$cfLWBt#Zvq~##Zwz^tp5WZv+gQK;6-EH?=Jo6lRQ^AX`#_aH zzO^^D`kU7;pTSv0>neX*y#WkDNVl<roi1aTAjH?uWD4RKe*5kc+rJ;rMB`|O%rs=C zN$2vE@+m?E`*pv3+HC_oVZa}R1$P#57hq-%>=bxyIJyt5NAB1ih(kE-f}N%t#N1MO zjHFGJ{2$bh9>e6B1W$J2dzLXZOU})Zkl*Ay(|~Q;NIUiYl+6Uzr5BjFaFKlS=?6O7 zfFYR4xmd-8bBo(Ay~6C=JOxKGVF_YAh5><Q))wS1K7;eh>lL|G%Z}Xgty(GdvtLt~ z%~P-?MMt1CXstCa)YP;ex9TP~#xcIAy)={pa;RlA%v#cnZ|rQisPjv$v1v_p+% z90LjrF(4?|g1jx4(w5jYw`wn+^-N;B_EyFcI|WC$196Ev6mR-5VLuq%fYO>?q?t+# zh=xYoNlWY|wrg*EBQx8z_uPc23~-X7BbhV>UIZT=bf`C)Fir=2-{-I!5~#r_ijJf( zG?KN%W^%js9)R4cWu2R~k8(+|{1v9)2vUZi7in&N`WZ+6xeHI3|JRG?DWx?W9zEiY zc6sq)nv@|Z*kB5YDKw^3%gvj$MML1`l5%M}DVRwLa!G+D1+74H>*KrB|NH4+xt*1n zZ%vd|)c&!Lj-PM7_^j(XreIcrDZ!8uQ>d~j2f{P9z)c7YAvozP{`%qHY3<*Jsp7Xv z$Wd16gAd+c-fza6y-1T73KnhgcqBzfGBp(c!dHBB62INL^OuocSveA_bJD!~i@7V+ zlX$ZeYD_t3b<vR&Y)Qrv2q8H16#s0-%ZI<3ANdPfj<ekE{c`TgoqDv{=qscUWG$F5 z2P^jTo?^clFF$;B{>C_*a@-Aj@ap`Pzt!W_dtHSBVy*diCvJY+h=2aYYvYN3cL>j@ zqu1uIw{F&UK52<%Q*i%LU+<{b7h31KGR}L^`M+7H`Q!a9{V*f=q3cHjUjl_iSFhKu RS114g002ovPDHLkV1jkW-?IP! literal 0 HcmV?d00001 diff --git a/static/images/meetecho-mini.xcf b/static/images/meetecho-mini.xcf new file mode 100644 index 0000000000000000000000000000000000000000..42f6feceaa290aaea722f6926ebd7d20096c2e85 GIT binary patch literal 3415 zcmcImdr(x@89%%72Hhx)j3(8!oo2LCy`X>!n3aGc1OzIH53rQT-DP>L12}>gTo8do zAv~(`l;tH45mZ1>c`Pm_p-mcUG@3>fSy&jWQzWcVgx%Zk+`H)2(m&dn+?jLFcfQ~6 zeCK<d@9s*ZQ3r+b2gE|LG?GN{2n_?vi(&A1@Un(s_CbzO2_7K-0)`O`K8&RX8S~x- zz6Z<;C17^#0a;WO865{}gux|lF=R*_87e$1jSCZcc>4HZ<Sm9OjS7*FYb7z#P^<$s z53P=kONb<ev9d^M=o*XFV#xtnq%20bnwi$dNInS>+OD%@o_Pd21GEOVelpM2Ib8VP z1-vyr_F%{XX|zOWH+L2&6XsJIbZZYqOXGyn=mTM7EZ#2@i(|>Smk#3H|EXd%uW!Y9 zFg(1*ISFQ=Vc?7|^b3i_3H1(1AY)+7)C12ZFmQcn7-j|;Z!kB=FrjxjvIR%B<j7VW zc_Bv@fQ;?VJ*FVw^YFvH8>|NLBMgENW;}q)m&1%7Av^flcrl5H^)Bu^wuv?&OZ0@W zb8_?WaNUFuVdt`aXTV4PAe*Cq5VlV4-n%~f{q{|WaM=3c{?uck{w@r#;r$(Zj;2TM zaz%urt4~5z)v*9K@RUb9aS(Y$Drz$KdTb;dK`beyb{P=+Egm8H<>|XU9f=Lj?mI)n zKla|r?peS21MfdX1n%5sPuM!Rxb5)rbl(IBpLlBT?CRm=xy@+<0f^HU(fb<-IG;~U z*>8r0lPx0n#0xun`}G9G0cyJqAQBLVF|i046V7PH%GuEzy@PZ-BY4j)u(klhIx{|6 zgl3FwR^!#|TT6|R6?$Rz&bkezuzs=lcgxNBApC2A#me>D?My(NUgTuA!io<CpE45s z%F%1juZ=Oma*L<KszpYS=P8pV?`__>&&wLzn=)U&&u8;*7Ms95nKE1Y+b#Zqem2Mg zJ>@M62=sSZ^R_ACn=fDI8W8BW6fHz!D+2;|ZC$&}+-im0w%tKK)<}THSL_P<*xmM* zs~kM{26+kK3o-GYf6$&C_M3h71#MpjAByLze1rCR`vnHNy$hd%+2tPw1@Z!QTbCjm zG;QO&chANpXc?L@v$o+QIm&{9lTpY1RDAl=1YB@3JSnHB;A8?qWcaZ&H7%`;SqZqV zq$Dw~s;TXAegYz6Pn37wy4BN|$^g-6C2f7*^)_W8@u8FF{`&CYtt+{ZfybmcCPVQ> z|G@X{MTf<)8D(GG@Bg+5BRuknEVZcf@BO#0md24$^4z-ao2^x!u}H*`tjhMA*K1G7 zNJ&gePO+jSFP=rj2a~f3OV6H4iXzFdn55$wX@|w&0FRs$%Z{XGq$f%d$s?!5;j)7f zB!q$iY4pKJF?>`>V-ku^E<mPXR>&kwFM+SyY*45qj2AX5i`)+tnA%OorWF;WB>*xR z5Ezk^TbLcs00BW!@&ZL>%uAr?6_8U~&H*a&4Lkn$dEG@WaIP+gF}3GReN#&rv*S5) z#<|Pqt12M8$%^6&&F#(XDOwh4ZN6}>9In;#it2_d9XA?TaDo9V+d5hrsw?1jX)9|k zwsxsHstXviKQVAcv#PVTv8EDk*+gaCr8cFitKu|EP&0(0nr@Y{?NV(es;D~Obgf&} zl`lWVB^2gV1LG>VckV*-^&35%*->v0vZ+dC&yDNN^$jf@s-BL|AWN@X$c_)c2#ofY z)=pJV`+1zkHwpVuLsyTgvrE-;tsZd*<BXF`0Z@HMkBX<7xLSv}3{Hu7Xu7UVb>m76 z6ljWLghSz@nX1~l3fy$_6QKS-2Y|n()bX=T?JZS#xVfn0{Ko57>+%qylFzn&{o{k% z^?A65sH0~te|hi0-L^bLovc==pFMkgyOaUa$~zv6jX(XN9wmNW+xt(g_SyYQa9<u% zi6<&=Jl1N*)$KWnnN_OC6WXzR7jdIdPtr@Tt0%PM50#nJ@xn{r43GAIRm~zPr|Q3a zH2Uc4=5#9QWJzOtcjx6i7D>z~K7XydtEJ#LMIAknUshF~lZcy(noP_%Ra#kbMvf>R zHJzA}o{>V~<^mu&?bFmm3Su#)pt&e$u3TuYTzw7{Vm3K5CzqEyTU?Neo9jqc*%hU- ztq%9lq2x0czq#96%K(Q`3Y+fq-z<9x+<OJ|svmHGf$t1EGH$AexWFUzSBxp@TlMfT z3p{7e7#LCy4nla7gLj68HN)&FS{51}9s&%SYfuf?$k2V}qzM+(Xog`cZZ3?DjA$Nx z&%i%1@I%lrI5ZdFnvv0wAO3cSA*dO`-2qT(u!X@vH5eZq`Eyg>YlOf2h#AA+9zJ|@ zq`UA9LT|%emcihfYmlYaE%er&9%2~a%&10<(`X>DPBQt$4BiNDRI5=V4#VIjPBc<$ zMn^`$0OArjC4%NsYam`VZZ3`u4h5QPPz}LCbInhH`u`jNLbFITK=WyxLI5{XM<_vl zp@g4!U)q?~^$Fnq>WDGgh@KfE&=_q(>*b)*D?!pJ5p7EA$KZD8SJGxQ^8!qSW&C<2 z1Fa9!1fbOk@Me85Hj@p~Og0kq!2x;&H~@x?Xe}Iz1o8oz?(>Bo@af<_kYtb>oj@e# z%XPu{--ymvuH-9q*};%h*kw%X31|{inr~Q%{=XFt85||RF~$;v*+;+~*ZJb$8I>um z8xz6B*2^FoMq`e{0ZBodvkD7(MuDTUV3(GVt3KM2U4W-!^pZJQ=c~Z!lFu18re{S+ z0V`cFdZn8YAy698v%&CZ*x&}(5#iWnG!Go|1(6#(T&R`uU;%i=)&6e@;-P;7VV7qN literal 0 HcmV?d00001 diff --git a/static/images/projector2.png b/static/images/projector2.png new file mode 100644 index 0000000000000000000000000000000000000000..b6dcd54f0af4da036df41a54224e0256aeca85c3 GIT binary patch literal 2421 zcmV-*35xcKP)<h;3K|Lk000e1NJLTq0015U001!v1^@s6&B_@;00004b3#c}2nYxW zd<bNS00009a7bBm000kR000kR0jNKxX#fBK8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H1026dYSad;kbZBpK07_+HZXiQpa&&2Ba)CL`<NyEwBXmVrbVOxy zV{&P5bZKvH0BCe{a5^t9cXxL#YGYw?bZKRBE^cLX5dK8i0000ObVXQnLvm$dbZKvH zAXI5>WdJfTFgPtRH7zkUX8pev0000<bVXQnLvL_-a%pF1bO2CwVr*$+AVhC%VQFq4 zXmoUNIxjC{a%Ew3X?A5}Z*6UFZgVbga%V4WX=7z>b7gZcaCKsAX=7w>ZDDC{FImI> z$^ZZeD@jB_R9J<zn0Z)K=M~0(-^?)Vpn^Eyj>e@fsBvFV+r~Xc<JKx5F2w~+6xSGx zOCXOcE*PUWR*h|<bzGWas#Rk(X{j-&2ySQu1qPyEqN3smbNUBp2ADxc?fX3Ux#zq0 z-1DCI+;hMC-HV9ubaT?O-p!T!E-q{Wnp>ZH)td$@Sipa9A3XZ?B3!2<*~v-$ODWZd z{{zYpE1*>~*t!bmD<^T97fE|}SBzS%I;)f#p_J0tiSu&APw_(rM6dLuK^@#vzs56W z4voA#*`yR#lu{FvQu<d*tV(uAzJZgcJ&B3;C8&>vn<hL@7^${nDH5vvsGN~w)X zskut2{x45#g;uRvkZ|}28#aB)^_*68dQ*{@g!j6o)M``@Kc$!i!~(G|Qi1Zss=V2@ zU0YVif5x7DMjq3i@F;~SbbGd-2^q#?y`H8@k*1UibEF3}b_eAU*eH;t%l^p0!w2co zwL2lx6*sbQPuPxI+#)<ZJ&03^6s6Q)hl#6#fB=8yEsW>T(IYf$9L%6_#qs}8<Itb+ zSv(7WHy3tlG%8Uk)wAlv6&^18@bU3sPV_qd{#626*ADa=p}2m7>L>T%6%|1<y%S&Q zG|H%y3b1v(&UV;eNG%o%>FF6<NWI9FOILXK@F7LT#rXNvq(jH{v}qf}(q&r+n{bT{ zYnIaE?F&pBXQ_7PFhnn>@4;hS)9ck7vspeaLt#CQ>GkZ5orlEEQ_`=0UrD)iR`!3n zM*zE<i;IiQnln?9Q<Eg+(plO0`F3gEtd2Bk<Sa*bAzyAo{QY=Rzc#DvPMNl#(P#ni z@NlPZ|2~9;hcYE{5}{#Z2yE1l!oosUuUtdu=x`nvKBi6EAigjjp#R_q29MXH*Tb}N z=!%MIYq!)-3oKTp_VKO3#?A3`?$QY-C#MS4`peEQSh;*P=g+6GV)-f-E_xrWR?EbQ z@uXc&Ch7diQeUKuAlo{BA@=t6rblr13Zwu^DMG?VGj?1k0Ds+OBr7w^W<-mW9lw%E zfB48@&+PNbDK>o+l_^$FrB?nZm4~;t*E2h_jAE;&qiUsg3}=$gJ+m7$sBhD!T&3+z znN?S8Hk;Y}$rb?gdObmHTibTDIM`gz!-q>g;zH_00N$A~4THg8Gmcbxj~&F?xb+;^ z_cs7Kb?!*$*fI8<%hme1Q>HCgiY+W%9K&AYegHam>BRbv*PzkZ*HK<^Sz^ueyV`6v zGjH|+jvhG%K=0r5VpZI7^!m!z0x#9o1@{Y>{D;VAq{BzN&DwRVD<QQKU@3dX=9Fm* zw{Pb$B_fh5m(u{4JT-#I=~JqlbmgUf#$C1$H!^MzF>xxUY!e!-hUkUwF=Ev4D$rSV zwY^d+r9ACj9u_hlQ??0!vEiZg>=lf~VzK95CFqn=QCcmrfQtx^09;)xT)Ub^US2-F zzCPT$d+*r=+qUjt+twZEbUF<F1_A>7@elCFztpbZpdRgBZ&!h+d~@k^T4Gj3;oPht zEZm6eg)~^cppe9*EZ!P0kQq_anHVvi%*-ruOu6Ld<nZH<w<szqA}cei!rRE#*N5ZZ z9JM8~EOAjWuI;;UPxp|AGea;eh=&285Vz<F!v?_Im^i$=eRy})dvxj6yCnN!3%73H zBFB_VPEJWOCQ}YMrd$dN3ar9g-D4YFE#@M;ng!ui&5s9L7va1!0o|anFr+UW*lOXx zq1()!F^g6$>+sI3#r&#eI}8Q`27{sOvZ4}+qAE7ms=lLn5Wfvm4Fm2iUPl)-kw-gX z5K}gE8v=RxaNH=gY@A8xm<i0EIg-qqX_ZjgK>3%1h|GOb#Ly*i+-=f>8z)YaQ9BUV zp1($I_<%?2=OT&UL+{~G!wXIr!9{bGA%jK{`$1@x6R^1g5s@Q6>!0tkX4GW7gWllW znTw>?X^iWj!Km#YQMhOl7!zP{ICN+Q2X{ijPpP~$a1`@rj^xtCa~0XD@^%uDA`w{! z1ZL%sFfa_?cKt|7O(VT-6I^~b3U<X&7&#K+x530H=obtp_Ca;GtAzZ198)I_;q<A0 zSE<x?e-!|}zzU!supZxiy_@U#sYup;kyE>QG%F0L<%87qN0!e=&i)gb7LIthD{0Z( zPoh7VEy2NErDUCsIP2M4CjRsORRFbswZOyI8j^T&Z%G2#-yuo+xW8ZmmU;%nuLiPc z26FK@vLYI3AEXndknL+qi1m&VTP08%*a$po+K6vY7)j4hMRJpn<OF{HFcNc<`bae| zWOf8{JrT)HLh@5fiFFuXh}bHD`oI?8aX>9D{Npoj=BFaJ&LNi%leapG2W^@n?rzA~ z;mEi9ONg~vvVq1gOl+0Fr@$GYpsqhD#?55qry_ahk*mkZ-L#Orfjtm67o@s3;^#ww zQal2d0nVy4h{I4ytt~ZUfOmmuwQ7>L;(go({gwcLyZL1OkcRnGGJ*9BAVTqi4dmat zL&i%Ji^z=9u@^8EnCVlU!X>kD9{DD<v>JGDkMx`j?r61c%`6_T-S0Rs+3+%@UI*+0 z^yQAb0@HvD4|nnwOegPpVo8%d$sL$-?fW-MK5<`=R1cUanT>t|{sgor=c5BgDaC1> zmclVZxw~ae>-3P3b(6GOmHk4cR!YqWh63)uIUpC<BO<5l_;=N6iO_0Q3$s}=fKOgT zZ5`kq&>rYjYWq7H$6>`vsolU`5s4@re-0FjNT@@B98}vIxC8iF9SbN`-OZ6$j($4@ n1JNRqXO*}KSPXP`B+&l=ZL%(OFLR3800000NkvXXu0mjfd<%B} literal 0 HcmV?d00001 diff --git a/static/images/projector2.svg b/static/images/projector2.svg new file mode 100644 index 000000000..3a8939380 --- /dev/null +++ b/static/images/projector2.svg @@ -0,0 +1,175 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="30" + height="46" + id="svg7594" + sodipodi:version="0.32" + inkscape:version="0.48.3.1 r9886" + version="1.0" + sodipodi:docname="projector2.svg" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + inkscape:export-filename="/home/mcr/galaxy/orlando/r6743/static/images/projector2.png" + inkscape:export-xdpi="100" + inkscape:export-ydpi="100"> + <defs + id="defs7596" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="14.818182" + inkscape:cx="16.117651" + inkscape:cy="20.30673" + inkscape:document-units="px" + inkscape:current-layer="g11262" + showgrid="false" + inkscape:window-width="1680" + inkscape:window-height="1003" + inkscape:window-x="-6" + inkscape:window-y="-6" + showguides="true" + inkscape:guide-bbox="true" + inkscape:window-maximized="0" /> + <metadata + id="metadata7599"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + <dc:date>2008-05-15</dc:date> + <dc:creator> + <cc:Agent> + <dc:title>Jean Cartier</dc:title> + </cc:Agent> + </dc:creator> + <cc:license + rdf:resource="http://creativecommons.org/licenses/publicdomain/" /> + <dc:language>fr-FR</dc:language> + <dc:subject> + <rdf:Bag> + <rdf:li>hardware</rdf:li> + <rdf:li>computer</rdf:li> + <rdf:li>desk</rdf:li> + <rdf:li>business</rdf:li> + </rdf:Bag> + </dc:subject> + <dc:contributor> + <cc:Agent> + <dc:title>Jean-Victor Balin (jean.victor.balin@gmail.com)</dc:title> + </cc:Agent> + </dc:contributor> + <dc:description>http://www.jcartier.net</dc:description> + </cc:Work> + <cc:License + rdf:about="http://creativecommons.org/licenses/publicdomain/"> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Reproduction" /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#Distribution" /> + <cc:permits + rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /> + </cc:License> + </rdf:RDF> + </metadata> + <g + inkscape:label="Calque 1" + inkscape:groupmode="layer" + id="layer1"> + <path + style="opacity:1;fill:#888888;fill-opacity:1;stroke:none;stroke-width:0.69999999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + d="" + id="path8460" /> + <path + style="opacity:1;fill:#888888;fill-opacity:1;stroke:none;stroke-width:0.69999999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + d="" + id="path8462" /> + <path + style="opacity:1;fill:#888888;fill-opacity:1;stroke:none;stroke-width:0.69999999;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + d="" + id="path8464" /> + <g + id="g11262" + transform="translate(-279.1135,-276.31506)"> + <g + id="g9079"> + <path + style="fill:#edd400;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + id="path4277" + sodipodi:nodetypes="cccccc" + d="M 305.1135,316.85015 L 281.1135,304.85015 L 281.1135,302.85015 L 281.12856,302.82044 L 305.1135,314.85015 L 305.1135,316.85015 z" /> + <path + style="fill:#c4a000;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + id="path4273" + sodipodi:nodetypes="czzzz" + d="M 281.1135,302.85016 C 281.1135,301.85017 282.1135,301.85015 283.1135,301.85015 C 284.1135,301.85015 307.1135,313.28194 307.1135,313.85015 C 307.1135,314.41836 306.1135,314.85015 305.1135,314.85015 C 304.1135,314.85015 281.1135,303.85015 281.1135,302.85016 z" /> + <path + style="fill:#fce94f;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + id="path4275" + d="M 307.1135,313.85015 L 307.1135,315.85015 L 305.1135,316.85015 L 305.1135,314.85015 L 307.1135,313.85015 z" /> + <path + id="path6219" + d="M 283.125,301.34375 C 282.625,301.34375 282.09524,301.33284 281.59375,301.5 C 281.343,301.58358 281.09119,301.73097 280.90625,301.96875 C 280.72131,302.20653 280.625,302.51042 280.625,302.84375 C 280.625,303.84375 280.62501,303.84374 280.625,304.84375 C 280.625,305.11458 280.73624,305.2247 280.8125,305.3125 C 280.88876,305.4003 280.96438,305.45244 281.0625,305.53125 C 281.25873,305.68886 281.51944,305.85868 281.84375,306.0625 C 282.49237,306.47015 283.40886,307.00501 284.5,307.59375 C 286.68227,308.77123 289.58992,310.24813 292.53125,311.6875 C 295.47258,313.12687 298.41606,314.51298 300.71875,315.5625 C 301.87009,316.08726 302.86048,316.5374 303.59375,316.84375 C 303.96038,316.99693 304.2633,317.10187 304.5,317.1875 C 304.61835,317.23031 304.72198,317.2866 304.8125,317.3125 C 304.90302,317.3384 304.97917,317.34375 305.125,317.34375 C 305.625,317.34375 306.15476,317.35466 306.65625,317.1875 C 306.907,317.10392 307.15881,316.98778 307.34375,316.75 C 307.52869,316.51222 307.625,316.17708 307.625,315.84375 C 307.625,314.84375 307.62501,314.84374 307.625,313.84375 C 307.625,313.57292 307.51376,313.49405 307.4375,313.40625 C 307.36124,313.31845 307.25437,313.23506 307.15625,313.15625 C 306.96002,312.99864 306.69931,312.82882 306.375,312.625 C 305.72638,312.21735 304.84114,311.71374 303.75,311.125 C 301.56773,309.94752 298.66008,308.47062 295.71875,307.03125 C 292.77742,305.59188 289.80269,304.17452 287.5,303.125 C 286.34866,302.60024 285.35827,302.18135 284.625,301.875 C 284.25836,301.72182 283.95545,301.58563 283.71875,301.5 C 283.6004,301.45719 283.49677,301.43215 283.40625,301.40625 C 283.31573,301.38035 283.27084,301.34375 283.125,301.34375 z M 283.125,302.34375 C 283.04167,302.34375 283.06983,302.35921 283.125,302.375 C 283.18017,302.39079 283.26996,302.3995 283.375,302.4375 C 283.58508,302.51349 283.89203,302.63169 284.25,302.78125 C 284.96595,303.08037 285.91697,303.50914 287.0625,304.03125 C 289.35356,305.07548 292.34758,306.50187 295.28125,307.9375 C 298.21492,309.37313 301.11977,310.83373 303.28125,312 C 304.36199,312.58313 305.23065,313.08343 305.84375,313.46875 C 306.1503,313.66141 306.39008,313.82412 306.53125,313.9375 C 306.58419,313.98002 306.60398,314.01038 306.625,314.03125 C 306.625,314.7835 306.625,314.9375 306.625,315.84375 C 306.625,316.01041 306.59631,316.08153 306.5625,316.125 C 306.52869,316.16847 306.46801,316.20858 306.34375,316.25 C 306.09524,316.33284 305.625,316.34375 305.125,316.34375 C 305.20833,316.34375 305.14892,316.35954 305.09375,316.34375 C 305.03858,316.32796 304.94879,316.288 304.84375,316.25 C 304.63367,316.174 304.35797,316.05581 304,315.90625 C 303.28405,315.60713 302.30178,315.17836 301.15625,314.65625 C 298.86519,313.61202 295.90242,312.21688 292.96875,310.78125 C 290.03508,309.34562 287.13023,307.88502 284.96875,306.71875 C 283.88801,306.13562 282.9881,305.60407 282.375,305.21875 C 282.06845,305.02609 281.82867,304.86338 281.6875,304.75 C 281.64797,304.71825 281.64745,304.70854 281.625,304.6875 C 281.625,303.89841 281.625,303.76562 281.625,302.84375 C 281.625,302.67708 281.65369,302.63722 281.6875,302.59375 C 281.72131,302.55028 281.782,302.47892 281.90625,302.4375 C 282.15476,302.35466 282.625,302.34375 283.125,302.34375 z" + style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + sodipodi:nodetypes="ccccc" + id="path9069" + d="M 284.2086,277.50524 L 304.25155,287.56045 L 304.04909,313.06966 L 284.14111,303.2169 L 284.2086,277.50524 z" + style="fill:#edd400;fill-opacity:1;stroke:none;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4" /> + <path + id="path4269" + d="M 284.40625,277.28125 C 284.30032,277.26783 284.18881,277.2693 284.03125,277.3125 C 283.95247,277.3341 283.84394,277.3678 283.75,277.46875 C 283.65606,277.5697 283.625,277.72396 283.625,277.84375 C 283.625,278.84375 283.62501,301.84378 283.625,302.84375 C 283.625,303.08333 283.712,303.19301 283.78125,303.28125 C 283.8505,303.36949 283.91245,303.42354 284,303.5 C 284.1751,303.65292 284.40433,303.84112 284.6875,304.03125 C 285.25384,304.41151 286.0564,304.87154 287,305.40625 C 288.8872,306.47566 291.36904,307.80952 293.875,309.0625 C 296.38096,310.31548 298.89194,311.4938 300.78125,312.3125 C 301.7259,312.72185 302.51771,313.02948 303.09375,313.21875 C 303.38177,313.31339 303.60063,313.37941 303.8125,313.40625 C 303.91843,313.41967 304.02994,313.44945 304.1875,313.40625 C 304.26628,313.38465 304.37481,313.3197 304.46875,313.21875 C 304.56269,313.1178 304.625,312.96354 304.625,312.84375 C 304.625,311.84375 304.62501,288.84378 304.625,287.84375 C 304.625,287.60417 304.50675,287.52574 304.4375,287.4375 C 304.36825,287.34926 304.3063,287.26396 304.21875,287.1875 C 304.04365,287.03458 303.81442,286.87763 303.53125,286.6875 C 302.96491,286.30724 302.1936,285.81596 301.25,285.28125 C 299.3628,284.21184 296.84971,282.90923 294.34375,281.65625 C 291.83779,280.40327 289.32681,279.22495 287.4375,278.40625 C 286.49285,277.9969 285.70104,277.65802 285.125,277.46875 C 284.83698,277.37412 284.61812,277.30809 284.40625,277.28125 z M 284.625,278.375 C 284.69989,278.39599 284.72017,278.40716 284.8125,278.4375 C 285.33021,278.6076 286.1009,278.90935 287.03125,279.3125 C 288.89195,280.1188 291.38096,281.31548 293.875,282.5625 C 296.36904,283.80952 298.8872,285.10066 300.75,286.15625 C 301.6814,286.68404 302.44134,287.14588 302.96875,287.5 C 303.23246,287.67706 303.44463,287.83456 303.5625,287.9375 C 303.60179,287.97181 303.60764,287.98161 303.625,288 C 303.625,289.23981 303.625,310.53023 303.625,312.34375 C 303.54164,312.32109 303.51404,312.31667 303.40625,312.28125 C 302.88854,312.11114 302.11785,311.8094 301.1875,311.40625 C 299.32681,310.59995 296.83779,309.40327 294.34375,308.15625 C 291.84971,306.90923 289.3628,305.58684 287.5,304.53125 C 286.5686,304.00346 285.77741,303.54162 285.25,303.1875 C 284.98629,303.01044 284.77412,302.85294 284.65625,302.75 C 284.6366,302.73284 284.63952,302.73275 284.625,302.71875 C 284.625,301.5318 284.625,280.23986 284.625,278.375 z" + style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + sodipodi:nodetypes="ccccc" + id="rect9054" + d="M 6.9509201,4.1595091 L 23.01227,12.122699 L 23.079754,33.852761 L 6.8159508,25.754602 L 6.9509201,4.1595091 z" + style="fill:#d3d7cf;fill-opacity:1;stroke:none;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4" + transform="translate(279.1135,276.31506)" /> + <path + id="path4271" + d="M 286.3125,280.25 C 286.21212,280.24665 286.09177,280.27159 285.9375,280.34375 C 285.78323,280.41591 285.625,280.64583 285.625,280.84375 C 285.625,281.92814 285.62501,300.84378 285.625,301.84375 C 285.625,302.04167 285.68994,302.16185 285.75,302.25 C 285.81006,302.33815 285.86335,302.39707 285.9375,302.46875 C 286.0858,302.6121 286.2698,302.77307 286.5,302.9375 C 286.96041,303.26636 287.61754,303.6519 288.375,304.09375 C 289.88992,304.97745 291.86755,306.05877 293.875,307.0625 C 295.88245,308.06623 297.88804,308.99241 299.40625,309.625 C 300.16535,309.94129 300.80595,310.1767 301.28125,310.3125 C 301.5189,310.3804 301.7055,310.43081 301.90625,310.4375 C 302.00663,310.44085 302.12698,310.44716 302.28125,310.375 C 302.43552,310.30284 302.625,310.04167 302.625,309.84375 C 302.625,308.84375 302.62501,289.84378 302.625,288.84375 C 302.625,288.64583 302.52881,288.5569 302.46875,288.46875 C 302.40869,288.3806 302.3554,288.29043 302.28125,288.21875 C 302.13295,288.0754 301.94895,287.94568 301.71875,287.78125 C 301.25834,287.45239 300.63246,287.0356 299.875,286.59375 C 298.36008,285.71005 296.3512,284.65998 294.34375,283.65625 C 292.3363,282.65252 290.33071,281.69508 288.8125,281.0625 C 288.0534,280.74621 287.4128,280.5108 286.9375,280.375 C 286.69985,280.3071 286.51325,280.25669 286.3125,280.25 z M 286.625,281.34375 C 286.64007,281.34783 286.64058,281.33927 286.65625,281.34375 C 287.05595,281.45795 287.66535,281.69129 288.40625,282 C 289.88804,282.61741 291.88245,283.56623 293.875,284.5625 C 295.86755,285.55877 297.88992,286.60245 299.375,287.46875 C 300.11754,287.9019 300.71041,288.29761 301.125,288.59375 C 301.3323,288.74182 301.50767,288.85429 301.59375,288.9375 C 301.62024,288.9631 301.61381,288.98685 301.625,289 C 301.625,290.22815 301.625,307.64251 301.625,309.375 C 301.59302,309.36691 301.59777,309.35383 301.5625,309.34375 C 301.1628,309.22955 300.5534,308.99621 299.8125,308.6875 C 298.33071,308.07009 296.3363,307.15252 294.34375,306.15625 C 292.3512,305.15998 290.36008,304.08505 288.875,303.21875 C 288.13246,302.7856 287.50834,302.42114 287.09375,302.125 C 286.88645,301.97693 286.74233,301.83321 286.65625,301.75 C 286.6397,301.734 286.63577,301.7306 286.625,301.71875 C 286.625,300.54277 286.625,283.20004 286.625,281.34375 z" + style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + id="path4279" + d="M 294,281.34375 L 293.875,281.40625 L 291.875,282.40625 L 292.34375,283.3125 L 294.28125,282.34375 L 294.625,282.34375 L 294.625,283.4375 L 295.625,283.4375 L 295.625,281.84375 L 295.625,281.34375 L 295.125,281.34375 L 294.125,281.34375 L 294,281.34375 z" + style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + id="path4281" + d="M 293.625,311.84375 L 293.625,315.6875 L 290.6875,320.59375 L 291.53125,321.09375 L 294.53125,316.09375 L 294.625,316 L 294.625,315.84375 L 294.625,311.84375 L 293.625,311.84375 z" + style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + sodipodi:nodetypes="cccccccc" + id="path4283" + d="M 293.6875,316.09375 L 296.6875,321.09375 L 297.53125,320.59375 L 295.21875,316.71875 L 296.96875,317.3125 L 297.28125,316.375 L 294.28125,315.375 L 293.6875,316.09375 z" + style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + </g> + <text + xml:space="preserve" + style="font-size:22.81445312px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:start;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;font-family:Arial Black;-inkscape-font-specification:Arial Black" + x="277.74319" + y="272.51889" + id="text2998" + sodipodi:linespacing="125%" + transform="matrix(1.0132707,0.11796914,0.02070387,0.98931351,0,0)"><tspan + sodipodi:role="line" + id="tspan3000" + x="277.74319" + y="272.51889">2</tspan></text> + </g> + </g> +</svg> diff --git a/static/js/agenda/agenda_edit.js b/static/js/agenda/agenda_edit.js index bcac2b6a8..08230dab0 100644 --- a/static/js/agenda/agenda_edit.js +++ b/static/js/agenda/agenda_edit.js @@ -19,16 +19,21 @@ //////////////-GLOBALS----//////////////////////////////////////// -var meeting_number = 0; // is the meeting name. -var schedule_id = 0; // what is the schedule we are editing. -var schedule_owner_href = ''; // who owns this schedule -var is_secretariat = false; -var meeting_objs = {}; // contains a list of session objects -- by session_id -var session_objs = {}; // contains a list of session objects -- by session_name -var slot_status = {}; // indexed by domid, contains an array of ScheduledSessions objects -var slot_objs = {}; // scheduledsession indexed by id. +// these need to be setup in landscape_edit's setup_slots() inline function: +//var meeting_number = 0; // is the meeting name. +//var schedule_id = 0; // what is the schedule we are editing. +//var schedule_name; // what is the schedule we are editing. +//var schedule_owner_href = ''; // who owns this schedule +//var scheduledsession_post_href; +//var meeting_base_url; +//var site_base_url; +//var total_rooms = 0; // the number of rooms +//var total_days = 0; // the number of days + +var is_secretariat = false; + +var agenda_globals; -var group_objs = {}; // list of working groups var area_directors = {}; // list of promises of area directors, index by href. var read_only = true; // it is true until we learn otherwise. @@ -47,9 +52,7 @@ var last_json_txt = ""; // last txt from a json call. var last_json_reply = []; // last parsed content var hidden_rooms = []; -var total_rooms = 0; // the number of rooms var hidden_days = []; -var total_days = 0; // the number of days /****************************************************/ @@ -67,21 +70,38 @@ $(document).ready(function() { This is ran at page load and sets up the entire page. */ function initStuff(){ + agenda_globals = new AgendaGlobals(); + //agenda_globals.__debug_session_move = true; + log("initstuff() running..."); - setup_slots(); - directorpromises = mark_area_directors(); + var directorpromises = []; + + /* define a slot for unscheduled items */ + var unassigned = new ScheduledSlot(); + unassigned.make_unassigned(); + + setup_slots(directorpromises); + mark_area_directors(directorpromises); log("setup_slots() ran"); droppable(); + log("droppable() ran"); $.when.apply($,directorpromises).done(function() { - /* can not load events until area director info has been loaded */ - log("droppable() ran"); + /* can not load events until area director info, + timeslots, sessions, and scheduledsessions + have been loaded + */ + log("loading/linking objects"); load_events(); log("load_events() ran"); find_meeting_no_room(); + calculate_name_select_box(); + calculate_room_select_box(); listeners(); droppable(); duplicate_sessions = find_double_timeslots(); + empty_info_table(); + count_sessions(); if(load_conflicts) { recalculate(null); @@ -90,7 +110,6 @@ function initStuff(){ static_listeners(); log("listeners() ran"); - calculate_name_select_box(); start_spin(); @@ -99,7 +118,7 @@ function initStuff(){ read_only_check(); stop_spin(); - meeting_objs_length = Object.keys(meeting_objs).length; + meeting_objs_length = Object.keys(agenda_globals.meeting_objs).length; /* Comment this out for fast loading */ //load_conflicts = false; @@ -134,7 +153,8 @@ function read_only_result(msg) { // XX go fetch the owner and display it. console.log("owner href:", schedule_owner_href); - empty_info_table(); + $("#pageloaded").show(); + listeners(); droppable(); } diff --git a/static/js/agenda/agenda_helpers.js b/static/js/agenda/agenda_helpers.js index f1402ab40..c132496a0 100644 --- a/static/js/agenda/agenda_helpers.js +++ b/static/js/agenda/agenda_helpers.js @@ -39,83 +39,25 @@ function log(text){ console.log(text); } - -/* move_slot - Moves a meeting(from) to a slot (to). - No checks are done to see if the slot it's moving to is free, - this can be considered a method of forcing a slot to a place. - - @params: - 'from' - meeting key (searching in meeting_objs[]) - 'to' - slot_status key (searching in slot_status[]) - -*/ - - - -var gfrom = null; -function move_slot(from,to){ - console.log("!!!"); - var meeting = meeting_objs[from]; - var from_slot = meeting_objs[from].slot_status_key; - - var to_slot = slot_status[to]; - - console.log(meeting_objs[from]); - console.log(from_slot); - - var result = update_to_slot(from, to, true); // true if the job succeeded - - if(result){ - if(update_from_slot(from,from_slot)){ - console.log("move_slot: success"); - }else{ - console.log("move_slot: fail"); - } - } - - meeting_objs[from].slot_status_key = to; - //***** do dajaxice call here ****** // - - var eTemplate = meeting.event_template() - - $("#session_"+from).remove(); - $("#"+to).append(eTemplate); - - var session_id = from; - var scheduledsession_id = slot_status[to].scheduledsession_id; - console.log(session_id); - console.log(scheduledsession_id); - // start_spin(); - Dajaxice.ietf.meeting.update_timeslot(dajaxice_callback, - { - 'schedule_id':schedule_id, - 'session_id':session_id, - 'scheduledsession_id': scheduledsession_id, - }); - - -} - function print_all(){ console.log("all"); - console.log(meeting_objs.length); - for(var i=0; i<meeting_objs.length; i++){ - meeting_objs[i].print_out(); + console.log(agenda_globals.meeting_objs.length); + for(var i=0; i<agenda_globals.meeting_objs.length; i++){ + agenda_globals.meeting_objs[i].print_out(); } } function find_title(title){ - $.each(meeting_objs, function(key){ - if (meeting_objs[key].title == title) { - console.log(meeting_objs[key]); + $.each(agenda_globals.meeting_objs, function(key){ + if (agenda_globals.meeting_objs[key].title == title) { + console.log(agenda_globals.meeting_objs[key]); } }); } function find_session_id(session_id){ - $.each(meeting_objs, function(key){ - if (meeting_objs[key].session_id == session_id) { - console.log(meeting_objs[key]); + $.each(agenda_globals.meeting_objs, function(key){ + if (agenda_globals.meeting_objs[key].session_id == session_id) { + console.log(agenda_globals.meeting_objs[key]); } }); } @@ -123,7 +65,7 @@ function find_session_id(session_id){ function find_same_area(area){ var areas = [] area = area.toUpperCase(); - $.each(meeting_objs, function(index,obj){ + $.each(agenda_globals.meeting_objs, function(index,obj){ if(obj.area == area){ areas.push({id:index,slot_status_key:obj.slot_status_key}) } @@ -138,6 +80,8 @@ function style_empty_slots(){ var __debug_load_events = false; /* this pushes every event into the agendas */ function load_events(){ + var slot_id; + console.log("load events..."); /* first delete all html items that might have gotten saved if @@ -146,17 +90,38 @@ function load_events(){ if(__debug_load_events) { console.log("processing double slot status relations"); } - $.each(slot_status, function(key) { - ssid_arr = slot_status[key]; + + /* clear out all the timeslots */ + $.each(agenda_globals.timeslot_bydomid, function(key) { + insert_cell(key, "", true); + + var timeslot = agenda_globals.timeslot_bydomid[key]; + slot_id = ("#"+key); + + $(slot_id).addClass("agenda_slot_" + timeslot.roomtype); + + if(timeslot.roomtype == "unavail") { + $(slot_id).removeClass("ui-droppable"); + $(slot_id).removeClass("free_slot"); + $(slot_id).addClass("agenda_slot_unavailable"); + } else { + $(slot_id).removeClass("agenda_slot_unavailable"); + $(slot_id).addClass("ui-droppable"); + } + }); + + $.each(agenda_globals.slot_status, function(key) { + ssid_arr = agenda_globals.slot_status[key]; for(var q = 0; q<ssid_arr.length; q++){ ssid = ssid_arr[q]; - insert_cell(ssid.domid, "", true); + + ssid.connect_to_timeslot_session(); // also see if the slots have any declared relationship, and take it forward as // well as backwards. if(ssid.extendedfrom_id != false) { - other = slot_objs[ssid.extendedfrom_id]; + other = agenda_globals.slot_objs[ssid.extendedfrom_id]; if(__debug_load_events) { console.log("slot:",ssid.scheduledsession_id, "extended from: ",key,ssid.extendedfrom_id); // ," is: ", other); } @@ -177,8 +142,8 @@ function load_events(){ if(__debug_load_events) { console.log("marking extended slots for slots with multiple sessions"); } - $.each(slot_status, function(key) { - ssid_arr = slot_status[key]; + $.each(agenda_globals.slot_status, function(key) { + ssid_arr = agenda_globals.slot_status[key]; var extendedto = undefined; for(var q = 0; q<ssid_arr.length; q++){ @@ -186,7 +151,7 @@ function load_events(){ if(extendedto == undefined && ssid.extendedto != undefined) { if(__debug_load_events) { - console.log("ssid",ssid.session_id,"extended"); + console.log("ssid",ssid.session_id,"extended 1"); } extendedto = ssid.extendedto; } @@ -195,7 +160,7 @@ function load_events(){ ssid = ssid_arr[q]; ssid.extendedto = extendedto; if(__debug_load_events) { - console.log("ssid",ssid.session_id,"extended"); + console.log("ssid",ssid.session_id,"extended 2"); } } }); @@ -203,35 +168,26 @@ function load_events(){ if(__debug_load_events) { console.log("finding responsible ad"); } - $.each(meeting_objs, function(key) { - session = meeting_objs[key]; + $.each(agenda_globals.meeting_objs, function(key) { + session = agenda_globals.meeting_objs[key]; session.find_responsible_ad(); }); - $.each(slot_status, function(key) { - ssid_arr = slot_status[key] + $.each(agenda_globals.slot_status, function(key) { + ssid_arr = agenda_globals.slot_status[key] if(key == "sortable-list"){ console.log("sortable list"); }else { - for(var q = 0; q<ssid_arr.length; q++){ ssid = ssid_arr[q]; - slot_id = ("#"+ssid.domid); + slot_id = ("#"+ssid.domid()); if(__debug_load_events) { console.log("populating slot: ",slot_id,key); } - /* also, since we are HERE, set the class to indicate if slot is available */ - $(slot_id).addClass("agenda_slot_" + ssid.roomtype); - if(ssid.roomtype == "unavail") { - $(slot_id).removeClass("ui-droppable"); - $(slot_id).removeClass("free_slot"); - $(slot_id).addClass("agenda_slot_unavailable"); - } else { - $(slot_id).removeClass("agenda_slot_unavailable"); - $(slot_id).addClass("ui-droppable"); - session = meeting_objs[ssid.session_id]; + if(ssid.timeslot.roomtype != "unavail") { + session = agenda_globals.meeting_objs[ssid.session_id]; if (session != null) { if(ssid.extendedto != undefined) { session.double_wide = true; @@ -250,7 +206,7 @@ function load_events(){ session.populate_event(key); } - session.placed(ssid, false); + session.placed(ssid.timeslot, false, ssid); } else { $(slot_id).addClass('free_slot'); } @@ -259,8 +215,8 @@ function load_events(){ } }); - $.each(meeting_objs, function(key) { - session = meeting_objs[key]; + $.each(agenda_globals.meeting_objs, function(key) { + session = agenda_globals.meeting_objs[key]; // note in the group, what the set of column classes is. // this is an array, as the group might have multiple @@ -278,18 +234,15 @@ function load_events(){ function check_free(inp){ var empty = false; - slot = slot_status[inp.id]; + slot = agenda_globals.timeslot_bydomid[inp.id]; if(slot == null){ -// console.log("\t from check_free, slot is null?", inp,inp.id, slot_status[inp.id]); + //console.log("\t from check_free, slot is null?", inp,inp.id, agenda_globals.slot_status[inp.id]); return false; } - for(var i =0;i<slot.length; i++){ - if (slot[i].empty == false || slot[i].empty == "False"){ - return false; - } + if (slot.empty == false) { + return false; } return true; - } /* clears any background highlight colors of scheduled sessions */ @@ -308,7 +261,7 @@ function clear_highlight(inp_arr){ // @args: array from slot_status{} /* based on any meeting object, it finds any other objects inside the same timeslot. */ function find_friends(inp){ var ts = $(inp).parent().attr('id'); - var ss_arr = slot_status[ts]; + var ss_arr = agenda_globals.slot_status[ts]; if (ss_arr != null){ return ss_arr; } @@ -320,7 +273,7 @@ function find_friends(inp){ function json_to_id(j){ - return (j.room+"_"+j.date+"_"+j.time); + return (j.room()+"_"+j.date()+"_"+j.time()); } function id_to_json(id){ @@ -359,6 +312,10 @@ function empty_info_table(){ } $("#info_responsible").html(""); $("#info_requestedby").html(""); + $("#agenda_requested_features").html(""); + + /* need to reset listeners, because we just changed the HTML */ + listeners(); } @@ -367,49 +324,83 @@ var temp_1; takes in a json. */ +function compare_timeslot(a,b) { + //console.log("day: a,b", a.day, b.day); + + // sometimes (a.day==b.say)==false and (a.day===b.day)==false, + // for days that appear identical, but built from different strings, + // yet (a.day-b.day)==0. + if((a.day - b.day) == 0) { + //console.log("time: a,b", a.starttime, b.starttime); + if(a.starttime == b.starttime) { + //console.log("room: a,b", a.room, b.room, a.room < b.room); + if(a.room > b.room) { + return 1; + } else { + return -1; + } + }; + if(a.starttime > b.starttime) { + return 1; + } else { + return -1; + } + } + if(a.day > b.day) { + return 1; + } else { + return -1; + } +} + var room_select_html = ""; function calculate_room_select_box() { var html = "<select id='info_location_select'>"; + var mobj_array = []; - var keys = Object.keys(slot_status) - var sorted = keys.sort(function(a,b) { - a1=slot_status[a]; - b1=slot_status[b]; - if (a1.date != b1.date) { - return a1.date-b1.date; - } - return a1.time - b1.time; - }); + $.each(agenda_globals.timeslot_byid, function(key, value){ + mobj_array.push(value) + }); - for (n in sorted) { - var k1 = sorted[n]; - var val_arr = slot_status[k1]; + var sorted = mobj_array.sort(compare_timeslot); + var lastone_id = undefined; - /* k1 is the slot_status key */ - /* v1 is the slot_obj */ - for(var i = 0; i<val_arr.length; i++){ - var v1 = val_arr[i]; - html=html+"<option value='"+k1; - html=html+"' id='info_location_select_option_"; - html=html+v1.timeslot_id+"'>"; - html=html+v1.short_string; - html=html+"</option>"; - } - } + $.each(sorted, function(index, value) { + // this check removes duplicates from the list, if there are any. + if(value.roomtype == "break" || value.roomtype=="reg") { + return; + } + if(value.timeslot_id == lastone_id) { + return; // from subfunction. + } + //console.log("room_select_html", index, value, value.short_string); + html=html+"<option value='"+value.timeslot_id; + html=html+"' id='info_location_select_option_"; + html=html+value.timeslot_id+"'>"; + html=html+value.short_string; + if(value.roomtype != "session") { + html = html+ "(" + value.roomtype + ")"; + } + html=html+"</option>"; + lastone_id = value.timeslot_id; + }); html = html+"</select>"; room_select_html = html; - + return room_select_html; } -var name_select_html = ""; + +var name_select_html = undefined; var temp_sorted = null; function calculate_name_select_box(){ var html = "<select id='info_name_select'>"; - var keys = Object.keys(meeting_objs) - var mobj_array = [] - $.each(meeting_objs, function(key, value){ mobj_array.push(value) }); + var mobj_array = []; + var mobj_array2; + $.each(agenda_globals.meeting_objs, function(key, value){ mobj_array.push(value) }); mobj_array2 = mobj_array.sort(function(a,b) { return a.title.localeCompare(b.title); }); - temp_sorted =mobj_array; - for(var i = 0; i < mobj_array.length; i++){ + + var mlen = mobj_array.length; + console.log("calculate name_select box with",mlen,"objects"); + for(var i = 0; i < mlen; i++){ //console.log("select box mobj["+i+"]="+mobj_array[i]); // html=html+"<option value='"+mobj_array[i].slot_status_key; html=html+"<option value='"+mobj_array[i].session_id; @@ -435,13 +426,15 @@ function calculate_name_select_box(){ html = html+"</select>"; name_select_html = html; - - + return html; } function generate_select_box(){ + if(!room_select_html) { + calculate_name_select_box(); + } return room_select_html; } @@ -472,23 +465,10 @@ function insert_cell(js_room_id, text, replace){ } -function find_empty_test(){ - $.each(slot_status, function(key){ - ss_arr = slot_status[key]; - for(var i = 0; i < ss_arr.length; i++){ - if(ss_arr[i].scheduledsession_id == null || ss_arr[i].session_id == null){ - console.log(ss_arr[i]); - } - } - }) - -} - - function find_meeting_no_room(){ - $.each(meeting_objs, function(key){ - if(meeting_objs[key].slot_status_key == null) { - session = meeting_objs[key] + $.each(agenda_globals.meeting_objs, function(key){ + if(agenda_globals.meeting_objs[key].slot_status_key == null) { + session = agenda_globals.meeting_objs[key] session.slot_status_key = null; session.populate_event(bucketlist_id); } @@ -509,10 +489,10 @@ function find_meeting_no_room(){ function find_double_timeslots(){ var duplicate = {}; - $.each(slot_status, function(key){ - for(var i =0; i<slot_status[key].length; i++){ + $.each(agenda_globals.slot_status, function(key){ + for(var i =0; i<agenda_globals.slot_status[key].length; i++){ // goes threw all the slots - var ss_id = slot_status[key][i].session_id; + var ss_id = agenda_globals.slot_status[key][i].session_id; if(duplicate[ss_id]){ duplicate[ss_id]['count']++; duplicate[ss_id]['ts'].push(key); @@ -533,7 +513,6 @@ function find_double_timeslots(){ } }); return dup; - } @@ -564,6 +543,7 @@ function auto_remove(){ } + /* for the spinnner */ /* spinner code from: @@ -629,13 +609,14 @@ function start_spin(opts){ // $("#schedule_name").hide(); $("#spinner").show(); $("#spinner").spin({lines:16, radius:8, length:16, width:4}); - + $("#pageloaded").hide(); } function stop_spin(){ //spinner $("#schedule_name").show(); $("#spinner").hide(); $("#spinner").spin(false); + $("#pageloaded").show(); } /* diff --git a/static/js/agenda/agenda_listeners.js b/static/js/agenda/agenda_listeners.js index efa1afa11..0db9e98da 100644 --- a/static/js/agenda/agenda_listeners.js +++ b/static/js/agenda/agenda_listeners.js @@ -18,13 +18,18 @@ var bucketlist_id = "sortable-list" // for if/when the id for bucket list change function resize_listeners() { for(i = 0; i<days.length;i++){ $("#resize-"+days[i]+"-spacer").resizable({maxHeight:10, - handles: "e, s", + handles: "e", minWidth:2, }); } + $("#session-info").resizable({handles: "s", + minWidth:"100%", + containment: "parent" + }); + } @@ -45,6 +50,9 @@ function listeners(){ $('#info_location_select').unbind('change'); $('#info_location_select').change(info_location_select_change); + $('#info_location_set').unbind('click'); + $('#info_location_set').click(info_location_select_set); + $('#info_name_select').unbind('change'); $('#info_name_select').change(info_name_select_change); @@ -58,10 +66,6 @@ function listeners(){ $('#double_slot').unbind('click'); - /* listener for when one clicks the 'show all' checkbox */ - $('.cb_all_conflict').unbind('click'); - $('.cb_all_conflict').click(cb_all_conflict); - /* hiding rooms */ $(".close_room").unbind('click'); $(".close_room").click(close_room) @@ -70,11 +74,6 @@ function listeners(){ $(".close_day").unbind('click'); $(".close_day").click(close_day); - // $("#show_hidden_days").unbind('click'); - // $("#show_hidden_days").click(show_hidden_days); - // $("#show_hidden_rooms").unbind('click'); - // $("#show_hidden_rooms").click(show_hidden_rooms); - $("#show_all_area").unbind('click'); $("#show_all_area").click(show_all_area); @@ -117,6 +116,7 @@ function toggle_dialog(parameters) { $( this ).dialog( "close" ); __debug_toggle_result = "yes"; parameters.session.double_wide = false; + parameters.same_timeslot = true; move_slot(parameters); }, //"Swap Slots": function(){ @@ -137,6 +137,7 @@ function resize_th(){ /* with the hovering rooms the sizes get messed up this function looks at the tr's height and resizes the room's height */ $.each($(".vert_time"), function(k,v){ + $(v).offset({ left: 2}); $(v).height($("#"+v.parentElement.id).height()-2); /* -2 so the grid remains */ }) } @@ -168,16 +169,8 @@ function all_click(event){ } /************ click functions *********************************************************************/ -function cb_all_conflict(event){ - var conflict_clicked = $(event.target).attr('id'); - try{ - var conflict_clicked = conflict_clicked.substr(3); - }catch(err){ - - } - $("."+conflict_clicked+" input").click(); - - +function update_room_count() { + $("#hidden_rooms").html((hidden_rooms.length.toString()+"/"+total_rooms.toString())); } function close_room(event){ @@ -186,7 +179,7 @@ function close_room(event){ //console.log("close_room",close_room); $("#"+close_room).hide("fast"); hidden_rooms.push("#"+close_room); - $("#hidden_rooms").html((hidden_rooms.length.toString()+"/"+total_rooms.toString())); + update_room_count(); } function show_hidden_rooms(event){ @@ -194,7 +187,11 @@ function show_hidden_rooms(event){ $(room).show("fast"); }); hidden_rooms = []; - $("#hidden_rooms").html(hidden_rooms.length.toString()+"/"+total_rooms.toString()); + update_room_count(); +} + +function update_day_count() { + $("#hidden_days").html(hidden_days.length.toString()+"/"+total_days.toString()); } function close_day(event){ @@ -203,7 +200,7 @@ function close_day(event){ close_day = ".day_"+close_day; $(close_day).hide("slow"); hidden_days.push(close_day); - $("#hidden_days").html(hidden_days.length.toString()+"/"+total_days.toString()); + update_day_count(); } function show_all(){ @@ -216,8 +213,7 @@ function show_hidden_days(event){ $(room).show("fast"); }); hidden_days = []; - $("#hidden_days").html(hidden_days.length.toString()+"/"+total_days.toString()); - + update_day_count(); } function show_all_area(event){ @@ -232,6 +228,11 @@ function show_all_area(event){ }); } +function show_all_session(event){ + var session_class = "." + last_session.short_name; + + $(session_class).parent().parent().parent().effect("highlight", {color:"lightcoral"}, 5000); +} /************ END click functions *********************************************************************/ @@ -250,19 +251,25 @@ function slot_item_hidden(selector){ -var free_slots = []; -function find_empty_slot(){ +function find_empty_slot(session) { + var free_slots = []; - $.each($(".free_slot"), function(index,item){ - if(!$(item).hasClass("show_conflict_view_highlight")){ - free_slots.push(item); + $.each(agenda_globals.timeslot_byid, function(id, slot) { + if(slot.empty && !slot.unscheduled_box) { + free_slots.push(slot); } }); - var perfect_slots = []; // array of slots that have a capacity higher than the session. + + //console.log("free_slot list", free_slots); + var target_capacity = session.attendees; + + // array of slots that have a capacity higher than the session. + var perfect_slots = []; + if(free_slots.length > 0){ for(var i = 0; i< free_slots.length; i++){ - if(parseInt($(free_slots[i]).attr("capacity")) >= parseInt(meeting_objs[current_item.attr('session_id')].attendees) ){ + if(free_slots[i].capacity >= target_capacity) { perfect_slots.push(free_slots[i]); } } @@ -272,55 +279,62 @@ function find_empty_slot(){ else{ return free_slots[0]; // just return the first one. } - } else{ return null; } - } function extend_slot(event) { // event is just the button push, ignore it. - session = last_session; - slot = session.slot; + session = last_session; - console.log("session", session.title, "slot:", slot.scheduledsession_id); + console.log("session", session.title, "sslot:", current_scheduledslot.scheduledsession_id); + + /* bind current_timeslot into this function and continuations */ + var slot = current_timeslot; // determine if this slot can be extended. - if(slot.can_extend_right()) { + if(current_timeslot.can_extend_right()) { $("#can-extend-dialog").html("Extend "+session.title+" to slot "+slot.following_timeslot.domid); $("#can-extend-dialog").dialog({ resizable: true, modal: true, + dialogClass: "extend-dialog", buttons: { - "Yes": function() { - Dajaxice.ietf.meeting.update_timeslot(dajaxice_callback, - { - 'schedule_id':schedule_id, - 'session_id': session.session_id, - 'scheduledsession_id': slot.following_timeslot.scheduledsession_id, - 'extended_from_id': slot.scheduledsession_id - }); - slot.extendedto = slot.following_timeslot; - slot.extendedto.extendedfrom = slot; - session.double_wide = true; - session.repopulate_event(slot.domid); - session.placed(slot.extendedto, false); - droppable(); - listeners(); - $( this ).dialog( "close" ); + "Yes": { + click: function() { + // need to create new scheduledsession + var new_ss = make_ss({ "session_id" : session.session_id, + "timeslot_id": slot.following_timeslot.timeslot_id, + "extended_from_id" : current_scheduledslot.scheduledsession_id}); + // make_ss also adds to slot_status. + new_ss.saveit(); - // may have caused some new conflicts!!!! - recalculate_conflicts_for_session(session, - [slot.column_class], - [slot.column_class, slot.extendedto.column_class]); - }, - Cancel: function() { - $( this ).dialog( "close" ); - result = "cancel" - } + slot.extendedto = slot.following_timeslot; + slot.extendedto.extendedfrom = slot; + session.double_wide = true; + session.repopulate_event(slot.domid); + + droppable(); + listeners(); + $( this ).dialog( "close" ); + + // may have caused some new conflicts!!!! + recalculate_conflicts_for_session(session, + [slot.column_class], + [slot.column_class, slot.extendedto.column_class]); + }, + text: "Yes", + id: "extend-yes"}, + "Cancel": { + click: function() { + $( this ).dialog( "close" ); + result = "cancel" + }, + text: "Cancel", + id: "extend-cancel"}, } }); } else { @@ -329,14 +343,17 @@ function extend_slot(event) { } function find_free(){ - var empty_slot = find_empty_slot(); - if(empty_slot != null){ - $(empty_slot).effect("highlight", {},3000); - if(current_item != null){ - $(current_item).addClass('ui-effects-transfer'); - $(current_item).effect("transfer", {to: $(empty_slot) }, 1000); - } - $(current_item).removeClass('ui-effects-transfer'); + if(last_session) { + var empty_slot = find_empty_slot(last_session); + if(empty_slot != null){ + var domthing = $("#"+empty_slot.domid); + domthing.effect("highlight", {},3000); + if(current_item != null){ + $(current_item).addClass('ui-effects-transfer'); + $(current_item).effect("transfer", {to: domthing }, 1000); + } + $(current_item).removeClass('ui-effects-transfer'); + } } } @@ -351,8 +368,8 @@ function expand_spacer(target) { } function sort_by_alphaname(a,b) { - am = meeting_objs[$(a).attr('session_id')] - bm = meeting_objs[$(b).attr('session_id')] + am = agenda_globals.meeting_objs[$(a).attr('session_id')] + bm = agenda_globals.meeting_objs[$(b).attr('session_id')] if(am.title < bm.title) { return -1; } else { @@ -360,8 +377,8 @@ function sort_by_alphaname(a,b) { } } function sort_by_area(a,b) { - am = meeting_objs[$(a).attr('session_id')] - bm = meeting_objs[$(b).attr('session_id')] + am = agenda_globals.meeting_objs[$(a).attr('session_id')] + bm = agenda_globals.meeting_objs[$(b).attr('session_id')] if(am.area < bm.area) { return -1; } else { @@ -369,12 +386,12 @@ function sort_by_area(a,b) { } } function sort_by_duration(a,b) { - am = meeting_objs[$(a).attr('session_id')] - bm = meeting_objs[$(b).attr('session_id')] - if(am.duration < bm.duration) { + am = agenda_globals.meeting_objs[$(a).attr('session_id')] + bm = agenda_globals.meeting_objs[$(b).attr('session_id')] + if(am.requested_duration < bm.requested_duration) { // sort duration biggest to smallest. return 1; - } else if(am.duration == bm.duration && + } else if(am.requested_duration == bm.requested_duration && am.title < bm.title) { return 1; } else { @@ -382,8 +399,8 @@ function sort_by_duration(a,b) { } } function sort_by_specialrequest(a,b) { - am = meeting_objs[$(a).attr('session_id')] - bm = meeting_objs[$(b).attr('session_id')] + am = agenda_globals.meeting_objs[$(a).attr('session_id')] + bm = agenda_globals.meeting_objs[$(b).attr('session_id')] if(am.special_request == '*' && bm.special_request == '') { return -1; } else if(am.special_request == '' && bm.special_request == '*') { @@ -426,6 +443,11 @@ function unassigned_sort_change(){ function static_listeners(){ $('#close_ietf_menubar').click(hide_ietf_menu_bar); + $("#show_hidden_days").unbind('click'); + $("#show_hidden_days").click(show_hidden_days); + $("#show_hidden_rooms").unbind('click'); + $("#show_hidden_rooms").click(show_hidden_rooms); + $('#unassigned_sort_button').unbind('change'); $('#unassigned_sort_button').change(unassigned_sort_change); $('#unassigned_sort_button').css('display','block'); @@ -490,8 +512,14 @@ var __debug_meeting_click = false; var clicked_event; var __DEBUG__SESSION_OBJ; var __DEBUG__SLOT_OBJ; +var __debug_click_slot_id; +var __debug_click_container; + +// current_item is the domid that was clicked +// last_session is the session active. var current_item = null; var current_timeslot = null; +var current_scheduledslot = null; var current_timeslot_id = null; // global used by empty_info_table to move picker. var meeting_clicked = false; function meeting_event_click(event){ @@ -508,43 +536,47 @@ function meeting_event_click(event){ event.preventDefault(); meeting_clicked = true; + var slot_id = $(event.target).closest('.agenda_slot').attr('id'); + var container = $(event.target).closest('.meeting_box_container'); + __debug_click_slot_id = slot_id; + __debug_click_container = container; + + if(container == undefined) { + return; + } + + var session_id = container.attr('session_id'); + var session = agenda_globals.meeting_objs[session_id]; + + select_session(session); + __DEBUG__SS_OBJ = current_scheduledslot; + __DEBUG__SLOT_OBJ = current_timeslot; + __DEBUG__SESSION_OBJ = session; +} + +function select_session(session) { if(last_session != null) { last_session.unselectit(); } + last_session = session; + + empty_info_table(); + current_item = session.element(); /* clear set ot conflict views */ clear_conflict_classes(); conflict_classes = {}; - var slot_id = $(event.target).closest('.agenda_slot').attr('id'); - var container = $(event.target).closest('.meeting_box_container'); - - if(container == undefined) { - fill_in_session_info(session, true, session.slot); - // no async necessary, session is completely loaded - //session.load_session_obj(fill_in_session_info, session.slot); - return; - } - - var session_id = container.attr('session_id'); - var session = meeting_objs[session_id]; - last_session = session; - - session.selectit(); - current_item = session.element(); - - current_timeslot = session.slot; - if(current_timeslot != undefined) { - current_timeslot_id = current_timeslot.timeslot_id; + current_timeslot = session.slot; + if(current_timeslot) { + current_timeslot_id = current_timeslot.timeslot_id; } + current_scheduledslot = session.scheduledsession; if(__debug_meeting_click) { console.log("2 meeting_click:", current_timeslot, session); } - empty_info_table(); fill_in_session_info(session, true, session.slot); - __DEBUG__SLOT_OBJ = current_timeslot; - __DEBUG__SESSION_OBJ = session; } var last_item = null; // used during location change we make the background color @@ -558,6 +590,61 @@ function info_location_select_change(){ $(last_item).addClass("selected_slot"); } +// called when the "Set" button is called, needs to perform a move, as if +// it was dragged and dropped. +function info_location_select_set() { + // figure out where the item was, and where it is going to. + var session = last_session; + var from_slot = session.slot; + + var id = $('#info_location_select').val(); + var to_slot = agenda_globals.timeslot_byid[id]; + + console.log("moved by select box from", from_slot, "to", to_slot); + + move_thing({ "session": session, + "to_slot_id": to_slot.domid, + "to_slot": to_slot, + "dom_obj": "#" + to_slot.domid, + "from_slot_id": from_slot.domid, + "from_slot": from_slot}); +} + +function move_thing(parameters) { + // hasn't moved don't do anything + if(parameters.from_slot_id == parameters.to_slot_id){ + return; + } + + parameters.too_small = false; + parameters.slot_occupied = false; + + var room_capacity = parameters.to_slot.capacity; + + if(parameters.session.session_attendees > room_capacity){ + parameters.too_small = true; + } + + bucket_list = (parameters.to_slot_id == bucketlist_id); + if(!bucket_list && !parameters.to_slot.empty){ + parameters.slot_occupied = true + } + + if(parameters.too_small || parameters.slot_occupied){ + toggle_dialog(parameters); + return + } + + clear_conflict_classes(); + // clear double wide setting for now. + // (could return if necessary) + parameters.session.double_wide = false; + + move_slot(parameters); +} + + + var last_session = null; var last_name_item = null; function info_name_select_change(){ @@ -575,30 +662,33 @@ function info_name_select_change(){ if(last_item != null) { $(last_item).removeClass("selected_slot"); } - if(current_item != null){ - $(current_item).addClass("selected_slot"); - } + var slot_id = $('#info_name_select').val(); last_name_item = '#'+slot_id; console.log("selecting group", slot_id); - var ssk = meeting_objs[slot_id].slot_status_key; + var ssk = agenda_globals.meeting_objs[slot_id].slot_status_key; // ssk is null when item is in bucket list. current_item = "#session_"+slot_id; //slot_status_obj[0].session_id; + if(current_item != null){ + $(current_item).addClass("selected_slot"); + $(current_item).get(0).scrollIntoView(true); + } if(ssk != null){ - var slot_status_obj = slot_status[ssk]; - current_timeslot = slot_status_obj[0].timeslot_id; + var slot_status_obj = agenda_globals.slot_status[ssk]; + current_timeslot = slot_status_obj[0].timeslot; + current_timeslot_id = slot_status_obj[0].timeslot_id; ss = slot_status_obj[0]; - session = ss.session(); + session = ss.session; last_session = session; last_session.selectit(); // now set up the call back that might have to retrieve info. fill_in_session_info(session, true, ss); } else { - ss = meeting_objs[slot_id]; + ss = agenda_globals.meeting_objs[slot_id]; last_session = ss; last_session.selectit(); fill_in_session_info(ss, true, ss.slot); @@ -640,10 +730,11 @@ function dajaxice_error(a){ function set_pin_session_button(scheduledsession) { $("#pin_slot").unbind('click'); if(scheduledsession == undefined) { + console.log("pin not set, scheduledsession undefined"); return; } state = scheduledsession.pinned; - console.log("button set to: ",state); + //console.log("button set to: ",state); if(state) { $("#pin_slot").html("unPin"); $("#agenda_pin_slot").addClass("button_down"); @@ -674,7 +765,7 @@ function update_pin_session(scheduledsession, state) { scheduledsession.pinned = state; session = scheduledsession.session() session.pinned = state; - session.repopulate_event(scheduledsession.domid); + session.repopulate_event(scheduledsession.domid()); set_pin_session_button(scheduledsession); }, { @@ -684,24 +775,93 @@ function update_pin_session(scheduledsession, state) { }); } +function enable_button(divid, buttonid, func) { + $(buttonid).unbind('click'); + $(buttonid).click(func); + $(buttonid).attr('disabled',false); + + $(divid).removeClass("button_disabled"); + $(divid).addClass("button_enabled"); +} + +function disable_button(divid, buttonid) { + $(buttonid).unbind('click'); + $(buttonid).attr('disabled',true); + + $(divid).addClass("button_disabled"); + $(divid).removeClass("button_enabled"); +} + +function highlight_session(session) { + var element = session.element()[0]; + element.scrollIntoView(true); + session.element().parent().parent().parent().effect("pulsate", {color:"lightcoral"}, 10000); +} + function fill_in_session_info(session, success, extra) { + var prev_session = null; + var next_session = null; + if(session == null || session == "None" || !success){ empty_info_table(); + return; } - $('#ss_info').html(session.generate_info_table()); - $('#double_slot').click(extend_slot); - $(".agenda_double_slot").removeClass("button_disabled"); - $(".agenda_double_slot").addClass("button_enabled"); - $(".agenda_selected_buttons").attr('disabled',false); + session.generate_info_table(); + + prev_session = session.prev_session; + next_session = session.next_session; if(!read_only) { + enable_button("#agenda_double_slot", "#double_slot", extend_slot); + $("#agenda_pin_slot").removeClass("button_disabled"); $("#agenda_pin_slot").addClass("button_enabled"); - set_pin_session_button(session.slot); + set_pin_session_button(session.scheduledsession); } else { $("#pin_slot").unbind('click'); } + enable_button("#agenda_show", "#show_session", show_all_session); + + if(prev_session) { + enable_button("#agenda_prev_session", "#prev_session", function(event) { + select_session(prev_session); + highlight_session(prev_session); + }); + } else { + disable_button("#agenda_prev_session", "#prev_session"); + } + if(next_session) { + enable_button("#agenda_next_session", "#next_session", function(event) { + select_session(next_session); + highlight_session(next_session); + }); + } else { + disable_button("#agenda_next_session", "#next_session"); + } + + $("#agenda_requested_features").html(""); + // fill in list of resources into #agenda_requested_features + if(session.resources != undefined) { + $.each(session.resources, function(index) { + resource = this; + + $("#agenda_requested_features").html(function(inbox, oldhtml) { + return "<div id=\"resource_"+resource.resource_id+"\"class=\"agenda_requested_feature\">"+ + "<img height=30 src=\""+ + resource.icon+"\" title=\""+ + resource.desc+"\" alt=\""+ + resource.name+ + "\" />"+ + "</div>"+ + oldhtml + }); + + console.log(session.title, "asks for resource", resource.desc); + }); + } + + // here is where we would set up the session request edit button. // $(".agenda_sreq_button").html(session.session_req_link); @@ -823,72 +983,83 @@ function droppable(){ } // end droppable() -var arr_key_index = null; function update_to_slot(session_id, to_slot_id, force){ - console.log("meeting_id:",session_id); - var to_slot = slot_status[to_slot_id]; - - var found = false; - for(var i=0; i<to_slot.length; i++){ - if(to_slot[i].empty == "True" || to_slot[i].empty == true){ // we found a empty place to put it. - // setup slot_status info. - to_slot[i].session_id = session_id; - - if(to_slot_id != bucketlist_id) { - to_slot[i].empty = false; - } - - // update meeting_obj - //meeting_objs[session_id].slot_status_key = to_slot[i].domid - arr_key_index = i; - meeting_objs[session_id].placed(to_slot, true); - found = true; - // update from_slot - - return found; - } + //console.log("update_to_slot meeting_id:",session_id, to_slot_id); + if(to_slot_id == "sortable-list") { + /* must be bucket list */ + return true; } - if(!found && force){ - var unassigned_slot_obj = new ScheduledSlot(); - unassigned_slot_obj.scheduledsession_id = to_slot[0].scheduledsession_id; - unassigned_slot_obj.timeslot_id = to_slot[0].timeslot_id; - unassigned_slot_obj.session_id = session_id; - // unassigned_slot_obj.session_id = to_slot[0].session_id; + var to_timeslot = agenda_globals.timeslot_bydomid[to_slot_id]; + if(to_timeslot != undefined && (to_timeslot.empty == true || force)) { + // add a new scheduledsession for this, save it. + var new_ss = make_ss({ "session_id" : session_id, + "timeslot_id": to_timeslot.timeslot_id}); + // make_ss also adds to slot_status. + var save_promise = new_ss.saveit(); - //console.log("session_id:",session_id); - //console.log("to_slot (BEFORE):", to_slot, to_slot.length); + if(to_slot_id != bucketlist_id) { + to_timeslot.mark_occupied(); + } - to_slot.push(unassigned_slot_obj); - //console.log("to_slot (AFTER):", to_slot, to_slot.length); - arr_key_index = to_slot.length-1; - found = true; - return found; + // update meeting_obj + agenda_globals.meeting_objs[session_id].placed(to_timeslot, true, new_ss); + + return save_promise; + } else { + console.log("update_to_slot failed", to_timeslot, force, "empty", to_timeslot.empty); + return false; } - return found; } +function update_from_slot(session_id, from_slot_id) +{ + var from_timeslot = agenda_globals.timeslot_bydomid[from_slot_id]; -function update_from_slot(session_id, from_slot_id){ + /* this is a list of scheduledsessions */ + var from_scheduledslots = agenda_globals.slot_status[from_slot_id]; + var delete_promises = []; - var from_slot = slot_status[meeting_objs[session_id].slot_status_key]; // remember this is an array... - var found = false; + // it will be null if it's coming from a bucketlist + if(from_slot_id != null && from_scheduledslots != undefined){ + //console.log("1 from_slot_id", from_slot_id, from_scheduledslots); + var count = from_scheduledslots.length; + var found = false; + for(var k = 0; k<from_scheduledslots.length; k++) { + var from_ss = from_scheduledslots[k]; + if(from_ss.session_id == session_id){ + found = true; - if(from_slot_id != null){ // it will be null if it's coming from a bucketlist - for(var k = 0; k<from_slot.length; k++){ - if(from_slot[k].session_id == session_id){ - found = true; - from_slot[k].empty = true; - from_slot[k].session_id = null; - return found; + var promise = from_ss.deleteit(); + delete_promises.push(promise); + from_scheduledslots.splice(k,1); + count--; } } + if(found && count == 0) { + from_timeslot.mark_empty(); + } else { + console.log("not setting fromslot empty", from_timeslot, count, found, session_id, "from_slot_id", from_slot_id); + } + + /* remove undefined entries */ + new_fromslots = []; + for(var k =0; k<from_scheduledslots.length; k++) { + if(from_scheduledslots[k] != undefined && from_scheduledslots[k].session_id != undefined) { + new_fromslots.push(from_scheduledslots[k]); + } + } + + /* any removed sessions will have been done */ + agenda_globals.slot_status[from_slot_id] = new_fromslots; + //console.log("2 from_slot_id", from_slot_id, new_fromslots); } else{ - found = true; // this may be questionable. It deals with the fact that it's coming from a bucketlist. - return found; + // this may be questionable. + // It deals with the fact that it's coming from the bucketlist. + delete_promises = [undefined]; } - return found; + return delete_promises; } @@ -897,71 +1068,22 @@ function drop_drop(event, ui){ var session_id = ui.draggable.attr('session_id'); // the session was added as an attribute // console.log("UI:", ui, session_id); - var session = meeting_objs[session_id]; + var session = agenda_globals.meeting_objs[session_id]; var to_slot_id = $(this).attr('id'); // where we are dragging it. - var to_slot = slot_status[to_slot_id] + var to_slot = agenda_globals.timeslot_bydomid[to_slot_id] var from_slot_id = session.slot_status_key; - var from_slot = slot_status[session.slot_status_key]; // remember this is an array... + var from_slot = agenda_globals.slot_status[session.slot_status_key]; // remember this is an array... - var room_capacity = parseInt($(this).attr('capacity')); - var session_attendees = parseInt(session.attendees); - - var too_small = false; - var occupied = false; - - - if(from_slot_id == to_slot_id){ // hasn't moved don't do anything - return - } - - - if(session_attendees > room_capacity){ - too_small = true; - } - - // console.log("from -> to", from_slot_id, to_slot_id); - - - bucket_list = (to_slot_id == bucketlist_id); - if(!check_free({id:to_slot_id}) ){ - console.log("not free..."); - if(!bucket_list) { - occupied = true - } - } - - if(too_small || occupied){ - toggle_dialog({ "session": session, - "to_slot_id": to_slot_id, - "to_slot": to_slot, - "from_slot_id": from_slot_id, - "from_slot": from_slot, - "bucket_list": bucket_list, - "event": event, - "ui": ui, - "dom_obj": this, - "slot_occupied": occupied, - "too_small": too_small}); - return - } - - clear_conflict_classes(); - // clear double wide setting for now. - // (could return if necessary) - session.double_wide = false; - - move_slot({"session": session, - "to_slot_id": to_slot_id, - "to_slot": to_slot, - "from_slot_id":from_slot_id, - "from_slot": from_slot, - "bucket_list": bucket_list, - "event": event, - "ui": ui, - "dom_obj": this, - "force": false}); + move_thing({ "session": session, + "to_slot_id": to_slot_id, + "to_slot": to_slot, + "from_slot_id": from_slot_id, + "from_slot": from_slot, + "event": event, + "ui": ui, + "dom_obj": this}); } function recalculate_conflicts_for_session(session, old_column_classes, new_column_classes) @@ -973,8 +1095,8 @@ function recalculate_conflicts_for_session(session, old_column_classes, new_colu session.examine_people_conflicts(old_column_classes); var sk; - for(sk in meeting_objs) { - var s = meeting_objs[sk]; + for(sk in agenda_globals.meeting_objs) { + var s = agenda_globals.meeting_objs[sk]; for(ocn in old_column_classes) { var old_column_class = old_column_classes[ocn]; @@ -1006,7 +1128,7 @@ function recalculate_conflicts_for_session(session, old_column_classes, new_colu show_all_conflicts(); } -var _move_debug = false; +var __debug_parameters; var _LAST_MOVED; function move_slot(parameters) { /* dom_obj: is a jquery selector of where the slot will be appeneded to @@ -1014,8 +1136,9 @@ function move_slot(parameters) { drop_drop where 'this' is the dom dest. */ var update_to_slot_worked = false; + __debug_parameters = parameters; - if(_move_debug) { + if(agenda_globals.__debug_session_move) { _LAST_MOVED = parameters.session; if(parameters.from_slot != undefined) { console.log("from_slot", parameters.from_slot.domid); @@ -1023,129 +1146,83 @@ function move_slot(parameters) { console.log("from_slot was unassigned"); } } + + /* if to slot was marked empty, then remove anything in it (like word "empty") */ + if(parameters.to_slot.empty && !parameters.to_slot.unscheduled_box) { + $(parameters.dom_obj).html(""); + } + var update_to_slot_worked = false; if(parameters.same_timeslot == null){ parameters.same_timeslot = false; } + var save_promise = undefined; if(parameters.bucket_list) { - update_to_slot_worked = update_to_slot(parameters.session.session_id, - parameters.to_slot_id, true); + save_promise = update_to_slot(parameters.session.session_id, + parameters.to_slot_id, true); } else{ - update_to_slot_worked = update_to_slot(parameters.session.session_id, - parameters.to_slot_id, parameters.same_timeslot); + save_promise = update_to_slot(parameters.session.session_id, + parameters.to_slot_id, parameters.same_timeslot); } - if(_move_debug) { - console.log("update_slot_worked", update_to_slot_worked); + if(agenda_globals.__debug_session_move) { + console.log("update_to_slot returned promise", save_promise); } - if(update_to_slot_worked){ - if(update_from_slot(parameters.session.session_id, parameters.from_slot_id)){ - remove_duplicate(parameters.from_slot_id, parameters.session.session_id); - // do something - } - else{ - if(_move_debug) { - console.log("issue updateing from_slot"); - console.log("parameters.from_slot_id",parameters.from_slot_id, slot_status[parameters.from_slot_id]); - } - return; - } + + if(save_promise == false){ + console.log("ERROR updating to_slot", save_promise, + parameters.to_slot_id, + agenda_globals.slot_status[parameters.to_slot_id]); + return undefined; } - else{ - if(_move_debug) { - console.log("issue updateing to_slot"); - console.log("to_slot_id",parameters.to_slot_id, slot_status[parameters.to_slot_id]); - } + + var delete_promises = update_from_slot(parameters.session.session_id, parameters.from_slot_id); + if(delete_promises.length > 0) { + remove_duplicate(parameters.from_slot_id, parameters.session.session_id); + // do something else? + } + else { + console.log("ERROR updating from_slot", parameters.from_slot_id, agenda_globals.slot_status[parameters.from_slot_id]); return; } - parameters.session.slot_status_key = parameters.to_slot[arr_key_index].domid; + parameters.session.slot_status_key = parameters.to_slot_id; var eTemplate = parameters.session.event_template() - - console.log("dom_obj:", parameters.dom_obj); $(parameters.dom_obj).append(eTemplate); - parameters.ui.draggable.remove(); - + if(parameters.ui) { + parameters.ui.draggable.remove(); + } + /* recalculate all the conflict classes given new slot */ + parameters.session.update_column_classes([parameters.to_slot], + parameters.bucket_list); /* set colours */ $(parameters.dom_obj).removeClass('highlight_free_slot'); - if(check_free({id:parameters.to_slot_id}) ){ - $(parameters.dom_obj).addClass('free_slot') - } - else{ + if(parameters.to_slot.empty) { $(parameters.dom_obj).removeClass('free_slot') } + else{ + $(parameters.dom_obj).addClass('free_slot') + } - if(check_free({id:parameters.from_slot_id}) ){ - $("#"+parameters.from_slot_id).addClass('free_slot'); + if(parameters.from_slot != undefined && parameters.from_slot.empty){ + $("#"+parameters.from_slot_id).removeClass('free_slot'); } else{ - $("#"+parameters.from_slot_id).removeClass('free_slot'); + $("#"+parameters.from_slot_id).addClass('free_slot'); } $("#"+bucketlist_id).removeClass('free_slot'); /******************************************************/ - var scheduledsession = null; - for(var i =0; i< parameters.to_slot.length; i++){ - if (parameters.to_slot[i].session_id == parameters.session.session_id){ - scheduledsession = parameters.to_slot[i]; - break; - } - } - - if(scheduledsession != null){ - console.log("moved session",parameters.session.title,"to",scheduledsession); - parameters.session.placed(scheduledsession, true); - start_spin(); - if(_move_debug) { - console.log('schedule_id',schedule_id, - 'session_id', parameters.session.session_id, - 'scheduledsession_id', scheduledsession.scheduledsession_id); - } - - if(parameters.session.slot2) { - parameters.session.double_wide = false; - Dajaxice.ietf.meeting.update_timeslot(dajaxice_callback, - { - 'schedule_id':schedule_id, - 'session_id': parameters.session.session_id, - 'scheduledsession_id': 0, - }); - parameters.session.slot2 = undefined; - } - - if(parameters.same_timeslot){ - Dajaxice.ietf.meeting.update_timeslot(dajaxice_callback, - { - 'schedule_id':schedule_id, - 'session_id': parameters.session.session_id, - 'scheduledsession_id': scheduledsession.scheduledsession__id, - 'extended_from_id': 0, - 'duplicate':true - }); - }else { - Dajaxice.ietf.meeting.update_timeslot(dajaxice_callback, - { - 'schedule_id':schedule_id, - 'session_id': parameters.session.session_id, - 'scheduledsession_id': scheduledsession.scheduledsession_id, - }); - } - - parameters.session.update_column_classes([scheduledsession], parameters.bucket_list); - } - else{ - if(_move_debug) { - console.log("issue sending ajax call!!!"); - } - } - droppable(); listeners(); sort_unassigned(); + + delete_promises.push(save_promise); + return $.when.apply($,delete_promises); } /* first thing that happens when we grab a meeting_event */ diff --git a/static/js/agenda/agenda_objects.js b/static/js/agenda/agenda_objects.js index 0e07035e7..56996364e 100644 --- a/static/js/agenda/agenda_objects.js +++ b/static/js/agenda/agenda_objects.js @@ -20,6 +20,18 @@ * */ +function AgendaGlobals() { + this.group_objs = {}; + this.slot_status = {}; + this.slot_objs = {}; + this.meeting_objs = {}; + this.sessions_objs = {}; + this.timeslot_bydomid = {}; + this.timeslot_byid = {}; + this.scheduledsession_promise = undefined; + this.timeslot_promise = undefined; + this.__debug_session_move = false; +} function createLine(x1,y1, x2,y2){ var length = Math.sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2)); @@ -45,7 +57,7 @@ function empty_callback(inp){ } function get_all_constraints(){ - for(s in meeting_objs){ + for(s in agenda_globals.meeting_objs){ show_non_conflicting_spots(s) } @@ -53,8 +65,8 @@ function get_all_constraints(){ function show_all_conflicts(){ console.log("showing all conflicts"); - for(sk in meeting_objs) { - var s = meeting_objs[sk]; + for(sk in agenda_globals.meeting_objs) { + var s = agenda_globals.meeting_objs[sk]; s.display_conflict(); s.display_personconflict(); } @@ -62,8 +74,8 @@ function show_all_conflicts(){ // not really used anymore -- just for debugging function hide_all_conflicts(){ - for(sk in meeting_objs) { - var s = meeting_objs[sk]; + for(sk in agenda_globals.meeting_objs) { + var s = agenda_globals.meeting_objs[sk]; s.hide_conflict(); } } @@ -73,8 +85,8 @@ function get_all_conflicts(){ var one_constraint; var sess1; //console.log("get_all_conflicts()"); - for(var s in meeting_objs){ - sess1 = meeting_objs[s]; + for(var s in agenda_globals.meeting_objs){ + sess1 = agenda_globals.meeting_objs[s]; sess1.clear_conflict(); sess1.display_conflict(); @@ -86,8 +98,8 @@ function get_all_conflicts(){ var all_constraints = $.when.apply($,all_constraint_promises); all_constraints.done(function() { - for(var s in meeting_objs) { - var sess2 = meeting_objs[s]; + for(var s in agenda_globals.meeting_objs) { + var sess2 = agenda_globals.meeting_objs[s]; sess2.examine_people_conflicts(); } }); @@ -273,80 +285,80 @@ function ColumnClass(room,date,time) { // ++++++++++++++++++ -// ScheduledSlot Object -// ScheduledSession is DJANGO name for this object, but needs to be renamed. -// It represents a TimeSlot that can be assigned in this schedule. -// { "scheduledsession_id": "{{s.id}}", -// "empty": "{{s.empty_str}}", -// "timeslot_id":"{{s.timeslot.id}}", -// "session_id" :"{{s.session.id}}", -// "room" :"{{s.timeslot.location|slugify}}", -// "extendedfrom_id" :refers to another scheduledsession by ss.id -// "time" :"{{s.timeslot.time|date:'Hi' }}", -// "date" :"{{s.timeslot.time|date:'Y-m-d'}}", -// "domid" :"{{s.timeslot.js_identifier}}"} -function ScheduledSlot(){ - this.extendedfrom = undefined; - this.extendedto = undefined; - this.extendedfrom_id = false; +// TimeSlot Object +// +// { "timeslot_id":"{{timeslot.id}}", +// "room" :"{{timeslot.location|slugify}}", +// "time" :"{{timeslot.time|date:'Hi' }}", +// "date" :"{{timeslot.time|date:'Y-m-d'}}", +// "domid" :"{{timeslot.js_identifier}}"} +function TimeSlot(){ + this.timeslot_id = undefined; + this.room = undefined; + this.time = undefined; + this.date = undefined; + this.domid = undefined; + this.empty = true; + this.scheduledsessions = []; + this.following_timeslot_id = undefined; + this.unscheduled_box = false; } -ScheduledSlot.prototype.initialize = function(json) { +TimeSlot.prototype.initialize = function(json) { for(var key in json) { this[key]=json[key]; } + //console.log("timeslot processing: ", this.timeslot_id, + // this.room, this.date, this.time); - /* this needs to be an object */ this.column_class=new ColumnClass(this.room, this.date, this.time); - var d = new Date(this.date); - var t = d.getUTCDay(); + this.day = new Date(this.date); + this.starttime = parseInt(this.time,10); + var t = this.day.getUTCDay(); if(this.room == "Unassigned"){ this.short_string = "Unassigned"; } else{ - this.short_string = daysofweek[t] + ", "+ this.time + ", " + upperCaseWords(this.room); - } - if(!this.domid) { - this.domid = json_to_id(this); - //console.log("gen "+timeslot_id+" is domid: "+this.domid); - } - //console.log("extend "+this.domid+" with "+JSON.stringify(this)); - - // translate Python booleans to JS. - if(this.pinned == "True") { - this.pinned = true; - } else { - this.pinned = false; + this.short_string = daysofweek[t] + ", "+ this.time + ", " + this.room; } - // the key so two sessions in the same timeslot - if(slot_status[this.domid] == null) { - slot_status[this.domid]=[]; - } - slot_status[this.domid].push(this); - //console.log("filling slot_objs", this.scheduledsession_id); - slot_objs[this.scheduledsession_id] = this; + agenda_globals.timeslot_bydomid[this.domid] = this; + agenda_globals.timeslot_byid[this.timeslot_id] = this; }; -ScheduledSlot.prototype.session = function() { - if(this.session_id != undefined) { - return meeting_objs[this.session_id]; - } else { - return undefined; +TimeSlot.prototype.title = function() { + return this.room + " "+this.date+" "+this.time; +}; +TimeSlot.prototype.slot_title = function() { + return "id#"+this.timeslot_id+" dom:"+this.domid; +}; +TimeSlot.prototype.mark_empty = function() { + if(agenda_globals.__debug_session_move) { + console.log("marking slot empty", this.domid); + $("#"+this.domid).html("empty"); } + this.empty = true; }; -ScheduledSlot.prototype.slot_title = function() { - return "id#"+this.scheduledsession_id+" dom:"+this.domid; +TimeSlot.prototype.mark_occupied = function() { + if(agenda_globals.__debug_session_move) { + console.log("marking slot occupied", this.domid); + + // if it was empty before, then clear it (might have word "empty" in it) + if(this.empty == true) { + $("#"+this.domid).html(""); + } + } + this.empty = false; }; -ScheduledSlot.prototype.can_extend_right = function() { +TimeSlot.prototype.can_extend_right = function() { if(this.following_timeslot == undefined) { if(this.following_timeslot_id != undefined) { - this.following_timeslot = slot_objs[this.following_timeslot_id]; + this.following_timeslot = agenda_globals.timeslot_byid[this.following_timeslot_id]; } } if(this.following_timeslot == undefined) { - console.log("can_extend_right:",this.scheduledsession_id," no slot to check"); + console.log("can_extend_right:",this.timeslot_id," no slot to check"); return false; } else { console.log("can_extend_right:", @@ -357,22 +369,279 @@ ScheduledSlot.prototype.can_extend_right = function() { } }; +function make_timeslot(json) { + var ts = new TimeSlot(); + ts.initialize(json); + return ts; +} + +/* feed this an array of timeslots */ +function make_timeslots(json, status, jqXHR) { + $.each(json, function(index) { + var thing = json[index]; + make_timeslot(thing); + }); +} + +function load_timeslots(href) { + if(agenda_globals.timeslot_promise == undefined) { + agenda_globals.timeslot_promise = $.Deferred(); + var ts_promise = agenda_globals.timeslot_promise; + + var ts = $.ajax(href); + ts.success(function(newobj, status, jqXHR) { + make_timeslots(newobj); + ts_promise.resolve(newobj); + console.log("finished timeslot promise"); + }); + } + return agenda_globals.timeslot_promise; +} + + +// ++++++++++++++++++ +// ScheduledSlot Object +// ScheduledSession is DJANGO name for this object, but needs to be renamed. +// It represents a TimeSlot that can be assigned in this schedule. +// { "scheduledsession_id": "{{s.id}}", +// "timeslot_id":"{{s.timeslot.id}}", +// "session_id" :"{{s.session.id}}", +// "extendedfrom_id" :refers to another scheduledsession by ss.id +function ScheduledSlot(){ + this.extendedfrom = undefined; + this.extendedto = undefined; + this.extendedfrom_id = false; + this.pinned = false; +} + +ScheduledSlot.prototype.room = function() { + return this.timeslot.room; +}; + +ScheduledSlot.prototype.time = function() { + return this.timeslot.time; +}; + +ScheduledSlot.prototype.date = function() { + return this.timeslot.date; +}; + +ScheduledSlot.prototype.domid = function() { + return this.timeslot.domid; +}; + +ScheduledSlot.prototype.column_class = function() { + return this.timeslot.column_class; +}; + +ScheduledSlot.prototype.short_string = function() { + return this.timeslot.short_string; +}; + +ScheduledSlot.prototype.saveit = function() { + var myss = this; + stuffjson = new Object(); + stuffjson.session_id = this.session_id; + stuffjson.timeslot_id = this.timeslot_id; + if(this.extendedfrom_id != undefined && this.extendedfrom_id != false) { + stuffjson.extendedfrom_id = this.extendedfrom_id; + } + + var stuff = JSON.stringify(stuffjson, null, '\t'); + + var saveit = $.ajax(scheduledsession_post_href,{ + "content-type": "text/json", + "type": "POST", + "data": stuff, + }); + // should do something on success and failure. + saveit.done(function(result, status, jqXHR) { + myss.initialize(result, "saveit"); + var session = myss.session; + if(session != undefined) { + session.placed(myss.timeslot); + } + }); + + // return the promise, in case someone (tests!) needs to know when we are done. + return saveit; +}; + +function remove_from_slot_status(domid, ss_id) { + var found_at; + if(agenda_globals.__debug_session_move) { + console.log("remove", domid, agenda_globals.slot_status[domid]); + } + if(agenda_globals.slot_status[domid] != undefined) { + $.each(agenda_globals.slot_status[domid], function(index, value) { + if(agenda_globals.__debug_session_move) { + console.log(" checking", index, value, ss_id); + } + if(value.scheduledsession_id == ss_id) { + found_at = index; + return; + } + }); + if(found_at != undefined) { + agenda_globals.slot_status[domid].splice(found_at, 1); + console.log("removed", found_at, agenda_globals.slot_status[domid]); + } + } else { + //console.log(" already empty"); + } +}; + +ScheduledSlot.prototype.deleteit = function() { + var deleteit = $.ajax(this.href, { + "content-type": "text/json", + "type": "DELETE", + }); + // now nuke self! + var me = this; + delete agenda_globals.slot_objs[this.scheduledsession_id]; + remove_from_slot_status(this.domid(), this.scheduledsession_id); + return deleteit; +}; + +function update_if_not_undefined(old, newval) { + if(newval != undefined) { + return newval; + } else { + return old; + } +} + +ScheduledSlot.prototype.make_unassigned = function() { + this.scheduledsession_id = 0; + this.empty = true; + this.session_id = null; + this.room = "unassigned"; + this.time = null; + this.date = null; + this.timeslot = new TimeSlot(); + this.timeslot.initialize({"domid":"sortable-list"}); + this.timeslot.unscheduled_box = true; + this.timeslot.short_string = "Unscheduled"; + + + agenda_globals.slot_status[this.domid()]=[]; + agenda_globals.slot_status[this.domid()].push(this); + agenda_globals.slot_objs[this.scheduledsession_id] = this; +}; + +ScheduledSlot.prototype.real_initialize = function(json, extra) { + /* do not copy everything over */ + this.pinned = update_if_not_undefined(this.pinned, json.pinned); + this.scheduledsession_id = update_if_not_undefined(this.scheduledsession_id, json.scheduledsession_id); + this.session_id = update_if_not_undefined(this.session_id, json.session_id); + this.timeslot_id = update_if_not_undefined(this.timeslot_id, json.timeslot_id); + this.href = update_if_not_undefined(this.href, json.href); + this.extendedfrom_id = update_if_not_undefined(this.extendedfrom_id, json.extendedfrom_id); + + //console.log("timeslot_id", this.timeslot_id); + this.timeslot = agenda_globals.timeslot_byid[this.timeslot_id]; + if(this.timeslot != undefined && this.session_id != undefined) { + this.timeslot.mark_occupied(); + } + if(this.session_id != undefined) { + this.session = agenda_globals.meeting_objs[this.session_id]; + } + + // translate Python booleans to JS. + if(this.pinned == "True") { + this.pinned = true; + } else { + this.pinned = false; + } + + // timeslot should never be null, but if there is old/flaky + // data it could happen, so guard against it. + if(this.timeslot != undefined) { + // do not include in data structures if session_id is nil + // the key so two sessions in the same timeslot + if(agenda_globals.slot_status[this.domid()] == null) { + agenda_globals.slot_status[this.domid()]=[]; + } + if(this.session_id != undefined) { + // remove any old duplicate that might exist. + remove_from_slot_status(this.domid(), this.scheduledsession_id); + if(agenda_globals.__debug_session_move) { + console.log(extra, "adding to slot_status", this.domid()); + } + + agenda_globals.slot_status[this.domid()].push(this); + //console.log("filling slot_objs", this.scheduledsession_id); + } + } + + agenda_globals.slot_objs[this.scheduledsession_id] = this; +}; +ScheduledSlot.prototype.initialize = ScheduledSlot.prototype.real_initialize; + +function load_scheduledsessions(ts_promise, session_promise, href) { + if(agenda_globals.scheduledsession_promise == undefined) { + agenda_globals.scheduledsession_promise = $.Deferred(); + + var ss = $.ajax(href); + var ss_loaded = $.when(ss,ts_promise,session_promise); + + ss_loaded.done(function(result, status, jqXHR) { + console.log("finished ss promise"); + + newobj = result[0] + $.each(newobj, function(index) { + one = newobj[index]; + //console.log("ss has:", one); + make_ss(one); + }); + agenda_globals.scheduledsession_promise.resolve(newobj); + }); + } + return agenda_globals.scheduledsession_promise; +} + +ScheduledSlot.prototype.connect_to_timeslot_session = function() { + if(this.timeslot == undefined) { + if(this.timeslot_id != undefined) { + this.timeslot = agenda_globals.timeslot_byid[this.timeslot_id]; + } else { + /* must be the unassigned one?! */ + this.timeslot = new TimeSlot(); + this.timeslot.domid = "sortable-list"; + } + } + /* session could be hooked up, but it is now always session() */ +}; + +ScheduledSlot.prototype.session = function() { + if(this.session_id != undefined) { + return agenda_globals.meeting_objs[this.session_id]; + } else { + console.log("ss id:", this.scheduledsession_id, "timeslot:", this.timeslot_id, this.timeslot.title(), "has null session"); + return undefined; + } +}; +ScheduledSlot.prototype.slot_title = function() { + return "id#"+this.scheduledsession_id+" dom:"+this.domid(); +}; + function make_ss(json) { var ss = new ScheduledSlot(); - ss.initialize(json); + ss.initialize(json, "make_ss"); + return ss; } // ++++++++++++++++++ // Session Objects // -// initialized from landscape_edit.html template with: -// session_obj({"title" : "{{ s.short_name }}", +// initialized by loading a json from /meeting/XX/sessions.json, return JSON that looks like: +// +// {"title" : "{{ s.short_name }}", // "description":"{{ s.group.name }}", // "special_request": "{{ s.special_request_token }}", // "session_id":"{{s.id}}", // "owner": "{{s.owner.owner}}", -// "group_id":"{{s.group.id}}", // "area":"{{s.group.parent.acronym|upper}}", // "duration":"{{s.requested_duration.seconds|durationFormat}}"}); // @@ -392,27 +661,40 @@ function Session() { this.special_request = ""; this.conflicted = false; this.double_wide = false; + this.attendees = undefined; } function session_obj(json) { session = new Session(); for(var key in json) { - if(json[key].length > 0) { + //console.log("copying", key, "value: ", json[key]); + if(json[key] != undefined && json[key] != "") { session[key]=json[key]; } } // dict will not pass .length > 0 above. session.group = json.group; + if(session.requested_duration == undefined) { + session.requested_duration = session.duration; + } + + // make it a number. + session.attendees = parseInt(session.attendees); + session.ogroup = session.group; if(session.group != undefined) { /* it has an inline group object, intern it, and redirect to interned object */ - //console.log("using embedded group: ",session.group.href); + //console.log(session.title, "using embedded group: ", + // session.group.acronym, session.group.href, session.group); session.group = load_group_from_json(session.group); session.group_href = session.group.href; + //console.log(session.title, "2 using embedded group: ", + // session.group.acronym, session.group.href, session.group); } else if(session.group_href != undefined) { - console.log("session has no embedded group, load by href", session.group_href); + console.log("session ",session.session_id, + "has no embedded group, load by href", session.group_href); session.group = find_group_by_href(session.group_href, "session_load"); } else { // bogus @@ -421,16 +703,60 @@ function session_obj(json) { // keep a list of sessions by name // this is mostly used for debug purposes only. - if(session_objs[session.title] == undefined) { - session_objs[session.title] = []; + if(agenda_globals.sessions_objs[session.title] == undefined) { + agenda_globals.sessions_objs[session.title] = []; } - session_objs[session.title].push(session); // an array since there can be more than one session/wg + agenda_globals.sessions_objs[session.title].push(session); // an array since there can be more than one session/wg - meeting_objs[session.session_id] = session; + agenda_globals.meeting_objs[session.session_id] = session; return session; } +/* feed this an array of sessions */ +function make_sessions(json, status, jqXHR) { + $.each(json, function(index) { + var thing = json[index]; + session_obj(thing); + }); +} + +function load_sessions(href) { + if(agenda_globals.session_promise == undefined) { + agenda_globals.session_promise = $.Deferred(); + + var ss = $.ajax(href); + ss.success(function(newobj, status, jqXHR) { + console.log("finished session promise"); + make_sessions(newobj); + agenda_globals.session_promise.resolve(newobj); + }); + } + return agenda_globals.session_promise; +} + +function count_sessions() { + $.each(agenda_globals.sessions_objs, function(title) { + //console.log("title", title, this); + var lastone = null; + var sessions = agenda_globals.sessions_objs[title]; + var num_sessions = sessions.length; + $.each(sessions, function(index) { + //console.log("session", index, this); + this.number = index; // a number + this.maxNum = num_sessions; + + this.prev_session = lastone; + this.next_session = null; + if(index < num_sessions) { + this.next_session = sessions[index+1]; + } + lastone = this; + }); + }); +} + + // augument to jQuery.getJSON( url, [data], [callback] ) Session.prototype.load_session_obj = function(andthen, arg) { session = this; @@ -494,18 +820,28 @@ Session.prototype.on_bucket_list = function() { this.column_class_list = []; this.element().parent("div").addClass("meeting_box_bucket_list"); }; -Session.prototype.placed = function(where, forceslot) { +Session.prototype.placed = function(where, forceslot, scheduledsession) { this.is_placed = true; // forceslot is set on a move, but unset on initial placement, // as placed might be called more than once for a double slot session. if(forceslot || this.slot==undefined) { this.slot = where; + this.scheduledsession = scheduledsession; + + /* we can not mark old slot as empty, because it + might have multiple sessions in it. + we can do the opposite: mark it was not empty when we fill it. + */ + + if(where != undefined) { + where.empty = false; + } } if(where != undefined) { this.add_column_class(where.column_class); } - //console.log("session:",session.title, "column_class", ssid.column_class); + //console.log("session:",session.title, "column_class", ssid.column_class()); this.element().parent("div").removeClass("meeting_box_bucket_list"); this.pinned = where.pinned; }; @@ -555,11 +891,11 @@ Session.prototype.clear_all_conflicts = function(old_column_classes) { $.each(session_obj.constraints.bethere, function(i) { var conflict = session_obj.constraints.bethere[i]; var person = conflict.person; - + person.clear_session(session_obj, old_column_classes); }); } -}; +}; Session.prototype.show_conflict = function() { if(_conflict_debug) { @@ -621,7 +957,7 @@ Session.prototype.examine_people_conflicts = function() { // reset people conflicts. session_obj.person_conflicted = false; - + for(ccn in session_obj.column_class_list) { var vertical_location = session_obj.column_class_list[ccn].column_tag; var room_tag = session_obj.column_class_list[ccn].room_tag; @@ -651,6 +987,17 @@ Session.prototype.area_scheme = function() { return this.area.toUpperCase() + "-scheme"; }; +Session.prototype.is_bof = function() { + return this.bof == "True"; +} +Session.prototype.wg_scheme = function() { + if(this.is_bof()) { + return "bof_style"; + } else { + return "wg_style"; + } +}; + Session.prototype.add_column_class = function(column_class) { if(__column_class_debug) { console.log("adding:",column_class, "to ", this.title); @@ -660,9 +1007,9 @@ Session.prototype.add_column_class = function(column_class) { var _LAST_MOVED_OLD; var _LAST_MOVED_NEW; -// scheduledsession_list is a list of slots where the session has been located. +// timeslot_list is a list of slots where the session has been located. // bucket_list is a boolean. -Session.prototype.update_column_classes = function(scheduledsession_list, bucket_list) { +Session.prototype.update_column_classes = function(timeslot_list, bucket_list) { // COLUMN CLASSES MUST BE A LIST because of multiple slot use console.log("updating column_classes for ", this.title); @@ -680,9 +1027,10 @@ Session.prototype.update_column_classes = function(scheduledsession_list, bucket this.on_bucket_list(); } else { - for(ssn in scheduledsession_list) { - ss = scheduledsession_list[ssn]; - this.add_column_class(ss.column_class); + for(tsn in timeslot_list) { + var ts = timeslot_list[tsn]; + console.log("timeslot_list", tsn, ts); + this.add_column_class(ts.column_class); } new_column_tag = this.column_class_list[0].column_tag; } @@ -709,8 +1057,8 @@ Session.prototype.update_column_classes = function(scheduledsession_list, bucket // utility/debug function, draws all events. function update_all_templates() { - for(key in meeting_objs) { - session = meeting_objs[key]; + for(key in agenda_globals.meeting_objs) { + session = agenda_globals.meeting_objs[key]; var slot = session.slot_status_key; if(slot != null) { session.repopulate_event(slot); @@ -722,29 +1070,34 @@ Session.prototype.event_template = function() { // the extra div is present so that the table can have a border which does not // affect the total height of the box. The border otherwise screws up the height, // causing things to the right to avoid this box. + var area_mark = ""; + if(this.responsible_ad != undefined) { + area_mark = this.responsible_ad.area_mark; + if(area_mark == undefined) { + area_mark = "ad:" + this.responsible_ad.href; + } + } + var bucket_list_style = "meeting_box_bucket_list" if(this.is_placed) { bucket_list_style = ""; + area_mark = ""; /* no mark for unplaced items: it goes to the wrong place */ } if(this.double_wide) { bucket_list_style = bucket_list_style + " meeting_box_double"; } - var area_mark = ""; - if(this.responsible_ad != undefined) { - area_mark = this.responsible_ad.area_mark; - } - - pinned = ""; + var pinned = ""; if(this.pinned) { bucket_list_style = bucket_list_style + " meeting_box_pinned"; pinned="<td class=\"pinned-tack\">P</td>"; } - groupacronym = "nogroup"; + var groupacronym = "nogroup"; if(this.group != undefined) { groupacronym = this.group.acronym; } + //console.log("acronym", groupacronym, this.group.acronym, this.visible_title()); // see comment in ietf.ccs, and // http://stackoverflow.com/questions/5148041/does-firefox-support-position-relative-on-table-elements @@ -754,9 +1107,10 @@ Session.prototype.event_template = function() { this.session_id+ "' session_id=\""+this.session_id+"\"" + "><tr id='meeting_event_title'><th class='"+ + this.wg_scheme()+" "+ this.area_scheme() +" meeting_obj'>"+ this.visible_title()+ - "<span> ("+this.duration+")</span>" + + "<span> ("+this.requested_duration+")</span>" + "</th>"+pinned+"</tr></table>"+ area_mark +"</div></div>"; }; @@ -766,6 +1120,12 @@ function andthen_alert(object, result, arg) { Session.prototype.generate_info_table = function() { $("#info_grp").html(name_select_html); + if(this.is_bof()) { + $("#grp_type").html("BOF"); + } else { + $("#grp_type").html("WG"); + } + $("#info_name_select").val($("#info_name_select_option_"+this.session_id).val()); if(this.description.length > 33) { $("#info_name").html("<span title=\""+this.description+"\">"+this.description.substring(0,35)+"...</span>"); @@ -795,7 +1155,7 @@ Session.prototype.generate_info_table = function() { if(this.slot != undefined) { ss = this.slot; if(ss.timeslot_id == null){ - $("#info_location_select").val(meeting_objs[ss.scheduledsession_id]); + $("#info_location_select").val(agenda_globals.meeting_objs[ss.scheduledsession_id]); }else{ $("#info_location_select").val(ss.timeslot_id); // *** } @@ -812,8 +1172,8 @@ Session.prototype.generate_info_table = function() { }; function load_all_groups() { - for(key in meeting_objs) { - session = meeting_objs[key]; + for(key in agenda_globals.meeting_objs) { + session = agenda_globals.meeting_objs[key]; session.group = find_group_by_href(session.group_href, "load all"); } } @@ -842,6 +1202,7 @@ Session.prototype.retrieve_constraints_by_session = function() { return this.constraints_promise; }; +var __verbose_person_conflicts = false; Session.prototype.calculate_bethere = function() { var session_obj = this; @@ -849,7 +1210,9 @@ Session.prototype.calculate_bethere = function() { $.each(this.constraints["bethere"], function(index) { var bethere = session_obj.constraints["bethere"][index]; find_person_by_href(bethere.person_href).done(function(person) { - console.log("person",person.ascii,"attends session",session_obj.group.acronym); + if(__verbose_person_conflicts) { + console.log("person",person.ascii,"attends session",session_obj.group.acronym); + } person.attend_session(session_obj); }); }); @@ -888,7 +1251,7 @@ Session.prototype.fill_in_constraints = function(constraint_list) { // ++++++++++++++++++ // Group Objects function Group() { - this.andthen_list = []; + this.andthen_list = []; /* should be removed, or replaced with promise */ this.all_sessions = []; } @@ -958,7 +1321,7 @@ Group.prototype.del_column_class = function(column_class) { } for(n in this.column_class_list) { if(this.column_class_list[n] == column_class) { - delete this.column_class_list[n]; + this.column_class_list.splice(n,1); } } }; @@ -978,17 +1341,20 @@ Group.prototype.del_column_classes = function(column_class_list) { var __debug_group_load = false; function create_group_by_href(href) { - if(group_objs[href] == undefined) { - group_objs[href]=new Group(); - g = group_objs[href]; + if(agenda_globals.group_objs[href] == undefined) { + agenda_globals.group_objs[href]=new Group(); + var g = agenda_globals.group_objs[href]; + if(__debug_group_load) { + console.log("creating new group for", href); + } g.loaded = false; g.loading= false; } - return g; + return agenda_globals.group_objs[href]; } function load_group_by_href(href) { - var g = group_objs[href]; + var g = agenda_globals.group_objs[href]; if(!g.loaded) { g.href = href; if(__debug_group_load) { @@ -1004,7 +1370,7 @@ function load_group_by_href(href) { // fields are added to the group object, and the group // is marked loaded. The resulting group object is returned. function load_group_from_json(json) { - g = create_group_by_href(json.href); + var g = create_group_by_href(json.href); for(var key in json) { if(json[key].length > 0) { g[key]=json[key]; @@ -1019,7 +1385,7 @@ var group_references = 0; var group_demand_loads = 0; function find_group_by_href(href, msg) { group_references++; - g=group_objs[href]; + var g=agenda_globals.group_objs[href]; if(g == undefined) { group_demand_loads++; if(__debug_group_load) { @@ -1178,7 +1544,7 @@ Constraint.prototype.build_people_conflict_view = function() { area_mark = this.person.area_mark_basic; } return "<div class='conflict our-"+this.conflict_type+"' id='"+this.dom_id+ - "'>"+this.person.ascii+area_mark+"</div>"; + "'>"+area_mark+"</div>"; }; Constraint.prototype.build_othername = function() { @@ -1327,8 +1693,7 @@ function find_person_by_href(href) { var area_result; // this function creates a unique per-area director mark -function mark_area_directors() { - var directorpromises = []; +function mark_area_directors(directorpromises) { $.each(area_directors, function(areaname) { var adnum = 1; $.each(area_directors[areaname], function(key) { diff --git a/static/js/agenda/timeslot_edit.js b/static/js/agenda/timeslot_edit.js index 6075f3ceb..18ad98b27 100644 --- a/static/js/agenda/timeslot_edit.js +++ b/static/js/agenda/timeslot_edit.js @@ -18,10 +18,7 @@ //////////////-GLOBALS----//////////////////////////////////////// -var meeting_objs = {}; // contains a list of session objects -var slot_status = {}; // the status of the slot, in format { room_year-month-day_hour: { free: t/f, timeslotid: id } } -var slot_objs = {}; -var group_objs = {}; // list of working groups +var agenda_globals; var days = []; var legend_status = {}; // agenda area colors. @@ -59,41 +56,102 @@ $(document).ready(function() { This is ran at page load and sets up the entire page. */ function init_timeslot_edit(){ + agenda_globals = new AgendaGlobals(); log("initstuff() ran"); - setup_slots(); + var directorpromises = []; + setup_slots(directorpromises); + log("setup_slots() ran"); - fill_timeslots(); - resize_listeners(); - static_listeners(); + $.when.apply($,directorpromises).done(function() { + fill_timeslots(); + resize_listeners(); - $(".delete_room").unbind('click'); - $(".delete_room").click(delete_room); + static_listeners(); - $("#add_room").unbind('click') - $("#add_room").click(add_room); + $(".delete_room").unbind('click'); + $(".delete_room").click(delete_room); - $(".delete_slot").unbind('click'); - $(".delete_slot").click(delete_slot); + $("#add_room").unbind('click') + $("#add_room").click(add_room); - $("#add_day").unbind('click') - $("#add_day").click(add_day); + $(".delete_slot").unbind('click'); + $(".delete_slot").click(delete_slot); + + $("#add_day").unbind('click') + $("#add_day").click(add_day); + console.log("timeslot editor ready"); + }); + + /* datepicker stuff */ + create_datetimepicker(); + + /* hide the django form stuff we don't need */ + $("#id_duration").hide(); + $("label[for*='id_duration']").hide(); + $("#duration_time").val("01:00"); + format_datetime(); + + $("#pageloaded").show(); +} + +function create_datetimepicker(){ + $("#start_date").datepicker({ + dateFormat: "yy-mm-dd", + }); + $("#duration_time").timepicker({ + timeFormat: 'HH:mm', + hourMin: 0, + hourMax: 8, + stepMinute:5, + defaultValue: "01:00", + onSelect: function(selected){ + $("input[name*='duration_hours']").val($(this).val().split(':')[0]); + $("input[name*='duration_minutes']").val($(this).val().split(':')[1]); + format_datetime(); + } + }) + + $("#id_time").datetimepicker({ + timeFormat: 'HH:mm', + dateFormat: "yy-mm-dd", + defaultValue: first_day, + hourMin: 9, + hourMax: 22, + stepMinute:5, + onSelect: function(selected){ + duration_set($(this).datetimepicker('getDate')); + format_datetime() + } + }); + $("#id_time").datepicker('setDate', first_day); +} + +function format_datetime(){ + var startDate = $("#id_time").datetimepicker('getDate'); + var endTime = $("#id_time").datetimepicker('getDate'); + endTime.setHours(endTime.getHours()+parseInt($("#duration_time").val().split(':')[0])) + endTime.setMinutes(endTime.getMinutes()+parseInt($("#duration_time").val().split(':')[1])) + $("#timespan").html(moment($("#id_time").datetimepicker('getDate')).format('HH:mm') + " <-> " + moment(endTime).format('HH:mm')); +} + +function duration_set(d){ + $("input[name*='duration_hours']").val(d.getHours()); + $("input[name*='duration_minutes']").val(d.getMinutes()); } function add_room(event) { event.preventDefault(); - var rooms_url = $(event.target).attr('href'); $("#add_room_dialog").dialog({ "title" : "Add new room", - buttons : { - "Cancel" : function() { - $(this).dialog("close"); + buttons : { + "Cancel" : function() { + $(this).dialog("close"); + } } - } }); - - $("#room_delete_dialog").dialog("open"); + $("#add_room_dialog").dialog("open"); } function delete_room(event) { @@ -168,33 +226,29 @@ function delete_slot(event) { } function fill_timeslots() { - $.each(slot_status, function(key) { - ssid_arr = slot_status[key]; - - for(var q = 0; q<ssid_arr.length; q++){ - ssid = ssid_arr[q]; - insert_timeslotedit_cell(ssid); - } + // add no_timeslot class to all timeslots, it will be removed + // when an item is placed into the slot. + $(".agenda_slot").addClass("no_timeslot"); + $.each(agenda_globals.timeslot_bydomid, function(key) { + ts = agenda_globals.timeslot_bydomid[key]; + insert_timeslotedit_cell(ts); }); + + // now add a create option for every slot which hasn't got a timeslot + $.each($(".no_timeslot"),function(slot) { + create_timeslotedit_cell(this); + }); + } -function insert_timeslotedit_cell(ssid) { - var domid = ssid.domid - var roomtype=ssid.roomtype - var slot_id = ("#"+domid) - +function build_select_box(roomtype, domid, slot_id, select_id) { + //console.log("updating for", ts); roomtypesession=""; roomtypeother=""; roomtypeplenary=""; roomtypereserved=""; roomtypeclass=""; roomtypeunavailable=""; - //console.log("domid: "+domid+" has roomtype: "+roomtype) - $(slot_id).removeClass("agenda_slot_unavailable") - $(slot_id).removeClass("agenda_slot_other") - $(slot_id).removeClass("agenda_slot_session") - $(slot_id).removeClass("agenda_slot_plenary") - $(slot_id).removeClass("agenda_slot_reserved") if(roomtype == "session") { roomtypesession="selected"; @@ -213,7 +267,6 @@ function insert_timeslotedit_cell(ssid) { roomtypeclass="agenda_slot_unavailable"; } - var select_id = domid + "_select" html = "<form action=\"/some/place\" method=\"post\"><select id='"+select_id+"'>"; html = html + "<option value='session' "+roomtypesession+" id='option_"+domid+"_session'>session</option>"; html = html + "<option value='other' "+roomtypeother+" id='option_"+domid+"_other'>non-session</option>"; @@ -222,9 +275,26 @@ function insert_timeslotedit_cell(ssid) { html = html + "<option value='unavail' "+roomtypeunavailable+" id='option_"+domid+"_unavail'>unavailable</option>"; html = html + "</select>"; - $(slot_id).html(html) - $(slot_id).addClass(roomtypeclass) + $(slot_id).addClass(roomtypeclass); + return roomtypeclass; +} + + +function insert_timeslotedit_cell(ts) { + var roomtype=ts.roomtype; + var domid =ts.domid; + var slot_id =("#" + domid); + + $(slot_id).removeClass("agenda_slot_unavailable") + $(slot_id).removeClass("agenda_slot_other") + $(slot_id).removeClass("agenda_slot_session") + $(slot_id).removeClass("agenda_slot_plenary") + $(slot_id).removeClass("agenda_slot_reserved") + $(slot_id).removeClass("no_timeslot"); + + var select_id = domid + "_select"; + var roomtypeclass = build_select_box(roomtype, domid, slot_id, select_id); $("#"+select_id).change(function(eventObject) { start_spin(); @@ -239,18 +309,68 @@ function insert_timeslotedit_cell(ssid) { } else { stop_spin(); for(var key in json) { - ssid[key]=json[key]; + ts[key]=json[key]; } - console.log("server replied, updating cell contents: "+ssid.roomtype); - insert_timeslotedit_cell(ssid); + console.log("server replied, updating cell contents: "+ts.roomtype); + insert_timeslotedit_cell(ts); } }, { - 'timeslot_id': ssid.timeslot_id, + 'meeting_num': meeting_number, + 'timeslot_id': ts.timeslot_id, 'purpose': newpurpose, }); }); +} +var __debug_object; +function create_timeslotedit_cell(slot_id) { + var roomtype = "unavailable"; + + __debug_object = object; + + var object = $(slot_id); + var room = object.attr('slot_room'); + var time = object.attr('slot_time'); + var duration=object.attr('slot_duration'); + var domid= object.attr('id'); + + //$(slot_id).removeClass("agenda_slot_unavailable") + $(slot_id).removeClass("agenda_slot_other") + $(slot_id).removeClass("agenda_slot_session") + $(slot_id).removeClass("agenda_slot_plenary") + $(slot_id).removeClass("agenda_slot_reserved") + + var select_id = domid + "_select"; + var roomtypeclass = build_select_box(roomtype, "default", slot_id, select_id); + + $("#"+select_id).change(function(eventObject) { + start_spin(); + var newpurpose = $("#"+select_id).val() + console.log("creating setting id: #"+select_id+" to "+newpurpose+" ("+roomtypeclass+")"); + + Dajaxice.ietf.meeting.update_timeslot_purpose( + function(json) { + if(json == "") { + console.log("No reply from server...."); + } else { + stop_spin(); + for(var key in json) { + ts[key]=json[key]; + } + console.log("server replied, updating cell contents: "+ts.roomtype); + insert_timeslotedit_cell(ts); + } + }, + { + 'timeslot_id': "0", /* 0 indicates to make a new one */ + 'meeting_num': meeting_number, + 'room_id': room, + 'time' : time, + 'duration':duration, + 'purpose': newpurpose, + }); + }); } /* diff --git a/static/js/jquery-ui-timepicker/jquery-ui-sliderAccess.js b/static/js/jquery-ui-timepicker/jquery-ui-sliderAccess.js new file mode 100644 index 000000000..b075c6698 --- /dev/null +++ b/static/js/jquery-ui-timepicker/jquery-ui-sliderAccess.js @@ -0,0 +1,91 @@ +/* + * jQuery UI Slider Access + * By: Trent Richardson [http://trentrichardson.com] + * Version 0.3 + * Last Modified: 10/20/2012 + * + * Copyright 2011 Trent Richardson + * Dual licensed under the MIT and GPL licenses. + * http://trentrichardson.com/Impromptu/GPL-LICENSE.txt + * http://trentrichardson.com/Impromptu/MIT-LICENSE.txt + * + */ + (function($){ + + $.fn.extend({ + sliderAccess: function(options){ + options = options || {}; + options.touchonly = options.touchonly !== undefined? options.touchonly : true; // by default only show it if touch device + + if(options.touchonly === true && !("ontouchend" in document)){ + return $(this); + } + + return $(this).each(function(i,obj){ + var $t = $(this), + o = $.extend({},{ + where: 'after', + step: $t.slider('option','step'), + upIcon: 'ui-icon-plus', + downIcon: 'ui-icon-minus', + text: false, + upText: '+', + downText: '-', + buttonset: true, + buttonsetTag: 'span', + isRTL: false + }, options), + $buttons = $('<'+ o.buttonsetTag +' class="ui-slider-access">'+ + '<button data-icon="'+ o.downIcon +'" data-step="'+ (o.isRTL? o.step : o.step*-1) +'">'+ o.downText +'</button>'+ + '<button data-icon="'+ o.upIcon +'" data-step="'+ (o.isRTL? o.step*-1 : o.step) +'">'+ o.upText +'</button>'+ + '</'+ o.buttonsetTag +'>'); + + $buttons.children('button').each(function(j, jobj){ + var $jt = $(this); + $jt.button({ + text: o.text, + icons: { primary: $jt.data('icon') } + }) + .click(function(e){ + var step = $jt.data('step'), + curr = $t.slider('value'), + newval = curr += step*1, + minval = $t.slider('option','min'), + maxval = $t.slider('option','max'), + slidee = $t.slider("option", "slide") || function(){}, + stope = $t.slider("option", "stop") || function(){}; + + e.preventDefault(); + + if(newval < minval || newval > maxval){ + return; + } + + $t.slider('value', newval); + + slidee.call($t, null, { value: newval }); + stope.call($t, null, { value: newval }); + }); + }); + + // before or after + $t[o.where]($buttons); + + if(o.buttonset){ + $buttons.removeClass('ui-corner-right').removeClass('ui-corner-left').buttonset(); + $buttons.eq(0).addClass('ui-corner-left'); + $buttons.eq(1).addClass('ui-corner-right'); + } + + // adjust the width so we don't break the original layout + var bOuterWidth = $buttons.css({ + marginLeft: ((o.where === 'after' && !o.isRTL) || (o.where === 'before' && o.isRTL)? 10:0), + marginRight: ((o.where === 'before' && !o.isRTL) || (o.where === 'after' && o.isRTL)? 10:0) + }).outerWidth(true) + 5; + var tOuterWidth = $t.outerWidth(true); + $t.css('display','inline-block').width(tOuterWidth-bOuterWidth); + }); + } + }); + +})(jQuery); \ No newline at end of file diff --git a/static/js/jquery-ui-timepicker/jquery-ui-timepicker-addon.js b/static/js/jquery-ui-timepicker/jquery-ui-timepicker-addon.js new file mode 100644 index 000000000..644703ff4 --- /dev/null +++ b/static/js/jquery-ui-timepicker/jquery-ui-timepicker-addon.js @@ -0,0 +1,2134 @@ +/*! jQuery Timepicker Addon - v1.4 - 2013-08-11 +* http://trentrichardson.com/examples/timepicker +* Copyright (c) 2013 Trent Richardson; Licensed MIT */ +(function ($) { + + /* + * Lets not redefine timepicker, Prevent "Uncaught RangeError: Maximum call stack size exceeded" + */ + $.ui.timepicker = $.ui.timepicker || {}; + if ($.ui.timepicker.version) { + return; + } + + /* + * Extend jQueryUI, get it started with our version number + */ + $.extend($.ui, { + timepicker: { + version: "1.4" + } + }); + + /* + * Timepicker manager. + * Use the singleton instance of this class, $.timepicker, to interact with the time picker. + * Settings for (groups of) time pickers are maintained in an instance object, + * allowing multiple different settings on the same page. + */ + var Timepicker = function () { + this.regional = []; // Available regional settings, indexed by language code + this.regional[''] = { // Default regional settings + currentText: 'Now', + closeText: 'Done', + amNames: ['AM', 'A'], + pmNames: ['PM', 'P'], + timeFormat: 'HH:mm', + timeSuffix: '', + timeOnlyTitle: 'Choose Time', + timeText: 'Time', + hourText: 'Hour', + minuteText: 'Minute', + secondText: 'Second', + millisecText: 'Millisecond', + microsecText: 'Microsecond', + timezoneText: 'Time Zone', + isRTL: false + }; + this._defaults = { // Global defaults for all the datetime picker instances + showButtonPanel: true, + timeOnly: false, + showHour: null, + showMinute: null, + showSecond: null, + showMillisec: null, + showMicrosec: null, + showTimezone: null, + showTime: true, + stepHour: 1, + stepMinute: 1, + stepSecond: 1, + stepMillisec: 1, + stepMicrosec: 1, + hour: 0, + minute: 0, + second: 0, + millisec: 0, + microsec: 0, + timezone: null, + hourMin: 0, + minuteMin: 0, + secondMin: 0, + millisecMin: 0, + microsecMin: 0, + hourMax: 23, + minuteMax: 59, + secondMax: 59, + millisecMax: 999, + microsecMax: 999, + minDateTime: null, + maxDateTime: null, + onSelect: null, + hourGrid: 0, + minuteGrid: 0, + secondGrid: 0, + millisecGrid: 0, + microsecGrid: 0, + alwaysSetTime: true, + separator: ' ', + altFieldTimeOnly: true, + altTimeFormat: null, + altSeparator: null, + altTimeSuffix: null, + pickerTimeFormat: null, + pickerTimeSuffix: null, + showTimepicker: true, + timezoneList: null, + addSliderAccess: false, + sliderAccessArgs: null, + controlType: 'slider', + defaultValue: null, + parse: 'strict' + }; + $.extend(this._defaults, this.regional['']); + }; + + $.extend(Timepicker.prototype, { + $input: null, + $altInput: null, + $timeObj: null, + inst: null, + hour_slider: null, + minute_slider: null, + second_slider: null, + millisec_slider: null, + microsec_slider: null, + timezone_select: null, + hour: 0, + minute: 0, + second: 0, + millisec: 0, + microsec: 0, + timezone: null, + hourMinOriginal: null, + minuteMinOriginal: null, + secondMinOriginal: null, + millisecMinOriginal: null, + microsecMinOriginal: null, + hourMaxOriginal: null, + minuteMaxOriginal: null, + secondMaxOriginal: null, + millisecMaxOriginal: null, + microsecMaxOriginal: null, + ampm: '', + formattedDate: '', + formattedTime: '', + formattedDateTime: '', + timezoneList: null, + units: ['hour', 'minute', 'second', 'millisec', 'microsec'], + support: {}, + control: null, + + /* + * Override the default settings for all instances of the time picker. + * @param {Object} settings object - the new settings to use as defaults (anonymous object) + * @return {Object} the manager object + */ + setDefaults: function (settings) { + extendRemove(this._defaults, settings || {}); + return this; + }, + + /* + * Create a new Timepicker instance + */ + _newInst: function ($input, opts) { + var tp_inst = new Timepicker(), + inlineSettings = {}, + fns = {}, + overrides, i; + + for (var attrName in this._defaults) { + if (this._defaults.hasOwnProperty(attrName)) { + var attrValue = $input.attr('time:' + attrName); + if (attrValue) { + try { + inlineSettings[attrName] = eval(attrValue); + } catch (err) { + inlineSettings[attrName] = attrValue; + } + } + } + } + + overrides = { + beforeShow: function (input, dp_inst) { + if ($.isFunction(tp_inst._defaults.evnts.beforeShow)) { + return tp_inst._defaults.evnts.beforeShow.call($input[0], input, dp_inst, tp_inst); + } + }, + onChangeMonthYear: function (year, month, dp_inst) { + // Update the time as well : this prevents the time from disappearing from the $input field. + tp_inst._updateDateTime(dp_inst); + if ($.isFunction(tp_inst._defaults.evnts.onChangeMonthYear)) { + tp_inst._defaults.evnts.onChangeMonthYear.call($input[0], year, month, dp_inst, tp_inst); + } + }, + onClose: function (dateText, dp_inst) { + if (tp_inst.timeDefined === true && $input.val() !== '') { + tp_inst._updateDateTime(dp_inst); + } + if ($.isFunction(tp_inst._defaults.evnts.onClose)) { + tp_inst._defaults.evnts.onClose.call($input[0], dateText, dp_inst, tp_inst); + } + } + }; + for (i in overrides) { + if (overrides.hasOwnProperty(i)) { + fns[i] = opts[i] || null; + } + } + + tp_inst._defaults = $.extend({}, this._defaults, inlineSettings, opts, overrides, { + evnts: fns, + timepicker: tp_inst // add timepicker as a property of datepicker: $.datepicker._get(dp_inst, 'timepicker'); + }); + tp_inst.amNames = $.map(tp_inst._defaults.amNames, function (val) { + return val.toUpperCase(); + }); + tp_inst.pmNames = $.map(tp_inst._defaults.pmNames, function (val) { + return val.toUpperCase(); + }); + + // detect which units are supported + tp_inst.support = detectSupport( + tp_inst._defaults.timeFormat + + (tp_inst._defaults.pickerTimeFormat ? tp_inst._defaults.pickerTimeFormat : '') + + (tp_inst._defaults.altTimeFormat ? tp_inst._defaults.altTimeFormat : '')); + + // controlType is string - key to our this._controls + if (typeof(tp_inst._defaults.controlType) === 'string') { + if (tp_inst._defaults.controlType === 'slider' && typeof($.ui.slider) === 'undefined') { + tp_inst._defaults.controlType = 'select'; + } + tp_inst.control = tp_inst._controls[tp_inst._defaults.controlType]; + } + // controlType is an object and must implement create, options, value methods + else { + tp_inst.control = tp_inst._defaults.controlType; + } + + // prep the timezone options + var timezoneList = [-720, -660, -600, -570, -540, -480, -420, -360, -300, -270, -240, -210, -180, -120, -60, + 0, 60, 120, 180, 210, 240, 270, 300, 330, 345, 360, 390, 420, 480, 525, 540, 570, 600, 630, 660, 690, 720, 765, 780, 840]; + if (tp_inst._defaults.timezoneList !== null) { + timezoneList = tp_inst._defaults.timezoneList; + } + var tzl = timezoneList.length, tzi = 0, tzv = null; + if (tzl > 0 && typeof timezoneList[0] !== 'object') { + for (; tzi < tzl; tzi++) { + tzv = timezoneList[tzi]; + timezoneList[tzi] = { value: tzv, label: $.timepicker.timezoneOffsetString(tzv, tp_inst.support.iso8601) }; + } + } + tp_inst._defaults.timezoneList = timezoneList; + + // set the default units + tp_inst.timezone = tp_inst._defaults.timezone !== null ? $.timepicker.timezoneOffsetNumber(tp_inst._defaults.timezone) : + ((new Date()).getTimezoneOffset() * -1); + tp_inst.hour = tp_inst._defaults.hour < tp_inst._defaults.hourMin ? tp_inst._defaults.hourMin : + tp_inst._defaults.hour > tp_inst._defaults.hourMax ? tp_inst._defaults.hourMax : tp_inst._defaults.hour; + tp_inst.minute = tp_inst._defaults.minute < tp_inst._defaults.minuteMin ? tp_inst._defaults.minuteMin : + tp_inst._defaults.minute > tp_inst._defaults.minuteMax ? tp_inst._defaults.minuteMax : tp_inst._defaults.minute; + tp_inst.second = tp_inst._defaults.second < tp_inst._defaults.secondMin ? tp_inst._defaults.secondMin : + tp_inst._defaults.second > tp_inst._defaults.secondMax ? tp_inst._defaults.secondMax : tp_inst._defaults.second; + tp_inst.millisec = tp_inst._defaults.millisec < tp_inst._defaults.millisecMin ? tp_inst._defaults.millisecMin : + tp_inst._defaults.millisec > tp_inst._defaults.millisecMax ? tp_inst._defaults.millisecMax : tp_inst._defaults.millisec; + tp_inst.microsec = tp_inst._defaults.microsec < tp_inst._defaults.microsecMin ? tp_inst._defaults.microsecMin : + tp_inst._defaults.microsec > tp_inst._defaults.microsecMax ? tp_inst._defaults.microsecMax : tp_inst._defaults.microsec; + tp_inst.ampm = ''; + tp_inst.$input = $input; + + if (tp_inst._defaults.altField) { + tp_inst.$altInput = $(tp_inst._defaults.altField).css({ + cursor: 'pointer' + }).focus(function () { + $input.trigger("focus"); + }); + } + + if (tp_inst._defaults.minDate === 0 || tp_inst._defaults.minDateTime === 0) { + tp_inst._defaults.minDate = new Date(); + } + if (tp_inst._defaults.maxDate === 0 || tp_inst._defaults.maxDateTime === 0) { + tp_inst._defaults.maxDate = new Date(); + } + + // datepicker needs minDate/maxDate, timepicker needs minDateTime/maxDateTime.. + if (tp_inst._defaults.minDate !== undefined && tp_inst._defaults.minDate instanceof Date) { + tp_inst._defaults.minDateTime = new Date(tp_inst._defaults.minDate.getTime()); + } + if (tp_inst._defaults.minDateTime !== undefined && tp_inst._defaults.minDateTime instanceof Date) { + tp_inst._defaults.minDate = new Date(tp_inst._defaults.minDateTime.getTime()); + } + if (tp_inst._defaults.maxDate !== undefined && tp_inst._defaults.maxDate instanceof Date) { + tp_inst._defaults.maxDateTime = new Date(tp_inst._defaults.maxDate.getTime()); + } + if (tp_inst._defaults.maxDateTime !== undefined && tp_inst._defaults.maxDateTime instanceof Date) { + tp_inst._defaults.maxDate = new Date(tp_inst._defaults.maxDateTime.getTime()); + } + tp_inst.$input.bind('focus', function () { + tp_inst._onFocus(); + }); + + return tp_inst; + }, + + /* + * add our sliders to the calendar + */ + _addTimePicker: function (dp_inst) { + var currDT = (this.$altInput && this._defaults.altFieldTimeOnly) ? this.$input.val() + ' ' + this.$altInput.val() : this.$input.val(); + + this.timeDefined = this._parseTime(currDT); + this._limitMinMaxDateTime(dp_inst, false); + this._injectTimePicker(); + }, + + /* + * parse the time string from input value or _setTime + */ + _parseTime: function (timeString, withDate) { + if (!this.inst) { + this.inst = $.datepicker._getInst(this.$input[0]); + } + + if (withDate || !this._defaults.timeOnly) { + var dp_dateFormat = $.datepicker._get(this.inst, 'dateFormat'); + try { + var parseRes = parseDateTimeInternal(dp_dateFormat, this._defaults.timeFormat, timeString, $.datepicker._getFormatConfig(this.inst), this._defaults); + if (!parseRes.timeObj) { + return false; + } + $.extend(this, parseRes.timeObj); + } catch (err) { + $.timepicker.log("Error parsing the date/time string: " + err + + "\ndate/time string = " + timeString + + "\ntimeFormat = " + this._defaults.timeFormat + + "\ndateFormat = " + dp_dateFormat); + return false; + } + return true; + } else { + var timeObj = $.datepicker.parseTime(this._defaults.timeFormat, timeString, this._defaults); + if (!timeObj) { + return false; + } + $.extend(this, timeObj); + return true; + } + }, + + /* + * generate and inject html for timepicker into ui datepicker + */ + _injectTimePicker: function () { + var $dp = this.inst.dpDiv, + o = this.inst.settings, + tp_inst = this, + litem = '', + uitem = '', + show = null, + max = {}, + gridSize = {}, + size = null, + i = 0, + l = 0; + + // Prevent displaying twice + if ($dp.find("div.ui-timepicker-div").length === 0 && o.showTimepicker) { + var noDisplay = ' style="display:none;"', + html = '<div class="ui-timepicker-div' + (o.isRTL ? ' ui-timepicker-rtl' : '') + '"><dl>' + '<dt class="ui_tpicker_time_label"' + ((o.showTime) ? '' : noDisplay) + '>' + o.timeText + '</dt>' + + '<dd class="ui_tpicker_time"' + ((o.showTime) ? '' : noDisplay) + '></dd>'; + + // Create the markup + for (i = 0, l = this.units.length; i < l; i++) { + litem = this.units[i]; + uitem = litem.substr(0, 1).toUpperCase() + litem.substr(1); + show = o['show' + uitem] !== null ? o['show' + uitem] : this.support[litem]; + + // Added by Peter Medeiros: + // - Figure out what the hour/minute/second max should be based on the step values. + // - Example: if stepMinute is 15, then minMax is 45. + max[litem] = parseInt((o[litem + 'Max'] - ((o[litem + 'Max'] - o[litem + 'Min']) % o['step' + uitem])), 10); + gridSize[litem] = 0; + + html += '<dt class="ui_tpicker_' + litem + '_label"' + (show ? '' : noDisplay) + '>' + o[litem + 'Text'] + '</dt>' + + '<dd class="ui_tpicker_' + litem + '"><div class="ui_tpicker_' + litem + '_slider"' + (show ? '' : noDisplay) + '></div>'; + + if (show && o[litem + 'Grid'] > 0) { + html += '<div style="padding-left: 1px"><table class="ui-tpicker-grid-label"><tr>'; + + if (litem === 'hour') { + for (var h = o[litem + 'Min']; h <= max[litem]; h += parseInt(o[litem + 'Grid'], 10)) { + gridSize[litem]++; + var tmph = $.datepicker.formatTime(this.support.ampm ? 'hht' : 'HH', {hour: h}, o); + html += '<td data-for="' + litem + '">' + tmph + '</td>'; + } + } + else { + for (var m = o[litem + 'Min']; m <= max[litem]; m += parseInt(o[litem + 'Grid'], 10)) { + gridSize[litem]++; + html += '<td data-for="' + litem + '">' + ((m < 10) ? '0' : '') + m + '</td>'; + } + } + + html += '</tr></table></div>'; + } + html += '</dd>'; + } + + // Timezone + var showTz = o.showTimezone !== null ? o.showTimezone : this.support.timezone; + html += '<dt class="ui_tpicker_timezone_label"' + (showTz ? '' : noDisplay) + '>' + o.timezoneText + '</dt>'; + html += '<dd class="ui_tpicker_timezone" ' + (showTz ? '' : noDisplay) + '></dd>'; + + // Create the elements from string + html += '</dl></div>'; + var $tp = $(html); + + // if we only want time picker... + if (o.timeOnly === true) { + $tp.prepend('<div class="ui-widget-header ui-helper-clearfix ui-corner-all">' + '<div class="ui-datepicker-title">' + o.timeOnlyTitle + '</div>' + '</div>'); + $dp.find('.ui-datepicker-header, .ui-datepicker-calendar').hide(); + } + + // add sliders, adjust grids, add events + for (i = 0, l = tp_inst.units.length; i < l; i++) { + litem = tp_inst.units[i]; + uitem = litem.substr(0, 1).toUpperCase() + litem.substr(1); + show = o['show' + uitem] !== null ? o['show' + uitem] : this.support[litem]; + + // add the slider + tp_inst[litem + '_slider'] = tp_inst.control.create(tp_inst, $tp.find('.ui_tpicker_' + litem + '_slider'), litem, tp_inst[litem], o[litem + 'Min'], max[litem], o['step' + uitem]); + + // adjust the grid and add click event + if (show && o[litem + 'Grid'] > 0) { + size = 100 * gridSize[litem] * o[litem + 'Grid'] / (max[litem] - o[litem + 'Min']); + $tp.find('.ui_tpicker_' + litem + ' table').css({ + width: size + "%", + marginLeft: o.isRTL ? '0' : ((size / (-2 * gridSize[litem])) + "%"), + marginRight: o.isRTL ? ((size / (-2 * gridSize[litem])) + "%") : '0', + borderCollapse: 'collapse' + }).find("td").click(function (e) { + var $t = $(this), + h = $t.html(), + n = parseInt(h.replace(/[^0-9]/g), 10), + ap = h.replace(/[^apm]/ig), + f = $t.data('for'); // loses scope, so we use data-for + + if (f === 'hour') { + if (ap.indexOf('p') !== -1 && n < 12) { + n += 12; + } + else { + if (ap.indexOf('a') !== -1 && n === 12) { + n = 0; + } + } + } + + tp_inst.control.value(tp_inst, tp_inst[f + '_slider'], litem, n); + + tp_inst._onTimeChange(); + tp_inst._onSelectHandler(); + }).css({ + cursor: 'pointer', + width: (100 / gridSize[litem]) + '%', + textAlign: 'center', + overflow: 'hidden' + }); + } // end if grid > 0 + } // end for loop + + // Add timezone options + this.timezone_select = $tp.find('.ui_tpicker_timezone').append('<select></select>').find("select"); + $.fn.append.apply(this.timezone_select, + $.map(o.timezoneList, function (val, idx) { + return $("<option />").val(typeof val === "object" ? val.value : val).text(typeof val === "object" ? val.label : val); + })); + if (typeof(this.timezone) !== "undefined" && this.timezone !== null && this.timezone !== "") { + var local_timezone = (new Date(this.inst.selectedYear, this.inst.selectedMonth, this.inst.selectedDay, 12)).getTimezoneOffset() * -1; + if (local_timezone === this.timezone) { + selectLocalTimezone(tp_inst); + } else { + this.timezone_select.val(this.timezone); + } + } else { + if (typeof(this.hour) !== "undefined" && this.hour !== null && this.hour !== "") { + this.timezone_select.val(o.timezone); + } else { + selectLocalTimezone(tp_inst); + } + } + this.timezone_select.change(function () { + tp_inst._onTimeChange(); + tp_inst._onSelectHandler(); + }); + // End timezone options + + // inject timepicker into datepicker + var $buttonPanel = $dp.find('.ui-datepicker-buttonpane'); + if ($buttonPanel.length) { + $buttonPanel.before($tp); + } else { + $dp.append($tp); + } + + this.$timeObj = $tp.find('.ui_tpicker_time'); + + if (this.inst !== null) { + var timeDefined = this.timeDefined; + this._onTimeChange(); + this.timeDefined = timeDefined; + } + + // slideAccess integration: http://trentrichardson.com/2011/11/11/jquery-ui-sliders-and-touch-accessibility/ + if (this._defaults.addSliderAccess) { + var sliderAccessArgs = this._defaults.sliderAccessArgs, + rtl = this._defaults.isRTL; + sliderAccessArgs.isRTL = rtl; + + setTimeout(function () { // fix for inline mode + if ($tp.find('.ui-slider-access').length === 0) { + $tp.find('.ui-slider:visible').sliderAccess(sliderAccessArgs); + + // fix any grids since sliders are shorter + var sliderAccessWidth = $tp.find('.ui-slider-access:eq(0)').outerWidth(true); + if (sliderAccessWidth) { + $tp.find('table:visible').each(function () { + var $g = $(this), + oldWidth = $g.outerWidth(), + oldMarginLeft = $g.css(rtl ? 'marginRight' : 'marginLeft').toString().replace('%', ''), + newWidth = oldWidth - sliderAccessWidth, + newMarginLeft = ((oldMarginLeft * newWidth) / oldWidth) + '%', + css = { width: newWidth, marginRight: 0, marginLeft: 0 }; + css[rtl ? 'marginRight' : 'marginLeft'] = newMarginLeft; + $g.css(css); + }); + } + } + }, 10); + } + // end slideAccess integration + + tp_inst._limitMinMaxDateTime(this.inst, true); + } + }, + + /* + * This function tries to limit the ability to go outside the + * min/max date range + */ + _limitMinMaxDateTime: function (dp_inst, adjustSliders) { + var o = this._defaults, + dp_date = new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay); + + if (!this._defaults.showTimepicker) { + return; + } // No time so nothing to check here + + if ($.datepicker._get(dp_inst, 'minDateTime') !== null && $.datepicker._get(dp_inst, 'minDateTime') !== undefined && dp_date) { + var minDateTime = $.datepicker._get(dp_inst, 'minDateTime'), + minDateTimeDate = new Date(minDateTime.getFullYear(), minDateTime.getMonth(), minDateTime.getDate(), 0, 0, 0, 0); + + if (this.hourMinOriginal === null || this.minuteMinOriginal === null || this.secondMinOriginal === null || this.millisecMinOriginal === null || this.microsecMinOriginal === null) { + this.hourMinOriginal = o.hourMin; + this.minuteMinOriginal = o.minuteMin; + this.secondMinOriginal = o.secondMin; + this.millisecMinOriginal = o.millisecMin; + this.microsecMinOriginal = o.microsecMin; + } + + if (dp_inst.settings.timeOnly || minDateTimeDate.getTime() === dp_date.getTime()) { + this._defaults.hourMin = minDateTime.getHours(); + if (this.hour <= this._defaults.hourMin) { + this.hour = this._defaults.hourMin; + this._defaults.minuteMin = minDateTime.getMinutes(); + if (this.minute <= this._defaults.minuteMin) { + this.minute = this._defaults.minuteMin; + this._defaults.secondMin = minDateTime.getSeconds(); + if (this.second <= this._defaults.secondMin) { + this.second = this._defaults.secondMin; + this._defaults.millisecMin = minDateTime.getMilliseconds(); + if (this.millisec <= this._defaults.millisecMin) { + this.millisec = this._defaults.millisecMin; + this._defaults.microsecMin = minDateTime.getMicroseconds(); + } else { + if (this.microsec < this._defaults.microsecMin) { + this.microsec = this._defaults.microsecMin; + } + this._defaults.microsecMin = this.microsecMinOriginal; + } + } else { + this._defaults.millisecMin = this.millisecMinOriginal; + this._defaults.microsecMin = this.microsecMinOriginal; + } + } else { + this._defaults.secondMin = this.secondMinOriginal; + this._defaults.millisecMin = this.millisecMinOriginal; + this._defaults.microsecMin = this.microsecMinOriginal; + } + } else { + this._defaults.minuteMin = this.minuteMinOriginal; + this._defaults.secondMin = this.secondMinOriginal; + this._defaults.millisecMin = this.millisecMinOriginal; + this._defaults.microsecMin = this.microsecMinOriginal; + } + } else { + this._defaults.hourMin = this.hourMinOriginal; + this._defaults.minuteMin = this.minuteMinOriginal; + this._defaults.secondMin = this.secondMinOriginal; + this._defaults.millisecMin = this.millisecMinOriginal; + this._defaults.microsecMin = this.microsecMinOriginal; + } + } + + if ($.datepicker._get(dp_inst, 'maxDateTime') !== null && $.datepicker._get(dp_inst, 'maxDateTime') !== undefined && dp_date) { + var maxDateTime = $.datepicker._get(dp_inst, 'maxDateTime'), + maxDateTimeDate = new Date(maxDateTime.getFullYear(), maxDateTime.getMonth(), maxDateTime.getDate(), 0, 0, 0, 0); + + if (this.hourMaxOriginal === null || this.minuteMaxOriginal === null || this.secondMaxOriginal === null || this.millisecMaxOriginal === null) { + this.hourMaxOriginal = o.hourMax; + this.minuteMaxOriginal = o.minuteMax; + this.secondMaxOriginal = o.secondMax; + this.millisecMaxOriginal = o.millisecMax; + this.microsecMaxOriginal = o.microsecMax; + } + + if (dp_inst.settings.timeOnly || maxDateTimeDate.getTime() === dp_date.getTime()) { + this._defaults.hourMax = maxDateTime.getHours(); + if (this.hour >= this._defaults.hourMax) { + this.hour = this._defaults.hourMax; + this._defaults.minuteMax = maxDateTime.getMinutes(); + if (this.minute >= this._defaults.minuteMax) { + this.minute = this._defaults.minuteMax; + this._defaults.secondMax = maxDateTime.getSeconds(); + if (this.second >= this._defaults.secondMax) { + this.second = this._defaults.secondMax; + this._defaults.millisecMax = maxDateTime.getMilliseconds(); + if (this.millisec >= this._defaults.millisecMax) { + this.millisec = this._defaults.millisecMax; + this._defaults.microsecMax = maxDateTime.getMicroseconds(); + } else { + if (this.microsec > this._defaults.microsecMax) { + this.microsec = this._defaults.microsecMax; + } + this._defaults.microsecMax = this.microsecMaxOriginal; + } + } else { + this._defaults.millisecMax = this.millisecMaxOriginal; + this._defaults.microsecMax = this.microsecMaxOriginal; + } + } else { + this._defaults.secondMax = this.secondMaxOriginal; + this._defaults.millisecMax = this.millisecMaxOriginal; + this._defaults.microsecMax = this.microsecMaxOriginal; + } + } else { + this._defaults.minuteMax = this.minuteMaxOriginal; + this._defaults.secondMax = this.secondMaxOriginal; + this._defaults.millisecMax = this.millisecMaxOriginal; + this._defaults.microsecMax = this.microsecMaxOriginal; + } + } else { + this._defaults.hourMax = this.hourMaxOriginal; + this._defaults.minuteMax = this.minuteMaxOriginal; + this._defaults.secondMax = this.secondMaxOriginal; + this._defaults.millisecMax = this.millisecMaxOriginal; + this._defaults.microsecMax = this.microsecMaxOriginal; + } + } + + if (adjustSliders !== undefined && adjustSliders === true) { + var hourMax = parseInt((this._defaults.hourMax - ((this._defaults.hourMax - this._defaults.hourMin) % this._defaults.stepHour)), 10), + minMax = parseInt((this._defaults.minuteMax - ((this._defaults.minuteMax - this._defaults.minuteMin) % this._defaults.stepMinute)), 10), + secMax = parseInt((this._defaults.secondMax - ((this._defaults.secondMax - this._defaults.secondMin) % this._defaults.stepSecond)), 10), + millisecMax = parseInt((this._defaults.millisecMax - ((this._defaults.millisecMax - this._defaults.millisecMin) % this._defaults.stepMillisec)), 10), + microsecMax = parseInt((this._defaults.microsecMax - ((this._defaults.microsecMax - this._defaults.microsecMin) % this._defaults.stepMicrosec)), 10); + + if (this.hour_slider) { + this.control.options(this, this.hour_slider, 'hour', { min: this._defaults.hourMin, max: hourMax }); + this.control.value(this, this.hour_slider, 'hour', this.hour - (this.hour % this._defaults.stepHour)); + } + if (this.minute_slider) { + this.control.options(this, this.minute_slider, 'minute', { min: this._defaults.minuteMin, max: minMax }); + this.control.value(this, this.minute_slider, 'minute', this.minute - (this.minute % this._defaults.stepMinute)); + } + if (this.second_slider) { + this.control.options(this, this.second_slider, 'second', { min: this._defaults.secondMin, max: secMax }); + this.control.value(this, this.second_slider, 'second', this.second - (this.second % this._defaults.stepSecond)); + } + if (this.millisec_slider) { + this.control.options(this, this.millisec_slider, 'millisec', { min: this._defaults.millisecMin, max: millisecMax }); + this.control.value(this, this.millisec_slider, 'millisec', this.millisec - (this.millisec % this._defaults.stepMillisec)); + } + if (this.microsec_slider) { + this.control.options(this, this.microsec_slider, 'microsec', { min: this._defaults.microsecMin, max: microsecMax }); + this.control.value(this, this.microsec_slider, 'microsec', this.microsec - (this.microsec % this._defaults.stepMicrosec)); + } + } + + }, + + /* + * when a slider moves, set the internal time... + * on time change is also called when the time is updated in the text field + */ + _onTimeChange: function () { + if (!this._defaults.showTimepicker) { + return; + } + var hour = (this.hour_slider) ? this.control.value(this, this.hour_slider, 'hour') : false, + minute = (this.minute_slider) ? this.control.value(this, this.minute_slider, 'minute') : false, + second = (this.second_slider) ? this.control.value(this, this.second_slider, 'second') : false, + millisec = (this.millisec_slider) ? this.control.value(this, this.millisec_slider, 'millisec') : false, + microsec = (this.microsec_slider) ? this.control.value(this, this.microsec_slider, 'microsec') : false, + timezone = (this.timezone_select) ? this.timezone_select.val() : false, + o = this._defaults, + pickerTimeFormat = o.pickerTimeFormat || o.timeFormat, + pickerTimeSuffix = o.pickerTimeSuffix || o.timeSuffix; + + if (typeof(hour) === 'object') { + hour = false; + } + if (typeof(minute) === 'object') { + minute = false; + } + if (typeof(second) === 'object') { + second = false; + } + if (typeof(millisec) === 'object') { + millisec = false; + } + if (typeof(microsec) === 'object') { + microsec = false; + } + if (typeof(timezone) === 'object') { + timezone = false; + } + + if (hour !== false) { + hour = parseInt(hour, 10); + } + if (minute !== false) { + minute = parseInt(minute, 10); + } + if (second !== false) { + second = parseInt(second, 10); + } + if (millisec !== false) { + millisec = parseInt(millisec, 10); + } + if (microsec !== false) { + microsec = parseInt(microsec, 10); + } + + var ampm = o[hour < 12 ? 'amNames' : 'pmNames'][0]; + + // If the update was done in the input field, the input field should not be updated. + // If the update was done using the sliders, update the input field. + var hasChanged = (hour !== this.hour || minute !== this.minute || second !== this.second || millisec !== this.millisec || microsec !== this.microsec || + (this.ampm.length > 0 && (hour < 12) !== ($.inArray(this.ampm.toUpperCase(), this.amNames) !== -1)) || (this.timezone !== null && timezone !== this.timezone)); + + if (hasChanged) { + + if (hour !== false) { + this.hour = hour; + } + if (minute !== false) { + this.minute = minute; + } + if (second !== false) { + this.second = second; + } + if (millisec !== false) { + this.millisec = millisec; + } + if (microsec !== false) { + this.microsec = microsec; + } + if (timezone !== false) { + this.timezone = timezone; + } + + if (!this.inst) { + this.inst = $.datepicker._getInst(this.$input[0]); + } + + this._limitMinMaxDateTime(this.inst, true); + } + if (this.support.ampm) { + this.ampm = ampm; + } + + // Updates the time within the timepicker + this.formattedTime = $.datepicker.formatTime(o.timeFormat, this, o); + if (this.$timeObj) { + if (pickerTimeFormat === o.timeFormat) { + this.$timeObj.text(this.formattedTime + pickerTimeSuffix); + } + else { + this.$timeObj.text($.datepicker.formatTime(pickerTimeFormat, this, o) + pickerTimeSuffix); + } + } + + this.timeDefined = true; + if (hasChanged) { + this._updateDateTime(); + } + }, + + /* + * call custom onSelect. + * bind to sliders slidestop, and grid click. + */ + _onSelectHandler: function () { + var onSelect = this._defaults.onSelect || this.inst.settings.onSelect; + var inputEl = this.$input ? this.$input[0] : null; + if (onSelect && inputEl) { + onSelect.apply(inputEl, [this.formattedDateTime, this]); + } + }, + + /* + * update our input with the new date time.. + */ + _updateDateTime: function (dp_inst) { + dp_inst = this.inst || dp_inst; + var dtTmp = (dp_inst.currentYear > 0? + new Date(dp_inst.currentYear, dp_inst.currentMonth, dp_inst.currentDay) : + new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay)), + dt = $.datepicker._daylightSavingAdjust(dtTmp), + //dt = $.datepicker._daylightSavingAdjust(new Date(dp_inst.selectedYear, dp_inst.selectedMonth, dp_inst.selectedDay)), + //dt = $.datepicker._daylightSavingAdjust(new Date(dp_inst.currentYear, dp_inst.currentMonth, dp_inst.currentDay)), + dateFmt = $.datepicker._get(dp_inst, 'dateFormat'), + formatCfg = $.datepicker._getFormatConfig(dp_inst), + timeAvailable = dt !== null && this.timeDefined; + this.formattedDate = $.datepicker.formatDate(dateFmt, (dt === null ? new Date() : dt), formatCfg); + var formattedDateTime = this.formattedDate; + + // if a slider was changed but datepicker doesn't have a value yet, set it + if (dp_inst.lastVa === "") { + dp_inst.currentYear = dp_inst.selectedYear; + dp_inst.currentMonth = dp_inst.selectedMonth; + dp_inst.currentDay = dp_inst.selectedDay; + } + + /* + * remove following lines to force every changes in date picker to change the input value + * Bug descriptions: when an input field has a default value, and click on the field to pop up the date picker. + * If the user manually empty the value in the input field, the date picker will never change selected value. + */ + //if (dp_inst.lastVal !== undefined && (dp_inst.lastVal.length > 0 && this.$input.val().length === 0)) { + // return; + //} + + if (this._defaults.timeOnly === true) { + formattedDateTime = this.formattedTime; + } else if (this._defaults.timeOnly !== true && (this._defaults.alwaysSetTime || timeAvailable)) { + formattedDateTime += this._defaults.separator + this.formattedTime + this._defaults.timeSuffix; + } + + this.formattedDateTime = formattedDateTime; + + if (!this._defaults.showTimepicker) { + this.$input.val(this.formattedDate); + } else if (this.$altInput && this._defaults.timeOnly === false && this._defaults.altFieldTimeOnly === true) { + this.$altInput.val(this.formattedTime); + this.$input.val(this.formattedDate); + } else if (this.$altInput) { + this.$input.val(formattedDateTime); + var altFormattedDateTime = '', + altSeparator = this._defaults.altSeparator ? this._defaults.altSeparator : this._defaults.separator, + altTimeSuffix = this._defaults.altTimeSuffix ? this._defaults.altTimeSuffix : this._defaults.timeSuffix; + + if (!this._defaults.timeOnly) { + if (this._defaults.altFormat) { + altFormattedDateTime = $.datepicker.formatDate(this._defaults.altFormat, (dt === null ? new Date() : dt), formatCfg); + } + else { + altFormattedDateTime = this.formattedDate; + } + + if (altFormattedDateTime) { + altFormattedDateTime += altSeparator; + } + } + + if (this._defaults.altTimeFormat) { + altFormattedDateTime += $.datepicker.formatTime(this._defaults.altTimeFormat, this, this._defaults) + altTimeSuffix; + } + else { + altFormattedDateTime += this.formattedTime + altTimeSuffix; + } + this.$altInput.val(altFormattedDateTime); + } else { + this.$input.val(formattedDateTime); + } + + this.$input.trigger("change"); + }, + + _onFocus: function () { + if (!this.$input.val() && this._defaults.defaultValue) { + this.$input.val(this._defaults.defaultValue); + var inst = $.datepicker._getInst(this.$input.get(0)), + tp_inst = $.datepicker._get(inst, 'timepicker'); + if (tp_inst) { + if (tp_inst._defaults.timeOnly && (inst.input.val() !== inst.lastVal)) { + try { + $.datepicker._updateDatepicker(inst); + } catch (err) { + $.timepicker.log(err); + } + } + } + } + }, + + /* + * Small abstraction to control types + * We can add more, just be sure to follow the pattern: create, options, value + */ + _controls: { + // slider methods + slider: { + create: function (tp_inst, obj, unit, val, min, max, step) { + var rtl = tp_inst._defaults.isRTL; // if rtl go -60->0 instead of 0->60 + return obj.prop('slide', null).slider({ + orientation: "horizontal", + value: rtl ? val * -1 : val, + min: rtl ? max * -1 : min, + max: rtl ? min * -1 : max, + step: step, + slide: function (event, ui) { + tp_inst.control.value(tp_inst, $(this), unit, rtl ? ui.value * -1 : ui.value); + tp_inst._onTimeChange(); + }, + stop: function (event, ui) { + tp_inst._onSelectHandler(); + } + }); + }, + options: function (tp_inst, obj, unit, opts, val) { + if (tp_inst._defaults.isRTL) { + if (typeof(opts) === 'string') { + if (opts === 'min' || opts === 'max') { + if (val !== undefined) { + return obj.slider(opts, val * -1); + } + return Math.abs(obj.slider(opts)); + } + return obj.slider(opts); + } + var min = opts.min, + max = opts.max; + opts.min = opts.max = null; + if (min !== undefined) { + opts.max = min * -1; + } + if (max !== undefined) { + opts.min = max * -1; + } + return obj.slider(opts); + } + if (typeof(opts) === 'string' && val !== undefined) { + return obj.slider(opts, val); + } + return obj.slider(opts); + }, + value: function (tp_inst, obj, unit, val) { + if (tp_inst._defaults.isRTL) { + if (val !== undefined) { + return obj.slider('value', val * -1); + } + return Math.abs(obj.slider('value')); + } + if (val !== undefined) { + return obj.slider('value', val); + } + return obj.slider('value'); + } + }, + // select methods + select: { + create: function (tp_inst, obj, unit, val, min, max, step) { + var sel = '<select class="ui-timepicker-select" data-unit="' + unit + '" data-min="' + min + '" data-max="' + max + '" data-step="' + step + '">', + format = tp_inst._defaults.pickerTimeFormat || tp_inst._defaults.timeFormat; + + for (var i = min; i <= max; i += step) { + sel += '<option value="' + i + '"' + (i === val ? ' selected' : '') + '>'; + if (unit === 'hour') { + sel += $.datepicker.formatTime($.trim(format.replace(/[^ht ]/ig, '')), {hour: i}, tp_inst._defaults); + } + else if (unit === 'millisec' || unit === 'microsec' || i >= 10) { sel += i; } + else {sel += '0' + i.toString(); } + sel += '</option>'; + } + sel += '</select>'; + + obj.children('select').remove(); + + $(sel).appendTo(obj).change(function (e) { + tp_inst._onTimeChange(); + tp_inst._onSelectHandler(); + }); + + return obj; + }, + options: function (tp_inst, obj, unit, opts, val) { + var o = {}, + $t = obj.children('select'); + if (typeof(opts) === 'string') { + if (val === undefined) { + return $t.data(opts); + } + o[opts] = val; + } + else { o = opts; } + return tp_inst.control.create(tp_inst, obj, $t.data('unit'), $t.val(), o.min || $t.data('min'), o.max || $t.data('max'), o.step || $t.data('step')); + }, + value: function (tp_inst, obj, unit, val) { + var $t = obj.children('select'); + if (val !== undefined) { + return $t.val(val); + } + return $t.val(); + } + } + } // end _controls + + }); + + $.fn.extend({ + /* + * shorthand just to use timepicker. + */ + timepicker: function (o) { + o = o || {}; + var tmp_args = Array.prototype.slice.call(arguments); + + if (typeof o === 'object') { + tmp_args[0] = $.extend(o, { + timeOnly: true + }); + } + + return $(this).each(function () { + $.fn.datetimepicker.apply($(this), tmp_args); + }); + }, + + /* + * extend timepicker to datepicker + */ + datetimepicker: function (o) { + o = o || {}; + var tmp_args = arguments; + + if (typeof(o) === 'string') { + if (o === 'getDate') { + return $.fn.datepicker.apply($(this[0]), tmp_args); + } else { + return this.each(function () { + var $t = $(this); + $t.datepicker.apply($t, tmp_args); + }); + } + } else { + return this.each(function () { + var $t = $(this); + $t.datepicker($.timepicker._newInst($t, o)._defaults); + }); + } + } + }); + + /* + * Public Utility to parse date and time + */ + $.datepicker.parseDateTime = function (dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings) { + var parseRes = parseDateTimeInternal(dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings); + if (parseRes.timeObj) { + var t = parseRes.timeObj; + parseRes.date.setHours(t.hour, t.minute, t.second, t.millisec); + parseRes.date.setMicroseconds(t.microsec); + } + + return parseRes.date; + }; + + /* + * Public utility to parse time + */ + $.datepicker.parseTime = function (timeFormat, timeString, options) { + var o = extendRemove(extendRemove({}, $.timepicker._defaults), options || {}), + iso8601 = (timeFormat.replace(/\'.*?\'/g, '').indexOf('Z') !== -1); + + // Strict parse requires the timeString to match the timeFormat exactly + var strictParse = function (f, s, o) { + + // pattern for standard and localized AM/PM markers + var getPatternAmpm = function (amNames, pmNames) { + var markers = []; + if (amNames) { + $.merge(markers, amNames); + } + if (pmNames) { + $.merge(markers, pmNames); + } + markers = $.map(markers, function (val) { + return val.replace(/[.*+?|()\[\]{}\\]/g, '\\$&'); + }); + return '(' + markers.join('|') + ')?'; + }; + + // figure out position of time elements.. cause js cant do named captures + var getFormatPositions = function (timeFormat) { + var finds = timeFormat.toLowerCase().match(/(h{1,2}|m{1,2}|s{1,2}|l{1}|c{1}|t{1,2}|z|'.*?')/g), + orders = { + h: -1, + m: -1, + s: -1, + l: -1, + c: -1, + t: -1, + z: -1 + }; + + if (finds) { + for (var i = 0; i < finds.length; i++) { + if (orders[finds[i].toString().charAt(0)] === -1) { + orders[finds[i].toString().charAt(0)] = i + 1; + } + } + } + return orders; + }; + + var regstr = '^' + f.toString() + .replace(/([hH]{1,2}|mm?|ss?|[tT]{1,2}|[zZ]|[lc]|'.*?')/g, function (match) { + var ml = match.length; + switch (match.charAt(0).toLowerCase()) { + case 'h': + return ml === 1 ? '(\\d?\\d)' : '(\\d{' + ml + '})'; + case 'm': + return ml === 1 ? '(\\d?\\d)' : '(\\d{' + ml + '})'; + case 's': + return ml === 1 ? '(\\d?\\d)' : '(\\d{' + ml + '})'; + case 'l': + return '(\\d?\\d?\\d)'; + case 'c': + return '(\\d?\\d?\\d)'; + case 'z': + return '(z|[-+]\\d\\d:?\\d\\d|\\S+)?'; + case 't': + return getPatternAmpm(o.amNames, o.pmNames); + default: // literal escaped in quotes + return '(' + match.replace(/\'/g, "").replace(/(\.|\$|\^|\\|\/|\(|\)|\[|\]|\?|\+|\*)/g, function (m) { return "\\" + m; }) + ')?'; + } + }) + .replace(/\s/g, '\\s?') + + o.timeSuffix + '$', + order = getFormatPositions(f), + ampm = '', + treg; + + treg = s.match(new RegExp(regstr, 'i')); + + var resTime = { + hour: 0, + minute: 0, + second: 0, + millisec: 0, + microsec: 0 + }; + + if (treg) { + if (order.t !== -1) { + if (treg[order.t] === undefined || treg[order.t].length === 0) { + ampm = ''; + resTime.ampm = ''; + } else { + ampm = $.inArray(treg[order.t].toUpperCase(), o.amNames) !== -1 ? 'AM' : 'PM'; + resTime.ampm = o[ampm === 'AM' ? 'amNames' : 'pmNames'][0]; + } + } + + if (order.h !== -1) { + if (ampm === 'AM' && treg[order.h] === '12') { + resTime.hour = 0; // 12am = 0 hour + } else { + if (ampm === 'PM' && treg[order.h] !== '12') { + resTime.hour = parseInt(treg[order.h], 10) + 12; // 12pm = 12 hour, any other pm = hour + 12 + } else { + resTime.hour = Number(treg[order.h]); + } + } + } + + if (order.m !== -1) { + resTime.minute = Number(treg[order.m]); + } + if (order.s !== -1) { + resTime.second = Number(treg[order.s]); + } + if (order.l !== -1) { + resTime.millisec = Number(treg[order.l]); + } + if (order.c !== -1) { + resTime.microsec = Number(treg[order.c]); + } + if (order.z !== -1 && treg[order.z] !== undefined) { + resTime.timezone = $.timepicker.timezoneOffsetNumber(treg[order.z]); + } + + + return resTime; + } + return false; + };// end strictParse + + // First try JS Date, if that fails, use strictParse + var looseParse = function (f, s, o) { + try { + var d = new Date('2012-01-01 ' + s); + if (isNaN(d.getTime())) { + d = new Date('2012-01-01T' + s); + if (isNaN(d.getTime())) { + d = new Date('01/01/2012 ' + s); + if (isNaN(d.getTime())) { + throw "Unable to parse time with native Date: " + s; + } + } + } + + return { + hour: d.getHours(), + minute: d.getMinutes(), + second: d.getSeconds(), + millisec: d.getMilliseconds(), + microsec: d.getMicroseconds(), + timezone: d.getTimezoneOffset() * -1 + }; + } + catch (err) { + try { + return strictParse(f, s, o); + } + catch (err2) { + $.timepicker.log("Unable to parse \ntimeString: " + s + "\ntimeFormat: " + f); + } + } + return false; + }; // end looseParse + + if (typeof o.parse === "function") { + return o.parse(timeFormat, timeString, o); + } + if (o.parse === 'loose') { + return looseParse(timeFormat, timeString, o); + } + return strictParse(timeFormat, timeString, o); + }; + + /** + * Public utility to format the time + * @param {string} format format of the time + * @param {Object} time Object not a Date for timezones + * @param {Object} [options] essentially the regional[].. amNames, pmNames, ampm + * @returns {string} the formatted time + */ + $.datepicker.formatTime = function (format, time, options) { + options = options || {}; + options = $.extend({}, $.timepicker._defaults, options); + time = $.extend({ + hour: 0, + minute: 0, + second: 0, + millisec: 0, + microsec: 0, + timezone: null + }, time); + + var tmptime = format, + ampmName = options.amNames[0], + hour = parseInt(time.hour, 10); + + if (hour > 11) { + ampmName = options.pmNames[0]; + } + + tmptime = tmptime.replace(/(?:HH?|hh?|mm?|ss?|[tT]{1,2}|[zZ]|[lc]|'.*?')/g, function (match) { + switch (match) { + case 'HH': + return ('0' + hour).slice(-2); + case 'H': + return hour; + case 'hh': + return ('0' + convert24to12(hour)).slice(-2); + case 'h': + return convert24to12(hour); + case 'mm': + return ('0' + time.minute).slice(-2); + case 'm': + return time.minute; + case 'ss': + return ('0' + time.second).slice(-2); + case 's': + return time.second; + case 'l': + return ('00' + time.millisec).slice(-3); + case 'c': + return ('00' + time.microsec).slice(-3); + case 'z': + return $.timepicker.timezoneOffsetString(time.timezone === null ? options.timezone : time.timezone, false); + case 'Z': + return $.timepicker.timezoneOffsetString(time.timezone === null ? options.timezone : time.timezone, true); + case 'T': + return ampmName.charAt(0).toUpperCase(); + case 'TT': + return ampmName.toUpperCase(); + case 't': + return ampmName.charAt(0).toLowerCase(); + case 'tt': + return ampmName.toLowerCase(); + default: + return match.replace(/'/g, ""); + } + }); + + return tmptime; + }; + + /* + * the bad hack :/ override datepicker so it doesn't close on select + // inspired: http://stackoverflow.com/questions/1252512/jquery-datepicker-prevent-closing-picker-when-clicking-a-date/1762378#1762378 + */ + $.datepicker._base_selectDate = $.datepicker._selectDate; + $.datepicker._selectDate = function (id, dateStr) { + var inst = this._getInst($(id)[0]), + tp_inst = this._get(inst, 'timepicker'); + + if (tp_inst) { + tp_inst._limitMinMaxDateTime(inst, true); + inst.inline = inst.stay_open = true; + //This way the onSelect handler called from calendarpicker get the full dateTime + this._base_selectDate(id, dateStr); + inst.inline = inst.stay_open = false; + this._notifyChange(inst); + this._updateDatepicker(inst); + } else { + this._base_selectDate(id, dateStr); + } + }; + + /* + * second bad hack :/ override datepicker so it triggers an event when changing the input field + * and does not redraw the datepicker on every selectDate event + */ + $.datepicker._base_updateDatepicker = $.datepicker._updateDatepicker; + $.datepicker._updateDatepicker = function (inst) { + + // don't popup the datepicker if there is another instance already opened + var input = inst.input[0]; + if ($.datepicker._curInst && $.datepicker._curInst !== inst && $.datepicker._datepickerShowing && $.datepicker._lastInput !== input) { + return; + } + + if (typeof(inst.stay_open) !== 'boolean' || inst.stay_open === false) { + + this._base_updateDatepicker(inst); + + // Reload the time control when changing something in the input text field. + var tp_inst = this._get(inst, 'timepicker'); + if (tp_inst) { + tp_inst._addTimePicker(inst); + } + } + }; + + /* + * third bad hack :/ override datepicker so it allows spaces and colon in the input field + */ + $.datepicker._base_doKeyPress = $.datepicker._doKeyPress; + $.datepicker._doKeyPress = function (event) { + var inst = $.datepicker._getInst(event.target), + tp_inst = $.datepicker._get(inst, 'timepicker'); + + if (tp_inst) { + if ($.datepicker._get(inst, 'constrainInput')) { + var ampm = tp_inst.support.ampm, + tz = tp_inst._defaults.showTimezone !== null ? tp_inst._defaults.showTimezone : tp_inst.support.timezone, + dateChars = $.datepicker._possibleChars($.datepicker._get(inst, 'dateFormat')), + datetimeChars = tp_inst._defaults.timeFormat.toString() + .replace(/[hms]/g, '') + .replace(/TT/g, ampm ? 'APM' : '') + .replace(/Tt/g, ampm ? 'AaPpMm' : '') + .replace(/tT/g, ampm ? 'AaPpMm' : '') + .replace(/T/g, ampm ? 'AP' : '') + .replace(/tt/g, ampm ? 'apm' : '') + .replace(/t/g, ampm ? 'ap' : '') + + " " + tp_inst._defaults.separator + + tp_inst._defaults.timeSuffix + + (tz ? tp_inst._defaults.timezoneList.join('') : '') + + (tp_inst._defaults.amNames.join('')) + (tp_inst._defaults.pmNames.join('')) + + dateChars, + chr = String.fromCharCode(event.charCode === undefined ? event.keyCode : event.charCode); + return event.ctrlKey || (chr < ' ' || !dateChars || datetimeChars.indexOf(chr) > -1); + } + } + + return $.datepicker._base_doKeyPress(event); + }; + + /* + * Fourth bad hack :/ override _updateAlternate function used in inline mode to init altField + * Update any alternate field to synchronise with the main field. + */ + $.datepicker._base_updateAlternate = $.datepicker._updateAlternate; + $.datepicker._updateAlternate = function (inst) { + var tp_inst = this._get(inst, 'timepicker'); + if (tp_inst) { + var altField = tp_inst._defaults.altField; + if (altField) { // update alternate field too + var altFormat = tp_inst._defaults.altFormat || tp_inst._defaults.dateFormat, + date = this._getDate(inst), + formatCfg = $.datepicker._getFormatConfig(inst), + altFormattedDateTime = '', + altSeparator = tp_inst._defaults.altSeparator ? tp_inst._defaults.altSeparator : tp_inst._defaults.separator, + altTimeSuffix = tp_inst._defaults.altTimeSuffix ? tp_inst._defaults.altTimeSuffix : tp_inst._defaults.timeSuffix, + altTimeFormat = tp_inst._defaults.altTimeFormat !== null ? tp_inst._defaults.altTimeFormat : tp_inst._defaults.timeFormat; + + altFormattedDateTime += $.datepicker.formatTime(altTimeFormat, tp_inst, tp_inst._defaults) + altTimeSuffix; + if (!tp_inst._defaults.timeOnly && !tp_inst._defaults.altFieldTimeOnly && date !== null) { + if (tp_inst._defaults.altFormat) { + altFormattedDateTime = $.datepicker.formatDate(tp_inst._defaults.altFormat, date, formatCfg) + altSeparator + altFormattedDateTime; + } + else { + altFormattedDateTime = tp_inst.formattedDate + altSeparator + altFormattedDateTime; + } + } + $(altField).val(altFormattedDateTime); + } + } + else { + $.datepicker._base_updateAlternate(inst); + } + }; + + /* + * Override key up event to sync manual input changes. + */ + $.datepicker._base_doKeyUp = $.datepicker._doKeyUp; + $.datepicker._doKeyUp = function (event) { + var inst = $.datepicker._getInst(event.target), + tp_inst = $.datepicker._get(inst, 'timepicker'); + + if (tp_inst) { + if (tp_inst._defaults.timeOnly && (inst.input.val() !== inst.lastVal)) { + try { + $.datepicker._updateDatepicker(inst); + } catch (err) { + $.timepicker.log(err); + } + } + } + + return $.datepicker._base_doKeyUp(event); + }; + + /* + * override "Today" button to also grab the time. + */ + $.datepicker._base_gotoToday = $.datepicker._gotoToday; + $.datepicker._gotoToday = function (id) { + var inst = this._getInst($(id)[0]), + $dp = inst.dpDiv; + this._base_gotoToday(id); + var tp_inst = this._get(inst, 'timepicker'); + selectLocalTimezone(tp_inst); + var now = new Date(); + this._setTime(inst, now); + $('.ui-datepicker-today', $dp).click(); + }; + + /* + * Disable & enable the Time in the datetimepicker + */ + $.datepicker._disableTimepickerDatepicker = function (target) { + var inst = this._getInst(target); + if (!inst) { + return; + } + + var tp_inst = this._get(inst, 'timepicker'); + $(target).datepicker('getDate'); // Init selected[Year|Month|Day] + if (tp_inst) { + inst.settings.showTimepicker = false; + tp_inst._defaults.showTimepicker = false; + tp_inst._updateDateTime(inst); + } + }; + + $.datepicker._enableTimepickerDatepicker = function (target) { + var inst = this._getInst(target); + if (!inst) { + return; + } + + var tp_inst = this._get(inst, 'timepicker'); + $(target).datepicker('getDate'); // Init selected[Year|Month|Day] + if (tp_inst) { + inst.settings.showTimepicker = true; + tp_inst._defaults.showTimepicker = true; + tp_inst._addTimePicker(inst); // Could be disabled on page load + tp_inst._updateDateTime(inst); + } + }; + + /* + * Create our own set time function + */ + $.datepicker._setTime = function (inst, date) { + var tp_inst = this._get(inst, 'timepicker'); + if (tp_inst) { + var defaults = tp_inst._defaults; + + // calling _setTime with no date sets time to defaults + tp_inst.hour = date ? date.getHours() : defaults.hour; + tp_inst.minute = date ? date.getMinutes() : defaults.minute; + tp_inst.second = date ? date.getSeconds() : defaults.second; + tp_inst.millisec = date ? date.getMilliseconds() : defaults.millisec; + tp_inst.microsec = date ? date.getMicroseconds() : defaults.microsec; + + //check if within min/max times.. + tp_inst._limitMinMaxDateTime(inst, true); + + tp_inst._onTimeChange(); + tp_inst._updateDateTime(inst); + } + }; + + /* + * Create new public method to set only time, callable as $().datepicker('setTime', date) + */ + $.datepicker._setTimeDatepicker = function (target, date, withDate) { + var inst = this._getInst(target); + if (!inst) { + return; + } + + var tp_inst = this._get(inst, 'timepicker'); + + if (tp_inst) { + this._setDateFromField(inst); + var tp_date; + if (date) { + if (typeof date === "string") { + tp_inst._parseTime(date, withDate); + tp_date = new Date(); + tp_date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec); + tp_date.setMicroseconds(tp_inst.microsec); + } else { + tp_date = new Date(date.getTime()); + tp_date.setMicroseconds(date.getMicroseconds()); + } + if (tp_date.toString() === 'Invalid Date') { + tp_date = undefined; + } + this._setTime(inst, tp_date); + } + } + + }; + + /* + * override setDate() to allow setting time too within Date object + */ + $.datepicker._base_setDateDatepicker = $.datepicker._setDateDatepicker; + $.datepicker._setDateDatepicker = function (target, date) { + var inst = this._getInst(target); + if (!inst) { + return; + } + + if (typeof(date) === 'string') { + date = new Date(date); + if (!date.getTime()) { + $.timepicker.log("Error creating Date object from string."); + } + } + + var tp_inst = this._get(inst, 'timepicker'); + var tp_date; + if (date instanceof Date) { + tp_date = new Date(date.getTime()); + tp_date.setMicroseconds(date.getMicroseconds()); + } else { + tp_date = date; + } + + // This is important if you are using the timezone option, javascript's Date + // object will only return the timezone offset for the current locale, so we + // adjust it accordingly. If not using timezone option this won't matter.. + // If a timezone is different in tp, keep the timezone as is + if (tp_inst) { + // look out for DST if tz wasn't specified + if (!tp_inst.support.timezone && tp_inst._defaults.timezone === null) { + tp_inst.timezone = tp_date.getTimezoneOffset() * -1; + } + date = $.timepicker.timezoneAdjust(date, tp_inst.timezone); + tp_date = $.timepicker.timezoneAdjust(tp_date, tp_inst.timezone); + } + + this._updateDatepicker(inst); + this._base_setDateDatepicker.apply(this, arguments); + this._setTimeDatepicker(target, tp_date, true); + }; + + /* + * override getDate() to allow getting time too within Date object + */ + $.datepicker._base_getDateDatepicker = $.datepicker._getDateDatepicker; + $.datepicker._getDateDatepicker = function (target, noDefault) { + var inst = this._getInst(target); + if (!inst) { + return; + } + + var tp_inst = this._get(inst, 'timepicker'); + + if (tp_inst) { + // if it hasn't yet been defined, grab from field + if (inst.lastVal === undefined) { + this._setDateFromField(inst, noDefault); + } + + var date = this._getDate(inst); + if (date && tp_inst._parseTime($(target).val(), tp_inst.timeOnly)) { + date.setHours(tp_inst.hour, tp_inst.minute, tp_inst.second, tp_inst.millisec); + date.setMicroseconds(tp_inst.microsec); + + // This is important if you are using the timezone option, javascript's Date + // object will only return the timezone offset for the current locale, so we + // adjust it accordingly. If not using timezone option this won't matter.. + if (tp_inst.timezone != null) { + // look out for DST if tz wasn't specified + if (!tp_inst.support.timezone && tp_inst._defaults.timezone === null) { + tp_inst.timezone = date.getTimezoneOffset() * -1; + } + date = $.timepicker.timezoneAdjust(date, tp_inst.timezone); + } + } + return date; + } + return this._base_getDateDatepicker(target, noDefault); + }; + + /* + * override parseDate() because UI 1.8.14 throws an error about "Extra characters" + * An option in datapicker to ignore extra format characters would be nicer. + */ + $.datepicker._base_parseDate = $.datepicker.parseDate; + $.datepicker.parseDate = function (format, value, settings) { + var date; + try { + date = this._base_parseDate(format, value, settings); + } catch (err) { + // Hack! The error message ends with a colon, a space, and + // the "extra" characters. We rely on that instead of + // attempting to perfectly reproduce the parsing algorithm. + if (err.indexOf(":") >= 0) { + date = this._base_parseDate(format, value.substring(0, value.length - (err.length - err.indexOf(':') - 2)), settings); + $.timepicker.log("Error parsing the date string: " + err + "\ndate string = " + value + "\ndate format = " + format); + } else { + throw err; + } + } + return date; + }; + + /* + * override formatDate to set date with time to the input + */ + $.datepicker._base_formatDate = $.datepicker._formatDate; + $.datepicker._formatDate = function (inst, day, month, year) { + var tp_inst = this._get(inst, 'timepicker'); + if (tp_inst) { + tp_inst._updateDateTime(inst); + return tp_inst.$input.val(); + } + return this._base_formatDate(inst); + }; + + /* + * override options setter to add time to maxDate(Time) and minDate(Time). MaxDate + */ + $.datepicker._base_optionDatepicker = $.datepicker._optionDatepicker; + $.datepicker._optionDatepicker = function (target, name, value) { + var inst = this._getInst(target), + name_clone; + if (!inst) { + return null; + } + + var tp_inst = this._get(inst, 'timepicker'); + if (tp_inst) { + var min = null, + max = null, + onselect = null, + overrides = tp_inst._defaults.evnts, + fns = {}, + prop; + if (typeof name === 'string') { // if min/max was set with the string + if (name === 'minDate' || name === 'minDateTime') { + min = value; + } else if (name === 'maxDate' || name === 'maxDateTime') { + max = value; + } else if (name === 'onSelect') { + onselect = value; + } else if (overrides.hasOwnProperty(name)) { + if (typeof (value) === 'undefined') { + return overrides[name]; + } + fns[name] = value; + name_clone = {}; //empty results in exiting function after overrides updated + } + } else if (typeof name === 'object') { //if min/max was set with the JSON + if (name.minDate) { + min = name.minDate; + } else if (name.minDateTime) { + min = name.minDateTime; + } else if (name.maxDate) { + max = name.maxDate; + } else if (name.maxDateTime) { + max = name.maxDateTime; + } + for (prop in overrides) { + if (overrides.hasOwnProperty(prop) && name[prop]) { + fns[prop] = name[prop]; + } + } + } + for (prop in fns) { + if (fns.hasOwnProperty(prop)) { + overrides[prop] = fns[prop]; + if (!name_clone) { name_clone = $.extend({}, name); } + delete name_clone[prop]; + } + } + if (name_clone && isEmptyObject(name_clone)) { return; } + if (min) { //if min was set + if (min === 0) { + min = new Date(); + } else { + min = new Date(min); + } + tp_inst._defaults.minDate = min; + tp_inst._defaults.minDateTime = min; + } else if (max) { //if max was set + if (max === 0) { + max = new Date(); + } else { + max = new Date(max); + } + tp_inst._defaults.maxDate = max; + tp_inst._defaults.maxDateTime = max; + } else if (onselect) { + tp_inst._defaults.onSelect = onselect; + } + } + if (value === undefined) { + return this._base_optionDatepicker.call($.datepicker, target, name); + } + return this._base_optionDatepicker.call($.datepicker, target, name_clone || name, value); + }; + + /* + * jQuery isEmptyObject does not check hasOwnProperty - if someone has added to the object prototype, + * it will return false for all objects + */ + var isEmptyObject = function (obj) { + var prop; + for (prop in obj) { + if (obj.hasOwnProperty(prop)) { + return false; + } + } + return true; + }; + + /* + * jQuery extend now ignores nulls! + */ + var extendRemove = function (target, props) { + $.extend(target, props); + for (var name in props) { + if (props[name] === null || props[name] === undefined) { + target[name] = props[name]; + } + } + return target; + }; + + /* + * Determine by the time format which units are supported + * Returns an object of booleans for each unit + */ + var detectSupport = function (timeFormat) { + var tf = timeFormat.replace(/'.*?'/g, '').toLowerCase(), // removes literals + isIn = function (f, t) { // does the format contain the token? + return f.indexOf(t) !== -1 ? true : false; + }; + return { + hour: isIn(tf, 'h'), + minute: isIn(tf, 'm'), + second: isIn(tf, 's'), + millisec: isIn(tf, 'l'), + microsec: isIn(tf, 'c'), + timezone: isIn(tf, 'z'), + ampm: isIn(tf, 't') && isIn(timeFormat, 'h'), + iso8601: isIn(timeFormat, 'Z') + }; + }; + + /* + * Converts 24 hour format into 12 hour + * Returns 12 hour without leading 0 + */ + var convert24to12 = function (hour) { + hour %= 12; + + if (hour === 0) { + hour = 12; + } + + return String(hour); + }; + + var computeEffectiveSetting = function (settings, property) { + return settings && settings[property] ? settings[property] : $.timepicker._defaults[property]; + }; + + /* + * Splits datetime string into date and time substrings. + * Throws exception when date can't be parsed + * Returns {dateString: dateString, timeString: timeString} + */ + var splitDateTime = function (dateTimeString, timeSettings) { + // The idea is to get the number separator occurrences in datetime and the time format requested (since time has + // fewer unknowns, mostly numbers and am/pm). We will use the time pattern to split. + var separator = computeEffectiveSetting(timeSettings, 'separator'), + format = computeEffectiveSetting(timeSettings, 'timeFormat'), + timeParts = format.split(separator), // how many occurrences of separator may be in our format? + timePartsLen = timeParts.length, + allParts = dateTimeString.split(separator), + allPartsLen = allParts.length; + + if (allPartsLen > 1) { + return { + dateString: allParts.splice(0, allPartsLen - timePartsLen).join(separator), + timeString: allParts.splice(0, timePartsLen).join(separator) + }; + } + + return { + dateString: dateTimeString, + timeString: '' + }; + }; + + /* + * Internal function to parse datetime interval + * Returns: {date: Date, timeObj: Object}, where + * date - parsed date without time (type Date) + * timeObj = {hour: , minute: , second: , millisec: , microsec: } - parsed time. Optional + */ + var parseDateTimeInternal = function (dateFormat, timeFormat, dateTimeString, dateSettings, timeSettings) { + var date, + parts, + parsedTime; + + parts = splitDateTime(dateTimeString, timeSettings); + date = $.datepicker._base_parseDate(dateFormat, parts.dateString, dateSettings); + + if (parts.timeString === '') { + return { + date: date + }; + } + + parsedTime = $.datepicker.parseTime(timeFormat, parts.timeString, timeSettings); + + if (!parsedTime) { + throw 'Wrong time format'; + } + + return { + date: date, + timeObj: parsedTime + }; + }; + + /* + * Internal function to set timezone_select to the local timezone + */ + var selectLocalTimezone = function (tp_inst, date) { + if (tp_inst && tp_inst.timezone_select) { + var now = date || new Date(); + tp_inst.timezone_select.val(-now.getTimezoneOffset()); + } + }; + + /* + * Create a Singleton Instance + */ + $.timepicker = new Timepicker(); + + /** + * Get the timezone offset as string from a date object (eg '+0530' for UTC+5.5) + * @param {number} tzMinutes if not a number, less than -720 (-1200), or greater than 840 (+1400) this value is returned + * @param {boolean} iso8601 if true formats in accordance to iso8601 "+12:45" + * @return {string} + */ + $.timepicker.timezoneOffsetString = function (tzMinutes, iso8601) { + if (isNaN(tzMinutes) || tzMinutes > 840 || tzMinutes < -720) { + return tzMinutes; + } + + var off = tzMinutes, + minutes = off % 60, + hours = (off - minutes) / 60, + iso = iso8601 ? ':' : '', + tz = (off >= 0 ? '+' : '-') + ('0' + Math.abs(hours)).slice(-2) + iso + ('0' + Math.abs(minutes)).slice(-2); + + if (tz === '+00:00') { + return 'Z'; + } + return tz; + }; + + /** + * Get the number in minutes that represents a timezone string + * @param {string} tzString formatted like "+0500", "-1245", "Z" + * @return {number} the offset minutes or the original string if it doesn't match expectations + */ + $.timepicker.timezoneOffsetNumber = function (tzString) { + var normalized = tzString.toString().replace(':', ''); // excuse any iso8601, end up with "+1245" + + if (normalized.toUpperCase() === 'Z') { // if iso8601 with Z, its 0 minute offset + return 0; + } + + if (!/^(\-|\+)\d{4}$/.test(normalized)) { // possibly a user defined tz, so just give it back + return tzString; + } + + return ((normalized.substr(0, 1) === '-' ? -1 : 1) * // plus or minus + ((parseInt(normalized.substr(1, 2), 10) * 60) + // hours (converted to minutes) + parseInt(normalized.substr(3, 2), 10))); // minutes + }; + + /** + * No way to set timezone in js Date, so we must adjust the minutes to compensate. (think setDate, getDate) + * @param {Date} date + * @param {string} toTimezone formatted like "+0500", "-1245" + * @return {Date} + */ + $.timepicker.timezoneAdjust = function (date, toTimezone) { + var toTz = $.timepicker.timezoneOffsetNumber(toTimezone); + if (!isNaN(toTz)) { + date.setMinutes(date.getMinutes() + -date.getTimezoneOffset() - toTz); + } + return date; + }; + + /** + * Calls `timepicker()` on the `startTime` and `endTime` elements, and configures them to + * enforce date range limits. + * n.b. The input value must be correctly formatted (reformatting is not supported) + * @param {Element} startTime + * @param {Element} endTime + * @param {Object} options Options for the timepicker() call + * @return {jQuery} + */ + $.timepicker.timeRange = function (startTime, endTime, options) { + return $.timepicker.handleRange('timepicker', startTime, endTime, options); + }; + + /** + * Calls `datetimepicker` on the `startTime` and `endTime` elements, and configures them to + * enforce date range limits. + * @param {Element} startTime + * @param {Element} endTime + * @param {Object} options Options for the `timepicker()` call. Also supports `reformat`, + * a boolean value that can be used to reformat the input values to the `dateFormat`. + * @param {string} method Can be used to specify the type of picker to be added + * @return {jQuery} + */ + $.timepicker.datetimeRange = function (startTime, endTime, options) { + $.timepicker.handleRange('datetimepicker', startTime, endTime, options); + }; + + /** + * Calls `datepicker` on the `startTime` and `endTime` elements, and configures them to + * enforce date range limits. + * @param {Element} startTime + * @param {Element} endTime + * @param {Object} options Options for the `timepicker()` call. Also supports `reformat`, + * a boolean value that can be used to reformat the input values to the `dateFormat`. + * @return {jQuery} + */ + $.timepicker.dateRange = function (startTime, endTime, options) { + $.timepicker.handleRange('datepicker', startTime, endTime, options); + }; + + /** + * Calls `method` on the `startTime` and `endTime` elements, and configures them to + * enforce date range limits. + * @param {string} method Can be used to specify the type of picker to be added + * @param {Element} startTime + * @param {Element} endTime + * @param {Object} options Options for the `timepicker()` call. Also supports `reformat`, + * a boolean value that can be used to reformat the input values to the `dateFormat`. + * @return {jQuery} + */ + $.timepicker.handleRange = function (method, startTime, endTime, options) { + options = $.extend({}, { + minInterval: 0, // min allowed interval in milliseconds + maxInterval: 0, // max allowed interval in milliseconds + start: {}, // options for start picker + end: {} // options for end picker + }, options); + + function checkDates(changed, other) { + var startdt = startTime[method]('getDate'), + enddt = endTime[method]('getDate'), + changeddt = changed[method]('getDate'); + + if (startdt !== null) { + var minDate = new Date(startdt.getTime()), + maxDate = new Date(startdt.getTime()); + + minDate.setMilliseconds(minDate.getMilliseconds() + options.minInterval); + maxDate.setMilliseconds(maxDate.getMilliseconds() + options.maxInterval); + + if (options.minInterval > 0 && minDate > enddt) { // minInterval check + endTime[method]('setDate', minDate); + } + else if (options.maxInterval > 0 && maxDate < enddt) { // max interval check + endTime[method]('setDate', maxDate); + } + else if (startdt > enddt) { + other[method]('setDate', changeddt); + } + } + } + + function selected(changed, other, option) { + if (!changed.val()) { + return; + } + var date = changed[method].call(changed, 'getDate'); + if (date !== null && options.minInterval > 0) { + if (option === 'minDate') { + date.setMilliseconds(date.getMilliseconds() + options.minInterval); + } + if (option === 'maxDate') { + date.setMilliseconds(date.getMilliseconds() - options.minInterval); + } + } + if (date.getTime) { + other[method].call(other, 'option', option, date); + } + } + + $.fn[method].call(startTime, $.extend({ + onClose: function (dateText, inst) { + checkDates($(this), endTime); + }, + onSelect: function (selectedDateTime) { + selected($(this), endTime, 'minDate'); + } + }, options, options.start)); + $.fn[method].call(endTime, $.extend({ + onClose: function (dateText, inst) { + checkDates($(this), startTime); + }, + onSelect: function (selectedDateTime) { + selected($(this), startTime, 'maxDate'); + } + }, options, options.end)); + + checkDates(startTime, endTime); + selected(startTime, endTime, 'minDate'); + selected(endTime, startTime, 'maxDate'); + return $([startTime.get(0), endTime.get(0)]); + }; + + /** + * Log error or data to the console during error or debugging + * @param {Object} err pass any type object to log to the console during error or debugging + * @return {void} + */ + $.timepicker.log = function (err) { + if (window.console) { + window.console.log(err); + } + }; + + /* + * Add util object to allow access to private methods for testability. + */ + $.timepicker._util = { + _extendRemove: extendRemove, + _isEmptyObject: isEmptyObject, + _convert24to12: convert24to12, + _detectSupport: detectSupport, + _selectLocalTimezone: selectLocalTimezone, + _computeEffectiveSetting: computeEffectiveSetting, + _splitDateTime: splitDateTime, + _parseDateTimeInternal: parseDateTimeInternal + }; + + /* + * Microsecond support + */ + if (!Date.prototype.getMicroseconds) { + Date.prototype.microseconds = 0; + Date.prototype.getMicroseconds = function () { return this.microseconds; }; + Date.prototype.setMicroseconds = function (m) { + this.setMilliseconds(this.getMilliseconds() + Math.floor(m / 1000)); + this.microseconds = m % 1000; + return this; + }; + } + + /* + * Keep up with the version + */ + $.timepicker.version = "1.4"; + +})(jQuery); diff --git a/static/js/lib/qunit-1.12.0.js b/static/js/lib/qunit-1.12.0.js new file mode 100644 index 000000000..84c73907d --- /dev/null +++ b/static/js/lib/qunit-1.12.0.js @@ -0,0 +1,2212 @@ +/** + * QUnit v1.12.0 - A JavaScript Unit Testing Framework + * + * http://qunitjs.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * https://jquery.org/license/ + */ + +(function( window ) { + +var QUnit, + assert, + config, + onErrorFnPrev, + testId = 0, + fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""), + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + // Keep a local reference to Date (GH-283) + Date = window.Date, + setTimeout = window.setTimeout, + defined = { + setTimeout: typeof window.setTimeout !== "undefined", + sessionStorage: (function() { + var x = "qunit-test-string"; + try { + sessionStorage.setItem( x, x ); + sessionStorage.removeItem( x ); + return true; + } catch( e ) { + return false; + } + }()) + }, + /** + * Provides a normalized error string, correcting an issue + * with IE 7 (and prior) where Error.prototype.toString is + * not properly implemented + * + * Based on http://es5.github.com/#x15.11.4.4 + * + * @param {String|Error} error + * @return {String} error message + */ + errorString = function( error ) { + var name, message, + errorString = error.toString(); + if ( errorString.substring( 0, 7 ) === "[object" ) { + name = error.name ? error.name.toString() : "Error"; + message = error.message ? error.message.toString() : ""; + if ( name && message ) { + return name + ": " + message; + } else if ( name ) { + return name; + } else if ( message ) { + return message; + } else { + return "Error"; + } + } else { + return errorString; + } + }, + /** + * Makes a clone of an object using only Array or Object as base, + * and copies over the own enumerable properties. + * + * @param {Object} obj + * @return {Object} New object with only the own properties (recursively). + */ + objectValues = function( obj ) { + // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392. + /*jshint newcap: false */ + var key, val, + vals = QUnit.is( "array", obj ) ? [] : {}; + for ( key in obj ) { + if ( hasOwn.call( obj, key ) ) { + val = obj[key]; + vals[key] = val === Object(val) ? objectValues(val) : val; + } + } + return vals; + }; + +function Test( settings ) { + extend( this, settings ); + this.assertions = []; + this.testNumber = ++Test.count; +} + +Test.count = 0; + +Test.prototype = { + init: function() { + var a, b, li, + tests = id( "qunit-tests" ); + + if ( tests ) { + b = document.createElement( "strong" ); + b.innerHTML = this.nameHtml; + + // `a` initialized at top of scope + a = document.createElement( "a" ); + a.innerHTML = "Rerun"; + a.href = QUnit.url({ testNumber: this.testNumber }); + + li = document.createElement( "li" ); + li.appendChild( b ); + li.appendChild( a ); + li.className = "running"; + li.id = this.id = "qunit-test-output" + testId++; + + tests.appendChild( li ); + } + }, + setup: function() { + if ( + // Emit moduleStart when we're switching from one module to another + this.module !== config.previousModule || + // They could be equal (both undefined) but if the previousModule property doesn't + // yet exist it means this is the first test in a suite that isn't wrapped in a + // module, in which case we'll just emit a moduleStart event for 'undefined'. + // Without this, reporters can get testStart before moduleStart which is a problem. + !hasOwn.call( config, "previousModule" ) + ) { + if ( hasOwn.call( config, "previousModule" ) ) { + runLoggingCallbacks( "moduleDone", QUnit, { + name: config.previousModule, + failed: config.moduleStats.bad, + passed: config.moduleStats.all - config.moduleStats.bad, + total: config.moduleStats.all + }); + } + config.previousModule = this.module; + config.moduleStats = { all: 0, bad: 0 }; + runLoggingCallbacks( "moduleStart", QUnit, { + name: this.module + }); + } + + config.current = this; + + this.testEnvironment = extend({ + setup: function() {}, + teardown: function() {} + }, this.moduleTestEnvironment ); + + this.started = +new Date(); + runLoggingCallbacks( "testStart", QUnit, { + name: this.testName, + module: this.module + }); + + /*jshint camelcase:false */ + + + /** + * Expose the current test environment. + * + * @deprecated since 1.12.0: Use QUnit.config.current.testEnvironment instead. + */ + QUnit.current_testEnvironment = this.testEnvironment; + + /*jshint camelcase:true */ + + if ( !config.pollution ) { + saveGlobal(); + } + if ( config.notrycatch ) { + this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); + return; + } + try { + this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); + } catch( e ) { + QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); + } + }, + run: function() { + config.current = this; + + var running = id( "qunit-testresult" ); + + if ( running ) { + running.innerHTML = "Running: <br/>" + this.nameHtml; + } + + if ( this.async ) { + QUnit.stop(); + } + + this.callbackStarted = +new Date(); + + if ( config.notrycatch ) { + this.callback.call( this.testEnvironment, QUnit.assert ); + this.callbackRuntime = +new Date() - this.callbackStarted; + return; + } + + try { + this.callback.call( this.testEnvironment, QUnit.assert ); + this.callbackRuntime = +new Date() - this.callbackStarted; + } catch( e ) { + this.callbackRuntime = +new Date() - this.callbackStarted; + + QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); + // else next test will carry the responsibility + saveGlobal(); + + // Restart the tests if they're blocking + if ( config.blocking ) { + QUnit.start(); + } + } + }, + teardown: function() { + config.current = this; + if ( config.notrycatch ) { + if ( typeof this.callbackRuntime === "undefined" ) { + this.callbackRuntime = +new Date() - this.callbackStarted; + } + this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); + return; + } else { + try { + this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); + } catch( e ) { + QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); + } + } + checkPollution(); + }, + finish: function() { + config.current = this; + if ( config.requireExpects && this.expected === null ) { + QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); + } else if ( this.expected !== null && this.expected !== this.assertions.length ) { + QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); + } else if ( this.expected === null && !this.assertions.length ) { + QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); + } + + var i, assertion, a, b, time, li, ol, + test = this, + good = 0, + bad = 0, + tests = id( "qunit-tests" ); + + this.runtime = +new Date() - this.started; + config.stats.all += this.assertions.length; + config.moduleStats.all += this.assertions.length; + + if ( tests ) { + ol = document.createElement( "ol" ); + ol.className = "qunit-assert-list"; + + for ( i = 0; i < this.assertions.length; i++ ) { + assertion = this.assertions[i]; + + li = document.createElement( "li" ); + li.className = assertion.result ? "pass" : "fail"; + li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" ); + ol.appendChild( li ); + + if ( assertion.result ) { + good++; + } else { + bad++; + config.stats.bad++; + config.moduleStats.bad++; + } + } + + // store result when possible + if ( QUnit.config.reorder && defined.sessionStorage ) { + if ( bad ) { + sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad ); + } else { + sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName ); + } + } + + if ( bad === 0 ) { + addClass( ol, "qunit-collapsed" ); + } + + // `b` initialized at top of scope + b = document.createElement( "strong" ); + b.innerHTML = this.nameHtml + " <b class='counts'>(<b class='failed'>" + bad + "</b>, <b class='passed'>" + good + "</b>, " + this.assertions.length + ")</b>"; + + addEvent(b, "click", function() { + var next = b.parentNode.lastChild, + collapsed = hasClass( next, "qunit-collapsed" ); + ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" ); + }); + + addEvent(b, "dblclick", function( e ) { + var target = e && e.target ? e.target : window.event.srcElement; + if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) { + target = target.parentNode; + } + if ( window.location && target.nodeName.toLowerCase() === "strong" ) { + window.location = QUnit.url({ testNumber: test.testNumber }); + } + }); + + // `time` initialized at top of scope + time = document.createElement( "span" ); + time.className = "runtime"; + time.innerHTML = this.runtime + " ms"; + + // `li` initialized at top of scope + li = id( this.id ); + li.className = bad ? "fail" : "pass"; + li.removeChild( li.firstChild ); + a = li.firstChild; + li.appendChild( b ); + li.appendChild( a ); + li.appendChild( time ); + li.appendChild( ol ); + + } else { + for ( i = 0; i < this.assertions.length; i++ ) { + if ( !this.assertions[i].result ) { + bad++; + config.stats.bad++; + config.moduleStats.bad++; + } + } + } + + runLoggingCallbacks( "testDone", QUnit, { + name: this.testName, + module: this.module, + failed: bad, + passed: this.assertions.length - bad, + total: this.assertions.length, + duration: this.runtime + }); + + QUnit.reset(); + + config.current = undefined; + }, + + queue: function() { + var bad, + test = this; + + synchronize(function() { + test.init(); + }); + function run() { + // each of these can by async + synchronize(function() { + test.setup(); + }); + synchronize(function() { + test.run(); + }); + synchronize(function() { + test.teardown(); + }); + synchronize(function() { + test.finish(); + }); + } + + // `bad` initialized at top of scope + // defer when previous test run passed, if storage is available + bad = QUnit.config.reorder && defined.sessionStorage && + +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); + + if ( bad ) { + run(); + } else { + synchronize( run, true ); + } + } +}; + +// Root QUnit object. +// `QUnit` initialized at top of scope +QUnit = { + + // call on start of module test to prepend name to all tests + module: function( name, testEnvironment ) { + config.currentModule = name; + config.currentModuleTestEnvironment = testEnvironment; + config.modules[name] = true; + }, + + asyncTest: function( testName, expected, callback ) { + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + + QUnit.test( testName, expected, callback, true ); + }, + + test: function( testName, expected, callback, async ) { + var test, + nameHtml = "<span class='test-name'>" + escapeText( testName ) + "</span>"; + + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + + if ( config.currentModule ) { + nameHtml = "<span class='module-name'>" + escapeText( config.currentModule ) + "</span>: " + nameHtml; + } + + test = new Test({ + nameHtml: nameHtml, + testName: testName, + expected: expected, + async: async, + callback: callback, + module: config.currentModule, + moduleTestEnvironment: config.currentModuleTestEnvironment, + stack: sourceFromStacktrace( 2 ) + }); + + if ( !validTest( test ) ) { + return; + } + + test.queue(); + }, + + // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through. + expect: function( asserts ) { + if (arguments.length === 1) { + config.current.expected = asserts; + } else { + return config.current.expected; + } + }, + + start: function( count ) { + // QUnit hasn't been initialized yet. + // Note: RequireJS (et al) may delay onLoad + if ( config.semaphore === undefined ) { + QUnit.begin(function() { + // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first + setTimeout(function() { + QUnit.start( count ); + }); + }); + return; + } + + config.semaphore -= count || 1; + // don't start until equal number of stop-calls + if ( config.semaphore > 0 ) { + return; + } + // ignore if start is called more often then stop + if ( config.semaphore < 0 ) { + config.semaphore = 0; + QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) ); + return; + } + // A slight delay, to avoid any current callbacks + if ( defined.setTimeout ) { + setTimeout(function() { + if ( config.semaphore > 0 ) { + return; + } + if ( config.timeout ) { + clearTimeout( config.timeout ); + } + + config.blocking = false; + process( true ); + }, 13); + } else { + config.blocking = false; + process( true ); + } + }, + + stop: function( count ) { + config.semaphore += count || 1; + config.blocking = true; + + if ( config.testTimeout && defined.setTimeout ) { + clearTimeout( config.timeout ); + config.timeout = setTimeout(function() { + QUnit.ok( false, "Test timed out" ); + config.semaphore = 1; + QUnit.start(); + }, config.testTimeout ); + } + } +}; + +// `assert` initialized at top of scope +// Assert helpers +// All of these must either call QUnit.push() or manually do: +// - runLoggingCallbacks( "log", .. ); +// - config.current.assertions.push({ .. }); +// We attach it to the QUnit object *after* we expose the public API, +// otherwise `assert` will become a global variable in browsers (#341). +assert = { + /** + * Asserts rough true-ish result. + * @name ok + * @function + * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); + */ + ok: function( result, msg ) { + if ( !config.current ) { + throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); + } + result = !!result; + msg = msg || (result ? "okay" : "failed" ); + + var source, + details = { + module: config.current.module, + name: config.current.testName, + result: result, + message: msg + }; + + msg = "<span class='test-message'>" + escapeText( msg ) + "</span>"; + + if ( !result ) { + source = sourceFromStacktrace( 2 ); + if ( source ) { + details.source = source; + msg += "<table><tr class='test-source'><th>Source: </th><td><pre>" + escapeText( source ) + "</pre></td></tr></table>"; + } + } + runLoggingCallbacks( "log", QUnit, details ); + config.current.assertions.push({ + result: result, + message: msg + }); + }, + + /** + * Assert that the first two arguments are equal, with an optional message. + * Prints out both actual and expected values. + * @name equal + * @function + * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); + */ + equal: function( actual, expected, message ) { + /*jshint eqeqeq:false */ + QUnit.push( expected == actual, actual, expected, message ); + }, + + /** + * @name notEqual + * @function + */ + notEqual: function( actual, expected, message ) { + /*jshint eqeqeq:false */ + QUnit.push( expected != actual, actual, expected, message ); + }, + + /** + * @name propEqual + * @function + */ + propEqual: function( actual, expected, message ) { + actual = objectValues(actual); + expected = objectValues(expected); + QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name notPropEqual + * @function + */ + notPropEqual: function( actual, expected, message ) { + actual = objectValues(actual); + expected = objectValues(expected); + QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name deepEqual + * @function + */ + deepEqual: function( actual, expected, message ) { + QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name notDeepEqual + * @function + */ + notDeepEqual: function( actual, expected, message ) { + QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name strictEqual + * @function + */ + strictEqual: function( actual, expected, message ) { + QUnit.push( expected === actual, actual, expected, message ); + }, + + /** + * @name notStrictEqual + * @function + */ + notStrictEqual: function( actual, expected, message ) { + QUnit.push( expected !== actual, actual, expected, message ); + }, + + "throws": function( block, expected, message ) { + var actual, + expectedOutput = expected, + ok = false; + + // 'expected' is optional + if ( typeof expected === "string" ) { + message = expected; + expected = null; + } + + config.current.ignoreGlobalErrors = true; + try { + block.call( config.current.testEnvironment ); + } catch (e) { + actual = e; + } + config.current.ignoreGlobalErrors = false; + + if ( actual ) { + // we don't want to validate thrown error + if ( !expected ) { + ok = true; + expectedOutput = null; + // expected is a regexp + } else if ( QUnit.objectType( expected ) === "regexp" ) { + ok = expected.test( errorString( actual ) ); + // expected is a constructor + } else if ( actual instanceof expected ) { + ok = true; + // expected is a validation function which returns true is validation passed + } else if ( expected.call( {}, actual ) === true ) { + expectedOutput = null; + ok = true; + } + + QUnit.push( ok, actual, expectedOutput, message ); + } else { + QUnit.pushFailure( message, null, "No exception was thrown." ); + } + } +}; + +/** + * @deprecated since 1.8.0 + * Kept assertion helpers in root for backwards compatibility. + */ +extend( QUnit, assert ); + +/** + * @deprecated since 1.9.0 + * Kept root "raises()" for backwards compatibility. + * (Note that we don't introduce assert.raises). + */ +QUnit.raises = assert[ "throws" ]; + +/** + * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 + * Kept to avoid TypeErrors for undefined methods. + */ +QUnit.equals = function() { + QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); +}; +QUnit.same = function() { + QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); +}; + +// We want access to the constructor's prototype +(function() { + function F() {} + F.prototype = QUnit; + QUnit = new F(); + // Make F QUnit's constructor so that we can add to the prototype later + QUnit.constructor = F; +}()); + +/** + * Config object: Maintain internal state + * Later exposed as QUnit.config + * `config` initialized at top of scope + */ +config = { + // The queue of tests to run + queue: [], + + // block until document ready + blocking: true, + + // when enabled, show only failing tests + // gets persisted through sessionStorage and can be changed in UI via checkbox + hidepassed: false, + + // by default, run previously failed tests first + // very useful in combination with "Hide passed tests" checked + reorder: true, + + // by default, modify document.title when suite is done + altertitle: true, + + // when enabled, all tests must call expect() + requireExpects: false, + + // add checkboxes that are persisted in the query-string + // when enabled, the id is set to `true` as a `QUnit.config` property + urlConfig: [ + { + id: "noglobals", + label: "Check for Globals", + tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." + }, + { + id: "notrycatch", + label: "No try-catch", + tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." + } + ], + + // Set of all modules. + modules: {}, + + // logging callback queues + begin: [], + done: [], + log: [], + testStart: [], + testDone: [], + moduleStart: [], + moduleDone: [] +}; + +// Export global variables, unless an 'exports' object exists, +// in that case we assume we're in CommonJS (dealt with on the bottom of the script) +if ( typeof exports === "undefined" ) { + extend( window, QUnit.constructor.prototype ); + + // Expose QUnit object + window.QUnit = QUnit; +} + +// Initialize more QUnit.config and QUnit.urlParams +(function() { + var i, + location = window.location || { search: "", protocol: "file:" }, + params = location.search.slice( 1 ).split( "&" ), + length = params.length, + urlParams = {}, + current; + + if ( params[ 0 ] ) { + for ( i = 0; i < length; i++ ) { + current = params[ i ].split( "=" ); + current[ 0 ] = decodeURIComponent( current[ 0 ] ); + // allow just a key to turn on a flag, e.g., test.html?noglobals + current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; + urlParams[ current[ 0 ] ] = current[ 1 ]; + } + } + + QUnit.urlParams = urlParams; + + // String search anywhere in moduleName+testName + config.filter = urlParams.filter; + + // Exact match of the module name + config.module = urlParams.module; + + config.testNumber = parseInt( urlParams.testNumber, 10 ) || null; + + // Figure out if we're running the tests from a server or not + QUnit.isLocal = location.protocol === "file:"; +}()); + +// Extend QUnit object, +// these after set here because they should not be exposed as global functions +extend( QUnit, { + assert: assert, + + config: config, + + // Initialize the configuration options + init: function() { + extend( config, { + stats: { all: 0, bad: 0 }, + moduleStats: { all: 0, bad: 0 }, + started: +new Date(), + updateRate: 1000, + blocking: false, + autostart: true, + autorun: false, + filter: "", + queue: [], + semaphore: 1 + }); + + var tests, banner, result, + qunit = id( "qunit" ); + + if ( qunit ) { + qunit.innerHTML = + "<h1 id='qunit-header'>" + escapeText( document.title ) + "</h1>" + + "<h2 id='qunit-banner'></h2>" + + "<div id='qunit-testrunner-toolbar'></div>" + + "<h2 id='qunit-userAgent'></h2>" + + "<ol id='qunit-tests'></ol>"; + } + + tests = id( "qunit-tests" ); + banner = id( "qunit-banner" ); + result = id( "qunit-testresult" ); + + if ( tests ) { + tests.innerHTML = ""; + } + + if ( banner ) { + banner.className = ""; + } + + if ( result ) { + result.parentNode.removeChild( result ); + } + + if ( tests ) { + result = document.createElement( "p" ); + result.id = "qunit-testresult"; + result.className = "result"; + tests.parentNode.insertBefore( result, tests ); + result.innerHTML = "Running...<br/> "; + } + }, + + // Resets the test setup. Useful for tests that modify the DOM. + /* + DEPRECATED: Use multiple tests instead of resetting inside a test. + Use testStart or testDone for custom cleanup. + This method will throw an error in 2.0, and will be removed in 2.1 + */ + reset: function() { + var fixture = id( "qunit-fixture" ); + if ( fixture ) { + fixture.innerHTML = config.fixture; + } + }, + + // Trigger an event on an element. + // @example triggerEvent( document.body, "click" ); + triggerEvent: function( elem, type, event ) { + if ( document.createEvent ) { + event = document.createEvent( "MouseEvents" ); + event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, + 0, 0, 0, 0, 0, false, false, false, false, 0, null); + + elem.dispatchEvent( event ); + } else if ( elem.fireEvent ) { + elem.fireEvent( "on" + type ); + } + }, + + // Safe object type checking + is: function( type, obj ) { + return QUnit.objectType( obj ) === type; + }, + + objectType: function( obj ) { + if ( typeof obj === "undefined" ) { + return "undefined"; + // consider: typeof null === object + } + if ( obj === null ) { + return "null"; + } + + var match = toString.call( obj ).match(/^\[object\s(.*)\]$/), + type = match && match[1] || ""; + + switch ( type ) { + case "Number": + if ( isNaN(obj) ) { + return "nan"; + } + return "number"; + case "String": + case "Boolean": + case "Array": + case "Date": + case "RegExp": + case "Function": + return type.toLowerCase(); + } + if ( typeof obj === "object" ) { + return "object"; + } + return undefined; + }, + + push: function( result, actual, expected, message ) { + if ( !config.current ) { + throw new Error( "assertion outside test context, was " + sourceFromStacktrace() ); + } + + var output, source, + details = { + module: config.current.module, + name: config.current.testName, + result: result, + message: message, + actual: actual, + expected: expected + }; + + message = escapeText( message ) || ( result ? "okay" : "failed" ); + message = "<span class='test-message'>" + message + "</span>"; + output = message; + + if ( !result ) { + expected = escapeText( QUnit.jsDump.parse(expected) ); + actual = escapeText( QUnit.jsDump.parse(actual) ); + output += "<table><tr class='test-expected'><th>Expected: </th><td><pre>" + expected + "</pre></td></tr>"; + + if ( actual !== expected ) { + output += "<tr class='test-actual'><th>Result: </th><td><pre>" + actual + "</pre></td></tr>"; + output += "<tr class='test-diff'><th>Diff: </th><td><pre>" + QUnit.diff( expected, actual ) + "</pre></td></tr>"; + } + + source = sourceFromStacktrace(); + + if ( source ) { + details.source = source; + output += "<tr class='test-source'><th>Source: </th><td><pre>" + escapeText( source ) + "</pre></td></tr>"; + } + + output += "</table>"; + } + + runLoggingCallbacks( "log", QUnit, details ); + + config.current.assertions.push({ + result: !!result, + message: output + }); + }, + + pushFailure: function( message, source, actual ) { + if ( !config.current ) { + throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) ); + } + + var output, + details = { + module: config.current.module, + name: config.current.testName, + result: false, + message: message + }; + + message = escapeText( message ) || "error"; + message = "<span class='test-message'>" + message + "</span>"; + output = message; + + output += "<table>"; + + if ( actual ) { + output += "<tr class='test-actual'><th>Result: </th><td><pre>" + escapeText( actual ) + "</pre></td></tr>"; + } + + if ( source ) { + details.source = source; + output += "<tr class='test-source'><th>Source: </th><td><pre>" + escapeText( source ) + "</pre></td></tr>"; + } + + output += "</table>"; + + runLoggingCallbacks( "log", QUnit, details ); + + config.current.assertions.push({ + result: false, + message: output + }); + }, + + url: function( params ) { + params = extend( extend( {}, QUnit.urlParams ), params ); + var key, + querystring = "?"; + + for ( key in params ) { + if ( hasOwn.call( params, key ) ) { + querystring += encodeURIComponent( key ) + "=" + + encodeURIComponent( params[ key ] ) + "&"; + } + } + return window.location.protocol + "//" + window.location.host + + window.location.pathname + querystring.slice( 0, -1 ); + }, + + extend: extend, + id: id, + addEvent: addEvent, + addClass: addClass, + hasClass: hasClass, + removeClass: removeClass + // load, equiv, jsDump, diff: Attached later +}); + +/** + * @deprecated: Created for backwards compatibility with test runner that set the hook function + * into QUnit.{hook}, instead of invoking it and passing the hook function. + * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. + * Doing this allows us to tell if the following methods have been overwritten on the actual + * QUnit object. + */ +extend( QUnit.constructor.prototype, { + + // Logging callbacks; all receive a single argument with the listed properties + // run test/logs.html for any related changes + begin: registerLoggingCallback( "begin" ), + + // done: { failed, passed, total, runtime } + done: registerLoggingCallback( "done" ), + + // log: { result, actual, expected, message } + log: registerLoggingCallback( "log" ), + + // testStart: { name } + testStart: registerLoggingCallback( "testStart" ), + + // testDone: { name, failed, passed, total, duration } + testDone: registerLoggingCallback( "testDone" ), + + // moduleStart: { name } + moduleStart: registerLoggingCallback( "moduleStart" ), + + // moduleDone: { name, failed, passed, total } + moduleDone: registerLoggingCallback( "moduleDone" ) +}); + +if ( typeof document === "undefined" || document.readyState === "complete" ) { + config.autorun = true; +} + +QUnit.load = function() { + runLoggingCallbacks( "begin", QUnit, {} ); + + // Initialize the config, saving the execution queue + var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, + urlConfigCheckboxesContainer, urlConfigCheckboxes, moduleFilter, + numModules = 0, + moduleNames = [], + moduleFilterHtml = "", + urlConfigHtml = "", + oldconfig = extend( {}, config ); + + QUnit.init(); + extend(config, oldconfig); + + config.blocking = false; + + len = config.urlConfig.length; + + for ( i = 0; i < len; i++ ) { + val = config.urlConfig[i]; + if ( typeof val === "string" ) { + val = { + id: val, + label: val, + tooltip: "[no tooltip available]" + }; + } + config[ val.id ] = QUnit.urlParams[ val.id ]; + urlConfigHtml += "<input id='qunit-urlconfig-" + escapeText( val.id ) + + "' name='" + escapeText( val.id ) + + "' type='checkbox'" + ( config[ val.id ] ? " checked='checked'" : "" ) + + " title='" + escapeText( val.tooltip ) + + "'><label for='qunit-urlconfig-" + escapeText( val.id ) + + "' title='" + escapeText( val.tooltip ) + "'>" + val.label + "</label>"; + } + for ( i in config.modules ) { + if ( config.modules.hasOwnProperty( i ) ) { + moduleNames.push(i); + } + } + numModules = moduleNames.length; + moduleNames.sort( function( a, b ) { + return a.localeCompare( b ); + }); + moduleFilterHtml += "<label for='qunit-modulefilter'>Module: </label><select id='qunit-modulefilter' name='modulefilter'><option value='' " + + ( config.module === undefined ? "selected='selected'" : "" ) + + ">< All Modules ></option>"; + + + for ( i = 0; i < numModules; i++) { + moduleFilterHtml += "<option value='" + escapeText( encodeURIComponent(moduleNames[i]) ) + "' " + + ( config.module === moduleNames[i] ? "selected='selected'" : "" ) + + ">" + escapeText(moduleNames[i]) + "</option>"; + } + moduleFilterHtml += "</select>"; + + // `userAgent` initialized at top of scope + userAgent = id( "qunit-userAgent" ); + if ( userAgent ) { + userAgent.innerHTML = navigator.userAgent; + } + + // `banner` initialized at top of scope + banner = id( "qunit-header" ); + if ( banner ) { + banner.innerHTML = "<a href='" + QUnit.url({ filter: undefined, module: undefined, testNumber: undefined }) + "'>" + banner.innerHTML + "</a> "; + } + + // `toolbar` initialized at top of scope + toolbar = id( "qunit-testrunner-toolbar" ); + if ( toolbar ) { + // `filter` initialized at top of scope + filter = document.createElement( "input" ); + filter.type = "checkbox"; + filter.id = "qunit-filter-pass"; + + addEvent( filter, "click", function() { + var tmp, + ol = document.getElementById( "qunit-tests" ); + + if ( filter.checked ) { + ol.className = ol.className + " hidepass"; + } else { + tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; + ol.className = tmp.replace( / hidepass /, " " ); + } + if ( defined.sessionStorage ) { + if (filter.checked) { + sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); + } else { + sessionStorage.removeItem( "qunit-filter-passed-tests" ); + } + } + }); + + if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { + filter.checked = true; + // `ol` initialized at top of scope + ol = document.getElementById( "qunit-tests" ); + ol.className = ol.className + " hidepass"; + } + toolbar.appendChild( filter ); + + // `label` initialized at top of scope + label = document.createElement( "label" ); + label.setAttribute( "for", "qunit-filter-pass" ); + label.setAttribute( "title", "Only show tests and assertions that fail. Stored in sessionStorage." ); + label.innerHTML = "Hide passed tests"; + toolbar.appendChild( label ); + + urlConfigCheckboxesContainer = document.createElement("span"); + urlConfigCheckboxesContainer.innerHTML = urlConfigHtml; + urlConfigCheckboxes = urlConfigCheckboxesContainer.getElementsByTagName("input"); + // For oldIE support: + // * Add handlers to the individual elements instead of the container + // * Use "click" instead of "change" + // * Fallback from event.target to event.srcElement + addEvents( urlConfigCheckboxes, "click", function( event ) { + var params = {}, + target = event.target || event.srcElement; + params[ target.name ] = target.checked ? true : undefined; + window.location = QUnit.url( params ); + }); + toolbar.appendChild( urlConfigCheckboxesContainer ); + + if (numModules > 1) { + moduleFilter = document.createElement( "span" ); + moduleFilter.setAttribute( "id", "qunit-modulefilter-container" ); + moduleFilter.innerHTML = moduleFilterHtml; + addEvent( moduleFilter.lastChild, "change", function() { + var selectBox = moduleFilter.getElementsByTagName("select")[0], + selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); + + window.location = QUnit.url({ + module: ( selectedModule === "" ) ? undefined : selectedModule, + // Remove any existing filters + filter: undefined, + testNumber: undefined + }); + }); + toolbar.appendChild(moduleFilter); + } + } + + // `main` initialized at top of scope + main = id( "qunit-fixture" ); + if ( main ) { + config.fixture = main.innerHTML; + } + + if ( config.autostart ) { + QUnit.start(); + } +}; + +addEvent( window, "load", QUnit.load ); + +// `onErrorFnPrev` initialized at top of scope +// Preserve other handlers +onErrorFnPrev = window.onerror; + +// Cover uncaught exceptions +// Returning true will suppress the default browser handler, +// returning false will let it run. +window.onerror = function ( error, filePath, linerNr ) { + var ret = false; + if ( onErrorFnPrev ) { + ret = onErrorFnPrev( error, filePath, linerNr ); + } + + // Treat return value as window.onerror itself does, + // Only do our handling if not suppressed. + if ( ret !== true ) { + if ( QUnit.config.current ) { + if ( QUnit.config.current.ignoreGlobalErrors ) { + return true; + } + QUnit.pushFailure( error, filePath + ":" + linerNr ); + } else { + QUnit.test( "global failure", extend( function() { + QUnit.pushFailure( error, filePath + ":" + linerNr ); + }, { validTest: validTest } ) ); + } + return false; + } + + return ret; +}; + +function done() { + config.autorun = true; + + // Log the last module results + if ( config.currentModule ) { + runLoggingCallbacks( "moduleDone", QUnit, { + name: config.currentModule, + failed: config.moduleStats.bad, + passed: config.moduleStats.all - config.moduleStats.bad, + total: config.moduleStats.all + }); + } + delete config.previousModule; + + var i, key, + banner = id( "qunit-banner" ), + tests = id( "qunit-tests" ), + runtime = +new Date() - config.started, + passed = config.stats.all - config.stats.bad, + html = [ + "Tests completed in ", + runtime, + " milliseconds.<br/>", + "<span class='passed'>", + passed, + "</span> assertions of <span class='total'>", + config.stats.all, + "</span> passed, <span class='failed'>", + config.stats.bad, + "</span> failed." + ].join( "" ); + + if ( banner ) { + banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" ); + } + + if ( tests ) { + id( "qunit-testresult" ).innerHTML = html; + } + + if ( config.altertitle && typeof document !== "undefined" && document.title ) { + // show ✖ for good, ✔ for bad suite result in title + // use escape sequences in case file gets loaded with non-utf-8-charset + document.title = [ + ( config.stats.bad ? "\u2716" : "\u2714" ), + document.title.replace( /^[\u2714\u2716] /i, "" ) + ].join( " " ); + } + + // clear own sessionStorage items if all tests passed + if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { + // `key` & `i` initialized at top of scope + for ( i = 0; i < sessionStorage.length; i++ ) { + key = sessionStorage.key( i++ ); + if ( key.indexOf( "qunit-test-" ) === 0 ) { + sessionStorage.removeItem( key ); + } + } + } + + // scroll back to top to show results + if ( window.scrollTo ) { + window.scrollTo(0, 0); + } + + runLoggingCallbacks( "done", QUnit, { + failed: config.stats.bad, + passed: passed, + total: config.stats.all, + runtime: runtime + }); +} + +/** @return Boolean: true if this test should be ran */ +function validTest( test ) { + var include, + filter = config.filter && config.filter.toLowerCase(), + module = config.module && config.module.toLowerCase(), + fullName = (test.module + ": " + test.testName).toLowerCase(); + + // Internally-generated tests are always valid + if ( test.callback && test.callback.validTest === validTest ) { + delete test.callback.validTest; + return true; + } + + if ( config.testNumber ) { + return test.testNumber === config.testNumber; + } + + if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { + return false; + } + + if ( !filter ) { + return true; + } + + include = filter.charAt( 0 ) !== "!"; + if ( !include ) { + filter = filter.slice( 1 ); + } + + // If the filter matches, we need to honour include + if ( fullName.indexOf( filter ) !== -1 ) { + return include; + } + + // Otherwise, do the opposite + return !include; +} + +// so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) +// Later Safari and IE10 are supposed to support error.stack as well +// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack +function extractStacktrace( e, offset ) { + offset = offset === undefined ? 3 : offset; + + var stack, include, i; + + if ( e.stacktrace ) { + // Opera + return e.stacktrace.split( "\n" )[ offset + 3 ]; + } else if ( e.stack ) { + // Firefox, Chrome + stack = e.stack.split( "\n" ); + if (/^error$/i.test( stack[0] ) ) { + stack.shift(); + } + if ( fileName ) { + include = []; + for ( i = offset; i < stack.length; i++ ) { + if ( stack[ i ].indexOf( fileName ) !== -1 ) { + break; + } + include.push( stack[ i ] ); + } + if ( include.length ) { + return include.join( "\n" ); + } + } + return stack[ offset ]; + } else if ( e.sourceURL ) { + // Safari, PhantomJS + // hopefully one day Safari provides actual stacktraces + // exclude useless self-reference for generated Error objects + if ( /qunit.js$/.test( e.sourceURL ) ) { + return; + } + // for actual exceptions, this is useful + return e.sourceURL + ":" + e.line; + } +} +function sourceFromStacktrace( offset ) { + try { + throw new Error(); + } catch ( e ) { + return extractStacktrace( e, offset ); + } +} + +/** + * Escape text for attribute or text content. + */ +function escapeText( s ) { + if ( !s ) { + return ""; + } + s = s + ""; + // Both single quotes and double quotes (for attributes) + return s.replace( /['"<>&]/g, function( s ) { + switch( s ) { + case "'": + return "'"; + case "\"": + return """; + case "<": + return "<"; + case ">": + return ">"; + case "&": + return "&"; + } + }); +} + +function synchronize( callback, last ) { + config.queue.push( callback ); + + if ( config.autorun && !config.blocking ) { + process( last ); + } +} + +function process( last ) { + function next() { + process( last ); + } + var start = new Date().getTime(); + config.depth = config.depth ? config.depth + 1 : 1; + + while ( config.queue.length && !config.blocking ) { + if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { + config.queue.shift()(); + } else { + setTimeout( next, 13 ); + break; + } + } + config.depth--; + if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { + done(); + } +} + +function saveGlobal() { + config.pollution = []; + + if ( config.noglobals ) { + for ( var key in window ) { + if ( hasOwn.call( window, key ) ) { + // in Opera sometimes DOM element ids show up here, ignore them + if ( /^qunit-test-output/.test( key ) ) { + continue; + } + config.pollution.push( key ); + } + } + } +} + +function checkPollution() { + var newGlobals, + deletedGlobals, + old = config.pollution; + + saveGlobal(); + + newGlobals = diff( config.pollution, old ); + if ( newGlobals.length > 0 ) { + QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); + } + + deletedGlobals = diff( old, config.pollution ); + if ( deletedGlobals.length > 0 ) { + QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); + } +} + +// returns a new Array with the elements that are in a but not in b +function diff( a, b ) { + var i, j, + result = a.slice(); + + for ( i = 0; i < result.length; i++ ) { + for ( j = 0; j < b.length; j++ ) { + if ( result[i] === b[j] ) { + result.splice( i, 1 ); + i--; + break; + } + } + } + return result; +} + +function extend( a, b ) { + for ( var prop in b ) { + if ( hasOwn.call( b, prop ) ) { + // Avoid "Member not found" error in IE8 caused by messing with window.constructor + if ( !( prop === "constructor" && a === window ) ) { + if ( b[ prop ] === undefined ) { + delete a[ prop ]; + } else { + a[ prop ] = b[ prop ]; + } + } + } + } + + return a; +} + +/** + * @param {HTMLElement} elem + * @param {string} type + * @param {Function} fn + */ +function addEvent( elem, type, fn ) { + // Standards-based browsers + if ( elem.addEventListener ) { + elem.addEventListener( type, fn, false ); + // IE + } else { + elem.attachEvent( "on" + type, fn ); + } +} + +/** + * @param {Array|NodeList} elems + * @param {string} type + * @param {Function} fn + */ +function addEvents( elems, type, fn ) { + var i = elems.length; + while ( i-- ) { + addEvent( elems[i], type, fn ); + } +} + +function hasClass( elem, name ) { + return (" " + elem.className + " ").indexOf(" " + name + " ") > -1; +} + +function addClass( elem, name ) { + if ( !hasClass( elem, name ) ) { + elem.className += (elem.className ? " " : "") + name; + } +} + +function removeClass( elem, name ) { + var set = " " + elem.className + " "; + // Class name may appear multiple times + while ( set.indexOf(" " + name + " ") > -1 ) { + set = set.replace(" " + name + " " , " "); + } + // If possible, trim it for prettiness, but not necessarily + elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, ""); +} + +function id( name ) { + return !!( typeof document !== "undefined" && document && document.getElementById ) && + document.getElementById( name ); +} + +function registerLoggingCallback( key ) { + return function( callback ) { + config[key].push( callback ); + }; +} + +// Supports deprecated method of completely overwriting logging callbacks +function runLoggingCallbacks( key, scope, args ) { + var i, callbacks; + if ( QUnit.hasOwnProperty( key ) ) { + QUnit[ key ].call(scope, args ); + } else { + callbacks = config[ key ]; + for ( i = 0; i < callbacks.length; i++ ) { + callbacks[ i ].call( scope, args ); + } + } +} + +// Test for equality any JavaScript type. +// Author: Philippe Rathé <prathe@gmail.com> +QUnit.equiv = (function() { + + // Call the o related callback with the given arguments. + function bindCallbacks( o, callbacks, args ) { + var prop = QUnit.objectType( o ); + if ( prop ) { + if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { + return callbacks[ prop ].apply( callbacks, args ); + } else { + return callbacks[ prop ]; // or undefined + } + } + } + + // the real equiv function + var innerEquiv, + // stack to decide between skip/abort functions + callers = [], + // stack to avoiding loops from circular referencing + parents = [], + parentsB = [], + + getProto = Object.getPrototypeOf || function ( obj ) { + /*jshint camelcase:false */ + return obj.__proto__; + }, + callbacks = (function () { + + // for string, boolean, number and null + function useStrictEquality( b, a ) { + /*jshint eqeqeq:false */ + if ( b instanceof a.constructor || a instanceof b.constructor ) { + // to catch short annotation VS 'new' annotation of a + // declaration + // e.g. var i = 1; + // var j = new Number(1); + return a == b; + } else { + return a === b; + } + } + + return { + "string": useStrictEquality, + "boolean": useStrictEquality, + "number": useStrictEquality, + "null": useStrictEquality, + "undefined": useStrictEquality, + + "nan": function( b ) { + return isNaN( b ); + }, + + "date": function( b, a ) { + return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); + }, + + "regexp": function( b, a ) { + return QUnit.objectType( b ) === "regexp" && + // the regex itself + a.source === b.source && + // and its modifiers + a.global === b.global && + // (gmi) ... + a.ignoreCase === b.ignoreCase && + a.multiline === b.multiline && + a.sticky === b.sticky; + }, + + // - skip when the property is a method of an instance (OOP) + // - abort otherwise, + // initial === would have catch identical references anyway + "function": function() { + var caller = callers[callers.length - 1]; + return caller !== Object && typeof caller !== "undefined"; + }, + + "array": function( b, a ) { + var i, j, len, loop, aCircular, bCircular; + + // b could be an object literal here + if ( QUnit.objectType( b ) !== "array" ) { + return false; + } + + len = a.length; + if ( len !== b.length ) { + // safe and faster + return false; + } + + // track reference to avoid circular references + parents.push( a ); + parentsB.push( b ); + for ( i = 0; i < len; i++ ) { + loop = false; + for ( j = 0; j < parents.length; j++ ) { + aCircular = parents[j] === a[i]; + bCircular = parentsB[j] === b[i]; + if ( aCircular || bCircular ) { + if ( a[i] === b[i] || aCircular && bCircular ) { + loop = true; + } else { + parents.pop(); + parentsB.pop(); + return false; + } + } + } + if ( !loop && !innerEquiv(a[i], b[i]) ) { + parents.pop(); + parentsB.pop(); + return false; + } + } + parents.pop(); + parentsB.pop(); + return true; + }, + + "object": function( b, a ) { + /*jshint forin:false */ + var i, j, loop, aCircular, bCircular, + // Default to true + eq = true, + aProperties = [], + bProperties = []; + + // comparing constructors is more strict than using + // instanceof + if ( a.constructor !== b.constructor ) { + // Allow objects with no prototype to be equivalent to + // objects with Object as their constructor. + if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) || + ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) { + return false; + } + } + + // stack constructor before traversing properties + callers.push( a.constructor ); + + // track reference to avoid circular references + parents.push( a ); + parentsB.push( b ); + + // be strict: don't ensure hasOwnProperty and go deep + for ( i in a ) { + loop = false; + for ( j = 0; j < parents.length; j++ ) { + aCircular = parents[j] === a[i]; + bCircular = parentsB[j] === b[i]; + if ( aCircular || bCircular ) { + if ( a[i] === b[i] || aCircular && bCircular ) { + loop = true; + } else { + eq = false; + break; + } + } + } + aProperties.push(i); + if ( !loop && !innerEquiv(a[i], b[i]) ) { + eq = false; + break; + } + } + + parents.pop(); + parentsB.pop(); + callers.pop(); // unstack, we are done + + for ( i in b ) { + bProperties.push( i ); // collect b's properties + } + + // Ensures identical properties name + return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); + } + }; + }()); + + innerEquiv = function() { // can take multiple arguments + var args = [].slice.apply( arguments ); + if ( args.length < 2 ) { + return true; // end transition + } + + return (function( a, b ) { + if ( a === b ) { + return true; // catch the most you can + } else if ( a === null || b === null || typeof a === "undefined" || + typeof b === "undefined" || + QUnit.objectType(a) !== QUnit.objectType(b) ) { + return false; // don't lose time with error prone cases + } else { + return bindCallbacks(a, callbacks, [ b, a ]); + } + + // apply transition with (1..n) arguments + }( args[0], args[1] ) && innerEquiv.apply( this, args.splice(1, args.length - 1 )) ); + }; + + return innerEquiv; +}()); + +/** + * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | + * http://flesler.blogspot.com Licensed under BSD + * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 + * + * @projectDescription Advanced and extensible data dumping for Javascript. + * @version 1.0.0 + * @author Ariel Flesler + * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} + */ +QUnit.jsDump = (function() { + function quote( str ) { + return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\""; + } + function literal( o ) { + return o + ""; + } + function join( pre, arr, post ) { + var s = jsDump.separator(), + base = jsDump.indent(), + inner = jsDump.indent(1); + if ( arr.join ) { + arr = arr.join( "," + s + inner ); + } + if ( !arr ) { + return pre + post; + } + return [ pre, inner + arr, base + post ].join(s); + } + function array( arr, stack ) { + var i = arr.length, ret = new Array(i); + this.up(); + while ( i-- ) { + ret[i] = this.parse( arr[i] , undefined , stack); + } + this.down(); + return join( "[", ret, "]" ); + } + + var reName = /^function (\w+)/, + jsDump = { + // type is used mostly internally, you can fix a (custom)type in advance + parse: function( obj, type, stack ) { + stack = stack || [ ]; + var inStack, res, + parser = this.parsers[ type || this.typeOf(obj) ]; + + type = typeof parser; + inStack = inArray( obj, stack ); + + if ( inStack !== -1 ) { + return "recursion(" + (inStack - stack.length) + ")"; + } + if ( type === "function" ) { + stack.push( obj ); + res = parser.call( this, obj, stack ); + stack.pop(); + return res; + } + return ( type === "string" ) ? parser : this.parsers.error; + }, + typeOf: function( obj ) { + var type; + if ( obj === null ) { + type = "null"; + } else if ( typeof obj === "undefined" ) { + type = "undefined"; + } else if ( QUnit.is( "regexp", obj) ) { + type = "regexp"; + } else if ( QUnit.is( "date", obj) ) { + type = "date"; + } else if ( QUnit.is( "function", obj) ) { + type = "function"; + } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { + type = "window"; + } else if ( obj.nodeType === 9 ) { + type = "document"; + } else if ( obj.nodeType ) { + type = "node"; + } else if ( + // native arrays + toString.call( obj ) === "[object Array]" || + // NodeList objects + ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) + ) { + type = "array"; + } else if ( obj.constructor === Error.prototype.constructor ) { + type = "error"; + } else { + type = typeof obj; + } + return type; + }, + separator: function() { + return this.multiline ? this.HTML ? "<br />" : "\n" : this.HTML ? " " : " "; + }, + // extra can be a number, shortcut for increasing-calling-decreasing + indent: function( extra ) { + if ( !this.multiline ) { + return ""; + } + var chr = this.indentChar; + if ( this.HTML ) { + chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); + } + return new Array( this.depth + ( extra || 0 ) ).join(chr); + }, + up: function( a ) { + this.depth += a || 1; + }, + down: function( a ) { + this.depth -= a || 1; + }, + setParser: function( name, parser ) { + this.parsers[name] = parser; + }, + // The next 3 are exposed so you can use them + quote: quote, + literal: literal, + join: join, + // + depth: 1, + // This is the list of parsers, to modify them, use jsDump.setParser + parsers: { + window: "[Window]", + document: "[Document]", + error: function(error) { + return "Error(\"" + error.message + "\")"; + }, + unknown: "[Unknown]", + "null": "null", + "undefined": "undefined", + "function": function( fn ) { + var ret = "function", + // functions never have name in IE + name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; + + if ( name ) { + ret += " " + name; + } + ret += "( "; + + ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" ); + return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" ); + }, + array: array, + nodelist: array, + "arguments": array, + object: function( map, stack ) { + /*jshint forin:false */ + var ret = [ ], keys, key, val, i; + QUnit.jsDump.up(); + keys = []; + for ( key in map ) { + keys.push( key ); + } + keys.sort(); + for ( i = 0; i < keys.length; i++ ) { + key = keys[ i ]; + val = map[ key ]; + ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) ); + } + QUnit.jsDump.down(); + return join( "{", ret, "}" ); + }, + node: function( node ) { + var len, i, val, + open = QUnit.jsDump.HTML ? "<" : "<", + close = QUnit.jsDump.HTML ? ">" : ">", + tag = node.nodeName.toLowerCase(), + ret = open + tag, + attrs = node.attributes; + + if ( attrs ) { + for ( i = 0, len = attrs.length; i < len; i++ ) { + val = attrs[i].nodeValue; + // IE6 includes all attributes in .attributes, even ones not explicitly set. + // Those have values like undefined, null, 0, false, "" or "inherit". + if ( val && val !== "inherit" ) { + ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" ); + } + } + } + ret += close; + + // Show content of TextNode or CDATASection + if ( node.nodeType === 3 || node.nodeType === 4 ) { + ret += node.nodeValue; + } + + return ret + open + "/" + tag + close; + }, + // function calls it internally, it's the arguments part of the function + functionArgs: function( fn ) { + var args, + l = fn.length; + + if ( !l ) { + return ""; + } + + args = new Array(l); + while ( l-- ) { + // 97 is 'a' + args[l] = String.fromCharCode(97+l); + } + return " " + args.join( ", " ) + " "; + }, + // object calls it internally, the key part of an item in a map + key: quote, + // function calls it internally, it's the content of the function + functionCode: "[code]", + // node calls it internally, it's an html attribute value + attribute: quote, + string: quote, + date: quote, + regexp: literal, + number: literal, + "boolean": literal + }, + // if true, entities are escaped ( <, >, \t, space and \n ) + HTML: false, + // indentation unit + indentChar: " ", + // if true, items in a collection, are separated by a \n, else just a space. + multiline: true + }; + + return jsDump; +}()); + +// from jquery.js +function inArray( elem, array ) { + if ( array.indexOf ) { + return array.indexOf( elem ); + } + + for ( var i = 0, length = array.length; i < length; i++ ) { + if ( array[ i ] === elem ) { + return i; + } + } + + return -1; +} + +/* + * Javascript Diff Algorithm + * By John Resig (http://ejohn.org/) + * Modified by Chu Alan "sprite" + * + * Released under the MIT license. + * + * More Info: + * http://ejohn.org/projects/javascript-diff-algorithm/ + * + * Usage: QUnit.diff(expected, actual) + * + * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick <del>brown </del> fox <del>jumped </del><ins>jumps </ins> over" + */ +QUnit.diff = (function() { + /*jshint eqeqeq:false, eqnull:true */ + function diff( o, n ) { + var i, + ns = {}, + os = {}; + + for ( i = 0; i < n.length; i++ ) { + if ( !hasOwn.call( ns, n[i] ) ) { + ns[ n[i] ] = { + rows: [], + o: null + }; + } + ns[ n[i] ].rows.push( i ); + } + + for ( i = 0; i < o.length; i++ ) { + if ( !hasOwn.call( os, o[i] ) ) { + os[ o[i] ] = { + rows: [], + n: null + }; + } + os[ o[i] ].rows.push( i ); + } + + for ( i in ns ) { + if ( hasOwn.call( ns, i ) ) { + if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) { + n[ ns[i].rows[0] ] = { + text: n[ ns[i].rows[0] ], + row: os[i].rows[0] + }; + o[ os[i].rows[0] ] = { + text: o[ os[i].rows[0] ], + row: ns[i].rows[0] + }; + } + } + } + + for ( i = 0; i < n.length - 1; i++ ) { + if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && + n[ i + 1 ] == o[ n[i].row + 1 ] ) { + + n[ i + 1 ] = { + text: n[ i + 1 ], + row: n[i].row + 1 + }; + o[ n[i].row + 1 ] = { + text: o[ n[i].row + 1 ], + row: i + 1 + }; + } + } + + for ( i = n.length - 1; i > 0; i-- ) { + if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && + n[ i - 1 ] == o[ n[i].row - 1 ]) { + + n[ i - 1 ] = { + text: n[ i - 1 ], + row: n[i].row - 1 + }; + o[ n[i].row - 1 ] = { + text: o[ n[i].row - 1 ], + row: i - 1 + }; + } + } + + return { + o: o, + n: n + }; + } + + return function( o, n ) { + o = o.replace( /\s+$/, "" ); + n = n.replace( /\s+$/, "" ); + + var i, pre, + str = "", + out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ), + oSpace = o.match(/\s+/g), + nSpace = n.match(/\s+/g); + + if ( oSpace == null ) { + oSpace = [ " " ]; + } + else { + oSpace.push( " " ); + } + + if ( nSpace == null ) { + nSpace = [ " " ]; + } + else { + nSpace.push( " " ); + } + + if ( out.n.length === 0 ) { + for ( i = 0; i < out.o.length; i++ ) { + str += "<del>" + out.o[i] + oSpace[i] + "</del>"; + } + } + else { + if ( out.n[0].text == null ) { + for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) { + str += "<del>" + out.o[n] + oSpace[n] + "</del>"; + } + } + + for ( i = 0; i < out.n.length; i++ ) { + if (out.n[i].text == null) { + str += "<ins>" + out.n[i] + nSpace[i] + "</ins>"; + } + else { + // `pre` initialized at top of scope + pre = ""; + + for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { + pre += "<del>" + out.o[n] + oSpace[n] + "</del>"; + } + str += " " + out.n[i].text + nSpace[i] + pre; + } + } + } + + return str; + }; +}()); + +// for CommonJS environments, export everything +if ( typeof exports !== "undefined" ) { + extend( exports, QUnit.constructor.prototype ); +} + +// get at whatever the global object is, like window in browsers +}( (function() {return this;}.call()) )); diff --git a/static/js/moment.min.js b/static/js/moment.min.js new file mode 100644 index 000000000..f56b370a8 --- /dev/null +++ b/static/js/moment.min.js @@ -0,0 +1,6 @@ +//! moment.js +//! version : 2.2.1 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com +(function(a){function b(a,b){return function(c){return i(a.call(this,c),b)}}function c(a,b){return function(c){return this.lang().ordinal(a.call(this,c),b)}}function d(){}function e(a){g(this,a)}function f(a){var b=a.years||a.year||a.y||0,c=a.months||a.month||a.M||0,d=a.weeks||a.week||a.w||0,e=a.days||a.day||a.d||0,f=a.hours||a.hour||a.h||0,g=a.minutes||a.minute||a.m||0,h=a.seconds||a.second||a.s||0,i=a.milliseconds||a.millisecond||a.ms||0;this._input=a,this._milliseconds=+i+1e3*h+6e4*g+36e5*f,this._days=+e+7*d,this._months=+c+12*b,this._data={},this._bubble()}function g(a,b){for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);return a}function h(a){return 0>a?Math.ceil(a):Math.floor(a)}function i(a,b){for(var c=a+"";c.length<b;)c="0"+c;return c}function j(a,b,c,d){var e,f,g=b._milliseconds,h=b._days,i=b._months;g&&a._d.setTime(+a._d+g*c),(h||i)&&(e=a.minute(),f=a.hour()),h&&a.date(a.date()+h*c),i&&a.month(a.month()+i*c),g&&!d&&L.updateOffset(a),(h||i)&&(a.minute(e),a.hour(f))}function k(a){return"[object Array]"===Object.prototype.toString.call(a)}function l(a,b){var c,d=Math.min(a.length,b.length),e=Math.abs(a.length-b.length),f=0;for(c=0;d>c;c++)~~a[c]!==~~b[c]&&f++;return f+e}function m(a){return a?ib[a]||a.toLowerCase().replace(/(.)s$/,"$1"):a}function n(a,b){return b.abbr=a,P[a]||(P[a]=new d),P[a].set(b),P[a]}function o(a){delete P[a]}function p(a){if(!a)return L.fn._lang;if(!P[a]&&Q)try{require("./lang/"+a)}catch(b){return L.fn._lang}return P[a]||L.fn._lang}function q(a){return a.match(/\[.*\]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function r(a){var b,c,d=a.match(T);for(b=0,c=d.length;c>b;b++)d[b]=mb[d[b]]?mb[d[b]]:q(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function s(a,b){return b=t(b,a.lang()),jb[b]||(jb[b]=r(b)),jb[b](a)}function t(a,b){function c(a){return b.longDateFormat(a)||a}for(var d=5;d--&&(U.lastIndex=0,U.test(a));)a=a.replace(U,c);return a}function u(a,b){switch(a){case"DDDD":return X;case"YYYY":return Y;case"YYYYY":return Z;case"S":case"SS":case"SSS":case"DDD":return W;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return $;case"a":case"A":return p(b._l)._meridiemParse;case"X":return bb;case"Z":case"ZZ":return _;case"T":return ab;case"MM":case"DD":case"YY":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":return V;default:return new RegExp(a.replace("\\",""))}}function v(a){var b=(_.exec(a)||[])[0],c=(b+"").match(fb)||["-",0,0],d=+(60*c[1])+~~c[2];return"+"===c[0]?-d:d}function w(a,b,c){var d,e=c._a;switch(a){case"M":case"MM":null!=b&&(e[1]=~~b-1);break;case"MMM":case"MMMM":d=p(c._l).monthsParse(b),null!=d?e[1]=d:c._isValid=!1;break;case"D":case"DD":null!=b&&(e[2]=~~b);break;case"DDD":case"DDDD":null!=b&&(e[1]=0,e[2]=~~b);break;case"YY":e[0]=~~b+(~~b>68?1900:2e3);break;case"YYYY":case"YYYYY":e[0]=~~b;break;case"a":case"A":c._isPm=p(c._l).isPM(b);break;case"H":case"HH":case"h":case"hh":e[3]=~~b;break;case"m":case"mm":e[4]=~~b;break;case"s":case"ss":e[5]=~~b;break;case"S":case"SS":case"SSS":e[6]=~~(1e3*("0."+b));break;case"X":c._d=new Date(1e3*parseFloat(b));break;case"Z":case"ZZ":c._useUTC=!0,c._tzm=v(b)}null==b&&(c._isValid=!1)}function x(a){var b,c,d,e=[];if(!a._d){for(d=z(a),b=0;3>b&&null==a._a[b];++b)a._a[b]=e[b]=d[b];for(;7>b;b++)a._a[b]=e[b]=null==a._a[b]?2===b?1:0:a._a[b];e[3]+=~~((a._tzm||0)/60),e[4]+=~~((a._tzm||0)%60),c=new Date(0),a._useUTC?(c.setUTCFullYear(e[0],e[1],e[2]),c.setUTCHours(e[3],e[4],e[5],e[6])):(c.setFullYear(e[0],e[1],e[2]),c.setHours(e[3],e[4],e[5],e[6])),a._d=c}}function y(a){var b=a._i;a._d||(a._a=[b.years||b.year||b.y,b.months||b.month||b.M,b.days||b.day||b.d,b.hours||b.hour||b.h,b.minutes||b.minute||b.m,b.seconds||b.second||b.s,b.milliseconds||b.millisecond||b.ms],x(a))}function z(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function A(a){var b,c,d,e=p(a._l),f=""+a._i;for(d=t(a._f,e).match(T),a._a=[],b=0;b<d.length;b++)c=(u(d[b],a).exec(f)||[])[0],c&&(f=f.slice(f.indexOf(c)+c.length)),mb[d[b]]&&w(d[b],c,a);f&&(a._il=f),a._isPm&&a._a[3]<12&&(a._a[3]+=12),a._isPm===!1&&12===a._a[3]&&(a._a[3]=0),x(a)}function B(a){var b,c,d,f,h,i=99;for(f=0;f<a._f.length;f++)b=g({},a),b._f=a._f[f],A(b),c=new e(b),h=l(b._a,c.toArray()),c._il&&(h+=c._il.length),i>h&&(i=h,d=c);g(a,d)}function C(a){var b,c=a._i,d=cb.exec(c);if(d){for(a._f="YYYY-MM-DD"+(d[2]||" "),b=0;4>b;b++)if(eb[b][1].exec(c)){a._f+=eb[b][0];break}_.exec(c)&&(a._f+=" Z"),A(a)}else a._d=new Date(c)}function D(b){var c=b._i,d=R.exec(c);c===a?b._d=new Date:d?b._d=new Date(+d[1]):"string"==typeof c?C(b):k(c)?(b._a=c.slice(0),x(b)):c instanceof Date?b._d=new Date(+c):"object"==typeof c?y(b):b._d=new Date(c)}function E(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function F(a,b,c){var d=O(Math.abs(a)/1e3),e=O(d/60),f=O(e/60),g=O(f/24),h=O(g/365),i=45>d&&["s",d]||1===e&&["m"]||45>e&&["mm",e]||1===f&&["h"]||22>f&&["hh",f]||1===g&&["d"]||25>=g&&["dd",g]||45>=g&&["M"]||345>g&&["MM",O(g/30)]||1===h&&["y"]||["yy",h];return i[2]=b,i[3]=a>0,i[4]=c,E.apply({},i)}function G(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=L(a).add("d",f),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function H(a){var b=a._i,c=a._f;return null===b||""===b?null:("string"==typeof b&&(a._i=b=p().preparse(b)),L.isMoment(b)?(a=g({},b),a._d=new Date(+b._d)):c?k(c)?B(a):A(a):D(a),new e(a))}function I(a,b){L.fn[a]=L.fn[a+"s"]=function(a){var c=this._isUTC?"UTC":"";return null!=a?(this._d["set"+c+b](a),L.updateOffset(this),this):this._d["get"+c+b]()}}function J(a){L.duration.fn[a]=function(){return this._data[a]}}function K(a,b){L.duration.fn["as"+a]=function(){return+this/b}}for(var L,M,N="2.2.1",O=Math.round,P={},Q="undefined"!=typeof module&&module.exports,R=/^\/?Date\((\-?\d+)/i,S=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)\:(\d+)\.?(\d{3})?/,T=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,U=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,V=/\d\d?/,W=/\d{1,3}/,X=/\d{3}/,Y=/\d{1,4}/,Z=/[+\-]?\d{1,6}/,$=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,_=/Z|[\+\-]\d\d:?\d\d/i,ab=/T/i,bb=/[\+\-]?\d+(\.\d{1,3})?/,cb=/^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,db="YYYY-MM-DDTHH:mm:ssZ",eb=[["HH:mm:ss.S",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],fb=/([\+\-]|\d\d)/gi,gb="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),hb={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},ib={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",w:"week",W:"isoweek",M:"month",y:"year"},jb={},kb="DDD w W M D d".split(" "),lb="M D H h m s w W".split(" "),mb={M:function(){return this.month()+1},MMM:function(a){return this.lang().monthsShort(this,a)},MMMM:function(a){return this.lang().months(this,a)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(a){return this.lang().weekdaysMin(this,a)},ddd:function(a){return this.lang().weekdaysShort(this,a)},dddd:function(a){return this.lang().weekdays(this,a)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return i(this.year()%100,2)},YYYY:function(){return i(this.year(),4)},YYYYY:function(){return i(this.year(),5)},gg:function(){return i(this.weekYear()%100,2)},gggg:function(){return this.weekYear()},ggggg:function(){return i(this.weekYear(),5)},GG:function(){return i(this.isoWeekYear()%100,2)},GGGG:function(){return this.isoWeekYear()},GGGGG:function(){return i(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return~~(this.milliseconds()/100)},SS:function(){return i(~~(this.milliseconds()/10),2)},SSS:function(){return i(this.milliseconds(),3)},Z:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+i(~~(a/60),2)+":"+i(~~a%60,2)},ZZ:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+i(~~(10*a/6),4)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()}};kb.length;)M=kb.pop(),mb[M+"o"]=c(mb[M],M);for(;lb.length;)M=lb.pop(),mb[M+M]=b(mb[M],2);for(mb.DDDD=b(mb.DDD,3),g(d.prototype,{set:function(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(a){return this._months[a.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(a){return this._monthsShort[a.month()]},monthsParse:function(a){var b,c,d;for(this._monthsParse||(this._monthsParse=[]),b=0;12>b;b++)if(this._monthsParse[b]||(c=L.utc([2e3,b]),d="^"+this.months(c,"")+"|^"+this.monthsShort(c,""),this._monthsParse[b]=new RegExp(d.replace(".",""),"i")),this._monthsParse[b].test(a))return b},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(a){return this._weekdays[a.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(a){return this._weekdaysShort[a.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(a){return this._weekdaysMin[a.day()]},weekdaysParse:function(a){var b,c,d;for(this._weekdaysParse||(this._weekdaysParse=[]),b=0;7>b;b++)if(this._weekdaysParse[b]||(c=L([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b},isPM:function(a){return"p"===(a+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(a,b){var c=this._calendar[a];return"function"==typeof c?c.apply(b):c},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)},pastFuture:function(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)},ordinal:function(a){return this._ordinal.replace("%d",a)},_ordinal:"%d",preparse:function(a){return a},postformat:function(a){return a},week:function(a){return G(a,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6}}),L=function(a,b,c){return H({_i:a,_f:b,_l:c,_isUTC:!1})},L.utc=function(a,b,c){return H({_useUTC:!0,_isUTC:!0,_l:c,_i:a,_f:b}).utc()},L.unix=function(a){return L(1e3*a)},L.duration=function(a,b){var c,d,e=L.isDuration(a),g="number"==typeof a,h=e?a._input:g?{}:a,i=S.exec(a);return g?b?h[b]=a:h.milliseconds=a:i&&(c="-"===i[1]?-1:1,h={y:0,d:~~i[2]*c,h:~~i[3]*c,m:~~i[4]*c,s:~~i[5]*c,ms:~~i[6]*c}),d=new f(h),e&&a.hasOwnProperty("_lang")&&(d._lang=a._lang),d},L.version=N,L.defaultFormat=db,L.updateOffset=function(){},L.lang=function(a,b){return a?(a=a.toLowerCase(),a=a.replace("_","-"),b?n(a,b):null===b?(o(a),a="en"):P[a]||p(a),L.duration.fn._lang=L.fn._lang=p(a),void 0):L.fn._lang._abbr},L.langData=function(a){return a&&a._lang&&a._lang._abbr&&(a=a._lang._abbr),p(a)},L.isMoment=function(a){return a instanceof e},L.isDuration=function(a){return a instanceof f},g(L.fn=e.prototype,{clone:function(){return L(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){return s(L(this).utc(),"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var a=this;return[a.year(),a.month(),a.date(),a.hours(),a.minutes(),a.seconds(),a.milliseconds()]},isValid:function(){return null==this._isValid&&(this._isValid=this._a?!l(this._a,(this._isUTC?L.utc(this._a):L(this._a)).toArray()):!isNaN(this._d.getTime())),!!this._isValid},invalidAt:function(){var a,b=this._a,c=(this._isUTC?L.utc(this._a):L(this._a)).toArray();for(a=6;a>=0&&b[a]===c[a];--a);return a},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(a){var b=s(this,a||L.defaultFormat);return this.lang().postformat(b)},add:function(a,b){var c;return c="string"==typeof a?L.duration(+b,a):L.duration(a,b),j(this,c,1),this},subtract:function(a,b){var c;return c="string"==typeof a?L.duration(+b,a):L.duration(a,b),j(this,c,-1),this},diff:function(a,b,c){var d,e,f=this._isUTC?L(a).zone(this._offset||0):L(a).local(),g=6e4*(this.zone()-f.zone());return b=m(b),"year"===b||"month"===b?(d=432e5*(this.daysInMonth()+f.daysInMonth()),e=12*(this.year()-f.year())+(this.month()-f.month()),e+=(this-L(this).startOf("month")-(f-L(f).startOf("month")))/d,e-=6e4*(this.zone()-L(this).startOf("month").zone()-(f.zone()-L(f).startOf("month").zone()))/d,"year"===b&&(e/=12)):(d=this-f,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-g)/864e5:"week"===b?(d-g)/6048e5:d),c?e:h(e)},from:function(a,b){return L.duration(this.diff(a)).lang(this.lang()._abbr).humanize(!b)},fromNow:function(a){return this.from(L(),a)},calendar:function(){var a=this.diff(L().zone(this.zone()).startOf("day"),"days",!0),b=-6>a?"sameElse":-1>a?"lastWeek":0>a?"lastDay":1>a?"sameDay":2>a?"nextDay":7>a?"nextWeek":"sameElse";return this.format(this.lang().calendar(b,this))},isLeapYear:function(){var a=this.year();return 0===a%4&&0!==a%100||0===a%400},isDST:function(){return this.zone()<this.clone().month(0).zone()||this.zone()<this.clone().month(5).zone()},day:function(a){var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?"string"==typeof a&&(a=this.lang().weekdaysParse(a),"number"!=typeof a)?this:this.add({d:a-b}):b},month:function(a){var b,c=this._isUTC?"UTC":"";return null!=a?"string"==typeof a&&(a=this.lang().monthsParse(a),"number"!=typeof a)?this:(b=this.date(),this.date(1),this._d["set"+c+"Month"](a),this.date(Math.min(b,this.daysInMonth())),L.updateOffset(this),this):this._d["get"+c+"Month"]()},startOf:function(a){switch(a=m(a)){case"year":this.month(0);case"month":this.date(1);case"week":case"isoweek":case"day":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return"week"===a?this.weekday(0):"isoweek"===a&&this.isoWeekday(1),this},endOf:function(a){return a=m(a),this.startOf(a).add("isoweek"===a?"week":a,1).subtract("ms",1)},isAfter:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)>+L(a).startOf(b)},isBefore:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)<+L(a).startOf(b)},isSame:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)===+L(a).startOf(b)},min:function(a){return a=L.apply(null,arguments),this>a?this:a},max:function(a){return a=L.apply(null,arguments),a>this?this:a},zone:function(a){var b=this._offset||0;return null==a?this._isUTC?b:this._d.getTimezoneOffset():("string"==typeof a&&(a=v(a)),Math.abs(a)<16&&(a=60*a),this._offset=a,this._isUTC=!0,b!==a&&j(this,L.duration(b-a,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},hasAlignedHourOffset:function(a){return a=a?L(a).zone():0,0===(this.zone()-a)%60},daysInMonth:function(){return L.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(a){var b=O((L(this).startOf("day")-L(this).startOf("year"))/864e5)+1;return null==a?b:this.add("d",a-b)},weekYear:function(a){var b=G(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==a?b:this.add("y",a-b)},isoWeekYear:function(a){var b=G(this,1,4).year;return null==a?b:this.add("y",a-b)},week:function(a){var b=this.lang().week(this);return null==a?b:this.add("d",7*(a-b))},isoWeek:function(a){var b=G(this,1,4).week;return null==a?b:this.add("d",7*(a-b))},weekday:function(a){var b=(this._d.getDay()+7-this.lang()._week.dow)%7;return null==a?b:this.add("d",a-b)},isoWeekday:function(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)},get:function(a){return a=m(a),this[a.toLowerCase()]()},set:function(a,b){a=m(a),this[a.toLowerCase()](b)},lang:function(b){return b===a?this._lang:(this._lang=p(b),this)}}),M=0;M<gb.length;M++)I(gb[M].toLowerCase().replace(/s$/,""),gb[M]);I("year","FullYear"),L.fn.days=L.fn.day,L.fn.months=L.fn.month,L.fn.weeks=L.fn.week,L.fn.isoWeeks=L.fn.isoWeek,L.fn.toJSON=L.fn.toISOString,g(L.duration.fn=f.prototype,{_bubble:function(){var a,b,c,d,e=this._milliseconds,f=this._days,g=this._months,i=this._data;i.milliseconds=e%1e3,a=h(e/1e3),i.seconds=a%60,b=h(a/60),i.minutes=b%60,c=h(b/60),i.hours=c%24,f+=h(c/24),i.days=f%30,g+=h(f/30),i.months=g%12,d=h(g/12),i.years=d},weeks:function(){return h(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+2592e6*(this._months%12)+31536e6*~~(this._months/12)},humanize:function(a){var b=+this,c=F(b,!a,this.lang());return a&&(c=this.lang().pastFuture(b,c)),this.lang().postformat(c)},add:function(a,b){var c=L.duration(a,b);return this._milliseconds+=c._milliseconds,this._days+=c._days,this._months+=c._months,this._bubble(),this},subtract:function(a,b){var c=L.duration(a,b);return this._milliseconds-=c._milliseconds,this._days-=c._days,this._months-=c._months,this._bubble(),this},get:function(a){return a=m(a),this[a.toLowerCase()+"s"]()},as:function(a){return a=m(a),this["as"+a.charAt(0).toUpperCase()+a.slice(1)+"s"]()},lang:L.fn.lang});for(M in hb)hb.hasOwnProperty(M)&&(K(M,hb[M]),J(M.toLowerCase()));K("Weeks",6048e5),L.duration.fn.asMonths=function(){return(+this-31536e6*this.years())/2592e6+12*this.years()},L.lang("en",{ordinal:function(a){var b=a%10,c=1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),Q&&(module.exports=L),"undefined"==typeof ender&&(this.moment=L),"function"==typeof define&&define.amd&&define("moment",[],function(){return L})}).call(this); \ No newline at end of file diff --git a/static/js/test/agenda_funcs.js b/static/js/test/agenda_funcs.js new file mode 100644 index 000000000..cdce50716 --- /dev/null +++ b/static/js/test/agenda_funcs.js @@ -0,0 +1,316 @@ +// globals needed for tests cases. +var agenda_globals; +var scheduledsession_post_href = "/test/agenda_ui.html"; +var read_only = false; +var days = []; + +function reset_globals() { + // hack to reach in and manipulate global specifically. + window.agenda_globals = new AgendaGlobals(); +} + +function three_by_eight_grid() { + var rooms = ["apple", "orange", "grape", "pineapple", + "tomato","squash", "raisin","cucumber" ]; + var times = [ + {"time":"0900", "date":"2013-12-02"}, + {"time":"1300", "date":"2013-12-02"}, + {"time":"0900", "date":"2013-12-03"} + ]; + var slots = [{}]; + var slotid= 1; + + days.push("2013-12-02"); + days.push("2013-12-03"); + + for(var roomkey in rooms) { + var room = rooms[roomkey]; + for(var timekey in times) { + var time = times[timekey]; + //console.log("data", room, time.date, time.time); + slot = make_timeslot({"timeslot_id": slotid, + "room" : room, + "roomtype" : "session", + "date" : time.date, + "time" : time.time, + "domid": "room" + roomkey + "_" + time.date + "_" + time.time + }); + slots[slotid] = slot; + slotid += 1; + } + } + return slots; +} + +function make_6_sessions() { + monarchs = ["henry", "george", "richard", "victoria", "william", "elizabeth"]; + $.each(monarchs, function(index) { + monarch = monarchs[index]; + console.log("monarch", monarch); + var group = create_group_by_href("http://localhost:8000/group/"+monarch+".json"); + group.acronym = monarch; + group.name = "Royalty fun" + monarch; + group.type = "wg"; + group.group_id = 1 + }); + + var sessions = {}; + var sessionid = 1; + monarch = "henry"; + sessions[monarch] = + session_obj({"title": monarch, + "description": "Henry Beauclerc", + "session_id": sessionid, + "attendees": 50, + "short_name": monarch, + "comments": "Long Live the King!", + "special_request": "", + "requested_time": "2013-11-27", + "requested_by": "Pope Francis", + "requested_duration": "1.0", + "area" : "TSV", + "group_href": "http://localhost:8000/group/"+monarch+".json" + }); + sessionid += 1; + + monarch = "george"; + sessions[monarch] = + session_obj({"title": monarch, + "description": "Georg Ludwig", + "session_id": sessionid, + "attendees": 60, + "short_name": monarch, + "comments": "Long Live the King!", + "special_request": "", + "requested_time": "2013-11-27", + "requested_by": "Pope Bacon", + "requested_duration": "1.5", + "area" : "SEC", + "group_href": "http://localhost:8000/group/"+monarch+".json" + }); + sessionid += 1; + + monarch = "richard"; + sessions[monarch] = + session_obj({"title": monarch, + "description": "Richard the Lionheart", + "session_id": sessionid, + "attendees": 70, + "short_name": monarch, + "comments": "Lion Hart!", + "special_request": "", + "requested_time": "2013-11-27", + "requested_by": "Robin Hood", + "requested_duration": "2.0", + "area" : "RTG", + "group_href": "http://localhost:8000/group/"+monarch+".json" + }); + sessionid += 1; + + monarch = "victoria"; + sessions[monarch] = + session_obj({"title": monarch, + "description": "the grandmother of Europe", + "session_id": sessionid, + "attendees": 80, + "short_name": monarch, + "comments": "Long Live the Queen!", + "special_request": "", + "requested_time": "2013-11-27", + "requested_by": "Docter Who", + "requested_duration": "1.0", + "area" : "INT", + "group_href": "http://localhost:8000/group/"+monarch+".json" + }); + sessionid += 1; + + monarch = "william"; + sessions[monarch] = + session_obj({"title": monarch, + "description": "William the Conqueror", + "session_id": sessionid, + "attendees": 90, + "short_name": monarch, + "comments": "Just Married!", + "special_request": "", + "requested_time": "2013-11-27", + "requested_by": "Pope Francis", + "requested_duration": "2.5", + "area" : "RAI", + "group_href": "http://localhost:8000/group/"+monarch+".json" + }); + sessionid += 1; + + monarch = "elizabeth"; + sessions[monarch] = + session_obj({"title": monarch, + "session_id": sessionid, + "description": "Head of the Commonwealth", + "attendees": 100, + "short_name": monarch, + "comments": "Long Live the Queen!", + "special_request": "", + "requested_time": "2013-11-27", + "requested_by": "Margaret Thatcher", + "requested_duration": "1.0", + "area" : "GEN", + "group_href": "http://localhost:8000/group/"+monarch+".json" + }); + sessionid += 1; + + return sessions; +} + +function place_6_sessions(slots, sessions) { + var ss_id = 1; + make_ss({"scheduledsession_id": ss_id, + "timeslot_id": slots[3].timeslot_id, + "session_id": sessions["henry"].session_id}); + ss_id += 1; + make_ss({"scheduledsession_id": ss_id, + "timeslot_id": slots[20].timeslot_id, + "session_id": sessions["george"].session_id}); + ss_id += 1; + make_ss({"scheduledsession_id": ss_id, + "timeslot_id": slots[5].timeslot_id, + "session_id": sessions["richard"].session_id}); + ss_id += 1; + make_ss({"scheduledsession_id": ss_id, + "timeslot_id": slots[9].timeslot_id, + "session_id": sessions["victoria"].session_id}); + ss_id += 1; + make_ss({"scheduledsession_id": ss_id, + "timeslot_id": slots[13].timeslot_id, + "session_id": sessions["william"].session_id}); + // last session is unscheduled. +} + +function conflict_4_sessions(sessions) { + // fill in session constraints + + $.each(sessions, function(index) { + var session = sessions[index]; + + var deferred = $.Deferred(); + session.constraints_promise = deferred; + + // $.ajax has a success option. + deferred.success = function(func) { + deferred.done(function(obj) { + func(obj, "success", {}); + }); + }; + + deferred.resolve({}); + + session.fill_in_constraints([]); + find_and_populate_conflicts(session); + }); + + sessions["henry"].fill_in_constraints([ + { "constraint_id": 21046, + "href": "http://localhost:8000/meeting/83/constraint/21046.json", + "meeting_href": "http://localhost:8000/meeting/83.json", + "name": "conflict", + "source_href": "http://localhost:8000/group/henry.json", + "target_href": "http://localhost:8000/group/george.json" + }, + { "constraint_id": 21047, + "href": "http://localhost:8000/meeting/83/constraint/21047.json", + "meeting_href": "http://localhost:8000/meeting/83.json", + "name": "conflic2", + "source_href": "http://localhost:8000/group/henry.json", + "target_href": "http://localhost:8000/group/richard.json" + }]); + find_and_populate_conflicts(sessions["henry"]); +} + + +function full_83_setup() { + reset_globals(); + scheduledsession_post_href = "/meeting/83/schedule/mtg_83/sessions.json"; + var ts_promise = load_timeslots("/meeting/83/timeslots.json"); + var session_promise = load_sessions("/meeting/83/sessions.json"); + var ss_promise = load_scheduledsessions(ts_promise, session_promise, + scheduledsession_post_href) + return ss_promise; +} + +function henry_setup(sessions) { + reset_globals(); + + /* define a slot for unscheduled items */ + var unassigned = new ScheduledSlot(); + unassigned.make_unassigned(); + + t_slots = three_by_eight_grid(); + t_sessions = make_6_sessions(); + place_6_sessions(t_slots, t_sessions); + conflict_4_sessions(t_sessions); + + load_events(); + + var henry0 = agenda_globals.sessions_objs["henry"]; + var henry = henry0[0]; + + return henry; +} + +var ss_id_next = 999; +function mock_scheduledslot_id(json) { + if(json.scheduledsession_id == undefined) { + console.log("adding scheduledsession_id to answer", ss_id_next); + ss_id_next += 1; + json.scheduledsession_id = ss_id_next; + } +}; + +ScheduledSlot.prototype.initialize = function(json) { + mock_scheduledslot_id(json); + this.real_initialize(json); +} + +function mock_ui_draggable() { + // mock up the ui object. + var ui = new Object(); + ui.draggable = new Object(); + ui.draggable.remove = function() { return true; }; + + return ui; +} + +function mock_dom_obj(domid) { + // mock up the dom object + var dom_obj = "#" + domid; + + // in the unit tests, the object won't exist, so make it. + // when testing this test code, it might already be there + if($(dom_obj).length == 0) { + var div = document.createElement("div"); + div.innerHTML = "welcome"; + div.id = dom_obj; + } + return dom_obj; +} + +function richard_move() { + var richard0 = agenda_globals.sessions_objs["richard"]; + var richard = richard0[0]; + + var ui = mock_ui_draggable(); + var dom_obj = mock_dom_obj(t_slots[4].domid); + + /* current situation was tested in above test, so go ahead */ + /* and move "richard" to another slot */ + move_slot({"session": richard, + "to_slot_id": t_slots[4].domid, + "to_slot": t_slots[4], + "from_slot_id":t_slots[5].domid, + "from_slot": [t_slots[5]], + "bucket_list": false, + "ui": ui, + "dom_obj": dom_obj, + "force": true}); + + return richard; +} diff --git a/static/js/test/agenda_tests.js b/static/js/test/agenda_tests.js new file mode 100644 index 000000000..c1d70d475 --- /dev/null +++ b/static/js/test/agenda_tests.js @@ -0,0 +1,370 @@ +test( "hello test", function() { + ok( 1 == "1", "Passed!" ); +}); + +test( "TimeSlot Create test", function() { + reset_globals(); + var nts = make_timeslot({"timeslot_id":"123", + "room" :"Regency A", + "time" :"0900", + "date" :"2013-11-04", + "domid" :"regencya_2013-11-04_0900"}); + equal(nts.slot_title(), "id#123 dom:regencya_2013-11-04_0900", "slot_title correct"); +}); + +asyncTest("Load Timeslots", function() { + reset_globals(); + expect( 1 ); // expect one assertion. + + var ts_promise = load_timeslots("/meeting/83/timeslots.json"); + ts_promise.done(function() { + equal(Object.keys(agenda_globals.timeslot_byid).length, 179, "179 timeslots loaded"); + start(); + }); +}); + +asyncTest("Load Sessions", function() { + reset_globals(); + expect( 1 ); // expect one assertion. + + var session_promise = load_sessions("/meeting/83/sessions.json"); + session_promise.done(function() { + equal(Object.keys(agenda_globals.meeting_objs).length, 145, "145 sessions loaded"); + start(); + }); +}); + +asyncTest("Load ScheduledSlot (ticket 1210)", function() { + expect( 1 ); // expect one assertion. + + var ss_promise = full_83_setup(); + ss_promise.done(function() { + equal(Object.keys(agenda_globals.slot_objs).length, 148, "148 scheduled sessions loaded"); + start(); + }); +}); + +asyncTest( "move a session using the API (ticket 1211)", function() { + expect(4); + + var ss_promise = full_83_setup(); + ss_promise.done(function() { + equal(Object.keys(agenda_globals.slot_objs).length, 148, "148 scheduled sessions loaded"); + + // now move a session.. like selenium test, move forced from Monday to Friday: + // monday_room_253 = is #room208_2012-03-26_1510 + // friday_room_252A = is #room209_2012-03-30_1230 + + var forces_list = agenda_globals.sessions_objs["forces"]; + var forces = forces_list[0]; + var from_slot_id = "room208_2012-03-26_1510"; + var from_slot = agenda_globals.timeslot_bydomid[from_slot_id]; + var to_slot_id = "room209_2012-03-30_1230"; + var to_slot = agenda_globals.timeslot_bydomid[to_slot_id]; + var ui = mock_ui_draggable(); + var dom_obj = "#" + to_slot_id; + + /* current situation was tested in above test, so go ahead */ + /* and move "richard" to another slot */ + + var move_promise = move_slot({"session": forces, + "to_slot_id": to_slot_id, + "to_slot": to_slot, + "from_slot_id":from_slot_id, + "from_slot": [from_slot], + "bucket_list": false, + "ui": ui, + "dom_obj": dom_obj, + "force": true}); + notEqual(move_promise, undefined); + + if(move_promise != undefined) { + // now we need to check that it is all been done. + move_promise.done(function() { + // see that the placed is right. + equal(forces.slot.domid, to_slot_id); + + // now move the item back again. + + var return_promise = move_slot({"session": forces, + "to_slot_id": from_slot_id, + "to_slot": from_slot, + "from_slot_id":to_slot_id, + "from_slot": [to_slot], + "bucket_list": false, + "ui": ui, + "dom_obj": dom_obj, + "force": true}); + + return_promise.done(function() { + // see that the placed is right. + equal(forces.slot.domid, from_slot_id); + start(); + }); + }); + } else { + // it is not legitimate to wind up here, but it does + // keep the test cases from hanging. + start(); + } + }); +}); + +test( "3x8 grid create (ticket 1212 - part 1)", function() { + expect(0); // just make sure things run without error + reset_globals(); + + t_slots = three_by_eight_grid(); + t_sessions = make_6_sessions(); + place_6_sessions(t_slots, t_sessions); +}); + +test( "calculate conflict columns for henry (ticket 1212 - part 2)", function() { + expect(10); + + scheduledsession_post_href = "/test/agenda_ui.html"; + + var henry = henry_setup(); + equal(henry.session_id, 1); + + equal(henry.column_class_list.length, 1); + equal(henry.column_class_list[0].room, "apple"); + equal(henry.column_class_list[0].time, "0900"); + equal(henry.column_class_list[0].date, "2013-12-03"); + + equal(henry.conflicts.length, 2); + + var conflict0 = henry.conflicts[0]; + equal(conflict0.conflict_groupP(), true); + + var classes = conflict0.column_class_list(); + var cc00 = classes[0]; + equal(cc00.th_tag, ".day_2013-12-02-1300"); + + var conflict1 = henry.conflicts[1]; + equal(conflict1.conflict_groupP(), true); + + var classes = conflict1.column_class_list(); + var cc10 = classes[0]; + equal(cc10.th_tag, ".day_2013-12-02-1300"); +}); + +test( "re-calculate conflict columns for henry (ticket 1213)", function() { + expect(5); + reset_globals(); + + scheduledsession_post_href = "/test/agenda_ui.html"; + agenda_globals.__debug_session_move = true; + + var henry = henry_setup(); + equal(henry.session_id, 1); + + var richard = richard_move(); + var conflict0 = henry.conflicts[0]; + equal(conflict0.conflict_groupP(), true); + + var classes = conflict0.column_class_list(); + var cc00 = classes[0]; + equal(cc00.th_tag, ".day_2013-12-02-1300"); + + var conflict1 = henry.conflicts[1]; + equal(conflict1.conflict_groupP(), true); + + var classes = conflict1.column_class_list(); + var cc10 = classes[0]; + equal(cc10.th_tag, ".day_2013-12-02-0900"); +}); + +test( "build WG template for regular group (ticket #1135)", function() { + reset_globals(); + var nts = make_timeslot({"timeslot_id":"123", + "room" :"Regency A", + "time" :"0900", + "date" :"2013-11-04", + "domid" :"regencya_2013-11-04_0900"}); + + // this is from http://localhost:8000/meeting/83/session/2157.json + var group1 = session_obj( + { + "agenda_note": "", + "area": "SEC", + "attendees": "45", + "bof": "False", + "comments": "please, no evening sessions.", + "description": "Public-Key Infrastructure (X.509)", + "group": { + "acronym": "pkix", + "ad_href": "http://localhost:8000/person/19483.json", + "comments": "1st met, 34th IETF Dallas, TX (December 4-8, 1995)", + "href": "http://localhost:8000/group/pkix.json", + "list_archive": "http://www.ietf.org/mail-archive/web/pkix/", + "list_email": "pkix@ietf.org", + "list_subscribe": "pkix-request@ietf.org", + "name": "Public-Key Infrastructure (X.509)", + "parent_href": "http://localhost:8000/group/sec.json", + "state": "active", + "type": "wg" + }, + "group_acronym": "pkix", + "group_href": "http://localhost:8000/group/pkix.json", + "group_id": "1223", + "href": "http://localhost:8000/meeting/83/session/2157.json", + "name": "", + "requested_by": "Stephen Kent", + "requested_duration": "2.0", + "requested_time": "2011-12-19", + "session_id": "2157", + "short_name": "pkix", + "special_request": "*", + "status": "Scheduled", + "title": "pkix" + }); + + // validate that the session id is there as a basic check. + ok(group1.event_template().search(/meeting_box_container/) > 0); + ok(group1.event_template().search(/session_2157/) > 0); + ok(group1.event_template().search(/wg_style /) > 0); +}); + +test( "build WG template for BOF group (ticket #1135)", function() { + reset_globals(); + + // this is from http://localhost:8000/meeting/83/session/2157.json + var group1 = session_obj( + { + "agenda_note": "", + "area": "GEN", + "attendees": "50", + "bof": "True", + "comments": "", + "description": "RFC Format", + "group": { + "acronym": "rfcform", + "ad_href": "http://localhost:8000/person/5376.json", + "comments": "", + "href": "http://localhost:8000/group/rfcform.json", + "list_archive": "", + "list_email": "", + "list_subscribe": "", + "name": "RFC Format", + "parent_href": "http://localhost:8000/group/gen.json", + "state": "bof", + "type": "wg" + }, + "group_acronym": "rfcform", + "group_href": "http://localhost:8000/group/rfcform.json", + "group_id": "1845", + "href": "http://localhost:8000/meeting/83/session/22081.json", + "name": "", + "requested_by": "Wanda Lo", + "requested_duration": "1.0", + "requested_time": "2012-02-27", + "session_id": "22081", + "short_name": "rfcform", + "special_request": "", + "status": "Scheduled", + "title": "rfcform" + } + ); + + // validate that the session id is there as a basic check. + ok(group1.event_template().search(/meeting_box_container/) > 0); + ok(group1.event_template().search(/session_22081/) > 0); + ok(group1.event_template().search(/bof_style /) > 0); +}); + +test( "compare timeslots sanely (ticket #1135)", function() { + var timeSlotA = {"timeslot_id":2383, + "room":"243", + "day":"2012-03-26T00:00:00.000Z", + "starttime":1300}; + + var timeSlotB = {"timeslot_id":2389, + "room":"241", + "day":"2012-03-26T00:00:00.000Z", + "starttime":900}; + + var timeSlotC = {"timeslot_id":2381, + "room":"245A", + "day":"2012-03-26T00:00:00.000Z", + "starttime":1300}; + + var timeSlotD = {"timeslot_id":2382, + "room":"245A", + "day":"2012-03-27T00:00:00.000Z", + "starttime":1510}; + + // three have the same day + ok(timeSlotA.day == timeSlotB.day); + ok(timeSlotA.day == timeSlotC.day); + ok(timeSlotA.day < timeSlotD.day); + + // two have the same starttime + ok(timeSlotA.starttime == timeSlotC.starttime); + + // canonical order is B, A, C, D. + equal(compare_timeslot(timeSlotB, timeSlotA), -1, "B < A"); + equal(compare_timeslot(timeSlotA, timeSlotC), -1, "A < C"); + equal(compare_timeslot(timeSlotC, timeSlotD), -1, "C < D"); + equal(compare_timeslot(timeSlotB, timeSlotD), -1, "B < D"); + equal(compare_timeslot(timeSlotA, timeSlotD), -1, "A < D"); + +}); + +asyncTest( "calculate info_room_select box (ticket 1220/1214)", function() { + expect(3); + + var ss_promise = full_83_setup(); + + ss_promise.done(function() { + var box = calculate_room_select_box(); + + // this is a box which has no session, and therefore no ss. + // validate that calculate_name_select_box() provides all the timeslots + ok(box.search(/Mon, 1510, Maillot/) > 0); + ok(box.search(/undefined/) == -1); + + // this one crept in: it is breakfast! + ok(box.search(/Mon, 0800, Halle Maillot A/) == -1); + start(); + }); +}); + +asyncTest( "calculate info_group_select box (ticket 1214)", function() { + expect(1); + + var ss_promise = full_83_setup(); + + ss_promise.done(function() { + var box = calculate_name_select_box(); + + // the list of all of the groups. + // count the number of occurances of value= + var count = 0; + var valueloc = box.search(/value=/); + while(valueloc != -1) { + //console.log(count, "valueat",valueloc, "box contains", box); + count += 1; + // eat everything upto value=, and then a bit. + box = box.substring(valueloc+1); + valueloc = box.search(/value=/); + } + + // 145 WG and other requests that "can meet" + equal(count, 145); + start(); + }); +}); + +asyncTest( "look for an empty slot(ticket 1215)", function() { + expect(1); + + var ss_promise = full_83_setup(); + + ss_promise.done(function() { + target_session = agenda_globals.sessions_objs["pcp"][0]; + ok(find_empty_slot(target_session) != null); + start(); + }); + +}); diff --git a/static/secretariat/css/custom.css b/static/secretariat/css/custom.css index 07be32d83..bb78c87e3 100644 --- a/static/secretariat/css/custom.css +++ b/static/secretariat/css/custom.css @@ -608,6 +608,11 @@ tr.bg2 { background: #EEEEEE; } +tr.bg3 { + background: #DDDDDD; +} + + /* table#sessions-new-table td { padding: 2px; diff --git a/static/test/agenda_tests.html b/static/test/agenda_tests.html new file mode 100644 index 000000000..65727f86e --- /dev/null +++ b/static/test/agenda_tests.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Agenda JS Unit Testing</title> + <link rel="stylesheet" href="/css/lib/qunit-1.12.0.css"> +</head> +<body> + <div id="qunit"></div> + <div id="qunit-fixture"></div> + <script src="/js/lib/qunit-1.12.0.js"></script> + <script type="text/javascript" src="/js/lib/jquery-1.8.2.min.js"></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/minified/jquery-ui.custom.min.js'></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/minified/jquery.ui.widget.min.js'></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/minified/jquery.ui.droppable.min.js'></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/minified/jquery.ui.sortable.min.js'></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/minified/jquery.ui.accordion.min.js'></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/minified/jquery.ui.draggable.min.js'></script> +<script type='text/javascript' src='/js/spin/dist/spin.min.js'></script> + + <script src="/js/agenda/agenda_objects.js"></script> + <script src="/js/agenda/agenda_helpers.js"></script> + <script src="/js/agenda/agenda_listeners.js"></script> + <script src="/js/test/agenda_funcs.js"></script> + <script src="/js/test/agenda_tests.js"></script> +</body> +</html> diff --git a/static/test/agenda_ui.html b/static/test/agenda_ui.html new file mode 100644 index 000000000..6b041f101 --- /dev/null +++ b/static/test/agenda_ui.html @@ -0,0 +1,603 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Agenda JS Unit Testing</title> + <link rel="stylesheet" type="text/css" href="/css/yui/yui-20100305.css"></link> + <link rel="stylesheet" type="text/css" href="/css/base2.css"></link> +<style type="text/css"> +.APP-scheme, .meeting_event th.APP-scheme, #APP-groups, #selector-APP { color:#008; background-color: #eef } +.director-mark-APP { + border: 2px solid #008; + color:#008; + background-color: #eef +} + +.GEN-scheme, .meeting_event th.GEN-scheme, #GEN-groups, #selector-GEN { color:#080; background-color: #efe } +.director-mark-GEN { + border: 2px solid #080; + color:#080; + background-color: #efe +} + +.INT-scheme, .meeting_event th.INT-scheme, #INT-groups, #selector-INT { color:#088; background-color: #eff } +.director-mark-INT { + border: 2px solid #088; + color:#088; + background-color: #eff +} + +.IRTF-scheme, .meeting_event th.IRTF-scheme, #IRTF-groups, #selector-IRTF { color:#448; background-color: #ddf } +.director-mark-IRTF { + border: 2px solid #448; + color:#448; + background-color: #ddf +} + +.OPS-scheme, .meeting_event th.OPS-scheme, #OPS-groups, #selector-OPS { color:#800; background-color: #fee } +.director-mark-OPS { + border: 2px solid #800; + color:#800; + background-color: #fee +} + +.RAI-scheme, .meeting_event th.RAI-scheme, #RAI-groups, #selector-RAI { color:#808; background-color: #fef } +.director-mark-RAI { + border: 2px solid #808; + color:#808; + background-color: #fef +} + +.RTG-scheme, .meeting_event th.RTG-scheme, #RTG-groups, #selector-RTG { color:#880; background-color: #ffe } +.director-mark-RTG { + border: 2px solid #880; + color:#880; + background-color: #ffe +} + +.SEC-scheme, .meeting_event th.SEC-scheme, #SEC-groups, #selector-SEC { color:#488; background-color: #dff } +.director-mark-SEC { + border: 2px solid #488; + color:#488; + background-color: #dff +} + +.TSV-scheme, .meeting_event th.TSV-scheme, #TSV-groups, #selector-TSV { color:#484; background-color: #dfd } +.director-mark-TSV { + border: 2px solid #484; + color:#484; + background-color: #dfd +} + + +</style> + +<link rel='stylesheet' type='text/css' href='/css/jquery-ui-themes/jquery-ui-1.8.11.custom.css' /> +<link rel="stylesheet" type="text/css" href="/css/base2.css"></link> +<link rel='stylesheet' type='text/css' href='/css/agenda.css' /> + +</head> +<body> + <div id="qunit"></div> + <div id="qunit-fixture"></div> + <script type="text/javascript" src="/js/lib/jquery-1.8.2.min.js"></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/minified/jquery-ui.custom.min.js'></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/minified/jquery.ui.widget.min.js'></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/minified/jquery.ui.droppable.min.js'></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/minified/jquery.ui.sortable.min.js'></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/minified/jquery.ui.accordion.min.js'></script> +<script type='text/javascript' src='/js/jquery-ui-1.9.0.custom/minified/jquery.ui.draggable.min.js'></script> +<script type='text/javascript' src='/js/spin/dist/spin.min.js'></script> + +<div id="unassigned-items"> + <div id="all_agendas" class="events_bar_buttons"> + <a href="/meeting/83/agendas/edit"> + <button class="styled_button">all agendas</button> + </a> + </div> + <div id="hidden_room" class="hide_buttons events_bar_buttons"> + <div class="very_small left">hidden rooms:<span id="hidden_rooms" >0/11</span></div> + <div><button class="small_button" id="show_hidden_rooms">Show</button></div> + </div> + <div id="hidden_day" class="hide_buttons events_bar_buttons"> + <div class="very_small left">hidden days:<span id="hidden_days" >0/7</span></div> + <div><button class="small_button" id="show_hidden_days">Show</button></div> + </div> + + </div> + + <div id="unassigned_order" class="events_bar_buttons"> + <select id="unassigned_sort_button" class="dialog"> + <option id="unassigned_alpha" value="alphaname" selected>Alphabetical</option> + <option id="unassigned_area" value="area">By Area</option> + <option id="unassigned_duration" value="duration">By Duration</option> + <option id="unassigned_special" value="special">Special Request</option> + </select> + </div> + <div class="agenda_slot_title" > + <div style="ui-icon ui-icon-arrow-1-w" id="close_ietf_menubar"> + < + </div> + <b>Unassigned Events:</b> + <span id="schedule_name">name: mtg_83</span> + </div> + <div id="sortable-list" class="ui-droppable bucket-list room_title"> + </div> +</div> + +<div class="agenda_div"> + + +<div id="dialog-confirm" title="" style="display:none"> + <p> + <span class="ui-icon ui-icon-alert" style="background: white; float: left; margin: 0 7px 20px 0;"></span> + Are you sure you want to put two sessions into the same slot? + </p> +</div> + +<div id="can-extend-dialog" title="" class="ui-dialog dialog" style="display:none"> +</div> + +<div id="can-not-extend-dialog" title="" class="ui-dialog dialog" style="display:none"> + <p> + <span class="ui-icon ui-icon-alert" style="background: white; float: left; margin: 0 7px 20px 0;"></span> + You can not extend this session. The slot is not available. + </p> +</div> + + +<div id="dialog-confirm" title="" style="display:none"> + <p> + <span class="ui-icon ui-icon-alert" style="background: white; float: left; margin: 0 7px 20px 0;"></span> + Are you sure you want to put two sessions into the same slot? + </p> +</div> + +<table id="meetings" class="ietf-navbar" style="width:100%"> +<tr> + <th class="schedule_title"><div id="spinner"><!-- spinney goes here --></div></th> + + <th colspan="2" id="2013-12-02-btn" class="day_2013-12-02 agenda_slot_title agenda_slot_unavailable"> + <div id="close_2013-12-02" class="close top_left very_small close_day">x</div> + Mon (2013-12-02) + </th> + <th class="day_2013-12-02 spacer 2013-12-02-spacer" id=""> + <div class="ui-widget-content ui-resizable" id="resize-2013-12-02-spacer"> + <div class="spacer_grip ui-resizable-handle ui-resizable-e"></div> + </div> + </th> + + <th colspan="1" id="2013-12-03-btn" class="day_2013-12-03 agenda_slot_title agenda_slot_unavailable"> + <div id="close_2013-12-03" class="close top_left very_small close_day">x</div> + Tue (2013-12-03) + + </th> + + <th class="day_2013-12-03 spacer 2013-12-03-spacer" id=""> + <div class="ui-widget-content ui-resizable" id="resize-2013-12-03-spacer"> + <div class="spacer_grip ui-resizable-handle ui-resizable-e"></div> + </div> + </th> +</tr> + + + <tr> + <th class="th_column"><button id="show_all_button" class="styled_button">show all</button></th> + <th class="day_2013-12-02-0900 day_2013-12-02 room_title ">0900-1130 </th> + <th class="day_2013-12-02-1300 day_2013-12-02 room_title ">1300-1500 </th> + + <th class="day_2013-12-02 spacer 2013-12-02-spacer"></th> + + <th class="day_2013-12-03-0900 day_2013-12-03 room_title ">0900-1130 </th> + + <th class="day_2013-12-03 spacer 2013-12-03-spacer"></th> + </tr> + + + <tr id="room0" class="agenda_row_alt agenda_slot"> + <th class="vert_time"> + <div class="close very_small close_room top_left small_button" id="close_room0">X</div> + <div class="right room_name">apple <span class="capacity">(61)</span></div> + + <!-- <span class="hide_room light_blue_border">X</span><span class="left">212/213</span>--> + </th> + + + <td id="room0_2013-12-02_0900" class="day_2013-12-02 agenda-column-2013-12-02-0900 agenda_slot agenda_slot_unavailable" capacity="62" ></td> + + <td id="room0_2013-12-02_1300" class="day_2013-12-02 agenda-column-2013-12-02-1300 agenda_slot agenda_slot_unavailable" capacity="63" ></td> + + <td class="day_2013-12-02 spacer 2013-12-02-spacer"></td> + + <td id="room0_2013-12-03_0900" class="day_2013-12-03 agenda-column-2013-12-03-0900 agenda_slot agenda_slot_unavailable" capacity="64" ></td> + + </tr> + + <tr id="room1" class="agenda_row_alt agenda_slot"> + <th class="vert_time"> + <div class="close very_small close_room top_left small_button" id="close_room1">X</div> + <div class="right room_name">orange <span class="capacity">(70)</span></div> + + <!-- <span class="hide_room light_blue_border">X</span><span class="left">212/213</span>--> + </th> + + + <td id="room1_2013-12-02_0900" class="day_2013-12-02 agenda-column-2013-12-02-0900 agenda_slot agenda_slot_unavailable" capacity="71" ></td> + + <td id="room1_2013-12-02_1300" class="day_2013-12-02 agenda-column-2013-12-02-1300 agenda_slot agenda_slot_unavailable" capacity="72" ></td> + + <td class="day_2013-12-02 spacer 2013-12-02-spacer"></td> + + <td id="room1_2013-12-03_0900" class="day_2013-12-03 agenda-column-2013-12-03-0900 agenda_slot agenda_slot_unavailable" capacity="73" ></td> + + </tr> + + <tr id="room2" class="agenda_row_alt agenda_slot"> + <th class="vert_time"> + <div class="close very_small close_room top_left small_button" id="close_room2">X</div> + <div class="right room_name">grape <span class="capacity">(80)</span></div> + + <!-- <span class="hide_room light_blue_border">X</span><span class="left">212/213</span>--> + </th> + + + <td id="room2_2013-12-02_0900" class="day_2013-12-02 agenda-column-2013-12-02-0900 agenda_slot agenda_slot_unavailable" capacity="81" ></td> + + <td id="room2_2013-12-02_1300" class="day_2013-12-02 agenda-column-2013-12-02-1300 agenda_slot agenda_slot_unavailable" capacity="82" ></td> + + <td class="day_2013-12-02 spacer 2013-12-02-spacer"></td> + + <td id="room2_2013-12-03_0900" class="day_2013-12-03 agenda-column-2013-12-03-0900 agenda_slot agenda_slot_unavailable" capacity="83" ></td> + + </tr> + + <tr id="room3" class="agenda_row_alt agenda_slot"> + <th class="vert_time"> + <div class="close very_small close_room top_left small_button" id="close_room3">X</div> + <div class="right room_name">pineapple <span class="capacity">(90)</span></div> + + <!-- <span class="hide_room light_blue_border">X</span><span class="left">212/213</span>--> + </th> + + + <td id="room3_2013-12-02_0900" class="day_2013-12-02 agenda-column-2013-12-02-0900 agenda_slot agenda_slot_unavailable" capacity="91" ></td> + + <td id="room3_2013-12-02_1300" class="day_2013-12-02 agenda-column-2013-12-02-1300 agenda_slot agenda_slot_unavailable" capacity="92" ></td> + + <td class="day_2013-12-02 spacer 2013-12-02-spacer"></td> + + <td id="room3_2013-12-03_0900" class="day_2013-12-03 agenda-column-2013-12-03-0900 agenda_slot agenda_slot_unavailable" capacity="93" ></td> + + </tr> + + <tr id="room4" class="agenda_row_alt agenda_slot"> + <th class="vert_time"> + <div class="close very_small close_room top_left small_button" id="close_room4">X</div> + <div class="right room_name">tomato <span class="capacity">(100)</span></div> + + <!-- <span class="hide_room light_blue_border">X</span><span class="left">212/213</span>--> + </th> + + + <td id="room4_2013-12-02_0900" class="day_2013-12-02 agenda-column-2013-12-02-0900 agenda_slot agenda_slot_unavailable" capacity="101" ></td> + + <td id="room4_2013-12-02_1300" class="day_2013-12-02 agenda-column-2013-12-02-1300 agenda_slot agenda_slot_unavailable" capacity="102" ></td> + + <td class="day_2013-12-02 spacer 2013-12-02-spacer"></td> + + <td id="room4_2013-12-03_0900" class="day_2013-12-03 agenda-column-2013-12-03-0900 agenda_slot agenda_slot_unavailable" capacity="103" ></td> + + </tr> + + <tr id="room5" class="agenda_row_alt agenda_slot"> + <th class="vert_time"> + <div class="close very_small close_room top_left small_button" id="close_room5">X</div> + <div class="right room_name">squash <span class="capacity">(110)</span></div> + + <!-- <span class="hide_room light_blue_border">X</span><span class="left">212/213</span>--> + </th> + + + <td id="room5_2013-12-02_0900" class="day_2013-12-02 agenda-column-2013-12-02-0900 agenda_slot agenda_slot_unavailable" capacity="111" ></td> + + <td id="room5_2013-12-02_1300" class="day_2013-12-02 agenda-column-2013-12-02-1300 agenda_slot agenda_slot_unavailable" capacity="112" ></td> + + <td class="day_2013-12-02 spacer 2013-12-02-spacer"></td> + + <td id="room5_2013-12-03_0900" class="day_2013-12-03 agenda-column-2013-12-03-0900 agenda_slot agenda_slot_unavailable" capacity="113" ></td> + + </tr> + + <tr id="room6" class="agenda_row_alt agenda_slot"> + <th class="vert_time"> + <div class="close very_small close_room top_left small_button" id="close_room6">X</div> + <div class="right room_name">raisin <span class="capacity">(120)</span></div> + + <!-- <span class="hide_room light_blue_border">X</span><span class="left">212/213</span>--> + </th> + + + <td id="room6_2013-12-02_0900" class="day_2013-12-02 agenda-column-2013-12-02-0900 agenda_slot agenda_slot_unavailable" capacity="121" ></td> + + <td id="room6_2013-12-02_1300" class="day_2013-12-02 agenda-column-2013-12-02-1300 agenda_slot agenda_slot_unavailable" capacity="122" ></td> + + <td class="day_2013-12-02 spacer 2013-12-02-spacer"></td> + + <td id="room6_2013-12-03_0900" class="day_2013-12-03 agenda-column-2013-12-03-0900 agenda_slot agenda_slot_unavailable" capacity="123" ></td> + + </tr> + + <tr id="room7" class="agenda_row_alt agenda_slot"> + <th class="vert_time"> + <div class="close very_small close_room top_left small_button" id="close_room7">X</div> + <div class="right room_name">cucumber <span class="capacity">(130)</span></div> + + <!-- <span class="hide_room light_blue_border">X</span><span class="left">212/213</span>--> + </th> + + + <td id="room7_2013-12-02_0900" class="day_2013-12-02 agenda-column-2013-12-02-0900 agenda_slot agenda_slot_unavailable" capacity="131" ></td> + + <td id="room7_2013-12-02_1300" class="day_2013-12-02 agenda-column-2013-12-02-1300 agenda_slot agenda_slot_unavailable" capacity="132" ></td> + + <td class="day_2013-12-02 spacer 2013-12-02-spacer"></td> + + <td id="room7_2013-12-03_0900" class="day_2013-12-03 agenda-column-2013-12-03-0900 agenda_slot agenda_slot_unavailable" capacity="133" ></td> + + </tr> + +</table> + + + +</div> + + +<div id="session-info" class="ui-droppable bucket-list room_title"> + <div class="agenda_slot_title"><b>Session Information:</b></div> + + <div class="ss_info_box"> + <div class="ss_info ss_info_left"> + <table> + <tr><td class="ss_info_name_short">Group:</td><td><span id="info_grp"></span> + <!-- <button id="agenda_sreq_button" class="right">Edit Request</button> --></tr> + <tr><td class="ss_info_name_short">Name:</td> <td id="info_name"></td></tr> + <tr><td class="ss_info_name_short">Area:</td> <td><span id="info_area"></span><button id="show_all_area" class="right">Show All</button></td></tr> + </table> + </div> + + <div class="ss_info ss_info_right"> + <table> + <tr><td class="ss_info_name_long">Duration/Capacity:</td><td class="info_split" id="info_duration"></td> <td class="info_split" id="info_capacity"></td></tr> + <tr><td class="ss_info_name_long">Location:</td><td colspan=2 id="info_location"></td></tr> + <tr><td class="ss_info_name_long">Responsible AD:</td><td colspan=2 id="info_responsible"></td></tr> + <tr><td class="ss_info_name_long">Requested By:</td><td colspan=2 id="info_requestedby"></td></tr> + </table> + </div> + + <div id="conflict_table"> + <div id="special_requests">Special Requests</div> + <table> + <tbody id="conflict_table_body"> + <tr class="conflict_list_row"> + <td class="conflict_list_title"> + Group conflicts + </td> + <td id="conflict_group_list"> + <ul> + </ul> + </td> + </tr> + <tr class="conflict_list_row"> + <td class="conflict_list_title"> + <b>be present</b> + </td> + <td id="conflict_people_list"> + <ul> + </ul> + </td> + </tr> + </tbody> + </table> + </div> + <div class="agenda_find_free"><button class="agenda_selected_buttons small_button" id="find_free">Find Free</button></div> + <div class="agenda_double_slot button_disabled"> + <button class="agenda_selected_buttons small_button" disabled + id="double_slot">Extend</button> + </div> + <div id="agenda_pin_slot" class="button_disabled"> + <button class="agenda_selected_buttons small_button" disabled + id="pin_slot">Pin</button> + </div> + <div class="color_legend"> + + <span class="APP-scheme"><input class='color_checkboxes' type="checkbox" id="APP" value="APP-value" checked>APP</span> + + <span class="GEN-scheme"><input class='color_checkboxes' type="checkbox" id="GEN" value="GEN-value" checked>GEN</span> + + <span class="INT-scheme"><input class='color_checkboxes' type="checkbox" id="INT" value="INT-value" checked>INT</span> + + <span class="IRTF-scheme"><input class='color_checkboxes' type="checkbox" id="IRTF" value="IRTF-value" checked>IRTF</span> + + <span class="OPS-scheme"><input class='color_checkboxes' type="checkbox" id="OPS" value="OPS-value" checked>OPS</span> + + <span class="RAI-scheme"><input class='color_checkboxes' type="checkbox" id="RAI" value="RAI-value" checked>RAI</span> + + <span class="RTG-scheme"><input class='color_checkboxes' type="checkbox" id="RTG" value="RTG-value" checked>RTG</span> + + <span class="SEC-scheme"><input class='color_checkboxes' type="checkbox" id="SEC" value="SEC-value" checked>SEC</span> + + <span class="TSV-scheme"><input class='color_checkboxes' type="checkbox" id="TSV" value="TSV-value" checked>TSV</span> + + </div> + </div> + + <div class="agenda_save_box"> + + <div id="agenda_title"><b>Agenda name: </b><span>mtg_83</span></div> + <div id="agenda_saveas"> + <form action="/meeting/83/schedule/mtg_83/edit" method="post"> + <p><label for="id_savename">Savename:</label> <input id="id_savename" type="text" name="savename" maxlength="100" /></p> + <input type="submit" name="saveas" value="saveas"> + </form> + </div> + </div> + + +</div> + <!-- <div class="ui-resizable-handle ui-resizable-se ui-icon ui-icon-gripsmall-diagonal-ew">*</div> --> +</div> + +<!-- some boxes for dialogues --> +<div id="dialog-confirm-two" title="" style="display:none"> + <p> + <span class="ui-icon ui-icon-alert" style="background: white; float: left; margin: 0 7px 20px 0;"></span> + <span class="dialog-confirm-text">Are you sure you want to put two sessions into the same slot?</span> + </p> +</div> + +<div id="dialog-confirm-toosmall" title="" style="display:none"> + <p> + <span class="ui-icon ui-icon-alert" style="background: white; float: left; margin: 0 7px 20px 0;"></span> + <span class="dialog-confirm-text">The room you are moving to has a lower + room capacity then the requested capacity,<br> + Are you sure you want to continue? + </span> + </p> +</div> + +<div id="dialog-confirm-twotoosmall" title="" style="display:none"> + <p> + <span class="ui-icon ui-icon-alert" style="background: white; float: left; margin: 0 7px 20px 0;"></span> + <span class="dialog-confirm-text"> + The slot you are moving to already has a session in it, <br> + the room is also smaller than the requested amount.<br> + Are you sure you want to continue? + </span> + </p> +</div> + +<div id="can-extend-dialog" title="" class="ui-dialog dialog" style="display:none"> +</div> + +<div id="can-not-extend-dialog" title="" class="ui-dialog dialog" style="display:none"> + <p> + <span class="ui-icon ui-icon-alert" style="background: white; float: left; margin: 0 7px 20px 0;"></span> + You can not extend this session. The slot is not available. + </p> +</div> + + + +<script type="text/javascript" src="/js/yui/yui-20100305.js"></script> +<script type="text/javascript"> +//<![CDATA[ +YAHOO.util.Event.onContentReady("wgs", function () { + var oMenu = new YAHOO.widget.Menu("wgs", { position: "static", hidedelay: 750, lazyload: true }); + oMenu.render(); +}); + + +//]]> +</script> + +<script src="/dajaxice/dajaxice.core.js" type="text/javascript" charset="utf-8"></script> + +<script type='text/javascript' src='/js/agenda/agenda_edit.js'></script> +<script type='text/javascript' src='/js/agenda/agenda_helpers.js'></script> +<script type='text/javascript' src='/js/agenda/agenda_objects.js'></script> +<script type='text/javascript' src='/js/agenda/agenda_listeners.js'></script> +<script type='text/javascript' src='/js/test/agenda_funcs.js'></script> + + + +<script type='text/javascript'> + + +__debug_conflict_calculate = true; + +var meeting_number = "83"; +var schedule_id = 24; +var schedule_owner_href = "wlo@amsl.com"; +var schedule_name = "83"; +var scheduledsession_post_href = "/test/agenda_ui.html"; +var meeting_base_url = "http://localhost:8000/meeting/83"; +var site_base_url = "http://localhost:8000"; +var total_days = 7; +var total_rooms = 11; + +function setup_slots(promiselist){ + days.push("2013-12-02"); + days.push("2013-12-03"); + +area_directors["app"] = []; +area_directors["app"] = []; +area_directors["gen"] = []; +area_directors["int"] = []; +area_directors["int"] = []; +area_directors["ops"] = []; +area_directors["ops"] = []; +area_directors["rai"] = []; +area_directors["rai"] = []; +area_directors["rtg"] = []; +area_directors["rtg"] = []; +area_directors["sec"] = []; +area_directors["sec"] = []; +area_directors["tsv"] = []; +area_directors["tsv"] = []; + +//area_directors["app"].push(find_person_by_href("http://knothole.gatineau.credil.org:8000/person/21684.json")); +//area_directors["app"].push(find_person_by_href("http://knothole.gatineau.credil.org:8000/person/18321.json")); +//area_directors["gen"].push(find_person_by_href("http://knothole.gatineau.credil.org:8000/person/21072.json")); +//area_directors["int"].push(find_person_by_href("http://knothole.gatineau.credil.org:8000/person/100664.json")); +//area_directors["int"].push(find_person_by_href("http://knothole.gatineau.credil.org:8000/person/20356.json")); +//area_directors["ops"].push(find_person_by_href("http://knothole.gatineau.credil.org:8000/person/105682.json")); +//area_directors["ops"].push(find_person_by_href("http://knothole.gatineau.credil.org:8000/person/20959.json")); +//area_directors["rai"].push(find_person_by_href("http://knothole.gatineau.credil.org:8000/person/103539.json")); +//area_directors["rai"].push(find_person_by_href("http://knothole.gatineau.credil.org:8000/person/108049.json")); +//area_directors["rtg"].push(find_person_by_href("http://knothole.gatineau.credil.org:8000/person/2329.json")); +//area_directors["rtg"].push(find_person_by_href("http://knothole.gatineau.credil.org:8000/person/104198.json")); +//area_directors["sec"].push(find_person_by_href("http://knothole.gatineau.credil.org:8000/person/19177.json")); +//area_directors["sec"].push(find_person_by_href("http://knothole.gatineau.credil.org:8000/person/19483.json")); +//area_directors["tsv"].push(find_person_by_href("http://knothole.gatineau.credil.org:8000/person/105519.json")); +//area_directors["tsv"].push(find_person_by_href("http://knothole.gatineau.credil.org:8000/person/107190.json")); + + t_slots = three_by_eight_grid(); + t_sessions = make_6_sessions(); + place_6_sessions(t_slots, t_sessions); + conflict_4_sessions(t_sessions); + + console.log("setup_slots run"); + + legend_status["APP"] = true; + legend_status["GEN"] = true; + legend_status["INT"] = true; + legend_status["IRTF"] = true; + legend_status["OPS"] = true; + legend_status["RAI"] = true; + legend_status["RTG"] = true; + legend_status["SEC"] = true; + legend_status["TSV"] = true; +} + + + + + +</script> +<style type='text/css'> + +</style> + + + + +<div id="ietf-extras"></div> + + + + +</body></html> + +</body> +</html> From 91d0261c9e2f16db7f2b57f901ea37754d7767fe Mon Sep 17 00:00:00 2001 From: Robert Sparks <rjsparks@nostrum.com> Date: Mon, 3 Feb 2014 15:43:22 +0000 Subject: [PATCH 3/5] Django 1.4 changed how to get to request entities. See <https://docs.djangoproject.com/en/dev/releases/1.4/#httprequest-raw-post-data-renamed-to-httprequest-body> - Legacy-Id: 7200 --- ietf/meeting/ajax.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/meeting/ajax.py b/ietf/meeting/ajax.py index de84aa811..1ded28ecd 100644 --- a/ietf/meeting/ajax.py +++ b/ietf/meeting/ajax.py @@ -486,7 +486,7 @@ def scheduledsessions_post(request, meeting, schedule): content_type="application/json") # get JSON out of raw body. XXX should check Content-Type! - newvalues = json.loads(request.raw_post_data) + newvalues = json.loads(request.body) if not ("session_id" in newvalues) or not ("timeslot_id" in newvalues): return HttpResponse(json.dumps({'error':'missing values, timeslot_id and session_id required'}), status = 406, From accbf3db720c1ac90601433d62075f72ed65e8bd Mon Sep 17 00:00:00 2001 From: Robert Sparks <rjsparks@nostrum.com> Date: Mon, 3 Feb 2014 21:15:28 +0000 Subject: [PATCH 4/5] Cleaned up some exceptions that were leading to quiet 500s when setting an official agenda. The secretariat code being reused expects querysets, not lists. Some objects needed to be saved before other functions tried to operate on them. - Legacy-Id: 7201 --- ietf/meeting/models.py | 6 +++--- ietf/secr/meetings/views.py | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 8c0364cca..4b182c206 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -208,9 +208,9 @@ class Meeting(models.Model): def set_official_agenda(self, agenda): if self.agenda != agenda: self.agenda = agenda + self.save() if self.agenda is not None: self.agenda.sendEmail() - self.save() class Meta: ordering = ["-date", ] @@ -612,9 +612,9 @@ class Schedule(models.Model): if session.status.slug == "schedw": session.status_id = "sched" session.scheduled = datetime.datetime.now() - import ietf.secr.meetings.views - ietf.secr.meetings.views.send_notification(None, [session]) session.save() + from ietf.secr.meetings.views import send_notification + send_notification(None, Session.objects.filter(id=session.id)) # to be renamed ScheduleTimeslotSessionAssignments (stsa) class ScheduledSession(models.Model): diff --git a/ietf/secr/meetings/views.py b/ietf/secr/meetings/views.py index 0e0fa7d1b..781c6a73a 100644 --- a/ietf/secr/meetings/views.py +++ b/ietf/secr/meetings/views.py @@ -172,7 +172,7 @@ def send_notification(request, sessions): to_email = sessions[0].requested_by.role_email('chair').address cc_list = get_cc_list(group, requestinguser) from_email = ('"IETF Secretariat"','agenda@ietf.org') - if sessions.count() == 1: + if len(sessions) == 1: subject = '%s - Requested session has been scheduled for IETF %s' % (group.acronym, sessions[0].meeting.number) else: subject = '%s - Requested sessions have been scheduled for IETF %s' % (group.acronym, sessions[0].meeting.number) @@ -198,6 +198,8 @@ def send_notification(request, sessions): context['agenda_note'] = sessions[0].agenda_note context['session'] = get_initial_session(sessions) context['session_info'] = session_info + context['group'] = group + context['login'] = sessions[0].requested_by send_mail(request, to_email, From 7c90ea5aa3a0a90498b7d7a418b1f91049ec22f3 Mon Sep 17 00:00:00 2001 From: Robert Sparks <rjsparks@nostrum.com> Date: Thu, 20 Feb 2014 21:50:37 +0000 Subject: [PATCH 5/5] Merged in Michael's update to newer dajaxice. Repaired the existing tests in trunk except for one. That one points to a real bug in the code - the access protection for the functions that change visible and public for an agenda are not right, leading to pages that can't work (see the failing test for more notes). This should be ready to start porting the updated tests into the make_test_data framework. - Legacy-Id: 7283 --- django-dajaxice/.travis.yml | 33 +++ django-dajaxice/AUTHORS | 18 ++ django-dajaxice/COPYING | 62 +++++ django-dajaxice/MANIFEST.in | 8 + django-dajaxice/README.rst | 19 ++ .../dajaxice}/__init__.py | 0 .../dajaxice}/core/Dajaxice.py | 0 .../dajaxice}/core/__init__.py | 0 .../dajaxice}/decorators.py | 0 .../dajaxice}/exceptions.py | 0 .../dajaxice}/finders.py | 0 .../dajaxice}/models.py | 0 .../templates/dajaxice/dajaxice.core.js | 0 .../dajaxice/dajaxice_function_loop.js | 0 .../dajaxice/dajaxice_module_loop.js | 0 .../dajaxice}/templatetags/__init__.py | 0 .../templatetags/dajaxice_templatetags.py | 0 .../dajaxice}/tests/__init__.py | 0 .../dajaxice}/tests/ajax.py | 14 +- .../dajaxice}/tests/requirements.txt | 0 .../dajaxice}/tests/urls.py | 0 .../dajaxice}/urls.py | 2 +- .../dajaxice}/utils.py | 0 .../dajaxice}/views.py | 6 +- django-dajaxice/docs/Makefile | 130 ++++++++++ django-dajaxice/docs/available-settings.rst | 38 +++ django-dajaxice/docs/changelog.rst | 124 ++++++++++ django-dajaxice/docs/conf.py | 224 ++++++++++++++++++ .../docs/custom-error-callbacks.rst | 31 +++ django-dajaxice/docs/index.rst | 45 ++++ django-dajaxice/docs/installation.rst | 92 +++++++ django-dajaxice/docs/migrating-to-05.rst | 55 +++++ .../docs/production-environment.rst | 7 + django-dajaxice/docs/quickstart.rst | 80 +++++++ django-dajaxice/docs/utils.rst | 16 ++ django-dajaxice/examples/examples/__init__.py | 0 django-dajaxice/examples/examples/settings.py | 177 ++++++++++++++ django-dajaxice/examples/examples/urls.py | 25 ++ django-dajaxice/examples/examples/wsgi.py | 28 +++ django-dajaxice/examples/manage.py | 10 + django-dajaxice/examples/simple/__init__.py | 0 django-dajaxice/examples/simple/ajax.py | 26 ++ django-dajaxice/examples/simple/models.py | 3 + .../simple/templates/simple/index.html | 13 + django-dajaxice/examples/simple/tests.py | 16 ++ django-dajaxice/examples/simple/views.py | 9 + django-dajaxice/setup.py | 31 +++ ietf/meeting/models.py | 2 +- ietf/meeting/test_data.py | 4 +- ietf/meeting/tests_api.py | 82 ++++--- ietf/meeting/tests_views.py | 2 +- ietf/secr/sreq/tests.py | 16 +- ietf/settings.py | 4 + 53 files changed, 1401 insertions(+), 51 deletions(-) create mode 100644 django-dajaxice/.travis.yml create mode 100644 django-dajaxice/AUTHORS create mode 100644 django-dajaxice/COPYING create mode 100644 django-dajaxice/MANIFEST.in create mode 100644 django-dajaxice/README.rst rename {dajaxice => django-dajaxice/dajaxice}/__init__.py (100%) rename {dajaxice => django-dajaxice/dajaxice}/core/Dajaxice.py (100%) rename {dajaxice => django-dajaxice/dajaxice}/core/__init__.py (100%) rename {dajaxice => django-dajaxice/dajaxice}/decorators.py (100%) rename {dajaxice => django-dajaxice/dajaxice}/exceptions.py (100%) rename {dajaxice => django-dajaxice/dajaxice}/finders.py (100%) rename {dajaxice => django-dajaxice/dajaxice}/models.py (100%) rename {dajaxice => django-dajaxice/dajaxice}/templates/dajaxice/dajaxice.core.js (100%) rename {dajaxice => django-dajaxice/dajaxice}/templates/dajaxice/dajaxice_function_loop.js (100%) rename {dajaxice => django-dajaxice/dajaxice}/templates/dajaxice/dajaxice_module_loop.js (100%) rename {dajaxice => django-dajaxice/dajaxice}/templatetags/__init__.py (100%) rename {dajaxice => django-dajaxice/dajaxice}/templatetags/dajaxice_templatetags.py (100%) rename {dajaxice => django-dajaxice/dajaxice}/tests/__init__.py (100%) rename {dajaxice => django-dajaxice/dajaxice}/tests/ajax.py (66%) rename {dajaxice => django-dajaxice/dajaxice}/tests/requirements.txt (100%) rename {dajaxice => django-dajaxice/dajaxice}/tests/urls.py (100%) rename {dajaxice => django-dajaxice/dajaxice}/urls.py (81%) rename {dajaxice => django-dajaxice/dajaxice}/utils.py (100%) rename {dajaxice => django-dajaxice/dajaxice}/views.py (92%) create mode 100644 django-dajaxice/docs/Makefile create mode 100644 django-dajaxice/docs/available-settings.rst create mode 100644 django-dajaxice/docs/changelog.rst create mode 100644 django-dajaxice/docs/conf.py create mode 100644 django-dajaxice/docs/custom-error-callbacks.rst create mode 100644 django-dajaxice/docs/index.rst create mode 100644 django-dajaxice/docs/installation.rst create mode 100644 django-dajaxice/docs/migrating-to-05.rst create mode 100644 django-dajaxice/docs/production-environment.rst create mode 100644 django-dajaxice/docs/quickstart.rst create mode 100644 django-dajaxice/docs/utils.rst create mode 100644 django-dajaxice/examples/examples/__init__.py create mode 100644 django-dajaxice/examples/examples/settings.py create mode 100644 django-dajaxice/examples/examples/urls.py create mode 100644 django-dajaxice/examples/examples/wsgi.py create mode 100644 django-dajaxice/examples/manage.py create mode 100644 django-dajaxice/examples/simple/__init__.py create mode 100644 django-dajaxice/examples/simple/ajax.py create mode 100644 django-dajaxice/examples/simple/models.py create mode 100644 django-dajaxice/examples/simple/templates/simple/index.html create mode 100644 django-dajaxice/examples/simple/tests.py create mode 100644 django-dajaxice/examples/simple/views.py create mode 100644 django-dajaxice/setup.py diff --git a/django-dajaxice/.travis.yml b/django-dajaxice/.travis.yml new file mode 100644 index 000000000..f8040fa4d --- /dev/null +++ b/django-dajaxice/.travis.yml @@ -0,0 +1,33 @@ +language: python +python: + - "2.6" + - "2.7" + - "3.2" + +env: + - DJANGO=django==1.3 + - DJANGO=django==1.4 + - DJANGO=https://github.com/django/django/tarball/stable/1.5.x + +install: + - pip install -q $DJANGO --use-mirrors + - pip install https://github.com/jorgebastida/django-app-test-runner/zipball/master + +script: + - app-test-runner dajaxice dajaxice + +branches: + only: + - master + - develop + +matrix: + exclude: + # Django < 1.5 not supported on python 3 + - python: "3.2" + env: DJANGO=django==1.3 + - python: "3.2" + env: DJANGO=django==1.4 + # Django 1.5 strongly recommends python 2.7 or later (so skip 2.6) + - python: "2.6" + env: DJANGO=https://github.com/django/django/tarball/stable/1.5.x diff --git a/django-dajaxice/AUTHORS b/django-dajaxice/AUTHORS new file mode 100644 index 000000000..7eb410402 --- /dev/null +++ b/django-dajaxice/AUTHORS @@ -0,0 +1,18 @@ +Glue is mainly developed and maintained by Jorge Bastida <me@jorgebastida.com> + +A big thanks to all the contributors: +Angel Abad for the Debian and Ubuntu distribution package. +Denis Darii <denis.darii@gmail.com> +Florian Le Goff <florian@9h37.fr> +Youen Péron <@youen> +Clément Nodet <clement.nodet@gmail.com> +Francisco Vianna <@fvianna> +PaweÅ‚ Krawczyk <@kravietz> +Michael Fladischer <michael@fladi.at> +Anton Agestam <msn@antonagestam.se> +Michal HoÅ™ejÅ¡ek <horejsekmichal@gmail.com> +Ken <@kkansky> + +And the authors of: +XmlHttpRequest.js project (License inside COPYING) +json2.js Library (License inside COPYING) diff --git a/django-dajaxice/COPYING b/django-dajaxice/COPYING new file mode 100644 index 000000000..a9456831d --- /dev/null +++ b/django-dajaxice/COPYING @@ -0,0 +1,62 @@ +# django-dajaxixe License ################################################ + + Copyright (c) 2009-2012 Benito Jorge Bastida + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + 3. Neither the name of the author nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +# XMLHttpRequest.js License ################################################ + + XMLHttpRequest.js Copyright (C) 2008 Sergey Ilinsky (http://www.ilinsky.com) + + This work is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + This work is distributed in the hope that it will be useful, + but without any warranty; without even the implied warranty of + merchantability or fitness for a particular purpose. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this library; if not, write to the Free Software Foundation, Inc., + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +# json2.js License ########################################################### + + http://www.json.org/json2.js 2009-09-29 + + Public Domain. NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + See http://www.JSON.org/js.html + You are free to copy, modify, or redistribute. + +################################################################################ diff --git a/django-dajaxice/MANIFEST.in b/django-dajaxice/MANIFEST.in new file mode 100644 index 000000000..c51b67d83 --- /dev/null +++ b/django-dajaxice/MANIFEST.in @@ -0,0 +1,8 @@ +include *.py +include COPYING +include AUTHORS +recursive-include dajaxice * +recursive-exclude dajaxice *.pyc +recursive-include examples * +recursive-exclude examples *.pyc +recursive-include docs * diff --git a/django-dajaxice/README.rst b/django-dajaxice/README.rst new file mode 100644 index 000000000..cb2f6b580 --- /dev/null +++ b/django-dajaxice/README.rst @@ -0,0 +1,19 @@ +django-dajaxice +=============== + +Dajaxice is the communication core of dajaxproject. It's main goal is to trivialize the asynchronous communication within the django server side code and your js code. + +dajaxice is JS-framework agnostic and focuses on decoupling the presentation logic from the server-side logic. dajaxice only requieres 5 minutes to start working. + +Project Aims +------------ + + * Isolate the communication between the client and the server. + * JS Framework agnostic (No Prototype, JQuery... needed ). + * Presentation logic outside the views (No presentation code inside ajax functions). + * Lightweight. + * Crossbrowsing ready. + * Unobtrusive standard-compliant (W3C) XMLHttpRequest 1.0 object usage. + +Official site http://dajaxproject.com +Documentation http://readthedocs.org/projects/django-dajaxice/ diff --git a/dajaxice/__init__.py b/django-dajaxice/dajaxice/__init__.py similarity index 100% rename from dajaxice/__init__.py rename to django-dajaxice/dajaxice/__init__.py diff --git a/dajaxice/core/Dajaxice.py b/django-dajaxice/dajaxice/core/Dajaxice.py similarity index 100% rename from dajaxice/core/Dajaxice.py rename to django-dajaxice/dajaxice/core/Dajaxice.py diff --git a/dajaxice/core/__init__.py b/django-dajaxice/dajaxice/core/__init__.py similarity index 100% rename from dajaxice/core/__init__.py rename to django-dajaxice/dajaxice/core/__init__.py diff --git a/dajaxice/decorators.py b/django-dajaxice/dajaxice/decorators.py similarity index 100% rename from dajaxice/decorators.py rename to django-dajaxice/dajaxice/decorators.py diff --git a/dajaxice/exceptions.py b/django-dajaxice/dajaxice/exceptions.py similarity index 100% rename from dajaxice/exceptions.py rename to django-dajaxice/dajaxice/exceptions.py diff --git a/dajaxice/finders.py b/django-dajaxice/dajaxice/finders.py similarity index 100% rename from dajaxice/finders.py rename to django-dajaxice/dajaxice/finders.py diff --git a/dajaxice/models.py b/django-dajaxice/dajaxice/models.py similarity index 100% rename from dajaxice/models.py rename to django-dajaxice/dajaxice/models.py diff --git a/dajaxice/templates/dajaxice/dajaxice.core.js b/django-dajaxice/dajaxice/templates/dajaxice/dajaxice.core.js similarity index 100% rename from dajaxice/templates/dajaxice/dajaxice.core.js rename to django-dajaxice/dajaxice/templates/dajaxice/dajaxice.core.js diff --git a/dajaxice/templates/dajaxice/dajaxice_function_loop.js b/django-dajaxice/dajaxice/templates/dajaxice/dajaxice_function_loop.js similarity index 100% rename from dajaxice/templates/dajaxice/dajaxice_function_loop.js rename to django-dajaxice/dajaxice/templates/dajaxice/dajaxice_function_loop.js diff --git a/dajaxice/templates/dajaxice/dajaxice_module_loop.js b/django-dajaxice/dajaxice/templates/dajaxice/dajaxice_module_loop.js similarity index 100% rename from dajaxice/templates/dajaxice/dajaxice_module_loop.js rename to django-dajaxice/dajaxice/templates/dajaxice/dajaxice_module_loop.js diff --git a/dajaxice/templatetags/__init__.py b/django-dajaxice/dajaxice/templatetags/__init__.py similarity index 100% rename from dajaxice/templatetags/__init__.py rename to django-dajaxice/dajaxice/templatetags/__init__.py diff --git a/dajaxice/templatetags/dajaxice_templatetags.py b/django-dajaxice/dajaxice/templatetags/dajaxice_templatetags.py similarity index 100% rename from dajaxice/templatetags/dajaxice_templatetags.py rename to django-dajaxice/dajaxice/templatetags/dajaxice_templatetags.py diff --git a/dajaxice/tests/__init__.py b/django-dajaxice/dajaxice/tests/__init__.py similarity index 100% rename from dajaxice/tests/__init__.py rename to django-dajaxice/dajaxice/tests/__init__.py diff --git a/dajaxice/tests/ajax.py b/django-dajaxice/dajaxice/tests/ajax.py similarity index 66% rename from dajaxice/tests/ajax.py rename to django-dajaxice/dajaxice/tests/ajax.py index 7d62d2e88..0fe1360bd 100644 --- a/dajaxice/tests/ajax.py +++ b/django-dajaxice/dajaxice/tests/ajax.py @@ -1,4 +1,4 @@ -import json +from django.utils import simplejson from dajaxice.decorators import dajaxice_register @@ -9,7 +9,7 @@ def test_registered_function(request): @dajaxice_register def test_string(request): - return json.dumps({'string': 'hello world'}) + return simplejson.dumps({'string': 'hello world'}) @dajaxice_register @@ -19,25 +19,25 @@ def test_ajax_exception(request): @dajaxice_register def test_foo(request): - return json.dumps({'foo': 'bar'}) + return simplejson.dumps({'foo': 'bar'}) @dajaxice_register def test_foo_with_params(request, param1): - return json.dumps({'param1': param1}) + return simplejson.dumps({'param1': param1}) @dajaxice_register(method='GET') def test_get_register(request): - return json.dumps({'foo': 'user'}) + return simplejson.dumps({'foo': 'user'}) @dajaxice_register(method='GET', name="get_user_data") def test_get_with_name_register(request): - return json.dumps({'bar': 'user'}) + return simplejson.dumps({'bar': 'user'}) @dajaxice_register(method='GET', name="get_multi") @dajaxice_register(name="post_multi") def test_multi_register(request): - return json.dumps({'foo': 'multi'}) + return simplejson.dumps({'foo': 'multi'}) diff --git a/dajaxice/tests/requirements.txt b/django-dajaxice/dajaxice/tests/requirements.txt similarity index 100% rename from dajaxice/tests/requirements.txt rename to django-dajaxice/dajaxice/tests/requirements.txt diff --git a/dajaxice/tests/urls.py b/django-dajaxice/dajaxice/tests/urls.py similarity index 100% rename from dajaxice/tests/urls.py rename to django-dajaxice/dajaxice/tests/urls.py diff --git a/dajaxice/urls.py b/django-dajaxice/dajaxice/urls.py similarity index 81% rename from dajaxice/urls.py rename to django-dajaxice/dajaxice/urls.py index 767dc500b..c920d1991 100644 --- a/dajaxice/urls.py +++ b/django-dajaxice/dajaxice/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import * +from django.conf.urls import patterns, url, include from .views import DajaxiceRequest urlpatterns = patterns('dajaxice.views', diff --git a/dajaxice/utils.py b/django-dajaxice/dajaxice/utils.py similarity index 100% rename from dajaxice/utils.py rename to django-dajaxice/dajaxice/utils.py diff --git a/dajaxice/views.py b/django-dajaxice/dajaxice/views.py similarity index 92% rename from dajaxice/views.py rename to django-dajaxice/dajaxice/views.py index b1c8adc63..b25c6850c 100644 --- a/dajaxice/views.py +++ b/django-dajaxice/dajaxice/views.py @@ -1,6 +1,7 @@ -import logging, json +import logging from django.conf import settings +import json from django.views.generic.base import View from django.http import HttpResponse, Http404 @@ -50,11 +51,10 @@ class DajaxiceRequest(View): try: response = function.call(request, **data) except Exception: - raise # always give us a backtrace if settings.DEBUG: raise response = dajaxice_config.DAJAXICE_EXCEPTION - return HttpResponse(response, mimetype="application/x-json") + return HttpResponse(response, content_type="application/x-json") else: raise FunctionNotCallableError(name) diff --git a/django-dajaxice/docs/Makefile b/django-dajaxice/docs/Makefile new file mode 100644 index 000000000..580eafcb4 --- /dev/null +++ b/django-dajaxice/docs/Makefile @@ -0,0 +1,130 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest + +help: + @echo "Please use \`make <target>' where <target> is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-dajaxice.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-dajaxice.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/django-dajaxice" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-dajaxice" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + make -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/django-dajaxice/docs/available-settings.rst b/django-dajaxice/docs/available-settings.rst new file mode 100644 index 000000000..ac70aee5c --- /dev/null +++ b/django-dajaxice/docs/available-settings.rst @@ -0,0 +1,38 @@ +Available Settings +================== + +DAJAXICE_MEDIA_PREFIX +--------------------- + +This will be the namespace that dajaxice will use as endpoint. + +Defaults to ``dajaxice`` + +Optional: ``True`` + +DAJAXICE_XMLHTTPREQUEST_JS_IMPORT +--------------------------------- + +Include XmlHttpRequest.js inside dajaxice.core.js + +Defaults to ``True`` + +Optional: ``True`` + +DAJAXICE_JSON2_JS_IMPORT +------------------------ + +Include json2.js inside dajaxice.core.js + +Defaults to ``True`` + +Optional: ``True`` + +DAJAXICE_EXCEPTION +------------------ + +Default data sent when an exception occurs. + +Defaults to ``"DAJAXICE_EXCEPTION"`` + +Optional: ``True`` diff --git a/django-dajaxice/docs/changelog.rst b/django-dajaxice/docs/changelog.rst new file mode 100644 index 000000000..7304a432d --- /dev/null +++ b/django-dajaxice/docs/changelog.rst @@ -0,0 +1,124 @@ +Changelog +========= + +0.5.5 +^^^^^ +* Return XMLHttpRequest from concreate functions as well as from function call. +* Fixed django 1.5 compatibility: Content-Type have to be application/x-www-form-urlencoded otherwise Django discards POST data. +* Fix JS generation errors +* Fix @dajaxice_register legacy decorator + +0.5.4.1 +^^^^^^^ +* Fix JS generation errors. + +0.5.4 +^^^^^ +* Fix JS generation errors. + +0.5.3 +^^^^^ +* Fix some Windows bugs. +* Fix some JS generation errors. +* Make dajaxice use CSRF_COOKIE_NAME. + +0.5.2 +^^^^^ +* Fix GET dajaxice requests in order to send args as part of the url. + +0.5.1 +^^^^^ +* Make django-dajaxice work with django 1.3 +* Fix installation steps +* Update json2.js + +0.5 +^^^ +* General Project clean-up +* Django>=1.3 is now a requirement +* Fixed numerous CSRF issues +* Dajaxice now use django.contrib.staticfiles +* Fix SERVER_ROOT_URL issues +* Fixed js_core issues accepting multiple arguments +* New upgraded documentation +* Marketing site (http://dajaxproject.com) is now open-source +* Fix JS generation issues +* Travis-ci integration + + +0.2 +^^^ +* Fix bug with the 'is_callback_a_function' variable in dajaxice.core.js +* Fix bug with csrftoken in landing pages using dajaxice. +* Improve reliability handling server errors. +* Exception handling was fully rewritten. Dajaxice default_error_callback is now configurable using Dajaxice.setup. +* Custom error messages per dajaxice call. +* Dajaxice now propagate docstrings to javascript dajaxice functions. +* Added DAJAXICE_JS_DOCSTRINGS to configure docstrings propagation behaviour, default=False. +* Updated installation guide for compatibility with django 1.3 +* dajaxice now uses the logger 'dajaxice' and not 'dajaxice.DajaxiceRequest' +* Documentation Updated. + +0.1.8.1 +^^^^^^^ +* Fixed bug #25 related to CSRF verification on Django 1.2.5 + +0.1.8 +^^^^^ +* Add build dir to ignores +* Remove MANIFEST file and auto-generate it through MANIFEST.in +* Add MANIFEST to ignores +* Include examples and docs dirs to source distribution +* Add long_description to setup.py +* Fixed Flaw in AJAX CSRF handling (X-CSRFToken Django 1.2.5) + +0.1.7 +^^^^^ +* Fixing dajaxice callback model to improve security against XSS attacks. +* Dajaxice callbacks should be passed as functions and not as strings. +* Old string-callback maintained for backward compatibility.(usage not recommended) +* New documentation using Sphinx +* Adding a decorators.py file with a helper decorator to register functions (Douglas Soares de Andrade) + +0.1.6 +^^^^^ +* Fixing registration bugs +* Added some tests + +0.1.5 +^^^^^ +* Now dajaxice functions must be registered using dajaxice_functions.register instead of adding that functions to DAJAXICE_FUNCTIONS list inside settings.py. This pattern is very similar to django.contrib.admin model registration. +* Now dajaxice functions could be placed inside any module depth. +* With this approach dajaxice app reusability was improved. +* Old style registration (using DAJAXICE_FUNCTIONS) works too, but isn't recommended. +* New tests added. + +0.1.3 +^^^^^ +* CSRF middleware buf fixed +* Improved production and development logging +* New custom Exception message +* New notify_exception to send traceback to admins +* Fixed semicolon issues +* Fixed unicode errors +* Fixed generate_static_dajaxice before easy_install usage +* Fixed IE6 bug in dajaxice.core.js + +0.1.2 +^^^^^ +* New and cleaned setup.py + +0.1.1 +^^^^^ +* json2.js and XMLHttpRequest libs included +* New settings DAJAXICE_XMLHTTPREQUEST_JS_IMPORT and DAJAXICE_JSON2_JS_IMPORT + +0.1.0 +^^^^^ +* dajaxice AJAX functions now receive parameters as function arguments. +* dajaxice now uses standard python logging +* some bugs fixed + +0.0.1 +^^^^^ +* First Release diff --git a/django-dajaxice/docs/conf.py b/django-dajaxice/docs/conf.py new file mode 100644 index 000000000..e6e8445f7 --- /dev/null +++ b/django-dajaxice/docs/conf.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +# +# django-dajaxice documentation build configuration file, created by +# sphinx-quickstart on Fri May 25 08:02:23 2012. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'django-dajaxice' +copyright = u'2012, Jorge Bastida' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +import pkg_resources +try: + release = pkg_resources.get_distribution('django-dajaxice').version +except pkg_resources.DistributionNotFound: + print 'To build the documentation, The distribution information of django-dajaxice' + print 'Has to be available. Either install the package into your' + print 'development environment or run "setup.py develop" to setup the' + print 'metadata. A virtualenv is recommended!' + sys.exit(1) +del pkg_resources + +version = '.'.join(release.split('.')[:2]) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'nature' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# "<project> v<release> documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a <link> tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'django-dajaxicedoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'django-dajaxice.tex', u'django-dajaxice Documentation', + u'Jorge Bastida', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'django-dajaxice', u'django-dajaxice Documentation', + [u'Jorge Bastida'], 1) +] diff --git a/django-dajaxice/docs/custom-error-callbacks.rst b/django-dajaxice/docs/custom-error-callbacks.rst new file mode 100644 index 000000000..3fe2e5b7f --- /dev/null +++ b/django-dajaxice/docs/custom-error-callbacks.rst @@ -0,0 +1,31 @@ +Custom error callbacks +====================== + + +How dajaxice handle errors +-------------------------- + +When one of your functions raises an exception dajaxice returns as response the ``DAJAXICE_EXCEPTION`` message. +On every response ``dajaxice.core.js`` checks if that response was an error or not and shows the user a default +error message ``Something goes wrong``. + + +Customize the default error message +----------------------------------- +This behaviour is configurable using the new ``Dajaxice.setup`` function. + +.. code-block:: javascript + + Dajaxice.setup({'default_exception_callback': function(){ alert('Error!'); }}); + +Customize error message per call +-------------------------------- +In this new version you can also specify an error callback per dajaxice call. + +.. code-block:: javascript + + function custom_error(){ + alert('Custom error of my_function.'); + } + + Dajaxice.simple.my_function(callback, {'user': 'tom'}, {'error_callback': custom_error}); diff --git a/django-dajaxice/docs/index.rst b/django-dajaxice/docs/index.rst new file mode 100644 index 000000000..b97695e24 --- /dev/null +++ b/django-dajaxice/docs/index.rst @@ -0,0 +1,45 @@ +.. django-dajaxice documentation master file, created by + sphinx-quickstart on Fri May 25 08:02:23 2012. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + + +django-dajaxice +=============== + +Dajaxixe is an Easy to use AJAX library for django. Its main goal is to trivialize the asynchronous communication within the django server code and your js code. Dajaxice uses the unobtrusive standard-compliant (W3C) XMLHttpRequest 1.0 object. + +django-dajaxice is a **JS-framework agnostic** library and focuses on decoupling the presentation logic from the server-side logic. dajaxice only requieres **5 minutes to start working.** + +Dajaxice has the following aims: + +* Isolate the communication between the client and the server. +* JS Framework agnostic (No Prototype, JQuery... needed ). +* Presentation logic outside the views (No presentation code inside ajax functions). +* Lightweight. +* Crossbrowsing ready. +* `Unobtrusive standard-compliant (W3C) XMLHttpRequest 1.0 <http://code.google.com/p/xmlhttprequest/>`_ object usage. + +Documentation +------------- +.. toctree:: + :maxdepth: 2 + + installation + quickstart + + custom-error-callbacks + utils + production-environment + migrating-to-05 + available-settings + + changelog + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/django-dajaxice/docs/installation.rst b/django-dajaxice/docs/installation.rst new file mode 100644 index 000000000..b5d990460 --- /dev/null +++ b/django-dajaxice/docs/installation.rst @@ -0,0 +1,92 @@ +Installation +============ +Follow this instructions to start using dajaxice in your django project. + +Installing dajaxice +------------------- + +Add `dajaxice` in your project settings.py inside ``INSTALLED_APPS``:: + + INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'dajaxice', + ... + ) + +Ensure that your ``TEMPLATE_LOADERS``, looks like the following. Probably you'll only need to uncomment the last line.:: + + TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + 'django.template.loaders.eggs.Loader', + ) + +Ensure that ``TEMPLATE_CONTEXT_PROCESSORS`` has ``django.core.context_processors.request``. Probably you'll only need to add the last line:: + + TEMPLATE_CONTEXT_PROCESSORS = ( + 'django.contrib.auth.context_processors.auth', + 'django.core.context_processors.debug', + 'django.core.context_processors.i18n', + 'django.core.context_processors.media', + 'django.core.context_processors.static', + 'django.core.context_processors.request', + 'django.contrib.messages.context_processors.messages' + ) + +Add ``dajaxice.finders.DajaxiceFinder`` to ``STATICFILES_FINDERS``:: + + STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'dajaxice.finders.DajaxiceFinder', + ) + +Configure dajaxice url +---------------------- + +Add the following code inside urls.py:: + + from dajaxice.core import dajaxice_autodiscover, dajaxice_config + dajaxice_autodiscover() + +Add a new line in urls.py urlpatterns with this code:: + + urlpatterns = patterns('', + ... + url(dajaxice_config.dajaxice_url, include('dajaxice.urls')), + ... + ) + +If you aren't using ``django.contrib.staticfiles``, you should also enable it importing:: + + from django.contrib.staticfiles.urls import staticfiles_urlpatterns + +and adding this line to the bottom of your urls.py:: + + urlpatterns += staticfiles_urlpatterns() + +Install dajaxice in your templates +---------------------------------- +Dajaxice needs some JS to work. To include it in your templates, you should load ``dajaxice_templatetags`` and use ``dajaxice_js_import`` TemplateTag inside your head section. This TemplateTag will print needed js. + +.. code-block:: html + + {% load dajaxice_templatetags %} + + <html> + <head> + <title>My base template</title> + ... + {% dajaxice_js_import %} + </head> + ... + </html> + +This templatetag will include all the js dajaxice needs. + +Use Dajaxice! +------------- +Now you can create your first ajax function following the :doc:`quickstart`. diff --git a/django-dajaxice/docs/migrating-to-05.rst b/django-dajaxice/docs/migrating-to-05.rst new file mode 100644 index 000000000..ae117a18b --- /dev/null +++ b/django-dajaxice/docs/migrating-to-05.rst @@ -0,0 +1,55 @@ +Migrating to 0.5 +================= + +Upgrade to django 1.3 or 1.4 +---------------------------- + +Dajaxice ``0.5`` requires ``django>=1.3``, so in order to make dajaxice work you'll need to upgrade your app to any of these ones. + +* `Django 1.3 release notes <https://docs.djangoproject.com/en/dev/releases/1.3/>`_ +* `Django 1.4 release notes <https://docs.djangoproject.com/en/dev/releases/1.4/>`_ + + +Make django static-files work +----------------------------- + +Add this at the beginning of your ``urls.py`` file:: + + from django.contrib.staticfiles.urls import staticfiles_urlpatterns + +and add this line to the bottom of your urls.py:: + + urlpatterns += staticfiles_urlpatterns() + +Add a new staticfiles finder named ``dajaxice.finders.DajaxiceFinder`` to the list of ``STATICFILES_FINDERS``:: + + STATICFILES_FINDERS = ('django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'dajaxice.finders.DajaxiceFinder') + +Update dajaxice core url +------------------------ + +Add ``dajaxice_config`` to the list of modules to import:: + + # Old import + from dajaxice.core import dajaxice_autodiscover + + # New import + from dajaxice.core import dajaxice_autodiscover, dajaxice_config + + +And replate your old dajaxice url with the new one:: + + # Old style + (r'^%s/' % settings.DAJAXICE_MEDIA_PREFIX, include('dajaxice.urls')), + + # New style + url(dajaxice_config.dajaxice_url, include('dajaxice.urls')), + + +Done! +----- + +Your app should be working now! +You can now read the :doc:`quickstart <quickstart>` to discover some of the new dajaxice features. diff --git a/django-dajaxice/docs/production-environment.rst b/django-dajaxice/docs/production-environment.rst new file mode 100644 index 000000000..37d0a98a8 --- /dev/null +++ b/django-dajaxice/docs/production-environment.rst @@ -0,0 +1,7 @@ +Production Environment +====================== + +Since ``0.5`` dajaxice takes advantage of ``django.contrib.staticfiles`` so deploying a dajaxice application live is much easy than in previous versions. + +You need to remember to run ``python manage.py collectstatic`` before deploying your code live. This command will collect all the static files your application need into ``STATIC_ROOT``. For further information, this is the `Django static files docuemntation <https://docs.djangoproject.com/en/dev/howto/static-files/>`_ + diff --git a/django-dajaxice/docs/quickstart.rst b/django-dajaxice/docs/quickstart.rst new file mode 100644 index 000000000..a18ba7cdf --- /dev/null +++ b/django-dajaxice/docs/quickstart.rst @@ -0,0 +1,80 @@ +Quickstart +========== + +Create your first ajax function +------------------------------- +Create a file named ``ajax.py`` inside any of your apps. For example ``example/ajax.py``. + +Inside this file create a simple function that return json.:: + + from django.utils import simplejson + + def sayhello(request): + return simplejson.dumps({'message':'Hello World'}) + +Now you'll need to register this function as a dajaxice function using the ``dajaxice_register`` decorator:: + + from django.utils import simplejson + from dajaxice.decorators import dajaxice_register + + @dajaxice_register + def sayhello(request): + return simplejson.dumps({'message':'Hello World'}) + +Invoque it from your JS +----------------------- + +You can invoque your ajax fuctions from javascript using: + +.. code-block:: javascript + + onclick="Dajaxice.example.sayhello(my_js_callback);" + +The function ``my_js_callback`` is your JS function that will use your example return data. For example alert the message: + +.. code-block:: javascript + + function my_js_callback(data){ + alert(data.message); + } + +That callback will alert the message ``Hello World``. + + +How can I do a GET request instead of a POST one? +------------------------------------------------- + +When you register your functions as ajax functions, you can choose the http method using:: + + from django.utils import simplejson + from dajaxice.decorators import dajaxice_register + + @dajaxice_register(method='GET') + def saybye(request): + return simplejson.dumps({'message':'Bye!'}) + +This function will be executed doing a GET request and not a POST one. + + +Can I combine both? +------------------- + +Yes! You can register a function as many times as you want, for example:: + + from django.utils import simplejson + from dajaxice.decorators import dajaxice_register + + @dajaxice_register(method='POST', name='user.update') + @dajaxice_register(method='GET', name='user.info') + def list_user(request): + if request.method == 'POST': + ... + else: + ... + +In this case you'll be able to call this two JS functions:: + + Dajaxice.user.info( callback ); + Dajaxice.user.update( callback ); + +The first one will be a GET call and the second one a POST one. diff --git a/django-dajaxice/docs/utils.rst b/django-dajaxice/docs/utils.rst new file mode 100644 index 000000000..1d30972c3 --- /dev/null +++ b/django-dajaxice/docs/utils.rst @@ -0,0 +1,16 @@ +Utils +===== + +dajaxice.utils.deserialize_form +------------------------------- + +Using ``deserialize_form`` you will be able to deserialize a query_string and use it as input of a Form:: + + from dajaxice.utils import deserialize_form + + @dajaxice_register + def send_form(request, form): + form = ExampleForm(deserialize_form(form)) + if form.is_valid(): + ... + ... diff --git a/django-dajaxice/examples/examples/__init__.py b/django-dajaxice/examples/examples/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/django-dajaxice/examples/examples/settings.py b/django-dajaxice/examples/examples/settings.py new file mode 100644 index 000000000..a2b05b050 --- /dev/null +++ b/django-dajaxice/examples/examples/settings.py @@ -0,0 +1,177 @@ +# Django settings for examples project. + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@example.com'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': '', # Or path to database file if using sqlite3. + 'USER': '', # Not used with sqlite3. + 'PASSWORD': '', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'America/Chicago' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale. +USE_L10N = True + +# If you set this to False, Django will not use timezone-aware datetimes. +USE_TZ = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = '' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +STATIC_ROOT = 'static' + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = '/static/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = '$zr@-0lstgzehu)k(-pbg7wz=mv8%n%o7+j_@h&-yy&sx)pyau' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + # Uncomment the next line for simple clickjacking protection: + # 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +ROOT_URLCONF = 'examples.urls' + +# Python dotted path to the WSGI application used by Django's runserver. +WSGI_APPLICATION = 'examples.wsgi.application' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'dajaxice', + 'simple' + # Uncomment the next line to enable the admin: + # 'django.contrib.admin', + # Uncomment the next line to enable admin documentation: + # 'django.contrib.admindocs', +) + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error when DEBUG=False. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse' + } + }, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + }, + 'console': { + 'level': 'INFO', + 'class': 'logging.StreamHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + 'dajaxice': { + 'handlers': ['console'], + 'level': 'INFO', + 'propagate': True, + }, + } +} + +STATICFILES_FINDERS = ("django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", + "dajaxice.finders.DajaxiceFinder") + +TEMPLATE_CONTEXT_PROCESSORS = ("django.contrib.auth.context_processors.auth", + "django.core.context_processors.debug", + "django.core.context_processors.i18n", + "django.core.context_processors.media", + "django.core.context_processors.static", + "django.core.context_processors.request", + "django.contrib.messages.context_processors.messages") diff --git a/django-dajaxice/examples/examples/urls.py b/django-dajaxice/examples/examples/urls.py new file mode 100644 index 000000000..3503483cf --- /dev/null +++ b/django-dajaxice/examples/examples/urls.py @@ -0,0 +1,25 @@ +from django.conf.urls import patterns, include, url +from django.contrib.staticfiles.urls import staticfiles_urlpatterns + +from dajaxice.core import dajaxice_autodiscover, dajaxice_config +dajaxice_autodiscover() + +# Uncomment the next two lines to enable the admin: +# from django.contrib import admin +# admin.autodiscover() + +urlpatterns = patterns('', + # Examples: + # url(r'^$', 'examples.views.home', name='home'), + # url(r'^examples/', include('examples.foo.urls')), + + # Uncomment the admin/doc line below to enable admin documentation: + # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), + + # Uncomment the next line to enable the admin: + # url(r'^admin/', include(admin.site.urls)), + (dajaxice_config.dajaxice_url, include('dajaxice.urls')), + url(r'', 'simple.views.index') +) + +urlpatterns += staticfiles_urlpatterns() diff --git a/django-dajaxice/examples/examples/wsgi.py b/django-dajaxice/examples/examples/wsgi.py new file mode 100644 index 000000000..ebf0cf413 --- /dev/null +++ b/django-dajaxice/examples/examples/wsgi.py @@ -0,0 +1,28 @@ +""" +WSGI config for examples project. + +This module contains the WSGI application used by Django's development server +and any production WSGI deployments. It should expose a module-level variable +named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover +this application via the ``WSGI_APPLICATION`` setting. + +Usually you will have the standard Django WSGI application here, but it also +might make sense to replace the whole Django WSGI application with a custom one +that later delegates to the Django one. For example, you could introduce WSGI +middleware here, or combine a Django application with an application of another +framework. + +""" +import os + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "examples.settings") + +# This application object is used by any WSGI server configured to use this +# file. This includes Django's development server, if the WSGI_APPLICATION +# setting points here. +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() + +# Apply WSGI middleware here. +# from helloworld.wsgi import HelloWorldApplication +# application = HelloWorldApplication(application) diff --git a/django-dajaxice/examples/manage.py b/django-dajaxice/examples/manage.py new file mode 100644 index 000000000..55e614cd9 --- /dev/null +++ b/django-dajaxice/examples/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "examples.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/django-dajaxice/examples/simple/__init__.py b/django-dajaxice/examples/simple/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/django-dajaxice/examples/simple/ajax.py b/django-dajaxice/examples/simple/ajax.py new file mode 100644 index 000000000..992068168 --- /dev/null +++ b/django-dajaxice/examples/simple/ajax.py @@ -0,0 +1,26 @@ +import simplejson + +from dajaxice.decorators import dajaxice_register + + +@dajaxice_register(method='GET') +@dajaxice_register(method='POST', name='other_post') +def hello(request): + return simplejson.dumps({'message': 'hello'}) + + +@dajaxice_register(method='GET') +@dajaxice_register(method='POST', name="more.complex.bye") +def bye(request): + raise Exception("PUMMMM") + return simplejson.dumps({'message': 'bye'}) + + +@dajaxice_register +def lol(request): + return simplejson.dumps({'message': 'lol'}) + + +@dajaxice_register(method='GET') +def get_args(request, foo): + return simplejson.dumps({'message': 'hello get args %s' % foo}) diff --git a/django-dajaxice/examples/simple/models.py b/django-dajaxice/examples/simple/models.py new file mode 100644 index 000000000..71a836239 --- /dev/null +++ b/django-dajaxice/examples/simple/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/django-dajaxice/examples/simple/templates/simple/index.html b/django-dajaxice/examples/simple/templates/simple/index.html new file mode 100644 index 000000000..b92241f02 --- /dev/null +++ b/django-dajaxice/examples/simple/templates/simple/index.html @@ -0,0 +1,13 @@ +{% load dajaxice_templatetags %} +<html> +<head> +{% dajaxice_js_import 'nocsrf' %} +</head> +<body> + <button onclick="Dajaxice.simple.hello(function(d){alert(d.message);})">Hello</button> + <button onclick="Dajaxice.simple.bye(function(d){alert(d.message);})">Bye</button> + <button onclick="Dajaxice.more.complex.bye(function(d){alert(d.message);})">Complex Bye</button> + <button onclick="Dajaxice.simple.lol(function(d){alert(d.message);})">LOL</button> + <button onclick="Dajaxice.simple.get_args(function(d){alert(d.message);}, {'foo': 'var'})">GET args</button> +</body> +</html> diff --git a/django-dajaxice/examples/simple/tests.py b/django-dajaxice/examples/simple/tests.py new file mode 100644 index 000000000..501deb776 --- /dev/null +++ b/django-dajaxice/examples/simple/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/django-dajaxice/examples/simple/views.py b/django-dajaxice/examples/simple/views.py new file mode 100644 index 000000000..80c80dc5d --- /dev/null +++ b/django-dajaxice/examples/simple/views.py @@ -0,0 +1,9 @@ +# Create your views here. +from django.shortcuts import render + +from dajaxice.core import dajaxice_functions + + +def index(request): + + return render(request, 'simple/index.html') diff --git a/django-dajaxice/setup.py b/django-dajaxice/setup.py new file mode 100644 index 000000000..cc544bb88 --- /dev/null +++ b/django-dajaxice/setup.py @@ -0,0 +1,31 @@ +from distutils.core import setup + +setup( + name='django-dajaxice', + version='0.5.5', + author='Jorge Bastida', + author_email='me@jorgebastida.com', + description='Agnostic and easy to use ajax library for django', + url='http://dajaxproject.com', + license='BSD', + packages=['dajaxice', + 'dajaxice.templatetags', + 'dajaxice.core'], + package_data={'dajaxice': ['templates/dajaxice/*']}, + long_description=("Easy to use AJAX library for django, all the " + "presentation logic resides outside the views and " + "doesn't require any JS Framework. Dajaxice uses the " + "unobtrusive standard-compliant (W3C) XMLHttpRequest " + "1.0 object."), + install_requires=[ + 'Django>=1.3' + ], + classifiers=['Development Status :: 4 - Beta', + 'Environment :: Web Environment', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Utilities'] +) diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 4b182c206..974179a98 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -236,7 +236,7 @@ class Room(models.Model): meeting = models.ForeignKey(Meeting) name = models.CharField(max_length=255) capacity = models.IntegerField(null=True, blank=True) - resources = models.ManyToManyField(ResourceAssociation) + resources = models.ManyToManyField(ResourceAssociation, blank = True) def __unicode__(self): return "%s size: %u" % (self.name, self.capacity) diff --git a/ietf/meeting/test_data.py b/ietf/meeting/test_data.py index ff92707e7..85a204581 100644 --- a/ietf/meeting/test_data.py +++ b/ietf/meeting/test_data.py @@ -8,9 +8,11 @@ from ietf.group.models import Group def make_meeting_test_data(): - make_test_data() + if not Group.objects.filter(acronym='mars'): + make_test_data() system_person = Person.objects.get(name="(System)") plainman = Person.objects.get(user__username="plain") + secretary = Person.objects.get(user__username="secretary") meeting = Meeting.objects.get(number="42", type="ietf") schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-agenda", visible=True, public=True) diff --git a/ietf/meeting/tests_api.py b/ietf/meeting/tests_api.py index f1e3cb566..831db8da1 100644 --- a/ietf/meeting/tests_api.py +++ b/ietf/meeting/tests_api.py @@ -20,52 +20,67 @@ class ApiTests(TestCase): r = self.client.get("/dajaxice/dajaxice.core.js") self.assertEqual(r.status_code, 200) - def test_update_agenda_item(self): + def test_update_agenda(self): meeting = make_meeting_test_data() - session = Session.objects.filter(meeting=meeting, group__acronym="mars").first() - mars_scheduled = ScheduledSession.objects.get(session=session) + schedule = Schedule.objects.get(meeting__number=42,name="test-agenda") + mars_session = Session.objects.filter(meeting=meeting, group__acronym="mars").first() + ames_session = Session.objects.filter(meeting=meeting, group__acronym="ames").first() + + mars_scheduled = ScheduledSession.objects.get(session=mars_session) mars_slot = mars_scheduled.timeslot - ames_scheduled = ScheduledSession.objects.get(session__meeting=meeting, session__group__acronym="ames") + ames_scheduled = ScheduledSession.objects.get(session=ames_session) ames_slot = ames_scheduled.timeslot - def do_post(to): - # move this session from one timeslot to another - return self.client.post('/dajaxice/ietf.meeting.update_timeslot/', { - 'argv': json.dumps({ - "schedule_id": mars_scheduled.schedule.pk, - "session_id": session.pk, - "scheduledsession_id": to.pk if to else None, - })}) + def do_unschedule(scheduledsession): + url = urlreverse("ietf.meeting.ajax.scheduledsession_json", + kwargs=dict(num=scheduledsession.session.meeting.number, + name=scheduledsession.schedule.name, + scheduledsession_id=scheduledsession.pk,)) + return self.client.delete(url) - # faulty post - not logged in - r = do_post(to=ames_scheduled) - self.assertEqual(r.status_code, 200) - self.assertTrue("error" in json.loads(r.content)) - self.assertEqual(ScheduledSession.objects.get(pk=mars_scheduled.pk).session, session) + def do_schedule(schedule,session,timeslot): + url = urlreverse("ietf.meeting.ajax.scheduledsessions_json", + kwargs=dict(num=session.meeting.number, + name=schedule.name,)) + post_data = '{ "session_id": "%s", "timeslot_id": "%s" }'%(session.pk,timeslot.pk) + return self.client.post(url,post_data,content_type='application/x-www-form-urlencoded') - # faulty post - logged in as non-owner + # not logged in + # faulty delete + r = do_unschedule(mars_scheduled) + self.assertEqual(r.status_code, 403) + self.assertEqual(ScheduledSession.objects.get(pk=mars_scheduled.pk).session, mars_session) + # faulty post + r = do_schedule(schedule,ames_session,mars_slot) + self.assertEqual(r.status_code, 403) + + # logged in as non-owner + # faulty delete self.client.login(remote_user="ad") - r = do_post(to=ames_scheduled) - self.assertEqual(r.status_code, 200) + r = do_unschedule(mars_scheduled) + self.assertEqual(r.status_code, 403) self.assertTrue("error" in json.loads(r.content)) + # faulty post + r = do_schedule(schedule,ames_session,mars_slot) + self.assertEqual(r.status_code, 403) - # move to ames + # Put ames in the same timeslot as mars self.client.login(remote_user="plain") - r = do_post(to=ames_scheduled) + r = do_unschedule(ames_scheduled) self.assertEqual(r.status_code, 200) self.assertTrue("error" not in json.loads(r.content)) - self.assertEqual(ScheduledSession.objects.get(pk=mars_scheduled.pk).session, None) - self.assertEqual(ScheduledSession.objects.get(pk=ames_scheduled.pk).session, session) + r = do_schedule(schedule,ames_session,mars_slot) + self.assertEqual(r.status_code, 201) - # unschedule - self.client.login(remote_user="plain") - r = do_post(to=None) + # Unschedule mars + r = do_unschedule(mars_scheduled) self.assertEqual(r.status_code, 200) self.assertTrue("error" not in json.loads(r.content)) - self.assertEqual(ScheduledSession.objects.get(pk=ames_scheduled.pk).session, None) + self.assertEqual(ScheduledSession.objects.filter(session=mars_session).count(), 0) + self.assertEqual(ScheduledSession.objects.get(session=ames_session).timeslot, mars_slot) def test_constraints_json(self): @@ -106,7 +121,7 @@ class ApiTests(TestCase): timeslots_before = meeting.timeslot_set.count() url = urlreverse("ietf.meeting.ajax.timeslot_roomsurl", kwargs=dict(num=meeting.number)) - post_data = { "name": "new room", "capacity": "50" } + post_data = { "name": "new room", "capacity": "50" , "resources": []} # unauthorized post r = self.client.post(url, post_data) @@ -116,6 +131,7 @@ class ApiTests(TestCase): # create room self.client.login(remote_user="secretary") r = self.client.post(url, post_data) + self.assertEqual(r.status_code, 302) self.assertTrue(meeting.room_set.filter(name="new room")) timeslots_after = meeting.timeslot_set.count() @@ -259,8 +275,14 @@ class ApiTests(TestCase): r = self.client.post(url, post_data) self.assertEqual(r.status_code, 403) + # TODO - permission protection on this function are not right + # Normal users are prevented from changing public/private on their own schedule + # The secretariat can't change normal user's agendas settings for them, and the + # page at /meeting/<num>/schedule/<name>/details behaves badly for the secretariat + # (pushing save seems to do nothing as the POST 401s in the background) + # change agenda - self.client.login(remote_user="ad") + self.client.login(remote_user="secretary") r = self.client.post(url, post_data) self.assertEqual(r.status_code, 302) changed_schedule = Schedule.objects.get(pk=meeting.agenda.pk) diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 5573e482e..6f7948ddc 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -159,7 +159,7 @@ class EditTests(TestCase): self.client.login(remote_user="secretary") r = self.client.get(urlreverse("ietf.meeting.views.edit_agenda", kwargs=dict(num=meeting.number))) self.assertEqual(r.status_code, 200) - self.assertTrue("session_obj" in r.content) + self.assertTrue("load_scheduledsessions" in r.content) def test_save_agenda_as_and_read_permissions(self): meeting = make_meeting_test_data() diff --git a/ietf/secr/sreq/tests.py b/ietf/secr/sreq/tests.py index 798bf03a7..bb7617a75 100644 --- a/ietf/secr/sreq/tests.py +++ b/ietf/secr/sreq/tests.py @@ -5,7 +5,8 @@ from django.core.urlresolvers import reverse from ietf.utils import TestCase from ietf.group.models import Group from ietf.ietfauth.utils import has_role -from ietf.utils.test_data import make_test_data +#from ietf.utils.test_data import make_test_data +from ietf.meeting.test_data import make_meeting_test_data as make_test_data #from pyquery import PyQuery @@ -13,7 +14,7 @@ SECR_USER='secretary' class SreqUrlTests(TestCase): def test_urls(self): - draft = make_test_data() + make_test_data() r = self.client.get("/secr/") self.assertEqual(r.status_code, 200) @@ -21,23 +22,24 @@ class SreqUrlTests(TestCase): r = self.client.get("/secr/sreq/") self.assertEqual(r.status_code, 200) - r = self.client.get("/secr/sreq/%s/new/" % draft.group.acronym) + testgroup=Group.objects.filter(type_id='wg').first() + r = self.client.get("/secr/sreq/%s/new/" % testgroup.acronym) self.assertEqual(r.status_code, 200) class MainTestCase(TestCase): def test_main(self): - draft = make_test_data() + make_test_data() url = reverse('sessions') r = self.client.get(url, REMOTE_USER=SECR_USER) self.assertEqual(r.status_code, 200) sched = r.context['scheduled_groups'] unsched = r.context['unscheduled_groups'] - self.failUnless(len(sched) == 0) - self.failUnless(len(unsched) > 0) + self.failUnless(len(unsched) == 0) + self.failUnless(len(sched) > 0) class SubmitRequestCase(TestCase): def test_submit_request(self): - draft = make_test_data() + make_test_data() acronym = Group.objects.all()[0].acronym url = reverse('sessions_new',kwargs={'acronym':acronym}) post_data = {'id_num_session':'1', diff --git a/ietf/settings.py b/ietf/settings.py index 63b11566d..9f9e48bf2 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -19,6 +19,9 @@ LOG_DIR = '/var/log/datatracker' import sys sys.path.append(os.path.abspath(BASE_DIR + "/..")) +# dajaxice now in subdirectory +sys.path.append(os.path.abspath(BASE_DIR + "/../django-dajaxice")) + DEBUG = True TEMPLATE_DEBUG = DEBUG @@ -137,6 +140,7 @@ ROOT_URLCONF = 'ietf.urls' TEMPLATE_DIRS = ( BASE_DIR + "/templates", BASE_DIR + "/secr/templates", + BASE_DIR+"/../django-dajaxice/dajaxice/templates", ) TEMPLATE_CONTEXT_PROCESSORS = (