Commit 58d9ec77 authored by Lukas Matt's avatar Lukas Matt
parent 5b5dc3ce
......@@ -14,6 +14,7 @@ css:node_modules/bootstrap/dist/css/bootstrap-reboot.min.css.map
css:node_modules/tether/dist/css/tether.min.css
js:node_modules/bootstrap/dist/js/bootstrap.min.js
js:node_modules/tether/dist/js/tether.min.js
js:node_modules/popper.js/dist/umd/popper.min.js
# Font-Awesome
fonts:node_modules/font-awesome/fonts/fontawesome-webfont.svg
......@@ -30,3 +31,7 @@ js:node_modules/another-rest-client/rest-client.min.js.map
# Markdown
js:node_modules/marked/marked.min.js
# Datetimepicker
css:node_modules/jquery-datetimepicker/build/jquery.datetimepicker.min.css
js:node_modules/jquery-datetimepicker/build/jquery.datetimepicker.full.min.js
$(function () {
var elem = $('[name="location"]');
navigator.geolocation.getCurrentPosition(function(position) {
elem.val(position.coords.latitude + "," + position.coords.longitude);
}, function(error) { elem.val("Berlin") });
$('[data-toggle="datetimepicker"]').datetimepicker({
minDate: new Date(),
minTime: new Date(),
validateOnBlur: false,
//closeOnDateSelect: true,
format: 'Y-m-d\\TH:m:sO',
onShow: function(ct) {
this.setOptions({timepicker: !$('[name="all_day"]').is(":checked")});
}
});
$('[name="all_day"]').click(function() {
if ($('[name="all_day"]').is(':checked')) {
$('[name="end"]').prop('disabled', true);
} else {
$('[name="end"]').prop('disabled', false);
}
});
$('[data-toggle="calendar"] > .row > .day > .events > .event').each(function() {
var popover = $(this).popover({
container: 'body',
content: $(this).find('.popover-content').html(),
html: true
});
});
$('[data-toggle="calendar"] > .row > .day > .events > .event').on('shown.bs.popover', function() {
var elem = $(this);
$('.popover:last-child').find('.close').on('click', function() {
elem.popover('hide');
});
});
});
......@@ -6,7 +6,25 @@
form.ajaxForm(function(result) {
window.location = "/posts/" + result['Guid'];
});
loadAspectList();
}
var form = $('#comment-editor-form');
if (form !== undefined) {
form.ajaxForm(function(result) {
location.reload();
});
}
var form = $('#calendar-editor-form');
if (form !== undefined) {
form.ajaxForm(function(result) {
location.reload();
});
loadAspectList();
}
function loadAspectList() {
API.aspects.get().then(function(aspects) {
$.each(aspects, function(i, aspect) {
$('#aspect-list').append(
......@@ -18,11 +36,4 @@
});
});
}
var form = $('#comment-editor-form');
if (form !== undefined) {
form.ajaxForm(function(result) {
location.reload();
});
}
})();
package controllers
//
// GangGo Application Server
// Copyright (C) 2017 Lukas Matt <lukas@zauberstuhl.de>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import (
"github.com/revel/revel"
"gopkg.in/ganggo/ganggo.v0/app/models"
)
type Calendar struct {
*revel.Controller
}
func (c Calendar) Index(name string, page int) revel.Result {
user, err := models.CurrentUser(c.Controller)
if err != nil {
c.Log.Error("Cannot find user", "error", err)
return c.RenderError(err)
}
// on error the template will display
// an option to create a new calendar
var calendars models.Calendars
calendars.FindByUser(user)
c.ViewArgs["calendars"] = calendars
c.ViewArgs["currentUser"] = user
return c.RenderTemplate("calendar/index.html")
}
func (c Calendar) Public(page int) revel.Result {
user, err := models.CurrentUser(c.Controller)
if err != nil {
c.Log.Error("Cannot find user", "error", err)
return c.RenderError(err)
}
var events models.CalendarEvents
err = events.FindAllPublic()
if err != nil {
c.Log.Error(err.Error())
return c.RenderError(err)
}
c.ViewArgs["page"] = page
c.ViewArgs["currentUser"] = user
c.ViewArgs["calendar"] = struct{
ID uint
Events models.CalendarEvents
}{0, events}
return c.RenderTemplate("calendar/calendar.html")
}
func (c Calendar) Show(name string, page int) revel.Result {
user, err := models.CurrentUser(c.Controller)
if err != nil {
c.Log.Error("Cannot find user", "error", err)
return c.RenderError(err)
}
var calendar models.Calendar
err = calendar.FindByUserAndName(user, name)
if err != nil {
c.Log.Error(err.Error())
return c.RenderError(err)
}
c.ViewArgs["page"] = page
c.ViewArgs["currentUser"] = user
c.ViewArgs["calendar"] = calendar
return c.RenderTemplate("calendar/calendar.html")
}
func (c Calendar) ShowEvent(id int) revel.Result {
return c.NotFound("NA")
}
......@@ -29,6 +29,7 @@ func init() {
revel.InterceptFunc(requiresHTTPLogin, revel.BEFORE, &Stream{})
revel.InterceptFunc(requiresHTTPLogin, revel.BEFORE, &Setting{})
revel.InterceptFunc(requiresHTTPLogin, revel.BEFORE, &Search{})
revel.InterceptFunc(requiresHTTPLogin, revel.BEFORE, &Calendar{})
}
func redirectIfLoggedIn(c *revel.Controller) revel.Result {
......
package models
//
// GangGo Application Server
// Copyright (C) 2017 Lukas Matt <lukas@zauberstuhl.de>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import (
"time"
"github.com/jinzhu/gorm"
)
type EventStatus int
const (
EventAccepted EventStatus = iota
EventDeclined
EventTentative
)
type Calendar struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Name string `gorm:"size:191"`
UserID uint
Default bool
Events CalendarEvents
}
type Calendars []Calendar
type CalendarEvent struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
Public bool
PersonID uint
Location string
// size should be max 191 with mysql innodb
// cause asumming we use utf8mb 4*191 = 764 < 767
Guid string `gorm:"size:191"`
Summary string `gorm:"type:text"`
Description string `gorm:"type:text"`
Start time.Time
End time.Time
AllDay bool
CalendarID uint
Person Person
Participations CalendarEventParticipations
}
type CalendarEvents []CalendarEvent
type CalendarEventParticipation struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
PersonID uint
CalendarEventID uint
Status EventStatus
Person Person
}
type CalendarEventParticipations []CalendarEventParticipation
func (c *Calendar) AfterFind(db *gorm.DB) error {
return db.Model(c).Related(&c.Events).Error
}
func (e *CalendarEvent) AfterFind(db *gorm.DB) error {
err := db.Model(e).Related(&e.Participations).Error
// if its a public event CalendarID can be zero
// so ignore RecordNotFound errors
if err != nil && err != gorm.ErrRecordNotFound {
return err
}
return db.Model(e).Related(&e.Person).Error
}
func (c *Calendar) Create() error {
db, err := OpenDatabase()
if err != nil {
return err
}
defer db.Close()
return db.Create(c).Error
}
func (e *CalendarEvent) Create() error {
db, err := OpenDatabase()
if err != nil {
return err
}
defer db.Close()
return db.Create(e).Error
}
func (c *Calendars) FindByUser(user User) error {
db, err := OpenDatabase()
if err != nil {
return err
}
defer db.Close()
return db.Where("user_id = ?", user.ID).Find(c).Error
}
func (c *Calendar) FindByUserAndID(user User, id uint) error {
db, err := OpenDatabase()
if err != nil {
return err
}
defer db.Close()
return db.Where("user_id = ? and id = ?", user.ID, id).Find(c).Error
}
func (c *Calendar) FindByUserAndName(user User, name string) error {
db, err := OpenDatabase()
if err != nil {
return err
}
defer db.Close()
return db.Where("user_id = ? and name = ?", user.ID, name).Find(c).Error
}
func (e *CalendarEvent) FindByUserAndID(user User, id uint) error {
db, err := OpenDatabase()
if err != nil {
return err
}
defer db.Close()
err = db.First(e, id).Error
if err != nil {
return err
}
// if it is public no need
// for further verifiction
if e.Public {
return nil
}
return db.Joins(`left join shareables on shareables.shareable_id = calendar_events.id`).
Where(`calendar_events.id = ?
and shareables.shareable_type = ?
and shareables.user_id = ?`, id, ShareableEvent, user.ID,
).Find(e).Error
}
func (e *CalendarEvents) FindAllPublic() error {
db, err := OpenDatabase()
if err != nil {
return err
}
defer db.Close()
return db.Where("public = ?", true).Find(e).Error
}
......@@ -58,6 +58,7 @@ const (
ShareableLike = "Like"
ShareableComment = "Comment"
ShareableContact = "Contact"
ShareableEvent = "Event"
)
var DB Database
......
......@@ -29,6 +29,7 @@ type Post struct {
CreatedAt time.Time
UpdatedAt time.Time
CalendarEventID uint
PersonID uint `gorm:"size:4"`
Public bool
// size should be max 191 with mysql innodb
......@@ -44,6 +45,7 @@ type Post struct {
ResharesCount int `gorm:"size:4"`
InteractedAt string `gorm:"size:191"`
Event CalendarEvent
Person Person `gorm:"ForeignKey:PersonID" json:",omitempty"`
Comments Comments `gorm:"ForeignKey:ShareableID" json:",omitempty"`
}
......@@ -83,6 +85,9 @@ func (post *Post) AfterFind(db *gorm.DB) error {
return err
}
// this can fail since not all posts have events
db.Model(post).Related(&post.Event)
return db.Preload("Comments").First(post).Error
}
......
......@@ -198,6 +198,23 @@ func loadSchema(db *gorm.DB) {
db.Model(oAuthToken).AddIndex("index_o_auth_token_on_user_id", "user_id")
db.Model(oAuthToken).AddIndex("index_o_auth_token_on_token", "token")
db.AutoMigrate(oAuthToken)
calendar := &Calendar{}
db.Model(calendar).AddUniqueIndex("index_calendar_on_user_id_and_name", "user_id", "name")
db.Model(calendar).AddIndex("index_calendar_on_user_id", "user_id")
db.Model(calendar).AddIndex("index_calendar_on_name", "name")
db.AutoMigrate(calendar)
calendarEvent := &CalendarEvent{}
db.Model(calendarEvent).AddIndex("index_calendar_event_on_person_id", "person_id")
db.Model(calendarEvent).AddIndex("index_calendar_event_on_guid", "guid")
db.AutoMigrate(calendarEvent)
calendarEventParticipation := &CalendarEventParticipation{}
db.Model(calendarEventParticipation).AddUniqueIndex("index_calendar_event_participation_on_person_id_and_calendar_event_id", "person_id", "calendar_event_id")
db.Model(calendarEventParticipation).AddIndex("index_calendar_event_participation_on_user_id", "person_id")
db.Model(calendarEventParticipation).AddIndex("index_calendar_event_participation_on_calendar_event_id", "calendar_event_id")
db.AutoMigrate(calendarEventParticipation)
}
func InitDB() {
......
{{- $title := msg . "calendars.title"}}
{{- $cal := FetchCalendarDays .page}}
{{- set . "title" $title}}
{{- set . "cal" $cal}}
{{template "header.html" .}}
{{stylesheet_link_tag "jquery.datetimepicker.min"}}
{{stylesheet_link_tag "calendar"}}
<div class="container" data-toggle="calendar">
<header>
<h4 class="display-4 mb-4 text-center">
{{- if gt .page 1}}
<a href="?page={{sub .page 1}}">
<i class="fa fa-backward float-left" aria-hidden="true"></i>
</a>
{{end}}
{{.cal.Month.String}} {{.cal.Year}}
<a href="?page={{add .page 1}}">
<i class="fa fa-forward float-right" aria-hidden="true"></i>
</a>
</h4>
<div class="row d-none d-sm-flex p-1 bg-dark text-white">
<h5 class="col-sm p-1 text-center">Sunday</h5>
<h5 class="col-sm p-1 text-center">Monday</h5>
<h5 class="col-sm p-1 text-center">Tuesday</h5>
<h5 class="col-sm p-1 text-center">Wednesday</h5>
<h5 class="col-sm p-1 text-center">Thursday</h5>
<h5 class="col-sm p-1 text-center">Friday</h5>
<h5 class="col-sm p-1 text-center">Saturday</h5>
</div>
</header>
<div class="row border border-right-0 border-bottom-0">
{{$root := .}}
{{set . "grey" 1}}
{{range $i, $day := .cal.Days}}
{{if eq $day 1}}
{{if eq $root.grey 1}}
{{set $root "grey" 0}}
{{else}}
{{set $root "grey" 1}}
{{end}}
{{end}}
{{if eq $root.grey 1}}
<div class="day col-sm p-2 border border-left-0 border-top-0 text-truncate d-none d-sm-inline-block bg-light text-muted">
{{else}}
<div class="day col-sm p-2 border border-left-0 border-top-0 text-truncate ">
{{end}}
<h5 class="row align-items-center">
<span class="date col-1">{{$day}}</span>
<!--<small class="col d-sm-none text-center text-muted">Sunday</small>-->
<span class="col-1"></span>
</h5>
<div class="events">
{{if ne $root.grey 1}}
{{set $root "day" $day}}
{{template "calendar/events.html" $root}}
{{end}}
</div>
</div>
{{$i := add $i 1}}
{{if mod $i 7}}
<div class="w-100"></div>
{{end}}
{{end}}
</div>
{{template "calendar/editor_box.html" .}}
</div>
{{javascript_include_tag "jquery.datetimepicker.full.min"}}
{{javascript_include_tag "calendar"}}
{{template "footer.html" .}}
<form class="pt-4" id="calendar-editor-form" method="POST" action="{{url "ApiCalendarEvent.Create"}}">
<div class="form-row">
<div class="form-group col-md-5">
<div class="input-group">
<input data-toggle="datetimepicker" name="start" type="text" class="form-control" placeholder="Start" autocomplete="off">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="fa fa-calendar" aria-hidden="true"></i>
</span>
</div>
</div>
</div>
<div class="form-group col-md-5">
<div class="input-group">
<input data-toggle="datetimepicker" name="end" type="text" class="form-control" placeholder="End" autocomplete="off">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="fa fa-calendar" aria-hidden="true"></i>
</span>
</div>
</div>
</div>
<div class="form-group col-md-2">
<div class="input-group">
<div class="pl-4">All day?</div>
<input class="form-control" type="checkbox" name="all_day">
</div>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-10">
<input class="form-control" type="text" name="summary" placeholder="Summary" />
</div>
<div class="form-group col-md-2">
<div class="input-group">
<input type="text" class="form-control" name="location">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="fa fa-map-marker" aria-hidden="true"></i>
</span>
</div>
</div>
</div>
</div>
<div class="form-group">
<textarea class="form-control" name="description" placeholder="Description" rows="4"></textarea>
</div>
<div class="form-row">
<div class="col-10">
<input type="hidden" name="calendarID" value="{{.calendar.ID}}" />
<button type="submit" class="btn btn-primary btn-block">
<i class="fa fa-share-square-o"></i> {{msg . "editor.event_button"}}
</button>
</div>
<div class="col">
<select id="aspect-list" name="aspectID" class="form-control">
<option value="0">{{msg . "editor.default_group"}}</option>
</select>
</div>
</div>
</form>
{{javascript_include_tag "editor_box"}}
{{$root := .}}
{{range .calendar.Events}}
{{if DateInRange .Start .End $root.cal.Year $root.cal.Month $root.day}}
<div class="event d-block p-1 pl-2 pr-2 mb-1 rounded text-truncate small bg-primary text-white" title="{{.Summary}}">
{{.Summary}}
<div class="popover-content" style="display:none">
<i class="fa fa-times fa-2x float-right close" aria-hidden="true"></i>
<div data-markdown>
{{.Description}}
</div>
<div><i class="fa fa-map-marker"></i> {{.Location}}</div>
<div><i class="fa fa-clock-o"></i> {{.Start}} - {{.End}}</div>
<div class="pt-4">
<img max-width="150px" src="{{.Person.Profile.ImageUrl}}" class="rounded" title="{{.Person.Profile.FullName}}">
</div>
<div class="row pt-2 mx-1 my-1">
<div class="col p-1">
<a type="button" class="btn btn-danger btn-block" href="#" title="123">Decline</a>
</div>
<div class="col p-1">
<a type="button" class="btn btn-warning btn-block" href="#" title="123">Tentative</a>
</div>
<div class="col p-1">
<a type="button" class="btn btn-success btn-block" href="#" title="123">Accept</a>
</div>
</div>
</div>
</div>
{{end}}
{{end}}
{{$title := msg . "calendars.title"}}
{{set . "title" $title}}
{{template "header.html" .}}
<div class="container">
<div class="card">
<div class="card-header text-center">
<h4>{{msg . "calendars.title"}}</h4>
</div>
<ul class="list-group list-group-flush text-center">
<li class="list-group-item"><a href="/calendars/public">Public</a></li>
{{range .calendars}}
<li class="list-group-item">
<a href="/calendars/{{.Name}}">{{.Name}}</a>
</li>
{{end}}
</ul>
<form id="calendar-editor-form" class="form-inline mx-4 my-4" action="{{url "ApiCalendar.Create"}}" method="post">
<div class="form-group">
<input type="text" class="form-control mr-4" name="name" placeholder="New Calendar">
</div>
<button type="submit" class="btn btn-primary">
Create <i class="fa fa-plus"></i>
</button>
</form>
</div>
</div>
{{javascript_include_tag "editor_box"}}
{{template "footer.html" .}}
</main>
<footer class="py-2 bg-inverse">
<footer class="py-2 bg-dark">
<div class="container">
<p class="m-0 float-left">GangG<i class="fa fa-heart-o"></i></p>
<p class="m-0 float-right">
......@@ -10,6 +10,7 @@
</div>
</footer>
{{javascript_include_tag "tether.min"}}
{{javascript_include_tag "popper.min"}}
{{javascript_include_tag "bootstrap.min"}}
{{javascript_include_tag "main"}}
</body>
......
......@@ -39,10 +39,10 @@
</script>
</head>
<body>
<nav class="navbar navbar-toggleable-md navbar-inverse bg-inverse fixed-top">
<a href="https://github.com/ganggo/ganggo">
<img style="position: absolute; top: 0; left: 0; border: 0;" src="/public/img/forkme_left_red_aa0000.png" title="Fork me on GitHub">
</a>
<nav class="navbar navbar-expand-xl navbar-dark bg-dark fixed-top">
<a href="https://github.com/ganggo/ganggo">
<img style="position: absolute; top: 0; left: 0; border: 0;" src="/public/img/forkme_left_red_aa0000.png" title="Fork me on GitHub">
</a>
<div class="container">
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="{{msg . "navigation.toggle"}}">
<span class="navbar-toggler-icon"></span>
......@@ -66,6 +66,7 @@
<i class="fa fa-user-circle-o"></i>
</a>
<div class="dropdown-menu" aria-labelledby="dropdown01">
<a class="dropdown-item" href="{{url "Calendar.Index"}}">{{msg . "calendars.title"}}</a>
<a class="dropdown-item" href="{{url "Setting.Index"}}">{{msg . "navigation.user.settings"}}</a>
</div>
</li>
......
<div class="row">
<div class="col-md-12">
<form id="post-editor-form" action="/api/v0/posts" method="post">
<form id="post-editor-form" action="{{url "ApiPost.Create"}}" method="post">
<div class="form-group">
<textarea class="form-control" name="post" placeholder="{{msg . "editor.placeholder"}}" rows="4"></textarea>
</div>
......
......@@ -21,6 +21,7 @@ import (
"regexp"
"path/filepath"
"os"
"time"
"github.com/shaoshing/train"
"github.com/revel/revel"
"github.com/revel/config"
......@@ -135,6 +136,8 @@ var TemplateFuncs = map[string]interface{}{
}
return i18n
},
"FetchCalendarDays": calendar,
"DateInRange": dateInRange,
"FindAvailableLocales": func() (list []string) {
directory := filepath.Join(revel.BasePath, "messages")
re := regexp.MustCompile(`ganggo\.([\w-_]{1,})$`)
......@@ -187,6 +190,9 @@ var TemplateFuncs = map[string]interface{}{
tmpl := `<link type="text/css" rel="stylesheet" href="/public` + src + `">`
return template.HTML(tmpl)
},
"mod": func(a, b int) bool {
return (a % b) == 0
},
"eq": func(a, b interface {}) bool {
return a == b
},
......@@ -243,3 +249,63 @@ func likes(id uint, like bool) (likes []models.Like) {
}
return
}
func calendar(offset int) interface{} {
var days []int
now := time.Now()
year, month := now.Year(), now.Month()
start := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(year, month, 32, 0, 0, 0, 0, time.UTC)
for i := 1; i < offset; i ++ {