post: add a tagging system, with a common base set of tags

Adds the tagging system, with 3 types of tags:
* Calculator models grouped by compatibility classes
* Programming languages
* Game, tools, and course categories

[MIGRATION] This commit contains a new version of the schema.
[BREAKS] This commit breaks existing tag assignments.
[MASTER] Run the 'update-tags' command of master.py.
This commit is contained in:
Lephe 2022-06-12 18:11:46 +01:00
parent f4b9110ce2
commit c74abf3fcc
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
5 changed files with 223 additions and 5 deletions

102
app/data/tags.yaml Normal file
View File

@ -0,0 +1,102 @@
# This is a list of all tags, sorted by category. The category names are used
# to name CSS rules and shouldn't be changed directly.
# The following category groups calculators by common compatibility properties.
# Each comment indicates why the group should exist on its own rather than
# being merged with another one.
calc:
# Middle-school level, only basic algorithms; a unique property in this list.
fx92:
pretty: fx-92 Scientifique Collège+
# Some of the most limited Graph models, no add-ins.
g25:
pretty: Graph 25/25+E/25+EII
# The whole series with more Basic constructs than g25, but SH3 for add-ins
# We don't separate based on whether an OS update is required (deemed safe)
gsh3:
pretty: Graph 35+/75/85/95 (SH3)
# Same as gsh3, but with SH4 for add-ins; support CasioPython
gsh4:
pretty: Graph 35+/35+E/75+/75+E (SH4)
# Like gsh3, but has Python; also; issues with the display and MonochromLib
g35+e2:
pretty: Graph 35+E II
# Color display, nothing like the previous models
cg20:
pretty: fx-CG 10/20/Prizm
# Like cg20, but has Python, and some incompatibilities on add-in
g90+e:
pretty: Graph 90+E
# Different series entirely; has an SDK for add-ins
cp300:
pretty: Classpad 300/330
# Like cp300, but does not have an SDK
cp330+:
pretty: Classpad 330+
# Color display, entirely new model; no SDK
cp400:
pretty: Classpad 400/400+E
lang:
basic:
pretty: Basic CASIO
cbasic:
pretty: C.Basic
python:
pretty: Python
c:
pretty: C/C++ (add-in)
lua:
pretty: LuaFX
other:
pretty: "Langage: autre"
games:
adventure:
pretty: Aventure
fighting:
pretty: Combat
other:
pretty: "Jeu: autre"
platform:
pretty: Plateforme
puzzle:
pretty: Puzzle
rpg:
pretty: RPG
rythm:
pretty: Rythme
shooting:
pretty: Tir/FPS
simulation:
pretty: Simulation
sport:
pretty: Sport
strategy:
pretty: Stratégie
survival:
pretty: Survie
tools:
conversion:
pretty: Outil de conversion
graphics:
pretty: Outil graphique
science:
pretty: Outil scientifique
programming:
pretty: Outil pour programmer
other:
pretty: "Outil: autre"
courses:
math:
pretty: Maths
physics:
pretty: Physique
engineering:
pretty: SI/Électronique
economics:
pretty: Économie
other:
pretty: "Cours: autre"

View File

@ -13,12 +13,16 @@ class Program(Post):
# TODO: Category (games/utilities/lessons)
# TODO: Compatible calculator models
# TODO: Number of views, statistics, etc
# Thread with the program description (top comment) and comments
thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False)
thread = db.relationship('Thread', foreign_keys=thread_id,
back_populates='owner_program')
# TODO: Number of views, statistics, attached files, etc
# Implicit attributes:
# * tags (inherited from Post)
# * attachements (available at thread.top_comment.attachments)
def __init__(self, author, title, thread):
"""

View File

@ -1,15 +1,43 @@
from app import db
class TagInformation(db.Model):
"""Detailed information about tags, by dot-string tag identifier."""
__tablename__ = 'tag_information'
# The ID is the dot-string of the tag (eg. "calc.g35+e2")
id = db.Column(db.String(64), primary_key=True)
# List of uses. Note how we load tag information along individual tags, but
# we don't load uses unless the field is accessed.
uses = db.relationship('Tag', back_populates='tag', lazy='dynamic')
# Pretty name
pretty = db.Column(db.String(64))
# ... any other static information about tags
def __init__(self, id):
self.id = id
class Tag(db.Model):
"""Association between a Post and a dot-string tag identifier."""
__tablename__ = 'tag'
id = db.Column(db.Integer, primary_key=True)
# Tagged post
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), index=True)
post = db.relationship('Post', back_populates='tags', foreign_keys=post_id)
# Tag name
name = db.Column(db.String(64), index=True)
# Tag name. Note how we always load the information along the tag, but not
# the other way around.
tag_id = db.Column(db.String(64), db.ForeignKey('tag_information.id'),
index=True)
tag = db.relationship('TagInformation', back_populates='uses',
foreign_keys=tag_id, lazy='joined')
def __init__(self, post, tag):
self.post = post
self.name = tag
if isinstance(tag, str):
tag = TagInformation.query.filter_by(id=tag).one()
self.tag = tag

View File

@ -5,6 +5,7 @@ from app.models.user import Member, Group, GroupPrivilege
from app.models.priv import SpecialPrivilege
from app.models.trophy import Trophy, Title
from app.models.forum import Forum
from app.models.tag import TagInformation
from app.utils import unicode_names
import os
import sys
@ -23,11 +24,13 @@ Listing commands:
groups Show privilege groups
forums Show forum tree
trophies Show trophies
tags Show tags
Install and update commands:
update-groups Create or update groups from app/data/
update-forums Create or update the forum tree from app/data/
update-trophies Create or update trophies
update-tags Create or update tag information
generate-trophy-icons Regenerate all trophy icons
create-common-accounts Remove and recreate 'Planète Casio' and 'GLaDOS'
add-group <member> #<id> Add <member> to group #<id> (presumably admins)
@ -56,6 +59,11 @@ def trophies(*args):
for t in Trophy.query.all():
print(t)
def tags(*args):
tags = TagInformation.query.all()
for t in sorted(tags, key=lambda t: t.id):
print(f"{t.id}: {t.pretty}")
#
# Install and update commands
#
@ -160,7 +168,6 @@ def update_trophies():
existing = Trophy.query.all()
# Get the list of what we want to obtain
tr = []
with open(os.path.join(app.root_path, "data", "trophies.yaml")) as fp:
tr = yaml.safe_load(fp.read())
tr = { t["name"]: t for t in tr }
@ -207,6 +214,39 @@ def update_trophies():
db.session.commit()
def update_tags():
existing = TagInformation.query.all()
with open(os.path.join(app.root_path, "data", "tags.yaml")) as fp:
data = yaml.safe_load(fp.read())
tags = { ctgy + "." + name: data[ctgy][name]
for ctgy in data for name in data[ctgy] }
# Remove bad tags
for t in existing:
if t.id not in tags:
print(f"[tags] Deleted '{t.id}'")
db.session.delete(t)
db.session.commit()
for name, info in tags.items():
pretty = info.get("pretty", name)
t = TagInformation.query.filter_by(id=name).first()
if t is not None:
changes = (t.pretty != pretty)
t.pretty = pretty
if changes:
print(f"[tags] Updated '{name}'")
else:
t = TagInformation(name)
t.pretty = pretty
print(f"[tags] Created '{name}'")
db.session.add(t)
db.session.commit()
def generate_trophy_icons():
tr = []
with open(os.path.join(app.root_path, "data", "trophies.yaml")) as fp:
@ -308,9 +348,11 @@ commands = {
"groups": groups,
"forums": forums,
"trophies": trophies,
"tags": tags,
"update-groups": update_groups,
"update-forums": update_forums,
"update-trophies": update_trophies,
"update-tags": update_tags,
"generate-trophy-icons": generate_trophy_icons,
"create-common-accounts": create_common_accounts,
"add-group": add_group,

View File

@ -0,0 +1,42 @@
"""add tag information
Revision ID: 189bbc0e1543
Revises: fa34c9f43c24
Create Date: 2022-06-09 22:26:58.562710
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '189bbc0e1543'
down_revision = 'fa34c9f43c24'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('tag_information',
sa.Column('id', sa.String(length=64), nullable=False),
sa.Column('pretty', sa.String(length=64), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.add_column('tag', sa.Column('tag_id', sa.String(length=64), nullable=True))
op.drop_index('ix_tag_name', table_name='tag')
op.create_index(op.f('ix_tag_tag_id'), 'tag', ['tag_id'], unique=False)
op.create_foreign_key(None, 'tag', 'tag_information', ['tag_id'], ['id'])
op.drop_column('tag', 'name')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('tag', sa.Column('name', sa.VARCHAR(length=64), autoincrement=False, nullable=True))
op.drop_constraint(None, 'tag', type_='foreignkey')
op.drop_index(op.f('ix_tag_tag_id'), table_name='tag')
op.create_index('ix_tag_name', 'tag', ['name'], unique=False)
op.drop_column('tag', 'tag_id')
op.drop_table('tag_information')
# ### end Alembic commands ###