diff --git a/.htaccess b/.htaccess deleted file mode 100644 index 7a1a452..0000000 --- a/.htaccess +++ /dev/null @@ -1,8 +0,0 @@ -Options +ExecCGI -Indexes -AddHandler cgi-script .py - -RewriteEngine on -RewriteCond %{HTTP_HOST} ^192.168.* -RewriteRule ^your_schedule\.pdf$ /~tdimson/coursequalifier.com/makePDFCalendarCGI.py [L,NC] - -RewriteRule ^your_schedule\.pdf$ makePDFCalendarCGI.py diff --git a/coursequalifier/config/deployment.ini_tmpl b/coursequalifier/config/deployment.ini_tmpl index 5e3f7df..1017892 100644 --- a/coursequalifier/config/deployment.ini_tmpl +++ b/coursequalifier/config/deployment.ini_tmpl @@ -25,6 +25,7 @@ beaker.session.secret = ${app_instance_secret} app_instance_uuid = ${app_instance_uuid} uwdata.address = uwdata.ca uwdata.key = GET_YOUR_OWN +maxSearchSpace = 10000 # If you'd like to fine-tune the individual locations of the cache data dirs # for the Cache data, or the Session saves, un-comment the desired settings diff --git a/coursequalifier/controllers/schedule.py b/coursequalifier/controllers/schedule.py index 98d84ee..8749fd4 100644 --- a/coursequalifier/controllers/schedule.py +++ b/coursequalifier/controllers/schedule.py @@ -4,9 +4,10 @@ import logging from itertools import chain -from pylons import request, response, session, tmpl_context as c +from pylons import request, response, session, tmpl_context as c, config from pylons.controllers.util import abort, redirect_to +from coursequalifier.lib.uwdata import CourseMissingException, UWDataError from coursequalifier.lib.base import BaseController, render from coursequalifier.lib.pdf_schedule import PDFSchedule from coursequalifier.lib.filters import NotFullFilter, \ @@ -31,38 +32,66 @@ def compute_all(self): directCourses = [] for course in r['courses']: query = course["course_query"].strip() - if query == "": - continue + if query == "": continue courseFilters = self._course_filters(course["options"]) m = self.directRE.match(query) if m: - courses = Course.coursesFromCode(m.group(1), m.group(2), term,\ - sectionFilters=requestSectionFilters) + try: + courses = Course.coursesFromCode(m.group(1), m.group(2), term,\ + sectionFilters=requestSectionFilters) + except CourseMissingException, e: + return json.dumps({"error": { + "type": "no_query_results", + "query": query + }}) + except UWDataError, e: + return json.dumps({"error": { + "type": "uwdata", + "query": query + }}) catalogFilters.extend(self._course_catalog_filters(courses, course["options"])) directCourses.extend([e for e in courses\ if all(courseFilter.passes(e) for courseFilter in courseFilters)]) else: courseGroups = [] - for courseGroup in Course.courseGroupsFromSearch(query, term, sectionFilters=requestSectionFilters): - filteredGroup = [e for e in courseGroup if all(courseFilter.passes(e) for courseFilter in courseFilters)] - if len(filteredGroup) > 0: - courseGroups.append(filteredGroup) + try: + for courseGroup in Course.courseGroupsFromSearch(query, term, sectionFilters=requestSectionFilters): + filteredGroup = [e for e in courseGroup if all(courseFilter.passes(e) for courseFilter in courseFilters)] + if len(filteredGroup) > 0: + courseGroups.append(filteredGroup) + except CourseMissingException, e: + return json.dumps({"error": { + "type": "no_query_results", + "query": query + }}) + except UWDataError, e: + return json.dumps({"error": { + "type": "uwdata", + "query": query + }}) catalogFilters.extend(self._course_catalog_filters(chain(*courseGroups), course["options"])) searchGroups.append(courseGroups) - allCourses = directCourses + list(chain(*([chain(*e) for e in searchGroups]))) + allCourses = directCourses + list(chain(*([chain(*e) for e in searchGroups]))) + searchSpaceCount = Catalog.searchSpaceCount(directCourses, searchGroups) + if searchSpaceCount > int(config['maxSearchSpace']): + return json.dumps({"error": { + "type": "large_search_space", + "size": searchSpaceCount + }}) - catalogs = [c for c in Catalog.computeAll(directCourses, searchGroups)\ - if all(catalogFilter.passes(c) for catalogFilter in catalogFilters)] + catalogs, conflicts = Catalog.computeAll(directCourses, searchGroups) + filteredCatalogs= [c for c in catalogs \ + if all(catalogFilter.passes(c) for catalogFilter in catalogFilters)] return json.dumps({"result": { - "courses": dict((e.uniqueName, self._course_dict(e)) for e in allCourses), - "conflicts": {"courses": [], "messages": []}, - "catalogs": [self._catalog_dict(c) for c in catalogs] + "courses": dict((e.uniqueName, self._course_dict(e)) for e in allCourses), + "conflicts": [self._conflict_dict(c) for c in conflicts], + "catalogs": [self._catalog_dict(c) for c in filteredCatalogs] }}) def pdf(self): @@ -125,6 +154,12 @@ def _request_section_filters(self, req): return ret + def _conflict_dict(self, conflict): + s1,s2 = conflict + return [{ "courseName": s.courseName, + "sectionNum": s.sectionNum + } for s in (s1,s2)] + def _course_dict(self, course): return { diff --git a/coursequalifier/lib/filters.py b/coursequalifier/lib/filters.py index 67c619e..9cf4ec2 100644 --- a/coursequalifier/lib/filters.py +++ b/coursequalifier/lib/filters.py @@ -59,7 +59,5 @@ def __init__(self, courseGroup, requiredSectionNumbers=set()): def passes(self, catalog): scopedNums = set(e.sectionNum for e in catalog.sections if e.courseName in self.courseNames) - print scopedNums - print self.requiredSectionNumbers return len(self.requiredSectionNumbers - scopedNums) == 0 diff --git a/coursequalifier/lib/uwdata.py b/coursequalifier/lib/uwdata.py index 824e385..15119e7 100644 --- a/coursequalifier/lib/uwdata.py +++ b/coursequalifier/lib/uwdata.py @@ -24,7 +24,6 @@ def pullSearchCourses(query): ) def getJSONFromPath(basePath, query=[]): - print config['uwdata.address'] connection = HTTPConnection(config['uwdata.address']) path = "%s?key=%s%s" % \ @@ -43,6 +42,8 @@ def getJSONFromPath(basePath, query=[]): text = data['error']['text'] if text == "Unknown course": raise CourseMissingException() + elif text == "No courses found": + raise CourseMissingException() else: raise UWDataError(data['error']['text']) else: diff --git a/coursequalifier/model/catalog.py b/coursequalifier/model/catalog.py index dfec2a4..8632649 100755 --- a/coursequalifier/model/catalog.py +++ b/coursequalifier/model/catalog.py @@ -13,9 +13,10 @@ def cartesian(*args): class CatalogCombinatorics(object): """Algorithm-object for doing actual 'course qualifying'.""" def __init__(self, direct, searchGroups): - self.direct = direct - self.searchGroups = searchGroups - self.output = [] + self.direct = direct + self.searchGroups = searchGroups + self.output = [] + self.conflictingSections = [] def computeSections(self): self.computeInternal(self.direct) @@ -42,24 +43,40 @@ def computeSearchCourses(self): def computeInternal(self, remainingCourses, sectionAcc=[]): if len(remainingCourses) == 0: - if len(sectionAcc) > 0: #and self.checkCatalog(sectionAcc): - self.output.append(sectionAcc) + if len(sectionAcc) > 0: + self.output.append(sectionAcc) #Side effect! else: currentCourse = remainingCourses.pop() for currSection in currentCourse.sections: for otherSection in sectionAcc: if currSection.conflictsWith(otherSection): - #self.conflictingSections.add((currSection, otherSection)) + self.conflictingSections.append((currSection, otherSection)) break else: self.computeInternal(remainingCourses[:], sectionAcc[:] + [currSection]) class Catalog(object): + @classmethod + def searchSpaceCount(cls, directCourses, searchGroups): + if len(directCourses) == 0 and len(searchGroups) == 0: + return 0 + + if len(directCourses) > 0: + numCatalogs = reduce(lambda x,y: x * y, (len(e.sections) \ + for e in directCourses if len(e.sections) > 0)) + else: + numCatalogs = 1 + + for courseOption in searchGroups: + numClasses *= sum(sum(len(c.sections) for c in courseGroup) for courseGroup in searchGroups) + + return numCatalogs + @classmethod def computeAll(cls, directCourses, searchGroups): c = CatalogCombinatorics(directCourses, searchGroups) c.computeSections() - return [cls(e) for e in c.output] + return ([cls(e) for e in c.output], c.conflictingSections) def __init__(self, sections): self.sections = sections diff --git a/coursequalifier/public/css/qualifier.css b/coursequalifier/public/css/qualifier.css index 141a6d2..e808a4e 100755 --- a/coursequalifier/public/css/qualifier.css +++ b/coursequalifier/public/css/qualifier.css @@ -1,7 +1,7 @@ body { text-align:center; - font-family:Arial, Helvetica, sans-serif; - font-size:0.9em; + font-family: Verdana, Arial, sans-serif; + font-size:75%; margin:0; padding:0; background-color:#646890; @@ -69,6 +69,22 @@ html .fb_share_button:hover { color:#a0a0a0; margin-left: 10px; border-color:#29 margin-top: 1em; } +#error_area { + border: 1px solid black; + padding: 10px 12px; + background: #800000; + width: 80%; + color: white; + display: none; +} + +#error_area h3 { + font-size: 18pt; + font-weight: bold; + margin-top: 0; + margin-bottom: 10px; +} + #too_many_explanation { border: 1px solid #c93; padding: 10px 12px; @@ -91,6 +107,10 @@ html .fb_share_button:hover { color:#a0a0a0; margin-left: 10px; border-color:#29 #too_many_number { } +#result_area { + display: none; +} + h2 { color: #0080c3; font-size: 1.5em; @@ -207,7 +227,7 @@ a, a:visited, a:active { } span.noemphasis { - font-size: 0.7em; + font-size: 0.8em; } diff --git a/coursequalifier/public/favicon.ico b/coursequalifier/public/favicon.ico deleted file mode 100644 index 21e215e..0000000 Binary files a/coursequalifier/public/favicon.ico and /dev/null differ diff --git a/coursequalifier/public/js/qualifier.js b/coursequalifier/public/js/qualifier.js index cfe38ef..a4e21bf 100755 --- a/coursequalifier/public/js/qualifier.js +++ b/coursequalifier/public/js/qualifier.js @@ -296,15 +296,12 @@ function getCoursesObject() { var courses = []; var course_nodes = Dom.getElementsByClassName( "course", null, course_form ); - for( var i = 0; i < course_nodes.length; i++ ) - { + for(var i = 0; i < course_nodes.length; i++) { courseObject = serializeFromRoot( course_nodes[i], true ); optionsSpan = Dom.getChildrenBy( course_nodes[i], function(e) { return e.className == "options"; } )[0] courseObject["options"] = serializeFromRoot( optionsSpan, false ); - courses.push( courseObject ); } - return courses; } @@ -345,11 +342,9 @@ function serializeFromRoot(root, first_level) { var select = selectElements[i]; var selectChildren = select.getElementsByTagName( "option" ); - for( var j =0;j < selectChildren.length; j++ ) - { + for(var j =0;j < selectChildren.length; j++) { var option = selectChildren[j]; - if( option.selected ) - { + if(option.selected) { options[ select.name ] = option.value; } } @@ -373,18 +368,16 @@ function submitCourses(e) { "courses": getCoursesObject() }; - document.getElementById("error_area").innerHTML = ""; - document.getElementById("info_area").innerHTML = ""; + document.getElementById("error_area").style.display = "none"; + document.getElementById("trace_area").style.display = "none"; + document.getElementById("result_area").style.display = "none"; document.getElementById("too_many_explanation").style.display = "none"; - document.getElementById( "row_select" ).style.display = "none"; - document.getElementById( "show_conflicts" ).innerHTML = ""; - document.getElementById( "show_conflicts_number" ).innerHTML = ""; + document.getElementById("row_select").style.display = "none"; + document.getElementById("show_conflicts").innerHTML = ""; + document.getElementById("show_conflicts_number").innerHTML = ""; showLoadingDialog(); hideConflicts(); - hideCalendar(); - hideCatalogInformation(); - hideQualifierGrid(); var pdfDiv = document.getElementById("create_pdf_div"); pdfDiv.style.display = "none"; qualifyRequest = YAHOO.util.Connect.asyncRequest('POST', "/schedule/compute_all", @@ -403,23 +396,28 @@ function displayInfo(infoString) { function displayErrorHTML(errorHTML) { var errorArea = document.getElementById( "error_area" ); errorArea.innerHTML = errorHTML; + errorArea.style.display = "block"; } function displayError(errorString) { var errorArea = document.getElementById( "error_area" ); errorArea.innerHTML = "Errors:
" + errorString.replace(/ /g, " " ).replace( /\n/g, "
" ); + errorArea.style.display = "block"; } -function handleQualifierException(exception) { - if(exception.name == "TooManySchedulesException") { - var tooManyArea = document.getElementById( "too_many_explanation" ); - var tooManyNumber = document.getElementById( 'too_many_number' ); - - tooManyNumber.innerHTML = exception.numClasses; +function handleQualifierError(error) { + if(error.type == "large_search_space") { + var tooManyArea = document.getElementById("too_many_explanation"); + var tooManyNumber = document.getElementById('too_many_number'); + tooManyNumber.innerHTML = error.size; tooManyArea.style.display = "block"; - } - else { - displayError( exception.string ); + } else if(error.type == "no_query_results") { + displayErrorHTML("

No Results

Your query of '" + error.query + "' returned no results."); + } else if(error.type == "uwdata") { + displayErrorHTML("

Backend Error

An error occurred in data backend, uwdata.ca, for query '" + error.query + "'." + + " This could be because uwdata.ca is down or because it has a bug."); + } else { + displayError(YAHOO.lang.dump(error)); } } @@ -435,43 +433,29 @@ function qualifyCallback(response) { } if(data.error != undefined) { - displayError( data.error ); - } - - if(data.exception != undefined) { - handleQualifierException( data.exception ); + handleQualifierError(data.error); + return; } if(data.error == undefined && data.exception == undefined) { - var info = data.info; - info = ""; // FIXME toggleConflicts.conflicts = data.result.conflicts; - if( data.result.conflicts.courses.length > 0 ) { + if(data.result.conflicts.length > 0 ) { var showConflictsText = document.getElementById( "show_conflicts" ); var showConflictsNumber = document.getElementById( "show_conflicts_number" ); showConflictsText.innerHTML = "Show conflicting courses"; - showConflictsNumber.innerHTML = "(" + data.result.conflicts.courses.length + ")"; + showConflictsNumber.innerHTML = "(" + data.result.conflicts.length + ")"; } - - if( info != "" ) { - displayInfo( info ); - } - if(data.result.catalogs.length == 0) { displayError( "No valid schedules found" ); } else { createQualifierGrid(data.result ); selectFirstRow(); + document.getElementById("result_area").style.display = "block"; } } } -function hideQualifierGrid() { - var qualifierGridLocation = document.getElementById("qualifier_grid"); - qualifierGridLocation.innerHTML = ""; -} - function selectPreviousRow(e) { YAHOO.util.Event.preventDefault(e); selected = qualifierTable.getSelectedTrEls()[0]; @@ -598,11 +582,6 @@ function rowSelected(e, scrollToGrid ) { } } -function hideCatalogInformation() { - var informationLocation = document.getElementById("catalog_information"); - informationLocation.innerHTML = ""; -} - function createPDF(e) { YAHOO.util.Event.preventDefault(e); var request = { @@ -618,9 +597,7 @@ function createPDF(e) { function createCatalogInformation(rowData, sections, responseData) { var i; var informationLocation = document.getElementById("catalog_information"); - - - hideCatalogInformation(); + informationLocation.innerHTML = ""; var pdfDiv = document.getElementById("create_pdf_div"); pdfDiv.style.display = "block"; @@ -665,15 +642,9 @@ function createCatalogInformation(rowData, sections, responseData) { informationLocation.appendChild( document.createElement( "br" ) ); } -function hideCalendar() { - var calendarLocation = document.getElementById("qualifier_calendar"); - calendarLocation.innerHTML = ""; -} - function createCalendar(courseData, sections) { var calendarLocation = document.getElementById("qualifier_calendar"); - - hideCalendar(); + calendarLocation.innerHTML = ""; var validDays = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ]; var stepSize = 30*60; @@ -785,8 +756,7 @@ function hideConflicts() { var conflictDiv = document.getElementById("conflict_area"); var showConflictsText = document.getElementById( "show_conflicts" ); - if( toggleConflicts.showing ) - { + if(toggleConflicts.showing) { conflictDiv.innerHTML = ""; showConflictsText.innerHTML = "Show conflicting courses"; toggleConflicts.showing = false; @@ -800,9 +770,7 @@ function toggleConflicts(e) { if(toggleConflicts.conflicts == undefined) { return; } - var conflictingCourses = toggleConflicts.conflicts.courses; - var conflictingMessages = toggleConflicts.conflicts.messages; - + var conflictingCourses = toggleConflicts.conflicts; var conflictDiv = document.getElementById("conflict_area"); var showConflictsText = document.getElementById( "show_conflicts" ); @@ -815,21 +783,12 @@ function toggleConflicts(e) { var course1 = conflictingCourses[i][0]; var course2 = conflictingCourses[i][1]; - var newText = document.createTextNode( course1.courseName + " (" + course1.sectionName + ")" - + " conflicts with " + course2.courseName + " (" + course2.sectionName + ")" ); + var newText = document.createTextNode( course1.courseName + " (" + course1.sectionNum + ")" + + " conflicts with " + course2.courseName + " (" + course2.sectionNum + ")" ); conflictDiv.appendChild( newText ); conflictDiv.appendChild( document.createElement( "br" ) ); } - for(i = 0; i< conflictingMessages.length; i++) { - var message = conflictingMessages[i]; - - var newText = document.createTextNode( message ); - - conflictDiv.appendChild( newText ); - conflictDiv.appendChild( document.createElement( "br" ) ); - } - showConflictsText.innerHTML = "Hide conflicting courses"; toggleConflicts.showing = true; } @@ -838,7 +797,9 @@ function toggleConflicts(e) { function qualifyErrorCallback(response) { hideLoadingDialog(); if(response.responseText && response.responseText != "") { - displayErrorHTML(response.responseText); + var traceArea = document.getElementById("trace_area"); + traceArea.innerHTML = response.responseText; + traceArea.style.display = "block"; } else { displayError( "Error communicating with server"); } diff --git a/coursequalifier/templates/welcome.mako b/coursequalifier/templates/welcome.mako index 6d9ee14..e53444c 100644 --- a/coursequalifier/templates/welcome.mako +++ b/coursequalifier/templates/welcome.mako @@ -41,10 +41,10 @@

Waterloo Course Qualifier

Better than Quest

- The Course Qualifier helps you generate all possible course timetables without time conflicts. You can choose a path that allows you to make the most efficient use of your time. + The Course Qualifier helps you generate all possible course timetables without time conflicts. You can choose a path that allows you to make the most efficient use of your time.

- Source now available on github + Source now available on github. Project issues on Feds SDN.

Facebook, @@ -146,6 +146,9 @@

+
+
+
@@ -159,42 +162,44 @@
-

Select a course sequence:

- -
-
+
+

Select a course sequence:

+ +
+
-
- Awaiting course selection... -
+
+ Awaiting course selection... +
- -

Course timetable / details:

- -
- Awaiting row selection... -
+ +

Course timetable / details:

+ +
+ Awaiting row selection... +
-
@@ -203,18 +208,21 @@

How to use Course Qualifier


- Relatively simple: + Simple:
  1. Pick some courses you might wish to take from the Undergraduate Course Calendar
  2. -
  3. From the course qualifier, pick your term (Winter 2010, etc.)
  4. +
  5. From the course qualifier, pick your term (Winter 2011, etc.)
  6. Enter your course subject followed by course number (ex. AMATH 250) -
  7. Toggle tutorial slots, other slots (ex. Discussions)
  8. Press Make it so and sort by a desired column
  9. Highlight the row of your course sequence and click. Mysteriously, a possible schedule appears!
+

+ Want more control? Toggle Advanced Options! +

+

Too many results? Filter some out!
@@ -226,14 +234,12 @@ Statisfied with your schedule? Make a PDF out of it by clicking "PDF Version".

-

- What are others taking? View the statistics! -

About Course Qualifier

-

The course qualifier is a quick longish moderately filthy Python script. Originally, it was a bit of a hack but it has been maintained very passively since April 2007 since people seem to like it.

+

The course qualifier is a quick longish Python service using the Pylons framework. + Originally, it was a bit of a hack but it has been maintained very passively since April 2007 since people seem to like it.

The table fields are as follows: