roundabout,
created on Sunday, 17 December 2023, 19:41:04 (1702842064),
received on Wednesday, 31 July 2024, 06:54:39 (1722408879)
Author identity: vlad <vlad.muntoiu@gmail.com>
a6d3365a21fa37b09f54fd8260ac0b60ea635550
app.py
@@ -78,6 +78,23 @@ with app.app_context():
self.accessLevel = level
class PostVote(db.Model):
id = db.Column(db.Integer, primary_key=True)
userUsername = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
postIdentifier = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=False)
voteScore = db.Column(db.SmallInteger(), nullable=False)
user = db.relationship("User", back_populates="votes")
post = db.relationship("Post", back_populates="votes")
__table_args__ = (db.UniqueConstraint("userUsername", "postIdentifier", name="_user_post_uc"),)
def __init__(self, user, post, score):
self.userUsername = user.username
self.postIdentifier = post.identifier
self.voteScore = score
class User(db.Model):
username = db.Column(db.String(32), unique=True, nullable=False, primary_key=True)
displayName = db.Column(db.Unicode(128), unique=False, nullable=True)
@@ -93,6 +110,7 @@ with app.app_context():
repositories = db.relationship("Repo", back_populates="owner")
repoAccess = db.relationship("RepoAccess", back_populates="user")
votes = db.relationship("PostVote", back_populates="user")
commits = db.relationship("Commit", back_populates="owner")
posts = db.relationship("Post", back_populates="owner")
@@ -163,7 +181,7 @@ with app.app_context():
owner = db.relationship("User", back_populates="commits")
def __init__(self, sha, owner, repo, date, message, ownerIdentity):
self.identifier = f"/{owner.username}/{repo.name}/{sha}"
self.identifier = f"{repo.route}/{sha}"
self.sha = sha
self.repoName = repo.route
self.repo = repo
@@ -176,14 +194,17 @@ with app.app_context():
class Post(db.Model):
identifier = db.Column(db.String(109), unique=True, nullable=False, primary_key=True)
number = db.Column(db.String(109), nullable=False)
number = db.Column(db.Integer, nullable=False)
repoName = db.Column(db.String(98), db.ForeignKey("repo.route"), nullable=False)
ownerName = db.Column(db.String(32), db.ForeignKey("user.username"), nullable=False)
votes = db.relationship("PostVote", back_populates="post")
voteSum = db.Column(db.Integer, nullable=False, default=0)
parentID = db.Column(db.String(109), db.ForeignKey("post.identifier"), nullable=True)
state = db.Column(db.SmallInteger, nullable=True, default=1)
date = db.Column(db.DateTime, default=datetime.now)
lastUpdated = db.Column(db.DateTime, default=datetime.now)
subject = db.Column(db.Unicode(384))
message = db.Column(db.UnicodeText)
repo = db.relationship("Repo", back_populates="posts")
@@ -192,7 +213,7 @@ with app.app_context():
children = db.relationship("Post", back_populates="parent", remote_side="Post.parentID")
def __init__(self, owner, repo, parent, subject, message):
self.identifier = f"/{owner.username}/{repo.name}/{repo.lastPostID}"
self.identifier = f"{repo.route}/{repo.lastPostID}"
self.number = repo.lastPostID
self.repoName = repo.route
self.repo = repo
@@ -203,6 +224,12 @@ with app.app_context():
self.parent = parent
repo.lastPostID += 1
def updateDate(self):
self.lastUpdated = datetime.now()
with db.session.no_autoflush:
if self.parent is not None:
self.parent.updateDate()
def getPermissionLevel(loggedIn, username, repository):
user = User.query.filter_by(username=loggedIn).first()
@@ -419,7 +446,7 @@ def repositoryRaw(username, repository, branch, subpath):
repository) is not None):
flask.abort(403)
serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository))
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
app.logger.info(f"Loading {serverRepoLocation}")
@@ -474,7 +501,7 @@ def repositoryTree(username, repository, branch, subpath):
repository) is not None):
flask.abort(403)
serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository))
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
app.logger.info(f"Loading {serverRepoLocation}")
@@ -619,7 +646,7 @@ def repositoryForum(username, repository):
repository) is not None):
flask.abort(403)
serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository))
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
app.logger.info(f"Loading {serverRepoLocation}")
@@ -629,20 +656,48 @@ def repositoryForum(username, repository):
repo = git.Repo(serverRepoLocation)
repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first()
user = User.query.filter_by(username=username).first()
user = User.query.filter_by(username=flask.session.get("username")).first()
relationships = RepoAccess.query.filter_by(repo=repoData)
userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first()
return flask.render_template("repo-forum.html", username=username, repository=repository, repoData=repoData, relationships=relationships, repo=repo, userRelationship=userRelationship, Post=Post)
@app.route("/<username>/<repository>/forum/<postID>")
@app.route("/<username>/<repository>/forum/new", methods=["POST"])
def repositoryForumAdd(username, repository):
if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username,
repository) is not None):
flask.abort(403)
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
app.logger.info(f"Loading {serverRepoLocation}")
if not os.path.exists(serverRepoLocation):
app.logger.error(f"Cannot load {serverRepoLocation}")
return flask.render_template("not-found.html"), 404
repo = git.Repo(serverRepoLocation)
repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first()
user = User.query.filter_by(username=flask.session.get("username")).first()
relationships = RepoAccess.query.filter_by(repo=repoData)
userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first()
post = Post(user, repoData, None, flask.request.form["subject"], flask.request.form["message"])
db.session.add(post)
db.session.commit()
return flask.redirect(flask.url_for("repositoryForumThread", username=username, repository=repository, postID=post.number), code=303)
@app.route("/<username>/<repository>/forum/<int:postID>")
def repositoryForumThread(username, repository, postID):
if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username,
repository) is not None):
flask.abort(403)
serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository))
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
app.logger.info(f"Loading {serverRepoLocation}")
@@ -652,20 +707,102 @@ def repositoryForumThread(username, repository, postID):
repo = git.Repo(serverRepoLocation)
repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first()
user = User.query.filter_by(username=username).first()
user = User.query.filter_by(username=flask.session.get("username")).first()
relationships = RepoAccess.query.filter_by(repo=repoData)
userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first()
return flask.render_template("repo-forum-thread.html", username=username, repository=repository, repoData=repoData, relationships=relationships, repo=repo, userRelationship=userRelationship, Post=Post, postID=postID, maxPostNesting=4)
@app.route("/<username>/<repository>/forum/<int:postID>/reply", methods=["POST"])
def repositoryForumReply(username, repository, postID):
if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username,
repository) is not None):
flask.abort(403)
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
app.logger.info(f"Loading {serverRepoLocation}")
if not os.path.exists(serverRepoLocation):
app.logger.error(f"Cannot load {serverRepoLocation}")
return flask.render_template("not-found.html"), 404
repo = git.Repo(serverRepoLocation)
repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first()
user = User.query.filter_by(username=flask.session.get("username")).first()
relationships = RepoAccess.query.filter_by(repo=repoData)
userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first()
if not user:
flask.abort(401)
parent = Post.query.filter_by(identifier=f"/{username}/{repository}/{postID}").first()
post = Post(user, repoData, parent, flask.request.form["subject"], flask.request.form["message"])
db.session.add(post)
post.updateDate()
db.session.commit()
return flask.redirect(flask.url_for("repositoryForumThread", username=username, repository=repository, postID=postID), code=303)
@app.route("/<username>/<repository>/forum/<int:postID>/voteup", defaults={"score": 1})
@app.route("/<username>/<repository>/forum/<int:postID>/votedown", defaults={"score": -1})
@app.route("/<username>/<repository>/forum/<int:postID>/votes", defaults={"score": 0})
def repositoryForumVote(username, repository, postID, score):
if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username,
repository) is not None):
flask.abort(403)
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
app.logger.info(f"Loading {serverRepoLocation}")
if not os.path.exists(serverRepoLocation):
app.logger.error(f"Cannot load {serverRepoLocation}")
return flask.render_template("not-found.html"), 404
repo = git.Repo(serverRepoLocation)
repoData = Repo.query.filter_by(route=f"/{username}/{repository}").first()
user = User.query.filter_by(username=flask.session.get("username")).first()
relationships = RepoAccess.query.filter_by(repo=repoData)
userRelationship = RepoAccess.query.filter_by(repo=repoData, user=user).first()
if not user:
flask.abort(401)
post = Post.query.filter_by(identifier=f"/{username}/{repository}/{postID}").first()
if score:
oldRelationship = PostVote.query.filter_by(userUsername=user.username, postIdentifier=post.identifier).first()
if oldRelationship:
if score == oldRelationship.voteScore:
db.session.delete(oldRelationship)
post.voteSum -= oldRelationship.voteScore
else:
post.voteSum -= oldRelationship.voteScore
post.voteSum += score
oldRelationship.voteScore = score
else:
relationship = PostVote(user, post, score)
post.voteSum += score
db.session.add(relationship)
db.session.commit()
userVote = PostVote.query.filter_by(userUsername=user.username, postIdentifier=post.identifier).first()
response = flask.make_response(str(post.voteSum) + " " + str(userVote.voteScore if userVote else 0))
response.content_type = "text/plain"
return response
@app.route("/<username>/<repository>/users/", methods=["GET", "POST"])
def repositoryUsers(username, repository):
if not (getVisibility(username, repository) or getPermissionLevel(flask.session.get("username"), username,
repository) is not None):
flask.abort(403)
serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository))
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
app.logger.info(f"Loading {serverRepoLocation}")
@@ -710,7 +847,7 @@ def repositoryBranches(username, repository):
repository) is not None):
flask.abort(403)
serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository))
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
app.logger.info(f"Loading {serverRepoLocation}")
@@ -730,7 +867,7 @@ def repositoryLog(username, repository):
repository) is not None):
flask.abort(403)
serverRepoLocation = os.path.join(config.REPOS_PATH, os.path.join(username, repository))
serverRepoLocation = os.path.join(config.REPOS_PATH, username, repository)
app.logger.info(f"Loading {serverRepoLocation}")
static/voting.js
@@ -0,0 +1,24 @@
function updateButtons(postID, score) {
document.getElementById(postID + "-voteup").classList.add("button-flat");
document.getElementById(postID + "-votedown").classList.add("button-flat");
if(score == 1) {
document.getElementById(postID + "-voteup").classList.remove("button-flat");
} else if(score == -1) {
document.getElementById(postID + "-votedown").classList.remove("button-flat");
}
}
async function vote(postID, score) {
if(score === 1) {
action = "voteup";
} else if(score === -1) {
action = "votedown";
} else {
action = "votes";
}
const response = await fetch(postID + "/" + action);
const info = await response.text();
document.getElementById(postID + "-vote").innerText = info.split(" ")[0];
updateButtons(postID, info.split(" ")[1])
}
templates/post.html
@@ -19,13 +19,41 @@
<p>
{{ post.message }}
</p>
{% if loggedInUser %}
<x-buttonbox class="box-center">
<button onclick="vote({{ post.number }}, 1);" class="button-flat big-button" id="{{ post.number }}-voteup"><iconify-icon icon="mdi:arrow-up"></iconify-icon></button>
<span style="font-size: 1.5em;" id="{{ post.number }}-vote">{{ post.voteSum }}</span>
<button onclick="vote({{ post.number }}, -1);" class="button-flat big-button" id="{{ post.number }}-votedown"><iconify-icon icon="mdi:arrow-down"></iconify-icon></button>
<script>vote({{ post.number }}, 0);</script>
</x-buttonbox>
{% endif %}
</section>
</article>
</dt>
{% if loggedInUser %}
<dd>
<details>
<summary>Reply</summary>
<form method="POST" action="{{ post.number }}/reply">
<x-vbox>
<x-vbox class="nopad">
<label for="{{ post.number }}-subject">Subject</label>
<input id="{{ post.number }}-subject" name="subject" value="Re: {{ post.subject }}" required>
</x-vbox>
<textarea name="message" style="box-sizing: border-box;" rows="8" required></textarea>
<x-buttonbox>
<button type="submit">Submit</button>
</x-buttonbox>
</x-vbox>
</form>
</details>
</dd>
{% endif %}
{% set level = level + 1 %}
{% if level <= maxPostNesting %}
{% if post.children %}
{% for post in Post.query.filter_by(repo=repoData, parent=parent) %}
{% for post in Post.query.filter_by(parent=post).order_by(Post.date) %}
<dd>{% include "post.html" %}</dd>
{% endfor %}
{% endif %}
templates/repo-forum-thread.html
@@ -1,12 +1,13 @@
{% extends "repo.html" %}
{% set parent = Post.query.filter_by(repo=repoData, number=postID).first() %}
{% block title %}
Forum of {{ username }}/{{ repository }}
{{ parent.subject }} in {{ username }}/{{ repository }}
{% endblock %}
{% block content %}
<script src="/static/voting.js"></script>
<x-vbox>
<x-frame style="--width: 896px;" class="flexible-space">
<x-vbox>
{% set parent = Post.query.filter_by(repo=repoData, number=postID).first() %}
{% set post = parent %}
{% set level = 0 %}
{% include "post.html" %}
templates/repo-forum.html
@@ -6,13 +6,25 @@
<x-vbox>
<x-frame style="--width: 896px;" class="flexible-space">
<x-vbox>
{% for post in Post.query.filter_by(repo=repoData, parent=none) %}
<form method="POST" action="new">
<x-vbox>
<x-vbox class="nopad">
<label for="subject">Subject</label>
<input id="subject" name="subject" required>
</x-vbox>
<textarea name="message" style="box-sizing: border-box;" rows="8" required></textarea>
<x-buttonbox>
<button type="submit">Add new topic</button>
</x-buttonbox>
</x-vbox>
</form>
{% for post in Post.query.filter_by(repo=repoData, parent=none).order_by(Post.lastUpdated.desc()) %}
<article class="card card-horizontal">
<figure class="branch-icon">
<iconify-icon icon="mdi:note"></iconify-icon>
</figure>
<section class="card-main">
<h3><a href="{{ post.number }}">{{ post.subject }}</a></h3>
<h3><a href="{{ post.number }}">{{ post.subject }}</a> ({{ post.children | length }})</h3>
<p><a href="/{{ post.owner.username }}">{{ post.owner.username }}</a></p>
</section>
</article>