~sircmpwn/todo.sr.ht

bb5a0a8ccf8d0fd04aba4939f78331743932385c — наб 3 months ago 68e1a17 0.62.0
Add option to notify users of their own web activity

Ref: ~sircmpwn/todo.sr.ht#94
M scss/main.scss => scss/main.scss +26 -0
@@ 239,3 239,29 @@ select.form-control {
    flex-grow: 1;
  }
}

// TODO: move me into core.sr.ht
details {
  padding: 0 1rem;
  margin: 0 -1rem 1rem;

  summary {
    background: $gray-300;
    padding: 0 1rem;
    margin: 0 -1rem;
  }

  &[open] {
    padding: 0 1rem;
    margin-left: calc(-1rem - 4px);
    border-left: 4px solid $gray-300;
  }
}

.prefs {
  padding: 0.5rem 0;

  .form-check {
    padding-left: 0;
  }
}

M todosrht-lmtp => todosrht-lmtp +1 -1
@@ 236,7 236,7 @@ class MailHandler:
            return "550 Comment must be between 3 and 16384 characters."

        event = add_comment(sender, ticket, text=body,
                resolution=resolution, resolve=resolve, reopen=reopen)
                resolution=resolution, resolve=resolve, reopen=reopen, from_email=True)
        TicketWebhook.deliver(TicketWebhook.Events.event_create,
                event.to_dict(),
                TicketWebhook.Subscription.ticket_id == ticket.id)

A todosrht/alembic/versions/3a9cb6757f59_add_user_notify_self.py => todosrht/alembic/versions/3a9cb6757f59_add_user_notify_self.py +23 -0
@@ 0,0 1,23 @@
"""Add User.notify_self

Revision ID: 3a9cb6757f59
Revises: 6c714f704591
Create Date: 2020-09-28 19:11:22.221191

"""

# revision identifiers, used by Alembic.
revision = '3a9cb6757f59'
down_revision = '6c714f704591'

from alembic import op
import sqlalchemy as sa


def upgrade():
    op.add_column('user', sa.Column('notify_self', sa.Boolean,
        nullable=False, server_default='FALSE'))


def downgrade():
    op.drop_column('user', 'notify_self')

M todosrht/blueprints/html.py => todosrht/blueprints/html.py +19 -7
@@ 1,12 1,14 @@
from flask import Blueprint, render_template, request, abort
from flask import Blueprint, render_template, request, abort, redirect, url_for
from todosrht.access import get_tracker, get_access
from todosrht.tickets import get_participant_for_user
from todosrht.types import Tracker, Ticket, TicketAccess
from todosrht.types import Event, EventNotification, EventType
from todosrht.types import User, Participant
from srht.config import cfg
from srht.oauth import current_user
from srht.database import db
from srht.oauth import current_user, loginrequired
from srht.flask import paginate_query, session
from srht.validation import Validation
from sqlalchemy import and_, or_

html = Blueprint('html', __name__)


@@ 51,7 53,7 @@ def filter_authorized_events(events):
    return events

@html.route("/")
def index():
def index_GET():
    if not current_user:
        return render_template("index.html")
    trackers = (Tracker.query


@@ 68,15 70,25 @@ def index():
            .order_by(Event.created.desc()))
    events = events.limit(10).all()

    notice = session.get("notice")
    if notice:
        del session["notice"]
    notice = session.pop("notice", None)
    prefs_updated = session.pop("prefs_updated", None)

    return render_template("dashboard.html",
        trackers=trackers, notice=notice,
        tracker_list_msg="Your Trackers",
        more_trackers=total_trackers > limit_trackers,
        events=events, EventType=EventType)
        events=events, EventType=EventType,
        prefs_updated=prefs_updated)

@html.route("/", methods=["POST"])
@loginrequired
def index_POST():
    valid = Validation(request)
    notify_self = valid.require("notify-self")
    current_user.notify_self = notify_self == "on"
    db.session.commit()
    session["prefs_updated"] = True
    return redirect(url_for("html.index_GET"))

@html.route("/~<username>")
def user_GET(username):

M todosrht/blueprints/settings.py => todosrht/blueprints/settings.py +1 -1
@@ 214,7 214,7 @@ def delete_POST(owner, name):
            { "id": tracker_id },
            UserWebhook.Subscription.user_id == owner_id)

    return redirect(url_for("html.index"))
    return redirect(url_for("html.index_GET"))

@settings.route("/<owner>/<name>/settings/import-export")
@loginrequired

M todosrht/templates/dashboard.html => todosrht/templates/dashboard.html +22 -0
@@ 51,6 51,28 @@
      >
        Create new tracker {{icon("caret-right")}}
      </a>
      <details
        style="margin: -0.5rem 0 0.5rem 0"
        {% if prefs_updated %}
        open
        {% endif %}
      >
        <summary>User preferences</summary>
        <form method="POST" class="prefs">
          {{csrf_token()}}
            <label class="form-check">
              <input
                type="checkbox"
                name="notify-self"
                id="notify-self"
                {{ "checked" if current_user.notify_self }} />
              Notify me of my own activity
            </label>
          <button class="btn btn-primary" type="submit">
            Apply {{icon("caret-right")}}
          </button>
        </form>
      </details>
      {% endif %}
      {% if len(trackers) > 0 %}
      <h3>{{ tracker_list_msg }}</h3>

M todosrht/tickets.py => todosrht/tickets.py +8 -7
@@ 211,7 211,7 @@ def _change_ticket_status(ticket, resolve, resolution, reopen):
            old_resolution, ticket.resolution)

def _send_comment_notifications(
        participant, ticket, event, comment, resolution):
        participant, ticket, event, comment, resolution, from_email):
    """
    Notify users subscribed to the ticket or tracker.
    Returns a list of notified users.


@@ 230,7 230,7 @@ def _send_comment_notifications(

    for subscriber, subscription in subscriptions.items():
        _create_event_notification(subscriber, event)
        if subscriber != participant:
        if (participant.notify_self and not from_email) or subscriber != participant:
            _send_comment_notification(
                subscription, ticket, participant, event, comment, resolution)



@@ 295,7 295,8 @@ def _handle_mentions(ticket, submitter, text, notified_users, comment=None):


def add_comment(submitter, ticket,
        text=None, resolve=False, resolution=None, reopen=False):
        text=None, resolve=False, resolution=None, reopen=False,
        from_email=False):
    """
    Comment on a ticket, optionally resolve or reopen the ticket.
    """


@@ 311,7 312,7 @@ def add_comment(submitter, ticket,
        return None
    event = _create_comment_event(ticket, submitter, comment, status_change)
    notified_participants = _send_comment_notifications(
        submitter, ticket, event, comment, resolution)
        submitter, ticket, event, comment, resolution, from_email)

    if comment and comment.text:
        _handle_mentions(


@@ 406,7 407,7 @@ def assign(ticket, assignee, assigner):
    assigner_participant = get_participant_for_user(assigner)

    subscription = get_or_create_subscription(ticket, assignee_participant)
    if assigner != assignee:
    if assigner.notify_self or assigner != assignee:
        notify_assignee(subscription, ticket, assigner, assignee)

    event = Event()


@@ 500,8 501,8 @@ def submit_ticket(tracker, submitter, title, description,
        # Send notifications
        for sub in all_subscriptions.values():
            _create_event_notification(sub.participant, event)
            # Notify submitter for tickets created by email
            if from_email or sub.participant != submitter:
            # Always notify submitter for tickets created by email
            if from_email or submitter.notify_self or sub.participant != submitter:
                _send_new_ticket_notification(sub, ticket, from_email_id)

        _handle_mentions(

M todosrht/types/__init__.py => todosrht/types/__init__.py +2 -1
@@ 1,9 1,10 @@
from srht.database import Base
from srht.oauth import ExternalUserMixin
from srht.oauth import ExternalOAuthTokenMixin
import sqlalchemy as sa

class User(Base, ExternalUserMixin):
    pass
    notify_self = sa.Column(sa.Boolean, nullable=False, server_default="FALSE")

class OAuthToken(Base, ExternalOAuthTokenMixin):
    pass

M todosrht/types/participant.py => todosrht/types/participant.py +7 -0
@@ 52,6 52,13 @@ class Participant(Base):
            return self.external_id
        assert False

    @property
    def notify_self(self):
        if self.participant_type == ParticipantType.user:
            return self.user.notify_self
        else:
            return False

    def __str__(self):
        return self.name