Skip to content

Commit

Permalink
Let admins control ordering of featured mods
Browse files Browse the repository at this point in the history
  • Loading branch information
HebaruSan committed Jan 3, 2024
1 parent 161a74e commit cef74e2
Show file tree
Hide file tree
Showing 12 changed files with 228 additions and 31 deletions.
20 changes: 11 additions & 9 deletions KerbalStuff/blueprints/anonymous.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,11 @@ def browse_top() -> str:

@anonymous.route("/browse/featured")
def browse_featured() -> str:
mods = Featured.query.order_by(Featured.created.desc())
mods, page, total_pages = paginate_query(mods)
mods = [f.mod for f in mods]
return render_template("browse-list.html", mods=mods, page=page, total_pages=total_pages,
featured = Featured.query.order_by(Featured.priority.desc())
featured, page, total_pages = paginate_query(featured)
mods = [f.mod for f in featured]
return render_template("browse-list.html", mods=mods, featured=featured,
page=page, total_pages=total_pages,
url="/browse/featured", name="Featured Mods", rss="/browse/featured.rss")


Expand Down Expand Up @@ -250,11 +251,12 @@ def singlegame_browse_top(gameshort: str) -> str:
@anonymous.route("/<gameshort>/browse/featured")
def singlegame_browse_featured(gameshort: str) -> str:
ga = get_game_info(short=gameshort)
mods = Featured.query.outerjoin(Mod).filter(
Mod.game_id == ga.id).order_by(Featured.created.desc())
mods, page, total_pages = paginate_query(mods)
mods = [f.mod for f in mods]
return render_template("browse-list.html", mods=mods, page=page, total_pages=total_pages, ga=ga,
featured = Featured.query.outerjoin(Mod)\
.filter(Mod.game_id == ga.id)\
.order_by(Featured.priority.desc())
featured, page, total_pages = paginate_query(featured)
mods = [f.mod for f in featured]
return render_template("browse-list.html", mods=mods, featured=featured, page=page, total_pages=total_pages, ga=ga,
url="/browse/featured", name="Featured Mods", rss="/browse/featured.rss")


Expand Down
2 changes: 1 addition & 1 deletion KerbalStuff/blueprints/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ def browse_top() -> Iterable[Dict[str, Any]]:
@api.route("/api/browse/featured")
@json_output
def browse_featured() -> Iterable[Dict[str, Any]]:
mods = Featured.query.order_by(Featured.created.desc())
mods = Featured.query.order_by(Featured.priority.desc())
mods, page, total_pages = paginate_query(mods)
return serialize_mod_list((f.mod for f in mods))

Expand Down
61 changes: 57 additions & 4 deletions KerbalStuff/blueprints/mods.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from shutil import rmtree
from typing import Any, Dict, Tuple, Optional, Union

from sqlalchemy import func
import werkzeug.wrappers
import user_agents

Expand Down Expand Up @@ -477,9 +478,12 @@ def feature(mod_id: int) -> Dict[str, Any]:
mod, game = _get_mod_game_info(mod_id)
if any(Featured.query.filter(Featured.mod_id == mod_id).all()):
abort(409)
featured = Featured()
featured.mod = mod
db.add(featured)
max_prio = db.query(func.max(Featured.priority))\
.outerjoin(Mod)\
.filter(Mod.game_id == game.id)\
.scalar()
db.add(Featured(mod=mod,
priority=max_prio + 1 if max_prio is not None else 0))
return {"success": True}


Expand All @@ -488,14 +492,63 @@ def feature(mod_id: int) -> Dict[str, Any]:
@json_output
@with_session
def unfeature(mod_id: int) -> Dict[str, Any]:
_get_mod_game_info(mod_id)
_, game = _get_mod_game_info(mod_id)
featured = Featured.query.filter(Featured.mod_id == mod_id).first()
if not featured:
abort(404)
for other in Featured.query.outerjoin(Mod)\
.filter(Mod.game_id == game.id,
Featured.priority > featured.priority)\
.all():
other.priority -= 1
db.delete(featured)
return {"success": True}


@mods.route('/mod/<int:mod_id>/feature-down', methods=['POST'])
@adminrequired
@json_output
@with_session
def feature_down(mod_id: int) -> Dict[str, Any]:
_, game = _get_mod_game_info(mod_id)
featured = Featured.query.filter(Featured.mod_id == mod_id).first()
if not featured:
abort(404)
if featured.priority > 0:
other = Featured.query.outerjoin(Mod)\
.filter(Mod.game_id == game.id,
Featured.priority == featured.priority - 1)\
.first()
featured.priority -= 1
if other:
other.priority += 1
return {"success": True}


@mods.route('/mod/<int:mod_id>/feature-up', methods=['POST'])
@adminrequired
@json_output
@with_session
def feature_up(mod_id: int) -> Dict[str, Any]:
_, game = _get_mod_game_info(mod_id)
featured = Featured.query.filter(Featured.mod_id == mod_id).first()
if not featured:
abort(404)
max_prio = db.query(func.max(Featured.priority))\
.outerjoin(Mod)\
.filter(Mod.game_id == game.id)\
.scalar()
if featured.priority < max_prio:
other = Featured.query.outerjoin(Mod)\
.filter(Mod.game_id == game.id,
Featured.priority == featured.priority + 1)\
.first()
featured.priority += 1
if other:
other.priority -= 1
return {"success": True}


@mods.route('/mod/<int:mod_id>/<path:mod_name>/publish')
@with_session
@loginrequired
Expand Down
2 changes: 1 addition & 1 deletion KerbalStuff/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def get_paginated_mods(ga: Optional[Game] = None, query: str = '', page_size: in


def get_featured_mods(game_id: Optional[int], limit: int) -> List[Mod]:
mods = Featured.query.outerjoin(Mod).filter(Mod.published).order_by(Featured.created.desc())
mods = Featured.query.outerjoin(Mod).filter(Mod.published).order_by(Featured.priority.desc())
if game_id:
mods = mods.filter(Mod.game_id == game_id)
return mods.limit(limit).all()
Expand Down
4 changes: 3 additions & 1 deletion KerbalStuff/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ class Following(Base): # type: ignore
send_autoupdate = Column(Boolean(), default=True, nullable=False)

def __init__(self, mod: Optional['Mod'] = None, user: Optional['User'] = None,
send_update: Optional[bool] = True, send_autoupdate: Optional[bool] = True) -> None:
send_update: Optional[bool] = True,
send_autoupdate: Optional[bool] = True) -> None:
self.mod = mod
self.user = user
self.send_update = send_update
Expand All @@ -40,6 +41,7 @@ class Featured(Base): # type: ignore
mod_id = Column(Integer, ForeignKey('mod.id', ondelete='CASCADE'))
mod = relationship('Mod', backref=backref('featured', passive_deletes=True, order_by=id))
created = Column(DateTime, default=datetime.now, index=True)
priority = Column(Integer, nullable=False, index=True)

def __repr__(self) -> str:
return '<Featured %r>' % self.id
Expand Down
63 changes: 63 additions & 0 deletions alembic/versions/2024_01_02_14_56_29-f5a5d29ec765.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Add featured.priority
Revision ID: f5a5d29ec765
Revises: ba0c9afb6cb0
Create Date: 2024-01-02 20:57:00.647417
"""

# revision identifiers, used by Alembic.
revision = 'f5a5d29ec765'
down_revision = 'ba0c9afb6cb0'

from datetime import datetime
from alembic import op
import sqlalchemy as sa
from sqlalchemy.orm import relationship, backref

Base = sa.orm.declarative_base()

class Featured(Base): # type: ignore
__tablename__ = 'featured'
id = sa.Column(sa.Integer, primary_key=True)
mod_id = sa.Column(sa.Integer, sa.ForeignKey('mod.id', ondelete='CASCADE'))
mod = relationship('Mod', backref=backref('featured', passive_deletes=True, order_by=id))
created = sa.Column(sa.DateTime, default=datetime.now, index=True)
priority = sa.Column(sa.Integer, nullable=False, index=True)


class Mod(Base): # type: ignore
__tablename__ = 'mod'
id = sa.Column(sa.Integer, primary_key=True)
game_id = sa.Column(sa.Integer, sa.ForeignKey('game.id', ondelete='CASCADE'))
game = relationship('Game', backref=backref('mods', passive_deletes=True))


class Game(Base): # type: ignore
__tablename__ = 'game'
id = sa.Column(sa.Integer, primary_key=True)


def upgrade() -> None:
op.add_column('featured', sa.Column('priority', sa.Integer(), nullable=True))
op.create_index('ix_featured_priority', 'featured', [sa.text('priority DESC')], unique=False)

bind = op.get_bind()
session = sa.orm.Session(bind=bind)
prio = 0
game_id = None
for feature in session.query(Featured)\
.outerjoin(Mod)\
.order_by(Mod.game_id, Featured.created)\
.all():
prio = prio + 1 if game_id == feature.mod.game_id else 0
game_id = feature.mod.game_id
feature.priority = prio
session.commit()

op.alter_column('featured', 'priority', nullable=False)


def downgrade() -> None:
op.drop_index('ix_featured_priority', table_name='featured')
op.drop_column('featured', 'priority')
53 changes: 45 additions & 8 deletions frontend/coffee/global.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,56 @@ link.addEventListener('click', (e) ->
xhr = new XMLHttpRequest()
if e.target.classList.contains('feature-button')
xhr.open('POST', "/mod/#{e.target.dataset.mod}/feature")
xhr.setRequestHeader('Accept', 'application/json')
e.target.classList.remove('feature-button')
e.target.classList.add('unfeature-button')
e.target.textContent = 'Unfeature this mod'
else
xhr.open('POST', "/mod/#{e.target.dataset.mod}/unfeature")
xhr.setRequestHeader('Accept', 'application/json')
e.target.classList.remove('unfeature-button')
e.target.classList.add('feature-button')
e.target.textContent = 'Feature this mod'
xhr.setRequestHeader('Accept', 'application/json')
xhr.onload = () ->
response = JSON.parse this.responseText
if response.success
if e.target.classList.contains('feature-button')
e.target.classList.remove('feature-button')
e.target.classList.add('unfeature-button')
e.target.textContent = 'Unfeature this mod'
else
e.target.classList.remove('unfeature-button')
e.target.classList.add('feature-button')
e.target.textContent = 'Feature this mod'
else
$('#alert-error-text').text response.reason
$('#alert-error').removeClass 'hidden'
xhr.send()
, false) for link in document.querySelectorAll('.feature-button, .unfeature-button')

link.addEventListener('click', (e) ->
xhr = new XMLHttpRequest()
mod_id = e.target.dataset.mod
xhr.open 'POST', "/mod/#{mod_id}/feature-down"
xhr.setRequestHeader 'Accept', 'application/json'
xhr.onload = () ->
response = JSON.parse this.responseText
if response.success
window.location.reload()
else
$('#alert-error-text').text response.reason
$('#alert-error').removeClass 'hidden'
xhr.send()
, false) for link in document.querySelectorAll('.lower-feature-priority-button')

link.addEventListener('click', (e) ->
xhr = new XMLHttpRequest()
mod_id = e.target.dataset.mod
xhr.open 'POST', "/mod/#{mod_id}/feature-up"
xhr.setRequestHeader 'Accept', 'application/json'
xhr.onload = () ->
response = JSON.parse this.responseText
if response.success
window.location.reload()
else
$('#alert-error-text').text response.reason
$('#alert-error').removeClass 'hidden'
xhr.send()
, false) for link in document.querySelectorAll('.raise-feature-priority-button')

readCookie = (name) ->
nameEQ = name + "="
ca = document.cookie.split(';')
Expand Down
4 changes: 3 additions & 1 deletion frontend/styles/listing.scss
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@
border-radius: 0;
}

.follow-mod-button, .unfollow-mod-button, .download-link, .following-mod-indicator, .locked-mod-indicator {
.follow-mod-button, .unfollow-mod-button, .following-mod-indicator,
.download-link, .locked-mod-indicator,
.lower-feature-priority-button, .raise-feature-priority-button {
text-decoration: none;
font-size: 20px;
margin-top: -2px;
Expand Down
23 changes: 22 additions & 1 deletion generate_revision.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
#!/usr/bin/env bash
set -e

case "$OSTYPE" in
linux*)
;;
msys | cygwin)
# Turn off db's volume mounting because we can't make the owners match,
# by mounting it at /var/lib/postgresql/data_dummy instead.
# The db will not persist between restarts, but that's better than
# failing to start and can still be used to investigate some issues.
echo 'Windows OS detected, disabling db volume mounting.'
echo 'NOTE: Database will NOT persist across restarts!'
echo
export DISABLE_DB_VOLUME=_dummy
;;
esac

docker-compose build backend

docker-compose up -d db
Expand All @@ -16,4 +31,10 @@ chmod g+rw alembic/versions/* &&
alembic history --verbose
"""

sudo chown "${USER}":"${USER}" alembic/versions/*
case "$OSTYPE" in
linux*)
sudo chown "${USER}":"${USER}" alembic/versions/*
;;
msys | cygwin)
;;
esac
13 changes: 10 additions & 3 deletions templates/browse-list.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,16 @@ <h3>{{ name }}</h3>
<p>Nothing to see here. If you're looking for a specific mod, why not ask the modder to upload it here?</p>
{% endif %}
<div class="row">
{% for mod in mods %}
{% include "mod-box.html" %}
{% endfor %}
{% if featured %}
{% for feature in featured %}
{% set mod = feature.mod %}
{% include "mod-box.html" %}
{% endfor %}
{% else %}
{% for mod in mods %}
{% include "mod-box.html" %}
{% endfor %}
{% endif %}
</div>
{%- if total_pages > 1 -%}
<div style="margin-top: 5mm" class="row vertical-centered" style="margin-bottom:2.5mm;">
Expand Down
6 changes: 4 additions & 2 deletions templates/game.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ <h3>Followed Mods <small>Recently updated mods you follow</small></h3>
</div>
{% endif %}

{% if featured %}
<div class="well" style="margin-bottom: 0;">
<div class="container main-cat">
<a href="{% if ga %}/{{ ga.short }}{% endif %}/browse/featured" class="btn btn-primary pull-right">
Expand All @@ -60,11 +61,12 @@ <h3>Featured Mods <small>Hand-picked by {{ site_name }} admins</small></h3>
<div class="container">
<div class="row">
{% for feature in featured[:6] %}
{% set mod = feature.mod %}
{% include "mod-box.html" %}
{% set mod = feature.mod %}
{% include "mod-box.html" %}
{% endfor %}
</div>
</div>
{% endif %}
<div class="well" style="margin-bottom: 0;margin-top: 2.5mm;">
<div class="container main-cat">
<a href="{% if ga %}/{{ ga.short }}{% endif %}/browse/new" class="btn btn-primary pull-right">
Expand Down
8 changes: 8 additions & 0 deletions templates/mod-box.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ <h2 class="group inner list-group-item-heading">
<a href="#" title="Unfollow" class="unfollow-mod-button glyphicon glyphicon-star" data-mod="{{ mod.id }}" data-id="{{ mod.id }}"></a>
<a href="#" title="Follow" class="follow-mod-button glyphicon glyphicon-star-empty" data-mod="{{ mod.id }}" data-id="{{ mod.id }}"></a>

{% if admin and feature %}

<a href="#" title="Raise feature priority" class="raise-feature-priority-button glyphicon glyphicon-chevron-left" data-mod="{{ mod.id }}" data-id="{{ mod.id }}"></a>

<a href="#" title="Lower feature priority" class="lower-feature-priority-button glyphicon glyphicon-chevron-right" data-mod="{{ mod.id }}" data-id="{{ mod.id }}"></a>

{% endif %}

<a href='{{url_for("mods.download", mod_id=mod.id, mod_name=mod.name)}}' title="Download" class="download-link glyphicon glyphicon-save-file"></a>
</div>
<div class="caption">
Expand Down

0 comments on commit cef74e2

Please sign in to comment.