From aeaab66638cc847ba8f0eae168ce9d7fdeafff12 Mon Sep 17 00:00:00 2001 From: Hal Blackburn Date: Sun, 10 May 2015 23:01:07 +0100 Subject: [PATCH 1/8] Add manage command to generate testcases for rollover code --- .../management/commands/rollover_testcases.py | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 app/django/timetables/management/commands/rollover_testcases.py diff --git a/app/django/timetables/management/commands/rollover_testcases.py b/app/django/timetables/management/commands/rollover_testcases.py new file mode 100644 index 0000000..8af22e7 --- /dev/null +++ b/app/django/timetables/management/commands/rollover_testcases.py @@ -0,0 +1,115 @@ +""" +Generate testcases to test event rollover from one academic year to another. +""" +from __future__ import unicode_literals + +import sys +import json +import argparse +import itertools +from datetime import time, datetime +from collections import OrderedDict + +import pytz + +from timetables.utils import manage_commands +from timetables.utils.academicyear import (TERM_MICHAELMAS, TERM_LENT, + TERM_EASTER, TERM_STARTS) +from timetables.utils.datetimes import termweek_to_date, DAYS_REVERSE + + +class Command(manage_commands.ArgparseBaseCommand): + + def __init__(self): + super(Command, self).__init__() + + self.parser = argparse.ArgumentParser( + prog="rollover_testcases", + description=__doc__, + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + self.parser.add_argument("years", metavar="YEAR", nargs="+", + type=get_academic_year) + self.parser.add_argument("--roll-timezone", type=get_timezone, + default="Europe/London", dest="roll_tz") + self.parser.add_argument("--report-timezone", type=get_timezone, + default="UTC", dest="report_tz") + + def handle(self, args): + testcases = self.get_testcases(args.years, args.roll_tz, + args.report_tz) + + output = OrderedDict([ + ("roll_zone", args.roll_tz.zone), + ("years", args.years), + ("testcases", list(testcases)) + ]) + + json.dump(output, sys.stdout, indent=4) + + def get_testcases(self, years, roll_tz, report_tz): + assert len(years) > 0 + for term, week, day, hour in self.get_term_instants(): + minute = 0 + + testcase = OrderedDict([ + ("academic", OrderedDict([ + ("term", term), + ("week", week), + ("day", day), + ("hour", hour), + ("minute", minute) + ])) + ]) + testcase.update( + ("{:d}".format(year), + self.get_timestamp(year, term, week, day, hour, minute, + roll_tz).astimezone(report_tz).isoformat()) + for year in years + ) + + yield testcase + + def get_timestamp(self, year, term, week, day, hour, minute, timezone): + """ + Get the instant represented by a time, day and week in an academic term + as an aware datetime in the specified timezone. + """ + date = termweek_to_date(year, term, week, day) + return timezone.localize(datetime.combine(date, time(hour, minute))) + + def get_term_instants(self): + """ + Generate the timestamps to include as test cases. + + We produce one for each day of term (weeks 1-8 inclusive), plus 2 weeks + either side, so -1 to 10 inclusive. + """ + terms = [TERM_MICHAELMAS, TERM_LENT, TERM_EASTER] + weeks = range(-1, 11) + # Days from Thurs, Fri ... Wed + days = [DAYS_REVERSE[(n + 3) % 7] for n in range(0, 7)] + hours = [0, 5, 10, 12, 15, 20] + + return itertools.product(terms, weeks, days, hours) + + +def get_academic_year(year): + try: + year = int(year) + except ValueError: + raise argparse.ArgumentTypeError("Invalid year: {}".format(year)) + + if year not in TERM_STARTS: + raise argparse.ArgumentTypeError("No term dates available for year: {}" + .format(year)) + return year + + +def get_timezone(zone_name): + try: + return pytz.timezone(zone_name) + except pytz.UnknownTimeZoneError: + raise argparse.ArgumentTypeError( + "Unknown timezone: {0}".format(zone_name)) From b933ab66ab41f49ec0b3f4e12cf906f1fe8f6b6c Mon Sep 17 00:00:00 2001 From: Hal Blackburn Date: Tue, 19 May 2015 18:04:22 +0100 Subject: [PATCH 2/8] Add gh export commands --- .../commands/grasshopper_export_data.py | 376 ++++++++++++++++++ .../commands/grasshopper_export_structure.py | 110 +++++ 2 files changed, 486 insertions(+) create mode 100644 app/django/timetables/management/commands/grasshopper_export_data.py create mode 100644 app/django/timetables/management/commands/grasshopper_export_structure.py diff --git a/app/django/timetables/management/commands/grasshopper_export_data.py b/app/django/timetables/management/commands/grasshopper_export_data.py new file mode 100644 index 0000000..e5b579c --- /dev/null +++ b/app/django/timetables/management/commands/grasshopper_export_data.py @@ -0,0 +1,376 @@ +""" +Copy this file to: + app/django/timetables/management/commands/grasshopper_export_data.py + +Change directory so your current working directory is: + app/django + +Execute: + python manage.py grasshopper_export_data > data_dump.csv + +Depending on your machine, this can take a couple of minutes. You should end up with a CSV file +that contains all the necessary data for each event to build up a tree. + + +Export all events in the system into CSV format. Events with dodgy +ancestors (invalid data) are skipped. +""" +import csv +import sys +import argparse + +import pytz + +from timetables.models import Event, NestedSubject +from timetables.utils import manage_commands +from timetables.utils.traversal import ( + EventTraverser, + SeriesTraverser, + ModuleTraverser, + SubpartTraverser, + PartTraverser, + TriposTraverser, + InvalidStructureException +) + + +class Command(manage_commands.ArgparseBaseCommand): + + def __init__(self): + super(Command, self).__init__() + + self.parser = argparse.ArgumentParser( + prog="grasshopper_export_events", + description=__doc__, + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + def handle(self, args): + events = self.get_events() + + exporter = CsvExporter( + self.get_columns(), + [UnicodeEncodeRowFilter()], + events + ) + + exporter.export_to_stream(sys.stdout) + + def get_columns(self): + return [ + TriposIdColumnSpec(), TriposNameColumnSpec(), + PartIdColumnSpec(), PartNameColumnSpec(), + SubPartIdColumnSpec(), SubPartNameColumnSpec(), + ModuleIdColumnSpec(), ModuleNameColumnSpec(), + SeriesIdColumnSpec(), SeriesNameColumnSpec(), + EventIdColumnSpec(), EventTitleColumnSpec(), + EventTypeColumnSpec(), + EventStartDateTimeColumnSpec(), EventEndDateTimeColumnSpec(), + EventLocationColumnSpec(), EventLecturerColumnSpec() + ] + + def get_events(self): + return ( + Event.objects + .just_active() + .prefetch_related("source__" # EventSource (series) + # m2m linking to module + "eventsourcetag_set__" + "thing__" # Module + "parent__" # Subpart or part + "parent__" # part or tripos + "parent")) # tripos or nothing + + +class CsvExporter(object): + def __init__(self, columns, filters, events): + self.columns = columns + self.filters = filters + self.events = events + + def export_to_stream(self, dest): + csv_writer = csv.writer(dest, delimiter=',', quotechar='"', quoting=csv.QUOTE_ALL) + return self.export_to_csv_writer(csv_writer) + + def export_to_csv_writer(self, csv_writer): + self.write_row(csv_writer, self.get_header_row()) + + for event in self.events: + try: + self.write_row(csv_writer, self.get_row(event)) + except InvalidStructureException as e: + print >> sys.stderr, "Skipping event", event.pk, e + + def get_header_row(self): + return [spec.get_column_name() for spec in self.columns] + + def get_row(self, event): + return [spec.extract_value(event) for spec in self.columns] + + def write_row(self, csv_writer, row): + for f in self.filters: + row = f.filter(row) + + csv_writer.writerow(row) + + +class UnicodeEncodeRowFilter(object): + encoding = "utf-8" + + def encode_unicode(self, val): + if isinstance(val, basestring): + val = val.replace("\n", "") + + if isinstance(val, unicode): + return val.encode(self.encoding) + return val + + def filter(self, row): + return [self.encode_unicode(val) for val in row] + + +class ColumnSpec(object): + name = None + + def __init__(self, name=None): + if name is not None: + self.name = name + if self.name is None: + raise ValueError("no name value provided") + + def get_column_name(self): + return self.name + + def extract_value(self, event): + raise NotImplementedError() + + def get_series(self, event): + return EventTraverser(event).step_up().get_value() + + def get_module(self, event): + series = self.get_series(event) + return SeriesTraverser(series).step_up().get_value() + + def get_subpart(self, event): + module = self.get_module(event) + traverser = ModuleTraverser(module).step_up() + + if traverser.name == SubpartTraverser.name: + return traverser.get_value() + return None + + def get_part(self, event): + subpart = self.get_subpart(event) + if subpart is not None: + traverser = SubpartTraverser(subpart) + else: + traverser = ModuleTraverser(self.get_module(event)) + + part_traverser = traverser.step_up() + assert part_traverser.name == PartTraverser.name + return part_traverser.get_value() + + def get_tripos(self, event): + part = self.get_part(event) + return PartTraverser(part).step_up().get_value() + + +class TriposIdColumnSpec(ColumnSpec): + name = "Tripos Id" + + def extract_value(self, event): + tripos = self.get_tripos(event) + return tripos.id + + +class TriposNameColumnSpec(ColumnSpec): + name = "Tripos Name" + + def extract_value(self, event): + tripos = self.get_tripos(event) + return tripos.fullname + + +class TriposShortNameColumnSpec(ColumnSpec): + name = "Tripos Short Name" + + def extract_value(self, event): + tripos = self.get_tripos(event) + return tripos.name + + +class PartIdColumnSpec(ColumnSpec): + name = "Part Id" + + def extract_value(self, event): + part = self.get_part(event) + return part.id + +class PartNameColumnSpec(ColumnSpec): + name = "Part Name" + + def extract_value(self, event): + part = self.get_part(event) + return part.fullname + + +class PartShortNameColumnSpec(ColumnSpec): + name = "Part Short Name" + + def extract_value(self, event): + part = self.get_part(event) + return part.name + + +class SubPartIdColumnSpec(ColumnSpec): + name = "Subpart Id" + + def extract_value(self, event): + subpart = self.get_subpart(event) + return None if subpart is None else subpart.id + +class SubPartNameColumnSpec(ColumnSpec): + name = "Subpart Name" + + def extract_value(self, event): + subpart = self.get_subpart(event) + return None if subpart is None else subpart.fullname + + +class SubPartShortNameColumnSpec(ColumnSpec): + name = "Subpart Short Name" + + def extract_value(self, event): + subpart = self.get_subpart(event) + return None if subpart is None else subpart.name + + +class ModuleIdColumnSpec(ColumnSpec): + name = "Module Id" + + def extract_value(self, event): + module = self.get_module(event) + return module.id + + +class ModuleNameColumnSpec(ColumnSpec): + name = "Module Name" + + def extract_value(self, event): + module = self.get_module(event) + return module.fullname + + +class ModuleShortNameColumnSpec(ColumnSpec): + name = "Module Short Name" + + def extract_value(self, event): + module = self.get_module(event) + return module.name + + +class SeriesIdColumnSpec(ColumnSpec): + name = "Series ID" + + def extract_value(self, event): + series = self.get_series(event) + return series.id + +class SeriesNameColumnSpec(ColumnSpec): + name = "Series Name" + + def extract_value(self, event): + series = self.get_series(event) + return series.title + + +class EventAttrColumnSpec(ColumnSpec): + attr_name = None + + def get_attr_name(self): + assert self.attr_name is not None + return self.attr_name + + def extract_value(self, event): + return getattr(event, self.get_attr_name()) + + +class EventIdColumnSpec(EventAttrColumnSpec): + name = "Event ID" + attr_name = "id" + +class EventTitleColumnSpec(EventAttrColumnSpec): + name = "Title" + attr_name = "title" + + +class EventLocationColumnSpec(EventAttrColumnSpec): + name = "Location" + attr_name = "location" + + +class EventUidColumnSpec(ColumnSpec): + name = "UID" + + def extract_value(self, event): + return event.get_ical_uid() + + +class EventDateTimeColumnSpec(ColumnSpec): + timezone = pytz.timezone("Europe/London") + + def get_datetime_utc(self, event): + raise NotImplementedError() + + def extract_value(self, event): + dt_utc = self.get_datetime_utc(event) + return self.timezone.normalize(dt_utc.astimezone(self.timezone)).isoformat() + + +class EventStartDateTimeColumnSpec(EventDateTimeColumnSpec): + name = "Start" + + def get_datetime_utc(self, event): + # FIXME: is this UTC? + return event.start + + +class EventEndDateTimeColumnSpec(EventDateTimeColumnSpec): + name = "End" + + def get_datetime_utc(self, event): + return event.end + + +class EventMetadataColumnSpec(ColumnSpec): + metadata_path = None + + def get_metadata_path(self): + if self.metadata_path is None: + raise ValueError("no metadata_path value provided") + return self.metadata_path + + def extract_value(self, event): + metadata = event.metadata + + segments = self.get_metadata_path().split(".") + for i, segment in enumerate(segments): + if not isinstance(metadata, dict): + return None + if i == len(segments) - 1: + return metadata.get(segment) + metadata = metadata.get(segment) + + +class EventTypeColumnSpec(EventMetadataColumnSpec): + name = "Type" + metadata_path = "type" + + +class EventLecturerColumnSpec(EventMetadataColumnSpec): + name = "People" + metadata_path = "people" + + def extract_value(self, event): + value = super(EventLecturerColumnSpec, self).extract_value(event) + return None if value is None else "#".join(value) diff --git a/app/django/timetables/management/commands/grasshopper_export_structure.py b/app/django/timetables/management/commands/grasshopper_export_structure.py new file mode 100644 index 0000000..40e9ddc --- /dev/null +++ b/app/django/timetables/management/commands/grasshopper_export_structure.py @@ -0,0 +1,110 @@ +""" +Copy this file to: + app/django/timetables/management/commands/grasshopper_export_structure.py + +Change directory so your current working directory is: + app/django + +Execute: + python manage.py grasshopper_export_structure > external-tree.json + + +Export the triposes, parts and subjects to a tree. The tree follows the same structure as GrassHopper's +import tree. i.e., + +- Course + - Subject (optional) + - Part + +The parts will also contain the external_url data object +""" +import csv +import sys +import argparse +import json + +import pytz + +from collections import defaultdict + +from timetables.models import Thing, Subjects +from timetables.utils import manage_commands + + +class Command(manage_commands.ArgparseBaseCommand): + + def __init__(self): + super(Command, self).__init__() + + self.parser = argparse.ArgumentParser( + prog="grasshopper_export_structure", + description=__doc__, + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + def handle(self, args): + # Get the tree + tree = self.get_tree() + + # Print the tree + print json.dumps(tree, indent=4) + + def get_tree(self): + + root = { + 'id': 0, + 'name': 'Timetable', + 'type': 'root', + 'nodes': {} + } + + for item in Subjects.all_subjects(): + tripos = item.get_tripos() + part = item.get_part() + subject = item.get_most_significant_thing() + + + partData = None + if part.data: + partData = json.loads(part.data) + if partData['external_website_url']: + partData['external'] = partData['external_website_url'] + del partData['external_website_url'] + + + # Add the tripos/course into the tree + if tripos.id not in root['nodes']: + root['nodes'][tripos.id] = { + 'id': tripos.id, + 'name': tripos.fullname, + 'type': 'course', + 'nodes': {} + } + + # If we're dealing with a subject, add it in + if subject.id != part.id: + if subject.id not in root['nodes'][tripos.id]['nodes']: + root['nodes'][tripos.id]['nodes'][subject.id] = { + 'id': subject.id, + 'name': subject.fullname, + 'type': 'subject', + 'nodes': {} + } + + root['nodes'][tripos.id]['nodes'][subject.id]['nodes'][part.id] = { + 'id': part.id, + 'name': part.fullname, + 'data': partData, + 'type': 'part', + 'nodes': {} + } + else: + root['nodes'][tripos.id]['nodes'][part.id] = { + 'id': part.id, + 'name': part.fullname, + 'data': partData, + 'type': 'part', + 'nodes': {} + } + + return root From 11cc7031debdfd487688f9ab9b0b6cf0a77992e0 Mon Sep 17 00:00:00 2001 From: Hal Blackburn Date: Wed, 27 May 2015 21:29:43 +0100 Subject: [PATCH 3/8] Add support for multiple parents in SeriesTraverser Although it wasn't an official feature, people have sometimes linked series to more than one "home" module as a form of borrowing. The series traverser now supports traversing all parent modules using walk_parents(). --- app/django/timetables/utils/traversal.py | 33 +++++++++++++++++------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/app/django/timetables/utils/traversal.py b/app/django/timetables/utils/traversal.py index 1f44e4b..d17939c 100644 --- a/app/django/timetables/utils/traversal.py +++ b/app/django/timetables/utils/traversal.py @@ -26,8 +26,11 @@ def validate_obj(self): pass def step_up(self): + return self._step(self.get_parent()) + + def _step(self, parent): try: - traverser, parent = self.get_parent() + traverser, parent = parent return traverser(parent) except ValidationException as e: raise InvalidStructureException( @@ -80,20 +83,30 @@ def validate_obj(self): "Expected an EventSource instance", self.obj) def get_parent(self): + return next(self.get_parents()) + + def walk_parents(self): + """ + Enumerate all traversers for all parent modules of the current series. + """ + for parent in self.get_parents(): + yield self._step(parent) + + def get_parents(self): series = self.obj - tags = series.eventsourcetag_set.all() + tags = list(series.eventsourcetag_set.filter(annotation="home")) - try: - tag = next(tag for tag in tags if tag.annotation == "home") - module = tag.thing - except StopIteration: + if(len(tags) == 0): raise ValidationException( "Orphaned series with no module encountered", series.pk) - if module.type != "module": - raise ValidationException( - "Series attached to non-module thing", series.pk) - return (ModuleTraverser, module) + for tag in tags: + module = tag.thing + + if module.type != "module": + raise ValidationException( + "Series attached to non-module thing", series.pk) + yield (ModuleTraverser, module) class ModuleTraverser(ThingTraverserMixin, Traverser): From 9383079f7db452cb736ee8f43305b4ba17b137e0 Mon Sep 17 00:00:00 2001 From: Hal Blackburn Date: Wed, 27 May 2015 21:36:34 +0100 Subject: [PATCH 4/8] Update exportcsv manage command to support borrowed series Events are output once for each module their series is linked to. --- .../management/commands/exportcsv.py | 140 +++++++++++------- 1 file changed, 84 insertions(+), 56 deletions(-) diff --git a/app/django/timetables/management/commands/exportcsv.py b/app/django/timetables/management/commands/exportcsv.py index 7658109..13996ce 100644 --- a/app/django/timetables/management/commands/exportcsv.py +++ b/app/django/timetables/management/commands/exportcsv.py @@ -5,6 +5,7 @@ import csv import sys import argparse +import collections import pytz @@ -84,15 +85,17 @@ def export_to_csv_writer(self, csv_writer): for event in self.events: try: - self.write_row(csv_writer, self.get_row(event)) + for event_path in paths_to(event): + self.write_row(csv_writer, self.get_row(event_path)) except InvalidStructureException as e: print >> sys.stderr, "Skipping event", event.pk, e def get_header_row(self): return [spec.get_column_name() for spec in self.columns] - def get_row(self, event): - return [spec.extract_value(event) for spec in self.columns] + def get_row(self, event_path): + assert isinstance(event_path, EventPath) + return [spec.extract_value(event_path) for spec in self.columns] def write_row(self, csv_writer, row): for f in self.filters: @@ -101,6 +104,43 @@ def write_row(self, csv_writer, row): csv_writer.writerow(row) + +EventPath = collections.namedtuple( + "EventPath", "tripos part subpart module series event".split()) + + +def paths_to(event): + """ + Get the possible paths through the Thing tree to an Event + + An iterable of EventPaths is returned representing each way + an Event can be reached from the root. There's usually a single + path, but when a series is linked to more than one module an Event + in the series will have > 1 way to access it from the root. + """ + series_traverser = EventTraverser(event).step_up() + for module_traverser in series_traverser.walk_parents(): + mod_parent_traverser = module_traverser.step_up() + + if mod_parent_traverser.name == SubpartTraverser.name: + subpart = mod_parent_traverser.get_value() + part_traverser = mod_parent_traverser.step_up() + else: + subpart = None + assert mod_parent_traverser.name == PartTraverser.name + part_traverser = mod_parent_traverser + + tripos_traverser = part_traverser.step_up() + + yield EventPath( + tripos_traverser.get_value(), + part_traverser.get_value(), + subpart, + module_traverser.get_value(), + series_traverser.get_value(), + event) + + class UnicodeEncodeRowFilter(object): encoding = "utf-8" @@ -125,109 +165,97 @@ def __init__(self, name=None): def get_column_name(self): return self.name - def extract_value(self, event): + def extract_value(self, event_path): raise NotImplementedError() - def get_series(self, event): - return EventTraverser(event).step_up().get_value() - - def get_module(self, event): - series = self.get_series(event) - return SeriesTraverser(series).step_up().get_value() + def get_event(self, event_path): + return event_path.event - def get_subpart(self, event): - module = self.get_module(event) - traverser = ModuleTraverser(module).step_up() + def get_series(self, event_path): + return event_path.series - if traverser.name == SubpartTraverser.name: - return traverser.get_value() - return None + def get_module(self, event_path): + return event_path.module - def get_part(self, event): - subpart = self.get_subpart(event) - if subpart is not None: - traverser = SubpartTraverser(subpart) - else: - traverser = ModuleTraverser(self.get_module(event)) + def get_subpart(self, event_path): + return event_path.subpart - part_traverser = traverser.step_up() - assert part_traverser.name == PartTraverser.name - return part_traverser.get_value() + def get_part(self, event_path): + return event_path.part - def get_tripos(self, event): - part = self.get_part(event) - return PartTraverser(part).step_up().get_value() + def get_tripos(self, event_path): + return event_path.tripos class TriposNameColumnSpec(ColumnSpec): name = "Tripos Name" - def extract_value(self, event): - tripos = self.get_tripos(event) + def extract_value(self, event_path): + tripos = self.get_tripos(event_path) return tripos.fullname class TriposShortNameColumnSpec(ColumnSpec): name = "Tripos Short Name" - def extract_value(self, event): - tripos = self.get_tripos(event) + def extract_value(self, event_path): + tripos = self.get_tripos(event_path) return tripos.name class PartNameColumnSpec(ColumnSpec): name = "Part Name" - def extract_value(self, event): - part = self.get_part(event) + def extract_value(self, event_path): + part = self.get_part(event_path) return part.fullname class PartShortNameColumnSpec(ColumnSpec): name = "Part Short Name" - def extract_value(self, event): - part = self.get_part(event) + def extract_value(self, event_path): + part = self.get_part(event_path) return part.name class SubPartNameColumnSpec(ColumnSpec): name = "Subpart Name" - def extract_value(self, event): - subpart = self.get_subpart(event) + def extract_value(self, event_path): + subpart = self.get_subpart(event_path) return None if subpart is None else subpart.fullname class SubPartShortNameColumnSpec(ColumnSpec): name = "Subpart Short Name" - def extract_value(self, event): - subpart = self.get_subpart(event) + def extract_value(self, event_path): + subpart = self.get_subpart(event_path) return None if subpart is None else subpart.name class ModuleNameColumnSpec(ColumnSpec): name = "Module Name" - def extract_value(self, event): - module = self.get_module(event) + def extract_value(self, event_path): + module = self.get_module(event_path) return module.fullname class ModuleShortNameColumnSpec(ColumnSpec): name = "Module Short Name" - def extract_value(self, event): - module = self.get_module(event) + def extract_value(self, event_path): + module = self.get_module(event_path) return module.name class SeriesNameColumnSpec(ColumnSpec): name = "Series Name" - def extract_value(self, event): - series = self.get_series(event) + def extract_value(self, event_path): + series = self.get_series(event_path) return series.title @@ -238,8 +266,8 @@ def get_attr_name(self): assert self.attr_name is not None return self.attr_name - def extract_value(self, event): - return getattr(event, self.get_attr_name()) + def extract_value(self, event_path): + return getattr(self.get_event(event_path), self.get_attr_name()) class EventTitleColumnSpec(EventAttrColumnSpec): @@ -255,8 +283,8 @@ class EventLocationColumnSpec(EventAttrColumnSpec): class EventUidColumnSpec(ColumnSpec): name = "UID" - def extract_value(self, event): - return event.get_ical_uid() + def extract_value(self, event_path): + return self.get_event(event_path).get_ical_uid() class EventDateTimeColumnSpec(ColumnSpec): @@ -265,8 +293,8 @@ class EventDateTimeColumnSpec(ColumnSpec): def get_datetime_utc(self, event): raise NotImplementedError() - def extract_value(self, event): - dt_utc = self.get_datetime_utc(event) + def extract_value(self, event_path): + dt_utc = self.get_datetime_utc(self.get_event(event_path)) return self.timezone.normalize(dt_utc.astimezone(self.timezone)).isoformat() @@ -292,8 +320,8 @@ def get_metadata_path(self): raise ValueError("no metadata_path value provided") return self.metadata_path - def extract_value(self, event): - metadata = event.metadata + def extract_value(self, event_path): + metadata = self.get_event(event_path).metadata segments = self.get_metadata_path().split(".") for i, segment in enumerate(segments): @@ -313,6 +341,6 @@ class EventLecturerColumnSpec(EventMetadataColumnSpec): name = "People" metadata_path = "people" - def extract_value(self, event): - value = super(EventLecturerColumnSpec, self).extract_value(event) + def extract_value(self, event_path): + value = super(EventLecturerColumnSpec, self).extract_value(event_path) return None if value is None else ", ".join(value) From e3cda91165f90c7b459f8d236fc8ab98f7e4cd4f Mon Sep 17 00:00:00 2001 From: Hal Blackburn Date: Wed, 27 May 2015 22:28:26 +0100 Subject: [PATCH 5/8] Update grasshopper_export_data to support borrowed series It now overrides most functionality from exportcsv which now has borrowed series support. --- .../commands/grasshopper_export_data.py | 321 +++--------------- 1 file changed, 38 insertions(+), 283 deletions(-) diff --git a/app/django/timetables/management/commands/grasshopper_export_data.py b/app/django/timetables/management/commands/grasshopper_export_data.py index e5b579c..1ac7549 100644 --- a/app/django/timetables/management/commands/grasshopper_export_data.py +++ b/app/django/timetables/management/commands/grasshopper_export_data.py @@ -19,25 +19,13 @@ import sys import argparse -import pytz +from timetables.management.commands import exportcsv -from timetables.models import Event, NestedSubject -from timetables.utils import manage_commands -from timetables.utils.traversal import ( - EventTraverser, - SeriesTraverser, - ModuleTraverser, - SubpartTraverser, - PartTraverser, - TriposTraverser, - InvalidStructureException -) - -class Command(manage_commands.ArgparseBaseCommand): +class Command(exportcsv.Command): def __init__(self): - super(Command, self).__init__() + super(exportcsv.Command, self).__init__() self.parser = argparse.ArgumentParser( prog="grasshopper_export_events", @@ -50,7 +38,7 @@ def handle(self, args): exporter = CsvExporter( self.get_columns(), - [UnicodeEncodeRowFilter()], + [StripNewlinesRowFilter(), exportcsv.UnicodeEncodeRowFilter()], events ) @@ -58,316 +46,83 @@ def handle(self, args): def get_columns(self): return [ - TriposIdColumnSpec(), TriposNameColumnSpec(), - PartIdColumnSpec(), PartNameColumnSpec(), - SubPartIdColumnSpec(), SubPartNameColumnSpec(), - ModuleIdColumnSpec(), ModuleNameColumnSpec(), - SeriesIdColumnSpec(), SeriesNameColumnSpec(), - EventIdColumnSpec(), EventTitleColumnSpec(), - EventTypeColumnSpec(), - EventStartDateTimeColumnSpec(), EventEndDateTimeColumnSpec(), - EventLocationColumnSpec(), EventLecturerColumnSpec() + TriposIdColumnSpec(), exportcsv.TriposNameColumnSpec(), + PartIdColumnSpec(), exportcsv.PartNameColumnSpec(), + SubPartIdColumnSpec(), exportcsv.SubPartNameColumnSpec(), + ModuleIdColumnSpec(), exportcsv.ModuleNameColumnSpec(), + SeriesIdColumnSpec(), exportcsv.SeriesNameColumnSpec(), + EventIdColumnSpec(), exportcsv.EventTitleColumnSpec(), + exportcsv.EventTypeColumnSpec(), + exportcsv.EventStartDateTimeColumnSpec(), + exportcsv.EventEndDateTimeColumnSpec(), + exportcsv.EventLocationColumnSpec(), + EventLecturerColumnSpec() ] - def get_events(self): - return ( - Event.objects - .just_active() - .prefetch_related("source__" # EventSource (series) - # m2m linking to module - "eventsourcetag_set__" - "thing__" # Module - "parent__" # Subpart or part - "parent__" # part or tripos - "parent")) # tripos or nothing - - -class CsvExporter(object): - def __init__(self, columns, filters, events): - self.columns = columns - self.filters = filters - self.events = events + +class CsvExporter(exportcsv.CsvExporter): def export_to_stream(self, dest): csv_writer = csv.writer(dest, delimiter=',', quotechar='"', quoting=csv.QUOTE_ALL) return self.export_to_csv_writer(csv_writer) - def export_to_csv_writer(self, csv_writer): - self.write_row(csv_writer, self.get_header_row()) - - for event in self.events: - try: - self.write_row(csv_writer, self.get_row(event)) - except InvalidStructureException as e: - print >> sys.stderr, "Skipping event", event.pk, e - - def get_header_row(self): - return [spec.get_column_name() for spec in self.columns] - - def get_row(self, event): - return [spec.extract_value(event) for spec in self.columns] - - def write_row(self, csv_writer, row): - for f in self.filters: - row = f.filter(row) - csv_writer.writerow(row) - - -class UnicodeEncodeRowFilter(object): - encoding = "utf-8" - - def encode_unicode(self, val): +class StripNewlinesRowFilter(object): + def strip_newlines(self, val): if isinstance(val, basestring): - val = val.replace("\n", "") - - if isinstance(val, unicode): - return val.encode(self.encoding) + return val.replace("\n", "") return val def filter(self, row): - return [self.encode_unicode(val) for val in row] - - -class ColumnSpec(object): - name = None - - def __init__(self, name=None): - if name is not None: - self.name = name - if self.name is None: - raise ValueError("no name value provided") - - def get_column_name(self): - return self.name - - def extract_value(self, event): - raise NotImplementedError() - - def get_series(self, event): - return EventTraverser(event).step_up().get_value() - - def get_module(self, event): - series = self.get_series(event) - return SeriesTraverser(series).step_up().get_value() - - def get_subpart(self, event): - module = self.get_module(event) - traverser = ModuleTraverser(module).step_up() - - if traverser.name == SubpartTraverser.name: - return traverser.get_value() - return None - - def get_part(self, event): - subpart = self.get_subpart(event) - if subpart is not None: - traverser = SubpartTraverser(subpart) - else: - traverser = ModuleTraverser(self.get_module(event)) - - part_traverser = traverser.step_up() - assert part_traverser.name == PartTraverser.name - return part_traverser.get_value() - - def get_tripos(self, event): - part = self.get_part(event) - return PartTraverser(part).step_up().get_value() + return [self.strip_newlines(val) for val in row] -class TriposIdColumnSpec(ColumnSpec): +class TriposIdColumnSpec(exportcsv.ColumnSpec): name = "Tripos Id" - def extract_value(self, event): - tripos = self.get_tripos(event) + def extract_value(self, event_path): + tripos = self.get_tripos(event_path) return tripos.id -class TriposNameColumnSpec(ColumnSpec): - name = "Tripos Name" - - def extract_value(self, event): - tripos = self.get_tripos(event) - return tripos.fullname - - -class TriposShortNameColumnSpec(ColumnSpec): - name = "Tripos Short Name" - - def extract_value(self, event): - tripos = self.get_tripos(event) - return tripos.name - - -class PartIdColumnSpec(ColumnSpec): +class PartIdColumnSpec(exportcsv.ColumnSpec): name = "Part Id" - def extract_value(self, event): - part = self.get_part(event) + def extract_value(self, event_path): + part = self.get_part(event_path) return part.id -class PartNameColumnSpec(ColumnSpec): - name = "Part Name" - - def extract_value(self, event): - part = self.get_part(event) - return part.fullname - -class PartShortNameColumnSpec(ColumnSpec): - name = "Part Short Name" - - def extract_value(self, event): - part = self.get_part(event) - return part.name - - -class SubPartIdColumnSpec(ColumnSpec): +class SubPartIdColumnSpec(exportcsv.ColumnSpec): name = "Subpart Id" - def extract_value(self, event): - subpart = self.get_subpart(event) + def extract_value(self, event_path): + subpart = self.get_subpart(event_path) return None if subpart is None else subpart.id -class SubPartNameColumnSpec(ColumnSpec): - name = "Subpart Name" - - def extract_value(self, event): - subpart = self.get_subpart(event) - return None if subpart is None else subpart.fullname - - -class SubPartShortNameColumnSpec(ColumnSpec): - name = "Subpart Short Name" - - def extract_value(self, event): - subpart = self.get_subpart(event) - return None if subpart is None else subpart.name - -class ModuleIdColumnSpec(ColumnSpec): +class ModuleIdColumnSpec(exportcsv.ColumnSpec): name = "Module Id" - def extract_value(self, event): - module = self.get_module(event) + def extract_value(self, event_path): + module = self.get_module(event_path) return module.id -class ModuleNameColumnSpec(ColumnSpec): - name = "Module Name" - - def extract_value(self, event): - module = self.get_module(event) - return module.fullname - - -class ModuleShortNameColumnSpec(ColumnSpec): - name = "Module Short Name" - - def extract_value(self, event): - module = self.get_module(event) - return module.name - - -class SeriesIdColumnSpec(ColumnSpec): +class SeriesIdColumnSpec(exportcsv.ColumnSpec): name = "Series ID" - def extract_value(self, event): - series = self.get_series(event) + def extract_value(self, event_path): + series = self.get_series(event_path) return series.id -class SeriesNameColumnSpec(ColumnSpec): - name = "Series Name" - def extract_value(self, event): - series = self.get_series(event) - return series.title - - -class EventAttrColumnSpec(ColumnSpec): - attr_name = None - - def get_attr_name(self): - assert self.attr_name is not None - return self.attr_name - - def extract_value(self, event): - return getattr(event, self.get_attr_name()) - - -class EventIdColumnSpec(EventAttrColumnSpec): +class EventIdColumnSpec(exportcsv.EventAttrColumnSpec): name = "Event ID" attr_name = "id" -class EventTitleColumnSpec(EventAttrColumnSpec): - name = "Title" - attr_name = "title" - - -class EventLocationColumnSpec(EventAttrColumnSpec): - name = "Location" - attr_name = "location" - - -class EventUidColumnSpec(ColumnSpec): - name = "UID" - - def extract_value(self, event): - return event.get_ical_uid() - - -class EventDateTimeColumnSpec(ColumnSpec): - timezone = pytz.timezone("Europe/London") - - def get_datetime_utc(self, event): - raise NotImplementedError() - - def extract_value(self, event): - dt_utc = self.get_datetime_utc(event) - return self.timezone.normalize(dt_utc.astimezone(self.timezone)).isoformat() - - -class EventStartDateTimeColumnSpec(EventDateTimeColumnSpec): - name = "Start" - - def get_datetime_utc(self, event): - # FIXME: is this UTC? - return event.start - - -class EventEndDateTimeColumnSpec(EventDateTimeColumnSpec): - name = "End" - - def get_datetime_utc(self, event): - return event.end - - -class EventMetadataColumnSpec(ColumnSpec): - metadata_path = None - - def get_metadata_path(self): - if self.metadata_path is None: - raise ValueError("no metadata_path value provided") - return self.metadata_path - - def extract_value(self, event): - metadata = event.metadata - - segments = self.get_metadata_path().split(".") - for i, segment in enumerate(segments): - if not isinstance(metadata, dict): - return None - if i == len(segments) - 1: - return metadata.get(segment) - metadata = metadata.get(segment) - - -class EventTypeColumnSpec(EventMetadataColumnSpec): - name = "Type" - metadata_path = "type" - -class EventLecturerColumnSpec(EventMetadataColumnSpec): +class EventLecturerColumnSpec(exportcsv.EventMetadataColumnSpec): name = "People" metadata_path = "people" From 8db8788bfbffa137204118835fdc7376465bd3ba Mon Sep 17 00:00:00 2001 From: Hal Blackburn Date: Thu, 28 May 2015 00:20:28 +0100 Subject: [PATCH 6/8] Add compact representative dataset This can be used to test the behaviour of CSV exporting commands like grasshopper_export_data and the program from fronteerio/grasshopper which generates a tree from the CSV export. --- fixtures/representative/README.md | 14 + fixtures/representative/django-fixture.json | 1086 +++++++++++++++++++ fixtures/representative/gh-data.csv | 18 + fixtures/representative/gh-tree.json | 423 ++++++++ 4 files changed, 1541 insertions(+) create mode 100644 fixtures/representative/README.md create mode 100644 fixtures/representative/django-fixture.json create mode 100644 fixtures/representative/gh-data.csv create mode 100644 fixtures/representative/gh-tree.json diff --git a/fixtures/representative/README.md b/fixtures/representative/README.md new file mode 100644 index 0000000..a0b7a2b --- /dev/null +++ b/fixtures/representative/README.md @@ -0,0 +1,14 @@ +This directory contains a small dataset intended to be representative of all +of the types of data encountered in Timetable. + +The ``django-fixture.json`` file was created using: + + $ dj dumpdata --indent=4 --format=json -e timetables.ThingLock timetables + +``gh-data.csv`` was created by loading ``django-fixture.json`` into a clean Timetable install, then running: + + $ dj grasshopper_export_data + +``gh-tree.json`` was created by running [``etc/scripts/timetable/old-stack-import/generate-tree.js``](https://github.com/fronteerio/grasshopper/blob/bd5741355509af84d2b374172579b17d83e72ac5/etc/scripts/timetable/old-stack-import/generate-tree.js) from the [grasshopper](https://github.com/fronteerio/grasshopper) repo on ``gh-data.csv``. + +The data in all 3 files has been manually verified to be mutually consistent. diff --git a/fixtures/representative/django-fixture.json b/fixtures/representative/django-fixture.json new file mode 100644 index 0000000..5f3ef9f --- /dev/null +++ b/fixtures/representative/django-fixture.json @@ -0,0 +1,1086 @@ +[ +{ + "pk": 1, + "model": "timetables.thing", + "fields": { + "name": "tripos", + "parent": null, + "data": "", + "pathid": "p4pmLBPvFQjdJGPWzPW-yahYfT0=", + "fullpath": "tripos", + "type": "", + "fullname": "All Triposes" + } +}, +{ + "pk": 2, + "model": "timetables.thing", + "fields": { + "name": "user", + "parent": null, + "data": "", + "pathid": "Et6pb-wgWTVmq3VpLJlJWWgzrck=", + "fullpath": "user", + "type": "", + "fullname": "All Users" + } +}, +{ + "pk": 3, + "model": "timetables.thing", + "fields": { + "name": "hal", + "parent": 2, + "data": "{\"salt\": \"YYmFEIPopckMevmjcJVGkEVf274=\"}", + "pathid": "6cNw3dH9KHajMrM--6Cu-BikfQg=", + "fullpath": "user/hal", + "type": "user", + "fullname": "A Users Calendar" + } +}, +{ + "pk": 4, + "model": "timetables.thing", + "fields": { + "name": "simple-tripos", + "parent": 1, + "data": "", + "pathid": "MlUWOuWXW7qRoJnpBQjSAx5pE9g=", + "fullpath": "tripos/simple-tripos", + "type": "tripos", + "fullname": "Simple Tripos" + } +}, +{ + "pk": 5, + "model": "timetables.thing", + "fields": { + "name": "I", + "parent": 4, + "data": "", + "pathid": "sv4CGCwxit9dVfnCk1n15Zuxrm8=", + "fullpath": "tripos/simple-tripos/I", + "type": "part", + "fullname": "Part I" + } +}, +{ + "pk": 6, + "model": "timetables.thing", + "fields": { + "name": "II", + "parent": 4, + "data": "", + "pathid": "TlMuU-CJWtjv_1ZWoDYvx134yJI=", + "fullpath": "tripos/simple-tripos/II", + "type": "part", + "fullname": "Part II" + } +}, +{ + "pk": 7, + "model": "timetables.thing", + "fields": { + "name": "nested-subject", + "parent": 1, + "data": "", + "pathid": "bUM7__pG2izMa6ey6dBcYfcYxdg=", + "fullpath": "tripos/nested-subject", + "type": "tripos", + "fullname": "Nested Subject" + } +}, +{ + "pk": 8, + "model": "timetables.thing", + "fields": { + "name": "I", + "parent": 7, + "data": "", + "pathid": "zpUrgf6k6I_VYLIsDXWSBoHFnzE=", + "fullpath": "tripos/nested-subject/I", + "type": "part", + "fullname": "Part I" + } +}, +{ + "pk": 9, + "model": "timetables.thing", + "fields": { + "name": "II", + "parent": 7, + "data": "", + "pathid": "IxvwEE76HyEfCGFA0ymMCWgZbh4=", + "fullpath": "tripos/nested-subject/II", + "type": "part", + "fullname": "Part II" + } +}, +{ + "pk": 10, + "model": "timetables.thing", + "fields": { + "name": "subj-a", + "parent": 8, + "data": "", + "pathid": "JvLOzsX_dgeWcR51JBSHdVkvMBA=", + "fullpath": "tripos/nested-subject/I/subj-a", + "type": "subject", + "fullname": "Subject A" + } +}, +{ + "pk": 11, + "model": "timetables.thing", + "fields": { + "name": "subj-b", + "parent": 8, + "data": "", + "pathid": "O6H16VH52s5uNh3OraL9egTCWr4=", + "fullpath": "tripos/nested-subject/I/subj-b", + "type": "subject", + "fullname": "Subject B" + } +}, +{ + "pk": 12, + "model": "timetables.thing", + "fields": { + "name": "subj-a", + "parent": 9, + "data": "", + "pathid": "tyUGbz2hOdCV5EkZvPgvuU8tFjE=", + "fullpath": "tripos/nested-subject/II/subj-a", + "type": "subject", + "fullname": "Subject A" + } +}, +{ + "pk": 13, + "model": "timetables.thing", + "fields": { + "name": "module_a", + "parent": 5, + "data": "", + "pathid": "ssnoorNX1YmiSWNEAn0N9vEMNzc=", + "fullpath": "tripos/simple-tripos/I/module_a", + "type": "module", + "fullname": "Module A" + } +}, +{ + "pk": 14, + "model": "timetables.thing", + "fields": { + "name": "module_b", + "parent": 6, + "data": "", + "pathid": "LE0DWn9RbW8L2fy54MWOnPw4sDw=", + "fullpath": "tripos/simple-tripos/II/module_b", + "type": "module", + "fullname": "Module B" + } +}, +{ + "pk": 15, + "model": "timetables.thing", + "fields": { + "name": "module_c", + "parent": 6, + "data": "", + "pathid": "mw9hI_aHX8jxVe17DqmF8hDJMLs=", + "fullpath": "tripos/simple-tripos/II/module_c", + "type": "module", + "fullname": "Module C" + } +}, +{ + "pk": 16, + "model": "timetables.thing", + "fields": { + "name": "module_d", + "parent": 10, + "data": "", + "pathid": "ZiJjJQ5gMD-6qSvcI3yXwrY-lYc=", + "fullpath": "tripos/nested-subject/I/subj-a/module_d", + "type": "module", + "fullname": "Module D" + } +}, +{ + "pk": 17, + "model": "timetables.thing", + "fields": { + "name": "module_e", + "parent": 11, + "data": "", + "pathid": "E43to0nxSw6uL4TKan5rfvAftlA=", + "fullpath": "tripos/nested-subject/I/subj-b/module_e", + "type": "module", + "fullname": "Module E" + } +}, +{ + "pk": 18, + "model": "timetables.thing", + "fields": { + "name": "module_f", + "parent": 11, + "data": "", + "pathid": "I-z48bz1pxWVv_2SwS_RobnOcew=", + "fullpath": "tripos/nested-subject/I/subj-b/module_f", + "type": "module", + "fullname": "Module F" + } +}, +{ + "pk": 19, + "model": "timetables.thing", + "fields": { + "name": "module_g", + "parent": 12, + "data": "", + "pathid": "JhM27mjwSqCwkU335K5hyNXS_2Y=", + "fullpath": "tripos/nested-subject/II/subj-a/module_g", + "type": "module", + "fullname": "Module G" + } +}, +{ + "pk": 20, + "model": "timetables.thing", + "fields": { + "name": "module_h", + "parent": 12, + "data": "", + "pathid": "untJEcEfOUYMAXVs7OWpyTvlSFU=", + "fullpath": "tripos/nested-subject/II/subj-a/module_h", + "type": "module", + "fullname": "Module H" + } +}, +{ + "pk": 21, + "model": "timetables.thing", + "fields": { + "name": "common", + "parent": 10, + "data": "", + "pathid": "wlYI2aRxNbYDwSyANMuAiOyADyQ=", + "fullpath": "tripos/nested-subject/I/subj-a/common", + "type": "module", + "fullname": "Common" + } +}, +{ + "pk": 22, + "model": "timetables.thing", + "fields": { + "name": "common", + "parent": 12, + "data": "", + "pathid": "xnS8_sJZB1cxp0yrcaKIK_ihGdw=", + "fullpath": "tripos/nested-subject/II/subj-a/common", + "type": "module", + "fullname": "Common" + } +}, +{ + "pk": 23, + "model": "timetables.thing", + "fields": { + "name": "External", + "parent": 4, + "data": "{\r\n \"external_website_url\": \"https://www.example.org/\"\r\n}", + "pathid": "h01nE8b2A-5I0kcrMjrMagMDZOo=", + "fullpath": "tripos/simple-tripos/External", + "type": "part", + "fullname": "External Part" + } +}, +{ + "pk": 1, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series A", + "sourcefile": "", + "current": true, + "master": 1, + "data": "{\"location\": \"Room B\", \"people\": [\"Dr A\", \"Mr B and Ms C\"], \"datePattern\": \"Mi1-2 Th 9\"}", + "versionstamp": "2015-05-22T20:05:37.722Z" + } +}, +{ + "pk": 2, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series B", + "sourcefile": "", + "current": true, + "master": 2, + "data": "{\"datePattern\": \"Mi4 M 12\", \"location\": \"Room B\", \"people\": [\"Mr ABC\"]}", + "versionstamp": "2015-05-22T20:16:57.322Z" + } +}, +{ + "pk": 3, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series C", + "sourcefile": "", + "current": true, + "master": 3, + "data": "{\"datePattern\": \"Le4 Tu 2:25-3:46\", \"location\": \"Room C\", \"people\": [\"abc12\", \"def34\"]}", + "versionstamp": "2015-05-22T20:21:01.215Z" + } +}, +{ + "pk": 4, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series D 1", + "sourcefile": "", + "current": true, + "master": 4, + "data": "{\"location\": \"Room D\", \"people\": [\"Prof D\"], \"datePattern\": \"Mi1 Le1 Ea1 F 12\"}", + "versionstamp": "2015-05-22T20:30:20.208Z" + } +}, +{ + "pk": 5, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series D2", + "sourcefile": "", + "current": true, + "master": 5, + "data": "{\"datePattern\": \"Mi-1,0,9-10 Th 9\", \"location\": \"Room D\", \"people\": [\"Mr Pink\"]}", + "versionstamp": "2015-05-22T20:38:01.420Z" + } +}, +{ + "pk": 6, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series E", + "sourcefile": "", + "current": true, + "master": 6, + "data": "{\"datePattern\": \"Mi1 Tu 2\", \"location\": \"Room E\", \"people\": [\"Ms E\"]}", + "versionstamp": "2015-05-22T20:42:35.392Z" + } +}, +{ + "pk": 7, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series F", + "sourcefile": "", + "current": true, + "master": 7, + "data": "{\"datePattern\": \"Mi5 F 8\", \"location\": \"Room F\", \"people\": [\"Mr F\"]}", + "versionstamp": "2015-05-22T20:44:11.531Z" + } +}, +{ + "pk": 8, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series G", + "sourcefile": "", + "current": true, + "master": 8, + "data": "{\"datePattern\": \"Le4 M 1\", \"location\": \"Room G\", \"people\": [\"Gee Gson\"]}", + "versionstamp": "2015-05-22T20:49:19.770Z" + } +}, +{ + "pk": 9, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series H", + "sourcefile": "", + "current": true, + "master": 9, + "data": "{\"datePattern\": \"Mi4 Th 5\", \"location\": \"Room H\", \"people\": [\"Dr H\"]}", + "versionstamp": "2015-05-22T20:52:05.040Z" + } +}, +{ + "pk": 10, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Shared Subject A Series", + "sourcefile": "", + "current": true, + "master": 10, + "data": "{\"datePattern\": \"Mi1 Th 9\", \"location\": \"A room\", \"people\": [\"Mr Blue\"]}", + "versionstamp": "2015-05-22T21:10:30.195Z" + } +}, +{ + "pk": 1, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-09T09:00:00Z", + "versionstamp": "2015-05-22T20:08:02.290Z", + "title": "Blah A", + "source": 1, + "endtz": "Europe/London", + "current": false, + "start": "2014-10-09T08:00:00Z", + "master": null, + "location": "Room A", + "uid": "d6560f87-3c7e-49ff-8706-16ba321cb490", + "data": "{\"type\": \"journal club\", \"people\": [\"Dr A\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 2, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-09T09:00:00Z", + "versionstamp": "2015-05-22T20:08:18.570Z", + "title": "Blah A", + "source": 1, + "endtz": "Europe/London", + "current": false, + "start": "2014-10-09T08:00:00Z", + "master": 1, + "location": "Room A", + "uid": "d6560f87-3c7e-49ff-8706-16ba321cb490", + "data": "{\"type\": \"journal club\", \"people\": [\"Dr A\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 3, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-16T09:00:00Z", + "versionstamp": "2015-05-22T20:08:18.587Z", + "title": "Blah A", + "source": 1, + "endtz": "Europe/London", + "current": false, + "start": "2014-10-16T08:00:00Z", + "master": null, + "location": "Room A", + "uid": "90bcdace-b09c-4860-8e86-1a2527931375", + "data": "{\"type\": \"journal club\", \"people\": [\"Dr A\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 4, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-09T09:00:00Z", + "versionstamp": "2015-05-22T20:15:33.343Z", + "title": "Blah A", + "source": 1, + "endtz": "Europe/London", + "current": false, + "start": "2014-10-09T08:00:00Z", + "master": 1, + "location": "Room A", + "uid": "d6560f87-3c7e-49ff-8706-16ba321cb490", + "data": "{\"type\": \"journal club\", \"people\": [\"Dr A\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 5, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-16T09:00:00Z", + "versionstamp": "2015-05-22T20:15:33.367Z", + "title": "Blah A", + "source": 1, + "endtz": "Europe/London", + "current": false, + "start": "2014-10-16T08:00:00Z", + "master": 3, + "location": "Room B", + "uid": "90bcdace-b09c-4860-8e86-1a2527931375", + "data": "{\"type\": \"journal club\", \"people\": [\"Dr A\", \"Mr B and Ms C\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 6, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-09T09:00:00Z", + "versionstamp": "2015-05-22T20:15:45.222Z", + "title": "Blah A", + "source": 1, + "endtz": "Europe/London", + "current": true, + "start": "2014-10-09T08:00:00Z", + "master": 1, + "location": "Room A", + "uid": "d6560f87-3c7e-49ff-8706-16ba321cb490", + "data": "{\"type\": \"journal club\", \"people\": [\"Dr A\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 7, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-16T09:00:00Z", + "versionstamp": "2015-05-22T20:15:45.241Z", + "title": "Blah A", + "source": 1, + "endtz": "Europe/London", + "current": true, + "start": "2014-10-16T08:00:00Z", + "master": 3, + "location": "Room B", + "uid": "90bcdace-b09c-4860-8e86-1a2527931375", + "data": "{\"type\": \"field trip\", \"people\": [\"Dr A\", \"Mr B and Ms C\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 8, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-11-03T13:00:00Z", + "versionstamp": "2015-05-22T20:20:42.584Z", + "title": "Thing B", + "source": 2, + "endtz": "Europe/London", + "current": true, + "start": "2014-11-03T12:00:00Z", + "master": null, + "location": "Room B", + "uid": "ef6ad702-7a52-45cd-8b18-09ffcff660b9", + "data": "{\"type\": \"practical\", \"people\": [\"Mr ABC\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 9, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2015-02-10T15:46:00Z", + "versionstamp": "2015-05-22T20:24:11.291Z", + "title": "Thing C", + "source": 3, + "endtz": "Europe/London", + "current": true, + "start": "2015-02-10T14:25:00Z", + "master": null, + "location": "Room C", + "uid": "17e8f1f6-3813-4066-911a-6e6ae4888add", + "data": "{\"type\": \"workshop\", \"people\": [\"abc12\", \"def34\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 10, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-10T12:00:00Z", + "versionstamp": "2015-05-22T20:34:01.717Z", + "title": "D1", + "source": 4, + "endtz": "Europe/London", + "current": false, + "start": "2014-10-10T11:00:00Z", + "master": null, + "location": "Room D", + "uid": "efd977cd-eb72-42e5-8516-8f6883620f28", + "data": "{\"type\": \"lecture\", \"people\": [\"Prof D\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 11, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-10T12:00:00Z", + "versionstamp": "2015-05-22T20:34:17.279Z", + "title": "D1", + "source": 4, + "endtz": "Europe/London", + "current": false, + "start": "2014-10-10T11:00:00Z", + "master": 10, + "location": "Room D", + "uid": "efd977cd-eb72-42e5-8516-8f6883620f28", + "data": "{\"type\": \"lecture\", \"people\": [\"Prof D\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 12, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2015-01-16T13:00:00Z", + "versionstamp": "2015-05-22T20:34:17.295Z", + "title": "D2", + "source": 4, + "endtz": "Europe/London", + "current": false, + "start": "2015-01-16T12:00:00Z", + "master": null, + "location": "Room D", + "uid": "77aadf4b-2a54-48f0-9d3b-3818471c95cf", + "data": "{\"type\": \"lecture\", \"people\": [\"Prof D\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 13, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-10T12:00:00Z", + "versionstamp": "2015-05-22T20:34:31.606Z", + "title": "D1", + "source": 4, + "endtz": "Europe/London", + "current": true, + "start": "2014-10-10T11:00:00Z", + "master": 10, + "location": "Room D", + "uid": "efd977cd-eb72-42e5-8516-8f6883620f28", + "data": "{\"type\": \"lecture\", \"people\": [\"Prof D\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 14, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2015-01-16T13:00:00Z", + "versionstamp": "2015-05-22T20:34:31.625Z", + "title": "D2", + "source": 4, + "endtz": "Europe/London", + "current": true, + "start": "2015-01-16T12:00:00Z", + "master": 12, + "location": "Room D", + "uid": "77aadf4b-2a54-48f0-9d3b-3818471c95cf", + "data": "{\"type\": \"lecture\", \"people\": [\"Prof D\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 15, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2015-04-24T12:00:00Z", + "versionstamp": "2015-05-22T20:34:31.640Z", + "title": "D3", + "source": 4, + "endtz": "Europe/London", + "current": true, + "start": "2015-04-24T11:00:00Z", + "master": null, + "location": "Room D", + "uid": "634053a1-a29c-4ecc-aa42-61c836208857", + "data": "{\"type\": \"lecture\", \"people\": [\"Prof D\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 16, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-09-25T09:00:00Z", + "versionstamp": "2015-05-22T20:40:04.192Z", + "title": "D2 Thing", + "source": 5, + "endtz": "Europe/London", + "current": true, + "start": "2014-09-25T08:00:00Z", + "master": null, + "location": "Room D", + "uid": "2ee5f91e-9d3d-46fd-8bf6-f54a5bf15498", + "data": "{\"type\": \"presentation\", \"people\": [\"Mr Pink\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 17, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-02T09:00:00Z", + "versionstamp": "2015-05-22T20:40:04.211Z", + "title": "D2 Thing", + "source": 5, + "endtz": "Europe/London", + "current": true, + "start": "2014-10-02T08:00:00Z", + "master": null, + "location": "Room D", + "uid": "16b69975-be65-41d2-a12c-6c10fd072e27", + "data": "{\"type\": \"presentation\", \"people\": [\"Mr Pink\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 18, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-12-04T10:00:00Z", + "versionstamp": "2015-05-22T20:40:04.228Z", + "title": "D2 Thing", + "source": 5, + "endtz": "Europe/London", + "current": true, + "start": "2014-12-04T09:00:00Z", + "master": null, + "location": "Room D", + "uid": "2f3761e1-1205-4262-85ae-2e8a22044bc4", + "data": "{\"type\": \"presentation\", \"people\": [\"Mr Pink\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 19, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-12-11T10:00:00Z", + "versionstamp": "2015-05-22T20:40:04.243Z", + "title": "D2 Thing", + "source": 5, + "endtz": "Europe/London", + "current": true, + "start": "2014-12-11T09:00:00Z", + "master": null, + "location": "Room D", + "uid": "28874f98-8e5b-40a4-a64a-699010af7f70", + "data": "{\"type\": \"presentation\", \"people\": [\"Mr Pink\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 20, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-14T14:00:00Z", + "versionstamp": "2015-05-22T20:43:53.282Z", + "title": "Event E", + "source": 6, + "endtz": "Europe/London", + "current": true, + "start": "2014-10-14T13:00:00Z", + "master": null, + "location": "Room E", + "uid": "13764e88-afb0-4407-89fb-9924c9d108a9", + "data": "{\"type\": \"class\", \"people\": [\"Ms E\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 21, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-11-07T09:00:00Z", + "versionstamp": "2015-05-22T20:48:50.190Z", + "title": "Event F", + "source": 7, + "endtz": "Europe/London", + "current": true, + "start": "2014-11-07T08:00:00Z", + "master": null, + "location": "Room F", + "uid": "75367174-0791-4132-89bb-541e19d64ac4", + "data": "{\"type\": \"seminar\", \"people\": [\"Mr F\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 22, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2015-02-09T14:00:00Z", + "versionstamp": "2015-05-22T20:51:09.211Z", + "title": "Event G", + "source": 8, + "endtz": "Europe/London", + "current": true, + "start": "2015-02-09T13:00:00Z", + "master": null, + "location": "Room G", + "uid": "288e88be-9fa0-46e1-bb94-cbd8ce39dc3d", + "data": "{\"type\": \"seminar\", \"people\": [\"Gee Gson\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 23, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-30T18:00:00Z", + "versionstamp": "2015-05-22T20:52:59.525Z", + "title": "Event H", + "source": 9, + "endtz": "Europe/London", + "current": true, + "start": "2014-10-30T17:00:00Z", + "master": null, + "location": "Room H", + "uid": "9ae81002-5484-47ee-a54c-fffe5cf5b8c1", + "data": "{\"type\": \"\", \"people\": [\"Dr H\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 24, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-09T09:00:00Z", + "versionstamp": "2015-05-22T21:13:41.890Z", + "title": "Intro to Subject A", + "source": 10, + "endtz": "Europe/London", + "current": true, + "start": "2014-10-09T08:00:00Z", + "master": null, + "location": "A room", + "uid": "6b4fd2f0-8983-4ca8-ab19-ff7fd1301f37", + "data": "{\"type\": \"workshop\", \"people\": [\"Mr Blue\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 1, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 13, + "eventsource": 1, + "annotation": "home" + } +}, +{ + "pk": 2, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 14, + "eventsource": 2, + "annotation": "home" + } +}, +{ + "pk": 3, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 15, + "eventsource": 3, + "annotation": "home" + } +}, +{ + "pk": 4, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 16, + "eventsource": 4, + "annotation": "home" + } +}, +{ + "pk": 5, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 16, + "eventsource": 5, + "annotation": "home" + } +}, +{ + "pk": 6, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 17, + "eventsource": 6, + "annotation": "home" + } +}, +{ + "pk": 7, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 18, + "eventsource": 7, + "annotation": "home" + } +}, +{ + "pk": 8, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 19, + "eventsource": 8, + "annotation": "home" + } +}, +{ + "pk": 9, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 20, + "eventsource": 9, + "annotation": "home" + } +}, +{ + "pk": 10, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 3, + "eventsource": 4, + "annotation": null + } +}, +{ + "pk": 11, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 3, + "eventsource": 8, + "annotation": null + } +}, +{ + "pk": 12, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 3, + "eventsource": 9, + "annotation": null + } +}, +{ + "pk": 13, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 21, + "eventsource": 10, + "annotation": "home" + } +}, +{ + "pk": 14, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 22, + "eventsource": 10, + "annotation": "home" + } +}, +{ + "pk": 15, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 3, + "eventsource": 10, + "annotation": null + } +}, +{ + "pk": 1, + "model": "timetables.thingtag", + "fields": { + "thing": 3, + "targetthing": 6, + "annotation": "admin" + } +}, +{ + "pk": 2, + "model": "timetables.thingtag", + "fields": { + "thing": 3, + "targetthing": 5, + "annotation": "admin" + } +}, +{ + "pk": 3, + "model": "timetables.thingtag", + "fields": { + "thing": 3, + "targetthing": 12, + "annotation": "admin" + } +}, +{ + "pk": 4, + "model": "timetables.thingtag", + "fields": { + "thing": 3, + "targetthing": 11, + "annotation": "admin" + } +}, +{ + "pk": 5, + "model": "timetables.thingtag", + "fields": { + "thing": 3, + "targetthing": 10, + "annotation": "admin" + } +}, +{ + "pk": 6, + "model": "timetables.thingtag", + "fields": { + "thing": 23, + "targetthing": 23, + "annotation": "disabled" + } +} +] diff --git a/fixtures/representative/gh-data.csv b/fixtures/representative/gh-data.csv new file mode 100644 index 0000000..1031f48 --- /dev/null +++ b/fixtures/representative/gh-data.csv @@ -0,0 +1,18 @@ +"Tripos Id","Tripos Name","Part Id","Part Name","Subpart Id","Subpart Name","Module Id","Module Name","Series ID","Series Name","Event ID","Title","Type","Start","End","Location","People" +"4","Simple Tripos","5","Part I","","","13","Module A","1","Series A","6","Blah A","journal club","2014-10-09T09:00:00+01:00","2014-10-09T10:00:00+01:00","Room A","Dr A" +"4","Simple Tripos","5","Part I","","","13","Module A","1","Series A","7","Blah A","field trip","2014-10-16T09:00:00+01:00","2014-10-16T10:00:00+01:00","Room B","Dr A#Mr B and Ms C" +"7","Nested Subject","8","Part I","10","Subject A","16","Module D","5","Series D2","18","D2 Thing","presentation","2014-12-04T09:00:00+00:00","2014-12-04T10:00:00+00:00","Room D","Mr Pink" +"4","Simple Tripos","6","Part II","","","14","Module B","2","Series B","8","Thing B","practical","2014-11-03T12:00:00+00:00","2014-11-03T13:00:00+00:00","Room B","Mr ABC" +"4","Simple Tripos","6","Part II","","","15","Module C","3","Series C","9","Thing C","workshop","2015-02-10T14:25:00+00:00","2015-02-10T15:46:00+00:00","Room C","abc12#def34" +"7","Nested Subject","8","Part I","10","Subject A","16","Module D","4","Series D 1","13","D1","lecture","2014-10-10T12:00:00+01:00","2014-10-10T13:00:00+01:00","Room D","Prof D" +"7","Nested Subject","8","Part I","10","Subject A","16","Module D","4","Series D 1","14","D2","lecture","2015-01-16T12:00:00+00:00","2015-01-16T13:00:00+00:00","Room D","Prof D" +"7","Nested Subject","8","Part I","10","Subject A","16","Module D","5","Series D2","19","D2 Thing","presentation","2014-12-11T09:00:00+00:00","2014-12-11T10:00:00+00:00","Room D","Mr Pink" +"7","Nested Subject","8","Part I","10","Subject A","16","Module D","4","Series D 1","15","D3","lecture","2015-04-24T12:00:00+01:00","2015-04-24T13:00:00+01:00","Room D","Prof D" +"7","Nested Subject","8","Part I","10","Subject A","16","Module D","5","Series D2","16","D2 Thing","presentation","2014-09-25T09:00:00+01:00","2014-09-25T10:00:00+01:00","Room D","Mr Pink" +"7","Nested Subject","9","Part II","12","Subject A","20","Module H","9","Series H","23","Event H","","2014-10-30T17:00:00+00:00","2014-10-30T18:00:00+00:00","Room H","Dr H" +"7","Nested Subject","8","Part I","10","Subject A","16","Module D","5","Series D2","17","D2 Thing","presentation","2014-10-02T09:00:00+01:00","2014-10-02T10:00:00+01:00","Room D","Mr Pink" +"7","Nested Subject","8","Part I","11","Subject B","17","Module E","6","Series E","20","Event E","class","2014-10-14T14:00:00+01:00","2014-10-14T15:00:00+01:00","Room E","Ms E" +"7","Nested Subject","8","Part I","10","Subject A","21","Common","10","Shared Subject A Series","24","Intro to Subject A","workshop","2014-10-09T09:00:00+01:00","2014-10-09T10:00:00+01:00","A room","Mr Blue" +"7","Nested Subject","9","Part II","12","Subject A","22","Common","10","Shared Subject A Series","24","Intro to Subject A","workshop","2014-10-09T09:00:00+01:00","2014-10-09T10:00:00+01:00","A room","Mr Blue" +"7","Nested Subject","8","Part I","11","Subject B","18","Module F","7","Series F","21","Event F","seminar","2014-11-07T08:00:00+00:00","2014-11-07T09:00:00+00:00","Room F","Mr F" +"7","Nested Subject","9","Part II","12","Subject A","19","Module G","8","Series G","22","Event G","seminar","2015-02-09T13:00:00+00:00","2015-02-09T14:00:00+00:00","Room G","Gee Gson" diff --git a/fixtures/representative/gh-tree.json b/fixtures/representative/gh-tree.json new file mode 100644 index 0000000..cd57a29 --- /dev/null +++ b/fixtures/representative/gh-tree.json @@ -0,0 +1,423 @@ +{ + "name": "Timetable", + "type": "root", + "nodes": { + "4": { + "id": "4", + "name": "Simple Tripos", + "type": "course", + "nodes": { + "5-1": { + "id": "5-1", + "name": "Part I", + "type": "part", + "nodes": { + "13": { + "id": "13", + "name": "Module A", + "type": "module", + "nodes": { + "1": { + "id": "1", + "name": "Series A", + "type": "series", + "nodes": { + "6": { + "id": "6", + "name": "Blah A", + "type": "event", + "event-type": "journal club", + "start": "2014-10-09T09:00:00+01:00", + "end": "2014-10-09T10:00:00+01:00", + "location": "Room A", + "people": [ + "Dr A" + ] + }, + "7": { + "id": "7", + "name": "Blah A", + "type": "event", + "event-type": "field trip", + "start": "2014-10-16T09:00:00+01:00", + "end": "2014-10-16T10:00:00+01:00", + "location": "Room B", + "people": [ + "Dr A", + "Mr B", + "Ms C" + ] + } + } + } + } + } + } + }, + "6-3": { + "id": "6-3", + "name": "Part II", + "type": "part", + "nodes": { + "14": { + "id": "14", + "name": "Module B", + "type": "module", + "nodes": { + "2": { + "id": "2", + "name": "Series B", + "type": "series", + "nodes": { + "8": { + "id": "8", + "name": "Thing B", + "type": "event", + "event-type": "practical", + "start": "2014-11-03T12:00:00+00:00", + "end": "2014-11-03T13:00:00+00:00", + "location": "Room B", + "people": [ + "Mr ABC" + ] + } + } + } + } + }, + "15": { + "id": "15", + "name": "Module C", + "type": "module", + "nodes": { + "3": { + "id": "3", + "name": "Series C", + "type": "series", + "nodes": { + "9": { + "id": "9", + "name": "Thing C", + "type": "event", + "event-type": "workshop", + "start": "2015-02-10T14:25:00+00:00", + "end": "2015-02-10T15:46:00+00:00", + "location": "Room C", + "people": [ + "abc12", + "def34" + ] + } + } + } + } + } + } + } + } + }, + "7": { + "id": "7", + "name": "Nested Subject", + "type": "course", + "nodes": { + "Subject A": { + "id": "Subject A", + "type": "subject", + "name": "Subject A", + "nodes": { + "8-2": { + "id": "8-2", + "name": "Part I", + "type": "part", + "nodes": { + "16": { + "id": "16", + "name": "Module D", + "type": "module", + "nodes": { + "4": { + "id": "4", + "name": "Series D 1", + "type": "series", + "nodes": { + "13": { + "id": "13", + "name": "D1", + "type": "event", + "event-type": "lecture", + "start": "2014-10-10T12:00:00+01:00", + "end": "2014-10-10T13:00:00+01:00", + "location": "Room D", + "people": [ + "Prof D" + ] + }, + "14": { + "id": "14", + "name": "D2", + "type": "event", + "event-type": "lecture", + "start": "2015-01-16T12:00:00+00:00", + "end": "2015-01-16T13:00:00+00:00", + "location": "Room D", + "people": [ + "Prof D" + ] + }, + "15": { + "id": "15", + "name": "D3", + "type": "event", + "event-type": "lecture", + "start": "2015-04-24T12:00:00+01:00", + "end": "2015-04-24T13:00:00+01:00", + "location": "Room D", + "people": [ + "Prof D" + ] + } + } + }, + "5": { + "id": "5", + "name": "Series D2", + "type": "series", + "nodes": { + "16": { + "id": "16", + "name": "D2 Thing", + "type": "event", + "event-type": "presentation", + "start": "2014-09-25T09:00:00+01:00", + "end": "2014-09-25T10:00:00+01:00", + "location": "Room D", + "people": [ + "Mr Pink" + ] + }, + "17": { + "id": "17", + "name": "D2 Thing", + "type": "event", + "event-type": "presentation", + "start": "2014-10-02T09:00:00+01:00", + "end": "2014-10-02T10:00:00+01:00", + "location": "Room D", + "people": [ + "Mr Pink" + ] + }, + "18": { + "id": "18", + "name": "D2 Thing", + "type": "event", + "event-type": "presentation", + "start": "2014-12-04T09:00:00+00:00", + "end": "2014-12-04T10:00:00+00:00", + "location": "Room D", + "people": [ + "Mr Pink" + ] + }, + "19": { + "id": "19", + "name": "D2 Thing", + "type": "event", + "event-type": "presentation", + "start": "2014-12-11T09:00:00+00:00", + "end": "2014-12-11T10:00:00+00:00", + "location": "Room D", + "people": [ + "Mr Pink" + ] + } + } + } + } + }, + "21": { + "id": "21", + "name": "Common", + "type": "module", + "nodes": { + "10": { + "id": "10", + "name": "Shared Subject A Series", + "type": "series", + "nodes": { + "24": { + "id": "24", + "name": "Intro to Subject A", + "type": "event", + "event-type": "workshop", + "start": "2014-10-09T09:00:00+01:00", + "end": "2014-10-09T10:00:00+01:00", + "location": "A room", + "people": [ + "Mr Blue" + ] + } + } + } + } + } + } + }, + "9-4": { + "id": "9-4", + "name": "Part II", + "type": "part", + "nodes": { + "19": { + "id": "19", + "name": "Module G", + "type": "module", + "nodes": { + "8": { + "id": "8", + "name": "Series G", + "type": "series", + "nodes": { + "22": { + "id": "22", + "name": "Event G", + "type": "event", + "event-type": "seminar", + "start": "2015-02-09T13:00:00+00:00", + "end": "2015-02-09T14:00:00+00:00", + "location": "Room G", + "people": [ + "Gee Gson" + ] + } + } + } + } + }, + "20": { + "id": "20", + "name": "Module H", + "type": "module", + "nodes": { + "9": { + "id": "9", + "name": "Series H", + "type": "series", + "nodes": { + "23": { + "id": "23", + "name": "Event H", + "type": "event", + "event-type": "", + "start": "2014-10-30T17:00:00+00:00", + "end": "2014-10-30T18:00:00+00:00", + "location": "Room H", + "people": [ + "Dr H" + ] + } + } + } + } + }, + "22": { + "id": "22", + "name": "Common", + "type": "module", + "nodes": { + "10": { + "id": "10", + "name": "Shared Subject A Series", + "type": "series", + "nodes": { + "24": { + "id": "24", + "name": "Intro to Subject A", + "type": "event", + "event-type": "workshop", + "start": "2014-10-09T09:00:00+01:00", + "end": "2014-10-09T10:00:00+01:00", + "location": "A room", + "people": [ + "Mr Blue" + ] + } + } + } + } + } + } + } + } + }, + "Subject B": { + "id": "Subject B", + "type": "subject", + "name": "Subject B", + "nodes": { + "8-5": { + "id": "8-5", + "name": "Part I", + "type": "part", + "nodes": { + "17": { + "id": "17", + "name": "Module E", + "type": "module", + "nodes": { + "6": { + "id": "6", + "name": "Series E", + "type": "series", + "nodes": { + "20": { + "id": "20", + "name": "Event E", + "type": "event", + "event-type": "class", + "start": "2014-10-14T14:00:00+01:00", + "end": "2014-10-14T15:00:00+01:00", + "location": "Room E", + "people": [ + "Ms E" + ] + } + } + } + } + }, + "18": { + "id": "18", + "name": "Module F", + "type": "module", + "nodes": { + "7": { + "id": "7", + "name": "Series F", + "type": "series", + "nodes": { + "21": { + "id": "21", + "name": "Event F", + "type": "event", + "event-type": "seminar", + "start": "2014-11-07T08:00:00+00:00", + "end": "2014-11-07T09:00:00+00:00", + "location": "Room F", + "people": [ + "Mr F" + ] + } + } + } + } + } + } + } + } + } + } + } + } +} \ No newline at end of file From dba294129e35864a8df230f5d653edaa0fb83092 Mon Sep 17 00:00:00 2001 From: Hal Blackburn Date: Tue, 9 Jun 2015 23:44:46 +0100 Subject: [PATCH 7/8] Add fixture with more disabled parts There's a disabled part with events and a URL, one with a URL and one without a URL. --- .../django-fixture.json | 1194 +++++++++++++++++ .../structure-verification/events.csv | 19 + .../structure-verification/structure.json | 102 ++ 3 files changed, 1315 insertions(+) create mode 100644 fixtures/representative/structure-verification/django-fixture.json create mode 100644 fixtures/representative/structure-verification/events.csv create mode 100644 fixtures/representative/structure-verification/structure.json diff --git a/fixtures/representative/structure-verification/django-fixture.json b/fixtures/representative/structure-verification/django-fixture.json new file mode 100644 index 0000000..f3206b3 --- /dev/null +++ b/fixtures/representative/structure-verification/django-fixture.json @@ -0,0 +1,1194 @@ +[ +{ + "pk": 1, + "model": "timetables.thing", + "fields": { + "name": "tripos", + "parent": null, + "data": "", + "pathid": "p4pmLBPvFQjdJGPWzPW-yahYfT0=", + "fullpath": "tripos", + "type": "", + "fullname": "All Triposes" + } +}, +{ + "pk": 2, + "model": "timetables.thing", + "fields": { + "name": "user", + "parent": null, + "data": "", + "pathid": "Et6pb-wgWTVmq3VpLJlJWWgzrck=", + "fullpath": "user", + "type": "", + "fullname": "All Users" + } +}, +{ + "pk": 3, + "model": "timetables.thing", + "fields": { + "name": "hal", + "parent": 2, + "data": "{\"salt\": \"YYmFEIPopckMevmjcJVGkEVf274=\"}", + "pathid": "6cNw3dH9KHajMrM--6Cu-BikfQg=", + "fullpath": "user/hal", + "type": "user", + "fullname": "A Users Calendar" + } +}, +{ + "pk": 4, + "model": "timetables.thing", + "fields": { + "name": "simple-tripos", + "parent": 1, + "data": "", + "pathid": "MlUWOuWXW7qRoJnpBQjSAx5pE9g=", + "fullpath": "tripos/simple-tripos", + "type": "tripos", + "fullname": "Simple Tripos" + } +}, +{ + "pk": 5, + "model": "timetables.thing", + "fields": { + "name": "I", + "parent": 4, + "data": "", + "pathid": "sv4CGCwxit9dVfnCk1n15Zuxrm8=", + "fullpath": "tripos/simple-tripos/I", + "type": "part", + "fullname": "Part I" + } +}, +{ + "pk": 6, + "model": "timetables.thing", + "fields": { + "name": "II", + "parent": 4, + "data": "", + "pathid": "TlMuU-CJWtjv_1ZWoDYvx134yJI=", + "fullpath": "tripos/simple-tripos/II", + "type": "part", + "fullname": "Part II" + } +}, +{ + "pk": 7, + "model": "timetables.thing", + "fields": { + "name": "nested-subject", + "parent": 1, + "data": "", + "pathid": "bUM7__pG2izMa6ey6dBcYfcYxdg=", + "fullpath": "tripos/nested-subject", + "type": "tripos", + "fullname": "Nested Subject" + } +}, +{ + "pk": 8, + "model": "timetables.thing", + "fields": { + "name": "I", + "parent": 7, + "data": "", + "pathid": "zpUrgf6k6I_VYLIsDXWSBoHFnzE=", + "fullpath": "tripos/nested-subject/I", + "type": "part", + "fullname": "Part I" + } +}, +{ + "pk": 9, + "model": "timetables.thing", + "fields": { + "name": "II", + "parent": 7, + "data": "", + "pathid": "IxvwEE76HyEfCGFA0ymMCWgZbh4=", + "fullpath": "tripos/nested-subject/II", + "type": "part", + "fullname": "Part II" + } +}, +{ + "pk": 10, + "model": "timetables.thing", + "fields": { + "name": "subj-a", + "parent": 8, + "data": "", + "pathid": "JvLOzsX_dgeWcR51JBSHdVkvMBA=", + "fullpath": "tripos/nested-subject/I/subj-a", + "type": "subject", + "fullname": "Subject A" + } +}, +{ + "pk": 11, + "model": "timetables.thing", + "fields": { + "name": "subj-b", + "parent": 8, + "data": "", + "pathid": "O6H16VH52s5uNh3OraL9egTCWr4=", + "fullpath": "tripos/nested-subject/I/subj-b", + "type": "subject", + "fullname": "Subject B" + } +}, +{ + "pk": 12, + "model": "timetables.thing", + "fields": { + "name": "subj-a", + "parent": 9, + "data": "", + "pathid": "tyUGbz2hOdCV5EkZvPgvuU8tFjE=", + "fullpath": "tripos/nested-subject/II/subj-a", + "type": "subject", + "fullname": "Subject A" + } +}, +{ + "pk": 13, + "model": "timetables.thing", + "fields": { + "name": "module_a", + "parent": 5, + "data": "", + "pathid": "ssnoorNX1YmiSWNEAn0N9vEMNzc=", + "fullpath": "tripos/simple-tripos/I/module_a", + "type": "module", + "fullname": "Module A" + } +}, +{ + "pk": 14, + "model": "timetables.thing", + "fields": { + "name": "module_b", + "parent": 6, + "data": "", + "pathid": "LE0DWn9RbW8L2fy54MWOnPw4sDw=", + "fullpath": "tripos/simple-tripos/II/module_b", + "type": "module", + "fullname": "Module B" + } +}, +{ + "pk": 15, + "model": "timetables.thing", + "fields": { + "name": "module_c", + "parent": 6, + "data": "", + "pathid": "mw9hI_aHX8jxVe17DqmF8hDJMLs=", + "fullpath": "tripos/simple-tripos/II/module_c", + "type": "module", + "fullname": "Module C" + } +}, +{ + "pk": 16, + "model": "timetables.thing", + "fields": { + "name": "module_d", + "parent": 10, + "data": "", + "pathid": "ZiJjJQ5gMD-6qSvcI3yXwrY-lYc=", + "fullpath": "tripos/nested-subject/I/subj-a/module_d", + "type": "module", + "fullname": "Module D" + } +}, +{ + "pk": 17, + "model": "timetables.thing", + "fields": { + "name": "module_e", + "parent": 11, + "data": "", + "pathid": "E43to0nxSw6uL4TKan5rfvAftlA=", + "fullpath": "tripos/nested-subject/I/subj-b/module_e", + "type": "module", + "fullname": "Module E" + } +}, +{ + "pk": 18, + "model": "timetables.thing", + "fields": { + "name": "module_f", + "parent": 11, + "data": "", + "pathid": "I-z48bz1pxWVv_2SwS_RobnOcew=", + "fullpath": "tripos/nested-subject/I/subj-b/module_f", + "type": "module", + "fullname": "Module F" + } +}, +{ + "pk": 19, + "model": "timetables.thing", + "fields": { + "name": "module_g", + "parent": 12, + "data": "", + "pathid": "JhM27mjwSqCwkU335K5hyNXS_2Y=", + "fullpath": "tripos/nested-subject/II/subj-a/module_g", + "type": "module", + "fullname": "Module G" + } +}, +{ + "pk": 20, + "model": "timetables.thing", + "fields": { + "name": "module_h", + "parent": 12, + "data": "", + "pathid": "untJEcEfOUYMAXVs7OWpyTvlSFU=", + "fullpath": "tripos/nested-subject/II/subj-a/module_h", + "type": "module", + "fullname": "Module H" + } +}, +{ + "pk": 21, + "model": "timetables.thing", + "fields": { + "name": "common", + "parent": 10, + "data": "", + "pathid": "wlYI2aRxNbYDwSyANMuAiOyADyQ=", + "fullpath": "tripos/nested-subject/I/subj-a/common", + "type": "module", + "fullname": "Common" + } +}, +{ + "pk": 22, + "model": "timetables.thing", + "fields": { + "name": "common", + "parent": 12, + "data": "", + "pathid": "xnS8_sJZB1cxp0yrcaKIK_ihGdw=", + "fullpath": "tripos/nested-subject/II/subj-a/common", + "type": "module", + "fullname": "Common" + } +}, +{ + "pk": 23, + "model": "timetables.thing", + "fields": { + "name": "External", + "parent": 4, + "data": "{\r\n \"external_website_url\": \"https://www.example.org/\"\r\n}", + "pathid": "h01nE8b2A-5I0kcrMjrMagMDZOo=", + "fullpath": "tripos/simple-tripos/External", + "type": "part", + "fullname": "External Part" + } +}, +{ + "pk": 24, + "model": "timetables.thing", + "fields": { + "name": "disabled_module", + "parent": 23, + "data": "", + "pathid": "GHyFEV_W17d7IrZaejM7BkB_3y8=", + "fullpath": "tripos/simple-tripos/External/disabled_module", + "type": "module", + "fullname": "Disabled Module" + } +}, +{ + "pk": 25, + "model": "timetables.thing", + "fields": { + "name": "external2", + "parent": 4, + "data": "{\r\n \"external_website_url\": \"https://www.example.org/2\"\r\n}", + "pathid": "-mKIjmOYdYVwsC-Q_2ZumKEmpuk=", + "fullpath": "tripos/simple-tripos/external2", + "type": "part", + "fullname": "External (no events with URL)" + } +}, +{ + "pk": 26, + "model": "timetables.thing", + "fields": { + "name": "external3", + "parent": 4, + "data": "", + "pathid": "wnWItYPDv6V5tEIWuD_3pczoarY=", + "fullpath": "tripos/simple-tripos/external3", + "type": "part", + "fullname": "External (no events no URL)" + } +}, +{ + "pk": 1, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series A", + "sourcefile": "", + "current": true, + "master": 1, + "data": "{\"location\": \"Room B\", \"people\": [\"Dr A\", \"Mr B and Ms C\"], \"datePattern\": \"Mi1-2 Th 9\"}", + "versionstamp": "2015-05-22T20:05:37.722Z" + } +}, +{ + "pk": 2, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series B", + "sourcefile": "", + "current": true, + "master": 2, + "data": "{\"datePattern\": \"Mi4 M 12\", \"location\": \"Room B\", \"people\": [\"Mr ABC\"]}", + "versionstamp": "2015-05-22T20:16:57.322Z" + } +}, +{ + "pk": 3, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series C", + "sourcefile": "", + "current": true, + "master": 3, + "data": "{\"datePattern\": \"Le4 Tu 2:25-3:46\", \"location\": \"Room C\", \"people\": [\"abc12\", \"def34\"]}", + "versionstamp": "2015-05-22T20:21:01.215Z" + } +}, +{ + "pk": 4, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series D 1", + "sourcefile": "", + "current": true, + "master": 4, + "data": "{\"location\": \"Room D\", \"people\": [\"Prof D\"], \"datePattern\": \"Mi1 Le1 Ea1 F 12\"}", + "versionstamp": "2015-05-22T20:30:20.208Z" + } +}, +{ + "pk": 5, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series D2", + "sourcefile": "", + "current": true, + "master": 5, + "data": "{\"datePattern\": \"Mi-1,0,9-10 Th 9\", \"location\": \"Room D\", \"people\": [\"Mr Pink\"]}", + "versionstamp": "2015-05-22T20:38:01.420Z" + } +}, +{ + "pk": 6, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series E", + "sourcefile": "", + "current": true, + "master": 6, + "data": "{\"datePattern\": \"Mi1 Tu 2\", \"location\": \"Room E\", \"people\": [\"Ms E\"]}", + "versionstamp": "2015-05-22T20:42:35.392Z" + } +}, +{ + "pk": 7, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series F", + "sourcefile": "", + "current": true, + "master": 7, + "data": "{\"datePattern\": \"Mi5 F 8\", \"location\": \"Room F\", \"people\": [\"Mr F\"]}", + "versionstamp": "2015-05-22T20:44:11.531Z" + } +}, +{ + "pk": 8, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series G", + "sourcefile": "", + "current": true, + "master": 8, + "data": "{\"datePattern\": \"Le4 M 1\", \"location\": \"Room G\", \"people\": [\"Gee Gson\"]}", + "versionstamp": "2015-05-22T20:49:19.770Z" + } +}, +{ + "pk": 9, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Series H", + "sourcefile": "", + "current": true, + "master": 9, + "data": "{\"datePattern\": \"Mi4 Th 5\", \"location\": \"Room H\", \"people\": [\"Dr H\"]}", + "versionstamp": "2015-05-22T20:52:05.040Z" + } +}, +{ + "pk": 10, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Shared Subject A Series", + "sourcefile": "", + "current": true, + "master": 10, + "data": "{\"datePattern\": \"Mi1 Th 9\", \"location\": \"A room\", \"people\": [\"Mr Blue\"]}", + "versionstamp": "2015-05-22T21:10:30.195Z" + } +}, +{ + "pk": 11, + "model": "timetables.eventsource", + "fields": { + "sourceurl": null, + "sourcetype": "pattern", + "title": "Disabled Series", + "sourcefile": "", + "current": true, + "master": 11, + "data": "{\"datePattern\": \"Mi1 Th 9\", \"location\": \"blah\", \"people\": [\"hi\"]}", + "versionstamp": "2015-06-09T20:41:56.977Z" + } +}, +{ + "pk": 1, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-09T09:00:00Z", + "versionstamp": "2015-05-22T20:08:02.290Z", + "title": "Blah A", + "source": 1, + "endtz": "Europe/London", + "current": false, + "start": "2014-10-09T08:00:00Z", + "master": 1, + "location": "Room A", + "uid": "d6560f87-3c7e-49ff-8706-16ba321cb490", + "data": "{\"type\": \"journal club\", \"people\": [\"Dr A\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 2, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-09T09:00:00Z", + "versionstamp": "2015-05-22T20:08:18.570Z", + "title": "Blah A", + "source": 1, + "endtz": "Europe/London", + "current": false, + "start": "2014-10-09T08:00:00Z", + "master": 1, + "location": "Room A", + "uid": "d6560f87-3c7e-49ff-8706-16ba321cb490", + "data": "{\"type\": \"journal club\", \"people\": [\"Dr A\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 3, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-16T09:00:00Z", + "versionstamp": "2015-05-22T20:08:18.587Z", + "title": "Blah A", + "source": 1, + "endtz": "Europe/London", + "current": false, + "start": "2014-10-16T08:00:00Z", + "master": 3, + "location": "Room A", + "uid": "90bcdace-b09c-4860-8e86-1a2527931375", + "data": "{\"type\": \"journal club\", \"people\": [\"Dr A\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 4, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-09T09:00:00Z", + "versionstamp": "2015-05-22T20:15:33.343Z", + "title": "Blah A", + "source": 1, + "endtz": "Europe/London", + "current": false, + "start": "2014-10-09T08:00:00Z", + "master": 1, + "location": "Room A", + "uid": "d6560f87-3c7e-49ff-8706-16ba321cb490", + "data": "{\"type\": \"journal club\", \"people\": [\"Dr A\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 5, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-16T09:00:00Z", + "versionstamp": "2015-05-22T20:15:33.367Z", + "title": "Blah A", + "source": 1, + "endtz": "Europe/London", + "current": false, + "start": "2014-10-16T08:00:00Z", + "master": 3, + "location": "Room B", + "uid": "90bcdace-b09c-4860-8e86-1a2527931375", + "data": "{\"type\": \"journal club\", \"people\": [\"Dr A\", \"Mr B and Ms C\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 6, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-09T09:00:00Z", + "versionstamp": "2015-05-22T20:15:45.222Z", + "title": "Blah A", + "source": 1, + "endtz": "Europe/London", + "current": true, + "start": "2014-10-09T08:00:00Z", + "master": 1, + "location": "Room A", + "uid": "d6560f87-3c7e-49ff-8706-16ba321cb490", + "data": "{\"type\": \"journal club\", \"people\": [\"Dr A\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 7, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-16T09:00:00Z", + "versionstamp": "2015-05-22T20:15:45.241Z", + "title": "Blah A", + "source": 1, + "endtz": "Europe/London", + "current": true, + "start": "2014-10-16T08:00:00Z", + "master": 3, + "location": "Room B", + "uid": "90bcdace-b09c-4860-8e86-1a2527931375", + "data": "{\"type\": \"field trip\", \"people\": [\"Dr A\", \"Mr B and Ms C\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 8, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-11-03T13:00:00Z", + "versionstamp": "2015-05-22T20:20:42.584Z", + "title": "Thing B", + "source": 2, + "endtz": "Europe/London", + "current": true, + "start": "2014-11-03T12:00:00Z", + "master": 8, + "location": "Room B", + "uid": "ef6ad702-7a52-45cd-8b18-09ffcff660b9", + "data": "{\"type\": \"practical\", \"people\": [\"Mr ABC\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 9, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2015-02-10T15:46:00Z", + "versionstamp": "2015-05-22T20:24:11.291Z", + "title": "Thing C", + "source": 3, + "endtz": "Europe/London", + "current": true, + "start": "2015-02-10T14:25:00Z", + "master": 9, + "location": "Room C", + "uid": "17e8f1f6-3813-4066-911a-6e6ae4888add", + "data": "{\"type\": \"workshop\", \"people\": [\"abc12\", \"def34\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 10, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-10T12:00:00Z", + "versionstamp": "2015-05-22T20:34:01.717Z", + "title": "D1", + "source": 4, + "endtz": "Europe/London", + "current": false, + "start": "2014-10-10T11:00:00Z", + "master": 10, + "location": "Room D", + "uid": "efd977cd-eb72-42e5-8516-8f6883620f28", + "data": "{\"type\": \"lecture\", \"people\": [\"Prof D\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 11, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-10T12:00:00Z", + "versionstamp": "2015-05-22T20:34:17.279Z", + "title": "D1", + "source": 4, + "endtz": "Europe/London", + "current": false, + "start": "2014-10-10T11:00:00Z", + "master": 10, + "location": "Room D", + "uid": "efd977cd-eb72-42e5-8516-8f6883620f28", + "data": "{\"type\": \"lecture\", \"people\": [\"Prof D\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 12, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2015-01-16T13:00:00Z", + "versionstamp": "2015-05-22T20:34:17.295Z", + "title": "D2", + "source": 4, + "endtz": "Europe/London", + "current": false, + "start": "2015-01-16T12:00:00Z", + "master": 12, + "location": "Room D", + "uid": "77aadf4b-2a54-48f0-9d3b-3818471c95cf", + "data": "{\"type\": \"lecture\", \"people\": [\"Prof D\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 13, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-10T12:00:00Z", + "versionstamp": "2015-05-22T20:34:31.606Z", + "title": "D1", + "source": 4, + "endtz": "Europe/London", + "current": true, + "start": "2014-10-10T11:00:00Z", + "master": 10, + "location": "Room D", + "uid": "efd977cd-eb72-42e5-8516-8f6883620f28", + "data": "{\"type\": \"lecture\", \"people\": [\"Prof D\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 14, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2015-01-16T13:00:00Z", + "versionstamp": "2015-05-22T20:34:31.625Z", + "title": "D2", + "source": 4, + "endtz": "Europe/London", + "current": true, + "start": "2015-01-16T12:00:00Z", + "master": 12, + "location": "Room D", + "uid": "77aadf4b-2a54-48f0-9d3b-3818471c95cf", + "data": "{\"type\": \"lecture\", \"people\": [\"Prof D\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 15, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2015-04-24T12:00:00Z", + "versionstamp": "2015-05-22T20:34:31.640Z", + "title": "D3", + "source": 4, + "endtz": "Europe/London", + "current": true, + "start": "2015-04-24T11:00:00Z", + "master": 15, + "location": "Room D", + "uid": "634053a1-a29c-4ecc-aa42-61c836208857", + "data": "{\"type\": \"lecture\", \"people\": [\"Prof D\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 16, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-09-25T09:00:00Z", + "versionstamp": "2015-05-22T20:40:04.192Z", + "title": "D2 Thing", + "source": 5, + "endtz": "Europe/London", + "current": true, + "start": "2014-09-25T08:00:00Z", + "master": 16, + "location": "Room D", + "uid": "2ee5f91e-9d3d-46fd-8bf6-f54a5bf15498", + "data": "{\"type\": \"presentation\", \"people\": [\"Mr Pink\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 17, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-02T09:00:00Z", + "versionstamp": "2015-05-22T20:40:04.211Z", + "title": "D2 Thing", + "source": 5, + "endtz": "Europe/London", + "current": true, + "start": "2014-10-02T08:00:00Z", + "master": 17, + "location": "Room D", + "uid": "16b69975-be65-41d2-a12c-6c10fd072e27", + "data": "{\"type\": \"presentation\", \"people\": [\"Mr Pink\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 18, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-12-04T10:00:00Z", + "versionstamp": "2015-05-22T20:40:04.228Z", + "title": "D2 Thing", + "source": 5, + "endtz": "Europe/London", + "current": true, + "start": "2014-12-04T09:00:00Z", + "master": 18, + "location": "Room D", + "uid": "2f3761e1-1205-4262-85ae-2e8a22044bc4", + "data": "{\"type\": \"presentation\", \"people\": [\"Mr Pink\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 19, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-12-11T10:00:00Z", + "versionstamp": "2015-05-22T20:40:04.243Z", + "title": "D2 Thing", + "source": 5, + "endtz": "Europe/London", + "current": true, + "start": "2014-12-11T09:00:00Z", + "master": 19, + "location": "Room D", + "uid": "28874f98-8e5b-40a4-a64a-699010af7f70", + "data": "{\"type\": \"presentation\", \"people\": [\"Mr Pink\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 20, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-14T14:00:00Z", + "versionstamp": "2015-05-22T20:43:53.282Z", + "title": "Event E", + "source": 6, + "endtz": "Europe/London", + "current": true, + "start": "2014-10-14T13:00:00Z", + "master": 20, + "location": "Room E", + "uid": "13764e88-afb0-4407-89fb-9924c9d108a9", + "data": "{\"type\": \"class\", \"people\": [\"Ms E\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 21, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-11-07T09:00:00Z", + "versionstamp": "2015-05-22T20:48:50.190Z", + "title": "Event F", + "source": 7, + "endtz": "Europe/London", + "current": true, + "start": "2014-11-07T08:00:00Z", + "master": 21, + "location": "Room F", + "uid": "75367174-0791-4132-89bb-541e19d64ac4", + "data": "{\"type\": \"seminar\", \"people\": [\"Mr F\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 22, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2015-02-09T14:00:00Z", + "versionstamp": "2015-05-22T20:51:09.211Z", + "title": "Event G", + "source": 8, + "endtz": "Europe/London", + "current": true, + "start": "2015-02-09T13:00:00Z", + "master": 22, + "location": "Room G", + "uid": "288e88be-9fa0-46e1-bb94-cbd8ce39dc3d", + "data": "{\"type\": \"seminar\", \"people\": [\"Gee Gson\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 23, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-30T18:00:00Z", + "versionstamp": "2015-05-22T20:52:59.525Z", + "title": "Event H", + "source": 9, + "endtz": "Europe/London", + "current": true, + "start": "2014-10-30T17:00:00Z", + "master": 23, + "location": "Room H", + "uid": "9ae81002-5484-47ee-a54c-fffe5cf5b8c1", + "data": "{\"type\": \"\", \"people\": [\"Dr H\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 24, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-09T09:00:00Z", + "versionstamp": "2015-05-22T21:13:41.890Z", + "title": "Intro to Subject A", + "source": 10, + "endtz": "Europe/London", + "current": true, + "start": "2014-10-09T08:00:00Z", + "master": 24, + "location": "A room", + "uid": "6b4fd2f0-8983-4ca8-ab19-ff7fd1301f37", + "data": "{\"type\": \"workshop\", \"people\": [\"Mr Blue\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 25, + "model": "timetables.event", + "fields": { + "status": 0, + "end": "2014-10-09T09:00:00Z", + "versionstamp": "2015-06-09T20:42:22.051Z", + "title": "Disabled Event", + "source": 11, + "endtz": "Europe/London", + "current": true, + "start": "2014-10-09T08:00:00Z", + "master": null, + "location": "blah", + "uid": "a5078c3b-9d1a-43ee-b4cb-8a8fde013488", + "data": "{\"type\": \"journal club\", \"people\": [\"hi\"]}", + "starttz": "Europe/London" + } +}, +{ + "pk": 1, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 13, + "eventsource": 1, + "annotation": "home" + } +}, +{ + "pk": 2, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 14, + "eventsource": 2, + "annotation": "home" + } +}, +{ + "pk": 3, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 15, + "eventsource": 3, + "annotation": "home" + } +}, +{ + "pk": 4, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 16, + "eventsource": 4, + "annotation": "home" + } +}, +{ + "pk": 5, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 16, + "eventsource": 5, + "annotation": "home" + } +}, +{ + "pk": 6, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 17, + "eventsource": 6, + "annotation": "home" + } +}, +{ + "pk": 7, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 18, + "eventsource": 7, + "annotation": "home" + } +}, +{ + "pk": 8, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 19, + "eventsource": 8, + "annotation": "home" + } +}, +{ + "pk": 9, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 20, + "eventsource": 9, + "annotation": "home" + } +}, +{ + "pk": 10, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 3, + "eventsource": 4, + "annotation": null + } +}, +{ + "pk": 11, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 3, + "eventsource": 8, + "annotation": null + } +}, +{ + "pk": 12, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 3, + "eventsource": 9, + "annotation": null + } +}, +{ + "pk": 13, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 21, + "eventsource": 10, + "annotation": "home" + } +}, +{ + "pk": 14, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 22, + "eventsource": 10, + "annotation": "home" + } +}, +{ + "pk": 15, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 3, + "eventsource": 10, + "annotation": null + } +}, +{ + "pk": 16, + "model": "timetables.eventsourcetag", + "fields": { + "thing": 24, + "eventsource": 11, + "annotation": "home" + } +}, +{ + "pk": 1, + "model": "timetables.thingtag", + "fields": { + "thing": 3, + "targetthing": 6, + "annotation": "admin" + } +}, +{ + "pk": 2, + "model": "timetables.thingtag", + "fields": { + "thing": 3, + "targetthing": 5, + "annotation": "admin" + } +}, +{ + "pk": 3, + "model": "timetables.thingtag", + "fields": { + "thing": 3, + "targetthing": 12, + "annotation": "admin" + } +}, +{ + "pk": 4, + "model": "timetables.thingtag", + "fields": { + "thing": 3, + "targetthing": 11, + "annotation": "admin" + } +}, +{ + "pk": 5, + "model": "timetables.thingtag", + "fields": { + "thing": 3, + "targetthing": 10, + "annotation": "admin" + } +}, +{ + "pk": 6, + "model": "timetables.thingtag", + "fields": { + "thing": 23, + "targetthing": 23, + "annotation": "disabled" + } +}, +{ + "pk": 7, + "model": "timetables.thingtag", + "fields": { + "thing": 3, + "targetthing": 23, + "annotation": "admin" + } +}, +{ + "pk": 8, + "model": "timetables.thingtag", + "fields": { + "thing": 25, + "targetthing": 25, + "annotation": "disabled" + } +}, +{ + "pk": 9, + "model": "timetables.thingtag", + "fields": { + "thing": 26, + "targetthing": 26, + "annotation": "disabled" + } +} +] diff --git a/fixtures/representative/structure-verification/events.csv b/fixtures/representative/structure-verification/events.csv new file mode 100644 index 0000000..18ec4e8 --- /dev/null +++ b/fixtures/representative/structure-verification/events.csv @@ -0,0 +1,19 @@ +"Tripos Id","Tripos Name","Part Id","Part Name","Subpart Id","Subpart Name","Module Id","Module Name","Series ID","Series Name","Event ID","Title","Type","Start","End","Location","People" +"4","Simple Tripos","5","Part I","","","13","Module A","1","Series A","6","Blah A","journal club","2014-10-09T09:00:00+01:00","2014-10-09T10:00:00+01:00","Room A","Dr A" +"4","Simple Tripos","5","Part I","","","13","Module A","1","Series A","7","Blah A","field trip","2014-10-16T09:00:00+01:00","2014-10-16T10:00:00+01:00","Room B","Dr A#Mr B and Ms C" +"4","Simple Tripos","6","Part II","","","14","Module B","2","Series B","8","Thing B","practical","2014-11-03T12:00:00+00:00","2014-11-03T13:00:00+00:00","Room B","Mr ABC" +"4","Simple Tripos","6","Part II","","","15","Module C","3","Series C","9","Thing C","workshop","2015-02-10T14:25:00+00:00","2015-02-10T15:46:00+00:00","Room C","abc12#def34" +"7","Nested Subject","8","Part I","10","Subject A","16","Module D","4","Series D 1","13","D1","lecture","2014-10-10T12:00:00+01:00","2014-10-10T13:00:00+01:00","Room D","Prof D" +"7","Nested Subject","8","Part I","10","Subject A","16","Module D","4","Series D 1","14","D2","lecture","2015-01-16T12:00:00+00:00","2015-01-16T13:00:00+00:00","Room D","Prof D" +"7","Nested Subject","8","Part I","10","Subject A","16","Module D","4","Series D 1","15","D3","lecture","2015-04-24T12:00:00+01:00","2015-04-24T13:00:00+01:00","Room D","Prof D" +"7","Nested Subject","8","Part I","10","Subject A","16","Module D","5","Series D2","16","D2 Thing","presentation","2014-09-25T09:00:00+01:00","2014-09-25T10:00:00+01:00","Room D","Mr Pink" +"7","Nested Subject","8","Part I","10","Subject A","16","Module D","5","Series D2","17","D2 Thing","presentation","2014-10-02T09:00:00+01:00","2014-10-02T10:00:00+01:00","Room D","Mr Pink" +"7","Nested Subject","8","Part I","10","Subject A","16","Module D","5","Series D2","18","D2 Thing","presentation","2014-12-04T09:00:00+00:00","2014-12-04T10:00:00+00:00","Room D","Mr Pink" +"7","Nested Subject","8","Part I","10","Subject A","16","Module D","5","Series D2","19","D2 Thing","presentation","2014-12-11T09:00:00+00:00","2014-12-11T10:00:00+00:00","Room D","Mr Pink" +"7","Nested Subject","8","Part I","11","Subject B","17","Module E","6","Series E","20","Event E","class","2014-10-14T14:00:00+01:00","2014-10-14T15:00:00+01:00","Room E","Ms E" +"7","Nested Subject","8","Part I","11","Subject B","18","Module F","7","Series F","21","Event F","seminar","2014-11-07T08:00:00+00:00","2014-11-07T09:00:00+00:00","Room F","Mr F" +"7","Nested Subject","9","Part II","12","Subject A","19","Module G","8","Series G","22","Event G","seminar","2015-02-09T13:00:00+00:00","2015-02-09T14:00:00+00:00","Room G","Gee Gson" +"7","Nested Subject","9","Part II","12","Subject A","20","Module H","9","Series H","23","Event H","","2014-10-30T17:00:00+00:00","2014-10-30T18:00:00+00:00","Room H","Dr H" +"7","Nested Subject","8","Part I","10","Subject A","21","Common","10","Shared Subject A Series","24","Intro to Subject A","workshop","2014-10-09T09:00:00+01:00","2014-10-09T10:00:00+01:00","A room","Mr Blue" +"7","Nested Subject","9","Part II","12","Subject A","22","Common","10","Shared Subject A Series","24","Intro to Subject A","workshop","2014-10-09T09:00:00+01:00","2014-10-09T10:00:00+01:00","A room","Mr Blue" +"4","Simple Tripos","23","External Part","","","24","Disabled Module","11","Disabled Series","25","Disabled Event","journal club","2014-10-09T09:00:00+01:00","2014-10-09T10:00:00+01:00","blah","hi" diff --git a/fixtures/representative/structure-verification/structure.json b/fixtures/representative/structure-verification/structure.json new file mode 100644 index 0000000..aea3b49 --- /dev/null +++ b/fixtures/representative/structure-verification/structure.json @@ -0,0 +1,102 @@ +{ + "nodes": { + "4": { + "nodes": { + "25": { + "type": "part", + "nodes": {}, + "data": { + "external": "https://www.example.org/2" + }, + "id": 25, + "name": "External (no events with URL)" + }, + "26": { + "type": "part", + "nodes": {}, + "data": null, + "id": 26, + "name": "External (no events no URL)" + }, + "5": { + "type": "part", + "nodes": {}, + "data": null, + "id": 5, + "name": "Part I" + }, + "6": { + "type": "part", + "nodes": {}, + "data": null, + "id": 6, + "name": "Part II" + }, + "23": { + "type": "part", + "nodes": {}, + "data": { + "external": "https://www.example.org/" + }, + "id": 23, + "name": "External Part" + } + }, + "type": "course", + "id": 4, + "name": "Simple Tripos" + }, + "7": { + "nodes": { + "10": { + "nodes": { + "8": { + "type": "part", + "nodes": {}, + "data": null, + "id": 8, + "name": "Part I" + } + }, + "type": "subject", + "id": 10, + "name": "Subject A" + }, + "11": { + "nodes": { + "8": { + "type": "part", + "nodes": {}, + "data": null, + "id": 8, + "name": "Part I" + } + }, + "type": "subject", + "id": 11, + "name": "Subject B" + }, + "12": { + "nodes": { + "9": { + "type": "part", + "nodes": {}, + "data": null, + "id": 9, + "name": "Part II" + } + }, + "type": "subject", + "id": 12, + "name": "Subject A" + } + }, + "type": "course", + "id": 7, + "name": "Nested Subject" + } + }, + "type": "root", + "id": 0, + "name": "Timetable" +} From 72ec4e43047f97b46bf85808871164c77069f530 Mon Sep 17 00:00:00 2001 From: Hal Blackburn Date: Thu, 11 Jun 2015 18:49:28 +0100 Subject: [PATCH 8/8] Fix unhandled exception from exportcsv/grasshopper_export_data Traversal was throwing the wrong type of exception in response to invalid structure. --- app/django/timetables/utils/traversal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/django/timetables/utils/traversal.py b/app/django/timetables/utils/traversal.py index d17939c..fd309a2 100644 --- a/app/django/timetables/utils/traversal.py +++ b/app/django/timetables/utils/traversal.py @@ -97,14 +97,14 @@ def get_parents(self): tags = list(series.eventsourcetag_set.filter(annotation="home")) if(len(tags) == 0): - raise ValidationException( + raise InvalidStructureException( "Orphaned series with no module encountered", series.pk) for tag in tags: module = tag.thing if module.type != "module": - raise ValidationException( + raise InvalidStructureException( "Series attached to non-module thing", series.pk) yield (ModuleTraverser, module)