Adding admin and report endpoints
This commit is contained in:
parent
af299a53c7
commit
6c128d0567
18 changed files with 335 additions and 6 deletions
|
|
@ -2,12 +2,16 @@
|
|||
|
||||
## 0.8-dev
|
||||
|
||||
* Schema changes: moved `full_name` field from table `userprofile` to table `user` for search improvement reasons.
|
||||
* Added the admin dashboard, accessible from `/admin/` via basic auth. Only users with admin right can access it. Added endpoints `admin.reports` and `admin.reports_detail`.
|
||||
* Safety is our top priority: added the ability to report someone other's post for everything violating the site's Terms of Service. The current reasons for reporting are: spam, impersonation, pornography, violence, harassment or bullying, hate speech or symbols, self injury, sale or promotion of firearms or drugs, and underage use.
|
||||
* Schema changes: moved `full_name` field from table `userprofile` to table `user` for search improvement reasons. Added `Report` model.
|
||||
* Now `profile_search` API endpoint searches by full name too.
|
||||
* Adding `messages_count`, `followers_count` and `following_count` to `profile_info` API endpoint (what I've done to 0.7.1 too).
|
||||
* Adding `create2` API endpoint that accepts media, due to an issue with the `create` endpoint that would make it incompatible.
|
||||
* Adding media URLs to messages in API.
|
||||
* Added `relationships_follow`, `relationships_unfollow`, `username_availability` and `edit_profile` endpoints to API.
|
||||
* Added `url` utility to model `Upload`.
|
||||
* Changed default `robots.txt`, adding report and admin-related lines.
|
||||
|
||||
## 0.7.1-dev
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ To run the app, do "flask run" in the package's parent directory.
|
|||
|
||||
Based on Tweepee example of [peewee](https://github.com/coleifer/peewee/).
|
||||
|
||||
This is the server. For the client, see [coriplusapp](https://github.com/sakuragasaki46/coriplusapp/).
|
||||
|
||||
## Features
|
||||
|
||||
* Create text statuses, optionally with image
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ This module also contains very basic web hooks, such as robots.txt.
|
|||
|
||||
For the website hooks, see `app.website`.
|
||||
For the AJAX hook, see `app.ajax`.
|
||||
For public API, see `app.api`.
|
||||
For report pages, see `app.reports`.
|
||||
For site administration, see `app.admin`.
|
||||
For template filters, see `app.filters`.
|
||||
For the database models, see `app.models`.
|
||||
For other, see `app.utils`.
|
||||
|
|
@ -118,4 +121,8 @@ app.register_blueprint(bp)
|
|||
from .api import bp
|
||||
app.register_blueprint(bp)
|
||||
|
||||
from .reports import bp
|
||||
app.register_blueprint(bp)
|
||||
|
||||
from .admin import bp
|
||||
app.register_blueprint(bp)
|
||||
|
|
|
|||
66
app/admin.py
Normal file
66
app/admin.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
'''
|
||||
Management of reports and the entire site.
|
||||
|
||||
New in 0.8.
|
||||
'''
|
||||
|
||||
from flask import Blueprint, redirect, render_template, request, url_for
|
||||
from .models import User, Message, Report, report_reasons, REPORT_STATUS_ACCEPTED, \
|
||||
REPORT_MEDIA_USER, REPORT_MEDIA_MESSAGE
|
||||
from .utils import pwdhash, object_list
|
||||
from functools import wraps
|
||||
|
||||
bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
|
||||
def check_auth(username, password):
|
||||
try:
|
||||
return User.get((User.username == username) & (User.password == pwdhash(password))
|
||||
).is_admin
|
||||
except User.DoesNotExist:
|
||||
return False
|
||||
|
||||
def admin_required(f):
|
||||
@wraps(f)
|
||||
def wrapped_view(**kwargs):
|
||||
auth = request.authorization
|
||||
if not (auth and check_auth(auth.username, auth.password)):
|
||||
return ('Unauthorized', 401, {
|
||||
'WWW-Authenticate': 'Basic realm="Login Required"'
|
||||
})
|
||||
return f(**kwargs)
|
||||
return wrapped_view
|
||||
|
||||
def review_reports(status, media_type, media_id):
|
||||
(Report
|
||||
.update(status=status)
|
||||
.where((Report.media_type == media_type) & (Report.media_id == media_id))
|
||||
.execute())
|
||||
if status == REPORT_STATUS_ACCEPTED:
|
||||
if media_type == REPORT_MEDIA_USER:
|
||||
user = User[media_id]
|
||||
user.is_disabled = 2
|
||||
user.save()
|
||||
elif media_type == REPORT_MEDIA_MESSAGE:
|
||||
Message.delete().where(Message.id == media_id).execute()
|
||||
|
||||
@bp.route('/')
|
||||
@admin_required
|
||||
def homepage():
|
||||
return render_template('admin_home.html')
|
||||
|
||||
@bp.route('/reports')
|
||||
@admin_required
|
||||
def reports():
|
||||
return object_list('admin_reports.html', Report.select().order_by(Report.created_date.desc()), 'report_list', report_reasons=dict(report_reasons))
|
||||
|
||||
@bp.route('/reports/<int:id>', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def reports_detail(id):
|
||||
report = Report[id]
|
||||
if request.method == 'POST':
|
||||
if request.form.get('take_down'):
|
||||
review_reports(REPORT_STATUS_ACCEPTED, report.media_type, report.media_id)
|
||||
elif request.form.get('discard'):
|
||||
review_reports(REPORT_STATUS_DECLINED, report.media_type, report.media_id)
|
||||
return redirect(url_for('admin.reports'))
|
||||
return render_template('admin_report_detail.html', report=report, report_reasons=dict(report_reasons))
|
||||
|
|
@ -241,14 +241,15 @@ def relationships_unfollow(self, userid):
|
|||
@validate_access
|
||||
def profile_search(self):
|
||||
data = request.get_json(True)
|
||||
query = User.select().where(User.username ** ('%' + data['q'] + '%')).limit(20)
|
||||
query = User.select().where((User.username ** ('%' + data['q'] + '%')) |
|
||||
(User.full_name ** ('%' + data['q'] + '%'))).limit(20)
|
||||
results = []
|
||||
for result in query:
|
||||
profile = result.profile
|
||||
results.append({
|
||||
"id": result.id,
|
||||
"username": result.username,
|
||||
"full_name": profile.full_name,
|
||||
"full_name": result.full_name,
|
||||
"followers_count": len(result.followers())
|
||||
})
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -199,10 +199,60 @@ class Notification(BaseModel):
|
|||
pub_date = DateTimeField()
|
||||
seen = IntegerField(default=0)
|
||||
|
||||
REPORT_MEDIA_USER = 1
|
||||
REPORT_MEDIA_MESSAGE = 2
|
||||
|
||||
REPORT_REASON_SPAM = 1
|
||||
REPORT_REASON_IMPERSONATION = 2
|
||||
REPORT_REASON_PORN = 3
|
||||
REPORT_REASON_VIOLENCE = 4
|
||||
REPORT_REASON_HATE = 5
|
||||
REPORT_REASON_BULLYING = 6
|
||||
REPORT_REASON_SELFINJURY = 7
|
||||
REPORT_REASON_FIREARMS = 8
|
||||
REPORT_REASON_DRUGS = 9
|
||||
REPORT_REASON_UNDERAGE = 10
|
||||
|
||||
report_reasons = [
|
||||
(REPORT_REASON_SPAM, "It's spam"),
|
||||
(REPORT_REASON_IMPERSONATION, "This profile is pretending to be someone else"),
|
||||
(REPORT_REASON_PORN, "Nudity or pornography"),
|
||||
(REPORT_REASON_VIOLENCE, "Violence or dangerous organization"),
|
||||
(REPORT_REASON_HATE, "Hate speech or symbols"),
|
||||
(REPORT_REASON_BULLYING, "Harassment or bullying"),
|
||||
(REPORT_REASON_SELFINJURY, "Self injury"),
|
||||
(REPORT_REASON_FIREARMS, "Sale or promotion of firearms"),
|
||||
(REPORT_REASON_DRUGS, "Sale or promotion of drugs"),
|
||||
(REPORT_REASON_UNDERAGE, "This user is less than 13 years old"),
|
||||
]
|
||||
|
||||
REPORT_STATUS_DELIVERED = 0
|
||||
REPORT_STATUS_ACCEPTED = 1
|
||||
REPORT_STATUS_DECLINED = 2
|
||||
|
||||
# New in 0.8.
|
||||
class Report(BaseModel):
|
||||
media_type = IntegerField()
|
||||
media_id = IntegerField()
|
||||
sender = ForeignKeyField(User, null=True)
|
||||
reason = IntegerField()
|
||||
status = IntegerField(default=REPORT_STATUS_DELIVERED)
|
||||
created_date = DateTimeField()
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
try:
|
||||
if self.media_type == REPORT_MEDIA_USER:
|
||||
return User[self.media_id]
|
||||
elif self.media_type == REPORT_MEDIA_MESSAGE:
|
||||
return Message[self.media_id]
|
||||
except DoesNotExist:
|
||||
return
|
||||
|
||||
def create_tables():
|
||||
with database:
|
||||
database.create_tables([
|
||||
User, UserAdminship, UserProfile, Message, Relationship,
|
||||
Upload, Notification])
|
||||
Upload, Notification, Report])
|
||||
if not os.path.isdir(UPLOAD_DIRECTORY):
|
||||
os.makedirs(UPLOAD_DIRECTORY)
|
||||
|
|
|
|||
42
app/reports.py
Normal file
42
app/reports.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
'''
|
||||
Module for user and message reports.
|
||||
|
||||
New in 0.8.
|
||||
'''
|
||||
|
||||
from flask import Blueprint, redirect, request, render_template, url_for
|
||||
from .models import Report, REPORT_MEDIA_USER, REPORT_MEDIA_MESSAGE, report_reasons
|
||||
from .utils import get_current_user
|
||||
import datetime
|
||||
|
||||
bp = Blueprint('reports', __name__, url_prefix='/report')
|
||||
|
||||
@bp.route('/user/<int:userid>', methods=['GET', 'POST'])
|
||||
def report_user(userid):
|
||||
if request.method == "POST":
|
||||
Report.create(
|
||||
media_type=REPORT_MEDIA_USER,
|
||||
media_id=userid,
|
||||
sender=get_current_user(),
|
||||
reason=request.form['reason'],
|
||||
created_date=datetime.datetime.now()
|
||||
)
|
||||
return redirect(url_for('reports.report_done'))
|
||||
return render_template('report_user.html', report_reasons=report_reasons)
|
||||
|
||||
@bp.route('/message/<int:userid>', methods=['GET', 'POST'])
|
||||
def report_message(userid):
|
||||
if request.method == "POST":
|
||||
Report.create(
|
||||
media_type=REPORT_MEDIA_MESSAGE,
|
||||
media_id=userid,
|
||||
sender=get_current_user(),
|
||||
reason=request.form['reason'],
|
||||
created_date=datetime.datetime.now()
|
||||
)
|
||||
return redirect(url_for('reports.report_done'))
|
||||
return render_template('report_message.html', report_reasons=report_reasons)
|
||||
|
||||
@bp.route('/done', methods=['GET', 'POST'])
|
||||
def report_done():
|
||||
return render_template('report_done.html')
|
||||
28
app/templates/admin_base.html
Normal file
28
app/templates/admin_base.html
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ site_name }}</title>
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css">
|
||||
<style>.done{opacity:.5}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><a href="{{ url_for('admin.homepage') }}">{{ site_name }} Admin</a></h1>
|
||||
<div class="metanav">
|
||||
<!-- what does it go here? -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
{% for message in get_flashed_messages() %}
|
||||
<div class="flash">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p class="copyright">© 2019 Sakuragasaki46.
|
||||
<a href="/about/">About</a> - <a href="/terms/">Terms</a> -
|
||||
<a href="/privacy/">Privacy</a></p>
|
||||
</div>
|
||||
<script src="/static/lib.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
9
app/templates/admin_home.html
Normal file
9
app/templates/admin_home.html
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{{ url_for('admin.reports') }}">Reports</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
27
app/templates/admin_report_detail.html
Normal file
27
app/templates/admin_report_detail.html
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<h2>Report detail #{{ report.id }}</h2>
|
||||
<p>Type: {{ [None, 'user', 'message'][report.media_type] }}</p>
|
||||
<p>Reason: <strong>{{ report_reasons[report.reason] }}</strong></p>
|
||||
<p>Status: <strong>{{ ['Unreviewed', 'Accepted', 'Declined'][report.status] }}</strong></p>
|
||||
|
||||
<h3>Detail</h3>
|
||||
{% if report.media is none %}
|
||||
<p><em>The media is unavailable.</em></p>
|
||||
{% elif report.media_type == 1 %}
|
||||
<p><em>Showing first 20 messages of the reported user.</em></p>
|
||||
<ul>
|
||||
{% for message in report.media.messages %}
|
||||
{% include "includes/reported_message.html" %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% elif report.media_type == 2 %}
|
||||
{% set message = report.media %}
|
||||
{% include "includes/reported_message.html" %}
|
||||
{% endif %}
|
||||
<form method="POST">
|
||||
<input type="submit" name="take_down" value="Take down">
|
||||
<input type="submit" name="discard" value="Discard">
|
||||
</form>
|
||||
{% endblock %}
|
||||
16
app/templates/admin_reports.html
Normal file
16
app/templates/admin_reports.html
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{% extends "admin_base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<ul>
|
||||
{% for report in report_list %}
|
||||
<li {% if report.status > 0 %}class="done"{% endif %}>
|
||||
<p><strong>#{{ report.id }}</strong>
|
||||
(<a href="{{ url_for('admin.reports_detail', id=report.id) }}">detail</a>)</p>
|
||||
<p>Type: {{ [None, 'user', 'message'][report.media_type] }}</p>
|
||||
<p>Reason: <strong>{{ report_reasons[report.reason] }}</strong></p>
|
||||
<p>Status: <strong>{{ ['Unreviewed', 'Accepted', 'Declined'][report.status] }}</strong></p>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% include "includes/pagination.html" %}
|
||||
{% endblock %}
|
||||
|
|
@ -22,6 +22,6 @@
|
|||
<li><a href="/edit/{{ message.id }}">Edit or change privacy</a></li>
|
||||
<li><a href="/delete/{{ message.id }}">Delete permanently</a></li>
|
||||
{% else %}
|
||||
<!--li><a href="/report/{{ message.id }}">Report</a></li-->
|
||||
<li><a href="/report/message/{{ message.id }}" target="_blank">Report</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
|
|
|||
15
app/templates/includes/reported_message.html
Normal file
15
app/templates/includes/reported_message.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<div>
|
||||
<p><strong>Message #{{ message.id }}</strong> (<a href="{# url_for('admin.message_info', id=message.id) #}">detail</a>)</p>
|
||||
<p>Author: <a href="{# url_for('admin.user_detail', id=message.user_id #}">{{ message.user.username }}</a></p>
|
||||
<p>Text:</p>
|
||||
<div style="border:1px solid gray;padding:12px">
|
||||
{{ message.text|enrich }}
|
||||
{% if message.uploads %}
|
||||
<div>
|
||||
<img src="/uploads/{{ message.uploads[0].filename() }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p>Privacy: {{ ['public', 'unlisted', 'friends', 'only me'][message.privacy] }}</p>
|
||||
<p>Date: {{ message.pub_date.strftime('%B %-d, %Y %H:%M:%S') }}</p>
|
||||
</div>
|
||||
27
app/templates/report_base.html
Normal file
27
app/templates/report_base.html
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Report – Cori+</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body{margin:0;font-family:sans-serif}
|
||||
.item{padding:12px;border-bottom:1px solid gray}
|
||||
.item h2{font-size:1em;margin:6px 0}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
<form id="reportForm" method="POST">
|
||||
<input id="reportFormValue" name="reason" type="hidden" value="">
|
||||
<input type="submit" style="display:none">
|
||||
</form>
|
||||
<script>
|
||||
function submitReport(value){
|
||||
reportFormValue.value = value;
|
||||
reportForm.submit();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
11
app/templates/report_done.html
Normal file
11
app/templates/report_done.html
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{% extends "report_base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="item">
|
||||
<h1>Done</h1>
|
||||
|
||||
<p>Your report has been sent.<br />
|
||||
We'll review the user or message, and, if against our Community
|
||||
Guidelines, we'll remove it.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
11
app/templates/report_message.html
Normal file
11
app/templates/report_message.html
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{% extends "report_base.html" %}
|
||||
|
||||
{% block body %}
|
||||
{% for reason in report_reasons %}
|
||||
<form method="POST">
|
||||
<div class="item" onclick="submitReport({{ reason[0] }})">
|
||||
<h2>{{ reason[1] }}</h2>
|
||||
</div>
|
||||
</form>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
11
app/templates/report_user.html
Normal file
11
app/templates/report_user.html
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{% extends "report_base.html" %}
|
||||
|
||||
{% block body %}
|
||||
{% for reason in report_reasons %}
|
||||
<form method="POST">
|
||||
<div class="item" onclick="submitReport({{ reason[0] }})">
|
||||
<h2>{{ reason[1] }}</h2>
|
||||
</div>
|
||||
</form>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
|
@ -1 +1,3 @@
|
|||
|
||||
User-Agent: *
|
||||
Disallow: /report/
|
||||
Noindex: /admin/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue