mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2025-01-07 10:20:26 +00:00
Add recent changes page
This commit is contained in:
parent
0e6c22080b
commit
4cf5937361
10
README.md
10
README.md
@ -1,6 +1,13 @@
|
|||||||
# 🍄 MycorrhizaWiki 0.8
|
# 🍄 MycorrhizaWiki 0.9
|
||||||
A wiki engine.
|
A wiki engine.
|
||||||
|
|
||||||
|
## 0.9
|
||||||
|
This is a development branch for 0.9 version. Features I want to implement in this release:
|
||||||
|
* [x] Recent changes page.
|
||||||
|
* [ ] Hypha deletion.
|
||||||
|
* [ ] Hypha renaming.
|
||||||
|
* [ ] Support async git ops.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
```sh
|
```sh
|
||||||
git clone --recurse-submodules https://github.com/bouncepaw/mycorrhiza
|
git clone --recurse-submodules https://github.com/bouncepaw/mycorrhiza
|
||||||
@ -31,5 +38,4 @@ Help is always needed. We have a [tg chat](https://t.me/mycorrhizadev) where som
|
|||||||
* Tagging system
|
* Tagging system
|
||||||
* Authorization
|
* Authorization
|
||||||
* Better history viewing
|
* Better history viewing
|
||||||
* Recent changes page
|
|
||||||
* More markups
|
* More markups
|
||||||
|
2
go.mod
2
go.mod
@ -2,4 +2,4 @@ module github.com/bouncepaw/mycorrhiza
|
|||||||
|
|
||||||
go 1.14
|
go 1.14
|
||||||
|
|
||||||
require github.com/valyala/quicktemplate v1.6.2
|
require github.com/valyala/quicktemplate v1.6.3
|
||||||
|
8
go.sum
8
go.sum
@ -1,11 +1,11 @@
|
|||||||
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||||
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.15.1/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
|
github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
|
||||||
github.com/valyala/quicktemplate v1.6.2 h1:k0vgK7zlmFzqAoIBIOrhrfmZ6JoTGJlLRPLbkPGr2/M=
|
github.com/valyala/quicktemplate v1.6.3 h1:O7EuMwuH7Q94U2CXD6sOX8AYHqQqWtmIk690IhmpkKA=
|
||||||
github.com/valyala/quicktemplate v1.6.2/go.mod h1:mtEJpQtUiBV0SHhMX6RtiJtqxncgrfmjcUy5T68X8TM=
|
github.com/valyala/quicktemplate v1.6.3/go.mod h1:fwPzK2fHuYEODzJ9pkw0ipCPNHZ2tD5KW4lOuSdPKzY=
|
||||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/util"
|
"github.com/bouncepaw/mycorrhiza/util"
|
||||||
@ -31,6 +32,58 @@ type Revision struct {
|
|||||||
Message string
|
Message string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TimeString returns a human readable time representation.
|
||||||
|
func (rev Revision) TimeString() string {
|
||||||
|
return rev.Time.Format(time.RFC822)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HyphaeLinks returns a comma-separated list of hyphae that were affected by this revision as HTML string.
|
||||||
|
func (rev Revision) HyphaeLinks() (html string) {
|
||||||
|
// diff-tree --no-commit-id --name-only -r
|
||||||
|
var (
|
||||||
|
// List of files affected by this revision, one per line.
|
||||||
|
out, err = gitsh("diff-tree", "--no-commit-id", "--name-only", "-r", rev.Hash)
|
||||||
|
// set is used to determine if a certain hypha has been already noted (hyphae are stored in 2 files at most).
|
||||||
|
set = make(map[string]bool)
|
||||||
|
isNewName = func(hyphaName string) bool {
|
||||||
|
if _, present := set[hyphaName]; present {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
set[hyphaName] = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, filename := range strings.Split(out.String(), "\n") {
|
||||||
|
// If filename has an ampersand:
|
||||||
|
if strings.IndexRune(filename, '&') >= 0 {
|
||||||
|
// Remove ampersanded suffix from filename:
|
||||||
|
ampersandPos := strings.LastIndexByte(filename, '&')
|
||||||
|
hyphaName := string([]byte(filename)[0:ampersandPos]) // is it safe?
|
||||||
|
if isNewName(hyphaName) {
|
||||||
|
// Entries are separated by commas
|
||||||
|
if len(set) > 1 {
|
||||||
|
html += `<span aria-hidden="true">, </span>`
|
||||||
|
}
|
||||||
|
html += fmt.Sprintf(`<a href="/rev/%[1]s/%[2]s">%[2]s</a>`, rev.Hash, hyphaName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rev Revision) RecentChangesEntry() (html string) {
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
<li><time>%s</time></li>
|
||||||
|
<li>%s</li>
|
||||||
|
<li>%s</li>
|
||||||
|
<li>%s</li>
|
||||||
|
`, rev.TimeString(), rev.Hash, rev.HyphaeLinks(), rev.Message)
|
||||||
|
}
|
||||||
|
|
||||||
// Path to git executable. Set at init()
|
// Path to git executable. Set at init()
|
||||||
var gitpath string
|
var gitpath string
|
||||||
|
|
||||||
|
@ -6,9 +6,30 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
"github.com/bouncepaw/mycorrhiza/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func RecentChanges(n int) string {
|
||||||
|
var (
|
||||||
|
out, err = gitsh(
|
||||||
|
"log", "--oneline", "--no-merges",
|
||||||
|
"--pretty=format:\"%h\t%ce\t%ct\t%s\"",
|
||||||
|
)
|
||||||
|
revs []Revision
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
for _, line := range strings.Split(out.String(), "\n") {
|
||||||
|
revs = append(revs, parseRevisionLine(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries := make([]string, len(revs))
|
||||||
|
for i, rev := range revs {
|
||||||
|
entries[i] = rev.RecentChangesEntry()
|
||||||
|
}
|
||||||
|
return templates.RecentChangesHTML(entries, n)
|
||||||
|
}
|
||||||
|
|
||||||
// Revisions returns slice of revisions for the given hypha name.
|
// Revisions returns slice of revisions for the given hypha name.
|
||||||
func Revisions(hyphaName string) ([]Revision, error) {
|
func Revisions(hyphaName string) ([]Revision, error) {
|
||||||
var (
|
var (
|
||||||
@ -48,7 +69,7 @@ func (rev *Revision) AsHtmlTableRow(hyphaName string) string {
|
|||||||
<td><time>%s</time></td>
|
<td><time>%s</time></td>
|
||||||
<td><a href="/rev/%s/%s">%s</a></td>
|
<td><a href="/rev/%s/%s">%s</a></td>
|
||||||
<td>%s</td>
|
<td>%s</td>
|
||||||
</tr>`, rev.Time.Format(time.RFC822), rev.Hash, hyphaName, rev.Hash, rev.Message)
|
</tr>`, rev.TimeString(), rev.Hash, hyphaName, rev.Hash, rev.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// See how the file with `filepath` looked at commit with `hash`.
|
// See how the file with `filepath` looked at commit with `hash`.
|
||||||
|
17
main.go
17
main.go
@ -10,6 +10,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/history"
|
"github.com/bouncepaw/mycorrhiza/history"
|
||||||
"github.com/bouncepaw/mycorrhiza/templates"
|
"github.com/bouncepaw/mycorrhiza/templates"
|
||||||
@ -83,6 +85,20 @@ func handlerRandom(w http.ResponseWriter, rq *http.Request) {
|
|||||||
http.Redirect(w, rq, "/page/"+randomHyphaName, http.StatusSeeOther)
|
http.Redirect(w, rq, "/page/"+randomHyphaName, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recent changes
|
||||||
|
func handlerRecentChanges(w http.ResponseWriter, rq *http.Request) {
|
||||||
|
log.Println(rq.URL)
|
||||||
|
var (
|
||||||
|
noPrefix = strings.TrimPrefix(rq.URL.String(), "/recent-changes/")
|
||||||
|
n, err = strconv.Atoi(noPrefix)
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
util.HTTP200Page(w, base(strconv.Itoa(n)+" recent changes", history.RecentChanges(n)))
|
||||||
|
} else {
|
||||||
|
http.Redirect(w, rq, "/recent-changes/20", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.Println("Running MycorrhizaWiki β")
|
log.Println("Running MycorrhizaWiki β")
|
||||||
|
|
||||||
@ -108,6 +124,7 @@ func main() {
|
|||||||
http.HandleFunc("/list", handlerList)
|
http.HandleFunc("/list", handlerList)
|
||||||
http.HandleFunc("/reindex", handlerReindex)
|
http.HandleFunc("/reindex", handlerReindex)
|
||||||
http.HandleFunc("/random", handlerRandom)
|
http.HandleFunc("/random", handlerRandom)
|
||||||
|
http.HandleFunc("/recent-changes/", handlerRecentChanges)
|
||||||
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, rq *http.Request) {
|
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, rq *http.Request) {
|
||||||
http.ServeFile(w, rq, WikiDir+"/static/favicon.ico")
|
http.ServeFile(w, rq, WikiDir+"/static/favicon.ico")
|
||||||
})
|
})
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit bdaaab62574023487610d608d1e9f2f351707a7f
|
Subproject commit 2c0e43199ed28f7022a38463a0eec3af3ecb03c9
|
40
templates/recent_changes.qtpl
Normal file
40
templates/recent_changes.qtpl
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{% func RecentChangesHTML(changes []string, n int) %}
|
||||||
|
<main class="recent-changes">
|
||||||
|
<h1>Recent Changes</h1>
|
||||||
|
|
||||||
|
<nav class="recent-changes__count">
|
||||||
|
See
|
||||||
|
{% for _, m := range []int{20, 0, 50, 0, 100} %}
|
||||||
|
{% switch m %}
|
||||||
|
{% case 0 %}
|
||||||
|
<span aria-hidden="true">|</span>
|
||||||
|
{% case n %}
|
||||||
|
<b>{%d n %}</b>
|
||||||
|
{% default %}
|
||||||
|
<a href="/recent-changes/{%d m %}">{%d m %}</a>
|
||||||
|
{% endswitch %}
|
||||||
|
{% endfor %}
|
||||||
|
recent changes
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{% comment %}
|
||||||
|
Here I am, willing to add some accesibility using ARIA. Turns out,
|
||||||
|
role="feed" is not supported in any screen reader as of September
|
||||||
|
2020. At least web search says so. Even JAWS doesn't support it!
|
||||||
|
How come? I'll add the role anyway. -- bouncepaw
|
||||||
|
{% endcomment %}
|
||||||
|
|
||||||
|
<section class="recent-changes__list" role="feed">
|
||||||
|
{% if len(changes) == 0 %}
|
||||||
|
<p>Could not find any recent changes.</p>
|
||||||
|
{% else %}
|
||||||
|
{% for i, entry := range changes %}
|
||||||
|
<ul class="recent-changes__entry" role="article"
|
||||||
|
aria-setsize="{%d n %}" aria-posinset="{%d i %}">
|
||||||
|
{%s= entry %}
|
||||||
|
</ul>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endfunc %}
|
156
templates/recent_changes.qtpl.go
Normal file
156
templates/recent_changes.qtpl.go
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
// Code generated by qtc from "recent_changes.qtpl". DO NOT EDIT.
|
||||||
|
// See https://github.com/valyala/quicktemplate for details.
|
||||||
|
|
||||||
|
//line templates/recent_changes.qtpl:1
|
||||||
|
package templates
|
||||||
|
|
||||||
|
//line templates/recent_changes.qtpl:1
|
||||||
|
import (
|
||||||
|
qtio422016 "io"
|
||||||
|
|
||||||
|
qt422016 "github.com/valyala/quicktemplate"
|
||||||
|
)
|
||||||
|
|
||||||
|
//line templates/recent_changes.qtpl:1
|
||||||
|
var (
|
||||||
|
_ = qtio422016.Copy
|
||||||
|
_ = qt422016.AcquireByteBuffer
|
||||||
|
)
|
||||||
|
|
||||||
|
//line templates/recent_changes.qtpl:1
|
||||||
|
func StreamRecentChangesHTML(qw422016 *qt422016.Writer, changes []string, n int) {
|
||||||
|
//line templates/recent_changes.qtpl:1
|
||||||
|
qw422016.N().S(`
|
||||||
|
<main class="recent-changes">
|
||||||
|
<h1>Recent Changes</h1>
|
||||||
|
|
||||||
|
<nav class="recent-changes__count">
|
||||||
|
See
|
||||||
|
`)
|
||||||
|
//line templates/recent_changes.qtpl:7
|
||||||
|
for _, m := range []int{20, 0, 50, 0, 100} {
|
||||||
|
//line templates/recent_changes.qtpl:7
|
||||||
|
qw422016.N().S(`
|
||||||
|
`)
|
||||||
|
//line templates/recent_changes.qtpl:8
|
||||||
|
switch m {
|
||||||
|
//line templates/recent_changes.qtpl:9
|
||||||
|
case 0:
|
||||||
|
//line templates/recent_changes.qtpl:9
|
||||||
|
qw422016.N().S(`
|
||||||
|
<span aria-hidden="true">|</span>
|
||||||
|
`)
|
||||||
|
//line templates/recent_changes.qtpl:11
|
||||||
|
case n:
|
||||||
|
//line templates/recent_changes.qtpl:11
|
||||||
|
qw422016.N().S(`
|
||||||
|
<b>`)
|
||||||
|
//line templates/recent_changes.qtpl:12
|
||||||
|
qw422016.N().D(n)
|
||||||
|
//line templates/recent_changes.qtpl:12
|
||||||
|
qw422016.N().S(`</b>
|
||||||
|
`)
|
||||||
|
//line templates/recent_changes.qtpl:13
|
||||||
|
default:
|
||||||
|
//line templates/recent_changes.qtpl:13
|
||||||
|
qw422016.N().S(`
|
||||||
|
<a href="/recent-changes/`)
|
||||||
|
//line templates/recent_changes.qtpl:14
|
||||||
|
qw422016.N().D(m)
|
||||||
|
//line templates/recent_changes.qtpl:14
|
||||||
|
qw422016.N().S(`">`)
|
||||||
|
//line templates/recent_changes.qtpl:14
|
||||||
|
qw422016.N().D(m)
|
||||||
|
//line templates/recent_changes.qtpl:14
|
||||||
|
qw422016.N().S(`</a>
|
||||||
|
`)
|
||||||
|
//line templates/recent_changes.qtpl:15
|
||||||
|
}
|
||||||
|
//line templates/recent_changes.qtpl:15
|
||||||
|
qw422016.N().S(`
|
||||||
|
`)
|
||||||
|
//line templates/recent_changes.qtpl:16
|
||||||
|
}
|
||||||
|
//line templates/recent_changes.qtpl:16
|
||||||
|
qw422016.N().S(`
|
||||||
|
recent changes
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
`)
|
||||||
|
//line templates/recent_changes.qtpl:25
|
||||||
|
qw422016.N().S(`
|
||||||
|
|
||||||
|
<section class="recent-changes__list" role="feed">
|
||||||
|
`)
|
||||||
|
//line templates/recent_changes.qtpl:28
|
||||||
|
if len(changes) == 0 {
|
||||||
|
//line templates/recent_changes.qtpl:28
|
||||||
|
qw422016.N().S(`
|
||||||
|
<p>Could not find any recent changes.</p>
|
||||||
|
`)
|
||||||
|
//line templates/recent_changes.qtpl:30
|
||||||
|
} else {
|
||||||
|
//line templates/recent_changes.qtpl:30
|
||||||
|
qw422016.N().S(`
|
||||||
|
`)
|
||||||
|
//line templates/recent_changes.qtpl:31
|
||||||
|
for i, entry := range changes {
|
||||||
|
//line templates/recent_changes.qtpl:31
|
||||||
|
qw422016.N().S(`
|
||||||
|
<ul class="recent-changes__entry" role="article"
|
||||||
|
aria-setsize="`)
|
||||||
|
//line templates/recent_changes.qtpl:33
|
||||||
|
qw422016.N().D(n)
|
||||||
|
//line templates/recent_changes.qtpl:33
|
||||||
|
qw422016.N().S(`" aria-posinset="`)
|
||||||
|
//line templates/recent_changes.qtpl:33
|
||||||
|
qw422016.N().D(i)
|
||||||
|
//line templates/recent_changes.qtpl:33
|
||||||
|
qw422016.N().S(`">
|
||||||
|
`)
|
||||||
|
//line templates/recent_changes.qtpl:34
|
||||||
|
qw422016.N().S(entry)
|
||||||
|
//line templates/recent_changes.qtpl:34
|
||||||
|
qw422016.N().S(`
|
||||||
|
</ul>
|
||||||
|
`)
|
||||||
|
//line templates/recent_changes.qtpl:36
|
||||||
|
}
|
||||||
|
//line templates/recent_changes.qtpl:36
|
||||||
|
qw422016.N().S(`
|
||||||
|
`)
|
||||||
|
//line templates/recent_changes.qtpl:37
|
||||||
|
}
|
||||||
|
//line templates/recent_changes.qtpl:37
|
||||||
|
qw422016.N().S(`
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
`)
|
||||||
|
//line templates/recent_changes.qtpl:40
|
||||||
|
}
|
||||||
|
|
||||||
|
//line templates/recent_changes.qtpl:40
|
||||||
|
func WriteRecentChangesHTML(qq422016 qtio422016.Writer, changes []string, n int) {
|
||||||
|
//line templates/recent_changes.qtpl:40
|
||||||
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
|
//line templates/recent_changes.qtpl:40
|
||||||
|
StreamRecentChangesHTML(qw422016, changes, n)
|
||||||
|
//line templates/recent_changes.qtpl:40
|
||||||
|
qt422016.ReleaseWriter(qw422016)
|
||||||
|
//line templates/recent_changes.qtpl:40
|
||||||
|
}
|
||||||
|
|
||||||
|
//line templates/recent_changes.qtpl:40
|
||||||
|
func RecentChangesHTML(changes []string, n int) string {
|
||||||
|
//line templates/recent_changes.qtpl:40
|
||||||
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
|
//line templates/recent_changes.qtpl:40
|
||||||
|
WriteRecentChangesHTML(qb422016, changes, n)
|
||||||
|
//line templates/recent_changes.qtpl:40
|
||||||
|
qs422016 := string(qb422016.B)
|
||||||
|
//line templates/recent_changes.qtpl:40
|
||||||
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
|
//line templates/recent_changes.qtpl:40
|
||||||
|
return qs422016
|
||||||
|
//line templates/recent_changes.qtpl:40
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user