Selaa lähdekoodia

Remove submodules

Andrea Fazzi 5 vuotta sitten
vanhempi
commit
092beeb9f7

+ 613 - 0
client/client.go

@@ -0,0 +1,613 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strconv"
+
+	"gogs.carducci-dante.gov.it/karmen/core/orm"
+	"gogs.carducci-dante.gov.it/karmen/core/renderer"
+)
+
+// A client represents a client connection to the Headmaster test
+// server.
+type Client struct {
+	Url      *url.URL
+	Username string
+	Password string
+	User     string
+
+	token string
+}
+
+// Dial connects to a test server instance at the specified address
+// using the given credentials.
+func Dial(host, username, password string) (*Client, error) {
+	url, err := url.Parse(host)
+	if err != nil {
+		return nil, err
+	}
+
+	client := &Client{
+		Url:      url,
+		Username: username,
+		Password: password,
+	}
+
+	response, err := client.SendRequest("GET", "get_token", nil)
+	if err != nil {
+		panic(err)
+	}
+
+	var data struct {
+		Token string
+		User  string
+	}
+	if err := json.Unmarshal(response, &data); err != nil {
+		panic(err)
+	}
+	client.token = data.Token
+	client.User = data.User
+
+	return client, nil
+}
+
+func (c *Client) SendRequest(method string, path string, data []byte) ([]byte, error) {
+	// Create the https request
+
+	folderUrl, err := url.Parse(path)
+	if err != nil {
+		return nil, err
+	}
+
+	client := &http.Client{}
+	req, err := http.NewRequest(method, c.Url.ResolveReference(folderUrl).String(), bytes.NewReader(data))
+	if err != nil {
+		return nil, err
+	}
+
+	req.Header.Set("Content-Type", "application/json")
+	req.SetBasicAuth(c.Username, c.Password)
+
+	if c.token != "" {
+		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token))
+	}
+
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	return body, nil
+}
+
+func (c *Client) GetTeachers() ([]*orm.Teacher, error) {
+	var (
+		response renderer.JsonResponse
+		teachers []*orm.Teacher
+	)
+
+	data, err := c.SendRequest("GET", "/api/teachers?format=json", nil)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := json.Unmarshal(data, &response); err != nil {
+		return nil, err
+	}
+
+	if string(response.Error) != "" {
+		return nil, errors.New(string(response.Error))
+	}
+
+	if err := json.Unmarshal(response.Result, &teachers); err != nil {
+		return nil, err
+	}
+	return teachers, nil
+}
+
+func (c *Client) GetAdministratives() ([]*orm.Administrative, error) {
+	var (
+		response        renderer.JsonResponse
+		administratives []*orm.Administrative
+	)
+
+	data, err := c.SendRequest("GET", "/api/administratives?format=json", nil)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := json.Unmarshal(data, &response); err != nil {
+		return nil, err
+	}
+
+	if string(response.Error) != "" {
+		return nil, errors.New(string(response.Error))
+	}
+
+	if err := json.Unmarshal(response.Result, &administratives); err != nil {
+		return nil, err
+	}
+	return administratives, nil
+}
+
+func (c *Client) GetTeacher(id uint) (*orm.Teacher, error) {
+	var (
+		response renderer.JsonResponse
+		teacher  *orm.Teacher
+	)
+
+	data, err := c.SendRequest("GET", fmt.Sprintf("/api/teachers/%d?format=json", id), nil)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := json.Unmarshal(data, &response); err != nil {
+		return nil, err
+	}
+
+	if string(response.Error) != "" {
+		return nil, errors.New(string(response.Error))
+	}
+
+	if err := json.Unmarshal(response.Result, &teacher); err != nil {
+		return nil, err
+	}
+	return teacher, nil
+}
+
+func (c *Client) GetJob(id uint) (*orm.Job, error) {
+	var (
+		response renderer.JsonResponse
+		job      *orm.Job
+	)
+
+	data, err := c.SendRequest("GET", fmt.Sprintf("/api/jobs/%d?format=json", id), nil)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := json.Unmarshal(data, &response); err != nil {
+		return nil, err
+	}
+
+	if string(response.Error) != "" {
+		return nil, errors.New(string(response.Error))
+	}
+
+	if err := json.Unmarshal(response.Result, &job); err != nil {
+		return nil, err
+	}
+	return job, nil
+}
+
+func (c *Client) GetStudents() ([]*orm.Student, error) {
+	var (
+		response renderer.JsonResponse
+		students []*orm.Student
+	)
+
+	data, err := c.SendRequest("GET", "/api/students?format=json", nil)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := json.Unmarshal(data, &response); err != nil {
+		return nil, err
+	}
+
+	if string(response.Error) != "" {
+		return nil, errors.New(string(response.Error))
+	}
+
+	if err := json.Unmarshal(response.Result, &students); err != nil {
+		return nil, err
+	}
+	return students, nil
+}
+
+func (c *Client) GetSubjects() ([]*orm.Subject, error) {
+	var (
+		response renderer.JsonResponse
+		subjects []*orm.Subject
+	)
+
+	data, err := c.SendRequest("GET", "/api/subjects?format=json", nil)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := json.Unmarshal(data, &response); err != nil {
+		return nil, err
+	}
+
+	if string(response.Error) != "" {
+		return nil, errors.New(string(response.Error))
+	}
+
+	if err := json.Unmarshal(response.Result, &subjects); err != nil {
+		return nil, err
+	}
+	return subjects, nil
+}
+
+func (c *Client) AddSubject(subject *orm.Subject) error {
+	var response renderer.JsonResponse
+
+	data, err := json.Marshal(subject)
+	if err != nil {
+		return err
+	}
+	resp, err := c.SendRequest("POST", "/api/subjects/add/?format=json", data)
+	if err != nil {
+		return err
+	}
+	if err := json.Unmarshal(resp, &response); err != nil {
+		return err
+	}
+	if string(response.Error) != "" {
+		return errors.New(string(response.Error))
+	}
+	return nil
+}
+
+func (c *Client) GetDepartments() ([]*orm.Department, error) {
+	var (
+		response    renderer.JsonResponse
+		departments []*orm.Department
+	)
+
+	data, err := c.SendRequest("GET", "/api/departments?format=json", nil)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := json.Unmarshal(data, &response); err != nil {
+		return nil, err
+	}
+
+	if string(response.Error) != "" {
+		return nil, errors.New(string(response.Error))
+	}
+
+	if err := json.Unmarshal(response.Result, &departments); err != nil {
+		return nil, err
+	}
+	return departments, nil
+}
+
+func (c *Client) GetOffices() ([]*orm.Office, error) {
+	var (
+		response renderer.JsonResponse
+		offices  []*orm.Office
+	)
+
+	data, err := c.SendRequest("GET", "/api/offices?format=json", nil)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := json.Unmarshal(data, &response); err != nil {
+		return nil, err
+	}
+
+	if string(response.Error) != "" {
+		return nil, errors.New(string(response.Error))
+	}
+
+	if err := json.Unmarshal(response.Result, &offices); err != nil {
+		return nil, err
+	}
+	return offices, nil
+}
+
+func (c *Client) GetClasses() ([]*orm.Class, error) {
+	var (
+		response renderer.JsonResponse
+		classes  []*orm.Class
+	)
+
+	data, err := c.SendRequest("GET", "/api/classes?format=json", nil)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := json.Unmarshal(data, &response); err != nil {
+		return nil, err
+	}
+
+	if string(response.Error) != "" {
+		return nil, errors.New(string(response.Error))
+	}
+
+	if err := json.Unmarshal(response.Result, &classes); err != nil {
+		return nil, err
+	}
+	return classes, nil
+}
+
+func (c *Client) GetClass(id uint) (*orm.Class, error) {
+	var (
+		response renderer.JsonResponse
+		teacher  *orm.Class
+	)
+
+	data, err := c.SendRequest("GET", fmt.Sprintf("/api/classes/%d?format=json", id), nil)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := json.Unmarshal(data, &response); err != nil {
+		return nil, err
+	}
+
+	if string(response.Error) != "" {
+		return nil, errors.New(string(response.Error))
+	}
+
+	if err := json.Unmarshal(response.Result, &teacher); err != nil {
+		return nil, err
+	}
+	return teacher, nil
+}
+
+func (c *Client) AddClass(class *orm.Class) error {
+	var response renderer.JsonResponse
+
+	data, err := json.Marshal(class)
+	if err != nil {
+		return err
+	}
+	resp, err := c.SendRequest("POST", "/api/classes/add/?format=json", data)
+	if err != nil {
+		return err
+	}
+	if err := json.Unmarshal(resp, &response); err != nil {
+		return err
+	}
+	if string(response.Error) != "" {
+		return errors.New(string(response.Error))
+	}
+	return nil
+}
+
+func (c *Client) GetActivities() ([]*orm.Activity, error) {
+	var (
+		response   renderer.JsonResponse
+		activities []*orm.Activity
+	)
+
+	data, err := c.SendRequest("GET", "/api/activities?format=json", nil)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := json.Unmarshal(data, &response); err != nil {
+		return nil, err
+	}
+
+	if string(response.Error) != "" {
+		return nil, errors.New(string(response.Error))
+	}
+
+	if err := json.Unmarshal(response.Result, &activities); err != nil {
+		return nil, err
+	}
+	return activities, nil
+}
+
+func (c *Client) AddStudent(student *orm.Student) (uint, error) {
+	var response renderer.JsonResponse
+
+	data, err := json.Marshal(student)
+	if err != nil {
+		return 0, err
+	}
+	resp, err := c.SendRequest("POST", "/api/students/add/?format=json", data)
+	if err != nil {
+		return 0, err
+	}
+	if err := json.Unmarshal(resp, &response); err != nil {
+		return 0, err
+	}
+	if string(response.Error) != "" {
+		return 0, errors.New(string(response.Error))
+	}
+
+	id, err := strconv.Atoi(string(response.Result))
+	if err != nil {
+		return 0, err
+	}
+	return uint(id), nil
+}
+
+func (c *Client) AddActivity(activity *orm.Activity) (uint, error) {
+	var response renderer.JsonResponse
+	data, err := json.Marshal(activity)
+	if err != nil {
+		return 0, err
+	}
+	resp, err := c.SendRequest("POST", "/api/activities/add/?format=json", data)
+	if err != nil {
+		return 0, err
+	}
+
+	if err := json.Unmarshal(resp, &response); err != nil {
+		return 0, err
+	}
+	if string(response.Error) != "" {
+		return 0, errors.New(string(response.Error))
+	}
+
+	id, err := strconv.Atoi(string(response.Result))
+	if err != nil {
+		return 0, err
+	}
+
+	return uint(id), nil
+}
+
+func (c *Client) DeleteActivity(activity *orm.Activity) (uint, error) {
+	var response renderer.JsonResponse
+	data, err := json.Marshal(activity)
+	if err != nil {
+		return 0, err
+	}
+	resp, err := c.SendRequest("DELETE", fmt.Sprintf("/api/activities/%d/delete?format=json", activity.ID), data)
+	if err != nil {
+		return 0, err
+	}
+
+	if err := json.Unmarshal(resp, &response); err != nil {
+		return 0, err
+	}
+	if string(response.Error) != "" {
+		return 0, errors.New(string(response.Error))
+	}
+
+	id, err := strconv.Atoi(string(response.Result))
+	if err != nil {
+		return 0, err
+	}
+
+	return uint(id), nil
+}
+
+func (c *Client) UpdateActivity(activity *orm.Activity) error {
+	data, err := json.Marshal(activity)
+	if err != nil {
+		return err
+
+	}
+	_, err = c.SendRequest("POST", fmt.Sprintf("/api/activities/%d/update?format=json", activity.ID), data)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *Client) UpdateStudent(student *orm.Student) error {
+	data, err := json.Marshal(student)
+	if err != nil {
+		return err
+
+	}
+	_, err = c.SendRequest("POST", fmt.Sprintf("/api/students/%d/update?format=json", student.ID), data)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *Client) UpdateTeacher(teacher *orm.Teacher) error {
+	var response renderer.JsonResponse
+
+	data, err := json.Marshal(teacher)
+	if err != nil {
+		return err
+	}
+	resp, err := c.SendRequest("POST", fmt.Sprintf("/api/teachers/%d/update?format=json", teacher.ID), data)
+	if err != nil {
+		return err
+	}
+	if err := json.Unmarshal(resp, &response); err != nil {
+		return err
+	}
+	if string(response.Error) != "" {
+		return errors.New(string(response.Error))
+	}
+	return nil
+}
+
+func (c *Client) UpdateJob(job *orm.Job) error {
+	var response renderer.JsonResponse
+
+	data, err := json.Marshal(job)
+	if err != nil {
+		return err
+	}
+	resp, err := c.SendRequest("POST", fmt.Sprintf("/api/jobs/%d/update?format=json", job.ID), data)
+	if err != nil {
+		return err
+	}
+	if err := json.Unmarshal(resp, &response); err != nil {
+		return err
+	}
+	if string(response.Error) != "" {
+		return errors.New(string(response.Error))
+	}
+	return nil
+}
+
+func (c *Client) UpdateUser(user orm.User) error {
+	var response renderer.JsonResponse
+
+	data, err := json.Marshal(user)
+	if err != nil {
+		return err
+	}
+	resp, err := c.SendRequest("POST", fmt.Sprintf("/api/%s/%d/update?format=json", user.RestAPIPath(), user.GetID()), data)
+	if err != nil {
+		return err
+	}
+	if err := json.Unmarshal(resp, &response); err != nil {
+		return err
+	}
+	if string(response.Error) != "" {
+		return errors.New(string(response.Error))
+	}
+	return nil
+}
+
+func (c *Client) AddTeacher(teacher *orm.Teacher) error {
+	var response renderer.JsonResponse
+
+	data, err := json.Marshal(teacher)
+	if err != nil {
+		return err
+	}
+	resp, err := c.SendRequest("POST", "/api/teachers/add/?format=json", data)
+	if err != nil {
+		return err
+	}
+	if err := json.Unmarshal(resp, &response); err != nil {
+		return err
+	}
+	if string(response.Error) != "" {
+		return errors.New(string(response.Error))
+	}
+	return nil
+}
+
+func (c *Client) GetGroups() ([]*orm.Group, error) {
+	var (
+		response renderer.JsonResponse
+		groups   []*orm.Group
+	)
+
+	data, err := c.SendRequest("GET", "/api/groups?format=json", nil)
+	if err != nil {
+		return nil, err
+	}
+	if err := json.Unmarshal(data, &response); err != nil {
+		return nil, err
+	}
+
+	if string(response.Error) != "" {
+		return nil, errors.New(string(response.Error))
+	}
+	if err := json.Unmarshal(response.Result, &groups); err != nil {
+		return nil, err
+	}
+	return groups, nil
+}

+ 366 - 0
ldap/ldap.go

@@ -0,0 +1,366 @@
+package ldap
+
+import (
+	"errors"
+	"fmt"
+	"sort"
+	"strconv"
+
+	"gogs.carducci-dante.gov.it/karmen/core/config"
+	"gogs.carducci-dante.gov.it/karmen/core/orm"
+	ldap "gopkg.in/ldap.v2"
+)
+
+type DNer interface {
+	DN() string
+}
+
+type CompleteNameI interface {
+	CompleteName() string
+}
+
+type Client struct {
+	Conn   *ldap.Conn
+	Config *config.ConfigT
+}
+
+func (c *Client) Close() {
+	c.Conn.Close()
+}
+
+func NewClient(host string, config *config.ConfigT) (*Client, error) {
+	var err error
+
+	client := new(Client)
+	client.Config = config
+
+	client.Conn, err = ldap.Dial("tcp", host)
+	if err != nil {
+		return nil, err
+	}
+
+	err = client.Conn.Bind(config.AdminCN(), config.Ldap.AdminPassword)
+	if err != nil {
+		return nil, err
+	}
+	return client, nil
+}
+
+func (c *Client) AddUser(user orm.User) error {
+	mailDir := fmt.Sprintf("%s/%s/", c.Config.Ldap.MailDirBasePath, user.Username())
+	uidNumber, err := c.NextMailUIDNumber()
+	if err != nil {
+		return err
+	}
+	addRequest := ldap.NewAddRequest(user.DN())
+	addRequest.Attribute("objectClass", []string{
+		"inetOrgPerson",
+		"posixAccount",
+		"PostfixBookMailAccount",
+		"organizationalPerson",
+		"extensibleObject",
+	})
+	addRequest.Attribute("sn", []string{user.GetSurname()})
+	addRequest.Attribute("uid", []string{user.Username()})
+	addRequest.Attribute("homeDirectory", []string{fmt.Sprintf("/home/users/%s", user.Username())})
+	addRequest.Attribute("givenName", []string{user.GetSurname()})
+	addRequest.Attribute("mail", []string{fmt.Sprintf("%s@%s", user.Username(), c.Config.Domain)})
+	addRequest.Attribute("mailEnabled", []string{"TRUE"})
+	addRequest.Attribute("mailGidNumber", []string{c.Config.Ldap.MailGIDNumber})
+	addRequest.Attribute("mailUidNumber", []string{uidNumber})
+	addRequest.Attribute("uidNumber", []string{uidNumber})
+	addRequest.Attribute("gidNumber", []string{uidNumber})
+	addRequest.Attribute("uniqueIdentifier", []string{user.Username()})
+	addRequest.Attribute("mailHomeDirectory", []string{mailDir})
+	addRequest.Attribute("mailStorageDirectory", []string{"maildir:" + mailDir})
+	addRequest.Attribute("mailQuota", []string{c.Config.Ldap.MailQuota})
+	addRequest.Attribute("userPassword", []string{fmt.Sprintf("{SSHA}%s", orm.SaltPassword(user.GetPlainPassword()))})
+
+	err = c.Conn.Add(addRequest)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (c *Client) UpdateUser(user orm.User) error {
+	mailDir := fmt.Sprintf("%s/%s/", c.Config.Ldap.MailDirBasePath, user.Username())
+	modRequest := ldap.NewModifyRequest(user.DN())
+	modRequest.Replace("mail", []string{fmt.Sprintf("%s@%s", user.Username(), c.Config.Domain)})
+	modRequest.Replace("mailHomeDirectory", []string{mailDir})
+	modRequest.Replace("mailStorageDirectory", []string{"maildir:" + mailDir})
+	modRequest.Replace("mailEnabled", []string{"TRUE"})
+	modRequest.Replace("mailGidNumber", []string{c.Config.Ldap.MailGIDNumber})
+	modRequest.Replace("mailQuota", []string{c.Config.Ldap.MailQuota})
+
+	err := c.Conn.Modify(modRequest)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (c *Client) UpdateUserPassword(user orm.User) error {
+	modRequest := ldap.NewModifyRequest(user.DN())
+	modRequest.Replace("userPassword", []string{fmt.Sprintf("{SSHA}%s", orm.SaltPassword(user.GetPlainPassword()))})
+	err := c.Conn.Modify(modRequest)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *Client) DeleteUser(user orm.User) error {
+	delRequest := ldap.NewDelRequest(user.DN(), nil)
+	err := c.Conn.Del(delRequest)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (c *Client) DeleteByDN(dn string) error {
+	delRequest := ldap.NewDelRequest(dn, nil)
+	err := c.Conn.Del(delRequest)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (c *Client) AddUserToGroup(user orm.User, groupDN string) error {
+	memberAttr, err := c.memberAttribute(groupDN)
+	if err != nil {
+		return err
+	}
+	modRequest := ldap.NewModifyRequest(fmt.Sprintf("%s,%s", groupDN, c.GroupsDN()))
+	switch memberAttr {
+	case "member":
+		modRequest.Add(memberAttr, []string{user.DN()})
+	case "memberuid":
+		modRequest.Add(memberAttr, []string{user.Username()})
+	default:
+		return errors.New(fmt.Sprintf("Attribute %s is not supported!", memberAttr))
+	}
+	err = c.Conn.Modify(modRequest)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (c *Client) RemoveUserFromGroup(user *orm.Credential, groupDN string) error {
+	memberAttr, err := c.memberAttribute(groupDN)
+	if err != nil {
+		return err
+	}
+	modRequest := ldap.NewModifyRequest(fmt.Sprintf("%s,%s", groupDN, c.GroupsDN()))
+	switch memberAttr {
+	case "member":
+		modRequest.Delete(memberAttr, []string{user.DN()})
+	case "memberuid":
+		modRequest.Delete(memberAttr, []string{user.Username()})
+	default:
+		return errors.New(fmt.Sprintf("Attribute %s is not supported!", memberAttr))
+	}
+	err = c.Conn.Modify(modRequest)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (c *Client) RemoveUserFromGroupByMemberValue(memberValue string, groupDN string) error {
+	memberAttr, err := c.memberAttribute(groupDN)
+	if err != nil {
+		return err
+	}
+	modRequest := ldap.NewModifyRequest(fmt.Sprintf("%s,%s", groupDN, c.GroupsDN()))
+	switch memberAttr {
+	case "member":
+		modRequest.Delete(memberAttr, []string{memberValue})
+	case "memberuid":
+		modRequest.Delete(memberAttr, []string{memberValue})
+	default:
+		return errors.New(fmt.Sprintf("Attribute %s is not supported!", memberAttr))
+	}
+	err = c.Conn.Modify(modRequest)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (c *Client) IsUserInGroup(user *orm.Credential, groupDN string) (bool, error) {
+	memberAttr, err := c.memberAttribute(groupDN)
+	if err != nil {
+		return false, err
+	}
+	var memberValue string
+	switch memberAttr {
+	case "member":
+		memberValue = user.DN()
+	case "memberuid":
+		memberValue = user.Username()
+	default:
+		return false, errors.New(fmt.Sprintf("Attribute %s is not supported!", memberAttr))
+	}
+
+	searchRequest := ldap.NewSearchRequest(
+		fmt.Sprintf("%s,%s", groupDN, c.GroupsDN()),
+		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
+		fmt.Sprintf("(&(%s=%s))", memberAttr, memberValue),
+		[]string{memberAttr},
+		nil,
+	)
+
+	sr, err := c.Conn.Search(searchRequest)
+	if err != nil {
+		return false, err
+	}
+
+	return len(sr.Entries) > 0, nil
+}
+
+func (c *Client) GroupMembers(groupDN string) ([]*ldap.Entry, error) {
+	searchRequest := ldap.NewSearchRequest(
+		fmt.Sprintf("%s,%s", groupDN, c.GroupsDN()),
+		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
+		"(|(member=*)(memberuid=*))",
+		[]string{"member", "memberuid"},
+		nil,
+	)
+	sr, err := c.Conn.Search(searchRequest)
+	if err != nil {
+		return nil, err
+	}
+
+	return sr.Entries, nil
+}
+
+func (c *Client) Users(searchDN string) ([]*ldap.Entry, error) {
+	result, err := c.Search(
+		searchDN,
+		"(&(objectClass=organizationalPerson))",
+		[]string{"dn", "cn"},
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	return result.Entries, nil
+}
+
+func (c *Client) Search(base string, filter string, attributes []string) (*ldap.SearchResult, error) {
+	searchRequest := ldap.NewSearchRequest(
+		base,
+		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
+		filter,
+		attributes,
+		nil,
+	)
+
+	sr, err := c.Conn.Search(searchRequest)
+	if err != nil {
+		return nil, err
+	}
+
+	return sr, nil
+}
+
+func (c *Client) UserExists(user *orm.Credential, searchDN string) (bool, error) {
+	searchRequest := ldap.NewSearchRequest(
+		searchDN,
+		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
+		fmt.Sprintf("(&(objectClass=organizationalPerson)(cn=%s))", user.CompleteName()),
+
+		[]string{"dn", "cn"},
+		nil,
+	)
+
+	sr, err := c.Conn.Search(searchRequest)
+	if err != nil {
+		return false, err
+	}
+
+	return len(sr.Entries) > 0, nil
+}
+
+func (c *Client) DomainDN() string {
+	return c.Config.DomainDN()
+}
+
+func (c *Client) GroupsDN() string {
+	return fmt.Sprintf("%s,%s", c.Config.Ldap.GroupsDN, c.DomainDN())
+}
+
+func (c *Client) PeopleDN() string {
+	return fmt.Sprintf("%s,%s", c.Config.Ldap.PeopleDN, c.DomainDN())
+}
+
+func (c *Client) NextMailUIDNumber() (string, error) {
+	searchRequest := ldap.NewSearchRequest(
+		c.PeopleDN(),
+		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
+		"(mailUidNumber=*)",
+		[]string{"mailUidNumber"},
+		nil,
+	)
+	sr, err := c.Conn.Search(searchRequest)
+	if err != nil {
+		return "0", err
+	}
+
+	if len(sr.Entries) == 0 {
+		return c.Config.Ldap.FirstUIDNumber, nil
+	}
+
+	var uids []int
+
+	for _, e := range sr.Entries {
+		n, err := strconv.Atoi(e.Attributes[0].Values[0])
+		uids = append(uids, n)
+		if err != nil {
+			return "0", err
+		}
+	}
+
+	sort.Ints(uids)
+
+	nextUid := uids[len(uids)-1] + 1
+
+	return strconv.Itoa(nextUid), nil
+}
+
+func (c *Client) memberAttribute(groupDN string) (string, error) {
+	searchRequest := ldap.NewSearchRequest(
+		fmt.Sprintf("%s,%s", groupDN, c.GroupsDN()),
+		ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
+		"(member=*)",
+		[]string{"member"},
+		nil,
+	)
+
+	sr, err := c.Conn.Search(searchRequest)
+	if err != nil {
+		return "", err
+	}
+
+	if len(sr.Entries) > 0 {
+		return "member", nil
+	}
+
+	return "memberuid", nil
+}
+
+func (c *Client) personDN(person CompleteNameI, baseDN string) string {
+	dn := fmt.Sprintf("cn=%s,%s", person.CompleteName(), baseDN)
+	return dn
+}

+ 1 - 1
templates/activities.html.tpl

@@ -17,7 +17,7 @@
     {{range $activity := .Data}}
     <a class="list-group-item list-group-item-action" href={{$activity.ID|show "activity"}}>
       <span class="fa fa-business-time"></span>
-      {{$activity.Name}}
+      {{$activity|string}}
       <div class="text-right">
         {{$options := `noElements: "no docente"`}}
         {{template "small" dict "options" ($options | yaml) "data" $activity.Teacher}}

+ 33 - 103
templates/activities_show.html.tpl

@@ -1,131 +1,61 @@
 {{ define "content" }}
 
 <div class="container">
-
-  <nav aria-label="breadcrumb">
-    <ol class="breadcrumb">
-      <li class="breadcrumb-item"><a href="/activities?{{query "tpl_layout" "base" "tpl_content" "activities"}}">Attività</a></li>
-      <li class="breadcrumb-item active"><a href="#">{{if .Data.Subject}}{{.Data.Subject.Name}}{{else}}no materia{{end}} {{if .Data.Class}}{{.Data.Class.Name}}{{else}}no classe{{end}} {{.Data.Hours}}h</a></li>
-    </ol>
-  </nav>
   
-  <div class="karmen-info-header">
-    <div class="row">
-      <div class="col-md-8">
-	<h1>{{if .Data.Subject}}{{.Data.Subject.Name}}{{else}}no materia{{end}} {{if .Data.Class}}{{.Data.Class.Name}}{{else}}no classe{{end}} {{.Data.Hours}}h</h1>
-      </div>
-      <div class="col-md-4">
-
-	<div class="btn-group float-right" role="group">
-	  <a href="/activities/add/?{{query "tpl_layout" "base" "tpl_content" "activities_add_update"}}" class="btn btn-success">
-	    <span class="fa fa-plus-circle" aria-hidden="true"></span>
-	    Crea
-	  </a>
-
-	  <a href="/activities/{{.Data.ID}}/update?{{query "tpl_layout" "base" "tpl_content" "activities_add_update" "update" "true"}}" class="btn btn-primary">
-	    <span class="fa fa-edit" aria-hidden="true"></span>
-	    Modifica
-	  </a>
-	  <button href="/activities/{{.Data.ID}}/delete"
-		  data-url="/activities/{{.Data.ID}}/delete"
-		  class="btn btn-danger karmen-ajax-delete">
-	    <span class="fa fa-trash" aria-hidden="true"></span>
-	    Elimina
-	  </button>
-	</div>
-
-      </div>
-    </div>
-  </div>
+  {{template "breadcrumb" toSlice "Attività" (all "Activity") (.Data|string) "current"}}
+  {{template "show_header" dict "title" (.Data|string) "updatePath" (.Data.ID|update "Activity") "deletePath" (.Data.ID|delete "Activity")}}
 
   <div class="row">
     <div class="col-md-12">
-      
-      <h2 class="karmen-relation-header">Materia</h2>
-      {{if .Data.Subject}}
-      <div class="list-group" id="materie_list_group">
-    	<a href="/subjects/{{.Data.Subject.ID}}?{{query "tpl_layout" "base" "tpl_content" "subjects_show"}}" class="list-group-item list-group-item-action">
-    	  <span class="fa fa-book"></span>
-    	  {{.Data.Subject.Name}}
-    	</a>
-      </div>
-      {{else}}
-      <p>All'attività non è associata alcuna materia
-    	didattica. Clicca <a href="/activities/update?{{query "tpl_layout" "base" "tpl_content" "activities_add_update"}}">qui</a> per
-    	modificare questa attività didattica.</p>
-      {{end}}
+      {{$options := `
+      title: "Materia"
+      model: "Subject"
+      icon: "fa fa-book"
+      `}}
+      {{$noElements := "Nessuna materia associata all'attività"}}
+      {{template "relation_list" dict "options" ($options|yaml) "data" .Data.Subject "noElements" $noElements}}
     </div>
-    
   </div>
 
   <div class="row">
     <div class="col-md-12">
-      
-      <h2 class="karmen-relation-header">Classe</h2>
-      {{if .Data.Class}}
-      <div class="list-group" id="classes_list_group">
-
-	<a href="/classes/{{.Data.Class.ID}}?{{query "tpl_layout" "base" "tpl_content" "classes_show"}}" class="list-group-item list-group-item-action">
-	  <span class="fa fa-users"></span>
-	  {{.Data.Class.Name}}
-	</a>
-	
-      </div>
-      {{else}}
-      
-      <p>All'attività non è associata alcuna classe
-    	didattica. Clicca <a href="/activities/update?{{query "tpl_layout" "base" "tpl_content" "activities_add_update"}}">qui</a> per
-    	modificare questa attività didattica.</p>
-      
-      {{end}}
+      {{$options := `
+      title: "Docente"
+      model: "Teacher"
+      icon: "fa fa-user"
+      `}}
+      {{$noElements := "Nessun docente associato all'attività"}}
+      {{template "relation_list" dict "options" ($options|yaml) "data" .Data.Teacher "noElements" $noElements}}
     </div>
-    
   </div>
 
   <div class="row">
     <div class="col-md-12">
-      
-      <h2 class="karmen-relation-header">Docente</h2>
-      {{if .Data.Teacher}}
-      <div class="list-group" id="classes_list_group">
-	<a href="/teachers/{{.Data.Teacher.ID}}?{{query "tpl_layout" "base" "tpl_content" "teachers_show"}}" class="list-group-item list-group-item-action">
-	  <span class="fa fa-user"></span>
-	  {{.Data.Teacher.Surname}} {{.Data.Teacher.Name}}
-	</a>
-      </div>
-            
-      {{else}}
-
-      <p>All'attività non è associato alcun docente
-    	didattica. Clicca <a href="/activities/update?{{query "base" "activities_add_update"}}">qui</a> per
-    	modificare questa attività didattica.</p>
-	    
-      {{end}}
+      {{$options := `
+      title: "Classe"
+      model: "Class"
+      icon: "fa fa-users"
+      `}}
+      {{$noElements := "Nessuna classe associata all'attività"}}
+      {{template "relation_list" dict "options" ($options|yaml) "data" .Data.Class "noElements" $noElements}}
     </div>
   </div>
 
   <div class="row">
     <div class="col-md-12">
-      
-      <h2 class="karmen-relation-header">Studente</h2>
-      {{if .Data.Student}}
-      <div class="list-group" id="classes_list_group">
-	<a href="/teachers/{{.Data.Student.ID}}?{{query "tpl_layout" "base" "tpl_content" "students_show"}}" class="list-group-item list-group-item-action">
-	  <span class="fa fa-user"></span>
-	  {{.Data.Student.Surname}} {{.Data.Student.Name}}
-	</a>
-      </div>
-      
-      {{else}}
-
-      <p>All'attività non è associato alcuno studente. Clicca <a href="/activities/update?{{query "base" "activities_add_update"}}">qui</a> per modificare questa attività didattica.</p>
-     
-      {{end}}
-      
+      {{$options := `
+      title: "Studente"
+      model: "Student"
+      icon: "fa fa-user"
+      `}}
+      {{$noElements := "Nessuno studente associato all'attività"}}
+      {{template "relation_list" dict "options" ($options|yaml) "data" .Data.Student "noElements" $noElements}}
     </div>
   </div>
 
-
 </div>    
 
 {{ end }}
+
+
+

+ 27 - 0
templates/layout/relation_list.html.tpl

@@ -1,5 +1,7 @@
 {{define "relation_list"}}
 <h2 class="karmen-relation-header">{{$title := .options.title}}{{if .title}}{{$title = .title}}{{end}}{{$title}}</h2>
+{{if (.data|isSlice)}}
+
 {{if gt (len .data) 0}}
 <div class="list-group" id="{{$title|toLower}}_list_group">
   {{range $el := .data}}
@@ -19,5 +21,30 @@
 </div>
 {{else}}
 <p>{{.noElements}}</p>
+{{end}}
+
+{{else}}
+
+{{if .data}}
+<div class="list-group" id="{{$title|toLower}}_list_group">
+
+  <a href="{{.data.ID | show $.options.model}}" class="list-group-item list-group-item-action">
+    <span class="{{$.options.icon}}"></span>
+    {{.data | string}}
+    {{if .small}}
+    {{range $s := .small}}
+    <div class="text-right">
+      {{$options := `noElements: "nessun elemento"`}}
+      {{template "small" dict "options" ($options | yaml) "data" (.data|field $s)}}
+    </div>
+    {{end}}
+    {{end}}
+  </a>
+</div>
+
+{{else}}
+<p>{{.noElements}}</p>
+{{end}}
+
 {{end}}
 {{end}}

+ 10 - 0
util/fileutil/fileutil.go

@@ -0,0 +1,10 @@
+package fileutil
+
+import (
+	"path"
+	"strings"
+)
+
+func ReplaceExt(filename, ext string) string {
+	return strings.TrimSuffix(filename, path.Ext(filename)) + "." + ext
+}

+ 22 - 0
util/fileutil/fileutil_test.go

@@ -0,0 +1,22 @@
+package fileutil
+
+import (
+	"testing"
+
+	"github.com/remogatto/prettytest"
+)
+
+type testSuite struct {
+	prettytest.Suite
+}
+
+func TestRunner(t *testing.T) {
+	prettytest.Run(
+		t,
+		new(testSuite),
+	)
+}
+
+func (t *testSuite) TestReplaceExt() {
+	t.Equal("foo.odt", ReplaceExt("foo.md", "odt"))
+}

+ 18 - 0
util/libreoffice/libreoffice.go

@@ -0,0 +1,18 @@
+package libreoffice
+
+import "os/exec"
+
+func Convert(from, toExt string, options ...string) error {
+	libreoffice_args := []string{"--headless", "--convert-to", toExt}
+	libreoffice_args = append(libreoffice_args, options...)
+	libreoffice_args = append(libreoffice_args, from)
+
+	err := exec.Command("libreoffice", libreoffice_args...).Run()
+	// if string(out) != "" {
+	// 	return fmt.Errorf("libreoffice: %s", string(out))
+	// }
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 35 - 0
util/libreoffice/libreoffice_test.go

@@ -0,0 +1,35 @@
+package libreoffice
+
+import (
+	"os"
+	"testing"
+
+	"github.com/remogatto/prettytest"
+)
+
+type testSuite struct {
+	prettytest.Suite
+}
+
+func TestRunner(t *testing.T) {
+	prettytest.Run(
+		t,
+		new(testSuite),
+	)
+}
+
+func (t *testSuite) TestConvert() {
+	err := Convert("testdata/test.odt", "pdf", "--outdir", "testdata")
+	t.Nil(err)
+	_, err = os.Stat("testdata/test.pdf")
+	t.Nil(err)
+	err = os.Remove("testdata/test.pdf")
+	t.Nil(err)
+
+	err = Convert("testdata/test.csv", "ods", "--outdir", "testdata")
+	t.Nil(err)
+	_, err = os.Stat("testdata/test.ods")
+	t.Nil(err)
+	err = os.Remove("testdata/test.ods")
+	t.Nil(err)
+}

+ 21 - 0
util/pandoc/pandoc.go

@@ -0,0 +1,21 @@
+package pandoc
+
+import (
+	"fmt"
+	"os/exec"
+)
+
+func Convert(from, to string, options ...string) error {
+	pandoc_args := []string{"-o", to, "--data-dir", "."}
+	pandoc_args = append(pandoc_args, options...)
+	pandoc_args = append(pandoc_args, from)
+
+	out, err := exec.Command("pandoc", pandoc_args...).CombinedOutput()
+	if string(out) != "" {
+		return fmt.Errorf("pandoc: %s", string(out))
+	}
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 28 - 0
util/pandoc/pandoc_test.go

@@ -0,0 +1,28 @@
+package pandoc
+
+import (
+	"os"
+	"testing"
+
+	"github.com/remogatto/prettytest"
+)
+
+type testSuite struct {
+	prettytest.Suite
+}
+
+func TestRunner(t *testing.T) {
+	prettytest.Run(
+		t,
+		new(testSuite),
+	)
+}
+
+func (t *testSuite) TestConvert() {
+	err := Convert("testdata/test.md", "testdata/test.odt")
+	t.Nil(err)
+	_, err = os.Stat("testdata/test.odt")
+	t.Nil(err)
+	err = os.Remove("testdata/test.odt")
+	t.Nil(err)
+}

+ 29 - 0
util/template/html.go

@@ -0,0 +1,29 @@
+package template
+
+import (
+	"html/template"
+	"io/ioutil"
+)
+
+func LoadHTMLTemplate(filename string, funcMap ...template.FuncMap) (*template.Template, error) {
+	var (
+		tpl *template.Template
+		err error
+	)
+	content, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return nil, err
+	}
+	if len(funcMap) > 0 {
+		tpl, err = template.New(filename).Funcs(funcMap[0]).Parse(string(content))
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		tpl, err = template.New(filename).Parse(string(content))
+		if err != nil {
+			return nil, err
+		}
+	}
+	return tpl, nil
+}

+ 56 - 0
util/template/template.go

@@ -0,0 +1,56 @@
+package template
+
+import (
+	"crypto/sha1"
+	"fmt"
+	"io/ioutil"
+	"text/template"
+)
+
+func LoadTextTemplate(filename string, funcMap ...template.FuncMap) (*template.Template, error) {
+	var (
+		tpl *template.Template
+		err error
+	)
+	content, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return nil, err
+	}
+	if len(funcMap) > 0 {
+		tpl, err = template.New(filename).Funcs(funcMap[0]).Parse(string(content))
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		tpl, err = template.New(filename).Parse(string(content))
+		if err != nil {
+			return nil, err
+		}
+	}
+	return tpl, nil
+}
+
+func LoadTextTemplateFromString(content string, funcMap ...template.FuncMap) (*template.Template, error) {
+	var (
+		tpl *template.Template
+		err error
+	)
+
+	h := sha1.New()
+	h.Write([]byte(content))
+
+	name := fmt.Sprintf("%x", h.Sum(nil))
+
+	if len(funcMap) > 0 {
+		tpl, err = template.New(name).Funcs(funcMap[0]).Parse(content)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		tpl, err = template.New(name).Parse(content)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return tpl, nil
+}

+ 46 - 0
util/template/template_test.go

@@ -0,0 +1,46 @@
+package template
+
+import (
+	"strings"
+	"testing"
+	"text/template"
+
+	"github.com/remogatto/prettytest"
+)
+
+var funcMap = template.FuncMap{
+	"toUpper": toUpper,
+}
+
+type testSuite struct {
+	prettytest.Suite
+}
+
+func toUpper(s string) string {
+	return strings.ToUpper(s)
+}
+
+func TestRunner(t *testing.T) {
+	prettytest.Run(
+		t,
+		new(testSuite),
+	)
+}
+
+func (t *testSuite) TestLoadTXTTemplate() {
+	tpl, err := LoadTextTemplate("testdata/test.tpl")
+	t.Nil(err)
+	t.Not(t.Nil(tpl))
+}
+
+func (t *testSuite) TestLoadTXTTemplateWithFuncs() {
+	tpl, err := LoadTextTemplate("testdata/test.tpl", funcMap)
+	t.Nil(err)
+	t.Not(t.Nil(tpl))
+}
+
+func (t *testSuite) TestLoadTemplateFromString() {
+	tpl, err := LoadTextTemplateFromString("{{.}}")
+	t.Nil(err)
+	t.Not(t.Nil(tpl))
+}