forked from rDrama/rDrama
hat submission UI
parent
faac45b425
commit
1a64ba3db8
|
@ -13,8 +13,10 @@ class HatDef(Base):
|
|||
description = Column(String)
|
||||
author_id = Column(Integer, ForeignKey('users.id'))
|
||||
price = Column(Integer)
|
||||
submitter_id = Column(Integer, ForeignKey("users.id"))
|
||||
|
||||
author = relationship("User", primaryjoin="HatDef.author_id == User.id", back_populates="designed_hats")
|
||||
submitter = relationship("User", primaryjoin="HatDef.submitter_id == User.id")
|
||||
|
||||
@property
|
||||
@lazy
|
||||
|
|
|
@ -212,7 +212,6 @@ SIDEBAR_THREAD = 0
|
|||
BANNER_THREAD = 0
|
||||
BADGE_THREAD = 0
|
||||
SNAPPY_THREAD = 0
|
||||
HAT_THREAD = 0
|
||||
|
||||
if SITE == 'rdrama.net':
|
||||
FEATURES['PRONOUNS'] = True
|
||||
|
@ -222,7 +221,6 @@ if SITE == 'rdrama.net':
|
|||
BANNER_THREAD = 37697
|
||||
BADGE_THREAD = 37833
|
||||
SNAPPY_THREAD = 37749
|
||||
HAT_THREAD = 100210
|
||||
|
||||
HOLE_COST = 50000
|
||||
HOLE_INACTIVITY_DELETION = True
|
||||
|
@ -941,7 +939,7 @@ if path.isfile(f'snappy_{SITE_NAME}.txt'):
|
|||
|
||||
YOUTUBE_KEY = environ.get("YOUTUBE_KEY", "").strip()
|
||||
|
||||
ADMIGGERS = {SIDEBAR_THREAD, BANNER_THREAD, BADGE_THREAD, SNAPPY_THREAD, HAT_THREAD}
|
||||
ADMIGGERS = {SIDEBAR_THREAD, BANNER_THREAD, BADGE_THREAD, SNAPPY_THREAD}
|
||||
|
||||
proxies = {"http":"http://127.0.0.1:18080","https":"http://127.0.0.1:18080"}
|
||||
|
||||
|
|
|
@ -12,11 +12,12 @@ marseyaward_body_regex = re.compile(">[^<\s+]|[^>\s+]<", flags=re.A)
|
|||
|
||||
marseyaward_title_regex = re.compile("( *<img[^>]+>)+", flags=re.A)
|
||||
|
||||
|
||||
marsey_regex = re.compile("marsey[a-z0-9]{1,24}", flags=re.A)
|
||||
|
||||
hat_regex = re.compile("[a-zA-Z0-9\-() ,_]{1,50}", flags=re.A)
|
||||
|
||||
tags_regex = re.compile("[a-z0-9: ]{1,200}", flags=re.A)
|
||||
hat_regex = re.compile("[a-zA-Z0-9\-() ,_]{1,50}", flags=re.A)
|
||||
description_regex = re.compile("[^<>&\n\t]{1,300}", flags=re.A)
|
||||
|
||||
|
||||
valid_sub_regex = re.compile("^[a-zA-Z0-9_\-]{3,25}$", flags=re.A)
|
||||
|
||||
|
|
|
@ -231,51 +231,6 @@ def comment(v):
|
|||
data=f'{{"files": ["https://{SITE}/assets/images/badges/{badge.id}.webp"]}}', timeout=5)
|
||||
except Exception as e:
|
||||
return {"error": str(e)}, 400
|
||||
elif v.admin_level > 2 and parent_post.id == HAT_THREAD:
|
||||
try:
|
||||
hat = loads(body)
|
||||
|
||||
name = hat["name"]
|
||||
if not hat_regex.fullmatch(name): return {"error": "Invalid name!"}, 400
|
||||
existing = g.db.query(HatDef.name).filter_by(name=name).one_or_none()
|
||||
if existing: return {"error": "A hat with this name already exists!"}, 403
|
||||
|
||||
if "author" in hat: user = get_user(hat["author"])
|
||||
elif "author_id" in hat: user = get_account(hat["author_id"])
|
||||
else: abort(400)
|
||||
|
||||
filename = f'files/assets/images/hats/{name}.webp'
|
||||
copyfile(oldname, filename)
|
||||
process_image(filename, 200)
|
||||
|
||||
hat_def = HatDef(
|
||||
name=name,
|
||||
description=hat["description"],
|
||||
author_id=user.id,
|
||||
price=hat["price"]
|
||||
)
|
||||
g.db.add(hat_def)
|
||||
g.db.flush()
|
||||
|
||||
hat = Hat(
|
||||
user_id=user.id,
|
||||
hat_id=hat_def.id
|
||||
)
|
||||
g.db.add(hat)
|
||||
|
||||
all_by_author = g.db.query(HatDef).filter_by(author_id=user.id).count()
|
||||
|
||||
if all_by_author >= 250:
|
||||
badge_grant(badge_id=166, user=user)
|
||||
elif all_by_author >= 100:
|
||||
badge_grant(badge_id=165, user=user)
|
||||
elif all_by_author >= 50:
|
||||
badge_grant(badge_id=164, user=user)
|
||||
elif all_by_author >= 10:
|
||||
badge_grant(badge_id=163, user=user)
|
||||
|
||||
except Exception as e:
|
||||
return {"error": str(e)}, 400
|
||||
body += f"\n\n![]({image})"
|
||||
elif file.content_type.startswith('video/'):
|
||||
body += f"\n\n{process_video(file)}"
|
||||
|
|
|
@ -441,6 +441,26 @@ def categories_json():
|
|||
return jsonify(data)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@app.get('/asset_submissions/<image>')
|
||||
@limiter.exempt
|
||||
def asset_submissions(image):
|
||||
if not image.endswith('.webp'): abort(404)
|
||||
resp = make_response(send_from_directory('/asset_submissions', image))
|
||||
resp.headers.remove("Cache-Control")
|
||||
resp.headers.add("Cache-Control", "public, max-age=3153600")
|
||||
resp.headers.remove("Content-Type")
|
||||
resp.headers.add("Content-Type", "image/webp")
|
||||
return resp
|
||||
|
||||
@app.get("/submit/marseys")
|
||||
@auth_required
|
||||
def submit_marseys(v):
|
||||
|
@ -526,7 +546,18 @@ def approve_marsey(v, name):
|
|||
if not tags:
|
||||
return {"error": "You need to include tags!"}, 400
|
||||
|
||||
marsey.name = request.values.get('name').lower().strip()
|
||||
new_name = request.values.get('name').lower().strip()
|
||||
if not new_name:
|
||||
return {"error": "You need to include name!"}, 400
|
||||
|
||||
|
||||
if not marsey_regex.fullmatch(new_name):
|
||||
return {"error": "Invalid name!"}, 400
|
||||
if not tags_regex.fullmatch(tags):
|
||||
return {"error": "Invalid tags!"}, 400
|
||||
|
||||
|
||||
marsey.name = new_name
|
||||
marsey.tags = tags
|
||||
g.db.add(marsey)
|
||||
|
||||
|
@ -546,11 +577,13 @@ def approve_marsey(v, name):
|
|||
data=f'{{"files": ["https://{SITE}/e/{marsey.name}.webp"]}}', timeout=5)
|
||||
cache.delete_memoized(marsey_list)
|
||||
|
||||
msg = f'@{v.username} has approved a marsey you submitted: :{marsey.name}:'
|
||||
send_repeatable_notification(marsey.submitter_id, msg)
|
||||
if v.id != marsey.submitter_id:
|
||||
msg = f"@{v.username} has approved a marsey you submitted: :{marsey.name}:"
|
||||
send_repeatable_notification(marsey.submitter_id, msg)
|
||||
|
||||
marsey.submitter_id = None
|
||||
|
||||
return {"message": f"{marsey.name} approved!"}
|
||||
return {"message": f"'{marsey.name}' approved!"}
|
||||
|
||||
@app.post("/admin/reject/marsey/<name>")
|
||||
@admin_level_required(3)
|
||||
|
@ -564,22 +597,158 @@ def reject_marsey(v, name):
|
|||
if not marsey:
|
||||
return {"error": f"This marsey '{name}' doesn't exist!"}, 404
|
||||
|
||||
msg = f"@{v.username} has rejected a marsey you submitted: `'{marsey.name}'`"
|
||||
send_repeatable_notification(marsey.submitter_id, msg)
|
||||
if v.id != marsey.submitter_id:
|
||||
msg = f"@{v.username} has rejected a marsey you submitted: `'{marsey.name}'`"
|
||||
send_repeatable_notification(marsey.submitter_id, msg)
|
||||
|
||||
g.db.delete(marsey)
|
||||
os.remove(f"/asset_submissions/{marsey.name}.webp")
|
||||
|
||||
return {"message": f"{marsey.name} rejected!"}
|
||||
return {"message": f"'{marsey.name}' rejected!"}
|
||||
|
||||
|
||||
@app.get('/asset_submissions/<image>')
|
||||
@limiter.exempt
|
||||
def asset_submissions(image):
|
||||
if not image.endswith('.webp'): abort(404)
|
||||
resp = make_response(send_from_directory('/asset_submissions', image))
|
||||
resp.headers.remove("Cache-Control")
|
||||
resp.headers.add("Cache-Control", "public, max-age=3153600")
|
||||
resp.headers.remove("Content-Type")
|
||||
resp.headers.add("Content-Type", "image/webp")
|
||||
return resp
|
||||
|
||||
|
||||
@app.get("/submit/hats")
|
||||
@auth_required
|
||||
def submit_hats(v):
|
||||
if v.admin_level > 2: hats = g.db.query(HatDef).filter(HatDef.submitter_id != None).all()
|
||||
else: hats = g.db.query(HatDef).filter(HatDef.submitter_id == v.id).all()
|
||||
return render_template("submit_hats.html", v=v, hats=hats)
|
||||
|
||||
|
||||
@app.post("/submit/hats")
|
||||
@auth_required
|
||||
def submit_hat(v):
|
||||
|
||||
def error(error):
|
||||
if v.admin_level > 2: hats = g.db.query(HatDef).filter(HatDef.submitter_id != None).all()
|
||||
else: hats = g.db.query(HatDef).filter(HatDef.submitter_id == v.id).all()
|
||||
return render_template("submit_hats.html", v=v, hats=hats, error=error), 400
|
||||
|
||||
if request.headers.get("cf-ipcountry") == "T1":
|
||||
return error("Image uploads are not allowed through TOR.")
|
||||
|
||||
file = request.files["image"]
|
||||
if not file or not file.content_type.startswith('image/'):
|
||||
return error("You need to submit an image!")
|
||||
|
||||
name = request.values.get('name').lower().strip()
|
||||
if not hat_regex.fullmatch(name):
|
||||
return error("Invalid name!")
|
||||
|
||||
existing = g.db.query(HatDef.name).filter_by(name=name).one_or_none()
|
||||
if existing:
|
||||
return error("A hat with this name already exists!")
|
||||
|
||||
description = request.values.get('description').lower().strip()
|
||||
if not description_regex.fullmatch(description):
|
||||
return error("Invalid description!")
|
||||
|
||||
author = request.values.get('author').strip()
|
||||
author = get_user(author)
|
||||
|
||||
highquality = f'/asset_submissions/{name}.png'
|
||||
file.save(highquality)
|
||||
|
||||
i = Image.open(highquality)
|
||||
if i.width > 100 or i.height > 130:
|
||||
return error("Images must be 100x130")
|
||||
|
||||
filename = f'/asset_submissions/{name}.webp'
|
||||
copyfile(highquality, filename)
|
||||
process_image(filename)
|
||||
|
||||
hat = HatDef(name=name, author_id=author.id, description=description, price=500, submitter_id=v.id)
|
||||
g.db.add(hat)
|
||||
|
||||
g.db.commit()
|
||||
|
||||
if v.admin_level > 2: hats = g.db.query(HatDef).filter(HatDef.submitter_id != None).all()
|
||||
else: hats = g.db.query(HatDef).filter(HatDef.submitter_id == v.id).all()
|
||||
return render_template("submit_hats.html", v=v, hats=hats, msg=f"'{name}' submitted successfully!")
|
||||
|
||||
|
||||
@app.post("/admin/approve/hat/<name>")
|
||||
@admin_level_required(3)
|
||||
def approve_hat(v, name):
|
||||
if CARP_ID and v.id != CARP_ID:
|
||||
return {"error": "Only Carp can approve hats!"}, 403
|
||||
|
||||
name = name.lower().strip()
|
||||
|
||||
hat = g.db.query(HatDef).filter_by(name=name).one_or_none()
|
||||
if not hat:
|
||||
return {"error": f"This hat '{name}' doesn't exist!"}, 404
|
||||
|
||||
description = request.values.get('description').lower().strip()
|
||||
if not description:
|
||||
return {"error": "You need to include description!"}, 400
|
||||
|
||||
new_name = request.values.get('name').lower().strip()
|
||||
if not new_name:
|
||||
return {"error": "You need to include name!"}, 400
|
||||
|
||||
if not hat_regex.fullmatch(new_name):
|
||||
return {"error": "Invalid name!"}, 400
|
||||
|
||||
if not description_regex.fullmatch(description):
|
||||
return {"error": "Invalid description!"}, 400
|
||||
|
||||
hat.name = new_name
|
||||
hat.description = description
|
||||
g.db.add(hat)
|
||||
|
||||
move(f"/asset_submissions/{name}.webp", f"files/assets/images/hats/{hat.name}.webp")
|
||||
|
||||
|
||||
g.db.flush()
|
||||
author = hat.author
|
||||
|
||||
all_by_author = g.db.query(HatDef).filter_by(author_id=author.id).count()
|
||||
|
||||
if all_by_author >= 250:
|
||||
badge_grant(badge_id=166, user=author)
|
||||
elif all_by_author >= 100:
|
||||
badge_grant(badge_id=165, user=author)
|
||||
elif all_by_author >= 50:
|
||||
badge_grant(badge_id=164, user=author)
|
||||
elif all_by_author >= 10:
|
||||
badge_grant(badge_id=163, user=author)
|
||||
|
||||
hat_copy = Hat(
|
||||
user_id=author.id,
|
||||
hat_id=hat.id
|
||||
)
|
||||
g.db.add(hat_copy)
|
||||
|
||||
|
||||
|
||||
if v.id != hat.submitter_id:
|
||||
msg = f"@{v.username} has approved a hat you submitted: '{hat.name}'"
|
||||
send_repeatable_notification(hat.submitter_id, msg)
|
||||
|
||||
hat.submitter_id = None
|
||||
|
||||
return {"message": f"'{hat.name}' approved!"}
|
||||
|
||||
@app.post("/admin/reject/hat/<name>")
|
||||
@admin_level_required(3)
|
||||
def reject_hat(v, name):
|
||||
if CARP_ID and v.id != CARP_ID:
|
||||
return {"error": "Only Carp can reject hats!"}, 403
|
||||
|
||||
name = name.lower().strip()
|
||||
|
||||
hat = g.db.query(HatDef).filter_by(name=name).one_or_none()
|
||||
if not hat:
|
||||
return {"error": f"This hat '{name}' doesn't exist!"}, 404
|
||||
|
||||
if v.id != hat.submitter_id:
|
||||
msg = f"@{v.username} has rejected a hat you submitted: `'{hat.name}'`"
|
||||
send_repeatable_notification(hat.submitter_id, msg)
|
||||
|
||||
g.db.delete(hat)
|
||||
os.remove(f"/asset_submissions/{hat.name}.webp")
|
||||
|
||||
return {"message": f"'{hat.name}' rejected!"}
|
|
@ -52,7 +52,7 @@
|
|||
'Submit Hats',
|
||||
'Submit a Hat to be added.',
|
||||
'fa-hat-cowboy', '#7c603e',
|
||||
'/post/100209',
|
||||
'/submit/hats',
|
||||
)
|
||||
])-%}
|
||||
{%- endif -%}
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
{% extends "default.html" %}
|
||||
|
||||
{% block title %}
|
||||
<title>Submit Hats</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block pagetype %}message{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if error %}
|
||||
<div class="alert alert-danger alert-dismissible fade show mb-3 mt-4" role="alert">
|
||||
<i class="fas fa-exclamation-circle my-auto"></i>
|
||||
<span>
|
||||
{{error}}
|
||||
</span>
|
||||
<button class="close" data-bs-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true"><i class="far fa-times"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if msg %}
|
||||
<div class="alert alert-success alert-dismissible fade show my-3" role="alert">
|
||||
<i class="fas fa-check-circle my-auto" aria-hidden="true"></i>
|
||||
<span>
|
||||
{{msg}}
|
||||
</span>
|
||||
<button class="close" data-bs-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true"><i class="far fa-times"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mx-4">
|
||||
<h2 class="mt-5">Submit Hat</h2>
|
||||
<div class="settings-section rounded">
|
||||
<div class="d-lg-flex">
|
||||
<div class="body w-lg-100">
|
||||
<form action="/submit/hats" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="formkey" value="{{v.formkey}}">
|
||||
|
||||
<div id="image-upload-block">
|
||||
<div><label class="mt-3">Image</label></div>
|
||||
|
||||
<img loading="lazy" id="image-preview" style="max-width:50%">
|
||||
<label class="btn btn-secondary m-0" for="file-upload">
|
||||
<div id="filename-show">Select Image</div>
|
||||
<input autocomplete="off" id="file-upload" accept="image/*" type="file" name="image" {% if request.headers.get('cf-ipcountry')=="T1" %}disabled{% endif %} hidden>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="mt-3" for="name">Name</label>
|
||||
<input autocomplete="off" type="text" id="name" class="form-control" name="name" maxlength="50" pattern='[a-zA-Z0-9\-() ,_]{1,50}' required>
|
||||
|
||||
<label class="mt-3" for="author">Author</label>
|
||||
<input autocomplete="off" type="text" id="author" class="form-control" name="author" value="{{v.username.lower()}}" maxlength="30" pattern='[a-z0-9_\-]{3,30}' required>
|
||||
|
||||
<label class="mt-3" for="description">Description</label>
|
||||
<input autocomplete="off" type="text" id="description" class="form-control" name="description" maxlength="300" pattern='[^<>]{1,300}' required>
|
||||
|
||||
<div class="footer mt-5">
|
||||
<div class="d-flex">
|
||||
<input id="submit-hat" disabled type="submit" onclick="disable(this)" class="btn btn-primary ml-auto" value="Submit Hat">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.onpaste = function(event) {
|
||||
files = event.clipboardData.files
|
||||
|
||||
filename = files[0]
|
||||
|
||||
if (filename)
|
||||
{
|
||||
filename = filename.name.toLowerCase()
|
||||
f=document.getElementById('file-upload');
|
||||
f.files = files;
|
||||
document.getElementById('filename-show').textContent = filename;
|
||||
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png") || filename.endsWith(".gif") || filename.endsWith(".webp"))
|
||||
{
|
||||
var fileReader = new FileReader();
|
||||
fileReader.readAsDataURL(f.files[0]);
|
||||
fileReader.addEventListener("load", function () {document.getElementById('image-preview').setAttribute('src', this.result);});
|
||||
}
|
||||
document.getElementById('file-upload').setAttribute('required', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('file-upload').addEventListener('change', function(){
|
||||
f=document.getElementById('file-upload');
|
||||
document.getElementById('filename-show').textContent = document.getElementById('file-upload').files[0].name.substr(0, 20);
|
||||
filename = f.files[0].name.toLowerCase()
|
||||
if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png") || filename.endsWith(".gif") || filename.endsWith(".webp"))
|
||||
{
|
||||
var fileReader = new FileReader();
|
||||
fileReader.readAsDataURL(f.files[0]);
|
||||
fileReader.addEventListener("load", function () {document.getElementById('image-preview').setAttribute('src', this.result);});
|
||||
document.getElementById('submit-hat').disabled = false;
|
||||
}
|
||||
})
|
||||
|
||||
function approve_hat(t, name) {
|
||||
t.disabled = true;
|
||||
t.classList.add("disabled");
|
||||
post_toast_callback(`/admin/approve/hat/${name}`,
|
||||
{
|
||||
"description": document.getElementById(`${name}-description`).value,
|
||||
"name": document.getElementById(`${name}-name`).value,
|
||||
"price": document.getElementById(`${name}-price`).value,
|
||||
},
|
||||
(xhr) => {
|
||||
if(xhr.status == 200) {
|
||||
document.getElementById(`${name}-hat`).classList.add('d-none')
|
||||
}
|
||||
}
|
||||
);
|
||||
setTimeout(() => {
|
||||
t.disabled = false;
|
||||
t.classList.remove("disabled");
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<h2 class="mt-5 mx-4">Pending Carp Approval</h2>
|
||||
<div class="row mt-5 mx-4">
|
||||
<div class="col px-0">
|
||||
<div class="settings">
|
||||
{% for hat in hats %}
|
||||
<div id="{{hat.name}}-hat" class="settings-section rounded">
|
||||
<div class="d-lg-flex">
|
||||
<div class="body w-lg-100">
|
||||
<input type="hidden" name="formkey" value="{{v.formkey}}">
|
||||
|
||||
<div><label class="mt-3">Image</label></div>
|
||||
<img loading="lazy" src="/asset_submissions/{{hat.name}}.webp" style="max-width:50%">
|
||||
|
||||
<div><label class="mt-3" for="{{hat.name}}-submitter">Submitter</label></div>
|
||||
<input autocomplete="off" type="text" id="{{hat.name}}-submitter" class="form-control" maxlength="30" value="{{hat.submitter.username}}" readonly>
|
||||
|
||||
<label class="mt-3" for="{{hat.name}}-author">Author</label>
|
||||
<input autocomplete="off" type="text" id="{{hat.name}}-author" class="form-control" maxlength="30" value="{{hat.author.username}}" readonly>
|
||||
|
||||
<label class="mt-3" for="{{hat.name}}-name">Name</label>
|
||||
<input autocomplete="off" type="text" id="{{hat.name}}-name" class="form-control" name="name" maxlength="30" value="{{hat.name}}" pattern='hat[a-z0-9]{1,24}' required>
|
||||
|
||||
<label class="mt-3" for="{{hat.name}}-description">Description</label>
|
||||
<input autocomplete="off" type="text" id="{{hat.name}}-description" class="form-control" name="description" maxlength="300" value="{{hat.description}}" pattern='[^<>&\n\t]{1,300}' required>
|
||||
|
||||
<div><label class="mt-3" for="{{hat.name}}-price">Price</label></div>
|
||||
<input autocomplete="off" type="number" id="{{hat.name}}-price" class="form-control" name="price" min="0" value="{{hat.price}}" required>
|
||||
</div>
|
||||
</div>
|
||||
{% if v.admin_level > 2 %}
|
||||
<div class="d-flex my-4 mx-3">
|
||||
<a role="button" class="btn btn-secondary ml-auto mr-2" onclick="post_toast(this,'/admin/reject/hat/{{hat.name}}', true)">Reject</a>
|
||||
<a role="button" class="btn btn-primary mr-0" onclick="approve_hat(this, '{{hat.name}}')">Approve</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -143,23 +143,23 @@
|
|||
<div><label class="mt-3">Image</label></div>
|
||||
<img loading="lazy" src="/asset_submissions/{{marsey.name}}.webp" style="max-width:50%">
|
||||
|
||||
<div><label class="mt-3" for="{{marsey.name}}-name">Name</label></div>
|
||||
<input autocomplete="off" type="text" id="{{marsey.name}}-name" class="form-control" name="name" maxlength="30" value="{{marsey.name}}" pattern='marsey[a-z0-9]{1,24}' required>
|
||||
|
||||
<div><label class="mt-3" for="{{marsey.name}}-submitter">Submitter</label></div>
|
||||
<input autocomplete="off" type="text" id="{{marsey.name}}-submitter" class="form-control" maxlength="30" value="{{marsey.submitter}}" readonly>
|
||||
|
||||
<label class="mt-3" for="{{marsey.name}}-author">Author</label>
|
||||
<input autocomplete="off" type="text" id="{{marsey.name}}-author" class="form-control" maxlength="30" value="{{marsey.author}}" readonly>
|
||||
|
||||
|
||||
<label class="mt-3" for="{{marsey.name}}-name">Name</label>
|
||||
<input autocomplete="off" type="text" id="{{marsey.name}}-name" class="form-control" name="name" maxlength="30" value="{{marsey.name}}" pattern='marsey[a-z0-9]{1,24}' required>
|
||||
|
||||
<label class="mt-3" for="{{marsey.name}}-tags">Tags</label>
|
||||
<input autocomplete="off" type="text" id="{{marsey.name}}-tags" class="form-control" name="tags" maxlength="200" value="{{marsey.tags}}" pattern='[a-z0-9: ]{1,200}' required>
|
||||
|
||||
<label class="mt-3" for="{{marsey.name}}-submitter">Submitter</label>
|
||||
<input autocomplete="off" type="text" id="{{marsey.name}}-submitter" class="form-control" maxlength="30" value="{{marsey.submitter}}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
{% if v.admin_level > 2 %}
|
||||
<div class="d-flex my-4 mx-3">
|
||||
<a role="button" class="btn btn-secondary mr-0" onclick="post_toast(this,'/admin/reject/marsey/{{marsey.name}}', true)">Reject</a>
|
||||
<a role="button" class="btn btn-primary ml-auto mr-2" onclick="approve_marsey(this, '{{marsey.name}}')">Approve</a>
|
||||
<a role="button" class="btn btn-secondary ml-auto mr-2" onclick="post_toast(this,'/admin/reject/marsey/{{marsey.name}}', true)">Reject</a>
|
||||
<a role="button" class="btn btn-primary mr-0" onclick="approve_marsey(this, '{{marsey.name}}')">Approve</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
alter table hat_defs add column submitter_id int;
|
||||
|
||||
ALTER TABLE ONLY public.hat_defs
|
||||
ADD CONSTRAINT hat_def_submitter_fkey FOREIGN KEY (submitter_id) REFERENCES public.users(id);
|
||||
|
||||
CREATE INDEX hat_defs_submitter_id_idx ON public.hat_defs USING btree (submitter_id);
|
Loading…
Reference in New Issue