Add basic mailing lists support

This commit is contained in:
Drew DeVault 2020-03-25 10:08:29 -04:00
parent 2aedc53e7f
commit f0f753bfd3
11 changed files with 321 additions and 8 deletions

View File

@ -1,7 +1,8 @@
from flask import Blueprint, render_template, request, redirect, url_for
from hubsrht.projects import ProjectAccess, get_project
from hubsrht.services import git
from hubsrht.services import git, lists
from hubsrht.types import Project, RepoType, SourceRepo, Visibility
from hubsrht.types import MailingList
from srht.config import get_origin
from srht.database import db
from srht.flask import paginate_query
@ -163,3 +164,59 @@ def set_summary_repo(owner, project_name, repo_id):
db.session.commit()
return redirect(url_for("projects.summary_GET",
owner=owner.canonical_name, project_name=project.name))
@projects.route("/<owner>/<project_name>/lists")
@loginrequired
def mailing_lists_GET(owner, project_name):
owner, project = get_project(owner, project_name, ProjectAccess.read)
mailing_lists = (MailingList.query
.filter(MailingList.project_id == project.id)
.order_by(MailingList.updated.desc()))
mailing_lists, pagination = paginate_query(mailing_lists)
return render_template("project-mailing-lists.html", view="mailing lists",
owner=owner, project=project, mailing_lists=mailing_lists,
**pagination)
@projects.route("/<owner>/<project_name>/lists/new")
@loginrequired
def mailing_lists_new_GET(owner, project_name):
owner, project = get_project(owner, project_name, ProjectAccess.write)
# TODO: Pagination
mls = lists.get_lists(owner)
mls = sorted(mls, key=lambda r: r["updated"], reverse=True)
return render_template("project-lists-new.html", view="new-resource",
owner=owner, project=project, lists=mls)
@projects.route("/<owner>/<project_name>/lists/new", methods=["POST"])
@loginrequired
def mailing_lists_new_POST(owner, project_name):
owner, project = get_project(owner, project_name, ProjectAccess.write)
valid = Validation(request)
if "create" in valid:
assert False # TODO: Create list
if "from-template" in valid:
assert False # TODO: Create lists from template
list_name = None
for field in valid.source:
if field.startswith("existing-"):
list_name = field[len("existing-"):]
break
mailing_list = lists.get_list(owner, list_name)
ml = MailingList()
ml.remote_id = mailing_list["id"]
ml.project_id = project.id
ml.owner_id = project.owner_id
ml.name = mailing_list["name"]
ml.description = mailing_list["description"]
db.session.add(ml)
lists.ensure_mailing_list_webhooks(owner, list_name, {
url_for("webhooks.mailing_list_update"): ["list:update", "list:delete"],
})
db.session.commit()
return redirect(url_for("projects.summary_GET",
owner=owner.canonical_name, project_name=project.name))

View File

@ -5,3 +5,7 @@ webhooks = Blueprint("webhooks", __name__)
@webhooks.route("/webhooks/git-repo")
def git_repo_update():
pass # TODO
@webhooks.route("/webhooks/mailing-list")
def mailing_list_update():
pass # TODO

View File

@ -3,6 +3,7 @@ from srht.api import ensure_webhooks, get_authorization, get_results
from srht.config import get_origin
_gitsrht = get_origin("git.sr.ht", external=True, default=None)
_listsrht = get_origin("lists.sr.ht", external=True, default=None)
class GitService:
def get_repos(self, user):
@ -27,4 +28,20 @@ class GitService:
def ensure_user_webhooks(self, user, config):
ensure_webhooks(user, f"{_gitsrht}/api/user/webhooks", config)
class ListService():
def get_lists(self, user):
return get_results(f"{_listsrht}/api/lists", user)
def get_list(self, user, list_name):
r = requests.get(f"{_listsrht}/api/lists/{list_name}",
headers=get_authorization(user))
if r.status_code != 200:
raise Exception(r.json())
return r.json()
def ensure_mailing_list_webhooks(self, user, list_name, config):
url = f"{_listsrht}/api/user/{user.canonical_name}/lists/{list_name}/webhooks"
ensure_webhooks(user, url, config)
git = GitService()
lists = ListService()

View File

@ -0,0 +1,145 @@
{% extends "project-resource-new.html" %}
{% block content %}
<form method="POST">
{{csrf_token()}}
<div class="row">
{# TODO: Hide this option for any projects which already have lists #}
<div class="col-lg-8">
<h3>Use a common template</h3>
<fieldset class="form-group">
<div class="form-check">
<label
class="form-check-label"
title="Only visible to you and your collaborators"
>
<input
class="form-check-input"
type="radio"
name="template"
value="public-inbox"
checked> public-inbox
<small id="visibility-unlisted-help" class="form-text text-muted">
Direct users to send patches &amp; questions to a single "public
inbox" shared with several other projects.
</small>
</label>
</div>
<div class="form-check">
<label
class="form-check-label"
title="Visible to anyone with the link, but not shown on your profile"
>
<input
class="form-check-input"
type="radio"
name="template"
value="announce-devel">
{{project.name}}-announce and
{{project.name}}-devel
<small id="visibility-unlisted-help" class="form-text text-muted">
Separate mailing lists for low-frequency project announcements,
and for patches &amp; development.
</small>
</label>
</div>
<div class="form-check">
<label class="form-check-label">
<input
class="form-check-input"
type="radio"
name="template"
value="announce-devel-discuss">
{{project.name}}-announce,
{{project.name}}-devel, and
{{project.name}}-discuss
<small id="visibility-public-help" class="form-text text-muted">
One list for announcements, one for patches &amp; development,
and another for user support and discussion.
</small>
</label>
</div>
</fieldset>
<div class="flex-grow-1 d-flex flex-row justify-content-end">
<button
type="submit"
class="btn btn-primary align-self-end"
name="from-template"
>Create these mailing lists {{icon("caret-right")}}</button>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<h3 style="margin-top: 1rem">
Or create a new mailing list
</h3>
<div class="form-group">
<label for="{{ typename }}">Name</label>
<input
type="text"
name="list_name"
id="name"
class="form-control {{ valid.cls("list_name") }}"
value="{{ list_name or project.name }}" />
{{ valid.summary("list_name") }}
<div class="form-group">
<label for="list_description">Description</label>
<input
type="text"
name="list_description"
id="list_description"
class="form-control {{valid.cls("list_description")}}"
value="{{ list_description or project.description }}" />
{{valid.summary("list_description")}}
</div>
</div>
<div class="flex-grow-1 d-flex flex-row justify-content-end">
<button
type="submit"
class="btn btn-primary align-self-end"
name="new"
>Create new mailing list {{icon("caret-right")}}</button>
</div>
</div>
</div>
{% if any(lists) %}
<div class="row">
<div class="col-lg-8">
<h3 style="margin-top: 1rem">
Or add an existing mailing list
</h3>
{# TODO: Pagination #}
<div class="form-group">
{# TODO: How exactly should this work #}
<input
name="search"
type="text"
placeholder="Search your mailing lists"
class="form-control"
value="{{ search if search else "" }}" />
</div>
<div class="event-list select-resource">
{% for list in lists %}
<div class="event">
<h3>
<button
type="submit"
name="existing-{{ list["name"] }}"
class="pull-right btn btn-primary btn-lg"
>Select list&nbsp;{{ icon("caret-right") }}</button>
<a
href="{{get_origin("lists.sr.ht",
external=True)}}/{{ list["owner"]["canonical_name"] }}/{{list["name"]}}"
target="_blank"
rel="noopener"
>{{ list["name"] }}</a>
</h3>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
</form>
{% endblock %}

View File

@ -0,0 +1,43 @@
{% extends "layout.html" %}
{% block body %}
<div class="header-tabbed">
<div class="container">
{% include 'project-nav.html' %}
</div>
</div>
{% if project.description %}
<div class="header-extension">
<div class="container">
{{ project.description }}
</div>
</div>
{% endif %}
<div class="container">
<div class="row">
<div class="col-md-10 event-list">
{% for mailing_list in mailing_lists %}
<div class="event">
<h4>
<a
href="{{mailing_list.url()}}"
>~{{owner.username}}/{{mailing_list.name}}</a>
</h4>
{% if mailing_list.description %}
{{ mailing_list.description | md }}
{% endif %}
</div>
{% endfor %}
{{pagination()}}
</div>
{% if current_user and current_user.id == owner.id %}
<div class="col-md-2">
<a
href="{{url_for("projects.mailing_lists_new_GET",
owner=owner.canonical_name, project_name=project.name)}}"
class="btn btn-primary btn-block"
>Add mailing list&nbsp;{{icon('caret-right')}}</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -21,6 +21,13 @@
project_name=project.name), "sources")}}
</li>
{% endif %}
{% if any(project.mailing_lists) %}
<li class="nav-item">
{{link(url_for("projects.mailing_lists_GET",
owner=owner.canonical_name,
project_name=project.name), "mailing lists")}}
</li>
{% endif %}
{# TODO
<li class="nav-item">
<a
@ -30,9 +37,6 @@
<li class="nav-item">
{{link("#", "documentation")}}
</li>
<li class="nav-item">
{{link("#", "mailing lists")}}
</li>
<li class="nav-item">
{{link("#", "tickets")}}
</li>

View File

@ -34,6 +34,7 @@
</button>
</div>
</div>
{% if any(repos) %}
<div class="row">
<div class="col-lg-8">
<h3 style="margin-top: 1rem">
@ -70,5 +71,6 @@
</div>
</div>
</div>
{% endif %}
</form>
{% endblock %}

View File

@ -36,9 +36,7 @@
href="{{url_for("projects.sources_new_GET",
owner=owner.canonical_name, project_name=project.name)}}"
class="btn btn-primary btn-block"
>
Add repository&nbsp;{{icon('caret-right')}}
</a>
>Add repository&nbsp;{{icon('caret-right')}}</a>
</div>
{% endif %}
</div>

View File

@ -46,13 +46,26 @@
{% endif %}
</li>
<li>
{% if any(project.mailing_lists) %}
{{icon('check', cls='text-success')}}
Add mailing lists
<br />
<small class="text-muted">
You can see the list of your mailing lists by clicking
"mailing lists" on the project navigation.
</small>
{% else %}
{{icon('plus-square', cls='text-info')}}
<a href="#">Add mailing lists&nbsp;{{icon('arrow-right')}}</a>
<a
href="{{url_for("projects.mailing_lists_new_GET",
owner=owner.canonical_name, project_name=project.name)}}"
>Add mailing lists&nbsp;{{icon('arrow-right')}}</a>
<br />
<small class="text-muted">
Mailing lists give users a means of asking questions about the
project or sending patches to contribute to the source code.
</small>
{% endif %}
</li>
<li>
{{icon('plus-square', cls='text-info')}}

View File

@ -12,3 +12,4 @@ class Visibility(Enum):
from hubsrht.types.project import Project
from hubsrht.types.sourcerepo import SourceRepo, RepoType
from hubsrht.types.mailinglist import MailingList

View File

@ -0,0 +1,29 @@
import sqlalchemy as sa
from srht.config import get_origin
from srht.database import Base
_listsrht = get_origin("lists.sr.ht", external=True, default=None)
class MailingList(Base):
__tablename__ = "mailing_list"
id = sa.Column(sa.Integer, primary_key=True)
remote_id = sa.Column(sa.Integer, nullable=False)
created = sa.Column(sa.DateTime, nullable=False)
updated = sa.Column(sa.DateTime, nullable=False)
project_id = sa.Column(sa.Integer,
sa.ForeignKey("project.id"), nullable=False)
project = sa.orm.relationship("Project",
backref=sa.orm.backref("mailing_lists"),
foreign_keys=[project_id])
# Note: in theory this may eventually be different from the project owner(?)
owner_id = sa.Column(sa.Integer,
sa.ForeignKey("user.id"), nullable=False)
owner = sa.orm.relationship("User")
name = sa.Column(sa.Unicode(128), nullable=False)
description = sa.Column(sa.Unicode(512), nullable=False)
def url(self):
return f"{_listsrht}/{self.owner.canonical_name}/{self.name}"