Fri, 09 Oct 2020 19:07:05 +0200
adds issue comments
1.1 --- a/setup/postgres/psql_create_tables.sql Fri Oct 09 19:06:51 2020 +0200 1.2 +++ b/setup/postgres/psql_create_tables.sql Fri Oct 09 19:07:05 2020 +0200 1.3 @@ -83,4 +83,12 @@ 1.4 primary key (issueid, versionid) 1.5 ); 1.6 1.7 - 1.8 +create table lpit_issue_comment ( 1.9 + commentid serial primary key, 1.10 + issueid integer not null references lpit_issue(issueid), 1.11 + userid integer references lpit_user(userid), 1.12 + created timestamp with time zone not null default now(), 1.13 + updated timestamp with time zone not null default now(), 1.14 + updatecount integer not null default 0, 1.15 + comment text not null 1.16 +);
2.1 --- a/src/main/java/de/uapcore/lightpit/dao/IssueDao.java Fri Oct 09 19:06:51 2020 +0200 2.2 +++ b/src/main/java/de/uapcore/lightpit/dao/IssueDao.java Fri Oct 09 19:07:05 2020 +0200 2.3 @@ -29,6 +29,7 @@ 2.4 package de.uapcore.lightpit.dao; 2.5 2.6 import de.uapcore.lightpit.entities.Issue; 2.7 +import de.uapcore.lightpit.entities.IssueComment; 2.8 import de.uapcore.lightpit.entities.Project; 2.9 import de.uapcore.lightpit.entities.Version; 2.10 2.11 @@ -59,6 +60,24 @@ 2.12 List<Issue> list(Version version) throws SQLException; 2.13 2.14 /** 2.15 + * Lists all comments for a specific issue in chronological order. 2.16 + * 2.17 + * @param issue the issue 2.18 + * @return the list of comments 2.19 + * @throws SQLException on any kind of SQL error 2.20 + */ 2.21 + List<IssueComment> listComments(Issue issue) throws SQLException; 2.22 + 2.23 + /** 2.24 + * Stores the specified comment in database. 2.25 + * This is an update-or-insert operation. 2.26 + * 2.27 + * @param comment the comment to save 2.28 + * @throws SQLException on any kind of SQL error 2.29 + */ 2.30 + void saveComment(IssueComment comment) throws SQLException; 2.31 + 2.32 + /** 2.33 * Saves an instances to the database. 2.34 * Implementations of this DAO must guarantee that the generated ID is stored in the instance. 2.35 *
3.1 --- a/src/main/java/de/uapcore/lightpit/dao/UserDao.java Fri Oct 09 19:06:51 2020 +0200 3.2 +++ b/src/main/java/de/uapcore/lightpit/dao/UserDao.java Fri Oct 09 19:07:05 2020 +0200 3.3 @@ -32,8 +32,19 @@ 3.4 3.5 import java.sql.SQLException; 3.6 import java.util.List; 3.7 +import java.util.Optional; 3.8 3.9 public interface UserDao extends GenericDao<User> { 3.10 3.11 List<User> list() throws SQLException; 3.12 + 3.13 + /** 3.14 + * Tries to find a user by their username. 3.15 + * The search is case-insensitive. 3.16 + * 3.17 + * @param username the username 3.18 + * @return the user object or an empty optional if no such user exists 3.19 + * @throws SQLException 3.20 + */ 3.21 + Optional<User> findByUsername(String username) throws SQLException; 3.22 }
4.1 --- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java Fri Oct 09 19:06:51 2020 +0200 4.2 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java Fri Oct 09 19:07:05 2020 +0200 4.3 @@ -47,6 +47,7 @@ 4.4 private final PreparedStatement affectedVersions, resolvedVersions; 4.5 private final PreparedStatement clearAffected, clearResolved; 4.6 private final PreparedStatement insertAffected, insertResolved; 4.7 + private final PreparedStatement insertComment, updateComment, listComments; 4.8 4.9 public PGIssueDao(Connection connection) throws SQLException { 4.10 list = connection.prepareStatement( 4.11 @@ -107,9 +108,19 @@ 4.12 ); 4.13 clearResolved = connection.prepareStatement("delete from lpit_issue_resolved_version where issueid = ?"); 4.14 insertResolved = connection.prepareStatement("insert into lpit_issue_resolved_version (issueid, versionid) values (?,?)"); 4.15 + 4.16 + insertComment = connection.prepareStatement( 4.17 + "insert into lpit_issue_comment (issueid, comment, userid) values (?, ? ,?)" 4.18 + ); 4.19 + updateComment = connection.prepareStatement( 4.20 + "update lpit_issue_comment set comment = ?, updated = now(), updatecount = updatecount+1 where commentid = ?" 4.21 + ); 4.22 + listComments = connection.prepareStatement( 4.23 + "select * from lpit_issue_comment left join lpit_user using (userid) where issueid = ? order by created" 4.24 + ); 4.25 } 4.26 4.27 - private User obtainAssignee(ResultSet result) throws SQLException { 4.28 + private User obtainUser(ResultSet result) throws SQLException { 4.29 final int id = result.getInt("userid"); 4.30 if (id != 0) { 4.31 final var user = new User(id); 4.32 @@ -132,7 +143,7 @@ 4.33 issue.setCategory(IssueCategory.valueOf(result.getString("category"))); 4.34 issue.setSubject(result.getString("subject")); 4.35 issue.setDescription(result.getString("description")); 4.36 - issue.setAssignee(obtainAssignee(result)); 4.37 + issue.setAssignee(obtainUser(result)); 4.38 issue.setCreated(result.getTimestamp("created")); 4.39 issue.setUpdated(result.getTimestamp("updated")); 4.40 issue.setEta(result.getDate("eta")); 4.41 @@ -252,4 +263,38 @@ 4.42 issue.setAffectedVersions(listVersions(affectedVersions, issue)); 4.43 issue.setResolvedVersions(listVersions(resolvedVersions, issue)); 4.44 } 4.45 + 4.46 + @Override 4.47 + public List<IssueComment> listComments(Issue issue) throws SQLException { 4.48 + listComments.setInt(1, issue.getId()); 4.49 + List<IssueComment> comments = new ArrayList<>(); 4.50 + try (var result = listComments.executeQuery()) { 4.51 + while (result.next()) { 4.52 + final var comment = new IssueComment(result.getInt("commentid"), issue); 4.53 + comment.setCreated(result.getTimestamp("created")); 4.54 + comment.setUpdated(result.getTimestamp("updated")); 4.55 + comment.setUpdateCount(result.getInt("updatecount")); 4.56 + comment.setComment(result.getString("comment")); 4.57 + comment.setAuthor(obtainUser(result)); 4.58 + comments.add(comment); 4.59 + } 4.60 + } 4.61 + return comments; 4.62 + } 4.63 + 4.64 + @Override 4.65 + public void saveComment(IssueComment comment) throws SQLException { 4.66 + Objects.requireNonNull(comment.getComment()); 4.67 + Objects.requireNonNull(comment.getIssue()); 4.68 + if (comment.getId() >= 0) { 4.69 + updateComment.setString(1, comment.getComment()); 4.70 + updateComment.setInt(2, comment.getId()); 4.71 + updateComment.execute(); 4.72 + } else { 4.73 + insertComment.setInt(1, comment.getIssue().getId()); 4.74 + insertComment.setString(2, comment.getComment()); 4.75 + setForeignKeyOrNull(insertComment, 3, comment.getAuthor(), User::getId); 4.76 + insertComment.execute(); 4.77 + } 4.78 + } 4.79 }
5.1 --- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGUserDao.java Fri Oct 09 19:06:51 2020 +0200 5.2 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGUserDao.java Fri Oct 09 19:07:05 2020 +0200 5.3 @@ -38,12 +38,13 @@ 5.4 import java.util.ArrayList; 5.5 import java.util.List; 5.6 import java.util.Objects; 5.7 +import java.util.Optional; 5.8 5.9 import static de.uapcore.lightpit.dao.Functions.setStringOrNull; 5.10 5.11 public final class PGUserDao implements UserDao { 5.12 5.13 - private final PreparedStatement insert, update, list, find; 5.14 + private final PreparedStatement insert, update, list, find, findByUsername; 5.15 5.16 public PGUserDao(Connection connection) throws SQLException { 5.17 list = connection.prepareStatement( 5.18 @@ -54,6 +55,10 @@ 5.19 "select userid, username, lastname, givenname, mail " + 5.20 "from lpit_user where userid = ? "); 5.21 5.22 + findByUsername = connection.prepareStatement( 5.23 + "select userid, username, lastname, givenname, mail " + 5.24 + "from lpit_user where lower(username) = lower(?) "); 5.25 + 5.26 insert = connection.prepareStatement("insert into lpit_user (username, lastname, givenname, mail) values (?, ?, ?, ?)"); 5.27 update = connection.prepareStatement("update lpit_user set lastname = ?, givenname = ?, mail = ? where userid = ?"); 5.28 } 5.29 @@ -111,4 +116,16 @@ 5.30 } 5.31 } 5.32 } 5.33 + 5.34 + @Override 5.35 + public Optional<User> findByUsername(String username) throws SQLException { 5.36 + findByUsername.setString(1, username); 5.37 + try (var result = findByUsername.executeQuery()) { 5.38 + if (result.next()) { 5.39 + return Optional.of(mapColumns(result)); 5.40 + } else { 5.41 + return Optional.empty(); 5.42 + } 5.43 + } 5.44 + } 5.45 }
6.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 6.2 +++ b/src/main/java/de/uapcore/lightpit/entities/IssueComment.java Fri Oct 09 19:07:05 2020 +0200 6.3 @@ -0,0 +1,85 @@ 6.4 +package de.uapcore.lightpit.entities; 6.5 + 6.6 +import java.sql.Timestamp; 6.7 +import java.time.Instant; 6.8 +import java.util.Objects; 6.9 + 6.10 +public class IssueComment { 6.11 + 6.12 + private final Issue issue; 6.13 + private final int commentid; 6.14 + 6.15 + private User author; 6.16 + private String comment; 6.17 + 6.18 + private Timestamp created = Timestamp.from(Instant.now()); 6.19 + private Timestamp updated = Timestamp.from(Instant.now()); 6.20 + private int updatecount = 0; 6.21 + 6.22 + 6.23 + public IssueComment(int id, Issue issue) { 6.24 + this.commentid = id; 6.25 + this.issue = issue; 6.26 + } 6.27 + 6.28 + public Issue getIssue() { 6.29 + return issue; 6.30 + } 6.31 + 6.32 + public int getId() { 6.33 + return commentid; 6.34 + } 6.35 + 6.36 + public User getAuthor() { 6.37 + return author; 6.38 + } 6.39 + 6.40 + public void setAuthor(User author) { 6.41 + this.author = author; 6.42 + } 6.43 + 6.44 + public String getComment() { 6.45 + return comment; 6.46 + } 6.47 + 6.48 + public void setComment(String comment) { 6.49 + this.comment = comment; 6.50 + } 6.51 + 6.52 + public Timestamp getCreated() { 6.53 + return created; 6.54 + } 6.55 + 6.56 + public void setCreated(Timestamp created) { 6.57 + this.created = created; 6.58 + } 6.59 + 6.60 + public Timestamp getUpdated() { 6.61 + return updated; 6.62 + } 6.63 + 6.64 + public void setUpdated(Timestamp updated) { 6.65 + this.updated = updated; 6.66 + } 6.67 + 6.68 + public int getUpdateCount() { 6.69 + return updatecount; 6.70 + } 6.71 + 6.72 + public void setUpdateCount(int updatecount) { 6.73 + this.updatecount = updatecount; 6.74 + } 6.75 + 6.76 + @Override 6.77 + public boolean equals(Object o) { 6.78 + if (this == o) return true; 6.79 + if (o == null || getClass() != o.getClass()) return false; 6.80 + IssueComment that = (IssueComment) o; 6.81 + return commentid == that.commentid; 6.82 + } 6.83 + 6.84 + @Override 6.85 + public int hashCode() { 6.86 + return Objects.hash(commentid); 6.87 + } 6.88 +}
7.1 --- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java Fri Oct 09 19:06:51 2020 +0200 7.2 +++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java Fri Oct 09 19:07:05 2020 +0200 7.3 @@ -282,11 +282,15 @@ 7.4 viewModel.setIssue(issue); 7.5 viewModel.configureVersionSelectors(viewModel.getProjectInfo().getVersions()); 7.6 viewModel.setUsers(dao.getUserDao().list()); 7.7 + if (issue.getId() >= 0) { 7.8 + viewModel.setComments(dao.getIssueDao().listComments(issue)); 7.9 + } 7.10 } 7.11 7.12 @RequestMapping(requestPath = "issues/edit", method = HttpMethod.GET) 7.13 public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException { 7.14 final var viewModel = new IssueEditView(); 7.15 + populate(viewModel, req, dao); 7.16 7.17 final var issueParam = getParameter(req, Integer.class, "issue"); 7.18 if (issueParam.isPresent()) { 7.19 @@ -294,10 +298,8 @@ 7.20 final var issue = issueDao.find(issueParam.get()); 7.21 issueDao.joinVersionInformation(issue); 7.22 req.getSession().setAttribute(SESSION_ATTR_SELECTED_PROJECT, issue.getProject().getId()); 7.23 - populate(viewModel, req, dao); 7.24 configure(viewModel, issue, dao); 7.25 } else { 7.26 - populate(viewModel, req, dao); 7.27 configure(viewModel, new Issue(-1), dao); 7.28 } 7.29 7.30 @@ -305,7 +307,7 @@ 7.31 } 7.32 7.33 @RequestMapping(requestPath = "issues/commit", method = HttpMethod.POST) 7.34 - public ResponseType commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException { 7.35 + public ResponseType commitIssue(HttpServletRequest req, DataAccessObjects dao) throws SQLException { 7.36 Issue issue = new Issue(-1); 7.37 try { 7.38 issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow()); 7.39 @@ -335,16 +337,57 @@ 7.40 // specifying the issue parameter keeps the edited issue as menu item 7.41 setRedirectLocation(req, "./projects/view?pid=" + issue.getProject().getId()); 7.42 setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); 7.43 + 7.44 + return ResponseType.HTML; 7.45 } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) { 7.46 // TODO: set request attribute with error text 7.47 LOG.warn("Form validation failure: {}", ex.getMessage()); 7.48 LOG.debug("Details:", ex); 7.49 final var viewModel = new IssueEditView(); 7.50 + populate(viewModel, req, dao); 7.51 configure(viewModel, issue, dao); 7.52 // TODO: set Error Text 7.53 return forwardView(req, viewModel, "issue-form"); 7.54 } 7.55 + } 7.56 7.57 - return ResponseType.HTML; 7.58 + @RequestMapping(requestPath = "issues/comment", method = HttpMethod.POST) 7.59 + public ResponseType commentIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException { 7.60 + final var issueIdParam = getParameter(req, Integer.class, "issueid"); 7.61 + if (issueIdParam.isEmpty()) { 7.62 + resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Detected manipulated form."); 7.63 + return ResponseType.NONE; 7.64 + } 7.65 + final var issue = new Issue(issueIdParam.get()); 7.66 + try { 7.67 + final var issueComment = new IssueComment(getParameter(req, Integer.class, "commentid").orElse(-1), issue); 7.68 + issueComment.setComment(getParameter(req, String.class, "comment").orElse("")); 7.69 + 7.70 + if (issueComment.getComment().isBlank()) { 7.71 + throw new IllegalArgumentException("comment.null"); 7.72 + } 7.73 + 7.74 + LOG.debug("User {} is commenting on issue #{}", req.getRemoteUser(), issue.getId()); 7.75 + if (req.getRemoteUser() != null) { 7.76 + dao.getUserDao().findByUsername(req.getRemoteUser()).ifPresent(issueComment::setAuthor); 7.77 + } 7.78 + 7.79 + dao.getIssueDao().saveComment(issueComment); 7.80 + 7.81 + // specifying the issue parameter keeps the edited issue as menu item 7.82 + setRedirectLocation(req, "./projects/issues/edit?issue=" + issue.getId()); 7.83 + setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL); 7.84 + 7.85 + return ResponseType.HTML; 7.86 + } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) { 7.87 + // TODO: set request attribute with error text 7.88 + LOG.warn("Form validation failure: {}", ex.getMessage()); 7.89 + LOG.debug("Details:", ex); 7.90 + final var viewModel = new IssueEditView(); 7.91 + populate(viewModel, req, dao); 7.92 + configure(viewModel, issue, dao); 7.93 + // TODO: set Error Text 7.94 + return forwardView(req, viewModel, "issue-form"); 7.95 + } 7.96 } 7.97 }
8.1 --- a/src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java Fri Oct 09 19:06:51 2020 +0200 8.2 +++ b/src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java Fri Oct 09 19:07:05 2020 +0200 8.3 @@ -11,6 +11,7 @@ 8.4 private Set<Version> versionsUpcoming = new HashSet<>(); 8.5 private Set<Version> versionsRecent = new HashSet<>(); 8.6 private List<User> users; 8.7 + private List<IssueComment> comments; 8.8 8.9 public void setIssue(Issue issue) { 8.10 this.issue = issue; 8.11 @@ -66,4 +67,12 @@ 8.12 public IssueCategory[] getIssueCategory() { 8.13 return IssueCategory.values(); 8.14 } 8.15 + 8.16 + public List<IssueComment> getComments() { 8.17 + return comments; 8.18 + } 8.19 + 8.20 + public void setComments(List<IssueComment> comments) { 8.21 + this.comments = comments; 8.22 + } 8.23 }
9.1 --- a/src/main/resources/localization/projects.properties Fri Oct 09 19:06:51 2020 +0200 9.2 +++ b/src/main/resources/localization/projects.properties Fri Oct 09 19:07:05 2020 +0200 9.3 @@ -28,6 +28,7 @@ 9.4 button.version.edit=Edit Version 9.5 button.issue.create=New Issue 9.6 button.issue.all=All Issues 9.7 +button.comment=Comment 9.8 9.9 no-projects=Welcome to LightPIT. Start off by creating a new project! 9.10 9.11 @@ -95,3 +96,6 @@ 9.12 issue.status.Rejected=Rejected 9.13 issue.status.Withdrawn=Withdrawn 9.14 issue.status.Duplicate=Duplicate 9.15 + 9.16 +issue.comments=Comments 9.17 +issue.comments.anonauthor=Anonymous Author
10.1 --- a/src/main/resources/localization/projects_de.properties Fri Oct 09 19:06:51 2020 +0200 10.2 +++ b/src/main/resources/localization/projects_de.properties Fri Oct 09 19:07:05 2020 +0200 10.3 @@ -28,6 +28,7 @@ 10.4 button.version.edit=Version Bearbeiten 10.5 button.issue.create=Neuer Vorgang 10.6 button.issue.all=Alle Vorg\u00e4nge 10.7 +button.comment=Kommentieren 10.8 10.9 no-projects=Wilkommen bei LightPIT. Beginnen Sie mit der Erstellung eines Projektes! 10.10 10.11 @@ -94,3 +95,6 @@ 10.12 issue.status.Rejected=Zur\u00fcckgewiesen 10.13 issue.status.Withdrawn=Zur\u00fcckgezogen 10.14 issue.status.Duplicate=Duplikat 10.15 + 10.16 +issue.comments=Kommentare 10.17 +issue.comments.anonauthor=Anonymer Autor 10.18 \ No newline at end of file
11.1 --- a/src/main/webapp/WEB-INF/jsp/issue-form.jsp Fri Oct 09 19:06:51 2020 +0200 11.2 +++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp Fri Oct 09 19:07:05 2020 +0200 11.3 @@ -32,10 +32,10 @@ 11.4 <c:set var="issue" scope="page" value="${viewmodel.issue}" /> 11.5 11.6 <form action="./projects/issues/commit" method="post"> 11.7 - <table class="formtable"> 11.8 + <table class="formtable fullwidth"> 11.9 <colgroup> 11.10 <col> 11.11 - <col style="width: 75ch"> 11.12 + <col style="width: 100%"> 11.13 </colgroup> 11.14 <tbody> 11.15 <c:if test="${viewmodel.issue.id ge 0}"> 11.16 @@ -169,3 +169,46 @@ 11.17 </tfoot> 11.18 </table> 11.19 </form> 11.20 +<hr class="comments-separator"/> 11.21 +<h2><fmt:message key="issue.comments"/></h2> 11.22 +<c:if test="${viewmodel.issue.id ge 0}"> 11.23 +<form id="comment-form" action="./projects/issues/comment" method="post"> 11.24 + <table class="formtable fullwidth"> 11.25 + <tbody> 11.26 + <tr> 11.27 + <td><textarea rows="5" name="comment"></textarea></td> 11.28 + </tr> 11.29 + </tbody> 11.30 + <tfoot> 11.31 + <tr> 11.32 + <td> 11.33 + <input type="hidden" name="issueid" value="${issue.id}"/> 11.34 + <button type="submit"><fmt:message key="button.comment"/></button> 11.35 + </td> 11.36 + </tr> 11.37 + </tfoot> 11.38 + </table> 11.39 +</form> 11.40 + <c:forEach var="comment" items="${viewmodel.comments}"> 11.41 + <div class="comment"> 11.42 + <div class="caption"> 11.43 + <c:if test="${not empty comment.author}"> 11.44 + <c:out value="${comment.author.displayname}"/> 11.45 + </c:if> 11.46 + <c:if test="${empty comment.author}"> 11.47 + <fmt:message key="issue.comments.anonauthor"/> 11.48 + </c:if> 11.49 + </div> 11.50 + <div class="smalltext"> 11.51 + <fmt:formatDate type="BOTH" value="${comment.created}" /> 11.52 + <c:if test="${comment.updateCount gt 0}"> 11.53 + <!-- TODO: update count --> 11.54 + </c:if> 11.55 + </div> 11.56 + <div class="medskip"> 11.57 + <c:out value="${comment.comment}"/> 11.58 + </div> 11.59 + </div> 11.60 + </c:forEach> 11.61 +</c:if> 11.62 +
12.1 --- a/src/main/webapp/lightpit.css Fri Oct 09 19:06:51 2020 +0200 12.2 +++ b/src/main/webapp/lightpit.css Fri Oct 09 19:07:05 2020 +0200 12.3 @@ -65,7 +65,7 @@ 12.4 flex-flow: column; 12.5 position: fixed; 12.6 height: 100%; 12.7 - width: 30ch; 12.8 + width: 40ch; /* adjust with sidebar-spacing.margin-left */ 12.9 padding-top: 2.25rem; 12.10 color: #3060f8; 12.11 border-image-source: linear-gradient(to bottom, #606060, rgba(60, 60, 60, .25)); 12.12 @@ -75,7 +75,7 @@ 12.13 } 12.14 12.15 #content-area.sidebar-spacing { 12.16 - margin-left: 30ch; 12.17 + margin-left: 40ch; /* adjust with sideMenu.width */ 12.18 } 12.19 12.20 #mainMenu { 12.21 @@ -197,6 +197,7 @@ 12.22 12.23 table.formtable tbody td > * { 12.24 width: 100%; 12.25 + box-sizing: border-box; 12.26 } 12.27 12.28 table.formtable input[type=date] {
13.1 --- a/src/main/webapp/projects.css Fri Oct 09 19:06:51 2020 +0200 13.2 +++ b/src/main/webapp/projects.css Fri Oct 09 19:07:05 2020 +0200 13.3 @@ -137,3 +137,15 @@ 13.4 color: lightgray; 13.5 background: darkgray; 13.6 } 13.7 + 13.8 +hr.comments-separator { 13.9 + border-image-source: linear-gradient(to right, rgba(60, 60, 60, .1), rgba(96, 96, 96, 1), rgba(60, 60, 60, .1)); 13.10 + border-image-slice: 1; 13.11 + border-width: 1pt; 13.12 + border-style: none; 13.13 + border-top-style: solid; 13.14 +} 13.15 + 13.16 +div.comment { 13.17 + margin-bottom: 1.25em; 13.18 +}