Explorar el Código

Merge branch 'master' into gomodule

Andrea Fazzi hace 5 años
padre
commit
6761c58efd

+ 211 - 0
Godeps/Godeps.json

@@ -0,0 +1,211 @@
+{
+	"ImportPath": "gogs.carducci-dante.gov.it/karmen/core",
+	"GoVersion": "go1.12",
+	"GodepVersion": "v79",
+	"Deps": [
+		{
+			"ImportPath": "github.com/auth0/go-jwt-middleware",
+			"Rev": "5493cabe49f7bfa6e2ec444a09d334d90cd4e2bd"
+		},
+		{
+			"ImportPath": "github.com/dgrijalva/jwt-go",
+			"Comment": "v3.1.0",
+			"Rev": "dbeaa9332f19a944acb5736b4456cfcc02140e29"
+		},
+		{
+			"ImportPath": "github.com/go-sql-driver/mysql",
+			"Comment": "v1.3-49-gcd4cb90",
+			"Rev": "cd4cb909ce1a31435164be29bf3682031f61539a"
+		},
+		{
+			"ImportPath": "github.com/gobwas/glob",
+			"Comment": "v0.2.3-4-ge7a84e9",
+			"Rev": "e7a84e9525fe90abcda167b604e483cc959ad4aa"
+		},
+		{
+			"ImportPath": "github.com/gobwas/glob/compiler",
+			"Comment": "v0.2.3-4-ge7a84e9",
+			"Rev": "e7a84e9525fe90abcda167b604e483cc959ad4aa"
+		},
+		{
+			"ImportPath": "github.com/gobwas/glob/match",
+			"Comment": "v0.2.3-4-ge7a84e9",
+			"Rev": "e7a84e9525fe90abcda167b604e483cc959ad4aa"
+		},
+		{
+			"ImportPath": "github.com/gobwas/glob/syntax",
+			"Comment": "v0.2.3-4-ge7a84e9",
+			"Rev": "e7a84e9525fe90abcda167b604e483cc959ad4aa"
+		},
+		{
+			"ImportPath": "github.com/gobwas/glob/syntax/ast",
+			"Comment": "v0.2.3-4-ge7a84e9",
+			"Rev": "e7a84e9525fe90abcda167b604e483cc959ad4aa"
+		},
+		{
+			"ImportPath": "github.com/gobwas/glob/syntax/lexer",
+			"Comment": "v0.2.3-4-ge7a84e9",
+			"Rev": "e7a84e9525fe90abcda167b604e483cc959ad4aa"
+		},
+		{
+			"ImportPath": "github.com/gobwas/glob/util/runes",
+			"Comment": "v0.2.3-4-ge7a84e9",
+			"Rev": "e7a84e9525fe90abcda167b604e483cc959ad4aa"
+		},
+		{
+			"ImportPath": "github.com/gobwas/glob/util/strings",
+			"Comment": "v0.2.3-4-ge7a84e9",
+			"Rev": "e7a84e9525fe90abcda167b604e483cc959ad4aa"
+		},
+		{
+			"ImportPath": "github.com/gobwas/glob",
+			"Comment": "v0.2.3-4-ge7a84e9",
+			"Rev": "e7a84e9525fe90abcda167b604e483cc959ad4aa"
+		},
+		{
+			"ImportPath": "github.com/gobwas/glob/compiler",
+			"Comment": "v0.2.3-4-ge7a84e9",
+			"Rev": "e7a84e9525fe90abcda167b604e483cc959ad4aa"
+		},
+		{
+			"ImportPath": "github.com/gobwas/glob/match",
+			"Comment": "v0.2.3-4-ge7a84e9",
+			"Rev": "e7a84e9525fe90abcda167b604e483cc959ad4aa"
+		},
+		{
+			"ImportPath": "github.com/gobwas/glob/syntax",
+			"Comment": "v0.2.3-4-ge7a84e9",
+			"Rev": "e7a84e9525fe90abcda167b604e483cc959ad4aa"
+		},
+		{
+			"ImportPath": "github.com/gobwas/glob/syntax/ast",
+			"Comment": "v0.2.3-4-ge7a84e9",
+			"Rev": "e7a84e9525fe90abcda167b604e483cc959ad4aa"
+		},
+		{
+			"ImportPath": "github.com/gobwas/glob/syntax/lexer",
+			"Comment": "v0.2.3-4-ge7a84e9",
+			"Rev": "e7a84e9525fe90abcda167b604e483cc959ad4aa"
+		},
+		{
+			"ImportPath": "github.com/gobwas/glob/util/runes",
+			"Comment": "v0.2.3-4-ge7a84e9",
+			"Rev": "e7a84e9525fe90abcda167b604e483cc959ad4aa"
+		},
+		{
+			"ImportPath": "github.com/gobwas/glob/util/strings",
+			"Comment": "v0.2.3-4-ge7a84e9",
+			"Rev": "e7a84e9525fe90abcda167b604e483cc959ad4aa"
+		},
+		{
+			"ImportPath": "github.com/gocarina/gocsv",
+			"Rev": "7099e67763c29f812fa2ed7083f32e38be60125a"
+		},
+		{
+			"ImportPath": "github.com/gorilla/context",
+			"Comment": "v1.1-7-g08b5f42",
+			"Rev": "08b5f424b9271eedf6f9f0ce86cb9396ed337a42"
+		},
+		{
+			"ImportPath": "github.com/gorilla/handlers",
+			"Comment": "v1.3.0",
+			"Rev": "90663712d74cb411cbef281bc1e08c19d1a76145"
+		},
+		{
+			"ImportPath": "github.com/gorilla/mux",
+			"Comment": "v1.6.0-1-g2d5fef0",
+			"Rev": "2d5fef06b891c971b14aa6f71ca5ab6c03a36e0e"
+		},
+		{
+			"ImportPath": "github.com/gorilla/schema",
+			"Comment": "v1.0.2-3-gda8e735",
+			"Rev": "da8e73546beca346d03fd40d5b146e06e6b98b7a"
+		},
+		{
+			"ImportPath": "github.com/gorilla/securecookie",
+			"Comment": "v1.1-6-ge59506c",
+			"Rev": "e59506cc896acb7f7bf732d4fdf5e25f7ccd8983"
+		},
+		{
+			"ImportPath": "github.com/gorilla/sessions",
+			"Comment": "v1.1-8-ga3acf13",
+			"Rev": "a3acf13e802c358d65f249324d14ed24aac11370"
+		},
+		{
+			"ImportPath": "github.com/jinzhu/gorm",
+			"Comment": "v1.0-168-gc3bb6aa",
+			"Rev": "c3bb6aaa828867eec72dd8571d111e442688f85f"
+		},
+		{
+			"ImportPath": "github.com/jinzhu/gorm/dialects/mysql",
+			"Comment": "v1.0-168-gc3bb6aa",
+			"Rev": "c3bb6aaa828867eec72dd8571d111e442688f85f"
+		},
+		{
+			"ImportPath": "github.com/jinzhu/inflection",
+			"Rev": "1c35d901db3da928c72a72d8458480cc9ade058f"
+		},
+		{
+			"ImportPath": "github.com/remogatto/cloud",
+			"Rev": "dcf2638e6bc75a8257301eafc8469ad14af7f78e"
+		},
+		{
+			"ImportPath": "github.com/remogatto/slicediff",
+			"Rev": "d19e1ebfb964929670d755944d08cbabbe3f9c2a"
+		},
+		{
+			"ImportPath": "github.com/robfig/cron",
+			"Comment": "v1.1",
+			"Rev": "b41be1df696709bb6395fe435af20370037c0b4c"
+		},
+		{
+			"ImportPath": "github.com/sethvargo/go-password/password",
+			"Comment": "v0.1.1-2-g669be2f",
+			"Rev": "669be2fd9899edfa540cb081008de6d098461541"
+		},
+		{
+			"ImportPath": "gogs.carducci-dante.gov.it/karmen/client",
+			"Rev": "a486e0d6b4427aa2429d9c2314d516abf216d7b0"
+		},
+		{
+			"ImportPath": "gogs.carducci-dante.gov.it/karmen/ldap",
+			"Rev": "83e2c51c67bc8c5f171ad956e200363b4ca0af00"
+		},
+		{
+			"ImportPath": "gogs.carducci-dante.gov.it/karmen/util/fileutil",
+			"Rev": "cdb7cebddc349ce594f7e1578f86f6c49bffac23"
+		},
+		{
+			"ImportPath": "gogs.carducci-dante.gov.it/karmen/util/libreoffice",
+			"Rev": "cdb7cebddc349ce594f7e1578f86f6c49bffac23"
+		},
+		{
+			"ImportPath": "gogs.carducci-dante.gov.it/karmen/util/pandoc",
+			"Rev": "cdb7cebddc349ce594f7e1578f86f6c49bffac23"
+		},
+		{
+			"ImportPath": "gogs.carducci-dante.gov.it/karmen/util/template",
+			"Rev": "cdb7cebddc349ce594f7e1578f86f6c49bffac23"
+		},
+		{
+			"ImportPath": "gopkg.in/asn1-ber.v1",
+			"Comment": "v1.2",
+			"Rev": "379148ca0225df7a432012b8df0355c2a2063ac0"
+		},
+		{
+			"ImportPath": "gopkg.in/gomail.v2",
+			"Comment": "2.0.0-23-g81ebce5",
+			"Rev": "81ebce5c23dfd25c6c67194b37d3dd3f338c98b1"
+		},
+		{
+			"ImportPath": "gopkg.in/ldap.v2",
+			"Comment": "v2.5.0",
+			"Rev": "8168ee085ee43257585e50c6441aadf54ecb2c9f"
+		},
+		{
+			"ImportPath": "gopkg.in/yaml.v2",
+			"Comment": "v2.2.2",
+			"Rev": "51d6538a90f86fe93ac480b35f37b2be17fef232"
+		}
+	]
+}

+ 20 - 0
compose/karmen/ldap/bootstrap/ldif/foo.org.ldif

@@ -85,6 +85,26 @@ gidNumber: 6001
 objectclass: posixGroup
 objectclass: top
 
+dn: ou=Segreterie,ou=Groups,dc=foo,dc=org
+changetype: add
+ou: Segreterie
+objectclass: organizationalUnit
+objectclass: top
+
+dn: cn=Segreteria del personale,ou=Segreterie,ou=Groups,dc=foo,dc=org
+changetype: add
+cn: Segreteria del personale
+gidNumber: 6015
+objectclass: posixGroup
+objectclass: top
+
+dn: cn=Segreteria didattica,ou=Segreterie,ou=Groups,dc=foo,dc=org
+changetype: add
+cn: Segreteria didattica
+gidNumber: 6016
+objectclass: posixGroup
+objectclass: top
+
 dn: ou=Dipartimenti,ou=Groups,dc=foo,dc=org
 changetype: add
 ou: Dipartimenti

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 12 - 0
compose/karmen/sql/karmen_test.sql


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 518 - 464
orm/compose/sql/karmen_test.sql


+ 284 - 0
orm/group.go

@@ -0,0 +1,284 @@
+package orm
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/gobwas/glob"
+	"github.com/jinzhu/gorm"
+	"gogs.carducci-dante.gov.it/karmen/core/renderer"
+)
+
+type Group struct {
+	gorm.Model
+
+	Name string
+
+	Query string
+
+	TeacherIDs []uint `schema:"teacher_ids" gorm:"-"`
+
+	Teachers []*Teacher `gorm:"many2many:group_teachers"`
+}
+
+type GroupForAdd struct {
+	AllTeachers []*Teacher
+}
+
+type GroupForUpdate struct {
+	Group Group
+
+	AllTeachers []*Teacher
+
+	SelectedTeacher map[uint]string
+}
+
+func (g *Group) GetID() uint { return g.ID }
+
+func GetGroups(args map[string]string) (interface{}, error) {
+	return GetGroupsAll(args)
+}
+
+func GetGroupsAll(args map[string]string) (interface{}, error) {
+	var groups []*Group
+
+	if err := DB().Preload("Teachers").Order("name").Find(&groups).Error; err != nil {
+		return nil, err
+	}
+
+	for _, group := range groups {
+		if group.Query != "" {
+			err := group.updateTeachersAssociation()
+			if err != nil {
+				return nil, err
+			}
+		}
+
+	}
+
+	return groups, nil
+}
+
+func GetGroupAll(args map[string]string) (interface{}, error) {
+	var group Group
+
+	id := args["id"]
+
+	if err := DB().Preload("Teachers").Where("id = ?", id).Find(&group).Error; err != nil {
+		return nil, err
+	}
+
+	if group.Query != "" {
+		err := group.updateTeachersAssociation()
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return group, nil
+}
+
+func GetGroupForUpdate(args map[string]string) (interface{}, error) {
+	var data GroupForUpdate
+
+	id := args["id"]
+
+	if err := DB().Preload("Teachers").First(&data.Group, id).Error; err != nil {
+		return nil, err
+	}
+
+	if err := DB().Find(&data.AllTeachers).Error; err != nil {
+		return nil, err
+	}
+
+	data.SelectedTeacher = make(map[uint]string)
+	for _, t := range data.Group.Teachers {
+		data.SelectedTeacher[t.ID] = "selected"
+	}
+
+	return data, nil
+}
+
+func GetGroupForAdd(args map[string]string) (interface{}, error) {
+	var data GroupForAdd
+
+	if err := DB().Find(&data.AllTeachers).Error; err != nil {
+		return nil, err
+	}
+
+	return data, nil
+}
+
+func GetGroup(args map[string]string) (interface{}, error) {
+	var group Group
+
+	id := args["id"]
+
+	if err := DB().Preload("Teachers").Where("id = ?", id).Find(&group).Error; err != nil {
+		return nil, err
+	}
+
+	return &group, nil
+
+}
+
+func UpdateGroup(args map[string]string, r *http.Request) (IDer, error) {
+	group, err := GetGroup(args)
+	if err != nil {
+		return nil, err
+	}
+	err = renderer.Decode(group, r)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := DB().Where(group.(*Group).TeacherIDs).Find(&group.(*Group).Teachers).Error; err != nil {
+		return nil, err
+	}
+
+	teachers, err := group.(*Group).executeTeacherQuery()
+
+	group.(*Group).Teachers = append(group.(*Group).Teachers, teachers...)
+
+	_, err = SaveGroup(group)
+	if err != nil {
+		return nil, err
+	}
+	if err := DB().Model(group).Association("Teachers").Replace(group.(*Group).Teachers).Error; err != nil {
+		return nil, err
+	}
+
+	group, err = GetGroup(args)
+	if err != nil {
+		return nil, err
+	}
+	return group.(*Group), nil
+}
+
+func SaveGroup(group interface{}) (interface{}, error) {
+	if err := DB().Save(group).Error; err != nil {
+		return nil, err
+	}
+	return group, nil
+}
+
+func DeleteGroup(args map[string]string, r *http.Request) (IDer, error) {
+	group, err := GetGroup(args)
+	if err != nil {
+		return nil, err
+	}
+	if err := DB().Unscoped().Delete(group.(*Group)).Error; err != nil {
+		return nil, err
+	}
+	return group.(*Group), nil
+}
+
+func AddGroup(args map[string]string, r *http.Request) (IDer, error) {
+	group := new(Group)
+	err := renderer.Decode(group, r)
+	if err != nil {
+		return nil, err
+	}
+	group, err = CreateGroup(group)
+	if err != nil {
+		return nil, err
+	}
+	return group, nil
+}
+
+func CreateGroup(group *Group) (*Group, error) {
+	var groups []*Group
+	if err := DB().Where("name=?", group.Name).Find(&groups).Error; err != nil {
+		return nil, err
+	}
+	if len(groups) > 0 {
+		return nil, fmt.Errorf("Group %s already exists!", group.Name)
+	}
+
+	if err := DB().Where(group.TeacherIDs).Find(&group.Teachers).Error; err != nil {
+		return nil, err
+	}
+
+	teachers, err := group.executeTeacherQuery()
+	if err != nil {
+		return nil, err
+	}
+	group.Teachers = append(group.Teachers, teachers...)
+	if err := DB().Create(group).Error; err != nil {
+		return nil, err
+	}
+	return group, nil
+}
+
+func (group *Group) RestAPIPath() string {
+	return "groupes"
+}
+
+func (group *Group) updateTeachersAssociation() error {
+	teachers, err := group.executeTeacherQuery()
+	if err != nil {
+		return err
+	}
+	if err := DB().Model(group).Association("Teachers").Replace(teachers).Error; err != nil {
+		return err
+	}
+	return nil
+}
+
+func (group *Group) executeTeacherQuery() ([]*Teacher, error) {
+	var (
+		result      []*Teacher
+		allTeachers []*Teacher
+	)
+
+	if group.Query != "" {
+
+		fields := strings.Split(group.Query, " ")
+
+		if err := DB().Find(&allTeachers).Error; err != nil {
+			return nil, err
+		}
+
+		for _, field := range fields {
+
+			nameValue := strings.Split(field, ":")
+
+			if len(nameValue) < 2 {
+				return nil, fmt.Errorf("Error parsing search query %s on field %s", group.Query, field)
+			}
+
+			name := nameValue[0]
+			value := nameValue[1]
+
+			switch name {
+			case "surname":
+				g, err := glob.Compile(value)
+				if err != nil {
+					return nil, err
+				}
+
+				for _, t := range allTeachers {
+					if g.Match(t.Surname) {
+						result = append(result, t)
+					}
+				}
+
+			case "coordinator":
+				for _, t := range allTeachers {
+					c, err := t.CoordinatorClasses()
+					if err != nil {
+						return nil, err
+					}
+					if len(c) > 0 {
+						result = append(result, t)
+					}
+				}
+
+			}
+		}
+
+	}
+
+	return result, nil
+}

+ 1 - 0
orm/orm.go

@@ -80,6 +80,7 @@ func AutoMigrate() {
 		&File{},
 		&GeneratorType{},
 		&Log{},
+		&Group{},
 	).Error; err != nil {
 		panic(err)
 	}

+ 52 - 0
orm/orm_test.go

@@ -313,3 +313,55 @@ func (t *testSuite) TestSaveStudent() {
 		t.Equal(true, student.(*Student).Handicap)
 	}
 }
+
+// Groups
+
+func (t *testSuite) TestGetGroups() {
+	groups, err := GetGroups(map[string]string{})
+	t.Nil(err)
+	t.Equal(1, len(groups.([]*Group)))
+}
+
+func (t *testSuite) TestGetGroup() {
+	group, err := GetGroup(map[string]string{"id": "1"})
+	t.Nil(err)
+	if !t.Failed() {
+		t.Equal("Gruppo", group.(*Group).Name)
+	}
+
+}
+
+func (t *testSuite) TestTeacherQuery() {
+	group := new(Group)
+
+	// Select all
+
+	group.Query = "surname:*"
+	teachers, err := group.executeTeacherQuery()
+	t.Nil(err)
+
+	if !t.Failed() {
+		t.Equal(11, len(teachers))
+	}
+
+	// Select teacher with surname beginning with the letter A
+
+	group.Query = "surname:A*"
+	teachers, err = group.executeTeacherQuery()
+	t.Nil(err)
+
+	if !t.Failed() {
+		t.Equal(2, len(teachers))
+	}
+
+	// Select teacher with surname beginning with the letter A
+
+	group.Query = "coordinator:true"
+	teachers, err = group.executeTeacherQuery()
+	t.Nil(err)
+
+	if !t.Failed() {
+		t.Equal(1, len(teachers))
+	}
+
+}

+ 2 - 1
orm/teacher.go

@@ -20,6 +20,7 @@ type Teacher struct {
 	Subjects    []*Subject
 	Activities  []*Activity
 	Departments []*Department
+	Groups      []*Group `gorm:"many2many:group_teachers"`
 
 	Coordinator []*Class
 	Minuter     []*Class
@@ -92,7 +93,7 @@ func (t *Teacher) Read(args map[string]string, r *http.Request) (interface{}, er
 
 	id := args["id"]
 
-	if err := DB().First(&teacher, id).Error; err != nil {
+	if err := DB().Preload("Groups").Where("id = ?", id).Find(&teacher).Error; err != nil {
 		return nil, err
 	}
 

+ 1 - 1
templates/classes_add_update.html.tpl

@@ -5,7 +5,7 @@
   {{if .Options.Get "update"}}
   <nav aria-label="breadcrumb">
     <ol class="breadcrumb">
-      <li class="breadcrumb-item"><a href="/classes">Classi</a></li>
+      <li class="breadcrumb-item"><a href="/classes?{{query "tpl_layout" "base" "tpl_content" "teachers"}}">Classi</a></li>
       <li class="breadcrumb-item"><a href="/classes/{{.Data.Class.ID}}?{{query "tpl_layout" "base" "tpl_content" "classes"}}">{{.Data.Class.Name}}</a></li>
       <li class="breadcrumb-item active"><a href="#">Aggiorna la classe</a></li>
     </ol>

+ 43 - 0
templates/groups.html.tpl

@@ -0,0 +1,43 @@
+{{ define "content" }}
+
+<div class="container">
+  
+  <div class="karmen-info-header">
+    <div class="row">
+      <div class="col-md-8">
+	<h1>Gruppi ({{len .Data}})</h1>
+      </div>
+      <div class="col-md-4">
+	<a href="/groups/add/?{{query "tpl_layout" "base" "tpl_content" "groups_add_update"}}" class="btn btn-primary float-right">
+	  <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+	  Crea nuovo gruppo
+	</a>
+      </div>
+    </div>
+  </div>
+
+  <div class="input-group" style="margin-bottom: 20px">
+    <span class="input-group-addon" id="search-query"><span class="glyphicon glyphicon-search"></span></span>
+    <input type="text" id="myInput" class="form-control" aria-describedby="search-query">
+  </div>
+  
+  {{if not .Data}}
+  <p>Non c'è alcun elemento da visualizzare</p>
+  {{else}}
+  <div class="list-group" id="myUL">
+    {{range $group := .Data}}
+    <a class="list-group-item list-group-item-action" href="/groups/{{$group.ID}}?{{query "tpl_layout" "base" "tpl_content" "groups_show"}}">
+      <span class="fa fa-users"></span>
+      {{$group.Name}}
+      <div class="text-right">
+ 	{{range $teacher := $group.Teachers}}
+	<small>{{$teacher.Surname}}</small>
+	{{end}}
+      </div>
+    </a>
+    {{end}}
+  </div>
+  {{end}}
+</div>
+  
+{{ end }}

+ 80 - 0
templates/groups_add_update.html.tpl

@@ -0,0 +1,80 @@
+{{ define "content" }}
+
+<div class="container">
+
+  {{if .Options.Get "update"}}
+  <nav aria-label="breadcrumb">
+    <ol class="breadcrumb">
+      <li class="breadcrumb-item"><a href="/groups?{{query "tpl_layout" "base" "tpl_content" "groups"}}">Gruppi</a></li>
+      <li class="breadcrumb-item active"><a href="#">Aggiorna gruppo</a></li>
+    </ol>
+  </nav>
+  {{else}}
+  <nav aria-label="breadcrumb">
+    <ol class="breadcrumb">
+      <li class="breadcrumb-item"><a href="/groups?{{query "tpl_layout" "base" "tpl_content" "groups"}}">Gruppi</a></li>
+      <li class="breadcrumb-item active"><a href="#">Aggiungi</a></li>
+    </ol>
+  </nav>
+  {{end}}
+
+  {{if .Options.Get "update"}}
+  <div class="karmen-info-header">
+    <div class="row">
+      <div class="col-md-8">
+	<h1>Aggiorna gruppo</h1>
+      </div>
+    </div>
+  </div>
+  
+  {{else}}
+  <h1 class="karmen-info-header">Crea nuovo gruppo</h1>
+  {{end}}
+
+  {{if .Options.Get "update"}}
+  <form id="form_groups_add_update" action="/groups/{{.Data.Group.ID}}/update" method="POST" role="form" data-toggle="validator">
+    {{else}}
+    <form id="form_groups_add_update" action="/groups/add/" method="POST" role="form" data-toggle="validator">
+      {{end}}
+
+      <div class="form-group has-feedback">
+        <label class="control-label" for="group_name">Nome</label>
+        <input type="text" name="Name" class="form-control" id="group_name" placeholder="Nome" {{if .Options.Get "update"}} value="{{.Data.Group.Name}}" {{end}} required>
+      </div>
+
+      <div class="form-group has-feedback">
+        <label class="control-label" for="group_query">Stringa di popolamento</label>
+        <input type="text" name="Query" class="form-control" id="group_query" placeholder="surname:*" {{if .Options.Get "update"}} value="{{.Data.Group.Query}}" {{end}}>
+      </div>
+
+      <div class="form-group">
+        <label class="control-label" for="teacher_id">Docenti</label>
+        <select name="teacher_ids" class="form-control selectpicker" id="teacher_ids" placeholder="Docenti" data-live-search="true" form="form_groups_add_update" title="Seleziona i nomi dei docenti" data-dropup-auto="false" multiple>
+          <option value="0"></option>
+          {{range $teacher := .Data.AllTeachers}}
+          {{if $.Options.Get "update"}}
+          <option
+	    value="{{$teacher.ID}}"
+	    {{index $.Data.SelectedTeacher $teacher.ID}}>{{$teacher.CompleteName}}
+          </option>
+          {{else}}
+          <option value="{{$teacher.ID}}">{{$teacher.CompleteName}}</option>
+          {{end}}
+          {{end}}
+        </select>
+      </div>
+
+      <div class="form-group">
+        <button type="submit" class="btn btn-primary">Salva</button>
+        {{if .Options.Get "update"}}
+        <a href="/groups/{{.Data.Group.ID}}?{{query "tpl_layout" "base" "tpl_content" "groups_show"}}" class="btn btn-default">Annulla</a>
+        {{else}}
+        <a href="/groups?{{query "tpl_layout" "base" "tpl_content" "groups"}}" class="btn btn-default">Annulla</a>
+        {{end}}
+      </div>
+    
+    </form>
+
+</div>
+
+{{ end }}

+ 60 - 0
templates/groups_show.html.tpl

@@ -0,0 +1,60 @@
+{{ define "content" }}
+
+<div class="container">
+
+  <nav aria-label="breadcrumb">
+  <ol class="breadcrumb">
+    <li class="breadcrumb-item"><a href="/groups?{{query "tpl_layout" "base" "tpl_content" "groups"}}">Gruppi</a></li>
+    <li class="breadcrumb-item active"><a href="#">{{.Data.Name}}</a></li>
+  </ol>
+  </nav>
+
+  <div class="karmen-info-header">
+    <div class="row">
+      <div class="col-md-8">
+	<h1>{{.Data.Name}}</h1>
+      </div>
+      <div class="col-md-4">
+	<div class="btn-group float-right" role="group">
+	  <a href="/groups/add/?{{query "tpl_layout" "base" "tpl_content" "groups_add_update"}}" class="btn btn-success">
+	    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
+	    Crea
+	  </a>
+	  <a href="/groups/{{.Data.ID}}/update?{{query "tpl_layout" "base" "tpl_content" "groups_add_update" "update" "true"}}"  class="btn btn-primary">
+	    <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
+	    Modifica
+	  </a>
+	  <button href="/groups/{{.Data.ID}}/delete"
+		  data-url="/groups/{{.Data.ID}}/delete"
+		  class="btn btn-danger karmen-ajax-delete">
+	    <span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
+	    Elimina
+	  </button>
+	</div>
+      </div>
+    </div>
+  </div>
+  
+  <div class="row">
+    <div class="col-md-12">
+      
+      <h2 class="karmen-relation-header">Docenti associati al gruppo</h2>
+      {{if .Data.Teachers}}
+      <div class="list-group" id="students_list_group">
+	{{range $teacher := .Data.Teachers}}
+	<a href="/teachers/{{$teacher.ID}}?{{query "tpl_layout" "base" "tpl_content" "teachers_show"}}" class="list-group-item list-group-item-action">
+	  <span class="fa fa-user"></span>
+	  {{$teacher.CompleteName}}
+	  {{end}}
+	  </a>
+      </div>
+      {{else}}
+      <p>Al gruppo non è associato alcun docente. Clicca <a href="/groups/{{.Data.ID}}/update?{{query "tpl_layout" "base" "tpl_content" "groups_add_update"}}">qui</a> per modificare questo gruppo.</p>
+      {{end}}
+    </div>
+    
+  </div>
+
+</div>    
+
+{{ end }}

+ 3 - 0
templates/layout/base.html.tpl

@@ -42,6 +42,9 @@
            <li class="nav-item">
 	     <a class="nav-item nav-link" href="/administratives?{{query "tpl_layout" "base" "tpl_content" "administratives"}}">ATA</a>
 	   </li>
+           <li class="nav-item">
+	     <a class="nav-item nav-link" href="/groups?{{query "tpl_layout" "base" "tpl_content" "groups"}}">Gruppi<span class="badge badge-danger experimental">Sperimentale</span></a>
+	   </li>           
            <li class="nav-item">
 	     <a class="nav-item nav-link" href="/documents?{{query "tpl_layout" "base" "tpl_content" "documents"}}">Documenti<span class="badge badge-danger experimental">Sperimentale</span></a>
 	   </li>

+ 19 - 0
templates/teachers_show.html.tpl

@@ -97,6 +97,25 @@
     
   </div>
 
+  <div class="row">
+    <div class="col-md-12">
+      
+      <h2 class="karmen-relation-header">Gruppi a cui il docente appartiene</h2>
+      {{if .Data.Groups}}
+      <div class="list-group" id="groups_list_group">
+    	{{range $group := .Data.Groups}}
+    	<a href="/groups/{{$group.ID}}?{{query "tpl_layout" "base" "tpl_content" "groups_show"}}" class="list-group-item list-group-item-action">
+    	  <span class="fa fa-book"></span>
+    	  {{$group.Name}}
+    	  {{end}}
+    	</a>
+      </div>
+      {{else}}
+      <p>Il docente non è associato ad alcun gruppo. Clicca <a href="/groups?{{query "tpl_layout" "base" "tpl_content" "groups"}}">qui</a> per creare un nuovo gruppo.</p>{{end}}
+    </div>
+    
+  </div>
+
   <div class="row">
     <div class="col-md-12">
    

+ 1 - 0
watch.sh

@@ -1,5 +1,6 @@
 #!/bin/bash
 
+
 echo "Executing Makefile... $1"
 
 make -k $1

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio