From 51354fd19f76c2cbfd31542e9c072b86a90134f0 Mon Sep 17 00:00:00 2001 From: ckonstanski Date: Tue, 21 Jul 2026 15:26:40 -0600 Subject: added education --- lisp/service/menu-service.lisp | 1 + lisp/service/resume-service.lisp | 142 +++++++++++++- lisp/sql/education.lisp | 3 + lisp/sql/language.lisp | 3 + lisp/sql/resume-pkg.lisp | 26 ++- .../resume/clojurescript/resume/src/core.cljs | 213 ++++++++++++++++++++- lisp/webapps/resume/site.lisp | 28 +++ sql/functions/resume.insert_education.sql | 49 +++++ sql/tables/resume.education.sql | 7 +- sql/tables/resume.language.sql | 1 + 10 files changed, 456 insertions(+), 17 deletions(-) create mode 100644 sql/functions/resume.insert_education.sql diff --git a/lisp/service/menu-service.lisp b/lisp/service/menu-service.lisp index a01c5f8..00dff60 100644 --- a/lisp/service/menu-service.lisp +++ b/lisp/service/menu-service.lisp @@ -6,6 +6,7 @@ (defparameter *menu-config* '((:id "a_menu_home" :label "Home" :handler "/home" :permissions "_Public") (:id "a_menu_resumes" :label "Resumes" :handler "/resumes" :permissions "resume-view") (:id "a_menu_resumes_contactinfo" :label "Contact Info" :handler "/resumes/contactinfo" :permissions "resume-view") + (:id "a_menu_resumes_education" :label "Education" :handler "/resumes/education" :permissions "resume-view") (:id "a_menu_contact_us" :label "Contact Me" :handler "/contact-us" :permissions "_Public") (:id "a_menu_messages" :label "Messages" :handler "/messages" :permissions "messages-view") (:id "a_menu_users" :label "Users" :handler "/users" :permissions "users-view"))) diff --git a/lisp/service/resume-service.lisp b/lisp/service/resume-service.lisp index 7e3aa71..8994595 100644 --- a/lisp/service/resume-service.lisp +++ b/lisp/service/resume-service.lisp @@ -99,7 +99,7 @@ (intersection `(,x) '(id))) (sb-introspect:function-lambda-list #'resumes-modify-submit-json)) do (setf (slot-value resume param) (symbol-value param))) - (update-resume resume-pkg resume) + (update-record resume-pkg resume) (setf (session-value :message) (format nil "Resume \"~a\" modified successfully." (name resume)))) (error (e) (setf (session-value :errormsg) (format nil "Error modifying resume \"~a\". ~a" name e))))))) @@ -235,7 +235,7 @@ (intersection `(,x) '(id))) (sb-introspect:function-lambda-list #'contactinfo-modify-submit-json)) do (setf (slot-value contactinfo param) (symbol-value param))) - (update-contactinfo resume-pkg contactinfo) + (update-record resume-pkg contactinfo) (setf (session-value :message) (format nil "Contact Info \"~a, ~a\" modified successfully." address city))) (error (e) (setf (session-value :errormsg) (format nil "Error modifying Contact Info \"~a, ~a\". ~a" address city e))))))) @@ -252,6 +252,142 @@ (contactinfo (get-record resume-pkg (make-instance 'contactinfo :id id :user_id (id user))))) (if contactinfo (progn - (delete-contactinfo resume-pkg contactinfo) + (delete-record resume-pkg contactinfo) (setf (session-value :message) (format nil "Contact Info \"~a, ~a\" deleted successfully." (address contactinfo) (city contactinfo)))) (setf (session-value :errormsg) "Error: could not delete Contact Info: not found.")))))) + +;; education + +(defun education-json () + (with-auth (instance resumes-service "resume-view") + (setf (title instance) "Education"))) + +(defclass education/view-service (resumes-service education) + ((educations :initarg :educations + :initform nil + :accessor educations) + (location-p :initform nil)) + (:documentation "")) + +(defun education-view-json () + (with-auth (instance education/view-service "resume-view") + (with-resume-database + (let ((resume-pkg (make-instance 'resume-pkg)) + (user (get-user))) + (setf (educations instance) (get-all-education resume-pkg user)))) + (sanitize-rest-json instance) + (loop for education in (educations instance) + do (sanitize-json education)))) + +(defclass education/add-service (education/view-service) + ((form :initarg :form + :initform nil + :accessor form)) + (:documentation "")) + +(defun education-add-json () + (with-auth (instance education/add-service "resume-modify") + (with-resume-database + (let* ((resume-pkg (make-instance 'resume-pkg)) + (user (get-user))) + (setf (title instance) "Education - Add") + (setf (form instance) (make-form "education-add-form" + nil + t + `((:name "school" :label "School" :field-type "text" :required "required") + (:name "major" :label "Major" :field-type "text" :required "required") + (:name "minor" :label "Minor" :field-type "text") + (:name "degree" :label "Degree" :field-type "text" :required "required") + (:name "progress" :label "Progress" :field-type "text" :required "required") + (:name "start_date" :label "Start Date" :field-type "text" :placeholder "MM/YYYY" :required "required") + (:name "end_date" :label "End Date" :field-type "text" :placeholder "MM/YYYY" :required "required") + (:label "Add Education" :field-type "button" :onclick "on_education_add_submit_clicked()")))))))) + +(defun education-add-submit-json (school major minor degree progress start_date end_date) + (declare (special school major minor degree progress start_date end_date)) + (with-auth (instance education/add-service "resume-modify") + (with-resume-database + (handler-case + (let* ((resume-pkg (make-instance 'resume-pkg)) + (user (get-user)) + (education (make-instance 'education :user_id (id user)))) + (loop for param in (remove-if (lambda (x) + (intersection `(,x) '(id))) + (sb-introspect:function-lambda-list #'education-add-submit-json)) + do (setf (slot-value education param) (symbol-value param))) + (insert-education resume-pkg education) + (setf (session-value :message) (format nil "Education \"~a, ~a\" created successfully." school major))) + (error (e) + (setf (session-value :errormsg) (format nil "Error creating Education \"~a, ~a\". ~a" school major e))))))) + +(defclass education/modify-service (education/add-service) + () + (:documentation "")) + +(defun education-modify-json (id) + (with-auth (instance education/modify-service "resume-modify") + (setf (title instance) "Education - Modify") + (with-resume-database + (let* ((resume-pkg (make-instance 'resume-pkg)) + (user (get-user)) + (education (get-record resume-pkg (make-instance 'education :id id :user_id (id user))))) + (if education + (setf (form instance) (make-form "education-modify-form" + nil + t + `((:name "id" :field-type "hidden" :value ,(id education) :required "required") + (:name "school" :label "School" :field-type "text" :value ,(school education) :required "required") + (:name "major" :label "Major" :field-type "text" :value ,(major education) :required "required") + (:name "minor" :label "Minor" :field-type "text" :value ,(minor education)) + (:name "degree" :label "Degree" :field-type "text" :value ,(degree education) :required "required") + (:name "progress" :label "Progress" :field-type "text" :value ,(progress education) :required "required") + (:name "start_date" :label "Start Date" :field-type "datetime-local" :value ,(start_date education) :required "required") + (:name "end_date" :label "End Date" :field-type "datetime-local" :value ,(end_date education) :required "required") + (:label "Modify Education" :field-type "button" :onclick "on_education_modify_submit_clicked()")))) + (setf (session-value :errormsg) "Error: could not modify Education. Not found.")))))) + +(defun education-modify-submit-json (id school major minor degree progress start_date end_date) + (declare (special id school major minor degree progress start_date end_date)) + (with-auth (instance education/modify-service "resume-modify") + (setf (title instance) "Education - Modify") + (with-resume-database + (handler-case + (let* ((resume-pkg (make-instance 'resume-pkg)) + (user (get-user)) + (education (get-record resume-pkg (make-instance 'education :id id :user_id (id user))))) + (loop for param in (remove-if (lambda (x) + (intersection `(,x) '(id))) + (sb-introspect:function-lambda-list #'education-modify-submit-json)) + do (setf (slot-value education param) (symbol-value param))) + (update-record resume-pkg education) + (setf (session-value :message) (format nil "Education \"~a, ~a\" modified successfully." school major))) + (error (e) + (setf (session-value :errormsg) (format nil "Error modifying Education \"~a, ~a\". ~a" school major e))))))) + +(defclass education/delete-service (education/add-service) + () + (:documentation "")) + +(defun education-delete-json (id) + (with-auth (instance education/delete-service "resume-modify") + (with-resume-database + (let* ((resume-pkg (make-instance 'resume-pkg)) + (user (get-user)) + (education (get-record resume-pkg (make-instance 'education :id id :user_id (id user))))) + (if education + (progn + (delete-record resume-pkg education) + (setf (session-value :message) (format nil "Education \"~a, ~a\" deleted successfully." (school education) (major education)))) + (setf (session-value :errormsg) "Error: could not delete Education: not found.")))))) + +;; language + + + +(defun language-options (level) + (mapcar (lambda (level) + `(:label level :value level)) + '("A1" "A2" "B1" "B2" "C1" "C2"))) + + + diff --git a/lisp/sql/education.lisp b/lisp/sql/education.lisp index 96b5382..45ee2c8 100644 --- a/lisp/sql/education.lisp +++ b/lisp/sql/education.lisp @@ -7,6 +7,9 @@ ((id :initarg :id :initform nil :accessor id) + (user_id :initarg :user_id + :initform nil + :accessor user_id) (school :initarg :school :initform nil :accessor school) diff --git a/lisp/sql/language.lisp b/lisp/sql/language.lisp index ca384b1..92f03a7 100644 --- a/lisp/sql/language.lisp +++ b/lisp/sql/language.lisp @@ -7,6 +7,9 @@ ((id :initarg :id :initform nil :accessor id) + (user_id :initarg :user_id + :initform nil + :accessor user_id) (name :initarg :name :initform nil :accessor name) diff --git a/lisp/sql/resume-pkg.lisp b/lisp/sql/resume-pkg.lisp index e44dfd4..a43a07e 100644 --- a/lisp/sql/resume-pkg.lisp +++ b/lisp/sql/resume-pkg.lisp @@ -20,9 +20,6 @@ (name resume))) (caar (call-pg-function resume-pkg resume))) -(defmethod update-resume ((resume-pkg resume-pkg) (resume resume)) - (update-record resume-pkg resume)) - (defmethod delete-resume ((resume-pkg resume-pkg) (resume resume)) (setf (*table resume) (format nil "resume.delete_resume(~a)" @@ -47,15 +44,24 @@ (email contactinfo))) (caar (call-pg-function resume-pkg contactinfo))) -(defmethod update-contactinfo ((resume-pkg resume-pkg) (contactinfo contactinfo)) - (update-record resume-pkg contactinfo)) - -(defmethod delete-contactinfo ((resume-pkg resume-pkg) (contactinfo contactinfo)) - (delete-record resume-pkg contactinfo)) - ;; education - +(defmethod get-all-education ((resume-pkg resume-pkg) (user user)) + (let ((education (make-instance 'education :user_id (id user)))) + (get-records resume-pkg education "school asc, end_date asc, major asc, minor asc"))) + +(defmethod insert-education ((resume-pkg resume-pkg) (education education)) + (setf (*table education) (format nil + "resume.insert_education(~a, '~a', '~a', '~a', '~a', '~a', '~a', '~a')" + (user_id education) + (school education) + (start_date education) + (end_date education) + (major education) + (minor education) + (degree education) + (progress education))) + (caar (call-pg-function resume-pkg education))) ;; language diff --git a/lisp/webapps/resume/clojurescript/resume/src/core.cljs b/lisp/webapps/resume/clojurescript/resume/src/core.cljs index cde1a89..d894e7f 100644 --- a/lisp/webapps/resume/clojurescript/resume/src/core.cljs +++ b/lisp/webapps/resume/clojurescript/resume/src/core.cljs @@ -172,6 +172,29 @@ (declare on-contactinfo-delete-clicked) (declare handler-contactinfo-delete) (declare render-contactinfo-delete) +(declare template-education) +(declare handler-education) +(declare render-education) +(declare template-education-view) +(declare handler-education-view) +(declare render-education-view) +(declare on-education-add-clicked) +(declare template-education-add) +(declare handler-education-add) +(declare render-education-add) +(declare on-education-add-submit-clicked) +(declare handler-education-add-submit) +(declare render-education-add-submit) +(declare on-education-modify-clicked) +(declare template-education-modify) +(declare handler-education-modify) +(declare render-education-modify) +(declare on-education-modify-submit-clicked) +(declare handler-education-modify-submit) +(declare render-education-modify-submit) +(declare on-education-delete-clicked) +(declare handler-education-delete) +(declare render-education-delete) (declare on-menu-clicked) (declare handler-location) (declare goto-location) @@ -1396,6 +1419,193 @@ {:format :raw :handler handler-contactinfo-delete})) +;; education + +(hiccups/defhtml template-education [jsonobj] + [:h3 {:style "text-align: center"} (get jsonobj "title")] + [:div {:id "content"}] + [:div {:id "modify" + :class "modal fade" + :role "dialog" + :tabindex "-1"} + [:div {:class "modal-dialog modal-lg"} + [:div {:class "modal-content"} + [:div {:class "modal-header"} + [:h5 [:span {:id "modify-title"}]] + [:button {:type "button" + :class "close" + :data-dismiss "modal"} + "×"]] + [:div {:id "modify-body" + :class "modal-body" + :style "height: 450px;"}] + [:div {:class "modal-footer"} + [:button {:type "submit" + :class "btn btn-danger btn-default" + :data-dismiss "modal"} + [:span {:class "glyphicon glyphicon-remove"}] + "Cancel"]]]]]) + +(defn handler-education [response] + (let [jsonobj (js->clj (js/JSON.parse response))] + (notifications jsonobj) + (dommy/set-html! (dommy/sel1 :#body) (template-education jsonobj)) + (render-education-view))) + +(defn render-education [] + (GET "/resumes/education" {:handler handler-education})) + +;; education-view + +(hiccups/defhtml template-education-view [jsonobj] + [:div {:style "text-align: right"} + [:img {:src "/static/images/add.png" + :style "cursor:pointer; cursor:hand" + :onclick (str (namespace ::x) ".on_education_add_clicked()")}]] + [:table {:class "table table-striped table-hover table-sm"} + [:thead + [:tr + [:th {:scope "col"} "School"] + [:th {:scope "col"} "Start Date"] + [:th {:scope "col"} "End Date"] + [:th {:scope "col"} "Major"] + [:th {:scope "col"} "Minor"] + [:th {:scope "col"} "Degree"] + [:th {:scope "col"} "Progress"] + [:th {:scope "col"} "Mod"] + [:th {:scope "col"} "Del"]]] + [:tbody + (for [education (get jsonobj "educations")] + [:tr + [:td (get education "school")] + [:td (get education "start_date")] + [:td (get education "end_date")] + [:td (get education "major")] + [:td (get education "minor")] + [:td (get education "degree")] + [:td (get education "progress")] + [:td {:width "30"} + [:img {:src "/static/images/modify.png" + :onclick (str (namespace ::x) ".on_education_modify_clicked(" (get education "id") ")")}]] + [:td {:width "30"} + [:img {:src "/static/images/delete.png" + :onclick (str (namespace ::x) ".on_education_delete_clicked(" (get education "id") ", '" (str (get education "school") ", " (get education "major")) "')")}]]])]]) + +(defn handler-education-view [response] + (let [jsonobj (js->clj (js/JSON.parse response))] + (dommy/set-html! (dommy/sel1 :#content) (template-education-view jsonobj)))) + +(defn render-education-view [] + (GET "/resumes/education/view" {:handler handler-education-view})) + +;; education-add + +(defn on-education-add-clicked [] + (render-education-add)) + +(hiccups/defhtml template-education-add [jsonobj] + (ck-form/template-generic-form (get jsonobj "form") (namespace ::x))) + +(defn handler-education-add [response] + (let [jsonobj (js->clj (js/JSON.parse response))] + (auth-notifications jsonobj) + (dommy/set-html! (dommy/sel1 :#modify-title) (get jsonobj "title")) + (dommy/set-html! (dommy/sel1 :#modify-body) (template-education-add jsonobj)) + (.modal (jquery "#modify")))) + +(defn render-education-add [] + (POST "/resumes/education/add" {:handler handler-education-add})) + +;; education-add-submit + +(defn on-education-add-submit-clicked [] + (when (-> (jquery "#education-add-form") + (.get "0") + (.checkValidity)) + (.modal (jquery "#modify") "hide") + (render-education-add-submit))) + +(defn handler-education-add-submit [response] + (let [jsonobj (js->clj (js/JSON.parse response))] + (auth-notifications jsonobj) + (on-menu-clicked "/resumes/education"))) + +(defn render-education-add-submit [] + (POST "/resumes/education/add/submit" + {:format :raw + :params {:school (dommy/value (dommy/sel1 :#school)) + :major (dommy/value (dommy/sel1 :#major)) + :minor (dommy/value (dommy/sel1 :#minor)) + :degree (dommy/value (dommy/sel1 :#degree)) + :progress (dommy/value (dommy/sel1 :#progress)) + :start_date (dommy/value (dommy/sel1 :#start_date)) + :end_date (dommy/value (dommy/sel1 :#end_date))} + :handler handler-education-add-submit})) + +;; education-modify + +(defn on-education-modify-clicked [id] + (render-education-modify id)) + +(hiccups/defhtml template-education-modify [jsonobj] + (ck-form/template-generic-form (get jsonobj "form") (namespace ::x))) + +(defn handler-education-modify [response] + (let [jsonobj (js->clj (js/JSON.parse response))] + (auth-notifications jsonobj) + (dommy/set-html! (dommy/sel1 :#modify-title) (get jsonobj "title")) + (dommy/set-html! (dommy/sel1 :#modify-body) (template-education-modify jsonobj)) + (.modal (jquery "#modify")))) + +(defn render-education-modify [id] + (POST "/resumes/education/modify" + {:format :raw + :params {:id id} + :handler handler-education-modify})) + +;; education-modify-submit + +(defn on-education-modify-submit-clicked [] + (when (-> (jquery "#education-modify-form") + (.get "0") + (.checkValidity)) + (.modal (jquery "#modify") "hide") + (render-education-modify-submit))) + +(defn handler-education-modify-submit [response] + (let [jsonobj (js->clj (js/JSON.parse response))] + (auth-notifications jsonobj) + (on-menu-clicked "/resumes/education"))) + +(defn render-education-modify-submit [] + (POST "/resumes/education/modify/submit" + {:format :raw + :params {:id (dommy/value (dommy/sel1 :#id)) + :school (dommy/value (dommy/sel1 :#school)) + :major (dommy/value (dommy/sel1 :#major)) + :minor (dommy/value (dommy/sel1 :#minor)) + :degree (dommy/value (dommy/sel1 :#degree)) + :progress (dommy/value (dommy/sel1 :#progress)) + :start_date (dommy/value (dommy/sel1 :#start_date)) + :end_date (dommy/value (dommy/sel1 :#end_date))} + :handler handler-education-modify-submit})) + +;; education-delete + +(defn on-education-delete-clicked [id name] + (when (js/confirm (str "Are you sure you want to delete " name "? This action cannot be undone.")) + (render-education-delete id))) + +(defn handler-education-delete [response] + (let [jsonobj (js->clj (js/JSON.parse response))] + (auth-notifications jsonobj) + (on-menu-clicked "/resumes/education"))) + +(defn render-education-delete [id] + (DELETE (str "/resumes/education/delete/" id) + {:format :raw + :handler handler-education-delete})) + ;; location (defn on-menu-clicked [handler] @@ -1410,7 +1620,8 @@ (= handler "/messages") (render-messages) (= handler "/users") (render-users) (= handler "/resumes") (render-resumes) - (= handler "/resumes/contactinfo") (render-contactinfo))) + (= handler "/resumes/contactinfo") (render-contactinfo) + (= handler "/resumes/education") (render-education))) (defn handler-location [response] (let [jsonobj (js->clj (js/JSON.parse response))] diff --git a/lisp/webapps/resume/site.lisp b/lisp/webapps/resume/site.lisp index 2142c9c..d8c1fb5 100644 --- a/lisp/webapps/resume/site.lisp +++ b/lisp/webapps/resume/site.lisp @@ -173,6 +173,27 @@ (defmacro .contactinfo-delete () `(contactinfo-delete-json id)) +(defmacro .education () + `(education-json)) + +(defmacro .education-view () + `(education-view-json)) + +(defmacro .education-add () + `(education-add-json)) + +(defmacro .education-add-submit () + `(education-add-submit-json school major minor degree progress start_date end_date)) + +(defmacro .education-modify () + `(education-modify-json id)) + +(defmacro .education-modify-submit () + `(education-modify-submit-json id school major minor degree progress start_date end_date)) + +(defmacro .education-delete () + `(education-delete-json id)) + (define-endpoint ("/" :method :get) () .base) (define-endpoint ("/location" :method :post) (&post (location :parameter-type 'string)) .location) (define-endpoint ("/home" :method :get) () .home-get) @@ -224,3 +245,10 @@ (define-endpoint ("/resumes/contactinfo/modify" :method :post) (&post (id :parameter-type 'integer)) .contactinfo-modify) (define-endpoint ("/resumes/contactinfo/modify/submit" :method :post) (&post (id :parameter-type 'integer) (address :parameter-type 'string) (city :parameter-type 'string) (state_id :parameter-type 'integer) (phone :parameter-type 'string) (email :parameter-type 'string) (visastatus_id :parameter-type 'integer)) .contactinfo-modify-submit) (define-endpoint ("/resumes/contactinfo/delete/:id" :method :delete) (&path (id 'integer)) .contactinfo-delete) +(define-endpoint ("/resumes/education" :method :get) () .education) +(define-endpoint ("/resumes/education/view" :method :get) () .education-view) +(define-endpoint ("/resumes/education/add" :method :post) () .education-add) +(define-endpoint ("/resumes/education/add/submit" :method :post) (&post (school :parameter-type 'string) (major :parameter-type 'string) (minor :parameter-type 'string) (degree :parameter-type 'string) (progress :parameter-type 'string) (start_date :parameter-type 'string) (end_date :parameter-type 'string)) .education-add-submit) +(define-endpoint ("/resumes/education/modify" :method :post) (&post (id :parameter-type 'integer)) .education-modify) +(define-endpoint ("/resumes/education/modify/submit" :method :post) (&post (id :parameter-type 'integer) (school :parameter-type 'string) (major :parameter-type 'string) (minor :parameter-type 'string) (degree :parameter-type 'string) (progress :parameter-type 'string) (start_date :parameter-type 'string) (end_date :parameter-type 'string)) .education-modify-submit) +(define-endpoint ("/resumes/education/delete/:id" :method :delete) (&path (id 'integer)) .education-delete) diff --git a/sql/functions/resume.insert_education.sql b/sql/functions/resume.insert_education.sql new file mode 100644 index 0000000..59ba98b --- /dev/null +++ b/sql/functions/resume.insert_education.sql @@ -0,0 +1,49 @@ +drop function resume.insert_education ( + p_user_id int, + p_school character varying, + p_start_date character varying, + p_end_date character varying, + p_major character varying, + p_minor character varying, + p_degree character varying, + p_progress character varying +); + +create or replace function resume.insert_education ( + p_user_id int, + p_school character varying, + p_start_date character varying, + p_end_date character varying, + p_major character varying, + p_minor character varying, + p_degree character varying, + p_progress character varying +) +returns bigint +as $$ +declare + l_id bigint; +begin + insert into resume.education ( + user_id, + school, + start_date, + end_date, + major, + minor, + degree, + progress + ) values ( + p_user_id, + p_school, + p_start_date, + p_end_date, + p_major, + p_minor, + p_degree, + p_progress + ) returning id into l_id; + + return l_id; +end; +$$ language plpgsql; diff --git a/sql/tables/resume.education.sql b/sql/tables/resume.education.sql index 638a50c..161dbfc 100644 --- a/sql/tables/resume.education.sql +++ b/sql/tables/resume.education.sql @@ -2,11 +2,12 @@ drop table resume.education cascade; create table resume.education ( id serial8 primary key, + user_id bigint not null references auth.users(id), school character varying(255) not null, - start_date timestamp without time zone not null default now(), - end_date timestamp without time zone not null default now(), + start_date character varying(255) not null, + end_date character varying(255) not null, major character varying(255) not null, - minor character varying(255) not null, + minor character varying(255), degree character varying(255) not null, progress character varying(255) not null ); diff --git a/sql/tables/resume.language.sql b/sql/tables/resume.language.sql index 0f5c452..6aaf0d8 100644 --- a/sql/tables/resume.language.sql +++ b/sql/tables/resume.language.sql @@ -2,6 +2,7 @@ drop table resume.language cascade; create table resume.language ( id serial8 primary key, + user_id bigint not null references auth.users(id), name character varying(255) not null, understanding_listening character varying(2) not null, understanding_reading character varying(2) not null, -- cgit v1.3