diff --git a/bin/yacs-purge-development b/bin/yacs-purge-development index aa171783..1d41fbdf 100755 --- a/bin/yacs-purge-development +++ b/bin/yacs-purge-development @@ -6,5 +6,5 @@ bin/yacs-compose-development stop kafka zookeeper redis core-consumer bin/yacs-compose-development rm -vf kafka bin/yacs-compose-development rm -vf zookeeper bin/yacs-compose-development rm -vf redis -echo "Term.create(shortname: \"201809\", longname: \"Fall 2018\")" | bin/yacs-compose-development run --rm core rails c +echo "Term.create(shortname: \"201909\", longname: \"Fall 2019\")" | bin/yacs-compose-development run --rm core rails c # echo "Term.create(shortname: \"201901\", longname: \"Spring 2019\")" | bin/yacs-compose-development run --rm core rails c diff --git a/core/app/consumers/application_consumer.rb b/core/app/consumers/application_consumer.rb index 77a54a88..5dd3f147 100644 --- a/core/app/consumers/application_consumer.rb +++ b/core/app/consumers/application_consumer.rb @@ -15,8 +15,8 @@ class ApplicationConsumer < Karafka::BaseConsumer subject: %i(shortname longname uuid school_uuid), listing: %i(shortname longname description min_credits max_credits uuid subject_uuid subject_shortname) << { required_textbooks: [], recommended_textbooks: [], tags: [] }, - section: %i(shortname crn seats seats_taken uuid listing_uuid instructors) << - { periods: %i(day start end type location) } + section: %i(shortname crn seats seats_taken uuid listing_uuid) << + { instructors: [], periods: %i(day start end type location) } }.with_indifferent_access.freeze include Karafka::Consumers::Callbacks diff --git a/core/db/migrate/20190326210039_alt_seats_and_seatstaken_to_nullable.rb b/core/db/migrate/20190326210039_alt_seats_and_seatstaken_to_nullable.rb new file mode 100644 index 00000000..0e0731e3 --- /dev/null +++ b/core/db/migrate/20190326210039_alt_seats_and_seatstaken_to_nullable.rb @@ -0,0 +1,12 @@ +class AltSeatsAndSeatstakenToNullable < ActiveRecord::Migration[5.1] + def change + change_column_null :sections, :seats, true + change_column_default :sections, :seats, nil + change_column_null :sections, :seats_taken, true + change_column_default :sections, :seats_taken, nil + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/core/db/schema.rb b/core/db/schema.rb index 227eb81c..56727602 100644 --- a/core/db/schema.rb +++ b/core/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20181031001113) do +ActiveRecord::Schema.define(version: 20190326210039) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -66,8 +66,8 @@ create_table "sections", id: :serial, force: :cascade do |t| t.string "shortname", null: false t.string "crn", null: false - t.integer "seats", null: false - t.integer "seats_taken", null: false + t.integer "seats" + t.integer "seats_taken" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "conflict_ids", default: [], null: false, array: true diff --git a/core/db/structure.sql b/core/db/structure.sql index 0a3210b2..ba393d84 100644 --- a/core/db/structure.sql +++ b/core/db/structure.sql @@ -2,17 +2,17 @@ * * This purpose of this file is to house any custom plpgsql functions * or other "raw" postgresql code that is needed to configure the database. - * + * * Any changes or additions to such code should be handled via migration, * using `20171210212207_add_conflict_ids_procedure.rb` as an example. - * + * * This file is needed because although it is possible to execute "raw" * sql code in a migration, this is not reflected in the generated schema.rb, * which causes problems with testing and deployment. - * + * * As such, this file should contain any and all "raw" sql code necessary * to configure the database to the state of the **latest** migration. - * + * * If you are getting odd test failures that could be related to such a * misconfiguration, check to make sure any "raw" migrations are reflected * here as well. @@ -36,30 +36,35 @@ DECLARE conflict_found BOOLEAN; BEGIN SELECT terms.id INTO this_term_id FROM terms - INNER JOIN listings ON listings.term_id = terms.id - INNER JOIN sections ON sections.listing_id = listings.id + LEFT OUTER JOIN listings ON listings.term_id = terms.id + LEFT OUTER JOIN sections ON sections.listing_id = listings.id WHERE sections.id = section_id; SELECT * INTO this_section FROM sections WHERE sections.id = section_id; + <> FOR other_section IN SELECT * FROM sections - INNER JOIN listings ON listings.id = sections.listing_id - INNER JOIN terms ON terms.id = listings.term_id + LEFT OUTER JOIN listings ON listings.id = sections.listing_id + LEFT OUTER JOIN terms ON terms.id = listings.term_id WHERE sections.listing_id != this_section.listing_id AND terms.id = this_term_id LOOP conflict_found := 'false'; + <> FOR this_section_period IN SELECT * FROM jsonb_array_elements(this_section.periods) LOOP + <> FOR other_section_period IN SELECT * FROM jsonb_array_elements(other_section.periods) LOOP - IF (other_section_period->'day' = other_section_period->'day' - AND ((this_section_period->'start' <= other_section_period->'state' AND this_section_period->'end' > other_section_period->'start') - OR (this_section_period->'start' >= other_section_period->'start' AND this_section_period->'start' < other_section_period->'end'))) + IF (this_section_period->>'day' = other_section_period->>'day' + AND (((this_section_period->>'start')::NUMERIC <= (other_section_period->>'start')::NUMERIC AND (this_section_period->>'end')::NUMERIC >= (other_section_period->>'start')::NUMERIC) + OR ((this_section_period->>'start')::NUMERIC >= (other_section_period->>'start')::NUMERIC AND (this_section_period->>'start')::NUMERIC <= (other_section_period->>'end')::NUMERIC))) THEN conflict_ids := conflict_ids || other_section.id; conflict_found := 'true'; END IF; - END LOOP; - END LOOP; - END LOOP; + EXIT inner_period_loop WHEN conflict_found IS TRUE; + END LOOP inner_period_loop; + EXIT outer_period_loop WHEN conflict_found IS TRUE; + END LOOP outer_period_loop; + END LOOP section_loop; RETURN conflict_ids; END; $$ LANGUAGE plpgsql; diff --git a/docker-compose.development.yml b/docker-compose.development.yml index 9fc291a9..5afc58b4 100644 --- a/docker-compose.development.yml +++ b/docker-compose.development.yml @@ -25,7 +25,7 @@ services: build: ./core environment: - RAILS_ENV=development - - TERM_SHORTNAME=201809 + - TERM_SHORTNAME=201909 - UNI_SHORTNAME volumes: - ./core:/usr/src/app @@ -47,7 +47,7 @@ services: volumes: - "./malg:/usr/src/app" environment: - - TERM_SHORTNAME=201809 + - TERM_SHORTNAME=201909 - UNI_SHORTNAME notifications: diff --git a/pipelines/rpi/adapters/acalog-rpi/acalog_client.rb b/pipelines/rpi/adapters/acalog-rpi/acalog_client.rb index 378d119f..76a200d2 100644 --- a/pipelines/rpi/adapters/acalog-rpi/acalog_client.rb +++ b/pipelines/rpi/adapters/acalog-rpi/acalog_client.rb @@ -74,13 +74,17 @@ def current_catalog_id def catalog_id_for term_shortname year, month = /(\d{4})(\d{2})/.match(term_shortname).captures.map &:to_i + catalogs_xml = request('content', method: :getCatalogs) year -= 1 if month < 9 - title = "Rensselaer Catalog #{year}-#{year + 1}" - node = request('content', method: :getCatalogs). - xpath("//catalog[title[contains(text(),\"#{title}\")]]/@id") + node = catalogs_xml.xpath("//catalog[title[contains(text(),\"#{title_for(year)}\")]]/@id") + node = catalogs_xml.xpath("//catalog[title[contains(text(),\"#{title_for(year - 1)}\")]]/@id") if node.empty? @catalog_id = /acalog-catalog-(?\d+)/.match(node.text)[:id].to_i end + def title_for year + "Rensselaer Catalog #{year}-#{year + 1}" + end + def request path, params params = params.merge({ key: @api_key, format: :xml }) uri = "#{@api_url}/v1/#{path}?#{params.to_query}" diff --git a/web/src/app/app.module.ts b/web/src/app/app.module.ts index 474488f1..442734c5 100644 --- a/web/src/app/app.module.ts +++ b/web/src/app/app.module.ts @@ -13,6 +13,7 @@ import { NavUserComponent } from './header/nav-user/component'; import { NoticeBarComponent } from './notice-bar/component'; import { FooterComponent } from './footer/component'; import { AboutComponent } from './about/component'; +import { TermSelectorComponent } from './header/term-selector/component'; import { SidebarModule } from './sidebar/module'; import { SchoolViewModule } from './school-view/module'; @@ -25,10 +26,10 @@ import { ConflictsService } from './services/conflicts.service'; import { NoticeService } from './services/notice.service'; import { UserService } from './services/user.service'; import { ColorService } from './services/color.service'; +import { SelectedTermService } from './services/selected-term.service'; import { SidebarService } from './services/sidebar.service'; - @NgModule({ imports: [ BrowserModule, @@ -50,7 +51,8 @@ import { SidebarService } from './services/sidebar.service'; NoticeBarComponent, FooterComponent, AboutComponent, - NavUserComponent + NavUserComponent, + TermSelectorComponent ], providers: [ SelectionService, @@ -58,6 +60,7 @@ import { SidebarService } from './services/sidebar.service'; NoticeService, UserService, ColorService, + SelectedTermService, SidebarService ], bootstrap: [AppComponent] diff --git a/web/src/app/footer/component.html b/web/src/app/footer/component.html index b77b5ca6..51b2720f 100644 --- a/web/src/app/footer/component.html +++ b/web/src/app/footer/component.html @@ -5,7 +5,7 @@ Learn more about YACS. diff --git a/web/src/app/header/term-selector/component.html b/web/src/app/header/term-selector/component.html new file mode 100644 index 00000000..a44257e8 --- /dev/null +++ b/web/src/app/header/term-selector/component.html @@ -0,0 +1,9 @@ + +   + + {{ internalName }} + View Only +   + + + diff --git a/web/src/app/header/term-selector/component.scss b/web/src/app/header/term-selector/component.scss new file mode 100644 index 00000000..e140b5c1 --- /dev/null +++ b/web/src/app/header/term-selector/component.scss @@ -0,0 +1,24 @@ + +.arrow { + border: solid white; + border-width: 0 3px 3px 0; + display: inline-block; + padding: 3px; + cursor: pointer; +} + +.arrow_disabled { + border: solid grey; + border-width: 0 3px 3px 0; + cursor: not-allowed; +} + +.right { + transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); +} + +.left { + transform: rotate(135deg); + -webkit-transform: rotate(135deg); +} diff --git a/web/src/app/header/term-selector/component.spec.ts b/web/src/app/header/term-selector/component.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/web/src/app/header/term-selector/component.ts b/web/src/app/header/term-selector/component.ts new file mode 100644 index 00000000..995322cb --- /dev/null +++ b/web/src/app/header/term-selector/component.ts @@ -0,0 +1,58 @@ +import {Component, OnInit} from '@angular/core'; +import {SelectedTermService} from '../../services/selected-term.service'; +import {Term} from 'yacs-api-client'; + +@Component({ + selector: 'term-selector', + templateUrl: './component.html', + styleUrls: ['./component.scss'] +}) +export class TermSelectorComponent implements OnInit { + + // cache the name of the term + private internalName: string; + + constructor( + private selectedTermService: SelectedTermService) { + // default the name to 'loading' until it loads + this.internalName = 'loading'; + } + + ngOnInit(): void { + this.selectedTermService.subscribeToTerm((term: Term) => { + this.internalName = term.longname; + }); + } + + get isFirstTerm(): boolean { + return this.selectedTermService.getCurrentOrdinal === 0; + } + + get isLastTerm(): boolean { + return this.selectedTermService.getCurrentOrdinal === this.selectedTermService.getMaximumOrdinal; + } + + get isActiveTerm(): boolean { + return this.selectedTermService.isCurrentTermActive; + } + + /** + * Move to the previous (more recent) Term + */ + previousTerm() { + const ord = this.selectedTermService.getCurrentOrdinal; + if (ord > 0) { + this.selectedTermService.setSelectedTermByOrdinal(ord - 1); + } + } + + /** + * Move to the next (less recent) Term + */ + nextTerm() { + const ord = this.selectedTermService.getCurrentOrdinal; + if (ord < this.selectedTermService.getMaximumOrdinal) { + this.selectedTermService.setSelectedTermByOrdinal(ord + 1); + } + } +} diff --git a/web/src/app/listing-view/component.ts b/web/src/app/listing-view/component.ts index 2b195dbf..6eb541fa 100644 --- a/web/src/app/listing-view/component.ts +++ b/web/src/app/listing-view/component.ts @@ -1,27 +1,34 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, OnDestroy, OnInit} from '@angular/core'; import { Router, ActivatedRoute, Params } from '@angular/router'; import { Term, Course, Listing } from 'yacs-api-client'; import { YacsService } from '../services/yacs.service'; import { ConflictsService } from '../services/conflicts.service'; +import { SelectedTermService } from '../services/selected-term.service'; +import { Subscription } from 'rxjs'; @Component({ selector: 'listing-view', templateUrl: './component.html', styleUrls: ['./component.scss'] }) -export class ListingViewComponent implements OnInit { +export class ListingViewComponent implements OnInit, OnDestroy { listings: Listing[] = []; isLoaded: boolean = false; + cachedTermId: string; + cachedQuery: Params; + termSubscription: Subscription; constructor ( private yacsService : YacsService, private activatedRoute: ActivatedRoute, - private conflictsService: ConflictsService) { } + private conflictsService: ConflictsService, + private selectedTermService: SelectedTermService) { + this.cachedTermId = this.selectedTermService.getCurrentTermId; + } - async getCourses (params: Params): Promise { + async getCourses (params: Params, termId: string): Promise { this.isLoaded = false; - const term = await Term.first(); - const query = Object.assign({ term_id: term.data.id }, params); + const query = Object.assign({ term_id: termId }, params); Listing .where(query) .includes('sections') @@ -38,7 +45,24 @@ export class ListingViewComponent implements OnInit { ngOnInit (): void { this.activatedRoute.queryParams.subscribe((params: Params) => { - this.getCourses(params); + this.cachedQuery = params; + // only operate if the selected term has been determined (prevents race condition) + if (this.cachedTermId !== undefined) { + this.getCourses(this.cachedQuery, this.cachedTermId); + } + }); + this.termSubscription = this.selectedTermService.subscribeToTerm((term: Term) => { + this.cachedTermId = term.id; + // only operate if the query has been determined (prevents race condition) + if (this.cachedQuery !== undefined) { + this.getCourses(this.cachedQuery, this.cachedTermId); + } }); + + } + + ngOnDestroy(): void { + // unsubscribe from the term subscription when this listing is destroyed to prevent leaking + this.termSubscription.unsubscribe(); } } diff --git a/web/src/app/listing/component.html b/web/src/app/listing/component.html index 21286c50..17aca63a 100644 --- a/web/src/app/listing/component.html +++ b/web/src/app/listing/component.html @@ -4,9 +4,9 @@
- +
-
{{listing.longname}} +
{{listing.longname}}
@@ -15,6 +15,15 @@
{{creditRange}}
+ +
 
+ +
Conflicting Sections
+
Conflicting Sections
+
Full Sections
+
Full Sections
+
+
diff --git a/web/src/app/listing/component.scss b/web/src/app/listing/component.scss index 8008586b..849f6394 100644 --- a/web/src/app/listing/component.scss +++ b/web/src/app/listing/component.scss @@ -26,7 +26,7 @@ margin: 10px 0px 10px 0px; } .fixed-height { - height: 7em; + height: 8.5em; } .course-description { @@ -123,7 +123,7 @@ i { } .down-arrow:hover { - animation: shake 0.3s; + animation: shake 0.3s; transform: translate3d(0, 0, 0); backface-visibility: hidden; perspective: 1000px; @@ -166,12 +166,14 @@ i { font-size:95%; text-align: justify; color: #969696; - margin-top: -3.2em; + margin-top: -4.6em; margin-left: 8em; margin-right: 2em; line-height: 1.7em; overflow: hidden; height: 3.5em; + position: relative; + z-index: 10; } .hideDescription:hover { @@ -183,13 +185,15 @@ i { display:flex; font-size:95%; text-align: justify; - margin-top: -3.2em; + margin-top: -3.6em; margin-left: 8em; margin-right: 2em; line-height: 1.7em; bottom: 1em; height: auto; min-height: 3.5em; + position: relative; + z-index: 10; } .showDescription:hover { @@ -227,7 +231,7 @@ section.conflicts, section.conflicts:hover { } section.closed, section.closed:hover { - color: #888; + color: #4a3c3c; } .indicator { @@ -277,3 +281,38 @@ section.closed, section.closed:hover { transform: translate3d(2px, 0, 0); } } + +.warning { + position: absolute; + height: auto; + max-width: 175px; + opacity: 0.25; + margin-left: -15px; + margin-top: -5px; + z-index: 1; +} + +table { + position: relative; +} + +.closed-conflict { + float: left; + color: white; + font-size: 80%; + text-align: center; + padding-left: 8px; + padding-right: 8px; + margin-top: 5px; + margin-right: 10px +} + +.some { + border: 1px solid #E5C201; + background-color: #E5C201; +} + +.all { + border: 1px solid #C65353; + background-color: #C65353; +} diff --git a/web/src/app/listing/component.ts b/web/src/app/listing/component.ts index c8153af7..a8b32088 100644 --- a/web/src/app/listing/component.ts +++ b/web/src/app/listing/component.ts @@ -27,7 +27,7 @@ export class ListingComponent implements OnInit{ public showingMenu; public showingDescription; public hovered; - + ngOnInit () { this.showingMenu = false; this.showingDescription = false; @@ -75,7 +75,29 @@ export class ListingComponent implements OnInit{ } public get tooltipDescription (): string { - return this.listing.description || 'No description available :('; + return this.listing.description || 'No description available :('; + } + + public getSectionClosedCount (): number { + let closedCount = 0; + this.listing.sections.forEach((s) => { + if (s.seatsTaken >= s.seats) { + closedCount += 1; + } + }); + return closedCount; + + } + + public getSectionConflictCount (): number { + let conflictCount = 0; + this.listing.sections.forEach((s) => { + if (this.doesConflict(s)) { + conflictCount += 1; + } + }); + return conflictCount; + } public removeButtonClick (): void { diff --git a/web/src/app/school-list/component.scss b/web/src/app/school-list/component.scss index 1819d51e..2b7a22ee 100644 --- a/web/src/app/school-list/component.scss +++ b/web/src/app/school-list/component.scss @@ -1,4 +1,6 @@ school { display: inline-block; width: 100%; + box-shadow: 1.5px 1.5px 4px 1px rgba(151, 153, 155, 0.2); + margin-bottom: 12px; } \ No newline at end of file diff --git a/web/src/app/school-list/school/component.scss b/web/src/app/school-list/school/component.scss index e775c489..ab1af106 100644 --- a/web/src/app/school-list/school/component.scss +++ b/web/src/app/school-list/school/component.scss @@ -1,11 +1,12 @@ .school-longname { display:block; /*width:600px;*/ - margin-right:10px; - font-size:21px; + /*margin-right:10px;*/ + font-size:22px; font-style:italic; - padding: 0px 1px; - border-bottom: 1px black solid; + padding: 2px 7px; + border-bottom: 1px rgba(108, 90, 90, 0.1) solid; + background-color: rgba(108, 90, 90, 0.15); } .subject-list { @@ -17,7 +18,7 @@ subject { display:block; cursor:pointer; - padding:2px 5px; + padding:2px 12px; /*width:5;*/ } subject:hover { diff --git a/web/src/app/services/selected-term.service.ts b/web/src/app/services/selected-term.service.ts new file mode 100644 index 00000000..287d5e82 --- /dev/null +++ b/web/src/app/services/selected-term.service.ts @@ -0,0 +1,145 @@ +import {Injectable} from '@angular/core'; +import {Term} from 'yacs-api-client'; +import {Subject, Subscription} from 'rxjs'; + +@Injectable() +export class SelectedTermService { + // a subject that can be written to and subscribed to for the selected term + public selectedTerm: Subject; + public activeTerm: Subject; + // hold the terms in two forms: a mapping between the graphiti id and the term, and an array for ordering + protected terms: Map; + // cache the current ordinal and graphiti ID + protected currentOrdinal: number; + protected currentId: string; + protected activeId: string; + + constructor() { + this.terms = new Map(); + this.selectedTerm = new Subject(); + this.activeTerm = new Subject(); + this.currentOrdinal = 0; + // acquire all the terms upon load + Term.all().then(terms => { + terms.data.forEach(term => { + // place the terms in the map and the array + this.terms.set(term.id, term); + }); + if (localStorage['atFirstTerm'] === 'true') { + // if the local storage term was the first term (the most recent) + // set the selected term to the most recent instead of restoring + // the last setting, which may unnecessarily obfuscate the most + // recent term + this.setSelectedTermByOrdinal(this.terms.size - 1); + } else { + // otherwise restore the selectedTerm from localStorage, verifying its validity + // and defaulting to the most recent + if (localStorage['selectedTerm'] !== undefined) { + const localTerm = this.terms.get(localStorage['selectedTerm']); + if (localTerm === undefined) { + this.setSelectedTermByOrdinal(this.terms.size - 1); + } else { + this.currentId = localTerm.id; + this.setSelectedTerm(localTerm.id); + } + } else { + this.setSelectedTermByOrdinal(this.terms.size - 1); + } + } + if (localStorage['activeTerm'] !== undefined) { + this.activeId = localStorage['activeTerm']; + // do not push to Subject because it will cause a clear + // in Selection service -- the only purpose of the subject in its current + // state is to do that anyway. + } else { + this.activeId = Array.from(this.terms.keys()).pop(); + } + console.log(this.activeId); + }); + // internal subscription to term for localstorage + this.subscribeToTerm((term: Term) => { + this.currentId = term.id; + localStorage.setItem('selectedTerm', term.id); + localStorage.setItem('atFirstTerm', `${this.currentOrdinal === this.terms.size - 1}`); + }); + this.subscribeToActiveTerm((term: Term) => { + this.activeId = this.currentId; + if (this.currentOrdinal === this.terms.size - 1) { + localStorage.removeItem('activeTerm'); + } else { + localStorage.setItem('activeTerm', term.id); + } + }); + } + + /** + * Set the selected term by Graphiti ID + * @param id + * @return The success of setting the term (returns false if the ID was invalid) + */ + public setSelectedTerm(id: string): boolean { + const nTerm = this.terms.get(id); + if (nTerm !== undefined) { + this.currentOrdinal = Array.from(this.terms.values()).findIndex((term) => term.id === id); + this.selectedTerm.next(nTerm); + return true; + } + return false; + } + + /** + * Sets the selected term by its ordinal + * @param newOrdinal + * @return The success of setting the term (returns false if the ordinal was out of bounds) + */ + public setSelectedTermByOrdinal(newOrdinal: number): boolean { + if (newOrdinal >= 0 && newOrdinal < this.terms.size) { + this.currentOrdinal = newOrdinal; + this.selectedTerm.next(Array.from(this.terms.values())[newOrdinal]); + return true; + } + return false; + } + + /** + * Attaches a subscriber to the selectedTerm, piping errors to console.error. + * @param func + */ + public subscribeToTerm(func: (Term) => void): Subscription { + return this.selectedTerm.subscribe(func, (e) => { console.error(e); }, () => {}); + } + + /** + * Attaches a subscriber to the activeTerm, piping errors to console.error. + * @param func + */ + public subscribeToActiveTerm(func: (Term) => void): Subscription { + return this.activeTerm.subscribe(func, (e) => { console.error(e); }, () => {}); + } + + public setSelectedTermAsActive(): void { + this.activeTerm.next(this.terms.get(this.currentId)); + } + + public get getCurrentOrdinal(): number { + return this.currentOrdinal; + } + + public get getMaximumOrdinal(): number { + return this.terms.size - 1; + } + + public get getCurrentTermId(): string { + return this.currentId; + } + + // for the future perhaps + public get getTerms(): Map { + return this.terms; + } + + public get isCurrentTermActive(): boolean { + return this.currentId === this.activeId; + } + +} diff --git a/web/src/app/services/selection.service.ts b/web/src/app/services/selection.service.ts index 87e49865..0e6d926a 100644 --- a/web/src/app/services/selection.service.ts +++ b/web/src/app/services/selection.service.ts @@ -1,25 +1,29 @@ import { Injectable } from '@angular/core'; import 'rxjs/Rx'; -import {Subject,Subscription, Subscriber} from 'rxjs/Rx'; +import {Subject, Subscription, Subscriber} from 'rxjs/Rx'; -import { Section } from 'yacs-api-client'; -import { Listing } from 'yacs-api-client'; +import { Section, Listing, Term } from 'yacs-api-client'; import { SidebarService } from './sidebar.service'; - +import { SelectedTermService } from './selected-term.service'; @Injectable() export class SelectionService { - + private clickEvent = new Subject(); constructor ( - public sidebarService : SidebarService) { } + public sidebarService: SidebarService, + protected selectedTermService: SelectedTermService) { + this.selectedTermService.subscribeToActiveTerm((term: Term) => { + this.clear(); + }) + } subscribe (next): Subscription { return this.clickEvent.subscribe(next); } - + next (event) { this.clickEvent.next(event); } @@ -33,12 +37,13 @@ export class SelectionService { } public toggleSection (section : Section) { - + if (!this.selectedTermService.isCurrentTermActive) { return; } this.isSectionSelected(section) ? this.removeSection(section) : this.addSection(section); this.next('event'); //this should be changed } public addSection (section: Section) { + if (!this.selectedTermService.isCurrentTermActive) { return; } let store = this.getSelections() || {}; store[section.listing.id] = store[section.listing.id] || []; if (store[section.listing.id].includes(section.id)) return false; @@ -51,6 +56,7 @@ export class SelectionService { } public removeSection (section: Section) { + if (!this.selectedTermService.isCurrentTermActive) { return; } let store = this.getSelections() || {}; if (!store[section.listing.id] || !store[section.listing.id].includes(section.id)) return false; store[section.listing.id].splice(store[section.listing.id].indexOf(section.id), 1); @@ -62,23 +68,21 @@ export class SelectionService { } public toggleCourse(course: Listing) { - + if (!this.selectedTermService.isCurrentTermActive) { return; } if (this.hasSelectedSection(course)) { let store = this.getSelections(); delete store[course.id]; this.setItem('selections', JSON.stringify(store)); } else { course.sections.forEach((s) => { - if (s.seatsTaken < s.seats) { - this.addSection(s); - } + this.addSection(s); }); } this.next('event'); } public removeListing(course: Listing) { - + if (!this.selectedTermService.isCurrentTermActive) { return; } if (this.hasSelectedSection(course)) { let store = this.getSelections(); delete store[course.id]; @@ -101,7 +105,7 @@ export class SelectionService { public getSelections () { return JSON.parse(this.getItem('selections')) || {}; } - + public getSelectedSectionIds () { const selections = this.getSelections(); const sectionIds = []; @@ -115,7 +119,7 @@ export class SelectionService { return Object.keys(this.getSelections()); } - public clear () { + public clear () { let store = {}; this.setItem('selections', JSON.stringify(store)); this.next('event'); diff --git a/web/src/app/sidebar/interested-courses/component.html b/web/src/app/sidebar/interested-courses/component.html index cb7efee1..2e450bf8 100644 --- a/web/src/app/sidebar/interested-courses/component.html +++ b/web/src/app/sidebar/interested-courses/component.html @@ -1,6 +1,13 @@
+
diff --git a/web/src/app/sidebar/interested-courses/component.scss b/web/src/app/sidebar/interested-courses/component.scss index b2fbf618..3fc91d11 100644 --- a/web/src/app/sidebar/interested-courses/component.scss +++ b/web/src/app/sidebar/interested-courses/component.scss @@ -10,5 +10,8 @@ .alert-sidebar { background-color: #d57f7f; color: #ffffff; +} + +.font-weight-300 { font-weight: 300; } diff --git a/web/src/app/sidebar/interested-courses/component.ts b/web/src/app/sidebar/interested-courses/component.ts index eeba75e6..50d8dfee 100644 --- a/web/src/app/sidebar/interested-courses/component.ts +++ b/web/src/app/sidebar/interested-courses/component.ts @@ -1,6 +1,7 @@ import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { SidebarService } from '../../services/sidebar.service'; import { ConflictsService } from '../../services/conflicts.service'; +import { SelectedTermService } from '../../services/selected-term.service'; // import { Course } from '../../models/course.model'; import { Term, Listing } from 'yacs-api-client'; import 'rxjs/Rx'; @@ -24,7 +25,8 @@ export class InterestedCoursesComponent implements OnInit { constructor ( public sidebarService : SidebarService, - private conflictsService: ConflictsService) { + private conflictsService: ConflictsService, + private selectedTermService: SelectedTermService) { this.subscription = this.sidebarService.subscribe(() => { this.getCourses(); }); @@ -35,6 +37,10 @@ export class InterestedCoursesComponent implements OnInit { this.getCourses(); } + get isActiveTerm(): boolean { + return this.selectedTermService.isCurrentTermActive; + } + async getCourses (): Promise { this.listingIds = this.sidebarService.getListingIds(); @@ -43,7 +49,6 @@ export class InterestedCoursesComponent implements OnInit { if (this.listingIds.size > 0) { this.showStatusText = false; this.isLoaded = false; - const term = await Term.first(); Listing .where({ id: Array.from(this.listingIds) }) .includes('sections') diff --git a/web/src/assets/images/eye.svg b/web/src/assets/images/eye.svg new file mode 100644 index 00000000..04a8b2a0 --- /dev/null +++ b/web/src/assets/images/eye.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/assets/images/yellow_danger.svg b/web/src/assets/images/yellow_danger.svg new file mode 100644 index 00000000..db90afc5 --- /dev/null +++ b/web/src/assets/images/yellow_danger.svg @@ -0,0 +1,2 @@ + +