diff --git a/files/assets/css/main.css b/files/assets/css/main.css index 806619bf1..094a57e69 100644 --- a/files/assets/css/main.css +++ b/files/assets/css/main.css @@ -4582,6 +4582,13 @@ div.deleted.banned { .patron[style="background-color:#FFFFFF;"] { color: black !important; } +.post--category-tag { + padding: 2px 5px 3px 5px; + border-radius: 5px; + font-size: 12px; + font-weight: 700; + margin-right: 0.25rem; +} .container, .container-fluid { background-color: var(--background) !important; } @@ -5686,6 +5693,19 @@ g { border-radius:.35rem; } +.category--tag-button { + display: inline-block; + cursor: pointer; +} + +#submit-categories input { + display: none; +} + +#submit-categories input:checked + label { + border: 5px var(--black) double; +} + /* ------- Font Awesome ------- */ @font-face{ font-family:"Font Awesome 6 Pro"; @@ -6038,6 +6058,7 @@ g { .fa-circle-info:before{content:"\f05a"} .fa-comment-question:before{content:"\e14b"} .fa-sitemap:before{content:"\f0e8"} +.fa-grid:before{content:"\e195"} .pronouns { font-size: 9px; diff --git a/files/assets/js/category_modal.js b/files/assets/js/category_modal.js new file mode 100644 index 000000000..5f353cecc --- /dev/null +++ b/files/assets/js/category_modal.js @@ -0,0 +1,41 @@ +function category_modal(id, title, sub) { + document.getElementById("category-modal-title").innerHTML = `Category: ${title}`; + + xhrCategories = new XMLHttpRequest(); + xhrCategories.open("GET", "/categories.json"); + xhrCategories.onload = function () { + let data; + try { + data = JSON.parse(xhrCategories.response); + } catch(e) { console.log(e) } + + categories = [{id: '', name: 'None', sub: sub, color_text: '#000', color_bg: '#FFF'}]; + categories = [].concat(categories, data[sub]); + + document.getElementById("category-modal-body").innerHTML = ''; + categories.forEach(function (c) { + document.getElementById("category-modal-body").innerHTML += + `
` + + `${c.name}` + + `
`; + }); + + document.querySelectorAll('.category--tag-button').forEach(tag => + tag.addEventListener('click', function (e) { + reqBody = new FormData(); + reqBody.append('formkey', formkey()); + reqBody.append('post_id', id); + reqBody.append('category_id', tag.dataset.category); + + xhrSubmit = new XMLHttpRequest(); + xhrSubmit.open('POST', `/post_recategorize`); + xhrSubmit.onload = function () { + window.location.reload(); + } + xhrSubmit.send(reqBody); + }) + ); + } + xhrCategories.send(); +} diff --git a/files/assets/js/submit.js b/files/assets/js/submit.js index e9196bd8a..63982e9ba 100644 --- a/files/assets/js/submit.js +++ b/files/assets/js/submit.js @@ -169,6 +169,31 @@ function checkRepost() { } } +function updateCategories() { + if (document.getElementById("submit-categories") == null) { + return; + } + sub = document.getElementById("sub").value; + + xhrCategories = new XMLHttpRequest(); + xhrCategories.open("GET", "/categories.json"); + xhrCategories.onload = function () { + let data; + try { + data = JSON.parse(xhrCategories.response); + } catch(e) { console.log(e) } + + document.getElementById("submit-categories").innerHTML = ''; + data[sub].forEach(function (c) { + document.getElementById("submit-categories").innerHTML += + `` + + ``; + }); + } + xhrCategories.send(); +} document.addEventListener('keydown', (e) => { if(!((e.ctrlKey || e.metaKey) && e.key === "Enter")) @@ -179,4 +204,5 @@ document.addEventListener('keydown', (e) => { submitButton.click(); }); -checkRepost() \ No newline at end of file +checkRepost(); +updateCategories(); diff --git a/files/classes/__init__.py b/files/classes/__init__.py index 6723e92a2..1fd0f795c 100644 --- a/files/classes/__init__.py +++ b/files/classes/__init__.py @@ -8,6 +8,7 @@ from .user import * from .userblock import * from .submission import * from .votes import * +from .category import * from .domains import * from .subscriptions import * from files.__main__ import app @@ -20,4 +21,4 @@ from .saves import * from .views import * from .notifications import * from .follows import * -from .lottery import * \ No newline at end of file +from .lottery import * diff --git a/files/classes/category.py b/files/classes/category.py new file mode 100644 index 000000000..655e711d6 --- /dev/null +++ b/files/classes/category.py @@ -0,0 +1,22 @@ +from sqlalchemy import * +from sqlalchemy.orm import relationship +from files.__main__ import Base + +class Category(Base): + __tablename__ = "category" + + id = Column(Integer, primary_key=True, nullable=False) + name = Column(String(128), nullable=False) + sub = Column(String(20), ForeignKey("subs.name")) + color_text = Column(String(6)) + color_bg = Column(String(6)) + + def as_json(self): + data = { + 'id': self.id, + 'name': self.name, + 'sub': self.sub if self.sub else '', + 'color_text': '#' + self.color_text, + 'color_bg': '#' + self.color_bg, + } + return data diff --git a/files/classes/mod_logs.py b/files/classes/mod_logs.py index 4ac311a8c..a6aef3dfe 100644 --- a/files/classes/mod_logs.py +++ b/files/classes/mod_logs.py @@ -303,6 +303,11 @@ ACTIONTYPES = { "icon": 'fa-thumbtack fa-rotate--45', "color": 'bg-success' }, + 'post_recategorize': { + "str": 'changed category of {self.target_link}', + "icon": 'fa-grid', + "color": 'bg-primary' + }, 'purge_cache': { "str": 'purged cache', "icon": 'fa-memory', diff --git a/files/classes/submission.py b/files/classes/submission.py index 786096345..b0a493457 100644 --- a/files/classes/submission.py +++ b/files/classes/submission.py @@ -53,6 +53,7 @@ class Submission(Base): body = Column(String) body_html = Column(String) flair = Column(String) + category_id = Column(Integer, ForeignKey("category.id")) ban_reason = Column(String) embed_url = Column(String) new = Column(Boolean) @@ -65,6 +66,7 @@ class Submission(Base): comments = relationship("Comment", primaryjoin="Comment.parent_submission==Submission.id", back_populates="post") subr = relationship("Sub", primaryjoin="foreign(Submission.sub)==remote(Sub.name)") options = relationship("SubmissionOption", order_by="SubmissionOption.id") + category = relationship("Category", primaryjoin="Submission.category_id==Category.id") bump_utc = deferred(Column(Integer, server_default=FetchedValue())) diff --git a/files/helpers/const.py b/files/helpers/const.py index f5599610e..b916da659 100644 --- a/files/helpers/const.py +++ b/files/helpers/const.py @@ -132,6 +132,8 @@ AGENDAPOSTER_MSG_HTML = """

Hi ") +@admin_level_required(PERMS['ADMIN_CATEGORIES_MANAGE']) +def admin_categories_update(v, cid): + if not FEATURES['CATEGORIES']: + abort(404) + + cat_name = request.values.get("name").strip() + cat_color_text = request.values.get("color_text").strip().strip('#').lower() + cat_color_bg = request.values.get("color_bg").strip().strip('#').lower() + + try: + cat_id = int(cid) + except: + abort(400) + + cat = g.db.query(Category).filter(Category.id == cat_id).one_or_none() + if not cat: + abort(400) + + cat.name = cat_name + cat.color_text = cat_color_text + cat.color_bg = cat_color_bg + + g.db.add(cat) + g.db.commit() + + return redirect("/admin/categories") + +@app.post("/admin/categories/delete/") +@admin_level_required(PERMS['ADMIN_CATEGORIES_MANAGE']) +def admin_categories_delete(v, cid): + if not FEATURES['CATEGORIES']: + abort(404) + + try: + cat_id = int(cid) + except: + abort(400) + + cat = g.db.query(Category).filter(Category.id == cat_id).one_or_none() + g.db.delete(cat) + g.db.commit() + + return redirect("/admin/categories") @app.post("/admin/nuke_user") @limiter.limit("1/second;30/minute;200/hour;1000/day") diff --git a/files/routes/posts.py b/files/routes/posts.py index 13e81a384..938935964 100644 --- a/files/routes/posts.py +++ b/files/routes/posts.py @@ -697,6 +697,14 @@ def submit_post(v, sub=None): if not sub and HOLE_REQUIRED: return error(f"You must choose a {HOLE_NAME} for your post!") + category = None + if FEATURES['CATEGORIES']: + category_id = request.values.get('category', '') + try: + category = int(category_id) + except: + category = None + if v.is_suspended: return error("You can't perform this action while banned.") if v.agendaposter and not v.marseyawarded: title = torture_ap(title, v.username) @@ -912,6 +920,7 @@ def submit_post(v, sub=None): title=title[:500], title_html=title_html, sub=sub, + category_id=category, ghost=ghost ) @@ -1207,6 +1216,42 @@ def toggle_post_nsfw(pid, v): if post.over_18: return {"message": "Post has been marked as +18!"} else: return {"message": "Post has been unmarked as +18!"} +@app.post("/post_recategorize") +@auth_required +def post_recategorize(v): + if not FEATURES['CATEGORIES']: + abort(404) + if v.admin_level < PERMS['ADMIN_CATEGORIES_CHANGE']: + abort(403) + + post_id = request.values.get("post_id") + category_id = request.values.get("category_id") + try: + pid = int(post_id) + cid = None + if category_id != '': + cid = int(category_id) + except: + abort(400) + + post = g.db.get(Submission, pid) + post.category_id = cid + g.db.add(post) + + category_new_name = '<none>' + if category_id != '': + category_new_name = g.db.get(Category, cid).name + ma = ModAction( + kind='post_recategorize', + user_id=v.id, + target_submission_id=post.id, + _note=category_new_name + ) + g.db.add(ma) + + g.db.commit() + return {"message": "Success!"} + @app.post("/save_post/") @limiter.limit("1/second;30/minute;200/hour;1000/day") @limiter.limit("1/second;30/minute;200/hour;1000/day", key_func=lambda:f'{SITE}-{session.get("lo_user")}') diff --git a/files/routes/static.py b/files/routes/static.py index 244f1ef5e..d04f63dad 100644 --- a/files/routes/static.py +++ b/files/routes/static.py @@ -419,3 +419,15 @@ def knowledgebase(v, page): abort(404) return render_template(template_path, v=v) + +@app.get("/categories.json") +def categories_json(): + categories = g.db.query(Category).all() + + data = {} + for c in categories: + sub = c.sub if c.sub else '' + sub_cats = (data[sub] if sub in data else []) + [c.as_json()] + data.update({sub: sub_cats}) + + return jsonify(data) diff --git a/files/templates/admin/admin_home.html b/files/templates/admin/admin_home.html index ba418c031..3bee53266 100644 --- a/files/templates/admin/admin_home.html +++ b/files/templates/admin/admin_home.html @@ -53,11 +53,6 @@ {%- endif %} -

API Access Control

- - {% if LOTTERY_ENABLED -%}

Lottery