From 2b55d20371446cac953cad33ccf97ffb12ee01d1 Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz <henrik@levkowetz.com>
Date: Thu, 7 Jul 2016 12:18:21 +0000
Subject: [PATCH 01/15] Added a filter 'nbsp' to turn spaces into
 nonbreaking-space characters.  - Legacy-Id: 11532

---
 ietf/doc/templatetags/ietf_filters.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/ietf/doc/templatetags/ietf_filters.py b/ietf/doc/templatetags/ietf_filters.py
index e4e1ceb05..7bb4821c6 100644
--- a/ietf/doc/templatetags/ietf_filters.py
+++ b/ietf/doc/templatetags/ietf_filters.py
@@ -616,3 +616,7 @@ def format_timedelta(timedelta):
     hours, remainder = divmod(s, 3600)
     minutes, seconds = divmod(remainder, 60)
     return '{hours:02d}:{minutes:02d}'.format(hours=hours,minutes=minutes)
+
+@register.filter()
+def nbsp(value):
+    return mark_safe("&nbsp;".join(value.split(' ')))

From a4b5bbc5cf70cb89494299dc754b3fd9f6004d9c Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz <henrik@levkowetz.com>
Date: Thu, 7 Jul 2016 12:19:02 +0000
Subject: [PATCH 02/15] Removed an unnecessary pyflakes:ignore.  - Legacy-Id:
 11533

---
 ietf/doc/resources.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ietf/doc/resources.py b/ietf/doc/resources.py
index eb66ab426..de2c8df9f 100644
--- a/ietf/doc/resources.py
+++ b/ietf/doc/resources.py
@@ -1,7 +1,7 @@
 # Autogenerated by the makeresources management command 2015-10-19 12:29 PDT
 from tastypie.resources import ModelResource
 from ietf.api import ToOneField
-from tastypie.fields import ToManyField, CharField # pyflakes:ignore
+from tastypie.fields import ToManyField, CharField
 from tastypie.constants import ALL, ALL_WITH_RELATIONS # pyflakes:ignore
 from tastypie.cache import SimpleCache
 

From ad9d5d72f9575c5e3f301caa71823033768707e7 Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz <henrik@levkowetz.com>
Date: Thu, 7 Jul 2016 12:19:59 +0000
Subject: [PATCH 03/15] Fixed an error in a docstring.  - Legacy-Id: 11534

---
 ietf/doc/models.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ietf/doc/models.py b/ietf/doc/models.py
index 561f4cb86..6b0a408d2 100644
--- a/ietf/doc/models.py
+++ b/ietf/doc/models.py
@@ -575,7 +575,7 @@ class DocHistory(DocumentInfo):
 def save_document_in_history(doc):
     """This should be called before saving changes to a Document instance,
     so that the DocHistory entries contain all previous states, while
-    the Group entry contain the current state.  XXX TODO: Call this
+    the Document entry contain the current state.  XXX TODO: Call this
     directly from Document.save(), and add event listeners for save()
     on related objects so we can save as needed when they change, too.
     """

From 11d3e6fcd5836d6fbce6ddeb5becbd2f6c55c191 Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz <henrik@levkowetz.com>
Date: Thu, 7 Jul 2016 12:21:25 +0000
Subject: [PATCH 04/15] Added a media subdirectory for floorplans.  -
 Legacy-Id: 11535

---
 ietf/settings.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/ietf/settings.py b/ietf/settings.py
index cddf5e254..52f38c1ba 100644
--- a/ietf/settings.py
+++ b/ietf/settings.py
@@ -556,12 +556,18 @@ YANG_INVAL_MODEL_DIR = '/a/www/ietf-ftp/yang/invalmod/'
 
 XML_LIBRARY = "/www/tools.ietf.org/tools/xml2rfc/web/public/rfc/"
 
+# === Meeting Related Settings =================================================
+
 MEETING_MATERIALS_SUBMISSION_START_DAYS = -90
 MEETING_MATERIALS_SUBMISSION_CUTOFF_DAYS = 26
 MEETING_MATERIALS_SUBMISSION_CORRECTION_DAYS = 50
 
 INTERNET_DRAFT_DAYS_TO_EXPIRE = 185
 
+FLOORPLAN_MEDIA_DIR = 'floor'
+
+# ==============================================================================
+
 DOT_BINARY = '/usr/bin/dot'
 UNFLATTEN_BINARY= '/usr/bin/unflatten'
 RSYNC_BINARY = '/usr/bin/rsync'

From c0c3d2a5d60d7ae17d2ec5cda6e6cf6684654727 Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz <henrik@levkowetz.com>
Date: Thu, 7 Jul 2016 12:23:34 +0000
Subject: [PATCH 05/15] Removed an unnecessary storage location argument, and
 corrected the use of ImageField upload_to in order to point at media/photo
 instead of media/photo/photo.  - Legacy-Id: 11536

---
 ietf/person/models.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/ietf/person/models.py b/ietf/person/models.py
index f420650d9..94d70a4f8 100644
--- a/ietf/person/models.py
+++ b/ietf/person/models.py
@@ -29,8 +29,8 @@ class PersonInfo(models.Model):
     affiliation = models.CharField(max_length=255, blank=True, help_text="Employer, university, sponsor, etc.")
     address = models.TextField(max_length=255, blank=True, help_text="Postal mailing address.")
     biography = models.TextField(blank=True, help_text="Short biography for use on leadership pages. Use plain text or reStructuredText markup.")
-    photo = models.ImageField(storage=NoLocationMigrationFileSystemStorage(location=settings.PHOTOS_DIR),upload_to=settings.PHOTOS_DIRNAME,blank=True, default=None)
-    photo_thumb = models.ImageField(storage=NoLocationMigrationFileSystemStorage(location=settings.PHOTOS_DIR),upload_to=settings.PHOTOS_DIRNAME,blank=True, default=None)
+    photo = models.ImageField(storage=NoLocationMigrationFileSystemStorage(), upload_to=settings.PHOTOS_DIRNAME, blank=True, default=None)
+    photo_thumb = models.ImageField(storage=NoLocationMigrationFileSystemStorage(), upload_to=settings.PHOTOS_DIRNAME, blank=True, default=None)
 
     def __unicode__(self):
         return self.plain_name()

From dd7e454401e5d2b15310cd961fcd0742558c1929 Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz <henrik@levkowetz.com>
Date: Thu, 7 Jul 2016 12:29:19 +0000
Subject: [PATCH 06/15] Added consistent navigation tabs for agenda-related
 pages, and added a floor-plan tab.  Tweaked the styling of by-room and
 by-type meeting tabs to be more consistent with the rest of the site.
 Factored out page header to the common header include file for some pages.  -
 Legacy-Id: 11537

---
 ietf/templates/meeting/agenda.html          |  36 ++-----
 ietf/templates/meeting/agenda_by_room.html  |  47 ++++-----
 ietf/templates/meeting/agenda_by_type.html  |  14 +--
 ietf/templates/meeting/floor-plan.html      | 106 ++++++++++++++++++++
 ietf/templates/meeting/meeting_heading.html |  71 +++++++++----
 ietf/templates/meeting/no-agenda.html       |   2 +-
 ietf/templates/meeting/room-view.html       |   2 +-
 7 files changed, 202 insertions(+), 76 deletions(-)
 create mode 100644 ietf/templates/meeting/floor-plan.html

diff --git a/ietf/templates/meeting/agenda.html b/ietf/templates/meeting/agenda.html
index ce30296e3..342045280 100644
--- a/ietf/templates/meeting/agenda.html
+++ b/ietf/templates/meeting/agenda.html
@@ -31,33 +31,17 @@
   {% origin %}
 
   <div class="row">
-    <div class="col-md-10">
-
-      {% include "meeting/meeting_heading.html" with meeting=schedule.meeting updated=updated %}
-
-      <p class="noprint h6 text-center panel panel-heading ">
-        {% if "-utc" in request.path %}
-          <a href="{% url 'ietf.meeting.views.agenda' num=schedule.meeting.number %}">Agenda in local timezone</a> |
-        {% else %}
-          {% comment %}<a href="{% url 'ietf.meeting.views.agenda' base='agenda-utc' %}">Agenda in UTC timezone</a> | {% endcomment %}
-          <a href="/meeting/agenda-utc">Agenda in UTC timezone</a> |
-        {% endif %}
-        <a href="{% url 'ietf.meeting.views.agenda' num=schedule.meeting.number ext='.txt' %}">Plaintext agenda</a> | 
-        <a href="https://tools.ietf.org/agenda/{{schedule.meeting.number}}/">Tools-style agenda</a>
-        {% if user|has_role:"Secretariat,Area Director,IAB" %}
-          |
-          {% if schedule != meeting.agenda %}
-            <a href="{% url 'ietf.meeting.views.agenda_by_room' num=schedule.meeting.number name=schedule.name owner=schedule.owner.email %}">List by Room</a> |
-            <a href="{% url 'ietf.meeting.views.agenda_by_type' num=schedule.meeting.number name=schedule.name owner=schedule.owner.email %}">List by Type</a> |
-            <a href="{% url 'ietf.meeting.views.room_view' num=schedule.meeting.number name=schedule.name owner=schedule.owner.email %}">Room Grid</a>
-          {% else %}
-            <a href="{% url 'ietf.meeting.views.agenda_by_room' num=schedule.meeting.number%}">List by Room</a> |
-            <a href="{% url 'ietf.meeting.views.agenda_by_type' num=schedule.meeting.number%}">List by Type</a> |
-            <a href="{% url 'ietf.meeting.views.room_view' num=schedule.meeting.number%}">Room Grid</a>
-          {% endif %}
-        {% endif %}
-      </p>
+    <div class="col-md-12">
+      {% if "-utc" in request.path %}
+        {% include "meeting/meeting_heading.html" with meeting=schedule.meeting updated=updated selected="agenda-utc" title_extra="(UTC)" %}
+      {% else %}
+        {% include "meeting/meeting_heading.html" with meeting=schedule.meeting updated=updated selected="agenda"     title_extra="" %}
+      {% endif %}
 
+    </div>
+  </div>
+  <div class="row">
+     <div class="col-md-10">
       {# cache this part for 5 minutes -- it takes 3-6 seconds to generate #}
       {% load cache %}
       {% cache 300 ietf_meeting_agenda_utc schedule.meeting.number request.path %}
diff --git a/ietf/templates/meeting/agenda_by_room.html b/ietf/templates/meeting/agenda_by_room.html
index 344c25016..f17ec7988 100644
--- a/ietf/templates/meeting/agenda_by_room.html
+++ b/ietf/templates/meeting/agenda_by_room.html
@@ -1,13 +1,13 @@
 {% extends "base.html" %}
 {% block morecss %}
 ul.daylist { list-style:none; padding-left:0; }
-li.daylistentry { font-size:162%; font-weight:700; }
+li h2 { font-weight: 600; margin-top: 0.5em; }
+li h3 { font-weight: 400; margin-top: 0.5em; }
 li.even { background-color:#EDF5FF; }
 li.odd { background-color:white; }
-ul.roomlist {list-style:none; padding-left:0; margin-bottom:20px;}
+ul.roomlist {list-style:none; margin-top: 0.5em; }
 li.roomlistentry { font-weight: 400; }
 ul.sessionlist { list-style:none; padding-left:2em; margin-bottom:10px;}
-li.sessionlistentry { font-size:62%; }
 
 .type-lead:after { content: " (DO NOT POST)"; color:red; }
 .type-offagenda:after { content:" (not published on agenda)"; }
@@ -17,25 +17,26 @@ li.sessionlistentry { font-size:62%; }
 {% block title %}Agenda for {{meeting}} by Room{% endblock %}
 
 {% block content %}
-{% include "meeting/meeting_heading.html" with meeting=schedule.meeting updated=meeting.updated %}
-<h1>Agenda for {{meeting}} by Room</h1>
-<ul class="daylist">
-{% for day,sessions in ss_by_day.items %}
-<li class="daylistentry {% cycle 'even' 'odd' %}">{{day|date:'l, j F Y'}}
-{% regroup sessions by timeslot.get_functional_location as room_list %}
-<ul class="roomlist">
-{% for room in room_list %}
-<li class="roomlistentry">{{room.grouper|default:"Location Unavailable"}}
-<ul class="sessionlist">
-{% for ss in room.list %}
-<li class="sessionlistentry type-{{ss.timeslot.type.slug}}">{{ss.timeslot.time|date:"H:i"}}-{{ss.timeslot.end_time|date:"H:i"}} {{ss.session.short_name}}</li>
-{% endfor %}
-</ul>
-</li>
-{% endfor %}
-</ul>
-</li>
-{% endfor %}
-</ul>
+
+  {% include "meeting/meeting_heading.html" with meeting=schedule.meeting updated=meeting.updated selected="by-room" title_extra="by Room" %}
+
+  <ul class="daylist">
+    {% for day,sessions in ss_by_day.items %}
+      <li class="daylistentry {% cycle 'even' 'odd' %}"><h2>{{day|date:'l, j F Y'}}</h2>
+	{% regroup sessions by timeslot.get_functional_location as room_list %}
+	<ul class="roomlist">
+	  {% for room in room_list %}
+	    <li class="roomlistentry"><h3>{{room.grouper|default:"Location Unavailable"}}</h3>
+	      <ul class="sessionlist">
+		{% for ss in room.list %}
+		  <li class="sessionlistentry type-{{ss.timeslot.type.slug}}">{{ss.timeslot.time|date:"H:i"}}-{{ss.timeslot.end_time|date:"H:i"}} {{ss.session.short_name}}</li>
+		{% endfor %}
+	      </ul>
+	    </li>
+	  {% endfor %}
+	</ul>
+      </li>
+    {% endfor %}
+  </ul>
 {% endblock %}
 
diff --git a/ietf/templates/meeting/agenda_by_type.html b/ietf/templates/meeting/agenda_by_type.html
index 128cf1177..32882f739 100644
--- a/ietf/templates/meeting/agenda_by_type.html
+++ b/ietf/templates/meeting/agenda_by_type.html
@@ -2,13 +2,15 @@
 {% block morecss %}
 
 ul.typelist { list-style:none; padding-left:0; }
-li.typelistentry { font-size:162%; font-weight:700; }
+li h2 { font-weight: 600; margin-top: 0.5em; }
+li h3 { font-weight: 400; margin-top: 0.5em; }
 li.even { background-color:#EDF5FF; }
 li.odd { background-color:white; }
 ul.daylist {list-style:none; padding-left:0; margin-bottom:20px;}
 li.daylistentry { margin-left:2em; font-weight: 400; }
 
-.sessiontable {margin-left:2em; font-size:62%;}
+
+.sessiontable {margin-left: 2em; }
 .sessiontable td {padding-right: 1em;}
 
 .typelabel { font-size:162%; font-weight:700; }
@@ -25,18 +27,18 @@ li.daylistentry { margin-left:2em; font-weight: 400; }
 {% block title %}Agenda for {{meeting}} by Session Type{% endblock %}
 
 {% block content %}
-{% include "meeting/meeting_heading.html" with meeting=schedule.meeting updated=meeting.updated %}
-<h1>Agenda for {{meeting}} by Session Type</h1>
+{% include "meeting/meeting_heading.html" with meeting=schedule.meeting updated=meeting.updated selected="by-type"  title_extra="by Session Type" %}
+
 {% regroup assignments by session.type.slug as type_list %}
 <ul class="typelist">
 {% for type in type_list %}
   <li class="typelistentry {% cycle 'even' 'odd' %}">
-    {{type.grouper}} {% if schedule == meeting.agenda %}<a id="ical-link" class="btn btn-primary" href="{% url "ietf.meeting.views.agenda_by_type_ics" num=meeting.number type=type.grouper %}">Download to Calendar</a>{% endif %}
+    <h2>{{type.grouper|title}}</h2> {% if schedule == meeting.agenda %}<a id="ical-link" class="btn btn-primary" href="{% url "ietf.meeting.views.agenda_by_type_ics" num=meeting.number type=type.grouper %}">Download to Calendar</a>{% endif %}
     <ul class="daylist">
     {% regroup type.list by timeslot.time|date:"l Y-M-d" as daylist %}
     {% for day in daylist %}
       <li class="daylistentry">
-        {{ day.grouper }}
+        <h3>{{ day.grouper }}</h3>
         <table class="sessiontable">
         {% for ss in day.list %}
           <tr>
diff --git a/ietf/templates/meeting/floor-plan.html b/ietf/templates/meeting/floor-plan.html
new file mode 100644
index 000000000..1237f9e07
--- /dev/null
+++ b/ietf/templates/meeting/floor-plan.html
@@ -0,0 +1,106 @@
+{% extends "base.html" %}
+{# Copyright The IETF Trust 2015, All Rights Reserved #}
+{% load origin %}
+
+{% load ietf_filters %}
+{% load staticfiles %}
+
+{% block title %}
+  IETF {{ schedule.meeting.number }} meeting agenda
+  {% if "-utc" in request.path %}
+    (UTC)
+  {% endif %}
+{% endblock %}
+
+{% block morecss %}
+.floor-plan {
+    position: relative;
+    top: 0;
+    left: 0;
+}
+.rooms a {
+    text-decoration: underline;
+}
+{% endblock %}
+
+{% block bodyAttrs %}data-spy="scroll" data-target="#affix"{% endblock %}
+
+{% block content %}
+  {% origin %}
+
+  <div class="row">
+    <div class="col-md-12" >
+
+      {% include "meeting/meeting_heading.html" with meeting=schedule.meeting selected="floor-plan" title_extra="Floor Plan" %}
+
+    </div>
+  </div>
+
+
+    <div class="row">
+      <div class="col-md-10">
+	{% for floor in floors %}
+	  <div class="anchor-target" id="{{floor.name|slugify}}"></div>
+	  <h3>{{ floor.name }}</h3>
+	  <div class="floor-plan">
+	    <img class="col-md-12" src="{{ floor.image.url }}" >
+	    <!-- We need as many of these as we can have individual rooms combining into one -->
+	    <div id="{{floor.name|slugify}}-arrowdiv"  style="position: absolute; left: 0; top: 67.5px; visibility: hidden;"><img id="arrow" src="{% static 'ietf/images/arrow-ani.gif' %}"></div>
+	    <div id="{{floor.name|slugify}}-arrowdiv1" style="position: absolute; left: 0; top: 67.5px; visibility: hidden;"><img id="arrow" src="{% static 'ietf/images/arrow-ani.gif' %}"></div>
+	    <div id="{{floor.name|slugify}}-arrowdiv2" style="position: absolute; left: 0; top: 67.5px; visibility: hidden;"><img id="arrow" src="{% static 'ietf/images/arrow-ani.gif' %}"></div>
+	    <div id="{{floor.name|slugify}}-arrowdiv3" style="position: absolute; left: 0; top: 67.5px; visibility: hidden;"><img id="arrow" src="{% static 'ietf/images/arrow-ani.gif' %}"></div>
+	  </div>
+	  <div class="rooms">
+	    {% for room in floor.room_set.all %}
+	      {#<a href="javascript: setarrow('{{room.name|slugify}}','{{floor.name|slugify}}')">{{ room.name|nbsp }}</a>#}
+	      {{ room.name|nbsp }}
+	    {% endfor %}
+	  </div>
+	  <div class="row"></div>
+	{% endfor %}
+      </div>
+      <div class="col-md-2 hidden-print bs-docs-sidebar" id="affix">
+	<ul class="nav nav-pills nav-stacked small" data-spy="affix">
+	  {% for floor in floors %}
+	  <li><a href="#{{floor.name|slugify}}">{{ floor.name }}</a></li>
+	  {% endfor %}
+	</ul>
+      </div>
+    </div>
+{% endblock %}
+
+{% block js %}
+{% with meeting=schedule.meeting %}
+  <script src="{% static 'ietf/js/room_params.js' %}"></script>
+  <script>
+    // These must match the 'arrowdiv' divs above
+    var arrowsuffixlist = [ '', '1', '2', '3' ];
+
+    function roommap(nm)
+    {
+	var c = findroom(nm);
+	if (c) return nm;
+	var m = suffixmap(nm);
+	// alert("m=" + m);
+	return m;
+    }
+
+    function findroom(nm)
+    {
+	var left = 0, top = 0, right = 0, bottom = 0;
+
+	if (0) { }
+	{% for room in meeting.room_set.all %}
+	else if (nm == '{{room.name|slugify}}') { left = {{room.left}}; top = {{room.top}}; right = {{room.right}}; bottom = {{room.bottom}}; }{% endfor %}
+
+	{% for room in meeting.room_set.all %}{% if room.functional_display_name %}
+	else if (nm == '{{room.functional_name|slugify}}') { left = {{room.left}}; top = {{room.top}}; right = {{room.right}}; bottom = {{room.bottom}}; }{% endif %}{% endfor %}
+
+	else return null;
+
+	// alert("nm=" + nm + ",left=" + left + ",top=" + top + ",r=" + right + ",b=" + bottom);
+	return [left, top, right, bottom];
+    }
+  </script>
+{% endwith %}
+{% endblock %}
diff --git a/ietf/templates/meeting/meeting_heading.html b/ietf/templates/meeting/meeting_heading.html
index e756afd96..0d0aab7ec 100644
--- a/ietf/templates/meeting/meeting_heading.html
+++ b/ietf/templates/meeting/meeting_heading.html
@@ -1,24 +1,57 @@
 {# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %}
 {# assumes meeting is in context #}
-{% if schedule != meeting.agenda %}
-<h3 class="alert alert-danger text-center">
-   This is schedule {{schedule.owner.email}}/{{ schedule.name }}, not the official schedule.
-</h3>
-{% endif %}
+{% load origin %}
+{% load ietf_filters %}
 
-<h1>
-  IETF {{ meeting.number }} Meeting Agenda
-  <br>
-  <small>
-    {{ meeting.city }}, {{ meeting.date|date:"F j" }} -
-    {% if meeting.date.month != meeting.end_date.month %}
-      {{ meeting.end_date|date:"F " }}
-    {% endif %}
-    {{ meeting.end_date|date:"j, Y" }}
-    <span class="pull-right">
-     Updated {{ updated|date:"Y-m-d \a\t G:i:s (T)" }}
-    </span>
+      {% origin %}
 
-  </small>
+      {% if schedule != meeting.agenda %}
+      <h3 class="alert alert-danger text-center">
+	 This is schedule {{schedule.owner.email}}/{{ schedule.name }}, not the official schedule.
+      </h3>
+      {% endif %}
 
-</h1>
+      <h1>
+	IETF {{ meeting.number }} Meeting Agenda {{ title_extra }}
+	<br>
+	<small>
+	  {{ meeting.city }}, {{ meeting.date|date:"F j" }} -
+	  {% if meeting.date.month != meeting.end_date.month %}
+	    {{ meeting.end_date|date:"F " }}
+	  {% endif %}
+	  {{ meeting.end_date|date:"j, Y" }}
+	  {% if updated %}
+	  <span class="pull-right">
+	   Updated {{ updated|date:"Y-m-d \a\t G:i:s (T)" }}
+	  </span>
+	  {% endif %}
+	</small>
+      </h1>
+
+      <ul class="nav nav-tabs" role="tablist">
+	<li {% if selected == "agenda" %}class="active"{% endif %}>
+	    <a href="{% url 'ietf.meeting.views.agenda' num=schedule.meeting.number %}">Agenda</a></li>
+	<li {% if selected == "agenda-utc" %}class="active"{% endif %}>
+	    <a href="{% url 'ietf.meeting.views.agenda' num=schedule.meeting.number utc='-utc' %}">UTC Agenda</a></li>
+	{% if user|has_role:"Secretariat,Area Director,IAB" %}
+          {% if schedule != meeting.agenda %}
+	    <li {% if selected == "by-room" %}class="active"{% endif %}>
+		<a href="{% url 'ietf.meeting.views.agenda_by_room' num=schedule.meeting.number name=schedule.name owner=schedule.owner.email %}">by Room</a></li>
+	    <li {% if selected == "by-type" %}class="active"{% endif %}>
+		<a href="{% url 'ietf.meeting.views.agenda_by_type' num=schedule.meeting.number name=schedule.name owner=schedule.owner.email %}">by Type</a></li>
+	    <li {% if selected == "room-view" %}class="active"{% endif %}>
+		<a href="{% url 'ietf.meeting.views.room_view'      num=schedule.meeting.number name=schedule.name owner=schedule.owner.email %}">Room grid</a></li>
+	  {% else %}
+	    <li {% if selected == "by-room" %}class="active"{% endif %}>
+		<a href="{% url 'ietf.meeting.views.agenda_by_room' num=schedule.meeting.number %}">by Room</a></li>
+	    <li {% if selected == "by-type" %}class="active"{% endif %}>
+		<a href="{% url 'ietf.meeting.views.agenda_by_type' num=schedule.meeting.number %}">by Type</a></li>
+	    <li {% if selected == "room-view" %}class="active"{% endif %}>
+		<a href="{% url 'ietf.meeting.views.room_view'      num=schedule.meeting.number %}">Room grid</a></li>
+	  {% endif %}
+	{% endif %}
+	<li {% if selected == "floor-plan" %}class="active"{% endif %}>
+	    <a href="{% url 'ietf.meeting.views.floor_plan' num=schedule.meeting.number %}">Floor plan</a></li>
+	<li><a href="{% url 'ietf.meeting.views.agenda' num=schedule.meeting.number ext='.txt' %}">Plaintext</a></li>
+	<li><a href="https://tools.ietf.org/agenda/{{schedule.meeting.number}}/">Tools-style &raquo;</a></li>
+      </ul>
diff --git a/ietf/templates/meeting/no-agenda.html b/ietf/templates/meeting/no-agenda.html
index 658fa5a3b..2961ba9f2 100644
--- a/ietf/templates/meeting/no-agenda.html
+++ b/ietf/templates/meeting/no-agenda.html
@@ -6,7 +6,7 @@
 
 {% block content %}
   {% origin %}
-  {% include "meeting/meeting_heading.html" %}
+  {% include "meeting/meeting_heading.html" with title_extra="" selected="" %}
 
   <div class="jumbotron">
     <p>There is no agenda available yet.</p>
diff --git a/ietf/templates/meeting/room-view.html b/ietf/templates/meeting/room-view.html
index 647e8833d..3c6692460 100644
--- a/ietf/templates/meeting/room-view.html
+++ b/ietf/templates/meeting/room-view.html
@@ -582,7 +582,7 @@
   </script>
 </head>
 <body onload="draw_calendar()" onresize="draw_calendar()" id="body">
-  <div id="mtgheader" style="overflow:auto">{% include "meeting/meeting_heading.html" with meeting=schedule.meeting updated=schedule.meeting.updated %}</div>
+  <div id="mtgheader" style="overflow:auto">{% include "meeting/meeting_heading.html" with meeting=schedule.meeting updated=schedule.meeting.updated selected="room-view" title_extra="Room Grid" %}</div>
   <div id="daycontainer" role="tabpanel">
     <ul id="daytabs" class="nav nav-tabs" role="tablist">
     {% for day in days %}

From 874000fd6deb3a97dca085a9da0434e8b7d9fbb4 Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz <henrik@levkowetz.com>
Date: Thu, 7 Jul 2016 12:30:50 +0000
Subject: [PATCH 07/15] Added z-index settings to make sure that the tab links
 aren't covered by other elements.  - Legacy-Id: 11538

---
 ietf/static/ietf/css/ietf.css | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css
index d12425897..c4bf16058 100644
--- a/ietf/static/ietf/css/ietf.css
+++ b/ietf/static/ietf/css/ietf.css
@@ -110,6 +110,7 @@ div.anchor-target:before {
   height: 65px;
   margin-top: -65px;
 }
+div.anchor-target { z-index: 0; }
 
 /* Make the panel title font normally large */
 .panel-title { font-size: 14px }
@@ -124,6 +125,7 @@ label.required:after { content: "\2217"; color: #a94442; font-weight: bold; }
 
 /* Add some margin to tabs */
 .nav-tabs[role=tablist] { margin-bottom: 0.7em; }
+.nav a { z-index: 100; }
 
 /* Styles needed for the ballot table */
 /*

From c84c18926a23bfd49372193d94cf4902d0f6db3e Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz <henrik@levkowetz.com>
Date: Thu, 7 Jul 2016 12:31:30 +0000
Subject: [PATCH 08/15] Added static media for floorplans.  - Legacy-Id: 11539

---
 ietf/static/ietf/images/arrow-ani.gif | Bin 0 -> 1803 bytes
 ietf/static/ietf/js/room_params.js    | 173 ++++++++++++++++++++++++++
 2 files changed, 173 insertions(+)
 create mode 100644 ietf/static/ietf/images/arrow-ani.gif
 create mode 100644 ietf/static/ietf/js/room_params.js

diff --git a/ietf/static/ietf/images/arrow-ani.gif b/ietf/static/ietf/images/arrow-ani.gif
new file mode 100644
index 0000000000000000000000000000000000000000..ba6c467a25b2bbb749bcc7d35695d590057ce68e
GIT binary patch
literal 1803
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

literal 0
HcmV?d00001

diff --git a/ietf/static/ietf/js/room_params.js b/ietf/static/ietf/js/room_params.js
new file mode 100644
index 000000000..dd7efa720
--- /dev/null
+++ b/ietf/static/ietf/js/room_params.js
@@ -0,0 +1,173 @@
+var verbose = 0;
+
+function suffixmap(nm)
+// Given a name like "foo-ab" or "foo-X-and-Y", change it to the "list-of-room-names" format, "foo-a/foo-b".
+{
+    var andsuffix = /^(.*-)([^-]+)-and-(.*)$/;
+    var andMatch = andsuffix.exec(nm);
+    if (andMatch && andMatch[0] != '') {
+	nm = andMatch[1] + andMatch[2] + "-" + andMatch[3];
+    }
+    // xyz-a/b/c => xyz-a/xyz-b/xyz-c
+    var abcsuffix = /^(.*)-([a-h0-9]+)[-\/]([a-h0-9]+)([-\/][a-h0-9]+)?$/;
+    var suffixMatch = abcsuffix.exec(nm);
+    if (verbose) alert("nm=" + nm);
+    if (suffixMatch && suffixMatch[0] != '') {
+	if (verbose) alert("matched");
+	nm = suffixMatch[1] + "-" + suffixMatch[2] + "/" +
+	     suffixMatch[1] + "-" + suffixMatch[3];
+	if (verbose) alert("nm=>" + nm);
+	if (suffixMatch[4] && suffixMatch[4] != '')
+	     nm += "/" + suffixMatch[1] + "-" + suffixMatch[4];
+	if (verbose) alert("nm=>" + nm);
+    }
+    // xyz-abc => xyz-a/xyz-b/xyz-c
+    abcsuffix = /^(.*)-([a-h])([a-h]+)([a-h])?$/;
+    var suffixMatch = abcsuffix.exec(nm);
+    if (suffixMatch && suffixMatch[0] != '') {
+	nm = suffixMatch[1] + "-" + suffixMatch[2] + "/" +
+	     suffixMatch[1] + "-" + suffixMatch[3];
+	if (suffixMatch[4] && suffixMatch[4] != '')
+	     nm += "/" + suffixMatch[1] + "-" + suffixMatch[4];
+    }
+    if (verbose) alert("suffixmap returning: " + nm);
+    return nm;
+}
+
+function roomcoords(nm)
+// Find the coordinates of a room or list of room names separated by "/".
+// Calls the function findroom() to get the coordinates for a specific room.
+{
+    if (!nm) return null;
+
+    if (nm.match("/")) {
+        var nms = nm.split("/");
+	var nm0 = findroom(nms[0]);
+	if (!nm0) return null;
+	for (var i = 1; i < nms.length; i++) {
+	    var nmi = roomcoords(nms[i]);
+	    if (!nmi) return null;
+	    if (nmi[0] < nm0[0]) nm0[0] = nmi[0];
+	    if (nmi[1] < nm0[1]) nm0[1] = nmi[1];
+	    if (nmi[2] > nm0[2]) nm0[2] = nmi[2];
+	    if (nmi[3] > nm0[3]) nm0[3] = nmi[3];
+	}
+	return [nm0[0], nm0[1], nm0[2], nm0[3]];
+    } else {
+	return findroom(nm);
+    }
+}
+
+function setarrow(nm, fl)
+// Place an arrow at the center of a given room name (or list of room names separated by "/").
+{
+    for (var i = 0; i < arrowsuffixlist.length; i++) {
+       removearrow(arrowsuffixlist[i], fl);
+    }
+    for (var i = 0; i < arguments.length; i+=2) {
+       nm = roommap(arguments[i]);
+       if (verbose) alert("nm=" + nm);
+       var rooms = nm.split(/[|]/);
+       for (var j = 0; j < rooms.length; j++) {
+	  var room = rooms[j];
+	  var ret = roomcoords(room);
+	  if (verbose) alert("roomcoords returned: " + ret);
+	  if (!ret) continue;
+
+	  var left = ret[0], top = ret[1], right = ret[2], bottom = ret[3], offsetleft = -25, offsettop = -25;
+	  if (verbose) alert("left=" + left + ", top=" + top + ", right=" + right + ", bottom=" + bottom);
+	  //alert("left=" + left + ", top=" + top + ", right=" + right + ", bottom=" + bottom);
+	  var arrowdiv = fl+'-arrowdiv' + (j > 0 ? j : "");
+	  //if (verbose) alert("arrowdiv: " + arrowdiv);
+	  var adiv = document.getElementById(arrowdiv);
+	  // if (verbose) alert("looking for 'arrowdiv'+" + j);
+	  if (adiv) {
+	      //if (verbose) alert("adiv found");
+	      adiv.style.left = left + (right - left) / 2 + offsetleft;
+	      adiv.style.top = top + (bottom - top) / 2 + offsettop;
+	      adiv.style.visibility = "visible";
+	  }
+      }
+   }
+}
+
+function removearrow(which, fl)
+{
+    for (var i = 0; i < arguments.length; i++) {
+       var which = arguments[i];
+       var arrowdiv = fl+'-arrowdiv' + (which ? which : "");
+       var adiv = document.getElementById(arrowdiv);
+       // if (verbose) alert("looking for '" + arrowdiv + "'");
+       if (adiv) {
+	   // if (verbose) alert("adiv found");
+	   adiv.style.left = -500;
+	   adiv.style.top = -500;
+	   adiv.style.visibility = "hidden";
+       }
+   }
+}
+
+function setarrowlist(which, names)
+{
+   for (var i = 1; i < arguments.length; i++) {
+      setarrow(arguments[i], which);
+   }
+}
+
+function QueryString()
+// Create a QueryString object
+{
+    // get the query string, ignore the ? at the front.
+    var querystring = location.search.substring(1);
+
+    // parse out name/value pairs separated via &
+    var args = querystring.split('&');
+
+    // split out each name = value pair
+    for (var i = 0; i < args.length; i++) {
+        var pair = args[i].split('=');
+
+        // Fix broken unescaping
+        var temp = unescape(pair[0]).split('+');
+        var name_ = temp.join(' ');
+
+        var value_ = '';
+        if (typeof pair[1] == 'string') {
+            temp = unescape(pair[1]).split('+');
+            value_ = temp.join(' ');
+        }
+
+        this[name_] = value_;
+    }
+
+    this.get = function(nm, def) {
+        var value_ = this[nm];
+        if (value_ == null) return def;
+        else return value_;
+    };
+}
+
+function checkParams()
+// Check the parameters for one named "room". If found, call setarrow(room).
+{
+    var querystring = new QueryString();
+    var room = querystring.get("room");
+    if (room && room != "") setarrow(room);
+}
+
+// new functions
+function located(loc)
+{
+   if (loc.civic && loc.civic.ROOM) {
+      // map from "TerminalRoom" to "terminal-room" as necessary.
+      setarrow(loc.civic.ROOM.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(), "-green");
+   }
+}
+
+// this needs to be called onload
+function automaticarrow()
+{
+    //   if (navigator.geolocation) {
+    //      navigator.geolocation.getCurrentPosition(located);
+    //   }
+}

From 9e5d99095445012335fc7a530bbbcd40923f96d9 Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz <henrik@levkowetz.com>
Date: Thu, 7 Jul 2016 12:37:09 +0000
Subject: [PATCH 09/15] Added meeting FloorPlan model and added location
 parameters and ordering to the Room model.  - Legacy-Id: 11540

---
 ietf/meeting/admin.py                         | 10 +++-
 ...0025_add_floorplan_and_room_coordinates.py | 59 +++++++++++++++++++
 ietf/meeting/models.py                        | 43 ++++++++++++++
 ietf/meeting/resources.py                     | 20 ++++++-
 4 files changed, 129 insertions(+), 3 deletions(-)
 create mode 100644 ietf/meeting/migrations/0025_add_floorplan_and_room_coordinates.py

diff --git a/ietf/meeting/admin.py b/ietf/meeting/admin.py
index 31a842c72..e575eeef0 100644
--- a/ietf/meeting/admin.py
+++ b/ietf/meeting/admin.py
@@ -1,9 +1,10 @@
 from django.contrib import admin
 
-from ietf.meeting.models import Meeting, Room, Session, TimeSlot, Constraint, Schedule, SchedTimeSessAssignment, ResourceAssociation
+from ietf.meeting.models import (Meeting, Room, Session, TimeSlot, Constraint, Schedule,
+    SchedTimeSessAssignment, ResourceAssociation, FloorPlan)
 
 class RoomAdmin(admin.ModelAdmin):
-    list_display = ["id", "meeting", "name", "capacity", ]
+    list_display = ["id", "meeting", "name", "capacity", "x1", "y1", "x2", "y2", ]
     list_filter = ["meeting"]
     ordering = ["-meeting"]
 
@@ -98,3 +99,8 @@ admin.site.register(SchedTimeSessAssignment, SchedTimeSessAssignmentAdmin)
 class ResourceAssociationAdmin(admin.ModelAdmin):
     list_display = ["desc", "icon", "desc", ]
 admin.site.register(ResourceAssociation, ResourceAssociationAdmin)
+
+class FloorPlanAdmin(admin.ModelAdmin):
+    list_display = ['id', 'meeting', 'name', 'order', 'image', ]
+    raw_id_fields = ['meeting', ]
+admin.site.register(FloorPlan, FloorPlanAdmin)
diff --git a/ietf/meeting/migrations/0025_add_floorplan_and_room_coordinates.py b/ietf/meeting/migrations/0025_add_floorplan_and_room_coordinates.py
new file mode 100644
index 000000000..8c41e29df
--- /dev/null
+++ b/ietf/meeting/migrations/0025_add_floorplan_and_room_coordinates.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import ietf.utils.storage
+import ietf.meeting.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('meeting', '0024_migrate_interim_meetings'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='FloorPlan',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('name', models.CharField(max_length=255)),
+                ('order', models.SmallIntegerField()),
+                ('image', models.ImageField(default=None, storage=ietf.utils.storage.NoLocationMigrationFileSystemStorage(location=None), upload_to=ietf.meeting.models.floorplan_path, blank=True)),
+                ('meeting', models.ForeignKey(to='meeting.Meeting')),
+            ],
+            options={
+            },
+            bases=(models.Model,),
+        ),
+        migrations.AddField(
+            model_name='room',
+            name='floorplan',
+            field=models.ForeignKey(default=None, blank=True, to='meeting.FloorPlan', null=True),
+            preserve_default=True,
+        ),
+        migrations.AddField(
+            model_name='room',
+            name='x1',
+            field=models.SmallIntegerField(default=None, null=True, blank=True),
+            preserve_default=True,
+        ),
+        migrations.AddField(
+            model_name='room',
+            name='x2',
+            field=models.SmallIntegerField(default=None, null=True, blank=True),
+            preserve_default=True,
+        ),
+        migrations.AddField(
+            model_name='room',
+            name='y1',
+            field=models.SmallIntegerField(default=None, null=True, blank=True),
+            preserve_default=True,
+        ),
+        migrations.AddField(
+            model_name='room',
+            name='y2',
+            field=models.SmallIntegerField(default=None, null=True, blank=True),
+            preserve_default=True,
+        ),
+    ]
diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py
index 5db515988..67bc8fb21 100644
--- a/ietf/meeting/models.py
+++ b/ietf/meeting/models.py
@@ -25,6 +25,7 @@ from ietf.group.models import Group
 from ietf.group.utils import can_manage_materials
 from ietf.name.models import MeetingTypeName, TimeSlotTypeName, SessionStatusName, ConstraintName, RoomResourceName
 from ietf.person.models import Person
+from ietf.utils.storage import NoLocationMigrationFileSystemStorage
 
 countries = pytz.country_names.items()
 countries.sort(lambda x,y: cmp(x[1], y[1]))
@@ -274,6 +275,8 @@ class Meeting(models.Model):
     class Meta:
         ordering = ["-date", "id"]
 
+# === Rooms, Resources, Floorplans =============================================
+
 class ResourceAssociation(models.Model):
     name = models.ForeignKey(RoomResourceName)
     #url  = models.UrlField()       # not sure what this was for.
@@ -298,6 +301,15 @@ class Room(models.Model):
     capacity = models.IntegerField(null=True, blank=True)
     resources = models.ManyToManyField(ResourceAssociation, blank = True)
     session_types = models.ManyToManyField(TimeSlotTypeName, blank = True)
+    # floorplan-related properties
+    floorplan = models.ForeignKey('FloorPlan', null=True, blank=True, default=None)
+    # floorplan: room pixel position : (0,0) is top left of image, (xd, yd)
+    # is room width, height.
+    x1 = models.SmallIntegerField(null=True, blank=True, default=None)
+    y1 = models.SmallIntegerField(null=True, blank=True, default=None)
+    x2 = models.SmallIntegerField(null=True, blank=True, default=None)
+    y2 = models.SmallIntegerField(null=True, blank=True, default=None)
+    # end floorplan-related stuff
 
     def __unicode__(self):
         return "%s size: %s" % (self.name, self.capacity)
@@ -332,6 +344,36 @@ class Room(models.Model):
             'capacity':             self.capacity,
             }
 
+    def left(self):
+        return min(self.x1, self.x2) if (self.x1 and self.x2) else 0
+    def top(self):
+        return min(self.y1, self.y2) if (self.y1 and self.y2) else 0
+    def right(self):
+        return max(self.x1, self.x2) if (self.x1 and self.x2) else 0
+    def bottom(self):
+        return max(self.y1, self.y2) if (self.y1 and self.y2) else 0
+    def functional_display_name(self):
+        if not self.functional_name:
+            return ""
+        if self.functional_name.lower().startswith('breakout'):
+            return ""
+        if self.functional_name[0].isdigit():
+            return ""
+        return self.functional_name
+    class Meta:
+        ordering = ["-meeting", "name"]
+
+def floorplan_path(instance, filename):
+    root, ext = os.path.splitext(filename)
+    return u"%s/floorplan-%s-%s%s" % (settings.FLOORPLAN_MEDIA_DIR, instance.meeting.number, slugify(instance.name), ext)
+
+class FloorPlan(models.Model):
+    name    = models.CharField(max_length=255)
+    meeting = models.ForeignKey(Meeting)
+    order   = models.SmallIntegerField()
+    image   = models.ImageField(storage=NoLocationMigrationFileSystemStorage(), upload_to=floorplan_path, blank=True, default=None)
+
+# === Schedules, Sessions, Timeslots and Assignments ===========================
 
 class TimeSlot(models.Model):
     """
@@ -1307,3 +1349,4 @@ class Session(models.Model):
         if self.badness_test(1):
             self.badness_log(1, "badgroup: %s badness = %u\n" % (self.group.acronym, badness))
         return badness
+
diff --git a/ietf/meeting/resources.py b/ietf/meeting/resources.py
index e83e6bbca..2ba4456b6 100644
--- a/ietf/meeting/resources.py
+++ b/ietf/meeting/resources.py
@@ -8,7 +8,7 @@ from tastypie.cache import SimpleCache
 from ietf import api
 
 from ietf.meeting.models import ( Meeting, ResourceAssociation, Constraint, Room, Schedule, Session,
-                                TimeSlot, SchedTimeSessAssignment, SessionPresentation )
+                                TimeSlot, SchedTimeSessAssignment, SessionPresentation, FloorPlan )
 
 from ietf.name.resources import MeetingTypeNameResource
 class MeetingResource(ModelResource):
@@ -83,11 +83,28 @@ class ConstraintResource(ModelResource):
         }
 api.meeting.register(ConstraintResource())
 
+class FloorPlanResource(ModelResource):
+    meeting          = ToOneField(MeetingResource, 'meeting')
+    class Meta:
+        queryset = FloorPlan.objects.all()
+        serializer = api.Serializer()
+        cache = SimpleCache()
+        #resource_name = 'floorplan'
+        filtering = { 
+            "id": ALL,
+            "name": ALL,
+            "order": ALL,
+            "image": ALL,
+            "meeting": ALL_WITH_RELATIONS,
+        }
+api.meeting.register(FloorPlanResource())
+
 from ietf.name.resources import TimeSlotTypeNameResource
 class RoomResource(ModelResource):
     meeting          = ToOneField(MeetingResource, 'meeting')
     resources        = ToManyField(ResourceAssociationResource, 'resources', null=True)
     session_types    = ToManyField(TimeSlotTypeNameResource, 'session_types', null=True)
+    floorplan        = ToOneField(FloorPlanResource, 'floorplan', null=True)
     class Meta:
         cache = SimpleCache()
         queryset = Room.objects.all()
@@ -101,6 +118,7 @@ class RoomResource(ModelResource):
             "meeting": ALL_WITH_RELATIONS,
             "resources": ALL_WITH_RELATIONS,
             "session_types": ALL_WITH_RELATIONS,
+            "floorplan": ALL_WITH_RELATIONS,
         }
 api.meeting.register(RoomResource())
 

From 8de23cb7f7a18356f6b5a5536108d0460ccf8daf Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz <henrik@levkowetz.com>
Date: Thu, 7 Jul 2016 12:37:51 +0000
Subject: [PATCH 10/15] Fixed some admin plural forms.  - Legacy-Id: 11541

---
 .../migrations/0004_auto_20160704_0728.py     | 22 +++++++++++++++++++
 1 file changed, 22 insertions(+)
 create mode 100644 ietf/mailinglists/migrations/0004_auto_20160704_0728.py

diff --git a/ietf/mailinglists/migrations/0004_auto_20160704_0728.py b/ietf/mailinglists/migrations/0004_auto_20160704_0728.py
new file mode 100644
index 000000000..4cec6b44d
--- /dev/null
+++ b/ietf/mailinglists/migrations/0004_auto_20160704_0728.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('mailinglists', '0003_import_subscribers'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='subscribed',
+            options={'verbose_name_plural': 'Subscribed'},
+        ),
+        migrations.AlterModelOptions(
+            name='whitelisted',
+            options={'verbose_name_plural': 'Whitelisted'},
+        ),
+    ]

From 1d5e0ec41a3c3370257626915ce4f915bc84005d Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz <henrik@levkowetz.com>
Date: Thu, 7 Jul 2016 12:38:39 +0000
Subject: [PATCH 11/15] Added a data migration to provide initial IETF-96
 floorplan data.  - Legacy-Id: 11542

---
 .../migrations/0026_add_floorplan_data.py     | 65 +++++++++++++++++++
 1 file changed, 65 insertions(+)
 create mode 100644 ietf/meeting/migrations/0026_add_floorplan_data.py

diff --git a/ietf/meeting/migrations/0026_add_floorplan_data.py b/ietf/meeting/migrations/0026_add_floorplan_data.py
new file mode 100644
index 000000000..38abc43a3
--- /dev/null
+++ b/ietf/meeting/migrations/0026_add_floorplan_data.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+floors = [
+    (1, "Berlin Intercontinental Floor 1",  1, 'floor/floorplan-96-berlin-intercontinental-floor-1.jpg'),
+    (2, "Berlin Intercontinental Floor 1",  2, 'floor/floorplan-96-berlin-intercontinental-floor-2.jpg'),
+    (3, "Berlin Intercontinental Floor 1",  14, 'floor/floorplan-96-berlin-intercontinental-floor-14.jpg'),
+]
+
+rooms = [
+    ("Bellevue",                 1,	176,	1348,	324,	1526),
+    ("Kaminzimmer",              1,	696,	820,	812,	1038),
+    ("Charlottenburg I",         1,	374,	320,	528,	400),
+    ("Charlottenburg II/III",	1,	374,	172,	528,	316),
+    ("Chess",                    2,	238,	614,	336,	782),
+    ("Glienicke",                1,	228,	1251,	324,	1310),
+    ("Hugos 360",                3,	801,	1346,	976,	1509),
+    ("King",                     2,	802,	1389,	890,	1508),
+    ("Koepenick I/II",           1,	370,	453,	458,	602),
+    ("Lincke",                   2,	40,	99,	532,	166),
+    ("Potsdam I",                1,	1228,	790,	1550,	994),
+    ("Potsdam I/III",            1,	1017,	792,	1550,	994),
+    ("Potsdam II",               1,	1311,	1036,	1536,	1142),
+    ("Potsdam III",              1,	1017,	792,	1228,	987),
+    ("Rook",                     2,	915,	1150,	1004,	1269),
+    ("Schoeneberg",              1,	369,	42,	534,	126),
+    ("Tegel",                    1,	201,	1088,	326,	1184),
+    ("Tiergarten",               1,	240,	612,	334,	780),
+    ("Wintergarten/Pavillion",	1,	466,	1038,	711,	1504),
+]
+
+def forward(apps, schema_editor):
+    FloorPlan = apps.get_model('meeting','FloorPlan')
+    Room = apps.get_model('meeting','Room')
+    Meeting = apps.get_model('meeting','Meeting')
+    meeting = Meeting.objects.get(number='96')
+    for item in floors:
+        id, name, order, image = item
+        f = FloorPlan(id=id, name=name, meeting=meeting, order=order, image=image)
+        f.save()
+
+    for item in rooms:
+        name, floor_id, x1, y1, x2, y2 = item
+        room = Room.objects.get(name=name, meeting=meeting)
+        room.floorplan_id = floor_id
+        room.x1 = x1
+        room.y1 = y1
+        room.x2 = x2
+        room.y2 = y2
+        room.save()
+
+def backward(apps, schema_editor):
+    pass
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('meeting', '0025_add_floorplan_and_room_coordinates'),
+    ]
+
+    operations = [
+        migrations.RunPython(forward,backward)
+    ]

From 8fee0318028ae23b48823340aa6663639a04128a Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz <henrik@levkowetz.com>
Date: Thu, 7 Jul 2016 12:39:46 +0000
Subject: [PATCH 12/15] Added a floorplan view function, and urls to go with
 it.  - Legacy-Id: 11543

---
 ietf/meeting/urls.py  |  4 +++-
 ietf/meeting/views.py | 16 ++++++++++++++--
 2 files changed, 17 insertions(+), 3 deletions(-)

diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py
index 03d3cdcf7..d9042af8d 100644
--- a/ietf/meeting/urls.py
+++ b/ietf/meeting/urls.py
@@ -52,7 +52,7 @@ type_ietf_only_patterns = [
 ]
 
 type_ietf_only_patterns_id_optional = [
-    url(r'^agenda(-utc)?(?P<ext>.html)?/?$',     views.agenda),
+    url(r'^agenda(?P<utc>-utc)?(?P<ext>.html)?/?$',     views.agenda),
     url(r'^agenda(?P<ext>.txt)$', views.agenda),
     url(r'^agenda(?P<ext>.csv)$', views.agenda),
     url(r'^agenda/edit$', views.edit_agenda),
@@ -61,6 +61,8 @@ type_ietf_only_patterns_id_optional = [
     url(r'^agenda.ics$', views.ical_agenda),
     url(r'^agenda/week-view(?:.html)?/?$', views.week_view),
     url(r'^agenda/room-view(?:.html)?/?$', views.room_view),
+    url(r'^agenda/floor-plan/?$', views.floor_plan),
+    url(r'^agenda/floor-plan/(?P<floor>[-a-z0-9_]+)/?$', views.floor_plan),
     url(r'^week-view(?:.html)?/?$', views.week_view),
     url(r'^room-view(?:.html)?/$', views.room_view),
     url(r'^materials(?:.html)?/$',     views.materials),
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index 4288b9a22..54c7327cd 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -32,7 +32,7 @@ from ietf.doc.models import Document, State, DocEvent
 from ietf.group.models import Group
 from ietf.group.utils import can_manage_materials
 from ietf.ietfauth.utils import role_required, has_role
-from ietf.meeting.models import Meeting, Session, Schedule, Room
+from ietf.meeting.models import Meeting, Session, Schedule, Room, FloorPlan
 from ietf.meeting.helpers import get_areas, get_person_by_email, get_schedule_by_name
 from ietf.meeting.helpers import build_all_agenda_slices, get_wg_name_list
 from ietf.meeting.helpers import get_all_assignments_from_schedule
@@ -393,7 +393,7 @@ def list_agendas(request, num=None ):
                                           })
 
 @ensure_csrf_cookie
-def agenda(request, num=None, name=None, base=None, ext=None, owner=None):
+def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""):
     base = base if base else 'agenda'
     ext = ext if ext else '.html'
     mimetype = {
@@ -1424,3 +1424,15 @@ def upcoming_ical(request):
     response['Content-Disposition'] = 'attachment; filename="upcoming.ics"'
     return response
     
+
+def floor_plan(request, num=None, floor=None, ):
+    meeting = get_meetings(num).first()
+    schedule = meeting.agenda
+    floors = FloorPlan.objects.filter(meeting=meeting).order_by('order')
+    if floor:
+        floors = floors.filter(name=floor)
+    return render(request, 'meeting/floor-plan.html', {
+            "schedule": schedule,
+            "number": num,
+            "floors": floors,
+        })

From 7f5863d2120afa8ac2c4c3038e994bd0f92e8d34 Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz <henrik@levkowetz.com>
Date: Thu, 7 Jul 2016 12:40:41 +0000
Subject: [PATCH 13/15] Added a FIXME comment about a test which can fail if
 run across the midnight date shift.  - Legacy-Id: 11544

---
 ietf/submit/tests.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py
index 3fb6316cb..5f74ec75b 100644
--- a/ietf/submit/tests.py
+++ b/ietf/submit/tests.py
@@ -546,6 +546,7 @@ class SubmitTests(TestCase):
 
         # edit
         mailbox_before = len(outbox)
+        # FIXME If this test is started before midnight, and ends after, it will fail
         document_date = datetime.date.today() - datetime.timedelta(days=-3)
         r = self.client.post(edit_url, {
             "edit-title": "some title",

From 2bebde1ebe55eed0e8310473dc730341aed89e42 Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz <henrik@levkowetz.com>
Date: Thu, 7 Jul 2016 12:41:27 +0000
Subject: [PATCH 14/15] Added a test for the floorplan view.  - Legacy-Id:
 11545

---
 ietf/meeting/factories.py   | 18 +++++++++++++++++-
 ietf/meeting/test_data.py   |  2 +-
 ietf/meeting/tests_views.py | 20 +++++++++++++++++++-
 3 files changed, 37 insertions(+), 3 deletions(-)

diff --git a/ietf/meeting/factories.py b/ietf/meeting/factories.py
index 5e33df5cb..ebff3a6c3 100644
--- a/ietf/meeting/factories.py
+++ b/ietf/meeting/factories.py
@@ -3,8 +3,9 @@ import random
 import datetime
 
 from django.db.models import Max
+from django.core.files.base import ContentFile
 
-from ietf.meeting.models import Meeting, Session, Schedule, TimeSlot, SessionPresentation
+from ietf.meeting.models import Meeting, Session, Schedule, TimeSlot, SessionPresentation, FloorPlan
 from ietf.group.factories import GroupFactory
 from ietf.person.factories import PersonFactory
 
@@ -106,3 +107,18 @@ class SessionPresentationFactory(factory.DjangoModelFactory):
     def rev(self):
         return self.document.rev
 
+class FloorPlanFactory(factory.DjangoModelFactory):
+    class Meta:
+        model = FloorPlan
+
+    name = factory.Sequence(lambda n: u'Venue Floor %d' % n)
+    meeting = factory.SubFactory(MeetingFactory)
+    order = factory.Sequence(lambda n: n)
+    image = factory.LazyAttribute(
+            lambda _: ContentFile(
+                factory.django.ImageField()._make_data(
+                    {'width': 1024, 'height': 768}
+                ), 'floorplan.jpg'
+            )
+        )
+        
diff --git a/ietf/meeting/test_data.py b/ietf/meeting/test_data.py
index b5ed80574..5475900c6 100644
--- a/ietf/meeting/test_data.py
+++ b/ietf/meeting/test_data.py
@@ -40,7 +40,7 @@ def make_meeting_test_data():
     unofficial_schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-unofficial-agenda", visible=True, public=True)
     pname = RoomResourceName.objects.create(name='projector',slug='proj')
     projector = ResourceAssociation.objects.create(name=pname,icon="notfound.png",desc="Basic projector")
-    room = Room.objects.create(meeting=meeting, name="Test Room", capacity=123)
+    room = Room.objects.create(meeting=meeting, name="Test Room", capacity=123, functional_name="Testing Ground")
     breakfast_room = Room.objects.create(meeting=meeting, name="Breakfast Room", capacity=40)
     room.session_types.add("session")
     breakfast_room.session_types.add("lead")
diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py
index 1fe43fecc..d2eedfbbf 100644
--- a/ietf/meeting/tests_views.py
+++ b/ietf/meeting/tests_views.py
@@ -26,7 +26,8 @@ from ietf.utils.mail import outbox
 
 from ietf.person.factories import PersonFactory
 from ietf.group.factories import GroupFactory
-from ietf.meeting.factories import SessionFactory, SessionPresentationFactory, ScheduleFactory, MeetingFactory
+from ietf.meeting.factories import ( SessionFactory, SessionPresentationFactory, ScheduleFactory,
+    MeetingFactory, FloorPlanFactory )
 from ietf.doc.factories import DocumentFactory
 
 class MeetingTests(TestCase):
@@ -1092,3 +1093,20 @@ class AjaxTests(TestCase):
         self.assertTrue('utc' in data)
         self.assertTrue('error' not in data)
         self.assertEqual(data['utc'], '20:00')
+
+class FloorPlanTests(TestCase):
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def test_floor_plan_page(self):
+        make_meeting_test_data()
+        meeting = Meeting.objects.filter(type_id='ietf').order_by('id').last()
+        FloorPlanFactory.create(meeting=meeting)
+
+        url = urlreverse('ietf.meeting.views.floor_plan')
+        r = self.client.get(url)
+        self.assertEqual(r.status_code, 200)
+        

From fa184df2a6ae1f1719c62c8ae3664cc1cb246f9d Mon Sep 17 00:00:00 2001
From: Henrik Levkowetz <henrik@levkowetz.com>
Date: Thu, 7 Jul 2016 16:27:52 +0000
Subject: [PATCH 15/15] Added an explicit PhantomJS(port=0) as a workaround for
 old phantomjs instances staying around and blocking the port needed to run
 tests.  - Legacy-Id: 11546

---
 ietf/meeting/tests_js.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py
index 0e1b7221e..5714b2dab 100644
--- a/ietf/meeting/tests_js.py
+++ b/ietf/meeting/tests_js.py
@@ -46,7 +46,7 @@ class ScheduleEditTests(StaticLiveServerTestCase):
     def setUp(self):
         set_coverage_checking(False)
         condition_data()
-        self.driver = webdriver.PhantomJS(service_log_path=settings.TEST_GHOSTDRIVER_LOG_PATH)
+        self.driver = webdriver.PhantomJS(port=0, service_log_path=settings.TEST_GHOSTDRIVER_LOG_PATH)
         self.driver.set_window_size(1024,768)
 
     def tearDown(self):