From 19903cccb541c0e18184989a62cab8e60df98cf6 Mon Sep 17 00:00:00 2001 From: Michael House Date: Mon, 16 May 2022 11:53:24 -0500 Subject: [PATCH] Adding usernotes. --- files/assets/css/TheMotte.css | 154 ++++++++++++++++++ .../assets/js/comments+submission_listing.js | 100 ++++++++++++ files/assets/js/micromodal.js | 1 + files/classes/__init__.py | 1 + files/classes/comment.py | 1 + files/classes/submission.py | 1 + files/classes/user.py | 2 + files/classes/usernotes.py | 66 ++++++++ files/routes/admin.py | 50 ++++++ files/routes/users.py | 10 -- files/templates/comments.html | 8 + files/templates/default.html | 5 + files/templates/submission.html | 6 + files/templates/submission_listing.html | 9 + files/templates/usernote.html | 41 +++++ schema.sql | 39 +++++ 16 files changed, 484 insertions(+), 10 deletions(-) create mode 100644 files/assets/js/micromodal.js create mode 100644 files/classes/usernotes.py create mode 100644 files/templates/usernote.html diff --git a/files/assets/css/TheMotte.css b/files/assets/css/TheMotte.css index 8e2000a55..a24bcf8c2 100644 --- a/files/assets/css/TheMotte.css +++ b/files/assets/css/TheMotte.css @@ -83,3 +83,157 @@ blockquote { #frontpage .post-title a:visited, .visited { color: #7a7a7a !important; } + +.usernote-link { + color: var(--primary); + text-decoration: none; + background-color: transparent; + cursor: pointer; +} + +.usernote-link:hover { + text-decoration: underline; +} + +.modal__overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.6); + display: flex; + justify-content: center; + align-items: center; + z-index:1033 !important; +} + +.modal__container { + background-color: #fff; + padding: 30px; + max-width: 500px; + max-height: 100vh; + border-radius: 4px; + overflow-y: auto; + box-sizing: border-box; +} + +.modal__header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.modal__title { + margin-top: 0; + margin-bottom: 0; + font-weight: 600; + font-size: 1.25rem; + line-height: 1.25; + color: #00449e; + box-sizing: border-box; +} + +.modal__close { + background: transparent; + border: 0; +} + +.modal__header .modal__close:before { content: "\2715"; } + +.modal__content { + margin-top: 2rem; + margin-bottom: 2rem; + line-height: 1.5; + color: rgba(0,0,0,.8); +} + +.modal__content > .content__wrapper { + display: flex; + flex-direction: column; + min-width: 400px; +} + +.modal__content > .content__wrapper > .table_content, +.modal__content > .content__wrapper > .table_content > .table_note, +.modal__content > .content__wrapper > .table_headers { + display: flex; +} +.modal__content > .content__wrapper > .table_content > .table_note { + margin-top: 10px; +} +.modal__content > .content__wrapper > .table_headers > *, +.modal__content > .content__wrapper > .table_content > .table_note > * { + flex-basis: 20%; +} +.modal__content > .content__wrapper > .table_headers > *:nth-child(2), +.modal__content > .content__wrapper > .table_content > .table_note > *:nth-child(2) { + flex-basis: 35%; + flex-grow: 1; +} +.modal__content > .content__wrapper > .table_content .table_note_message { + padding: 5px; + box-sizing: border-box; +} +.modal__content > .content__wrapper > .table_content > .table_note .table_note_date, +.modal__content > .content__wrapper > .table_content > .table_note > .table_note_delete > span { + color: var(--primary); + border: none !important; + text-decoration: none; + background-color: transparent; + cursor: pointer; +} +.modal__content > .content__wrapper > .table_content > .table_note .table_note_date:hover, +.modal__content > .content__wrapper > .table_content > .table_note > .table_note_delete > span:hover { + text-decoration: underline; +} + +.modal__content > form { + display: flex; + flex-direction: column; +} + +.modal__content > .content__wrapper > .table_content { + flex-direction: column; +} + +.modal__content textarea { + resize: vertical !important; + min-height: 50px; + margin-top:20px; +} + +.modal__content select { + display: table-cell; + padding: 5px; + box-sizing: border-box; + margin-top:5px; + vertical-align: middle; +} + +.modal__btn { + font-size: .875rem; + padding-left: 1rem; + padding-right: 1rem; + padding-top: .5rem; + padding-bottom: .5rem; + background-color: #e6e6e6; + color: rgba(0,0,0,.8); + border-radius: .25rem; + border-style: none; + border-width: 0; + cursor: pointer; + -webkit-appearance: button; + text-transform: none; + overflow: visible; + line-height: 1.15; + margin: 0; +} + +.micromodal-slide { + display: none; +} + +.micromodal-slide.is-open { + display: block; +} diff --git a/files/assets/js/comments+submission_listing.js b/files/assets/js/comments+submission_listing.js index 2a2a6e5c1..19da955a7 100644 --- a/files/assets/js/comments+submission_listing.js +++ b/files/assets/js/comments+submission_listing.js @@ -31,6 +31,106 @@ function popclick(author) { ; }, 1); } +function fillnote(user,post,comment) { + + let dialog = document.getElementById("modal-1"); + let table = ""; + + for(let i = 0; i < user.notes.length; ++i){ + let note_id = "note_" + i; + let note = user.notes[i]; + let date = new Date(parseInt(note.created) * 1000); + let date_str = date.toLocaleDateString(); + let time_str = date.toLocaleTimeString(); + + let tag = "None"; + switch(note.tag){ + case 0: tag = "Quality"; break; + case 1: tag = "Good" ; break; + case 2: tag = "Comment"; break; + case 3: tag = "Warning"; break; + case 4: tag = "Tempban"; break; + case 5: tag = "Permban"; break; + case 6: tag = "Spam" ; break; + case 7: tag = "Bot" ; break; + } + + table += "" + + "
" + + "
" + + note.author_name + "
" + + "" + date_str + ", " + time_str + "" + + "
" + + "
" + note.note + "
" + + "
" + tag + "
" + + "
Delete
" + + "
\n" + } + + dialog.getElementsByClassName('notes_target')[0].innerText = user.username; + dialog.getElementsByClassName('table_content')[0].innerHTML = table; + + dialog.dataset.context = JSON.stringify({ + 'url': user.url, + 'post': post, + 'comment': comment, + 'user': user.id, + }); +} + +function delete_note(element,url) { + let note = document.getElementById(element); + let id = note.dataset.id; + + const xhr = new XMLHttpRequest(); + xhr.open("POST", url + "/delete_note/" + id); + xhr.setRequestHeader('xhr', 'xhr'); + xhr.responseType = 'json'; + + xhr.onload = function() { + if(xhr.status === 200) { + console.log(xhr.response); + location.reload(); + } + } + + var form = new FormData() + form.append("formkey", formkey()); + xhr.send(form); +} + +function send_note() { + let dialog = document.getElementById("modal-1"); + let context = JSON.parse(dialog.dataset.context); + + let note = document.querySelector("#modal-1 textarea").value; + let tag = document.querySelector("#modal-1 #usernote_tag").value; + + const xhr = new XMLHttpRequest(); + xhr.open("POST", context.url + "/create_note"); + xhr.setRequestHeader('xhr', 'xhr'); + xhr.responseType = 'json'; + var form = new FormData() + + form.append("formkey", formkey()); + form.append("data", JSON.stringify({ + 'note': note, + 'post': context.post, + 'comment': context.comment, + 'user': context.user, + 'tag': tag + })); + + xhr.onload = function() { + if(xhr.status === 200) { + console.log(xhr.response); + location.reload(); + } + } + + xhr.send(form); +} + document.addEventListener("click", function(){ active = document.activeElement.getAttributeNode("class"); if (active && active.nodeValue == "user-name text-decoration-none"){ diff --git a/files/assets/js/micromodal.js b/files/assets/js/micromodal.js new file mode 100644 index 000000000..09167d26a --- /dev/null +++ b/files/assets/js/micromodal.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).MicroModal=t()}(this,(function(){"use strict";function e(e,t){for(var o=0;oe.length)&&(t=e.length);for(var o=0,n=new Array(t);o0&&this.registerTriggers.apply(this,t(a)),this.onClick=this.onClick.bind(this),this.onKeydown=this.onKeydown.bind(this)}var i,a,r;return i=o,(a=[{key:"registerTriggers",value:function(){for(var e=this,t=arguments.length,o=new Array(t),n=0;n0&&void 0!==arguments[0]?arguments[0]:null;if(this.activeElement=document.activeElement,this.modal.setAttribute("aria-hidden","false"),this.modal.classList.add(this.config.openClass),this.scrollBehaviour("disable"),this.addEventListeners(),this.config.awaitOpenAnimation){var o=function t(){e.modal.removeEventListener("animationend",t,!1),e.setFocusToFirstNode()};this.modal.addEventListener("animationend",o,!1)}else this.setFocusToFirstNode();this.config.onShow(this.modal,this.activeElement,t)}},{key:"closeModal",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,t=this.modal;if(this.modal.setAttribute("aria-hidden","true"),this.removeEventListeners(),this.scrollBehaviour("enable"),this.activeElement&&this.activeElement.focus&&this.activeElement.focus(),this.config.onClose(this.modal,this.activeElement,e),this.config.awaitCloseAnimation){var o=this.config.openClass;this.modal.addEventListener("animationend",(function e(){t.classList.remove(o),t.removeEventListener("animationend",e,!1)}),!1)}else t.classList.remove(this.config.openClass)}},{key:"closeModalById",value:function(e){this.modal=document.getElementById(e),this.modal&&this.closeModal()}},{key:"scrollBehaviour",value:function(e){if(this.config.disableScroll){var t=document.querySelector("body");switch(e){case"enable":Object.assign(t.style,{overflow:""});break;case"disable":Object.assign(t.style,{overflow:"hidden"})}}}},{key:"addEventListeners",value:function(){this.modal.addEventListener("touchstart",this.onClick),this.modal.addEventListener("click",this.onClick),document.addEventListener("keydown",this.onKeydown)}},{key:"removeEventListeners",value:function(){this.modal.removeEventListener("touchstart",this.onClick),this.modal.removeEventListener("click",this.onClick),document.removeEventListener("keydown",this.onKeydown)}},{key:"onClick",value:function(e){(e.target.hasAttribute(this.config.closeTrigger)||e.target.parentNode.hasAttribute(this.config.closeTrigger))&&(e.preventDefault(),e.stopPropagation(),this.closeModal(e))}},{key:"onKeydown",value:function(e){27===e.keyCode&&this.closeModal(e),9===e.keyCode&&this.retainFocus(e)}},{key:"getFocusableNodes",value:function(){var e=this.modal.querySelectorAll(n);return Array.apply(void 0,t(e))}},{key:"setFocusToFirstNode",value:function(){var e=this;if(!this.config.disableFocus){var t=this.getFocusableNodes();if(0!==t.length){var o=t.filter((function(t){return!t.hasAttribute(e.config.closeTrigger)}));o.length>0&&o[0].focus(),0===o.length&&t[0].focus()}}}},{key:"retainFocus",value:function(e){var t=this.getFocusableNodes();if(0!==t.length)if(t=t.filter((function(e){return null!==e.offsetParent})),this.modal.contains(document.activeElement)){var o=t.indexOf(document.activeElement);e.shiftKey&&0===o&&(t[t.length-1].focus(),e.preventDefault()),!e.shiftKey&&t.length>0&&o===t.length-1&&(t[0].focus(),e.preventDefault())}else t[0].focus()}}])&&e(i.prototype,a),r&&e(i,r),o}(),a=null,r=function(e){if(!document.getElementById(e))return console.warn("MicroModal: ❗Seems like you have missed %c'".concat(e,"'"),"background-color: #f8f9fa;color: #50596c;font-weight: bold;","ID somewhere in your code. Refer example below to resolve it."),console.warn("%cExample:","background-color: #f8f9fa;color: #50596c;font-weight: bold;",'')),!1},s=function(e,t){if(function(e){e.length<=0&&(console.warn("MicroModal: ❗Please specify at least one %c'micromodal-trigger'","background-color: #f8f9fa;color: #50596c;font-weight: bold;","data attribute."),console.warn("%cExample:","background-color: #f8f9fa;color: #50596c;font-weight: bold;",''))}(e),!t)return!0;for(var o in t)r(o);return!0},{init:function(e){var o=Object.assign({},{openTrigger:"data-micromodal-trigger"},e),n=t(document.querySelectorAll("[".concat(o.openTrigger,"]"))),r=function(e,t){var o=[];return e.forEach((function(e){var n=e.attributes[t].value;void 0===o[n]&&(o[n]=[]),o[n].push(e)})),o}(n,o.openTrigger);if(!0!==o.debugMode||!1!==s(n,r))for(var l in r){var c=r[l];o.targetModal=l,o.triggers=t(c),a=new i(o)}},show:function(e,t){var o=t||{};o.targetModal=e,!0===o.debugMode&&!1===r(e)||(a&&a.removeEventListeners(),(a=new i(o)).showModal())},close:function(e){e?a.closeModalById(e):a.closeModal()}});return"undefined"!=typeof window&&(window.MicroModal=l),l})); diff --git a/files/classes/__init__.py b/files/classes/__init__.py index 8ce4237ba..37ac578fd 100644 --- a/files/classes/__init__.py +++ b/files/classes/__init__.py @@ -6,6 +6,7 @@ from .domains import * from .flags import * from .user import * from .userblock import * +from .usernotes import * from .submission import * from .votes import * from .domains import * diff --git a/files/classes/comment.py b/files/classes/comment.py index 900c02032..b1f7f93b0 100644 --- a/files/classes/comment.py +++ b/files/classes/comment.py @@ -57,6 +57,7 @@ class Comment(Base): child_comments = relationship("Comment", lazy="dynamic", remote_side=[parent_comment_id], viewonly=True) awards = relationship("AwardRelationship", viewonly=True) reports = relationship("CommentFlag", viewonly=True) + notes = relationship("UserNote", back_populates="comment") def __init__(self, *args, **kwargs): if "created_utc" not in kwargs: diff --git a/files/classes/submission.py b/files/classes/submission.py index adcae93e6..c46aa6683 100644 --- a/files/classes/submission.py +++ b/files/classes/submission.py @@ -59,6 +59,7 @@ class Submission(Base): reports = relationship("Flag", viewonly=True) comments = relationship("Comment", primaryjoin="Comment.parent_submission==Submission.id") subr = relationship("Sub", primaryjoin="foreign(Submission.sub)==remote(Sub.name)", viewonly=True) + notes = relationship("UserNote", back_populates="post") bump_utc = deferred(Column(Integer, server_default=FetchedValue())) diff --git a/files/classes/user.py b/files/classes/user.py index d177cbbd8..607f37693 100644 --- a/files/classes/user.py +++ b/files/classes/user.py @@ -130,6 +130,7 @@ class User(Base): authorizations = relationship("ClientAuth", viewonly=True) awards = relationship("AwardRelationship", primaryjoin="User.id==AwardRelationship.user_id", viewonly=True) referrals = relationship("User", viewonly=True) + notes = relationship("UserNote", foreign_keys='UserNote.reference_user', back_populates="user") def __init__(self, **kwargs): @@ -509,6 +510,7 @@ class User(Base): 'post_count': 0 if self.shadowbanned and not (v and (v.shadowbanned or v.admin_level > 2)) else self.post_count, 'comment_count': 0 if self.shadowbanned and not (v and (v.shadowbanned or v.admin_level > 2)) else self.comment_count, 'badges': [x.path for x in self.badges], + 'notes': [x.json() for x in self.notes] } return data diff --git a/files/classes/usernotes.py b/files/classes/usernotes.py new file mode 100644 index 000000000..f66bfe65a --- /dev/null +++ b/files/classes/usernotes.py @@ -0,0 +1,66 @@ +import time +from flask import * +from sqlalchemy import * +from sqlalchemy.orm import relationship +from files.__main__ import Base +from files.helpers.const import * +from enum import Enum +from sqlalchemy import Enum as EnumType + +class UserTag(Enum): + Quality = 0 + Good = 1 + Comment = 2 + Warning = 3 + Tempban = 4 + Permban = 5 + Spam = 6 + Bot = 7 + +class UserNote(Base): + + __tablename__ = "usernotes" + + id = Column(Integer, primary_key=True) + author_id = Column(Integer, ForeignKey("users.id"), nullable=False) + created_utc = Column(Integer, nullable=False) + reference_user = Column(Integer, ForeignKey("users.id", ondelete='CASCADE'), nullable=False) + reference_comment = Column(Integer, ForeignKey("comments.id", ondelete='SET NULL')) + reference_post = Column(Integer, ForeignKey("submissions.id", ondelete='SET NULL')) + note = Column(String, nullable=False) + tag = Column(EnumType(UserTag), nullable=False) + + author = relationship("User", foreign_keys='UserNote.author_id') + user = relationship("User", foreign_keys='UserNote.reference_user', back_populates="notes") + + comment = relationship("Comment", back_populates="notes") + post = relationship("Submission", back_populates="notes") + + def __init__(self, *args, **kwargs): + if "created_utc" not in kwargs: + kwargs["created_utc"] = int(time.time()) + super().__init__(*args, **kwargs) + + def __repr__(self): + return f"" + + def json(self): + reference = None + + if self.comment: + reference = self.comment.permalink + elif self.post: + reference = self.post.permalink + + data = {'id': self.id, + 'author_name': self.author.username, + 'author_id': self.author.id, + 'user_name': self.user.username, + 'user_id': self.user.id, + 'created': self.created_utc, + 'reference': reference, + 'note': self.note, + 'tag': self.tag.value + } + + return data \ No newline at end of file diff --git a/files/routes/admin.py b/files/routes/admin.py index e8adb197a..f510b1ec2 100644 --- a/files/routes/admin.py +++ b/files/routes/admin.py @@ -217,6 +217,56 @@ def distribute(v, comment): g.db.commit() return {"message": f"Each winner has received {coinsperperson} coins!"} +@app.post("/@/delete_note/") +@admin_level_required(3) +def delete_note(v,username,id): + g.db.query(UserNote).filter_by(id=id).delete() + g.db.commit() + + return make_response(jsonify({ + 'success':True, 'message': 'Note deleted', 'note': id + }), 200) + +@app.post("/@/create_note") +@admin_level_required(3) +def create_note(v,username): + + def result(msg,succ,note): + return make_response(jsonify({ + 'success':succ, 'message': msg, 'note': note + }), 200) + + data = json.loads(request.values.get('data')) + user = g.db.query(User).filter_by(username=username).one_or_none() + + if not user: + return result('User not found',False,None) + + author_id = v.id + reference_user = user.id + reference_comment = data.get('comment',None) + reference_post = data.get('post',None) + note = data['note'] + tag = UserTag(int(data['tag'])) + + if reference_comment: + reference_post = None + elif reference_post: + reference_comment = None + + note = UserNote( + author_id=author_id, + reference_user=reference_user, + reference_comment=reference_comment, + reference_post=reference_post, + note=note, + tag=tag) + + g.db.add(note) + g.db.commit() + + return result('Note saved',True,note.json()) + @app.post("/@/revert_actions") @limiter.limit("1/second;30/minute;200/hour;1000/day") @admin_level_required(3) diff --git a/files/routes/users.py b/files/routes/users.py index 1c65e728e..2bd42a3f1 100644 --- a/files/routes/users.py +++ b/files/routes/users.py @@ -71,16 +71,6 @@ def leaderboard_thread(): gevent.spawn(leaderboard_thread()) - - - - - - - - - - @app.get("/@/upvoters//posts") @auth_required def upvoters_posts(v, username, uid): diff --git a/files/templates/comments.html b/files/templates/comments.html index 23d71d028..6c1cc36a1 100644 --- a/files/templates/comments.html +++ b/files/templates/comments.html @@ -44,6 +44,8 @@ {% endif %} +{% include 'usernote.html' %} + {% macro single_comment(c, level=1) %} {% set ups=c.upvotes %} @@ -201,6 +203,12 @@ {{c.print()}} {% endif %} {{c.author_name}} + {% if v and v.admin_level > 2 %} + _U_ + {% endif %} {% if c.author.customtitle %}  {{c.author.customtitle | safe}}{% endif %} {% endif %} diff --git a/files/templates/default.html b/files/templates/default.html index 2306f63d0..19f5476cf 100644 --- a/files/templates/default.html +++ b/files/templates/default.html @@ -5,6 +5,7 @@ + {% if v %} @@ -327,6 +328,10 @@ + + diff --git a/files/templates/submission.html b/files/templates/submission.html index e413063d3..3fd0c75c2 100644 --- a/files/templates/submission.html +++ b/files/templates/submission.html @@ -468,6 +468,12 @@ {% if p.author.verified %} {% endif %} {{p.author_name}}{% if p.author.customtitle %}  {{p.author.customtitle | safe}}{% endif %} + {% if v and v.admin_level > 2 %} + _U_ + {% endif %} {% endif %}  {{p.age_string}} ({% if p.is_image %}image post{% elif p.is_video %}video post{% elif p.domain %}{{p.domain}}{% else %}text post{% endif %}) diff --git a/files/templates/submission_listing.html b/files/templates/submission_listing.html index c319ec1fc..13c4af51c 100644 --- a/files/templates/submission_listing.html +++ b/files/templates/submission_listing.html @@ -4,6 +4,7 @@ {% endif %} + +{% include 'usernote.html' %} + {% for p in listing %} {% set ups=p.upvotes %} @@ -182,6 +185,12 @@ {% if p.author.verified %} {% endif %} {{p.author_name}}{% if p.author.customtitle %}  {{p.author.customtitle | safe}}{% endif %} + {% if v and v.admin_level > 2 %} + _U_ + {% endif %} {% endif %}  {{p.age_string}}   diff --git a/files/templates/usernote.html b/files/templates/usernote.html new file mode 100644 index 000000000..7d24cf433 --- /dev/null +++ b/files/templates/usernote.html @@ -0,0 +1,41 @@ + \ No newline at end of file diff --git a/schema.sql b/schema.sql index 7de470f52..d0b800b2b 100644 --- a/schema.sql +++ b/schema.sql @@ -248,6 +248,39 @@ CREATE TABLE public.commentflags ( created_utc integer NOT NULL ); +-- +-- Name: usernotes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.usernotes ( + id integer NOT NULL, + author_id integer NOT NULL, + created_utc integer NOT NULL, + reference_user integer NOT NULL, + reference_comment integer, + reference_post integer, + note character varying(10000), + tag character varying(10) +); + +-- +-- Name: usernotes_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.usernotes_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: usernotes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.usernotes_id_seq OWNED BY public.usernotes.id; -- -- Name: comments; Type: TABLE; Schema: public; Owner: - @@ -691,6 +724,12 @@ ALTER TABLE ONLY public.award_relationships ALTER COLUMN id SET DEFAULT nextval( ALTER TABLE ONLY public.badge_defs ALTER COLUMN id SET DEFAULT nextval('public.badge_defs_id_seq'::regclass); +-- +-- Name: usernotes id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.usernotes ALTER COLUMN id SET DEFAULT nextval('public.usernotes_id_seq'::regclass); + -- -- Name: comments id; Type: DEFAULT; Schema: public; Owner: - --