Commits

Horst Gutmann  committed edb2391

WIP: Styling, row stripping

  • Participants
  • Parent commits 10eeadd

Comments (0)

Files changed (7)

File pyconde/schedule/admin.py

 
 
 class SideEventAdmin(admin.ModelAdmin):
-    list_display = ['name', 'conference', 'location']
+    list_display = ['name', 'start', 'end', 'conference', 'location']
     list_filter = ['conference', 'location']
 
 

File pyconde/schedule/migrations/0004_auto__add_field_sideevent_is_pause.py

+# encoding: 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 field 'SideEvent.is_pause'
+        db.add_column('schedule_sideevent', 'is_pause', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False)
+
+
+    def backwards(self, orm):
+        
+        # Deleting field 'SideEvent.is_pause'
+        db.delete_column('schedule_sideevent', 'is_pause')
+
+
+    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': '30'})
+        },
+        'conference.audiencelevel': {
+            'Meta': {'object_name': 'AudienceLevel'},
+            'conference': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.Conference']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'level': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'})
+        },
+        'conference.conference': {
+            'Meta': {'object_name': 'Conference'},
+            'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'reviews_active': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            'reviews_end_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'reviews_start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'start_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+            'timezone': ('timezones.fields.TimeZoneField', [], {'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'conference.location': {
+            'Meta': {'ordering': "['order']", 'object_name': 'Location'},
+            'conference': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.Conference']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+            'used_for_sessions': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
+        },
+        'conference.section': {
+            'Meta': {'object_name': 'Section'},
+            'conference': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.Conference']"}),
+            'end_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'start_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'conference.sessionduration': {
+            'Meta': {'object_name': 'SessionDuration'},
+            'conference': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.Conference']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'minutes': ('django.db.models.fields.IntegerField', [], {}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'})
+        },
+        'conference.sessionkind': {
+            'Meta': {'object_name': 'SessionKind'},
+            'closed': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
+            'conference': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.Conference']"}),
+            'end_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+            'start_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+        },
+        'conference.track': {
+            'Meta': {'ordering': "['order']", 'object_name': 'Track'},
+            'conference': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.Conference']"}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
+            'visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
+        },
+        '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'})
+        },
+        'proposals.proposal': {
+            'Meta': {'object_name': 'Proposal'},
+            'abstract': ('django.db.models.fields.TextField', [], {}),
+            'additional_speakers': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'proposal_participations'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['speakers.Speaker']"}),
+            'audience_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.AudienceLevel']"}),
+            'conference': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.Conference']"}),
+            'description': ('django.db.models.fields.TextField', [], {'max_length': '400'}),
+            'duration': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.SessionDuration']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'kind': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.SessionKind']"}),
+            'modified_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'speaker': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'proposals'", 'to': "orm['speakers.Speaker']"}),
+            'submission_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'track': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.Track']", 'null': 'True', 'blank': 'True'})
+        },
+        'schedule.session': {
+            'Meta': {'object_name': 'Session'},
+            'abstract': ('django.db.models.fields.TextField', [], {}),
+            'additional_speakers': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'session_participations'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['speakers.Speaker']"}),
+            'audience_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.AudienceLevel']"}),
+            'conference': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.Conference']"}),
+            'description': ('django.db.models.fields.TextField', [], {'max_length': '400'}),
+            'duration': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.SessionDuration']"}),
+            'end': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'kind': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.SessionKind']"}),
+            'location': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.Location']", 'null': 'True', 'blank': 'True'}),
+            'modified_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'proposal': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'session'", 'null': 'True', 'to': "orm['proposals.Proposal']"}),
+            'section': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'sessions'", 'null': 'True', 'to': "orm['conference.Section']"}),
+            'speaker': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sessions'", 'to': "orm['speakers.Speaker']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'submission_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'track': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.Track']", 'null': 'True', 'blank': 'True'})
+        },
+        'schedule.sideevent': {
+            'Meta': {'object_name': 'SideEvent'},
+            'conference': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.Conference']"}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'end': ('django.db.models.fields.DateTimeField', [], {}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_global': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_pause': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'location': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['conference.Location']", 'null': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'section': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'side_events'", 'null': 'True', 'to': "orm['conference.Section']"}),
+            'start': ('django.db.models.fields.DateTimeField', [], {})
+        },
+        'speakers.speaker': {
+            'Meta': {'object_name': 'Speaker'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'speaker_profile'", 'unique': 'True', 'to': "orm['auth.User']"})
+        },
+        'taggit.tag': {
+            'Meta': {'object_name': 'Tag'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100', 'db_index': 'True'})
+        },
+        'taggit.taggeditem': {
+            'Meta': {'object_name': 'TaggedItem'},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+            'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"})
+        }
+    }
+
+    complete_apps = ['schedule']

File pyconde/schedule/models.py

     location = models.ForeignKey(conference_models.Location, blank=True,
         null=True)
     is_global = models.BooleanField(default=False)
+    is_pause = models.BooleanField(default=False)
     conference = models.ForeignKey(conference_models.Conference,
         verbose_name=_("conference"))
 
 
     def __unicode__(self):
         return self.name
+
+    def get_absolute_url(self):
+        return reverse('side_event', kwargs={'pk': self.pk})

File pyconde/schedule/tests.py

         start = dt(2012, 6, 1, 9, 0)
         end = dt(2012, 6, 1, 10, 30)
         self.assertEquals(3, len(utils._create_base_grid(start, end, 30)))
+
+    def test_strip_rows(self):
+        e1 = utils.GridCell(None, 2)
+        e1.end = dt(2012, 6, 1, 10, 30)
+        rows = [
+            utils.GridRow(dt(2012, 6, 1, 9, 0), dt(2012, 6, 1, 9, 30), []),
+            utils.GridRow(dt(2012, 6, 1, 9, 30), dt(2012, 6, 1, 10, 0), [e1]),
+            utils.GridRow(dt(2012, 6, 1, 10, 0), dt(2012, 6, 1, 10, 30), []),
+            utils.GridRow(dt(2012, 6, 1, 10, 30), dt(2012, 6, 1, 11, 00), []),
+        ]
+        result = utils._strip_empty_rows(rows)
+        self.assertEquals(2, len(result))
+        self.assertEquals(rows[1:3], result)
+
+    def test_strip_rows_empty(self):
+        rows = [
+            utils.GridRow(dt(2012, 6, 1, 9, 0), dt(2012, 6, 1, 9, 30), []),
+            utils.GridRow(dt(2012, 6, 1, 9, 30), dt(2012, 6, 1, 10, 0), []),
+            utils.GridRow(dt(2012, 6, 1, 10, 0), dt(2012, 6, 1, 10, 30), []),
+            utils.GridRow(dt(2012, 6, 1, 10, 30), dt(2012, 6, 1, 11, 00), []),
+        ]
+        result = utils._strip_empty_rows(rows)
+        self.assertEquals(0, len(result))

File pyconde/schedule/urls.py

 urlpatterns = patterns('',
     url(r'^session-by-proposal/(?P<proposal_pk>\d+)/$', views.session_by_proposal, name='session_by_proposal'),
     url(r'^sessions/(?P<session_pk>\d+)/$', views.view_session, name='session'),
+    url(r'^events/(?P<pk>\d+)/$', views.view_session, name='side_event'),
     url(r'^schedule/$', views.view_schedule, name='schedule'),
     )

File pyconde/schedule/utils.py

     grid = _create_base_grid(start_time, end_time, row_duration)
     for evt in events:
         # Normalize date for the grid
-        grid[evt.start].append(GridCell(evt, (evt.end - evt.start).total_seconds() / (row_duration * 60)))
+        grid[evt.start].append(GridCell(evt, int((evt.end - evt.start).total_seconds() / (row_duration * 60))))
     # Now sort by location order in order to being able to render the grid as
     # HTML
     new_grid = []
     for k, v in grid.iteritems():
         v.sort(cmp=_gridcell_location_cmp)
-        new_grid.append(GridRow(k, v))
-    new_grid.sort(cmp=lambda a, b: cmp(a.date, b.date))
+        new_grid.append(GridRow(k, k + datetime.timedelta(0, row_duration * 60), v))
+    new_grid.sort(cmp=lambda a, b: cmp(a.start, b.start))
 
     # Split the whole section grid into days
     current_day = None
     days = []
+    prev_row = None
     for row in new_grid:
         if current_day is None:
-            current_day = SectionDay(row.date.date(), [])
-        elif current_day.day > row.date.date():
+            current_day = SectionDay(row.start.date(), [])
+        elif current_day.day < row.start.date():
             days.append(current_day)
-            current_day = SectionDay(row.date.date(), [])
+            current_day = SectionDay(row.start.date(), [])
         current_day.rows.append(row)
+        # Propagate pause rows if necessary
+        if prev_row is not None:
+            if prev_row.is_pause_row() and not row.events:
+                if prev_row.events:
+                    prev_row.pause_until = prev_row.events[0].end
+                if prev_row.pause_until is not None and prev_row.pause_until >= row.end:
+                    row.pause = True
+                    row.pause_until = prev_row.pause_until
+        prev_row = row
     if current_day and current_day.rows:
         days.append(current_day)
+    # Strip out heading and tailing empty rows
+    for day in days:
+        day.rows = _strip_empty_rows(day.rows)
     return (locations, days)
 
 
-GridRow = collections.namedtuple('GridRow', ['date', 'events'])
-GridCell = collections.namedtuple('GridCell', ['event', 'rowspan'])
-SectionDay = collections.namedtuple('SectionDay', ['day', 'rows'])
+class GridRow(object):
+    def __init__(self, start, end, events):
+        self.start = start
+        self.end = end
+        self.events = events
+        self.pause = None
+        self.pause_until = None
+
+    def is_pause_row(self):
+        if self.pause is not None:
+            return self.pause
+        return len(self.events) == 1 and self.events[0].is_global and self.events[0].is_pause
+
+    def __str__(self):
+        return "<GridRow %s - %s>" % (self.start, self.end)
+
+    def __unicode__(self):
+        return str(self)
+
+    def __repr__(self):
+        return str(self)
+
+
+class GridCell(object):
+    def __init__(self, event, rowspan):
+        self.rowspan = rowspan
+        self.speakers = []
+        self.track_name = None
+        self.name = None
+        self.url = ""
+        self.is_global = False
+        self.is_pause = False
+        self.start = None
+        self.end = None
+        if event is not None:
+            self.event = event
+            self.type = event.__class__.__name__.lower()
+            if hasattr(event, 'get_absolute_url'):
+                self.url = event.get_absolute_url()
+            self.start = event.start
+            self.end = event.end
+            if isinstance(event, models.Session):
+                self.name = event.title
+                if event.speaker:
+                    self.speakers.append(unicode(event.speaker))
+                for speaker in event.additional_speakers.all():
+                    self.speakers.append(unicode(speaker))
+                if event.track:
+                    self.track_name = event.track.name
+            else:
+                self.is_global = event.is_global
+                self.is_pause = event.is_pause
+                self.name = event.name
+
+
+class SectionDay(object):
+    def __init__(self, day, rows):
+        self.day = day
+        self.rows = rows
 
 
 def _create_base_grid(start_time, end_time, row_duration):
 
 def _gridcell_location_cmp(a, b):
     return a.event.location.order - b.event.location.order
+
+
+def _strip_empty_rows(rows):
+    first_idx_with_events = None
+    last_idx_with_events = None
+    rows_until = None
+    for idx, row in enumerate(rows):
+        if first_idx_with_events is None and row.events:
+            first_idx_with_events = idx
+        if first_idx_with_events is not None and row.events:
+            last_idx_with_events = idx
+            for evt in row.events:
+                if rows_until is None or evt.end > rows_until:
+                    rows_until = evt.end
+        if last_idx_with_events is not None and not row.events and row.end <= rows_until:
+            last_idx_with_events = idx
+    if not first_idx_with_events:
+        first_idx_with_events = 0
+    if not last_idx_with_events:
+        last_idx_with_events = first_idx_with_events - 1
+    return rows[first_idx_with_events:last_idx_with_events + 1]

File pyconde/templates/schedule/schedule.html

-{% extends "base.html" %}
-{% block content %}
+{% extends "cms/page_templates/fullpage.html" %}
+{% block apphook %}
 {% for section, grid in schedule.items %}
 <h2>{{ section.name }}</h2>
 {% for day in grid.1 %}
-<h3>{{ day.day|date }}</h3>
-<table>
+<table class="schedule">
+    <caption>{{ day.day|date }}</caption>
     <thead>
         <tr>
             <th>Zeit</th>
     </thead>
     <tbody>
         {% for row in day.rows %}
-        <tr>
-            <td>{{ row.date|time }}</td>
+        <tr{% if row.is_pause_row %} class="pause"{% endif %}>
+            <td class="timetable">{{ row.start|time:'H:i' }} - {{ row.end|time:'H:i' }}</td>
             {% for evt in row.events %}
-                {% if evt.event.is_global %}
-                <td rowspan="{{ evt.rowspan }}" colspan="2">{{ evt.event }}</td>
+                {% if evt.is_global %}
+                <td rowspan="{{ evt.rowspan }}" colspan="{{ grid.0|length }}">{{ evt.event }}</td>
                 {% else %}
-                <td rowspan="{{ evt.rowspan }}">{{ evt.event }}</td>
+                <td rowspan="{{ evt.rowspan }}">
+                    <a href="{{ evt.url }}">{{ evt.name }}</a>
+                    {% if evt.speakers %}von {{ evt.speakers|join:', ' }}{% endif %}
+                </td>
                 {% endif %}
             {% endfor %}
         </tr>