diff --git a/app/data/tags.yaml b/app/data/tags.yaml index 8e09d88..88a3bea 100644 --- a/app/data/tags.yaml +++ b/app/data/tags.yaml @@ -52,10 +52,14 @@ lang: pretty: "Langage: autre" games: + action: + pretty: Action adventure: pretty: Aventure fighting: pretty: Combat + narrative: + pretty: Narratif other: pretty: "Jeu: autre" platform: @@ -98,5 +102,7 @@ courses: pretty: SI/Électronique economics: pretty: Économie + informatics: + pretty: Informatique other: pretty: "Cours: autre" diff --git a/app/forms/programs.py b/app/forms/programs.py index 1f462e7..ca4cf16 100644 --- a/app/forms/programs.py +++ b/app/forms/programs.py @@ -3,12 +3,13 @@ from wtforms import StringField, SubmitField, TextAreaField, MultipleFileField from wtforms.validators import InputRequired, Length import app.utils.validators as vf from app.utils.antibot_field import AntibotField +from app.utils.tag_field import TagListField from app.forms.forum import CommentForm class ProgramCreationForm(CommentForm): name = StringField('Nom du programme', validators=[InputRequired(), Length(min=3, max=64)]) - tags = StringField('Liste de tags', description='Séparés par des virgules') + tags = TagListField('Liste de tags') submit = SubmitField('Soumettre le programme') diff --git a/app/models/program.py b/app/models/program.py index 23bf006..ba291ad 100644 --- a/app/models/program.py +++ b/app/models/program.py @@ -24,18 +24,18 @@ class Program(Post): # * tags (inherited from Post) # * attachements (available at thread.top_comment.attachments) - def __init__(self, author, title, thread): + def __init__(self, author, name, thread): """ Create a Program. Arguments: author -- post author (User, though only Members can post) - title -- program title (unicode string) + name -- program name (unicode string) thread -- discussion thread attached to the topic """ Post.__init__(self, author) - self.title = title + self.name = name self.thread = thread @staticmethod diff --git a/app/models/tag.py b/app/models/tag.py index a7ad50d..ed4f548 100644 --- a/app/models/tag.py +++ b/app/models/tag.py @@ -19,6 +19,19 @@ class TagInformation(db.Model): def __init__(self, id): self.id = id + def category(self): + return self.id.split(".", 1)[0] + + @staticmethod + def all_tags(): + all_tags = {} + for ti in TagInformation.query.all(): + ctgy = ti.category() + if ctgy not in all_tags: + all_tags[ctgy] = [] + all_tags[ctgy].append(ti) + return all_tags + class Tag(db.Model): """Association between a Post and a dot-string tag identifier.""" diff --git a/app/processors/utilities.py b/app/processors/utilities.py index 0b8edf2..18bbf1d 100644 --- a/app/processors/utilities.py +++ b/app/processors/utilities.py @@ -3,15 +3,16 @@ from flask import url_for from config import V5Config from slugify import slugify from app.utils.login_as import is_vandal +from app.models.tag import TagInformation @app.context_processor def utilities_processor(): """ Add some utilities to render context """ return dict( len=len, - # enumerate=enumerate, _url_for=lambda route, args, **other: url_for(route, **args, **other), V5Config=V5Config, slugify=slugify, - is_vandal=is_vandal + is_vandal=is_vandal, + db_all_tags=TagInformation.all_tags, ) diff --git a/app/routes/programs/submit.py b/app/routes/programs/submit.py index 0675a84..8bbdb7e 100644 --- a/app/routes/programs/submit.py +++ b/app/routes/programs/submit.py @@ -34,9 +34,8 @@ def program_submit(): db.session.commit() # Add tags - # TODO: Check tags against a predefined set - for tag in form.tags.data.split(","): - db.session.add(Tag(p, tag.strip())) + for tag in form.tags.selected_tags(): + db.session.add(Tag(p, tag)) db.session.commit() # Manage files diff --git a/app/static/css/form.css b/app/static/css/form.css index b780e4a..9465375 100644 --- a/app/static/css/form.css +++ b/app/static/css/form.css @@ -8,7 +8,7 @@ .form form label + .desc { margin: 0 0 4px 0; font-size: 80%; - opacity: .75; + opacity: .65; } .form form .avatar { width: 128px; @@ -98,6 +98,21 @@ .form input[type='email'].abfield { display: none; } +form .dynamic-tag-selector { + display: none; +} +form .dynamic-tag-selector input[type="text"] { + display: none; +} +form .dynamic-tag-selector .tag { + cursor: pointer; +} +form .dynamic-tag-selector .tags-selected { + margin: 0 0 4px 0; +} +form .dynamic-tag-selector .tags-selected .tag { + display: none; +} .form.filter { margin-bottom: 16px; } diff --git a/app/static/css/themes/FK_dark_theme.css b/app/static/css/themes/FK_dark_theme.css index 2a8ae11..3d9bd13 100644 --- a/app/static/css/themes/FK_dark_theme.css +++ b/app/static/css/themes/FK_dark_theme.css @@ -148,3 +148,23 @@ div.editor-toolbar, div.CodeMirror { --border: rgba(255, 255, 255, 0.8); --selected: rgba(255, 0, 0, 1.0); } + +.tag { + --background: #22292c; + --color: white; +} +.tag.tag-calc { + --background: #917e1a; +} +.tag.tag-lang { + --background: #4a8033; +} +.tag.tag-games { + --background: #488695; +} +.tag.tag-tools { + --background: #70538a; +} +.tag.tag-courses { + --background: #884646; +} diff --git a/app/static/css/themes/Tituya_v43_theme.css b/app/static/css/themes/Tituya_v43_theme.css index f2cb187..571176c 100644 --- a/app/static/css/themes/Tituya_v43_theme.css +++ b/app/static/css/themes/Tituya_v43_theme.css @@ -172,3 +172,22 @@ div.pagination { font-size: 14px; margin: 13px; } + +.tag { + --background: #e0e0e0; +} +.tag.tag-calc { + --background: #f0ca81; +} +.tag.tag-lang { + --background: #aad796; +} +.tag.tag-games { + --background: #a7ccd5; +} +.tag.tag-tools { + --background: #c6aae1; +} +.tag.tag-courses { + --background: #f0a0a0; +} diff --git a/app/static/css/themes/default_theme.css b/app/static/css/themes/default_theme.css index b87e4f8..1168211 100644 --- a/app/static/css/themes/default_theme.css +++ b/app/static/css/themes/default_theme.css @@ -142,3 +142,22 @@ div.editor-toolbar, div.CodeMirror { table.codehilitetable td.linenos { color: #808080; } + +.tag { + --background: #e0e0e0; +} +.tag.tag-calc { + --background: #f0ca81; +} +.tag.tag-lang { + --background: #aad796; +} +.tag.tag-games { + --background: #a7ccd5; +} +.tag.tag-tools { + --background: #c6aae1; +} +.tag.tag-courses { + --background: #f0a0a0; +} diff --git a/app/static/css/widgets.css b/app/static/css/widgets.css index a7bb6eb..fcf977d 100644 --- a/app/static/css/widgets.css +++ b/app/static/css/widgets.css @@ -207,4 +207,15 @@ hr.signature { } .gallery-spot * { cursor: pointer; +} +.tag { + display: inline-block; + background: var(--background); + color: var(--color); + padding: 4px 12px; + margin: 4px 0; + border-radius: 8px; + border-radius: calc(4.5em); + user-select: none; + cursor: default; } \ No newline at end of file diff --git a/app/static/less/form.less b/app/static/less/form.less index 36e37b4..046b7dc 100644 --- a/app/static/less/form.less +++ b/app/static/less/form.less @@ -13,7 +13,7 @@ & + .desc { margin: 0 0 4px 0; font-size: 80%; - opacity: .75; + opacity: .65; } } @@ -115,6 +115,29 @@ } +/* Interactive tag selector */ + +form .dynamic-tag-selector { + display: none; + + input[type="text"] { + display: none; + } + + .tag { + cursor: pointer; + } + + .tags-selected { + margin: 0 0 4px 0; + + .tag { + display: none; + } + } +} + + /* Interactive filter forms */ .form.filter { diff --git a/app/static/less/widgets.less b/app/static/less/widgets.less index c86f2e0..41d602e 100644 --- a/app/static/less/widgets.less +++ b/app/static/less/widgets.less @@ -239,3 +239,17 @@ hr.signature { cursor: pointer; } } + +/* Tags */ + +.tag { + display: inline-block; + background: var(--background); + color: var(--color); + padding: 4px 12px; + margin: 4px 0; + border-radius: 8px; + border-radius: calc(0.5em + 4px); + user-select: none; + cursor: default; +} diff --git a/app/static/scripts/tag_selector.js b/app/static/scripts/tag_selector.js new file mode 100644 index 0000000..9836205 --- /dev/null +++ b/app/static/scripts/tag_selector.js @@ -0,0 +1,61 @@ +function tag_selector_find(node) { + while(node != document.body) { + if(node.classList.contains("dynamic-tag-selector")) + return node; + node = node.parentNode; + } + return undefined; +} + +function tag_selector_get(ts) { + return ts.querySelector("input").value + .split(",") + .map(str => str.trim()) + .filter(str => str !== ""); +} + +function tag_selector_set(ts, values) { + ts.querySelector("input").value = values.join(", "); + tag_selector_update(ts); +} + +function tag_selector_update(ts) { + if(ts === undefined) return; + const input_names = tag_selector_get(ts); + + /* Update visibility of selected tags */ + ts.querySelectorAll(".tags-selected .tag[data-name]").forEach(tag => { + const visible = input_names.includes(tag.dataset.name); + tag.style.display = visible ? "inline-block" : "none"; + }); + + /* Update visibility of pool tags */ + ts.querySelectorAll(".tags-pool .tag[data-name]").forEach(tag => { + const visible = !input_names.includes(tag.dataset.name); + tag.style.display = visible ? "inline-block" : "none"; + }); +} + +function tag_selector_add(ts, id) { + if(ts === undefined) return; + + let tags = tag_selector_get(ts); + if(!tags.includes(id)) + tags.push(id); + + tag_selector_set(ts, tags); +} + +function tag_selector_remove(ts, id) { + if(ts === undefined) return; + + let tags = tag_selector_get(ts); + tags = tags.filter(str => str !== id); + + tag_selector_set(ts, tags); +} + +document.querySelectorAll(".dynamic-tag-selector").forEach(ts => { + ts.style.display = "block"; + tag_selector_update(ts); +}); diff --git a/app/templates/account/register.html b/app/templates/account/register.html index 6efe74f..272318b 100644 --- a/app/templates/account/register.html +++ b/app/templates/account/register.html @@ -46,7 +46,7 @@