Verbessere Dokumentation

This commit is contained in:
Gonne 2022-11-04 21:15:38 +01:00
parent ec24c6c4dc
commit fe54d76ab2
14 changed files with 75 additions and 34 deletions

View file

@ -1,3 +1,7 @@
// Package config implements the officeHours configuration
//
// It provides a struct type holding all necessary information for the programm.
// The configuration can be read from a JSON file.
package config package config
import ( import (
@ -8,32 +12,34 @@ import (
"log" "log"
) )
// A Config holds all the constants for the programm.
// It is passed to most repositories.
type Config struct { type Config struct {
Server struct { Server struct {
ListenAddress string ListenAddress string // Address on which the http server is supposed to listen
ListenPort int ListenPort int // Port on which the http server is supposed to listen
Protocol string Protocol string // Protocol to access the server; either "http" or "https"
Domain string Domain string // A string indicating the tool's base domain and path, e.g. "example.com/subfolder"
} }
Date struct { Date struct {
MinuteGranularity int MinuteGranularity int // Restricts the minutes on which office hours can start and end to multiples of it.
} }
Request struct { Request struct {
SecretLength int SecretLength int // Length of the secret token for requests
} }
Mailer struct { Mailer struct {
Type string Type string // Send mail to "Stdout" or "Smtp"
FromAddress string FromAddress string // Send mail from this address
FromName template.HTML FromName template.HTML // Send mail from this name, e.g. "Office hours <officeHours@localhost>"
SmtpHost string SmtpHost string // Host to use as smarthost
SmtpPort int SmtpPort int // Port of the smarthost
SmtpUseAuth bool SmtpUseAuth bool // Set whether to use authentication on the smarthosthost
SmtpIdentity string SmtpIdentity string // Smarthost username
SmtpPassword string SmtpPassword string // Smarthost password
} }
SQL struct { SQL struct {
Type string Type string // Can be "SQLite" or "Mysql"
SQLiteFile string SQLiteFile string // Path to SQLite file
MysqlUser string MysqlUser string
MysqlPassword string MysqlPassword string
MysqlHost string MysqlHost string
@ -41,7 +47,8 @@ type Config struct {
MysqlDatabase string MysqlDatabase string
} }
Tutor struct { Tutor struct {
MailSuffix string MailSuffix string // Restrict mailaddresses of tutors to suffixes.
// e.g. "example.com" allowes "foo@example.com", "foo@sub.example.com", but not "foo@another.org" or "foo@barexample.com".
} }
} }
@ -92,5 +99,4 @@ func validateConfig(conf *Config) error {
log.Println(err.Error()) log.Println(err.Error())
} }
return err return err
} }

View file

@ -28,6 +28,7 @@ type maskData struct {
Config config.Config Config config.Config
} }
// Offer a form to add office hours and validate its input on receiving.
func (b *BaseHandler) AddOfficeHourHandler(w http.ResponseWriter, req *http.Request) { func (b *BaseHandler) AddOfficeHourHandler(w http.ResponseWriter, req *http.Request) {
var errors []string var errors []string
courses, err := b.courseRepo.GetAll() courses, err := b.courseRepo.GetAll()
@ -123,6 +124,9 @@ func (b *BaseHandler) AddOfficeHourHandler(w http.ResponseWriter, req *http.Requ
} else if !allowed { } else if !allowed {
errors = append(errors, "In dem Raum muss noch Platz für weitere Sprechstunden sein.") errors = append(errors, "In dem Raum muss noch Platz für weitere Sprechstunden sein.")
} }
// If there were errors in the data for the new office hour,
// answer with the form prefilled with the sent data.
if len(errors) != 0 { if len(errors) != 0 {
var data maskData = maskData{ var data maskData = maskData{
courses, courses,
@ -141,6 +145,8 @@ func (b *BaseHandler) AddOfficeHourHandler(w http.ResponseWriter, req *http.Requ
} }
b.writeAddOfficeHourMask(w, req, data) b.writeAddOfficeHourMask(w, req, data)
} else { } else {
// if the data for a new office hour was sent correctly, save it.
officeHour := models.OfficeHour{Id: 0, officeHour := models.OfficeHour{Id: 0,
Tutor: models.Tutor{Id: 0, Name: name, Email: email.Address}, Tutor: models.Tutor{Id: 0, Name: name, Email: email.Address},
Date: date, Date: date,
@ -166,12 +172,11 @@ func (b *BaseHandler) AddOfficeHourHandler(w http.ResponseWriter, req *http.Requ
log.Printf("Error adding request: %s", err.Error()) log.Printf("Error adding request: %s", err.Error())
} }
templating.ServeTemplate(w, "addSuccess", nil) templating.ServeTemplate(w, "addSuccess", nil)
} }
} }
func (b *BaseHandler) writeAddOfficeHourMask(w http.ResponseWriter, req *http.Request, data maskData) { func (b *BaseHandler) writeAddOfficeHourMask(w http.ResponseWriter, req *http.Request, data maskData) {
if req.Method == http.MethodGet { if req.Method == http.MethodGet { // if the current request is GET, we assume no office hour addition was tried so far and reset the errors.
data.Errors = []string{} data.Errors = []string{}
} }
if len(data.Errors) != 0 { if len(data.Errors) != 0 {

View file

@ -1,19 +1,27 @@
package controllers package controllers
import ( import (
"database/sql"
"errors"
"net/http" "net/http"
"officeHours/templating" "officeHours/templating"
) )
// Check the secret token for requests and execute the request for correct tokens
func (b *BaseHandler) ConfirmRequestHandler(w http.ResponseWriter, req *http.Request) { func (b *BaseHandler) ConfirmRequestHandler(w http.ResponseWriter, req *http.Request) {
secret := req.FormValue("code") secret := req.FormValue("code")
request, err := b.requestRepo.FindBySecret(secret) request, err := b.requestRepo.FindBySecret(secret)
if err != nil { if errors.Is(err, sql.ErrNoRows) { // There was no request with this secret
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
templating.ServeTemplate(w, "requestNotFound", nil) templating.ServeTemplate(w, "requestNotFound", nil)
return return
} }
if err != nil { // Some other error happened finding the request with this secret
w.WriteHeader(http.StatusInternalServerError)
templating.ServeTemplate(w, "executeFailure", err.Error())
return
}
err = b.requestRepo.Execute(request) err = b.requestRepo.Execute(request)
if err != nil { if err != nil {
@ -22,5 +30,4 @@ func (b *BaseHandler) ConfirmRequestHandler(w http.ResponseWriter, req *http.Req
return return
} }
templating.ServeTemplate(w, "executeSuccess", nil) templating.ServeTemplate(w, "executeSuccess", nil)
} }

View file

@ -10,6 +10,9 @@ import (
"strconv" "strconv"
) )
// Offer a table of all office hours to delete,
// verify the corresponding mail address and
// then send a confirmation mail.
func (b *BaseHandler) DeleteOfficeHourHandler(w http.ResponseWriter, req *http.Request) { func (b *BaseHandler) DeleteOfficeHourHandler(w http.ResponseWriter, req *http.Request) {
if req.FormValue("id") != "" { if req.FormValue("id") != "" {
id, err := strconv.Atoi(req.FormValue("id")) id, err := strconv.Atoi(req.FormValue("id"))

View file

@ -1,4 +1,3 @@
// course
package models package models
type Course struct { type Course struct {

View file

@ -1,13 +1,17 @@
// date
package models package models
import (
"log"
)
type Date struct { type Date struct {
Week int Week int // Set whether the date is all weeks (0), odd weeks (1) or even weeks (2).
Day int Day int
Hour int Hour int
Minute int Minute int
} }
// Return the name of the day for a given integer.
func DayName(day int) string { func DayName(day int) string {
switch day { switch day {
case 0: case 0:
@ -21,10 +25,12 @@ func DayName(day int) string {
case 4: case 4:
return "Freitag" return "Freitag"
default: default:
log.Printf("No day name found for day %d", day)
return "" return ""
} }
} }
// Compare whether first date is strictly before second date.
func DateLess(first Date, second Date) bool { func DateLess(first Date, second Date) bool {
if first.Day < second.Day { if first.Day < second.Day {
return true return true
@ -44,6 +50,7 @@ func DateLess(first Date, second Date) bool {
return false return false
} }
// Get the end date for some duration.
func GetEndDate(date Date, duration int, ignoreWeek bool) Date { func GetEndDate(date Date, duration int, ignoreWeek bool) Date {
var endDate Date var endDate Date
if ignoreWeek { if ignoreWeek {
@ -51,6 +58,7 @@ func GetEndDate(date Date, duration int, ignoreWeek bool) Date {
} else { } else {
endDate = Date{date.Week, date.Day, date.Hour, date.Minute} endDate = Date{date.Week, date.Day, date.Hour, date.Minute}
} }
endDate.Day = (endDate.Day + (endDate.Hour*60+endDate.Minute+duration)/(60*24)) % 7
endDate.Hour = endDate.Hour + (endDate.Minute+duration)/60 endDate.Hour = endDate.Hour + (endDate.Minute+duration)/60
endDate.Minute = (endDate.Minute + duration) % 60 endDate.Minute = (endDate.Minute + duration) % 60
return endDate return endDate

View file

@ -1,4 +1,4 @@
// officeHour // The package models defines the stucts for objects, their repositories and the signatures of their functions.
package models package models
type OfficeHour struct { type OfficeHour struct {
@ -7,10 +7,10 @@ type OfficeHour struct {
Date Date
Room Room
Course Course
RoomName string RoomName string // A description of the room for special rooms that are not preconfigured.
Info string Info string
Active bool Active bool
Duration int Duration int // Duration in minutes. Must be divisible by the config field "MinuteGranularity".
} }
type OfficeHourRepository interface { type OfficeHourRepository interface {

View file

@ -8,8 +8,8 @@ type Request struct {
Secret string Secret string
} }
const RequestActivate int = 1 const RequestActivate int = 1 // Fix integer to represent request for activation of an office hour.
const RequestDelete int = 2 const RequestDelete int = 2 // Fix integer to represent request for deletion of an office hour.
type RequestRepository interface { type RequestRepository interface {
Add(officeHour OfficeHour, action int) (int, error) Add(officeHour OfficeHour, action int) (int, error)

View file

@ -1,4 +1,3 @@
// raum
package models package models
type Room struct { type Room struct {

View file

@ -1,4 +1,3 @@
// course
package repositories package repositories
import ( import (
@ -9,6 +8,7 @@ import (
"officeHours/models" "officeHours/models"
) )
// A struct to hold the db connection
type CourseRepo struct { type CourseRepo struct {
db *sql.DB db *sql.DB
} }
@ -38,6 +38,7 @@ func (r *CourseRepo) GetAll() ([]models.Course, error) {
return r.getFromRows(rows) return r.getFromRows(rows)
} }
// Helper function to get a course from multiple SQL result rows
func (r *CourseRepo) getFromRows(rows *sql.Rows) ([]models.Course, error) { func (r *CourseRepo) getFromRows(rows *sql.Rows) ([]models.Course, error) {
var courses []models.Course var courses []models.Course
for rows.Next() { for rows.Next() {
@ -51,6 +52,7 @@ func (r *CourseRepo) getFromRows(rows *sql.Rows) ([]models.Course, error) {
return courses, nil return courses, nil
} }
// Helper function to get a course from an SQL result row
func (r *CourseRepo) getFromRow(row *sql.Row) (models.Course, error) { func (r *CourseRepo) getFromRow(row *sql.Row) (models.Course, error) {
var course models.Course var course models.Course
if err := row.Scan(&course.Id, &course.Name); err != nil { if err := row.Scan(&course.Id, &course.Name); err != nil {

View file

@ -89,6 +89,10 @@ func (r *OfficeHourRepo) FindById(id int) (models.OfficeHour, error) {
return r.getFromRow(r.db.QueryRow("SELECT * FROM officeHour WHERE id=?", id)) return r.getFromRow(r.db.QueryRow("SELECT * FROM officeHour WHERE id=?", id))
} }
// Add an office hour if it doesn't exist yet.
// Also add the incluyey tutor if it doesn't exist yet.
//
// Returns the id of the new office hour.
func (r *OfficeHourRepo) Add(officeHour models.OfficeHour) (int, error) { func (r *OfficeHourRepo) Add(officeHour models.OfficeHour) (int, error) {
// Find correct tutor or add if not existent // Find correct tutor or add if not existent
_, err := r.tutorRepo.Add(officeHour.Tutor) _, err := r.tutorRepo.Add(officeHour.Tutor)
@ -215,6 +219,7 @@ func (r *OfficeHourRepo) getFromRows(rows *sql.Rows) ([]models.OfficeHour, error
return officeHours, nil return officeHours, nil
} }
// Get the number of office hours that are maximally parallel during a time span.
func (r *OfficeHourRepo) NumberByTimeSpanAndRoom(date models.Date, duration int, room models.Room, activeOnly bool) (int, error) { func (r *OfficeHourRepo) NumberByTimeSpanAndRoom(date models.Date, duration int, room models.Room, activeOnly bool) (int, error) {
var rows *sql.Rows var rows *sql.Rows
var err error var err error
@ -252,6 +257,7 @@ func (r *OfficeHourRepo) NumberByTimeSpanAndRoom(date models.Date, duration int,
return count, nil return count, nil
} }
// Check whether the room capacity allows for another office hour during a time span.
func (r *OfficeHourRepo) AllowedAt(date models.Date, duration int, room models.Room, activeOnly bool) (bool, error) { func (r *OfficeHourRepo) AllowedAt(date models.Date, duration int, room models.Room, activeOnly bool) (bool, error) {
numberOfOfficeHours, err := r.NumberByTimeSpanAndRoom(date, duration, room, activeOnly) numberOfOfficeHours, err := r.NumberByTimeSpanAndRoom(date, duration, room, activeOnly)
if err != nil { if err != nil {

View file

@ -68,6 +68,8 @@ func (r *RequestRepo) FindByOfficeHour(officeHour models.OfficeHour) ([]models.R
return requests, nil return requests, nil
} }
// Add a request to the database if it doesnt already exist.
// Send a mail with the secret to the confirmation address in any case.
func (r *RequestRepo) Add(officeHour models.OfficeHour, action int) (int, error) { func (r *RequestRepo) Add(officeHour models.OfficeHour, action int) (int, error) {
existents, err := r.FindByOfficeHour(officeHour) existents, err := r.FindByOfficeHour(officeHour)
if err != nil && !errors.Is(err, sql.ErrNoRows) { if err != nil && !errors.Is(err, sql.ErrNoRows) {
@ -97,6 +99,7 @@ func (r *RequestRepo) Add(officeHour models.OfficeHour, action int) (int, error)
return request.Id, r.sendConfirmationMail(request) return request.Id, r.sendConfirmationMail(request)
} }
// Execute a request and delete it.
func (r *RequestRepo) Execute(request models.Request) error { func (r *RequestRepo) Execute(request models.Request) error {
var err error var err error
switch request.Action { switch request.Action {
@ -107,11 +110,13 @@ func (r *RequestRepo) Execute(request models.Request) error {
err = r.officeHourRepo.Delete(request.OfficeHour) err = r.officeHourRepo.Delete(request.OfficeHour)
r.db.Exec("DELETE FROM request WHERE officeHour=?", request.OfficeHour.Id) r.db.Exec("DELETE FROM request WHERE officeHour=?", request.OfficeHour.Id)
default: default:
log.Printf("Executing request: Action type %d unknown.", request.Action)
_, err = r.db.Exec("DELETE FROM request WHERE id=?", request.Id) _, err = r.db.Exec("DELETE FROM request WHERE id=?", request.Id)
} }
return err return err
} }
// Find a new secret token with configured length that is currently unused.
func (r *RequestRepo) newSecret() (string, error) { func (r *RequestRepo) newSecret() (string, error) {
var err error var err error
var secret string var secret string

View file

@ -1,4 +1,3 @@
// raum
package repositories package repositories
import ( import (

View file

@ -9,6 +9,7 @@ import (
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
) )
// Connect to a database using or throw an error
func Connect(config config.Config) (*sql.DB, error) { func Connect(config config.Config) (*sql.DB, error) {
switch config.SQL.Type { switch config.SQL.Type {
case "SQLite": case "SQLite":
@ -24,6 +25,7 @@ func Connect(config config.Config) (*sql.DB, error) {
} }
} }
// Connect to a SQLite database and check the connection.
func connectSQLite(file string) (*sql.DB, error) { func connectSQLite(file string) (*sql.DB, error) {
db, err := sql.Open("sqlite3", file) db, err := sql.Open("sqlite3", file)
if err != nil { if err != nil {
@ -32,6 +34,7 @@ func connectSQLite(file string) (*sql.DB, error) {
return db, nil return db, nil
} }
// Connect to a Mysql database and check the connection.
func connectMysql(user string, password string, address string, port int, database string) (*sql.DB, error) { func connectMysql(user string, password string, address string, port int, database string) (*sql.DB, error) {
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", user, password, address, port, database)) db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", user, password, address, port, database))
if err != nil { if err != nil {
@ -39,7 +42,6 @@ func connectMysql(user string, password string, address string, port int, databa
} }
err = db.Ping() err = db.Ping()
// handle error
if err != nil { if err != nil {
return db, fmt.Errorf("Error pinging Mysql database: %w", err) return db, fmt.Errorf("Error pinging Mysql database: %w", err)
} }