Namespaces,
finally. Creation of them still TODO Also brought back account IDs.
This commit is contained in:
parent
fa3fdc263b
commit
6197b4ec9a
12 changed files with 153 additions and 40 deletions
|
@ -22,9 +22,6 @@ public class Startup {
|
|||
@ConfigProperty(name = "talkpages.systemUser.name")
|
||||
private String username;
|
||||
|
||||
@ConfigProperty(name = "talkpages.createExamplePage")
|
||||
private boolean createExamplePage;
|
||||
|
||||
void installStaticRoute(@Observes StartupEvent startupEvent, Router router) {
|
||||
router.route()
|
||||
.path("/static/*")
|
||||
|
@ -33,7 +30,7 @@ public class Startup {
|
|||
|
||||
@Transactional
|
||||
public void examplePage(@Observes StartupEvent ignoredEvent) {
|
||||
if (Account.findById(1) != null) {
|
||||
if (Account.findByName(username) != null) {
|
||||
// system user exists so assuming this is not the first run
|
||||
return;
|
||||
}
|
||||
|
@ -43,15 +40,18 @@ public class Startup {
|
|||
Account account = new Account(username);
|
||||
account.persistAndFlush();
|
||||
|
||||
if (createExamplePage) {
|
||||
addPage(account, "TalkPages", "<p>A website where the users collaboratively create content</p><ul><li><a href=\"https://git.m724.eu/Minecon724/talkpages\">Source code</a></li></ul>");
|
||||
Page talkPagesPage = addPage(account, "TalkPages", "<p>A website where the users collaboratively create content</p><ul><li><a href=\"https://git.m724.eu/Minecon724/talkpages\">Source code</a></li></ul>");
|
||||
addPage(account, "Talkpages", "ambiguous for [TalkPages]");
|
||||
addPage(account, "TP", "@TalkPages");
|
||||
}
|
||||
|
||||
Page tosPage = addPage(account, "Terms of Service", "TODO");
|
||||
addPage(account, "ToS", "@TalkPages/Terms of Service");
|
||||
|
||||
tosPage.setParentPage(talkPagesPage);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void addPage(Account account, String title, String content) {
|
||||
public Page addPage(Account account, String title, String content) {
|
||||
Page page = new Page(title);
|
||||
PageRevision revision = new PageRevision(page, account, content);
|
||||
|
||||
|
@ -59,6 +59,6 @@ public class Startup {
|
|||
page.persistAndFlush();
|
||||
|
||||
account.getRevisions().add(revision);
|
||||
|
||||
return page;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ public class AuthService {
|
|||
*/
|
||||
@Transactional
|
||||
Session register(String username, String password) throws UsernameExistsException {
|
||||
Account account = Account.findById(username);
|
||||
Account account = Account.findByName(username);
|
||||
|
||||
if (account != null) {
|
||||
throw new UsernameExistsException();
|
||||
|
@ -63,7 +63,7 @@ public class AuthService {
|
|||
|
||||
@Transactional
|
||||
Session authenticate(String username, String password) throws InvalidCredentialsException {
|
||||
Account account = Account.findById(username);
|
||||
Account account = Account.findByName(username);
|
||||
|
||||
if (account == null) {
|
||||
throw new InvalidCredentialsException(false);
|
||||
|
|
|
@ -14,8 +14,13 @@ import java.util.List;
|
|||
import java.util.Set;
|
||||
|
||||
// TODO consider moving away authentication to allow like multiple profiles on one account
|
||||
// TODO also consider giving different, namespaced ids to system and addressed accounts
|
||||
@Entity
|
||||
public class Account extends PanacheEntityBase {
|
||||
@Table(
|
||||
indexes = @Index(name = "idx_name", columnList = "name"),
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = "name")
|
||||
)
|
||||
public class Account extends PanacheEntity {
|
||||
private Account() {}
|
||||
|
||||
/**
|
||||
|
@ -46,7 +51,6 @@ public class Account extends PanacheEntityBase {
|
|||
|
||||
// Columns
|
||||
|
||||
@Id
|
||||
private String name;
|
||||
|
||||
private String slug;
|
||||
|
@ -72,4 +76,11 @@ public class Account extends PanacheEntityBase {
|
|||
public List<PageRevision> getRevisions() { return revisions; }
|
||||
|
||||
public boolean isSystemAccount() { return password == null; }
|
||||
|
||||
|
||||
// Operations
|
||||
|
||||
public static Account findByName(String name) {
|
||||
return Account.find("name", name).firstResult();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +1,29 @@
|
|||
package eu.m724.talkpages.orm.entity.content;
|
||||
|
||||
import eu.m724.talkpages.page.action.NoSuchNamespaceException;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
indexes = @Index(name = "idx_title", columnList = "title"),
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = "title")
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"title", "parentPage"})
|
||||
)
|
||||
public class Page extends PanacheEntityBase {
|
||||
public class Page extends PanacheEntity {
|
||||
private Page() {}
|
||||
|
||||
public Page(String title) {
|
||||
// TODO maybe map space to _
|
||||
this.slug = URLEncoder.encode(title, StandardCharsets.UTF_8).replace("+", "%20");
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
// Columns
|
||||
|
||||
@Id
|
||||
private String slug;
|
||||
|
||||
// TODO is this really necessary
|
||||
private String title;
|
||||
|
||||
@OneToOne
|
||||
|
@ -38,21 +32,103 @@ public class Page extends PanacheEntityBase {
|
|||
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<PageRevision> revisions = new ArrayList<>();
|
||||
|
||||
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH}, mappedBy = "parentPage")
|
||||
private List<Page> subpages = new ArrayList<>();
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "parentPage_id")
|
||||
private Page parentPage = null;
|
||||
|
||||
|
||||
// Hooks
|
||||
|
||||
@PreRemove
|
||||
private void preRemove() {
|
||||
subpages.forEach(page -> page.parentPage = null);
|
||||
}
|
||||
|
||||
|
||||
// Getters
|
||||
|
||||
public String getSlug() { return slug; }
|
||||
public String getTitle() { return title; }
|
||||
public PageRevision getLatestRevision() { return latestRevision; }
|
||||
public List<PageRevision> getRevisions() { return revisions; }
|
||||
public List<Page> getSubpages() { return subpages; }
|
||||
public Page getParentPage() { return parentPage; }
|
||||
|
||||
public void setLatestRevision(PageRevision pageRevision) {
|
||||
revisions.add(pageRevision);
|
||||
latestRevision = pageRevision;
|
||||
}
|
||||
|
||||
public void setParentPage(Page parentPage) {
|
||||
// Remove from current parent, if any
|
||||
if (this.parentPage != null)
|
||||
parentPage.subpages.remove(this);
|
||||
|
||||
// Set the parent page and add as a child
|
||||
this.parentPage = parentPage;
|
||||
if (parentPage != null)
|
||||
parentPage.getSubpages().add(this);
|
||||
}
|
||||
|
||||
public String getSlug() {
|
||||
// TODO maybe map space to _
|
||||
return URLEncoder.encode(title, StandardCharsets.UTF_8).replace("+", "%20");
|
||||
}
|
||||
|
||||
/**
|
||||
* Chain of parents. The broadest (root) one is the first one.<br>
|
||||
* The list is empty if there's no parents.
|
||||
*
|
||||
* @return chain of parents
|
||||
*/
|
||||
public List<Page> getChain() {
|
||||
List<Page> parents = new ArrayList<>();
|
||||
|
||||
Page parent = this.getParentPage();
|
||||
while (parent != null) {
|
||||
parents.add(parent);
|
||||
parent = parent.getParentPage();
|
||||
}
|
||||
|
||||
return parents.reversed();
|
||||
}
|
||||
|
||||
// Operations
|
||||
|
||||
/**
|
||||
* Find a {@link Page} by title. The title must not be encoded.<br>
|
||||
* Separate namespaces with a slash (<code>/</code>)<br>
|
||||
*
|
||||
* @param title the title, not encoded
|
||||
* @return page if found, else null
|
||||
* @throws IllegalStateException if
|
||||
*/
|
||||
public static Page findByPath(String title) {
|
||||
String[] parts = title.split("/", -1);
|
||||
|
||||
Page page = findByTitle(parts[0]);
|
||||
|
||||
for (int i=1; i<parts.length; i++) {
|
||||
String part = parts[i];
|
||||
|
||||
// those checks apply to the previous iteration, but won't run on the last one
|
||||
if (page == null)
|
||||
break; // TODO maybe raise an error here
|
||||
|
||||
if (page.title.isBlank())
|
||||
throw new IllegalStateException("Can't have empty title in the middle"); // TODO also enforce
|
||||
|
||||
|
||||
page = page.getSubpages().stream()
|
||||
.filter(p -> p.getTitle().equals(part))
|
||||
.findFirst().orElse(null); // TODO optimize
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
public static Page findByTitle(String title) {
|
||||
return find("title", title).firstResult();
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import java.time.LocalDateTime;
|
|||
|
||||
@Entity
|
||||
@Table(
|
||||
indexes = @Index(columnList = "page_slug"),
|
||||
indexes = @Index(columnList = "page_id"),
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"id", "index"})
|
||||
)
|
||||
public class PageRevision extends PanacheEntity {
|
||||
|
@ -49,7 +49,7 @@ public class PageRevision extends PanacheEntity {
|
|||
private Account author;
|
||||
|
||||
// TODO wondering about a table only for content and meta and title perhaps
|
||||
@Column(columnDefinition="text")
|
||||
@Column(columnDefinition = "text")
|
||||
private String content;
|
||||
|
||||
private int delta;
|
||||
|
|
|
@ -34,7 +34,7 @@ public class HistoryResource {
|
|||
}
|
||||
|
||||
@GET
|
||||
@Path("/{title}")
|
||||
@Path("/{title:.+}")
|
||||
public Response pageHistory(@PathParam("title") String title) {
|
||||
Page page = Page.findByTitle(title);
|
||||
|
||||
|
|
|
@ -35,16 +35,16 @@ public class PageResource {
|
|||
}
|
||||
|
||||
@GET
|
||||
@Path("/{pageId}")
|
||||
public Response viewPage(@PathParam("pageId") String pageId, @QueryParam("revision") Integer revisionId, @QueryParam("redirectFrom") String redirectFrom) {
|
||||
Page page = Page.findByTitle(pageId);
|
||||
@Path("/{title:.+}")
|
||||
public Response viewPage(@PathParam("title") String title, @QueryParam("revision") Integer revisionId, @QueryParam("redirectFrom") String redirectFrom) {
|
||||
Page page = Page.findByPath(title);
|
||||
|
||||
if (page == null) {
|
||||
List<Page> suggestions = Page.findByTitleIgnoreCase(pageId);
|
||||
List<Page> suggestions = Page.findByTitleIgnoreCase(title);
|
||||
if (suggestions.size() == 1) {
|
||||
return redirectService.page(suggestions.getFirst()).build();
|
||||
}
|
||||
return Response.status(Response.Status.NOT_FOUND).entity(Templates.notFound(pageId, suggestions)).build();
|
||||
return Response.status(Response.Status.NOT_FOUND).entity(Templates.notFound(title, suggestions)).build();
|
||||
}
|
||||
|
||||
if (revisionId == null) {
|
||||
|
@ -52,7 +52,7 @@ public class PageResource {
|
|||
if (revision.getContent().startsWith("@")) {
|
||||
String target = revision.getContent().substring(1);
|
||||
if (redirectFrom == null)
|
||||
redirectFrom = pageId;
|
||||
redirectFrom = title;
|
||||
return Response.temporaryRedirect(URI.create("/page/" + redirectService.titleEncoded(target) + "?redirectFrom=" + URLEncoder.encode(redirectFrom, StandardCharsets.UTF_8))).build();
|
||||
}
|
||||
return Response.ok().entity(Templates.page(page, page.getLatestRevision(), false)).build();
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package eu.m724.talkpages.page.action;
|
||||
|
||||
public class NoSuchNamespaceException extends RuntimeException {
|
||||
public final String namespace;
|
||||
|
||||
public NoSuchNamespaceException(String namespace) {
|
||||
this.namespace = namespace;
|
||||
}
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
talkpages.homePage=/
|
||||
talkpages.systemUser.name=System
|
||||
talkpages.createExamplePage=true
|
||||
|
||||
quarkus.http.auth.basic=true
|
||||
|
||||
quarkus.hibernate-orm.database.generation=drop-and-create
|
||||
quarkus.hibernate-orm.database.generation=update
|
||||
|
||||
quarkus.datasource.db-kind=h2
|
||||
quarkus.datasource.username=username-default
|
||||
|
|
0
src/main/resources/static/style.css
Normal file
0
src/main/resources/static/style.css
Normal file
|
@ -1,4 +1,4 @@
|
|||
{#include layout}
|
||||
{#include layout customTitle=true noHeading=true}
|
||||
{#header}
|
||||
{#if http:param("redirectFrom") != null}
|
||||
<p>Redirected from {http:param("redirectFrom")}</p>
|
||||
|
@ -15,9 +15,23 @@
|
|||
<h4>This is the current revision #{revision.index} authored by {revision.author.name}.</h4>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{/header}
|
||||
|
||||
{#pageTitle}{page.title}{/pageTitle}
|
||||
{#pageTitle}{page.getTitle} - TalkPages{/pageTitle}
|
||||
|
||||
{#if page.getChain.isEmpty} <!-- TODO don't repeat that -->
|
||||
<h1>{page.getTitle}</h1>
|
||||
{#else}
|
||||
<h1>
|
||||
<small>
|
||||
{#for parent in page.getChain}
|
||||
<a href="/page/{parent.getSlug}">{parent.getTitle}</a> /
|
||||
{/for}
|
||||
</small>
|
||||
{page.getTitle}
|
||||
</h1>
|
||||
{/if}
|
||||
|
||||
{revision.content.sanitizeContent.raw}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<!-- TODO opengraph tags and maybe some nice css. also add errors and stuff here -->
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
{#if theme:darkTheme}
|
||||
<link rel="stylesheet" href="/static/dark.css">
|
||||
{/if}
|
||||
|
@ -11,7 +12,10 @@
|
|||
</head>
|
||||
<body>
|
||||
{#insert header}{/}
|
||||
|
||||
{#if !noHeading??}
|
||||
<h1>{#insert pageTitle /}</h1>
|
||||
{/if}
|
||||
|
||||
{#insert}{/}
|
||||
</body>
|
||||
|
|
Loading…
Reference in a new issue