diff --git a/_config.yml b/_config.yml index 772a75c..ca4ac35 100644 --- a/_config.yml +++ b/_config.yml @@ -35,16 +35,18 @@ course_email: COURSE_EMAIL@berkeley.edu gradescope_course_id: 123456 # you can find this in the Gradescope URL after /courses bcourses_course_id: 123456 # Same as above, but for bCourses. Leave blank if not in use... ed_course_id: 123456 # Again, same as above. -sememster: spYY | suYY | faYY # set for the current seemester +semester: spYY | suYY | faYY # set for the current semester +semester_start_date: '2025-01-21' # Required for the archive banner logic (YYYY-MM-DD) +semester_end_date: '2025-05-09' # Required for the archive banner logic (YYYY-MM-DD) # TODO(setup): Set this to the file path of your course logo image, or leave it blank to display your course name. # To edit the size and positioning of the logo/course name, see _sass/custom/course_overrides.scss logo: /assets/images/berkeley_walking_bear.png # This should be one of eecs, dsus, stat # (Future) This will control some footer text, and later custom styling. course_department: dsus -# This should be the page of all class archives -# Typically just / for DS courses (with a visible index page), or /archives if you're hosting your own, or a link to the inst.eecs page -# If you have no archive page, comment this line out or leave blank. +# This path/URL is used by the archive banner shown when the site date is outside the semester window. +# Keep this key present. Default `/` is fine for class-listing homepages, and `/archive` is also a good option. +# We recommend listing archived offerings as "Archive" or "Not Updated" in course listings. class_archive_path: / # TODO(setup): Remove this line if your favicon is named favicon.ico and is in the root directory, diff --git a/_includes/header_custom.html b/_includes/header_custom.html new file mode 100644 index 0000000..f11b25c --- /dev/null +++ b/_includes/header_custom.html @@ -0,0 +1,41 @@ +{% assign archive_url = site.class_archive_path | absolute_url %} +
+ This site is archived. Please check: + {{ archive_url }} +
+ + diff --git a/_plugins/config_validator.rb b/_plugins/config_validator.rb index d49aa59..8d845f3 100644 --- a/_plugins/config_validator.rb +++ b/_plugins/config_validator.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require 'date' +require 'uri' + # A UC Berkeley-specific validator for Jekyll sites # This class validates the following options: # 1) Ensures required attributes are present in the config file. @@ -44,7 +47,10 @@ class ConfigValidator url: :validate_clean_url, baseurl: :validate_semester_format, course_department: :inclusion_validator, - color_scheme: :inclusion_validator + color_scheme: :inclusion_validator, + semester_start_date: :validate_iso8601_date, + semester_end_date: :validate_iso8601_date, + class_archive_path: :validate_archive_path }.freeze attr_accessor :config, :errors @@ -61,13 +67,15 @@ def validate send(validator, key, config[key.to_s]) if @config.key?(key.to_s) end + validate_semester_date_range + raise ConfigValidationError, errors if errors.length.positive? puts 'Passed Berkeley YAML Config Validations' end def validate_keys! - required_keys = %i[baseurl course_department] + required_keys = %i[baseurl course_department semester_start_date semester_end_date class_archive_path] required_keys.each do |key| errors << "#{key} is missing from site config" unless @config.key?(key.to_s) end @@ -95,6 +103,40 @@ def inclusion_validator(key, value) allowed = self.class.const_get("VALID_#{key.upcase}") errors << "`#{key}` must be one of #{allowed} (not '#{value}')" unless allowed.include?(value) end + + def validate_iso8601_date(key, value) + return if parse_iso8601_date(value) + + errors << "`#{key}` must be a valid ISO-8601 date string (YYYY-MM-DD), not '#{value}'" + end + + def validate_semester_date_range + start_date = parse_iso8601_date(config['semester_start_date']) + end_date = parse_iso8601_date(config['semester_end_date']) + return unless start_date && end_date + return unless start_date > end_date + + errors << '`semester_start_date` must be on or before `semester_end_date`' + end + + def validate_archive_path(_key, value) + path = value.to_s.strip + return errors << '`class_archive_path` cannot be blank' if path.empty? + return if path.match?(%r{^/}) + + uri = URI.parse(path) + return if uri.is_a?(URI::HTTP) && uri.host + + errors << "`class_archive_path` must be an absolute path starting with `/` or a full URL, not '#{value}'" + rescue URI::InvalidURIError + errors << "`class_archive_path` must be an absolute path starting with `/` or a full URL, not '#{value}'" + end + + def parse_iso8601_date(value) + Date.iso8601(value.to_s) + rescue Date::Error + nil + end end end diff --git a/_sass/berkeley/variables.scss b/_sass/berkeley/variables.scss index 366f259..828c41b 100644 --- a/_sass/berkeley/variables.scss +++ b/_sass/berkeley/variables.scss @@ -12,6 +12,7 @@ $serif-font-family: 'Source Serif 4', 'Georiga', 'Baskerville', serif; $berkeley-blue-old: #003262; $berkeley-blue: #002676; // rgb(0, 38, 118); +$berkeley-rose-medium: #d02670; // Colors $link-color: $berkeley-blue; diff --git a/_sass/custom/custom.scss b/_sass/custom/custom.scss index 50df695..a9d693e 100644 --- a/_sass/custom/custom.scss +++ b/_sass/custom/custom.scss @@ -137,9 +137,34 @@ p.note::before { @include btn-color($white, #6929c4); } - .btn-officehours{ - @include btn-color($white, #009d9a); - } +.btn-officehours{ + @include btn-color($white, #009d9a); +} + +.archive-banner { + display: none; + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 999; + padding: 0.75rem 1rem; + background-color: $berkeley-rose-medium; + color: #fff; + font-weight: 700; + text-align: center; +} + +.archive-banner-link, +.archive-banner-link:visited { + color: #fff; + font-weight: 700; + text-decoration: underline; +} + +body.archive-banner-visible { + padding-top: 3rem; +} @if $color-scheme == dark { @include dark-mode-overrides; diff --git a/spec/jekyll/config_validator_spec.rb b/spec/jekyll/config_validator_spec.rb new file mode 100644 index 0000000..b617bf3 --- /dev/null +++ b/spec/jekyll/config_validator_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_relative '../../_plugins/config_validator' + +RSpec.describe Jekyll::ConfigValidator do + let(:base_config) do + { + 'baseurl' => '/sp26', + 'course_department' => 'dsus', + 'semester_start_date' => '2026-01-21', + 'semester_end_date' => '2026-05-09', + 'class_archive_path' => '/' + } + end + + it 'validates with required semester dates present' do + validator = described_class.new(base_config) + + expect { validator.validate }.not_to raise_error + end + + it 'requires semester_start_date and semester_end_date' do + config = base_config.except('semester_start_date', 'semester_end_date') + validator = described_class.new(config) + + expect { validator.validate } + .to raise_error(ConfigValidationError, /semester_start_date is missing.*semester_end_date is missing/m) + end + + it 'requires class_archive_path' do + validator = described_class.new(base_config.except('class_archive_path')) + + expect { validator.validate } + .to raise_error(ConfigValidationError, /class_archive_path is missing from site config/) + end + + it 'allows class_archive_path as a full URL' do + validator = described_class.new(base_config.merge('class_archive_path' => 'https://c88c.org/archive')) + + expect { validator.validate }.not_to raise_error + end + + it 'rejects class_archive_path values that are not paths or full URLs' do + validator = described_class.new(base_config.merge('class_archive_path' => 'archive')) + expected = %r{`class_archive_path` must be an absolute path starting with `/` or a full URL} + expect { validator.validate }.to raise_error(ConfigValidationError, expected) + end + + it 'validates ISO-8601 semester dates' do + validator = described_class.new(base_config.merge('semester_start_date' => 'spring-2026')) + + expect { validator.validate } + .to raise_error(ConfigValidationError, /`semester_start_date` must be a valid ISO-8601 date string/) + end + + it 'requires the start date to be on or before the end date' do + validator = described_class.new(base_config.merge('semester_start_date' => '2026-06-01')) + + expect { validator.validate } + .to raise_error(ConfigValidationError, /`semester_start_date` must be on or before `semester_end_date`/) + end +end