mirror of https://git.sr.ht/~sircmpwn/hub.sr.ht
parent
df5ddcc3cb
commit
dc078f279f
|
@ -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")
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")}}
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue