Compare commits

...

12 Commits

Author SHA1 Message Date
Frédéric Guillot dd68181a83 Update ChangeLog 2018-03-05 21:42:52 -08:00
Frédéric Guillot a8be61cdbb Filter the list of timezones 2018-03-04 17:38:08 -08:00
Frédéric Guillot 609c57332e Show last login and session creation date in current timezone 2018-03-04 17:04:31 -08:00
Frédéric Guillot 5185bf0c7e Fix typo in edit user template 2018-03-01 23:06:28 -08:00
Frédéric Guillot cbd273da2b Improve for/range loop to avoid linter error 2018-03-01 21:43:25 -08:00
Frédéric Guillot 0c7039de0e Entries date should contains user timezone (API) 2018-03-01 21:43:04 -08:00
Frédéric Guillot f110384f11 Improve parser error messages 2018-02-27 21:19:59 -08:00
Frédéric Guillot 953d0a2dc0 Support localized feed errors generated by background workers 2018-02-27 21:08:32 -08:00
ReVanTis, Zhao 9694861cb6 Add Simplified Chinese Localization 2018-02-27 19:58:51 -08:00
Nicolas Carlier 34ce114231 Add Nunux Keeper integration 2018-02-25 11:49:08 -08:00
Frédéric Guillot 3030145b30 Remove parentheses around feed error messages 2018-02-23 19:58:08 -08:00
Frédéric Guillot a9f0fdaf22 Print info message if DATABASE_URL is not set 2018-02-23 18:26:34 -08:00
57 changed files with 949 additions and 165 deletions
+14
View File
@@ -1,3 +1,17 @@
Version 2.0.4 (Mar 5, 2018)
---------------------------
* Add Simplified Chinese translation
* Add Nunux Keeper integration
* Filter the list of timezones
* Add timezone to entries dates for REST and Fever API
* Show last login and session creation date in current timezone
* Fix typo in edit user template
* Improve parser error messages
* Remove parentheses around feed error messages
* Support localized feed errors generated by background workers
* Print info message if DATABASE_URL is not set
Version 2.0.3 (Feb 19, 2018)
----------------------------
+2
View File
@@ -100,6 +100,7 @@ func (c *Controller) Users(ctx *handler.Context, request *handler.Request, respo
return
}
users.UseTimezone(ctx.UserTimezone())
response.JSON().Standard(users)
}
@@ -127,6 +128,7 @@ func (c *Controller) UserByID(ctx *handler.Context, request *handler.Request, re
return
}
user.UseTimezone(ctx.UserTimezone())
response.JSON().Standard(user)
}
+5 -4
View File
@@ -28,6 +28,11 @@ func Parse() {
flag.Parse()
cfg := config.NewConfig()
if *flagDebugMode || cfg.HasDebugMode() {
logger.EnableDebug()
}
store := storage.NewStorage(
cfg.DatabaseURL(),
cfg.DatabaseMaxConnections(),
@@ -63,9 +68,5 @@ func Parse() {
return
}
if *flagDebugMode || cfg.HasDebugMode() {
logger.EnableDebug()
}
daemon.Run(cfg, store)
}
+12 -1
View File
@@ -8,6 +8,8 @@ import (
"net/url"
"os"
"strconv"
"github.com/miniflux/miniflux/logger"
)
const (
@@ -89,7 +91,16 @@ func (c *Config) BasePath() string {
// DatabaseURL returns the database URL.
func (c *Config) DatabaseURL() string {
return c.get("DATABASE_URL", defaultDatabaseURL)
value, exists := os.LookupEnv("DATABASE_URL")
if !exists {
logger.Info("The environment variable DATABASE_URL is not configured (the default value is used instead)")
}
if value == "" {
value = defaultDatabaseURL
}
return value
}
// DatabaseMaxConnections returns the number of maximum database connections.
+4 -2
View File
@@ -12,6 +12,7 @@ import (
"time"
"github.com/miniflux/miniflux/config"
"github.com/miniflux/miniflux/locale"
"github.com/miniflux/miniflux/logger"
"github.com/miniflux/miniflux/reader/feed"
"github.com/miniflux/miniflux/scheduler"
@@ -26,9 +27,10 @@ func Run(cfg *config.Config, store *storage.Storage) {
signal.Notify(stop, os.Interrupt)
signal.Notify(stop, syscall.SIGTERM)
feedHandler := feed.NewFeedHandler(store)
translator := locale.Load()
feedHandler := feed.NewFeedHandler(store, translator)
pool := scheduler.NewWorkerPool(feedHandler, cfg.WorkerPoolSize())
server := newServer(cfg, store, pool, feedHandler)
server := newServer(cfg, store, pool, feedHandler, translator)
scheduler.NewFeedScheduler(
store,
+1 -2
View File
@@ -23,9 +23,8 @@ import (
"github.com/gorilla/mux"
)
func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handler, pool *scheduler.WorkerPool) *mux.Router {
func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handler, pool *scheduler.WorkerPool, translator *locale.Translator) *mux.Router {
router := mux.NewRouter()
translator := locale.Load()
templateEngine := template.NewEngine(cfg, router, translator)
apiController := api.NewController(store, feedHandler)
+3 -2
View File
@@ -10,6 +10,7 @@ import (
"time"
"github.com/miniflux/miniflux/config"
"github.com/miniflux/miniflux/locale"
"github.com/miniflux/miniflux/logger"
"github.com/miniflux/miniflux/reader/feed"
"github.com/miniflux/miniflux/scheduler"
@@ -18,7 +19,7 @@ import (
"golang.org/x/crypto/acme/autocert"
)
func newServer(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler) *http.Server {
func newServer(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler, translator *locale.Translator) *http.Server {
certFile := cfg.CertFile()
keyFile := cfg.KeyFile()
certDomain := cfg.CertDomain()
@@ -28,7 +29,7 @@ func newServer(cfg *config.Config, store *storage.Storage, pool *scheduler.Worke
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
Addr: cfg.ListenAddr(),
Handler: routes(cfg, store, feedHandler, pool),
Handler: routes(cfg, store, feedHandler, pool, translator),
}
if certDomain != "" && certCache != "" {
+2 -2
View File
@@ -27,6 +27,6 @@ func (l LocalizedError) Localize(translation *locale.Language) string {
}
// NewLocalizedError returns a new LocalizedError.
func NewLocalizedError(message string, args ...interface{}) LocalizedError {
return LocalizedError{message: message, args: args}
func NewLocalizedError(message string, args ...interface{}) *LocalizedError {
return &LocalizedError{message: message, args: args}
}
+2 -1
View File
@@ -149,11 +149,12 @@ func (c *Client) buildRequest(method string, body io.Reader) (*http.Request, err
return nil, err
}
request.Header = c.buildHeaders()
if c.username != "" && c.password != "" {
request.SetBasicAuth(c.username, c.password)
}
request.Header = c.buildHeaders()
return request, nil
}
+12
View File
@@ -6,6 +6,7 @@ package integration
import (
"github.com/miniflux/miniflux/integration/instapaper"
"github.com/miniflux/miniflux/integration/nunuxkeeper"
"github.com/miniflux/miniflux/integration/pinboard"
"github.com/miniflux/miniflux/integration/wallabag"
"github.com/miniflux/miniflux/logger"
@@ -48,4 +49,15 @@ func SendEntry(entry *model.Entry, integration *model.Integration) {
logger.Error("[Integration] %v", err)
}
}
if integration.NunuxKeeperEnabled {
client := nunuxkeeper.NewClient(
integration.NunuxKeeperURL,
integration.NunuxKeeperAPIKey,
)
if err := client.AddEntry(entry.URL, entry.Title, entry.Content); err != nil {
logger.Error("[Integration] %v", err)
}
}
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package nunuxkeeper
import (
"fmt"
"net/url"
"path"
"github.com/miniflux/miniflux/http"
)
// Document structure of a Nununx Keeper document
type Document struct {
Title string `json:"title,omitempty"`
Origin string `json:"origin,omitempty"`
Content string `json:"content,omitempty"`
ContentType string `json:"contentType,omitempty"`
}
// Client represents an Nunux Keeper client.
type Client struct {
baseURL string
apiKey string
}
// AddEntry sends an entry to Nunux Keeper.
func (c *Client) AddEntry(link, title, content string) error {
doc := &Document{
Title: title,
Origin: link,
Content: content,
ContentType: "text/html",
}
apiURL, err := getAPIEndpoint(c.baseURL, "/v2/documents")
if err != nil {
return err
}
client := http.NewClientWithCredentials(apiURL, "api", c.apiKey)
response, err := client.PostJSON(doc)
if response.HasServerFailure() {
return fmt.Errorf("nunux-keeper: unable to send entry, status=%d", response.StatusCode)
}
return err
}
// NewClient returns a new Nunux Keeepr client.
func NewClient(baseURL, apiKey string) *Client {
return &Client{baseURL: baseURL, apiKey: apiKey}
}
func getAPIEndpoint(baseURL, pathURL string) (string, error) {
u, err := url.Parse(baseURL)
if err != nil {
return "", fmt.Errorf("nunux-keeper: invalid API endpoint: %v", err)
}
u.Path = path.Join(u.Path, pathURL)
return u.String(), nil
}
+1
View File
@@ -31,5 +31,6 @@ func AvailableLanguages() map[string]string {
"fr_FR": "Français",
"de_DE": "Deutsch",
"pl_PL": "Polski",
"zh_CN": "简体中文",
}
}
+255 -23
View File
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
// 2018-02-19 22:49:00.3105786 +0100 STD m=+0.012978001
// 2018-02-27 21:15:00.586846241 -0800 PST m=+0.029084447
package locale
@@ -116,12 +116,12 @@ var translations = map[string]string{
"Unable to execute request: %v": "Diese Anfrage konnte nicht ausgeführt werden: %v",
"Last Parsing Error": "Letzter Analysefehler",
"There is a problem with this feed": "Es gibt ein Problem mit diesem Abonnement",
"Unable to parse OPML file: %v.": "OPML Datei konnte nicht gelesen werden: %v.",
"Unable to parse RSS feed: %v.": "RSS Feed konnte nicht gelesen werden: %v.",
"Unable to parse Atom feed: %v.": "Atom Feed konnte nicht gelesen werden: %v.",
"Unable to parse JSON feed: %v.": "JSON Feed konnte nicht gelesen werden: %v.",
"Unable to parse RDF feed: %v.": "RDF Feed konnte nicht gelesen werden: %v.",
"Unable to normalize encoding: %v": "Zeichenkodierung konnte nicht normalisiert werden: %v",
"Unable to parse OPML file: %q": "OPML Datei konnte nicht gelesen werden: %q",
"Unable to parse RSS feed: %q": "RSS Feed konnte nicht gelesen werden: %q",
"Unable to parse Atom feed: %q": "Atom Feed konnte nicht gelesen werden: %q",
"Unable to parse JSON feed: %q": "JSON Feed konnte nicht gelesen werden: %q",
"Unable to parse RDF feed: %q": "RDF Feed konnte nicht gelesen werden: %q",
"Unable to normalize encoding: %q": "Zeichenkodierung konnte nicht normalisiert werden: %q",
"Unable to create this category.": "Diese Kategorie konnte nicht angelegt werden.",
"yes": "ja",
"no": "nein",
@@ -168,6 +168,9 @@ var translations = map[string]string{
"Wallabag Client Secret": "Wallabag Client-Secret",
"Wallabag Username": "Wallabag Benutzername",
"Wallabag Password": "Wallabag Passwort",
"Save articles to Nunux Keeper": "Artikel in Nunux Keeper speichern",
"Nunux Keeper API Endpoint": "Nunux Keeper API-Endpunkt",
"Nunux Keeper API key": "Nunux Keeper API-Schlüssel",
"Keyboard Shortcut: %s": "Tastenkürzel: %s",
"Favorites": "Lesezeichen",
"Star": "Lesezeichen hinzufügen",
@@ -345,12 +348,12 @@ var translations = map[string]string{
"Unable to execute request: %v": "Impossible d'exécuter cette requête: %v",
"Last Parsing Error": "Dernière erreur d'analyse",
"There is a problem with this feed": "Il y a un problème avec cet abonnement",
"Unable to parse OPML file: %v.": "Impossible de lire ce fichier OPML : %v.",
"Unable to parse RSS feed: %v.": "Impossible de lire ce flux RSS: %v.",
"Unable to parse Atom feed: %v.": "Impossible de lire ce flux Atom: %v.",
"Unable to parse JSON feed: %v.": "Impossible de lire ce flux JSON: %v.",
"Unable to parse RDF feed: %v.": "Impossible de lire ce flux RDF: %v.",
"Unable to normalize encoding: %v": "Impossible de normaliser l'encodage : %v",
"Unable to parse OPML file: %q": "Impossible de lire ce fichier OPML : %q",
"Unable to parse RSS feed: %q": "Impossible de lire ce flux RSS : %q",
"Unable to parse Atom feed: %q": "Impossible de lire ce flux Atom : %q",
"Unable to parse JSON feed: %q": "Impossible de lire ce flux JSON : %q",
"Unable to parse RDF feed: %q": "Impossible de lire ce flux RDF : %q",
"Unable to normalize encoding: %q": "Impossible de normaliser l'encodage : %q",
"Unable to create this category.": "Impossible de créer cette catégorie.",
"yes": "oui",
"no": "non",
@@ -397,6 +400,9 @@ var translations = map[string]string{
"Wallabag Client Secret": "Clé secrète du client Wallabag",
"Wallabag Username": "Nom d'utilisateur de Wallabag",
"Wallabag Password": "Mot de passe de Wallabag",
"Save articles to Nunux Keeper": "Sauvegarder les articles vers Nunux Keeper",
"Nunux Keeper API Endpoint": "URL de l'API de Nunux Keeper",
"Nunux Keeper API key": "Clé d'API de Nunux Keeper",
"Keyboard Shortcut: %s": "Raccourci clavier : %s",
"Favorites": "Favoris",
"Star": "Favoris",
@@ -460,7 +466,7 @@ var translations = map[string]string{
],
"plural.categories.feed_count": [
"Jest %d kanał.",
"Są %d kanały.",
"Są %d kanały.",
"Jest %d kanałów."
],
"Username": "Nazwa użytkownika",
@@ -566,12 +572,12 @@ var translations = map[string]string{
"Unable to execute request: %v": "To polecenie nie mogło zostać wykonane: %v",
"Last Parsing Error": "Ostatni błąd analizy",
"There is a problem with this feed": "Z tym kanałem jest problem",
"Unable to parse OPML file: %v.": "Plik OPML nie mógł zostać odczytany: %v.",
"Unable to parse RSS feed: %v.": "Nie można było odczytać kanału RSS: %v.",
"Unable to parse Atom feed: %v.": "Nie można było odczytać kanału Atom: %v.",
"Unable to parse JSON feed: %v.": "Nie można było odczytać kanału JSON: %v.",
"Unable to parse RDF feed: %v.": "Nie można było odczytać kanału RDF: %v.",
"Unable to normalize encoding: %v": "Kodowanie znaków nie mogło zostać znormalizowane: %v",
"Unable to parse OPML file: %q": "Plik OPML nie mógł zostać odczytany: %q",
"Unable to parse RSS feed: %q": "Nie można było odczytać kanału RSS: %q",
"Unable to parse Atom feed: %q": "Nie można było odczytać kanału Atom: %q",
"Unable to parse JSON feed: %q": "Nie można było odczytać kanału JSON: %q",
"Unable to parse RDF feed: %q": "Nie można było odczytać kanału RDF: %q",
"Unable to normalize encoding: %q": "Kodowanie znaków nie mogło zostać znormalizowane: %q",
"Unable to create this category.": "Ta kategoria nie mogła zostać utworzona.",
"yes": "tak",
"no": "nie",
@@ -618,6 +624,9 @@ var translations = map[string]string{
"Wallabag Client Secret": "Wallabag Client Secret",
"Wallabag Username": "Login do Wallabag",
"Wallabag Password": "Hasło do Wallabag",
"Save articles to Nunux Keeper": "Zapisz artykuly do Nunux Keeper",
"Nunux Keeper API Endpoint": "Nunux Keeper URL",
"Nunux Keeper API key": "Nunux Keeper API key",
"Keyboard Shortcut: %s": "Skróty klawiszowe: %s",
"Favorites": "Ulubione",
"Star": "Oznacz gwiazdką",
@@ -672,12 +681,235 @@ var translations = map[string]string{
"This website is permanently unreachable (original error: %q)": "Ta strona jest niedostępna (błąd: %q)",
"Website unreachable, the request timed out after %d seconds": "Strona internetowa nieosiągalna, żądanie wygasło po %d sekundach"
}
`,
"zh_CN": `{
"plural.feed.error_count": [
"%d 错误",
"%d 错误"
],
"plural.categories.feed_count": [
"有 %d 个源.",
"有 %d 个源."
],
"Username": "用户名",
"Password": "密码",
"Unread": "未读",
"History": "历史",
"Feeds": "源",
"Categories": "分类",
"Settings": "设置",
"Logout": "登出",
"Next": "下一页",
"Previous": "上一页",
"New Subscription": "新订阅",
"Import": "导入",
"Export": "导出",
"There is no category. You must have at least one category.": "目前没有分类.需要有一个已有的分类存在.",
"URL": "URL",
"Category": "分类",
"Find a subscription": "寻找订阅",
"Loading...": "载入中...",
"Create a category": "新建分类",
"There is no category.": "目前没有分类.",
"Edit": "编辑",
"Remove": "删除",
"No feed.": "没有源.",
"There is no article in this category.": "该分类下没有文章.",
"Original": "原始链接",
"Mark this page as read": "标记为已读",
"not yet": "尚未",
"just now": "刚刚",
"1 minute ago": "1 分钟前",
"%d minutes ago": "%d 分钟前",
"1 hour ago": "1 小时前",
"%d hours ago": "%d 小时前",
"yesterday": "昨天",
"%d days ago": "%d 天前",
"%d weeks ago": "%d 周前",
"%d months ago": "%d 月前",
"%d years ago": "%d 年前",
"Date": "日期",
"IP Address": "IP地址",
"User Agent": "User Agent",
"Actions": "操作",
"Current session": "当前会话",
"Sessions": "会话",
"Users": "用户",
"Add user": "新建用户",
"Choose a Subscription": "选择一个订阅",
"Subscribe": "订阅",
"New Category": "新分类",
"Title": "标题",
"Save": "保存",
"or": "或",
"cancel": "取消",
"New User": "新用户",
"Confirmation": "确认",
"Administrator": "管理员",
"Edit Category: %s": "编辑分类 : %s",
"Update": "更新",
"Edit Feed: %s": "编辑源 : %s",
"There is no category!": "没有分类!",
"Edit user: %s": "编辑用户: %s",
"There is no article for this feed.": "这个源中没有文章.",
"Add subscription": "新增订阅",
"You don't have any subscription.": "当前没有订阅",
"Last check:": "最后检查时间:",
"Refresh": "更新",
"There is no history at the moment.": "当前没有历史.",
"OPML file": "OPML 文件",
"Sign In": "登陆",
"Sign in": "登陆",
"Theme": "主题",
"Timezone": "时区",
"Language": "语言",
"There is no unread article.": "目前没有未读文章.",
"You are the only user.": "你是目前仅有的用户.",
"Last Login": "最后登录时间",
"Yes": "是",
"No": "否",
"This feed already exists (%s)": "源已存在 (%s)",
"Unable to fetch feed (statusCode=%d)": "无法获取源 (错误代码=%d)",
"Unable to open this link: %v": "无法打开这一链接: %v",
"Unable to analyze this page: %v": "无法分析这一页面: %v",
"Unable to find any subscription.": "找不到任何订阅.",
"The URL and the category are mandatory.": "必须填写URL和分类.",
"All fields are mandatory.": "必须填写全部信息.",
"Passwords are not the same.": "两次输入的密码不同.",
"You must use at least 6 characters.": "请至少使用6个字符.",
"The username is mandatory.": "必须填写用户名.",
"The username, theme, language and timezone fields are mandatory.": "必须填写用户名,主题,语言和时区.",
"The title is mandatory.": "必须填写标题.",
"About": "关于",
"version": "版本",
"Version:": "版本:",
"Build Date:": "构建日期:",
"Author:": "作者:",
"Authors": "作者",
"License:": "协议:",
"Attachments": "附件",
"Download": "下载",
"Invalid username or password.": "用户名或密码无效.",
"Never": "永不",
"Unable to execute request: %v": "无法执行这一请求: %v",
"Last Parsing Error": "最后一次解析错误",
"There is a problem with this feed": "这一源存在问题",
"Unable to parse OPML file: %q": "无法解析OPML文件: %q",
"Unable to parse RSS feed: %q": "无法解析RSS源: %q",
"Unable to parse Atom feed: %q": "无法解析Atom源: %q",
"Unable to parse JSON feed: %q": "无法解析JSON源: %q",
"Unable to parse RDF feed: %q": "无法解析RDF源: %q",
"Unable to normalize encoding: %q": "无法正则化编码: %q",
"Unable to create this category.": "无法建立这个分类.",
"yes": "是",
"no": "否",
"Are you sure?": "您确认吗?",
"Work in progress...": "执行中...",
"This user already exists.": "用户已存在.",
"This category already exists.": "分类已存在.",
"Unable to update this category.": "无法更新该分类.",
"Integrations": "集成",
"Bookmarklet": "书签小应用",
"Drag and drop this link to your bookmarks.": "拖动这个链接到书签.",
"This special link allows you to subscribe to a website directly by using a bookmark in your web browser.": "你可以打开这个特殊的书签来直接订阅网站.",
"Add to Miniflux": "新增到Miniflux",
"Refresh all feeds in background": "在后台更新全部源",
"Sign in with Google": "使用Google登陆",
"Unlink my Google account": "去除Google账号关联",
"Link my Google account": "关联我的Google账户",
"Category not found for this user": "未找到该用户的这一分类",
"Invalid theme.": "无效的主题.",
"Entry Sorting": "内容排序",
"Older entries first": "旧->新",
"Recent entries first": "新->旧",
"Saving...": "保存中",
"Done!": "完成!",
"Save this article": "保存这篇文章",
"Mark bookmark as unread": "标记为未读",
"Pinboard Tags": "Pinboard 标签",
"Pinboard API Token": "Pinboard API Token",
"Save articles to Pinboard": "保存文章到Pinboard",
"Save articles to Instapaper": "保存文章到Instapaper",
"Instapaper Username": "Instapaper 用户名",
"Instapaper Password": "Instapaper 密码",
"Activate Fever API": "启用 Fever API",
"Fever Username": "Fever 用户名",
"Fever Password": "Fever 密码",
"Fetch original content": "抓取原内容",
"Scraper Rules": "Scraper规则",
"Rewrite Rules": "重写规则",
"Preferences saved!": "偏好已存储!",
"Your external account is now linked !": "您的外部账号已关联!",
"Save articles to Wallabag": "保存文章到Wallabag",
"Wallabag API Endpoint": "Wallabag URL",
"Wallabag Client ID": "Wallabag 客户端ID",
"Wallabag Client Secret": "Wallabag 客户端Secret",
"Wallabag Username": "Wallabag 用户名",
"Wallabag Password": "Wallabag 密码",
"Save articles to Nunux Keeper": "保存文章到Nunux Keeper",
"Nunux Keeper API Endpoint": "Nunux Keeper API Endpoint",
"Nunux Keeper API key": "Nunux Keeper API key",
"Keyboard Shortcut: %s": "快捷键: %s",
"Favorites": "收藏",
"Star": "标记星标",
"Unstar": "去掉星标",
"Starred": "星标",
"There is no bookmark at the moment.": "当前没有书签.",
"Last checked:": "上次检查:",
"ETag header:": "ETag header:",
"LastModified header:": "最后修改的Header:",
"None": "无",
"Keyboard Shortcuts": "快捷键",
"Sections Navigation": "分区导航",
"Go to unread": "去往未读",
"Go to bookmarks": "去往书签",
"Go to history": "去往历史",
"Go to feeds": "去往源",
"Go to categories": "去往分类",
"Go to settings": "去往设定",
"Show keyboard shortcuts": "显示快捷键",
"Items Navigation": "条目导航",
"Go to previous item": "上一条目",
"Go to next item": "下一条目",
"Pages Navigation": "页面导航",
"Go to previous page": "上一页",
"Go to next page": "下一页",
"Open selected item": "打开选定的条目",
"Open original link": "打开原始链接",
"Toggle read/unread": "切换已读/未读状态",
"Mark current page as read": "标记当前",
"Download original content": "下载原始内容",
"Toggle bookmark": "切换收藏状态",
"Close modal dialog": "关闭模态对话窗口",
"Save article": "保存文章",
"There is already someone associated with this provider!": "该Provider已被关联!",
"There is already someone else with the same Fever username!": "Fever用户名已被占用!",
"Mark all as read": "全标记为已读",
"This feed is empty": "该源是空的",
"Flush history": "清理历史",
"Site URL": "站点URL",
"Feed URL": "源URL",
"Logged as %s": "当前登录 %s",
"Unread Items": "未读条目",
"Change entry status": "更改状态",
"Read": "标为已读",
"Fever API endpoint:": "Fever API Endpoint:",
"Miniflux API": "Miniflux API",
"API Endpoint": "API Endpoint",
"Your account password": "您账户的密码",
"This web page is empty": "该网页是空的",
"Invalid SSL certificate (original error: %q)": "无效的SSL证书 (原始错误: %q)",
"This website is temporarily unreachable (original error: %q)": "该网站暂时不可达 (原始错误: %q)",
"This website is permanently unreachable (original error: %q)": "该网站永久不可达 (原始错误: %q)",
"Website unreachable, the request timed out after %d seconds": "网站不可达, 请求已在 %d 秒后超时"
}
`,
}
var translationsChecksums = map[string]string{
"de_DE": "53f7637318ac418ce0e3dd483923dab39ff4f4062b4909e5e03efcc3b693e5d6",
"de_DE": "da3e70c096b35c205d89dddd400bbf34927bb495d4ee0f4eb3c3dc04e02b99c1",
"en_US": "6fe95384260941e8a5a3c695a655a932e0a8a6a572c1e45cb2b1ae8baa01b897",
"fr_FR": "ae61f82ac14bc2c6c6a3c2d38cf1ad8309ac2eef19b0726b2969ac155ccddc14",
"pl_PL": "9c10899ec62f97ebb6d5d4d88cde8c68ac584e514ce840b51ba3aeff9ea3efe3",
"fr_FR": "e842d6503b4d50ba5e3cd862b3d92c64f031356cf87f9989d2ac9a1ba0246ac8",
"pl_PL": "0d8a76425cf634b96cfc425127d8e83db7662e2d4dbc30674098e3fb6cea7c8d",
"zh_CN": "c19cb45a49af7957748fa006b51421edaa9774ef1ab0e91eb2c0552635016b62",
}
+9 -6
View File
@@ -110,12 +110,12 @@
"Unable to execute request: %v": "Diese Anfrage konnte nicht ausgeführt werden: %v",
"Last Parsing Error": "Letzter Analysefehler",
"There is a problem with this feed": "Es gibt ein Problem mit diesem Abonnement",
"Unable to parse OPML file: %v.": "OPML Datei konnte nicht gelesen werden: %v.",
"Unable to parse RSS feed: %v.": "RSS Feed konnte nicht gelesen werden: %v.",
"Unable to parse Atom feed: %v.": "Atom Feed konnte nicht gelesen werden: %v.",
"Unable to parse JSON feed: %v.": "JSON Feed konnte nicht gelesen werden: %v.",
"Unable to parse RDF feed: %v.": "RDF Feed konnte nicht gelesen werden: %v.",
"Unable to normalize encoding: %v": "Zeichenkodierung konnte nicht normalisiert werden: %v",
"Unable to parse OPML file: %q": "OPML Datei konnte nicht gelesen werden: %q",
"Unable to parse RSS feed: %q": "RSS Feed konnte nicht gelesen werden: %q",
"Unable to parse Atom feed: %q": "Atom Feed konnte nicht gelesen werden: %q",
"Unable to parse JSON feed: %q": "JSON Feed konnte nicht gelesen werden: %q",
"Unable to parse RDF feed: %q": "RDF Feed konnte nicht gelesen werden: %q",
"Unable to normalize encoding: %q": "Zeichenkodierung konnte nicht normalisiert werden: %q",
"Unable to create this category.": "Diese Kategorie konnte nicht angelegt werden.",
"yes": "ja",
"no": "nein",
@@ -162,6 +162,9 @@
"Wallabag Client Secret": "Wallabag Client-Secret",
"Wallabag Username": "Wallabag Benutzername",
"Wallabag Password": "Wallabag Passwort",
"Save articles to Nunux Keeper": "Artikel in Nunux Keeper speichern",
"Nunux Keeper API Endpoint": "Nunux Keeper API-Endpunkt",
"Nunux Keeper API key": "Nunux Keeper API-Schlüssel",
"Keyboard Shortcut: %s": "Tastenkürzel: %s",
"Favorites": "Lesezeichen",
"Star": "Lesezeichen hinzufügen",
+9 -6
View File
@@ -110,12 +110,12 @@
"Unable to execute request: %v": "Impossible d'exécuter cette requête: %v",
"Last Parsing Error": "Dernière erreur d'analyse",
"There is a problem with this feed": "Il y a un problème avec cet abonnement",
"Unable to parse OPML file: %v.": "Impossible de lire ce fichier OPML : %v.",
"Unable to parse RSS feed: %v.": "Impossible de lire ce flux RSS: %v.",
"Unable to parse Atom feed: %v.": "Impossible de lire ce flux Atom: %v.",
"Unable to parse JSON feed: %v.": "Impossible de lire ce flux JSON: %v.",
"Unable to parse RDF feed: %v.": "Impossible de lire ce flux RDF: %v.",
"Unable to normalize encoding: %v": "Impossible de normaliser l'encodage : %v",
"Unable to parse OPML file: %q": "Impossible de lire ce fichier OPML : %q",
"Unable to parse RSS feed: %q": "Impossible de lire ce flux RSS : %q",
"Unable to parse Atom feed: %q": "Impossible de lire ce flux Atom : %q",
"Unable to parse JSON feed: %q": "Impossible de lire ce flux JSON : %q",
"Unable to parse RDF feed: %q": "Impossible de lire ce flux RDF : %q",
"Unable to normalize encoding: %q": "Impossible de normaliser l'encodage : %q",
"Unable to create this category.": "Impossible de créer cette catégorie.",
"yes": "oui",
"no": "non",
@@ -162,6 +162,9 @@
"Wallabag Client Secret": "Clé secrète du client Wallabag",
"Wallabag Username": "Nom d'utilisateur de Wallabag",
"Wallabag Password": "Mot de passe de Wallabag",
"Save articles to Nunux Keeper": "Sauvegarder les articles vers Nunux Keeper",
"Nunux Keeper API Endpoint": "URL de l'API de Nunux Keeper",
"Nunux Keeper API key": "Clé d'API de Nunux Keeper",
"Keyboard Shortcut: %s": "Raccourci clavier : %s",
"Favorites": "Favoris",
"Star": "Favoris",
+10 -7
View File
@@ -6,7 +6,7 @@
],
"plural.categories.feed_count": [
"Jest %d kanał.",
"Są %d kanały.",
"Są %d kanały.",
"Jest %d kanałów."
],
"Username": "Nazwa użytkownika",
@@ -112,12 +112,12 @@
"Unable to execute request: %v": "To polecenie nie mogło zostać wykonane: %v",
"Last Parsing Error": "Ostatni błąd analizy",
"There is a problem with this feed": "Z tym kanałem jest problem",
"Unable to parse OPML file: %v.": "Plik OPML nie mógł zostać odczytany: %v.",
"Unable to parse RSS feed: %v.": "Nie można było odczytać kanału RSS: %v.",
"Unable to parse Atom feed: %v.": "Nie można było odczytać kanału Atom: %v.",
"Unable to parse JSON feed: %v.": "Nie można było odczytać kanału JSON: %v.",
"Unable to parse RDF feed: %v.": "Nie można było odczytać kanału RDF: %v.",
"Unable to normalize encoding: %v": "Kodowanie znaków nie mogło zostać znormalizowane: %v",
"Unable to parse OPML file: %q": "Plik OPML nie mógł zostać odczytany: %q",
"Unable to parse RSS feed: %q": "Nie można było odczytać kanału RSS: %q",
"Unable to parse Atom feed: %q": "Nie można było odczytać kanału Atom: %q",
"Unable to parse JSON feed: %q": "Nie można było odczytać kanału JSON: %q",
"Unable to parse RDF feed: %q": "Nie można było odczytać kanału RDF: %q",
"Unable to normalize encoding: %q": "Kodowanie znaków nie mogło zostać znormalizowane: %q",
"Unable to create this category.": "Ta kategoria nie mogła zostać utworzona.",
"yes": "tak",
"no": "nie",
@@ -164,6 +164,9 @@
"Wallabag Client Secret": "Wallabag Client Secret",
"Wallabag Username": "Login do Wallabag",
"Wallabag Password": "Hasło do Wallabag",
"Save articles to Nunux Keeper": "Zapisz artykuly do Nunux Keeper",
"Nunux Keeper API Endpoint": "Nunux Keeper URL",
"Nunux Keeper API key": "Nunux Keeper API key",
"Keyboard Shortcut: %s": "Skróty klawiszowe: %s",
"Favorites": "Ulubione",
"Star": "Oznacz gwiazdką",
+221
View File
@@ -0,0 +1,221 @@
{
"plural.feed.error_count": [
"%d 错误",
"%d 错误"
],
"plural.categories.feed_count": [
"有 %d 个源.",
"有 %d 个源."
],
"Username": "用户名",
"Password": "密码",
"Unread": "未读",
"History": "历史",
"Feeds": "源",
"Categories": "分类",
"Settings": "设置",
"Logout": "登出",
"Next": "下一页",
"Previous": "上一页",
"New Subscription": "新订阅",
"Import": "导入",
"Export": "导出",
"There is no category. You must have at least one category.": "目前没有分类.需要有一个已有的分类存在.",
"URL": "URL",
"Category": "分类",
"Find a subscription": "寻找订阅",
"Loading...": "载入中...",
"Create a category": "新建分类",
"There is no category.": "目前没有分类.",
"Edit": "编辑",
"Remove": "删除",
"No feed.": "没有源.",
"There is no article in this category.": "该分类下没有文章.",
"Original": "原始链接",
"Mark this page as read": "标记为已读",
"not yet": "尚未",
"just now": "刚刚",
"1 minute ago": "1 分钟前",
"%d minutes ago": "%d 分钟前",
"1 hour ago": "1 小时前",
"%d hours ago": "%d 小时前",
"yesterday": "昨天",
"%d days ago": "%d 天前",
"%d weeks ago": "%d 周前",
"%d months ago": "%d 月前",
"%d years ago": "%d 年前",
"Date": "日期",
"IP Address": "IP地址",
"User Agent": "User Agent",
"Actions": "操作",
"Current session": "当前会话",
"Sessions": "会话",
"Users": "用户",
"Add user": "新建用户",
"Choose a Subscription": "选择一个订阅",
"Subscribe": "订阅",
"New Category": "新分类",
"Title": "标题",
"Save": "保存",
"or": "或",
"cancel": "取消",
"New User": "新用户",
"Confirmation": "确认",
"Administrator": "管理员",
"Edit Category: %s": "编辑分类 : %s",
"Update": "更新",
"Edit Feed: %s": "编辑源 : %s",
"There is no category!": "没有分类!",
"Edit user: %s": "编辑用户: %s",
"There is no article for this feed.": "这个源中没有文章.",
"Add subscription": "新增订阅",
"You don't have any subscription.": "当前没有订阅",
"Last check:": "最后检查时间:",
"Refresh": "更新",
"There is no history at the moment.": "当前没有历史.",
"OPML file": "OPML 文件",
"Sign In": "登陆",
"Sign in": "登陆",
"Theme": "主题",
"Timezone": "时区",
"Language": "语言",
"There is no unread article.": "目前没有未读文章.",
"You are the only user.": "你是目前仅有的用户.",
"Last Login": "最后登录时间",
"Yes": "是",
"No": "否",
"This feed already exists (%s)": "源已存在 (%s)",
"Unable to fetch feed (statusCode=%d)": "无法获取源 (错误代码=%d)",
"Unable to open this link: %v": "无法打开这一链接: %v",
"Unable to analyze this page: %v": "无法分析这一页面: %v",
"Unable to find any subscription.": "找不到任何订阅.",
"The URL and the category are mandatory.": "必须填写URL和分类.",
"All fields are mandatory.": "必须填写全部信息.",
"Passwords are not the same.": "两次输入的密码不同.",
"You must use at least 6 characters.": "请至少使用6个字符.",
"The username is mandatory.": "必须填写用户名.",
"The username, theme, language and timezone fields are mandatory.": "必须填写用户名,主题,语言和时区.",
"The title is mandatory.": "必须填写标题.",
"About": "关于",
"version": "版本",
"Version:": "版本:",
"Build Date:": "构建日期:",
"Author:": "作者:",
"Authors": "作者",
"License:": "协议:",
"Attachments": "附件",
"Download": "下载",
"Invalid username or password.": "用户名或密码无效.",
"Never": "永不",
"Unable to execute request: %v": "无法执行这一请求: %v",
"Last Parsing Error": "最后一次解析错误",
"There is a problem with this feed": "这一源存在问题",
"Unable to parse OPML file: %q": "无法解析OPML文件: %q",
"Unable to parse RSS feed: %q": "无法解析RSS源: %q",
"Unable to parse Atom feed: %q": "无法解析Atom源: %q",
"Unable to parse JSON feed: %q": "无法解析JSON源: %q",
"Unable to parse RDF feed: %q": "无法解析RDF源: %q",
"Unable to normalize encoding: %q": "无法正则化编码: %q",
"Unable to create this category.": "无法建立这个分类.",
"yes": "是",
"no": "否",
"Are you sure?": "您确认吗?",
"Work in progress...": "执行中...",
"This user already exists.": "用户已存在.",
"This category already exists.": "分类已存在.",
"Unable to update this category.": "无法更新该分类.",
"Integrations": "集成",
"Bookmarklet": "书签小应用",
"Drag and drop this link to your bookmarks.": "拖动这个链接到书签.",
"This special link allows you to subscribe to a website directly by using a bookmark in your web browser.": "你可以打开这个特殊的书签来直接订阅网站.",
"Add to Miniflux": "新增到Miniflux",
"Refresh all feeds in background": "在后台更新全部源",
"Sign in with Google": "使用Google登陆",
"Unlink my Google account": "去除Google账号关联",
"Link my Google account": "关联我的Google账户",
"Category not found for this user": "未找到该用户的这一分类",
"Invalid theme.": "无效的主题.",
"Entry Sorting": "内容排序",
"Older entries first": "旧->新",
"Recent entries first": "新->旧",
"Saving...": "保存中",
"Done!": "完成!",
"Save this article": "保存这篇文章",
"Mark bookmark as unread": "标记为未读",
"Pinboard Tags": "Pinboard 标签",
"Pinboard API Token": "Pinboard API Token",
"Save articles to Pinboard": "保存文章到Pinboard",
"Save articles to Instapaper": "保存文章到Instapaper",
"Instapaper Username": "Instapaper 用户名",
"Instapaper Password": "Instapaper 密码",
"Activate Fever API": "启用 Fever API",
"Fever Username": "Fever 用户名",
"Fever Password": "Fever 密码",
"Fetch original content": "抓取原内容",
"Scraper Rules": "Scraper规则",
"Rewrite Rules": "重写规则",
"Preferences saved!": "偏好已存储!",
"Your external account is now linked !": "您的外部账号已关联!",
"Save articles to Wallabag": "保存文章到Wallabag",
"Wallabag API Endpoint": "Wallabag URL",
"Wallabag Client ID": "Wallabag 客户端ID",
"Wallabag Client Secret": "Wallabag 客户端Secret",
"Wallabag Username": "Wallabag 用户名",
"Wallabag Password": "Wallabag 密码",
"Save articles to Nunux Keeper": "保存文章到Nunux Keeper",
"Nunux Keeper API Endpoint": "Nunux Keeper API Endpoint",
"Nunux Keeper API key": "Nunux Keeper API key",
"Keyboard Shortcut: %s": "快捷键: %s",
"Favorites": "收藏",
"Star": "标记星标",
"Unstar": "去掉星标",
"Starred": "星标",
"There is no bookmark at the moment.": "当前没有书签.",
"Last checked:": "上次检查:",
"ETag header:": "ETag header:",
"LastModified header:": "最后修改的Header:",
"None": "无",
"Keyboard Shortcuts": "快捷键",
"Sections Navigation": "分区导航",
"Go to unread": "去往未读",
"Go to bookmarks": "去往书签",
"Go to history": "去往历史",
"Go to feeds": "去往源",
"Go to categories": "去往分类",
"Go to settings": "去往设定",
"Show keyboard shortcuts": "显示快捷键",
"Items Navigation": "条目导航",
"Go to previous item": "上一条目",
"Go to next item": "下一条目",
"Pages Navigation": "页面导航",
"Go to previous page": "上一页",
"Go to next page": "下一页",
"Open selected item": "打开选定的条目",
"Open original link": "打开原始链接",
"Toggle read/unread": "切换已读/未读状态",
"Mark current page as read": "标记当前",
"Download original content": "下载原始内容",
"Toggle bookmark": "切换收藏状态",
"Close modal dialog": "关闭模态对话窗口",
"Save article": "保存文章",
"There is already someone associated with this provider!": "该Provider已被关联!",
"There is already someone else with the same Fever username!": "Fever用户名已被占用!",
"Mark all as read": "全标记为已读",
"This feed is empty": "该源是空的",
"Flush history": "清理历史",
"Site URL": "站点URL",
"Feed URL": "源URL",
"Logged as %s": "当前登录 %s",
"Unread Items": "未读条目",
"Change entry status": "更改状态",
"Read": "标为已读",
"Fever API endpoint:": "Fever API Endpoint:",
"Miniflux API": "Miniflux API",
"API Endpoint": "API Endpoint",
"Your account password": "您账户的密码",
"This web page is empty": "该网页是空的",
"Invalid SSL certificate (original error: %q)": "无效的SSL证书 (原始错误: %q)",
"This website is temporarily unreachable (original error: %q)": "该网站暂时不可达 (原始错误: %q)",
"This website is permanently unreachable (original error: %q)": "该网站永久不可达 (原始错误: %q)",
"Website unreachable, the request timed out after %d seconds": "网站不可达, 请求已在 %d 秒后超时"
}
+3
View File
@@ -24,4 +24,7 @@ type Integration struct {
WallabagClientSecret string
WallabagUsername string
WallabagPassword string
NunuxKeeperEnabled bool
NunuxKeeperURL string
NunuxKeeperAPIKey string
}
+16
View File
@@ -7,6 +7,8 @@ package model
import (
"errors"
"time"
"github.com/miniflux/miniflux/timezone"
)
// User represents a user in the system.
@@ -99,5 +101,19 @@ func (u *User) Merge(override *User) {
}
}
// UseTimezone converts last login date to the given timezone.
func (u *User) UseTimezone(tz string) {
if u.LastLoginAt != nil {
*u.LastLoginAt = timezone.Convert(tz, *u.LastLoginAt)
}
}
// Users represents a list of users.
type Users []*User
// UseTimezone converts last login timestamp of all users to the given timezone.
func (u Users) UseTimezone(tz string) {
for _, user := range u {
user.UseTimezone(tz)
}
}
+20 -4
View File
@@ -4,8 +4,12 @@
package model
import "time"
import "fmt"
import (
"fmt"
"time"
"github.com/miniflux/miniflux/timezone"
)
// UserSession represents a user session in the system.
type UserSession struct {
@@ -17,9 +21,21 @@ type UserSession struct {
IP string
}
func (s *UserSession) String() string {
return fmt.Sprintf(`ID="%d", UserID="%d", IP="%s", Token="%s"`, s.ID, s.UserID, s.IP, s.Token)
func (u *UserSession) String() string {
return fmt.Sprintf(`ID="%d", UserID="%d", IP="%s", Token="%s"`, u.ID, u.UserID, u.IP, u.Token)
}
// UseTimezone converts creation date to the given timezone.
func (u *UserSession) UseTimezone(tz string) {
u.CreatedAt = timezone.Convert(tz, u.CreatedAt)
}
// UserSessions represents a list of sessions.
type UserSessions []*UserSession
// UseTimezone converts creation date of all sessions to the given timezone.
func (u UserSessions) UseTimezone(tz string) {
for _, session := range u {
session.UseTimezone(tz)
}
}
+2 -2
View File
@@ -14,14 +14,14 @@ import (
)
// Parse returns a normalized feed struct from a Atom feed.
func Parse(data io.Reader) (*model.Feed, error) {
func Parse(data io.Reader) (*model.Feed, *errors.LocalizedError) {
atomFeed := new(atomFeed)
decoder := xml.NewDecoder(data)
decoder.CharsetReader = encoding.CharsetReader
err := decoder.Decode(atomFeed)
if err != nil {
return nil, errors.NewLocalizedError("Unable to parse Atom feed: %v.", err)
return nil, errors.NewLocalizedError("Unable to parse Atom feed: %q", err)
}
return atomFeed.Transform(), nil
-6
View File
@@ -8,8 +8,6 @@ import (
"bytes"
"testing"
"time"
"github.com/miniflux/miniflux/errors"
)
func TestParseAtomSample(t *testing.T) {
@@ -430,8 +428,4 @@ func TestParseInvalidXml(t *testing.T) {
if err == nil {
t.Error("Parse should returns an error")
}
if _, ok := err.(errors.LocalizedError); !ok {
t.Error("The error returned must be a LocalizedError")
}
}
+26 -17
View File
@@ -10,6 +10,7 @@ import (
"github.com/miniflux/miniflux/errors"
"github.com/miniflux/miniflux/http"
"github.com/miniflux/miniflux/locale"
"github.com/miniflux/miniflux/logger"
"github.com/miniflux/miniflux/model"
"github.com/miniflux/miniflux/reader/icon"
@@ -23,14 +24,15 @@ var (
errServerFailure = "Unable to fetch feed (statusCode=%d)"
errDuplicate = "This feed already exists (%s)"
errNotFound = "Feed %d not found"
errEncoding = "Unable to normalize encoding: %v"
errEncoding = "Unable to normalize encoding: %q"
errCategoryNotFound = "Category not found for this user"
errEmptyFeed = "This feed is empty"
)
// Handler contains all the logic to create and refresh feeds.
type Handler struct {
store *storage.Storage
store *storage.Storage
translator *locale.Translator
}
// CreateFeed fetch, parse and store a new feed.
@@ -44,7 +46,7 @@ func (h *Handler) CreateFeed(userID, categoryID int64, url string, crawler bool)
client := http.NewClient(url)
response, err := client.Get()
if err != nil {
if _, ok := err.(errors.LocalizedError); ok {
if _, ok := err.(*errors.LocalizedError); ok {
return nil, err
}
return nil, errors.NewLocalizedError(errRequestFailed, err)
@@ -68,9 +70,9 @@ func (h *Handler) CreateFeed(userID, categoryID int64, url string, crawler bool)
return nil, errors.NewLocalizedError(errEncoding, err)
}
subscription, err := parseFeed(body)
if err != nil {
return nil, err
subscription, feedErr := parseFeed(body)
if feedErr != nil {
return nil, feedErr
}
feedProcessor := processor.NewFeedProcessor(userID, h.store, subscription)
@@ -110,6 +112,13 @@ func (h *Handler) CreateFeed(userID, categoryID int64, url string, crawler bool)
// RefreshFeed fetch and update a feed if necessary.
func (h *Handler) RefreshFeed(userID, feedID int64) error {
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[Handler:RefreshFeed] feedID=%d", feedID))
userLanguage, err := h.store.UserLanguage(userID)
if err != nil {
logger.Error("[Handler:RefreshFeed] %v", err)
userLanguage = "en_US"
}
currentLanguage := h.translator.GetLanguage(userLanguage)
originalFeed, err := h.store.FeedByID(userID, feedID)
if err != nil {
@@ -124,14 +133,14 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
response, err := client.Get()
if err != nil {
var customErr errors.LocalizedError
if lerr, ok := err.(errors.LocalizedError); ok {
customErr = lerr
if lerr, ok := err.(*errors.LocalizedError); ok {
customErr = *lerr
} else {
customErr = errors.NewLocalizedError(errRequestFailed, err)
customErr = *errors.NewLocalizedError(errRequestFailed, err)
}
originalFeed.ParsingErrorCount++
originalFeed.ParsingErrorMsg = customErr.Error()
originalFeed.ParsingErrorMsg = customErr.Localize(currentLanguage)
h.store.UpdateFeed(originalFeed)
return customErr
}
@@ -141,7 +150,7 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
if response.HasServerFailure() {
err := errors.NewLocalizedError(errServerFailure, response.StatusCode)
originalFeed.ParsingErrorCount++
originalFeed.ParsingErrorMsg = err.Error()
originalFeed.ParsingErrorMsg = err.Localize(currentLanguage)
h.store.UpdateFeed(originalFeed)
return err
}
@@ -153,7 +162,7 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
if response.ContentLength == 0 {
err := errors.NewLocalizedError(errEmptyFeed)
originalFeed.ParsingErrorCount++
originalFeed.ParsingErrorMsg = err.Error()
originalFeed.ParsingErrorMsg = err.Localize(currentLanguage)
h.store.UpdateFeed(originalFeed)
return err
}
@@ -163,10 +172,10 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
return errors.NewLocalizedError(errEncoding, err)
}
subscription, err := parseFeed(body)
if err != nil {
subscription, parseErr := parseFeed(body)
if parseErr != nil {
originalFeed.ParsingErrorCount++
originalFeed.ParsingErrorMsg = err.Error()
originalFeed.ParsingErrorMsg = parseErr.Localize(currentLanguage)
h.store.UpdateFeed(originalFeed)
return err
}
@@ -209,6 +218,6 @@ func (h *Handler) RefreshFeed(userID, feedID int64) error {
}
// NewFeedHandler returns a feed handler.
func NewFeedHandler(store *storage.Storage) *Handler {
return &Handler{store: store}
func NewFeedHandler(store *storage.Storage, translator *locale.Translator) *Handler {
return &Handler{store, translator}
}
+4 -4
View File
@@ -7,11 +7,11 @@ package feed
import (
"bytes"
"encoding/xml"
"errors"
"io"
"strings"
"time"
"github.com/miniflux/miniflux/errors"
"github.com/miniflux/miniflux/logger"
"github.com/miniflux/miniflux/model"
"github.com/miniflux/miniflux/reader/atom"
@@ -66,13 +66,13 @@ func DetectFeedFormat(r io.Reader) string {
return FormatUnknown
}
func parseFeed(r io.Reader) (*model.Feed, error) {
func parseFeed(r io.Reader) (*model.Feed, *errors.LocalizedError) {
defer timer.ExecutionTime(time.Now(), "[Feed:ParseFeed]")
var buffer bytes.Buffer
size, _ := io.Copy(&buffer, r)
if size == 0 {
return nil, errors.New("This feed is empty")
return nil, errors.NewLocalizedError("This feed is empty")
}
str := stripInvalidXMLCharacters(buffer.String())
@@ -90,7 +90,7 @@ func parseFeed(r io.Reader) (*model.Feed, error) {
case FormatRDF:
return rdf.Parse(reader)
default:
return nil, errors.New("Unsupported feed format")
return nil, errors.NewLocalizedError("Unsupported feed format")
}
}
+2 -2
View File
@@ -13,11 +13,11 @@ import (
)
// Parse returns a normalized feed struct from a JON feed.
func Parse(data io.Reader) (*model.Feed, error) {
func Parse(data io.Reader) (*model.Feed, *errors.LocalizedError) {
feed := new(jsonFeed)
decoder := json.NewDecoder(data)
if err := decoder.Decode(&feed); err != nil {
return nil, errors.NewLocalizedError("Unable to parse JSON Feed: %v", err)
return nil, errors.NewLocalizedError("Unable to parse JSON Feed: %q", err)
}
return feed.Transform(), nil
-6
View File
@@ -9,8 +9,6 @@ import (
"strings"
"testing"
"time"
"github.com/miniflux/miniflux/errors"
)
func TestParseJsonFeed(t *testing.T) {
@@ -377,8 +375,4 @@ func TestParseInvalidJSON(t *testing.T) {
if err == nil {
t.Error("Parse should returns an error")
}
if _, ok := err.(errors.LocalizedError); !ok {
t.Error("The error returned must be a LocalizedError")
}
}
+2 -2
View File
@@ -13,14 +13,14 @@ import (
)
// Parse reads an OPML file and returns a SubcriptionList.
func Parse(data io.Reader) (SubcriptionList, error) {
func Parse(data io.Reader) (SubcriptionList, *errors.LocalizedError) {
feeds := new(opml)
decoder := xml.NewDecoder(data)
decoder.CharsetReader = encoding.CharsetReader
err := decoder.Decode(feeds)
if err != nil {
return nil, errors.NewLocalizedError("Unable to parse OPML file: %v.", err)
return nil, errors.NewLocalizedError("Unable to parse OPML file: %q", err)
}
return feeds.Transform(), nil
-6
View File
@@ -7,8 +7,6 @@ package opml
import (
"bytes"
"testing"
"github.com/miniflux/miniflux/errors"
)
func TestParseOpmlWithoutCategories(t *testing.T) {
@@ -130,8 +128,4 @@ func TestParseInvalidXML(t *testing.T) {
if err == nil {
t.Error("Parse should generate an error")
}
if _, ok := err.(errors.LocalizedError); !ok {
t.Error("The error returned must be a LocalizedError")
}
}
+2 -2
View File
@@ -14,14 +14,14 @@ import (
)
// Parse returns a normalized feed struct from a RDF feed.
func Parse(data io.Reader) (*model.Feed, error) {
func Parse(data io.Reader) (*model.Feed, *errors.LocalizedError) {
feed := new(rdfFeed)
decoder := xml.NewDecoder(data)
decoder.CharsetReader = encoding.CharsetReader
err := decoder.Decode(feed)
if err != nil {
return nil, errors.NewLocalizedError("Unable to parse RDF feed: %v.", err)
return nil, errors.NewLocalizedError("Unable to parse RDF feed: %q", err)
}
return feed.Transform(), nil
-6
View File
@@ -9,8 +9,6 @@ import (
"strings"
"testing"
"time"
"github.com/miniflux/miniflux/errors"
)
func TestParseRDFSample(t *testing.T) {
@@ -330,8 +328,4 @@ func TestParseInvalidXml(t *testing.T) {
if err == nil {
t.Error("Parse should returns an error")
}
if _, ok := err.(errors.LocalizedError); !ok {
t.Error("The error returned must be a LocalizedError")
}
}
+2 -2
View File
@@ -14,14 +14,14 @@ import (
)
// Parse returns a normalized feed struct from a RSS feed.
func Parse(data io.Reader) (*model.Feed, error) {
func Parse(data io.Reader) (*model.Feed, *errors.LocalizedError) {
feed := new(rssFeed)
decoder := xml.NewDecoder(data)
decoder.CharsetReader = encoding.CharsetReader
err := decoder.Decode(feed)
if err != nil {
return nil, errors.NewLocalizedError("Unable to parse RSS feed: %v.", err)
return nil, errors.NewLocalizedError("Unable to parse RSS feed: %q", err)
}
return feed.Transform(), nil
-6
View File
@@ -8,8 +8,6 @@ import (
"bytes"
"testing"
"time"
"github.com/miniflux/miniflux/errors"
)
func TestParseRss2Sample(t *testing.T) {
@@ -564,8 +562,4 @@ func TestParseInvalidXml(t *testing.T) {
if err == nil {
t.Error("Parse should returns an error")
}
if _, ok := err.(errors.LocalizedError); !ok {
t.Error("The error returned must be a LocalizedError")
}
}
+2 -2
View File
@@ -15,7 +15,7 @@ import (
func NewFeedScheduler(store *storage.Storage, workerPool *WorkerPool, frequency, batchSize int) {
go func() {
c := time.Tick(time.Duration(frequency) * time.Minute)
for _ = range c {
for range c {
jobs, err := store.NewBatch(batchSize)
if err != nil {
logger.Error("[FeedScheduler] %v", err)
@@ -31,7 +31,7 @@ func NewFeedScheduler(store *storage.Storage, workerPool *WorkerPool, frequency,
func NewSessionScheduler(store *storage.Storage, frequency int) {
go func() {
c := time.Tick(time.Duration(frequency) * time.Hour)
for _ = range c {
for range c {
nbSessions := store.CleanOldSessions()
nbUserSessions := store.CleanOldUserSessions()
logger.Info("[SessionScheduler] cleaned %d sessions and %d user sessions", nbSessions, nbUserSessions)
+3
View File
@@ -0,0 +1,3 @@
alter table integrations add column nunux_keeper_enabled bool default 'f';
alter table integrations add column nunux_keeper_url text default '';
alter table integrations add column nunux_keeper_api_key text default '';
+5 -1
View File
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
// 2018-02-10 22:26:57.088648586 -0800 PST m=+0.004853992
// 2018-02-24 17:47:34.98646993 +0000 GMT
package sql
@@ -126,6 +126,9 @@ alter table integrations add column wallabag_password text default '';`,
"schema_version_13": `create index entries_user_status_idx on entries(user_id, status);
create index feeds_user_category_idx on feeds(user_id, category_id);
`,
"schema_version_14": `alter table integrations add column nunux_keeper_enabled bool default 'f';
alter table integrations add column nunux_keeper_url text default '';
alter table integrations add column nunux_keeper_api_key text default '';`,
"schema_version_2": `create extension if not exists hstore;
alter table users add column extra hstore;
create index users_extra_idx on users using gin(extra);
@@ -170,6 +173,7 @@ var SqlMapChecksums = map[string]string{
"schema_version_11": "dc5bbc302e01e425b49c48ddcd8e29e3ab2bb8e73a6cd1858a6ba9fbec0b5243",
"schema_version_12": "a95abab6cdf64811fc744abd37457e2928939d999c5ef00d2bdd9398e16f32fb",
"schema_version_13": "9073fae1e796936f4a43a8120ebdb4218442fe7d346ace6387556a357c2d7edf",
"schema_version_14": "4622e42c4a5a88b6fe1e61f3d367b295968f7260ab5b96481760775ba9f9e1fe",
"schema_version_2": "e8e9ff32478df04fcddad10a34cba2e8bb1e67e7977b5bd6cdc4c31ec94282b4",
"schema_version_3": "a54745dbc1c51c000f74d4e5068f1e2f43e83309f023415b1749a47d5c1e0f12",
"schema_version_4": "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9",
+9 -1
View File
@@ -13,6 +13,7 @@ import (
"github.com/miniflux/miniflux/model"
"github.com/miniflux/miniflux/timer"
"github.com/miniflux/miniflux/timezone"
)
// EntryQueryBuilder builds a SQL query to fetch entries.
@@ -160,7 +161,8 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
e.url, e.author, e.content, e.status, e.starred,
f.title as feed_title, f.feed_url, f.site_url, f.checked_at,
f.category_id, c.title as category_title, f.scraper_rules, f.rewrite_rules, f.crawler,
fi.icon_id
fi.icon_id,
u.timezone
FROM entries e
LEFT JOIN feeds f ON f.id=e.feed_id
LEFT JOIN categories c ON c.id=f.category_id
@@ -183,6 +185,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
for rows.Next() {
var entry model.Entry
var iconID interface{}
var tz string
entry.Feed = &model.Feed{UserID: e.userID}
entry.Feed.Category = &model.Category{UserID: e.userID}
@@ -210,6 +213,7 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
&entry.Feed.RewriteRules,
&entry.Feed.Crawler,
&iconID,
&tz,
)
if err != nil {
@@ -222,6 +226,10 @@ func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
entry.Feed.Icon.IconID = iconID.(int64)
}
// Make sure that timestamp fields contains timezone information (API)
entry.Date = timezone.Convert(tz, entry.Date)
entry.Feed.CheckedAt = timezone.Convert(tz, entry.Feed.CheckedAt)
entry.Feed.ID = entry.FeedID
entry.Feed.Icon.FeedID = entry.FeedID
entries = append(entries, &entry)
+11 -2
View File
@@ -12,6 +12,7 @@ import (
"github.com/miniflux/miniflux/model"
"github.com/miniflux/miniflux/timer"
"github.com/miniflux/miniflux/timezone"
)
// FeedExists checks if the given feed exists.
@@ -56,7 +57,8 @@ func (s *Storage) Feeds(userID int64) (model.Feeds, error) {
f.parsing_error_count, f.parsing_error_msg,
f.scraper_rules, f.rewrite_rules, f.crawler,
f.category_id, c.title as category_title,
fi.icon_id
fi.icon_id,
u.timezone
FROM feeds f
LEFT JOIN categories c ON c.id=f.category_id
LEFT JOIN feed_icons fi ON fi.feed_id=f.id
@@ -73,6 +75,7 @@ func (s *Storage) Feeds(userID int64) (model.Feeds, error) {
for rows.Next() {
var feed model.Feed
var iconID interface{}
var tz string
feed.Category = &model.Category{UserID: userID}
err := rows.Scan(
@@ -92,6 +95,7 @@ func (s *Storage) Feeds(userID int64) (model.Feeds, error) {
&feed.Category.ID,
&feed.Category.Title,
&iconID,
&tz,
)
if err != nil {
@@ -102,6 +106,7 @@ func (s *Storage) Feeds(userID int64) (model.Feeds, error) {
feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: iconID.(int64)}
}
feed.CheckedAt = timezone.Convert(tz, feed.CheckedAt)
feeds = append(feeds, &feed)
}
@@ -114,6 +119,7 @@ func (s *Storage) FeedByID(userID, feedID int64) (*model.Feed, error) {
var feed model.Feed
var iconID interface{}
var tz string
feed.Category = &model.Category{UserID: userID}
query := `
@@ -123,7 +129,8 @@ func (s *Storage) FeedByID(userID, feedID int64) (*model.Feed, error) {
f.parsing_error_count, f.parsing_error_msg,
f.scraper_rules, f.rewrite_rules, f.crawler,
f.category_id, c.title as category_title,
fi.icon_id
fi.icon_id,
u.timezone
FROM feeds f
LEFT JOIN categories c ON c.id=f.category_id
LEFT JOIN feed_icons fi ON fi.feed_id=f.id
@@ -147,6 +154,7 @@ func (s *Storage) FeedByID(userID, feedID int64) (*model.Feed, error) {
&feed.Category.ID,
&feed.Category.Title,
&iconID,
&tz,
)
switch {
@@ -160,6 +168,7 @@ func (s *Storage) FeedByID(userID, feedID int64) (*model.Feed, error) {
feed.Icon = &model.FeedIcon{FeedID: feed.ID, IconID: iconID.(int64)}
}
feed.CheckedAt = timezone.Convert(tz, feed.CheckedAt)
return &feed, nil
}
+15 -3
View File
@@ -67,7 +67,10 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
wallabag_client_id,
wallabag_client_secret,
wallabag_username,
wallabag_password
wallabag_password,
nunux_keeper_enabled,
nunux_keeper_url,
nunux_keeper_api_key
FROM integrations
WHERE user_id=$1
`
@@ -91,6 +94,9 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
&integration.WallabagClientSecret,
&integration.WallabagUsername,
&integration.WallabagPassword,
&integration.NunuxKeeperEnabled,
&integration.NunuxKeeperURL,
&integration.NunuxKeeperAPIKey,
)
switch {
case err == sql.ErrNoRows:
@@ -122,8 +128,11 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
wallabag_client_id=$14,
wallabag_client_secret=$15,
wallabag_username=$16,
wallabag_password=$17
WHERE user_id=$18
wallabag_password=$17,
nunux_keeper_enabled=$18,
nunux_keeper_url=$19,
nunux_keeper_api_key=$20
WHERE user_id=$21
`
_, err := s.db.Exec(
query,
@@ -144,6 +153,9 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
integration.WallabagClientSecret,
integration.WallabagUsername,
integration.WallabagPassword,
integration.NunuxKeeperEnabled,
integration.NunuxKeeperURL,
integration.NunuxKeeperAPIKey,
integration.UserID,
)
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"github.com/miniflux/miniflux/sql"
)
const schemaVersion = 13
const schemaVersion = 14
// Migrate run database migrations.
func (s *Storage) Migrate() {
+5 -4
View File
@@ -6,6 +6,7 @@ package storage
import (
"fmt"
"strings"
"time"
"github.com/miniflux/miniflux/timer"
@@ -14,10 +15,8 @@ import (
// Timezones returns all timezones supported by the database.
func (s *Storage) Timezones() (map[string]string, error) {
defer timer.ExecutionTime(time.Now(), "[Storage:Timezones]")
timezones := make(map[string]string)
query := `select name from pg_timezone_names() order by name asc`
rows, err := s.db.Query(query)
rows, err := s.db.Query(`SELECT name FROM pg_timezone_names() ORDER BY name ASC`)
if err != nil {
return nil, fmt.Errorf("unable to fetch timezones: %v", err)
}
@@ -29,7 +28,9 @@ func (s *Storage) Timezones() (map[string]string, error) {
return nil, fmt.Errorf("unable to fetch timezones row: %v", err)
}
timezones[timezone] = timezone
if !strings.HasPrefix(timezone, "posix") && !strings.HasPrefix(timezone, "SystemV") && timezone != "localtime" {
timezones[timezone] = timezone
}
}
return timezones, nil
+13 -2
View File
@@ -11,11 +11,10 @@ import (
"strings"
"time"
"github.com/lib/pq/hstore"
"github.com/miniflux/miniflux/model"
"github.com/miniflux/miniflux/timer"
"github.com/lib/pq/hstore"
"golang.org/x/crypto/bcrypt"
)
@@ -175,6 +174,18 @@ func (s *Storage) UpdateUser(user *model.User) error {
return nil
}
// UserLanguage returns the language of the given user.
func (s *Storage) UserLanguage(userID int64) (language string, err error) {
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:UserLanguage] userID=%d", userID))
err = s.db.QueryRow(`SELECT language FROM users WHERE id = $1`, userID).Scan(&language)
if err == sql.ErrNoRows {
return "en_US", nil
} else if err != nil {
return "", fmt.Errorf("unable to fetch user language: %v", err)
}
return language, nil
}
// UserByID finds a user by the ID.
func (s *Storage) UserByID(userID int64) (*model.User, error) {
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[Storage:UserByID] userID=%d", userID))
+1 -1
View File
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
// 2018-02-04 14:28:15.225458631 -0800 PST m=+0.036040293
// 2018-02-24 17:47:34.998457627 +0000 GMT
package template
+4 -14
View File
@@ -9,6 +9,7 @@ import (
"time"
"github.com/miniflux/miniflux/locale"
"github.com/miniflux/miniflux/timezone"
)
// Texts to be translated if necessary.
@@ -28,24 +29,13 @@ var (
// ElapsedTime returns in a human readable format the elapsed time
// since the given datetime.
func elapsedTime(language *locale.Language, timezone string, t time.Time) string {
func elapsedTime(language *locale.Language, tz string, t time.Time) string {
if t.IsZero() {
return language.Get(NotYet)
}
var now time.Time
loc, err := time.LoadLocation(timezone)
if err != nil {
now = time.Now()
} else {
now = time.Now().In(loc)
// The provided date is already converted to the user timezone by Postgres,
// but the timezone information is not set in the time struct.
// We cannot use time.In() because the date will be converted a second time.
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), loc)
}
now := timezone.Now(tz)
t = timezone.Convert(tz, t)
if now.Before(t) {
return language.Get(NotYet)
}
+3 -2
View File
@@ -85,8 +85,9 @@ func (f *funcMap) Map() template.FuncMap {
case string:
return f.Language.Get(key.(string), args...)
case errors.LocalizedError:
err := key.(errors.LocalizedError)
return err.Localize(f.Language)
return key.(errors.LocalizedError).Localize(f.Language)
case *errors.LocalizedError:
return key.(*errors.LocalizedError).Localize(f.Language)
case error:
return key.(error).Error()
default:
+1 -1
View File
@@ -2,7 +2,7 @@
{{ define "content"}}
<section class="page-header">
<h1>{{ t "Edit user %s" .selected_user.Username }}"</h1>
<h1>{{ t "Edit user %s" .selected_user.Username }}</h1>
<ul>
<li>
<a href="{{ route "settings" }}">{{ t "Settings" }}</a>
+1 -1
View File
@@ -66,7 +66,7 @@
{{ if ne .ParsingErrorCount 0 }}
<div class="parsing-error">
<strong title="{{ .ParsingErrorMsg }}" class="parsing-error-count">{{ plural "plural.feed.error_count" .ParsingErrorCount .ParsingErrorCount }}</strong>
<small class="parsing-error-message">({{ .ParsingErrorMsg }})</small>
- <small class="parsing-error-message">{{ .ParsingErrorMsg }}</small>
</div>
{{ end }}
</article>
+13
View File
@@ -94,6 +94,19 @@
<label for="form-wallabag-password">{{ t "Wallabag Password" }}</label>
<input type="password" name="wallabag_password" id="form-wallabag-password" value="{{ .form.WallabagPassword }}">
</div>
<h3>Nunux Keeper</h3>
<div class="form-section">
<label>
<input type="checkbox" name="nunux_keeper_enabled" value="1" {{ if .form.NunuxKeeperEnabled }}checked{{ end }}> {{ t "Save articles to Nunux Keeper" }}
</label>
<label for="form-nunux-keeper-url">{{ t "Nunux Keeper API Endpoint" }}</label>
<input type="url" name="nunux_keeper_url" id="form-nunux-keeper-url" value="{{ .form.NunuxKeeperURL }}" placeholder="https://api.nunux.org/keeper">
<label for="form-nunux-keeper-api-key">{{ t "Nunux Keeper API key" }}</label>
<input type="text" name="nunux_keeper_api_key" id="form-nunux-keeper-api-key" value="{{ .form.NunuxKeeperAPIKey }}">
</div>
<div class="buttons">
<button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button>
+19 -6
View File
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
// 2018-02-04 14:28:15.215257434 -0800 PST m=+0.025839096
// 2018-03-01 23:04:58.998374277 -0800 PST m=+0.022158179
package template
@@ -416,7 +416,7 @@ var templateViewsMap = map[string]string{
{{ define "content"}}
<section class="page-header">
<h1>{{ t "Edit user %s" .selected_user.Username }}"</h1>
<h1>{{ t "Edit user %s" .selected_user.Username }}</h1>
<ul>
<li>
<a href="{{ route "settings" }}">{{ t "Settings" }}</a>
@@ -690,7 +690,7 @@ var templateViewsMap = map[string]string{
{{ if ne .ParsingErrorCount 0 }}
<div class="parsing-error">
<strong title="{{ .ParsingErrorMsg }}" class="parsing-error-count">{{ plural "plural.feed.error_count" .ParsingErrorCount .ParsingErrorCount }}</strong>
<small class="parsing-error-message">({{ .ParsingErrorMsg }})</small>
- <small class="parsing-error-message">{{ .ParsingErrorMsg }}</small>
</div>
{{ end }}
</article>
@@ -869,6 +869,19 @@ var templateViewsMap = map[string]string{
<label for="form-wallabag-password">{{ t "Wallabag Password" }}</label>
<input type="password" name="wallabag_password" id="form-wallabag-password" value="{{ .form.WallabagPassword }}">
</div>
<h3>Nunux Keeper</h3>
<div class="form-section">
<label>
<input type="checkbox" name="nunux_keeper_enabled" value="1" {{ if .form.NunuxKeeperEnabled }}checked{{ end }}> {{ t "Save articles to Nunux Keeper" }}
</label>
<label for="form-nunux-keeper-url">{{ t "Nunux Keeper API Endpoint" }}</label>
<input type="url" name="nunux_keeper_url" id="form-nunux-keeper-url" value="{{ .form.NunuxKeeperURL }}" placeholder="https://api.nunux.org/keeper">
<label for="form-nunux-keeper-api-key">{{ t "Nunux Keeper API key" }}</label>
<input type="text" name="nunux_keeper_api_key" id="form-nunux-keeper-api-key" value="{{ .form.NunuxKeeperAPIKey }}">
</div>
<div class="buttons">
<button type="submit" class="button button-primary" data-label-loading="{{ t "Loading..." }}">{{ t "Update" }}</button>
@@ -1219,13 +1232,13 @@ var templateViewsMapChecksums = map[string]string{
"create_user": "233764778c915754141a20429ec8db9bf80ef2d7704867a2d7232c1e9df233ae",
"edit_category": "cee720faadcec58289b707ad30af623d2ee66c1ce23a732965463250d7ff41c5",
"edit_feed": "d2c1c8486d7faf4ee58151ccf3e3c690e53bd6872050d291c5db8452a83c3d53",
"edit_user": "5edd693460330750ba5ee03319d4e3cb5aabbd9a0e48b3b760799bca72c5ec4e",
"edit_user": "321e0a60cf3bf7441bff970f4920e4c5b7c1883f80ab1d1674f8137954b25033",
"entry": "27ea028515e79beb546f0b2792a3918c455fd877eea4c41d1a061f8e7b54a430",
"feed_entries": "420da786e827a77fecc8794207d158af3a30e489ca2b2019f48d5228919af4a7",
"feeds": "0c884b7a9dfc4541b988641516fd95df062a5bf05018d28276a3c0a10323cffd",
"feeds": "2a5abe37968ea34a0576dbef52341645cb1fc9562e351382fbf721491da6f4fa",
"history": "967bc95236269ab3a77455910aca1939f43f93171fe1af77eb3b1b4eac579e55",
"import": "73b5112e20bfd232bf73334544186ea419505936bc237d481517a8622901878f",
"integrations": "958b73d632a3e2a79368bb1582efb8aabc438cef4fa6e8dc1aa4932494916aca",
"integrations": "979193f39c2a3b43cec192aa119713cc9cbe2d5fdaedf8d2b3573c752823446c",
"login": "7d83c3067c02f1f6aafdd8816c7f97a4eb5a5a4bdaaaa4cc1e2fbb9c17ea65e8",
"sessions": "3fa79031dd883847eba92fbafe5f535fa3a4e1614bb610f20588b6f8fc8b3624",
"settings": "ea2505b9d0a6d6bb594dba87a92079de19baa6d494f0651693a7685489fb7de9",
+47
View File
@@ -0,0 +1,47 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package timezone
import (
"time"
)
// Convert converts provided date time to actual timezone.
func Convert(tz string, t time.Time) time.Time {
userTimezone := getLocation(tz)
if t.Location().String() == "" {
// In this case, the provided date is already converted to the user timezone by Postgres,
// but the timezone information is not set in the time struct.
// We cannot use time.In() because the date will be converted a second time.
t = time.Date(
t.Year(),
t.Month(),
t.Day(),
t.Hour(),
t.Minute(),
t.Second(),
t.Nanosecond(),
userTimezone,
)
} else if t.Location() != userTimezone {
t = t.In(userTimezone)
}
return t
}
// Now returns the current time with the given timezone.
func Now(tz string) time.Time {
return time.Now().In(getLocation(tz))
}
func getLocation(tz string) *time.Location {
loc, err := time.LoadLocation(tz)
if err != nil {
loc = time.Local
}
return loc
}
+75
View File
@@ -0,0 +1,75 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package timezone
import (
"testing"
"time"
)
func TestNow(t *testing.T) {
tz := "Europe/Paris"
now := Now(tz)
if now.Location().String() != tz {
t.Fatalf(`Unexpected timezone, got %q instead of %q`, now.Location(), tz)
}
}
func TestNowWithInvalidTimezone(t *testing.T) {
tz := "Invalid Timezone"
expected := time.Local
now := Now(tz)
if now.Location().String() != expected.String() {
t.Fatalf(`Unexpected timezone, got %q instead of %q`, now.Location(), expected)
}
}
func TestConvertTimeWithNoTimezoneInformation(t *testing.T) {
tz := "Canada/Pacific"
input := time.Date(2018, 3, 1, 14, 2, 3, 0, time.FixedZone("", 0))
output := Convert(tz, input)
if output.Location().String() != tz {
t.Fatalf(`Unexpected timezone, got %q instead of %s`, output.Location(), tz)
}
hours, minutes, secs := output.Clock()
if hours != 14 || minutes != 2 || secs != 3 {
t.Fatalf(`Unexpected time, got hours=%d, minutes=%d, secs=%d`, hours, minutes, secs)
}
}
func TestConvertTimeWithDifferentTimezone(t *testing.T) {
tz := "Canada/Central"
input := time.Date(2018, 3, 1, 14, 2, 3, 0, time.UTC)
output := Convert(tz, input)
if output.Location().String() != tz {
t.Fatalf(`Unexpected timezone, got %q instead of %s`, output.Location(), tz)
}
hours, minutes, secs := output.Clock()
if hours != 8 || minutes != 2 || secs != 3 {
t.Fatalf(`Unexpected time, got hours=%d, minutes=%d, secs=%d`, hours, minutes, secs)
}
}
func TestConvertTimeWithIdenticalTimezone(t *testing.T) {
tz := "Canada/Central"
loc, _ := time.LoadLocation(tz)
input := time.Date(2018, 3, 1, 14, 2, 3, 0, loc)
output := Convert(tz, input)
if output.Location().String() != tz {
t.Fatalf(`Unexpected timezone, got %q instead of %s`, output.Location(), tz)
}
hours, minutes, secs := output.Clock()
if hours != 14 || minutes != 2 || secs != 3 {
t.Fatalf(`Unexpected time, got hours=%d, minutes=%d, secs=%d`, hours, minutes, secs)
}
}
+9
View File
@@ -28,6 +28,9 @@ type IntegrationForm struct {
WallabagClientSecret string
WallabagUsername string
WallabagPassword string
NunuxKeeperEnabled bool
NunuxKeeperURL string
NunuxKeeperAPIKey string
}
// Merge copy form values to the model.
@@ -48,6 +51,9 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
integration.WallabagClientSecret = i.WallabagClientSecret
integration.WallabagUsername = i.WallabagUsername
integration.WallabagPassword = i.WallabagPassword
integration.NunuxKeeperEnabled = i.NunuxKeeperEnabled
integration.NunuxKeeperURL = i.NunuxKeeperURL
integration.NunuxKeeperAPIKey = i.NunuxKeeperAPIKey
}
// NewIntegrationForm returns a new AuthForm.
@@ -69,5 +75,8 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
WallabagClientSecret: r.FormValue("wallabag_client_secret"),
WallabagUsername: r.FormValue("wallabag_username"),
WallabagPassword: r.FormValue("wallabag_password"),
NunuxKeeperEnabled: r.FormValue("nunux_keeper_enabled") == "1",
NunuxKeeperURL: r.FormValue("nunux_keeper_url"),
NunuxKeeperAPIKey: r.FormValue("nunux_keeper_api_key"),
}
}
+3
View File
@@ -46,6 +46,9 @@ func (c *Controller) ShowIntegrations(ctx *handler.Context, request *handler.Req
WallabagClientSecret: integration.WallabagClientSecret,
WallabagUsername: integration.WallabagUsername,
WallabagPassword: integration.WallabagPassword,
NunuxKeeperEnabled: integration.NunuxKeeperEnabled,
NunuxKeeperURL: integration.NunuxKeeperURL,
NunuxKeeperAPIKey: integration.NunuxKeeperAPIKey,
},
}))
}
+1
View File
@@ -24,6 +24,7 @@ func (c *Controller) ShowSessions(ctx *handler.Context, request *handler.Request
return
}
sessions.UseTimezone(user.Timezone)
response.HTML().Render("sessions", args.Merge(tplParams{
"sessions": sessions,
"currentSessionToken": ctx.UserSessionToken(),
+1 -1
View File
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
// 2018-01-02 21:59:10.082800492 -0800 PST m=+0.010175821
// 2018-02-24 17:47:34.994475549 +0000 GMT
package static
+1 -1
View File
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
// 2018-02-10 22:33:47.060422476 -0800 PST m=+0.024328540
// 2018-02-24 17:47:34.995215527 +0000 GMT
package static
+1 -1
View File
@@ -1,5 +1,5 @@
// Code generated by go generate; DO NOT EDIT.
// 2018-01-29 20:38:56.472493026 -0800 PST m=+0.027898185
// 2018-02-24 17:47:34.995856638 +0000 GMT
package static
+1
View File
@@ -34,6 +34,7 @@ func (c *Controller) ShowUsers(ctx *handler.Context, request *handler.Request, r
return
}
users.UseTimezone(user.Timezone)
response.HTML().Render("users", args.Merge(tplParams{
"users": users,
"menu": "settings",