amuse.git

commit 794571637c1702273c43687be33decd00d6595e8

Author: Adam <git@apiote.tk>

show film experiences

 datastructure/experiences.go | 41 ++++++++++++++++
 db/db.go | 55 ++++++++++++++++++++++
 front/capnproto.go | 4 +
 front/html.go | 10 ++++
 front/renderer.go | 1 
 i18n/en-GB.toml | 1 
 libamuse/experiences.go | 71 +++++++++++++++++++++++++++++
 router.go | 7 ++
 static/style/style.css | 8 +++
 templates/experiences.html | 92 ++++++++++++++++++++++++++++++++++++++


diff --git a/datastructure/experiences.go b/datastructure/experiences.go
new file mode 100644
index 0000000000000000000000000000000000000000..0e4c3ff42140f068f8862caa5d3fb7c8f768f797
--- /dev/null
+++ b/datastructure/experiences.go
@@ -0,0 +1,41 @@
+package datastructure
+
+import (
+	"notabug.org/apiote/amuse/i18n"
+
+	"time"
+)
+
+type ExperiencesEntry struct {
+	ItemInfo
+	Type     string
+	Id       string
+	Datetime time.Time
+}
+
+type Experiences struct {
+	List  []ExperiencesEntry
+	Page  int
+	Pages int
+	Query string
+}
+
+func (e ExperiencesEntry) FormatDatetime(strings i18n.Translation) string {
+	return i18n.FormatDate(e.Datetime, strings.Global["date_format_time"], strings.Global)
+}
+
+func (e Experiences) NextPage() int {
+	if e.Page < e.Pages {
+		return e.Page + 1
+	} else {
+		return e.Page
+	}
+}
+
+func (e Experiences) PrevPage() int {
+	if e.Page > 1 {
+		return e.Page - 1
+	} else {
+		return e.Page
+	}
+}




diff --git a/db/db.go b/db/db.go
index 9a3c8f6fbd0608f4bd1ab680aa7169a33192ac88..bc112ed62e6cc8fe96acf68117bc3f1e890ba681 100644
--- a/db/db.go
+++ b/db/db.go
@@ -584,3 +584,58 @@ 	}
 
 	return watchlist, nil
 }
+
+func GetUserExperiences(username, filter string, page int) (datastructure.Experiences, error) {
+	experiences := datastructure.Experiences{}
+	db, err := sql.Open("sqlite3", utils.DataHome+"/amuse.db")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "DB open err\n")
+		return experiences, err
+	}
+	defer db.Close()
+
+	if page <= 0 {
+		page = 1
+	}
+
+	var pages float64
+	row := db.QueryRow(`select count(*) from experiences where username = ?`, username)
+	err = row.Scan(&pages)
+	if err != nil {
+		return experiences, err
+	}
+	experiences.Pages = int(math.Ceil(pages / 18))
+
+	offset := (page - 1) * 18
+
+	//todo filter, order by
+
+	var whereClause string
+	if filter != "" {
+		whereClause = "and c1.title like '%" + filter + "%'"
+	}
+
+	rows, err := db.Query(`select item_id, item_type, cover, status, title, year_start, based_on, genres, runtime, part, time from experiences natural join item_cache where username = ? `+whereClause+` order by time desc limit ?,18;`, username, offset)
+
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Select err: %v\n", err)
+		return experiences, err
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		var (
+			entry    datastructure.ExperiencesEntry
+		)
+		err := rows.Scan(&entry.Id, &entry.Type, &entry.Cover, &entry.Status, &entry.Title, &entry.YearStart, &entry.BasedOn, &entry.Genres, &entry.Runtime, &entry.Part, &entry.Datetime)
+		if err != nil {
+			fmt.Println("Scan error")
+			return datastructure.Experiences{}, err
+		}
+
+		experiences.List = append(experiences.List, entry)
+	}
+
+	return experiences, nil
+
+}




diff --git a/front/capnproto.go b/front/capnproto.go
index 3d87d7b473c763b8789d757fb46a33f5478a411f..b2656f82d2d1f393f25a434b94740ce0a079814e 100644
--- a/front/capnproto.go
+++ b/front/capnproto.go
@@ -58,3 +58,7 @@
 func (CapnprotoRenderer) RenderWatchlist(watchlist datastructure.Watchlist, languages []language.Tag) string {
 	return TODO("implement CapnprotoRenderer.RenderWatchlist").(string)
 }
+
+func (CapnprotoRenderer) RenderExperiences(experiences datastructure.Experiences, languages []language.Tag) string {
+	return TODO("implement CapnprotoRenderer.RenderExperiences").(string)
+}




diff --git a/front/html.go b/front/html.go
index 9f3c0d199770332eef67a18153d830b1fc5ca1e2..3e96480f9dd37dba02c8c689e0f56c57233968ea 100644
--- a/front/html.go
+++ b/front/html.go
@@ -40,6 +40,10 @@ func (d RenderData) FormatDate(date time.Time) string {
 	return i18n.FormatDate(date, d.Strings.Global["date_format"], d.Strings.Global)
 }
 
+func (d RenderData) FormatDateNice(date time.Time, timezone string) string {
+	return i18n.FormatDateNice(date, d.Strings, timezone)
+}
+
 func (d RenderData) RenderAsciiDoc(s string) template.HTML {
 	return i18n.RenderAsciiDoc(s)
 }
@@ -139,3 +143,9 @@ 	data := RenderData{Data: watchlist}
 	data.State.User = r.user
 	return render(languages, data, "watchlist")
 }
+
+func (r HtmlRenderer) RenderExperiences(experiences datastructure.Experiences, languages []language.Tag) string {
+	data := RenderData{Data: experiences}
+	data.State.User = r.user
+	return render(languages, data, "experiences")
+}




diff --git a/front/renderer.go b/front/renderer.go
index 9c083c6e7cbe6fb5b35367825d986613462d0cd6..bb4b743bd5b287c25f4e50b395fe47ed12ed76f8 100644
--- a/front/renderer.go
+++ b/front/renderer.go
@@ -29,6 +29,7 @@ 	RenderAbout([]language.Tag) string
 	RenderErrorPage(int, []language.Tag) string
 	RenderLogin([]language.Tag, error, string) string
 	RenderWatchlist(datastructure.Watchlist, []language.Tag) string
+	RenderExperiences(datastructure.Experiences, []language.Tag) string
 }
 
 func NewRenderer(mimetype string, user accounts.User) (Renderer, error) {




diff --git a/i18n/en-GB.toml b/i18n/en-GB.toml
index 3c46225631b4d5c5b863400b1bd5f8d422091031..eb0309044f33f609afb6958759598b2b49214590 100644
--- a/i18n/en-GB.toml
+++ b/i18n/en-GB.toml
@@ -11,6 +11,7 @@ [global]
 # format as per POSIX date(1p) without %c, %r, %x, %X
 date_format = "%d %B %Y"
 date_format_full = "%d %B %Y, %H:%M"
+date_format_time = "%H:%M"
 
 experience_format_today = "today"
 experience_format_yesterday = "yesterday"




diff --git a/libamuse/experiences.go b/libamuse/experiences.go
new file mode 100644
index 0000000000000000000000000000000000000000..fecffbc08697ad7310e7f2472693fb62f08eb8fd
--- /dev/null
+++ b/libamuse/experiences.go
@@ -0,0 +1,71 @@
+package libamuse
+
+import (
+	"notabug.org/apiote/amuse/accounts"
+	"notabug.org/apiote/amuse/datastructure"
+	"notabug.org/apiote/amuse/db"
+
+	"notabug.org/apiote/gott"
+
+	"time"
+)
+
+func getExperiences(args ...interface{}) (interface{}, error) {
+	request := args[0].(*RequestData)
+	result := args[1].(*Result)
+	page := args[2].(int)
+	experiences, err := db.GetUserExperiences(result.user.Username, request.id, page)
+	result.result = experiences
+
+	return gott.Tuple(args), err
+}
+
+func parseExperienceDates(args ...interface{}) interface{} {
+	result := args[1].(*Result)
+	experiences := result.result.(datastructure.Experiences)
+	location, _ := time.LoadLocation(result.user.Timezone)
+	for i, experience := range experiences.List {
+		experience.Datetime = experience.Datetime.In(location)
+		experiences.List[i] = experience
+	}
+	result.result = experiences
+
+	return gott.Tuple(args)
+}
+
+func renderExperiences(args ...interface{}) interface{} {
+	request := args[0].(*RequestData)
+	result := args[1].(*Result)
+	page := args[2].(int)
+	experiences := result.result.(datastructure.Experiences)
+	experiences.Page = page
+	experiences.Query = request.id
+	result.page = result.renderer.RenderExperiences(experiences, result.languages)
+
+	return gott.Tuple(args)
+}
+
+func ShowExperiences(username string, auth accounts.Authentication, languages, mimetype, filter string, page int) (string, error) {
+	auth.Necessary = true
+	if page <= 0 {
+		page = 1
+	}
+
+	request := &RequestData{id: filter, language: languages, mimetype: mimetype, auth: auth, username: username}
+	r, err := gott.
+		NewResult(gott.Tuple{request, &Result{}, page}).
+		Bind(parseLanguage).
+		Bind(verifyToken).
+		Bind(verifyUser).
+		Bind(getExperiences).
+		Map(parseExperienceDates).
+		Bind(createRenderer).
+		Map(renderExperiences).
+		Finish()
+
+	if err != nil {
+		return "", err
+	} else {
+		return r.(gott.Tuple)[1].(*Result).page, nil
+	}
+}




diff --git a/router.go b/router.go
index 3047e0319638c6f45e182a87c722e8e0b035d3ef..d395cf8677d7a01d89895e384a4860e7766f7a65 100644
--- a/router.go
+++ b/router.go
@@ -320,7 +320,12 @@ }
 
 func userExperiences(w http.ResponseWriter, r *http.Request, username string, auth accounts.Authentication, acceptLanguages string, mimetype string) {
 	if r.Method == "" || r.Method == "GET" {
-		// todo
+		var page int
+		r.ParseForm()
+		filter := r.Form.Get("filter")
+		fmt.Sscanf(r.Form.Get("page"), "%d", &page)
+		experiences, err := libamuse.ShowExperiences(username, auth, acceptLanguages, mimetype, filter, page)
+		render(experiences, err, w, acceptLanguages, mimetype)
 	} else if r.Method == "POST" {
 		r.ParseForm()
 		itemId := r.PostForm.Get("itemId")




diff --git a/static/style/style.css b/static/style/style.css
index 1f5d73f48342cad85ab13e64a6cf3c5495f32f14..932095c6bc9ffb7fdd54d05e828a5fe6be7f2763 100644
--- a/static/style/style.css
+++ b/static/style/style.css
@@ -301,6 +301,10 @@ .margin-top-0 {
 	margin-top: 0;
 }
 
+.margin-top-_25 {
+	margin-top: .25rem;
+}
+
 .margin-top-1 {
 	margin-top: 1rem;
 }
@@ -311,6 +315,10 @@ }
 
 .margin-bottom-_5 {
 	margin-bottom: .5rem;
+}
+
+.margin-bottom-1 {
+	margin-bottom: 1rem;
 }
 
 .margin-bottom-2 {




diff --git a/templates/experiences.html b/templates/experiences.html
new file mode 100644
index 0000000000000000000000000000000000000000..8b34d1b9650afd79c4a691cca7e0fe7a839f7476
--- /dev/null
+++ b/templates/experiences.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta charset="UTF-8">
+		<meta name="viewport" content="width=device-width, initial-scale=1.0">
+		<title>{{.State.User.Username}}’s experiences — a·muse</title>
+		<link rel="stylesheet" href="/static/style/style.css" />
+		<link rel="icon" type="image/svg+xml" href="/static/img/logo.svg">
+		<link rel="apple-touch-icon" type="image/svg+xml" href="/static/img/logo.svg">
+	</head>
+	<body>
+		<header class="w12 padding-bottom-_25 flex flex-row flex-justify-space flex-align-centre">
+			<a href="/" class="decoration-none">
+				<h1 class="inline valign-mid text sans margin-lr-1">a·muse</h1>
+			</a>
+			<div class="margin-lr-1 text">
+				<nav>
+					<label for="hamburger" class="cursor-hand">
+						<img src="/users/{{.State.User.Username}}/avatar?size=small" class="border-radius-25 width-1_5"/>
+					</label>
+					<input type="checkbox" id="hamburger" class="display-none" />
+					<ul class="absolute right top-1 padding-lr-1 padding-tb-_5 bg align-right list-style-none sans">
+						<li><a href="/users/{{.State.User.Username}}" class="decoration-none text-accent">Profile</a><span class="material-icon padding-lr-_5">&#xe851;</span></li>
+						<li><a href="/users/{{.State.User.Username}}/watchlist" class="decoration-none text-accent">Watchlist</a><span class="material-icon padding-lr-_5">&#xe04a;</span></li>
+						<li><a href="/users/{{.State.User.Username}}/readlist" class="decoration-none text-accent">Readlist</a><span class="material-icon padding-lr-_5">&#xe431;</span></li>
+						<li><a href="/users/{{.State.User.Username}}/experiences" class="decoration-none text-accent">Experiences</a><span class="material-icon padding-lr-_5">&#xe042;</span></li>
+						<li class="bg-error">
+							<form action="/users/{{.State.User.Username}}/sessions/{{.State.User.Session}}" method="POST" class="inline">
+								<input type="hidden" value="DELETE" name="method" />
+								<input type="submit" value="Log out" class="border-none bg-none font-normal text-accent padding-lr-0 cursor-hand font-1" />
+							</form><span class="material-icon padding-lr-_5">&#xe7ff;</span>
+						</li>
+					</ul>
+				</nav>
+			</div>
+		</header>
+		<main class="margin-lr-1">
+			<!-- search, filter, order -->
+			<div class="flex flex-row flex-wrap flex-centre flex-align-start margin-top-1">
+				<form method="GET" class="flex inline margin-lr-1 border-bottom">
+					<input type="search" name="filter" class="border-none bg-none sans text" placeholder="filter experiences" value="{{.Data.Query}}" />
+				</form>
+			</div>
+			<div class="flex flex-row flex-wrap flex-justify-space flex-align-start margin-top-1">
+				<div>
+					{{if gt .Data.Page 1}}
+					<a href="/users/{{.State.User.Username}}/experiences?filter={{.Data.Query}}&page={{.Data.PrevPage}}" class="decoration-none" title="{{.Strings.Search.prev_link_title}}"><span class="material-icon font-2">&#xe408;</span></a>
+					{{end}}
+				</div>
+				<div>
+					{{if lt .Data.Page .Data.Pages}}
+					<a href="/users/{{.State.User.Username}}/experiences?filter={{.Data.Query}}&page={{.Data.NextPage}}" class="decoration-none" title="{{.Strings.Search.next_link_title}}"><span class="material-icon font-2">&#xe409;</span></a>
+					{{end}}
+				</div>
+			</div>
+			<div class="flex flex-align-start">
+				{{- $lastDate:="" -}}
+				{{- range .Data.List -}}
+					{{- if ne $lastDate ($.FormatDate .Datetime)}}
+				<div class="w12 margin-lr-5">
+					<span class="sans">{{$.FormatDateNice .Datetime $.State.User.Timezone}}</span><hr class="margin-top-_25 margin-bottom-1"/>
+					{{- end -}}
+					<p>
+					<span class="sans">{{.FormatDatetime $.Strings}}</span>
+					<a href="/{{.Type}}s/{{.Id}}" class="sans decoration-none">{{.Title}} ({{.YearStart}})</a>
+					<!-- if series then with item_id=id/code show code -->
+						{{- if gt .Collection 0 -}}
+					({{.Collection}} <!-- collection name and link --> #{{.Part}})
+						{{- end -}}
+					</p>
+					{{- if and (ne $lastDate ($.FormatDate .Datetime)) (ne $lastDate "")}}
+				</div>
+					{{end}}
+					{{$lastDate = ($.FormatDate .Datetime) -}}
+				{{end}}
+				</div>
+			</div>
+			<div class="flex flex-row flex-wrap flex-justify-space flex-align-start margin-top-1">
+				<div>
+					{{if gt .Data.Page 1}}
+					<a href="/users/{{.State.User.Username}}/experiences?filter={{.Data.Query}}&page={{.Data.PrevPage}}" class="decoration-none" title="{{.Strings.Search.prev_link_title}}"><span class="material-icon font-2">&#xe408;</span></a>
+					{{end}}
+				</div>
+				<div>
+					{{if lt .Data.Page .Data.Pages}}
+					<a href="/users/{{.State.User.Username}}/experiences?filter={{.Data.Query}}&page={{.Data.NextPage}}" class="decoration-none" title="{{.Strings.Search.next_link_title}}"><span class="material-icon font-2">&#xe409;</span></a>
+					{{end}}
+				</div>
+			</div>
+		</main>
+	</body>
+</html>