Add project tags

Ref: ~sircmpwn/hub.sr.ht#19
This commit is contained in:
наб 2020-09-11 18:00:05 +02:00 committed by Drew DeVault
parent df5ddcc3cb
commit dc078f279f
14 changed files with 153 additions and 4 deletions

View File

@ -0,0 +1,24 @@
"""Add project.tags
Revision ID: 4da86bb54214
Revises: 6763318a8bcb
Create Date: 2020-09-10 03:41:10.011430
"""
# revision identifiers, used by Alembic.
revision = '4da86bb54214'
down_revision = '6763318a8bcb'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column("project", sa.Column("tags",
sa.ARRAY(sa.String(16), dimensions=1),
nullable=False, server_default="{}"))
def downgrade():
op.drop_column("project", "tags")

View File

@ -121,6 +121,21 @@ def dismiss_checklist_POST(owner, project_name):
owner=current_user.canonical_name,
project_name=project.name))
def _verify_tags(valid, raw_tags):
raw_tags = raw_tags or ""
tags = list(filter(lambda t: t, map(lambda t: t.strip(), raw_tags.split(","))))
valid.expect(len(tags) <= 3,
f"Too many tags ({len(tags)}, max 3)",
field="tags")
valid.expect(all(len(t) <= 16 for t in tags),
"Tags may be no longer than 16 characters",
field="tags")
valid.expect(all(re.match(r"^[A-Za-z0-9_][A-Za-z0-9_.-]*$", t) for t in tags),
"Tags must start with alphanumerics or underscores " +
"and may additionally include dots and dashes",
field="tags")
return tags
@projects.route("/projects/create")
@loginrequired
def create_GET():
@ -132,6 +147,7 @@ def create_POST():
valid = Validation(request)
name = valid.require("name")
description = valid.require("description")
raw_tags = valid.require("tags")
visibility = valid.require("visibility", cls=Visibility)
valid.expect(not name or len(name) < 128,
"Name must be fewer than 128 characters", field="name")
@ -144,12 +160,14 @@ def create_POST():
valid.expect(not description or len(description) < 512,
"Description must be fewer than 512 characters",
field="description")
tags = _verify_tags(valid, raw_tags)
if not valid.ok:
return render_template("project-create.html", **valid.kwargs)
project = Project()
project.name = name
project.description = description
project.tags = tags
project.visibility = visibility
project.owner_id = current_user.id
db.session.add(project)
@ -173,6 +191,7 @@ def config_POST(owner, project_name):
valid = Validation(request)
description = valid.require("description")
tags = _verify_tags(valid, valid.require("tags"))
website = valid.optional("website")
visibility = valid.require("visibility", cls=Visibility)
valid.expect(not website or valid_url(website),
@ -182,6 +201,7 @@ def config_POST(owner, project_name):
owner=owner, project=project, **valid.kwargs)
project.description = description
project.tags = tags
project.website = website
project.visibility = visibility
db.session.commit()

View File

@ -1,3 +1,4 @@
from sqlalchemy.sql import operators
from flask import Blueprint, render_template, request, session
from hubsrht.types import Project, Feature, Event, EventType, Visibility, User
from srht.flask import paginate_query
@ -49,7 +50,10 @@ def project_index():
if search:
try:
projects = search_by(projects, search,
[Project.name, Project.description])
[Project.name, Project.description],
key_fns={"tag": lambda t:
Project.tags.any(t, operator=operators.ilike_op)},
term_map=lambda t: f"tag:{t[1:]}" if t.startswith("#") else t)
except ValueError as e:
search_error = str(e)

View File

@ -1,4 +1,5 @@
from sqlalchemy import or_
from sqlalchemy.sql import operators
from flask import Blueprint, render_template, request, abort
from hubsrht.types import User, Project, Visibility, Event, EventType
from hubsrht.types import SourceRepo, MailingList, Tracker
@ -59,7 +60,10 @@ def projects_GET(owner):
if search:
try:
projects = search_by(projects, search,
[Project.name, Project.description])
[Project.name, Project.description],
key_fns={"tag": lambda t:
Project.tags.any(t, operator=operators.ilike_op)},
term_map=lambda t: f"tag:{t[1:]}" if t.startswith("#") else t)
except ValueError as e:
search_error = str(e)

View File

@ -57,6 +57,14 @@
project_name=project.name)}}">{{project.name}}</a>
</h4>
<p>{{project.description}}</p>
{% if project.tags %}
<div class="tags">
{% for tag in project.tags %}
<a href="{{url_for("public.project_index", search="#"+tag)}}"
class="tag">#{{tag}}</a>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>

View File

@ -23,6 +23,14 @@
<blockquote style="margin-top: 0.5rem">
{{feature.summary | md}}
</blockquote>
{% if project.tags %}
<div class="tags">
{% for tag in project.tags %}
<a href="{{url_for("public.project_index", search="#"+tag)}}"
class="tag">#{{tag}}</a>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
{{pagination()}}

View File

@ -41,6 +41,14 @@
<blockquote>
{{feature.summary | md}}
</blockquote>
{% if project.tags %}
<div class="tags">
{% for tag in project.tags %}
<a href="{{url_for("public.project_index", search="#"+tag)}}"
class="tag">#{{tag}}</a>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>

View File

@ -53,6 +53,15 @@
project_name=project.name)}}">{{project.name}}</a>
</h4>
<p>{{project.description}}</p>
{% if project.tags %}
<div class="tags">
{% for tag in project.tags %}
<a href="{{url_for("public.project_index",
search=((search or "").strip() + " #"+tag).lstrip())}}"
class="tag">#{{tag}}</a>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>

View File

@ -33,6 +33,18 @@
required />
{{valid.summary("description")}}
</div>
<div class="form-group">
<label for="tags">Tags</label>
<input
class="form-control {{valid.cls("tags")}}"
type="text"
id="tags"
name="tags"
placeholder="Up to three comma-separated topics"
value="{{tags or ", ".join(project.tags)}}"
required />
{{valid.summary("tags")}}
</div>
<div class="form-group">
<label for="project-website">Website</label>
<input

View File

@ -43,6 +43,18 @@
value="{{description or ""}}" />
{{valid.summary("description")}}
</div>
<div class="form-group">
<label for="tags">Tags</label>
<input
type="text"
id="tags"
name="tags"
class="form-control {{valid.cls("tags")}}"
placeholder="Up to three comma-separated topics"
required
value="{{", ".join(tags) or ""}}" />
{{valid.summary("tags")}}
</div>
<div class="flex-grow-1 d-flex flex-row justify-content-end">
<button type="submit" class="btn btn-primary align-self-end">
Create project {{icon("caret-right")}}

View File

@ -12,8 +12,8 @@
name="search"
type="text"
placeholder="Search all public projects"
class="form-control {% if search_error %} is-invalid{% endif %}"
value="{{search if search else ""}}" />
class="form-control {% if search_error %}is-invalid{% endif %}"
value="{{search or ""}}" />
{% if search_error %}
<div class="invalid-feedback">{{ search_error }}</div>
{% endif %}
@ -68,6 +68,14 @@
>{{project.name}}</a>
</h4>
<p>{{project.description}}</p>
{% if project.tags %}
<div class="tags">
{% for tag in project.tags %}
<a href="{{url_for("public.project_index", search="#"+tag)}}"
class="tag">#{{tag}}</a>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
@ -84,6 +92,15 @@
owner=feature.project.owner.canonical_name,
project_name=feature.project.name)}}"
>{{feature.project.name}}</a>
{% if feature.project.tags %}
<span class="tags pull-right">
{% for tag in feature.project.tags %}
<a href="{{url_for("public.project_index",
search=((search or "").strip() + " #"+tag).lstrip())}}"
class="tag">#{{tag}}</a>
{% endfor %}
</span>
{% endif %}
</h5>
<blockquote style="margin-top: 0.5rem">
{{feature.summary | md}}

View File

@ -53,6 +53,14 @@
project_name=project.name)}}">{{project.name}}</a>
</h4>
<p>{{project.description}}</p>
{% if project.tags %}
<div class="tags">
{% for tag in project.tags %}
<a href="{{url_for("public.project_index", search="#"+tag)}}"
class="tag">#{{tag}}</a>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>

View File

@ -17,6 +17,8 @@ class Project(Base):
name = sa.Column(sa.Unicode(128), nullable=False)
description = sa.Column(sa.Unicode(512), nullable=False)
tags = sa.Column(sa.ARRAY(sa.String(16), dimensions=1),
nullable=False, server_default="{}")
website = sa.Column(sa.Unicode)
visibility = sa.Column(sau.ChoiceType(Visibility, impl=sa.String()),
nullable=False, server_default="unlisted")

View File

@ -135,3 +135,16 @@
margin: 0;
}
}
.tags .tag:not(:last-child) {
margin-right: 0.5rem;
}
.event-list .event p {
margin-bottom: 0;
}
h5 .tags {
font-weight: normal;
font-size: 0.9rem;
}