Skip to content

Commit

Permalink
Merge 1839af4 into 5553d3b
Browse files Browse the repository at this point in the history
  • Loading branch information
TimJentzsch committed Apr 21, 2022
2 parents 5553d3b + 1839af4 commit 98a35f6
Show file tree
Hide file tree
Showing 4 changed files with 306 additions and 16 deletions.
165 changes: 160 additions & 5 deletions api/slack/commands/info.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
from api.serializers import VolunteerSerializer
from datetime import datetime, timedelta
from typing import Dict, Optional

from django.utils import timezone

from api.models import TranscriptionCheck
from api.slack import client
from api.slack.utils import dict_to_table, parse_user
from api.views.misc import Summary
from authentication.models import BlossomUser
from blossom.strings import translation

i18n = translation()
Expand All @@ -22,13 +28,162 @@ def info_cmd(channel: str, message: str) -> None:
elif len(parsed_message) == 2:
user, username = parse_user(parsed_message[1])
if user:
v_data = VolunteerSerializer(user).data
msg = i18n["slack"]["user_info"].format(
username, "\n".join(dict_to_table(v_data))
)
msg = user_info_text(user)
else:
msg = i18n["slack"]["errors"]["unknown_username"].format(username=username)
else:
msg = i18n["slack"]["errors"]["too_many_params"]

client.chat_postMessage(channel=channel, text=msg)


def user_info_text(user: BlossomUser) -> str:
"""Get the info message for the given user."""
name_link = f"<https://reddit.com/u/{user.username}|u/{user.username}>"
title = f"Info about *{name_link}*:"

general = _format_info_section("General", user_general_info(user))
transcription_quality = _format_info_section(
"Transcription Quality", user_transcription_quality_info(user)
)
debug = _format_info_section("Debug Info", user_debug_info(user))

return f"{title}\n\n{general}\n\n{transcription_quality}\n\n{debug}"


def _format_info_section(name: str, section: Dict) -> str:
"""Format a given info section to a readable string.
Example:
*Section name*:
- Key 1: Value 1
- Key 2: Value 2
"""
section_items = "\n".join([f"- {key}: {value}" for key, value in section.items()])

return f"*{name}*:\n{section_items}"


def user_general_info(user: BlossomUser) -> Dict:
"""Get general info for the given user."""
total_gamma = user.gamma
recent_gamma = user.gamma_at_time(start_time=timezone.now() - timedelta(weeks=2))
gamma = f"{total_gamma} Γ ({recent_gamma} Γ in last 2 weeks)"
joined_on = _format_time(user.date_joined)
last_active = _format_time(user.date_last_active()) or "Never"

return {
"Gamma": gamma,
"Joined on": joined_on,
"Last active": last_active,
}


def user_transcription_quality_info(user: BlossomUser) -> Dict:
"""Get info about the transcription quality of the given user."""
gamma = user.gamma
check_status = TranscriptionCheck.TranscriptionCheckStatus

# The checks for the given user
user_checks = TranscriptionCheck.objects.filter(transcription__author=user)
check_count = user_checks.count()
check_ratio = check_count / gamma if gamma > 0 else 0
checks = f"{check_count} ({check_ratio:.1%} of transcriptions)"

# The warnings for the given user
user_warnings_pending = user_checks.filter(status=check_status.WARNING_PENDING)
user_warnings_resolved = user_checks.filter(status=check_status.WARNING_RESOLVED)
user_warnings_unfixed = user_checks.filter(status=check_status.WARNING_UNFIXED)
warnings_count = (
user_warnings_pending.count()
+ user_warnings_resolved.count()
+ user_warnings_unfixed.count()
)
warnings_ratio = warnings_count / check_count if check_count > 0 else 0
warnings = f"{warnings_count} ({warnings_ratio:.1%} of checks)"

watch_status = user.transcription_check_reason(ignore_low_activity=True)

return {
"Checks": checks,
"Warnings": warnings,
"Watch status": watch_status,
}


def user_debug_info(user: BlossomUser) -> Dict:
"""Get debug info about the given user."""
user_id = f"`{user.id}`"
blacklisted = bool_str(user.blacklisted)
bot = bool_str(user.is_bot)
accepted_coc = bool_str(user.accepted_coc)

return {
"ID": user_id,
"Blacklisted": blacklisted,
"Bot": bot,
"Accepted CoC": accepted_coc,
}


def bool_str(bl: bool) -> str:
"""Convert a bool to a Yes/No string."""
return "Yes" if bl else "No"


def _format_time(time: Optional[datetime]) -> Optional[str]:
"""Format the given time in absolute and relative strings."""
if time is None:
return None

now = timezone.now()
absolute = time.date().isoformat()

relative_delta = now - time
relative = _relative_duration(relative_delta)

if now >= time:
return f"{absolute} ({relative} ago)"
else:
return f"{absolute} (in {relative})"


def _relative_duration(delta: timedelta) -> str:
"""Format the delta into a relative time string."""
seconds = abs(delta.total_seconds())
minutes = seconds / 60
hours = minutes / 60
days = hours / 24
weeks = days / 7
months = days / 30
years = days / 365

# Determine major time unit
if years >= 1:
value, unit = years, "year"
elif months >= 1:
value, unit = months, "month"
elif weeks >= 1:
value, unit = weeks, "week"
elif days >= 1:
value, unit = days, "day"
elif hours >= 1:
value, unit = hours, "hour"
elif minutes >= 1:
value, unit = minutes, "min"
elif seconds > 5:
value, unit = seconds, "sec"
else:
duration_ms = seconds / 1000
value, unit = duration_ms, "ms"

if unit == "ms":
duration_str = f"{value:0.0f} ms"
else:
# Add plural s if necessary
if value != 1:
unit += "s"

duration_str = f"{value:.1f} {unit}"

return duration_str
118 changes: 118 additions & 0 deletions api/tests/slack/commands/test_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from datetime import datetime, timedelta
from unittest.mock import patch

import pytz
from django.test import Client

from api.models import TranscriptionCheck
from api.slack.commands.info import user_info_text
from utils.test_helpers import (
create_check,
create_submission,
create_transcription,
setup_user_client,
)


def test_user_info_text_new_user(client: Client) -> None:
"""Verify that the string for a new user is generated correctly."""
client, headers, user = setup_user_client(
client,
id=123,
username="Userson",
date_joined=datetime(2021, 5, 21, tzinfo=pytz.UTC),
accepted_coc=False,
is_bot=False,
)

expected = """Info about *<https://reddit.com/u/Userson|u/Userson>*:
*General*:
- Gamma: 0 Γ (0 Γ in last 2 weeks)
- Joined on: 2021-05-21 (1.0 day ago)
- Last active: Never
*Transcription Quality*:
- Checks: 0 (0.0% of transcriptions)
- Warnings: 0 (0.0% of checks)
- Watch status: Automatic (100.0%)
*Debug Info*:
- ID: `123`
- Blacklisted: No
- Bot: No
- Accepted CoC: No"""

with patch(
"api.slack.commands.info.timezone.now",
return_value=datetime(2021, 5, 22, tzinfo=pytz.UTC),
):
actual = user_info_text(user)

assert actual == expected


def test_user_info_text_old_user(client: Client) -> None:
"""Verify that the string for a new user is generated correctly."""
client, headers, user = setup_user_client(
client,
id=123,
username="Userson",
date_joined=datetime(2020, 4, 21, tzinfo=pytz.UTC),
accepted_coc=True,
is_bot=False,
)

tr_data = [
(datetime(2020, 7, 3, tzinfo=pytz.UTC), True, False),
(datetime(2020, 7, 7, tzinfo=pytz.UTC), False, False),
(datetime(2020, 7, 9, tzinfo=pytz.UTC), False, False),
(datetime(2021, 4, 10, tzinfo=pytz.UTC), True, True),
(datetime(2021, 4, 12, tzinfo=pytz.UTC), False, False),
]

for idx, (date, has_check, is_warning) in enumerate(tr_data):
submission = create_submission(
id=100 + idx,
create_time=date - timedelta(days=1),
claim_time=date,
complete_time=date + timedelta(days=1),
claimed_by=user,
completed_by=user,
)
transcription = create_transcription(
submission, user, id=200 + idx, create_time=date
)

if has_check:
check_status = TranscriptionCheck.TranscriptionCheckStatus
status = (
check_status.WARNING_RESOLVED if is_warning else check_status.APPROVED
)
create_check(transcription, status=status)

expected = """Info about *<https://reddit.com/u/Userson|u/Userson>*:
*General*:
- Gamma: 5 Γ (2 Γ in last 2 weeks)
- Joined on: 2020-04-21 (1.0 year ago)
- Last active: 2021-04-13 (1.1 weeks ago)
*Transcription Quality*:
- Checks: 2 (40.0% of transcriptions)
- Warnings: 1 (50.0% of checks)
- Watch status: Automatic (100.0%)
*Debug Info*:
- ID: `123`
- Blacklisted: No
- Bot: No
- Accepted CoC: Yes"""

with patch(
"api.slack.commands.info.timezone.now",
return_value=datetime(2021, 4, 21, tzinfo=pytz.UTC),
):
actual = user_info_text(user)

assert actual == expected
31 changes: 28 additions & 3 deletions authentication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@

class BlossomUserManager(UserManager):
# https://stackoverflow.com/a/7774039
def filter(self, **kwargs: Any) -> QuerySet:
def filter(self, **kwargs: Any) -> QuerySet: # noqa: ANN401
"""Override `filter` to make usernames case insensitive."""
if "username" in kwargs:
kwargs["username__iexact"] = kwargs["username"]
del kwargs["username"]
return super().filter(**kwargs)

def get(self, **kwargs: Any) -> QuerySet:
def get(self, **kwargs: Any) -> QuerySet: # noqa: ANN401
"""Override `get` to make usernames case insensitive."""
if "username" in kwargs:
kwargs["username__iexact"] = kwargs["username"]
Expand Down Expand Up @@ -100,6 +100,30 @@ class Meta:

objects = BlossomUserManager()

def date_last_active(self) -> Optional[datetime]:
"""Return the time where the user was last active.
This will give the time where the user last claimed or completed a post.
"""
recently_claimed = (
Submission.objects.filter(claimed_by=self).order_by("-claim_time").first()
)
recent_claim_time = recently_claimed.claim_time if recently_claimed else None

recently_completed = (
Submission.objects.filter(completed_by=self)
.order_by("-complete_time")
.first()
)
recent_complete_time = (
recently_completed.complete_time if recently_completed else None
)

if recent_claim_time and recent_complete_time:
return max(recent_complete_time, recent_claim_time)

return recent_claim_time or recent_complete_time

@property
def gamma(self) -> int:
"""
Expand Down Expand Up @@ -184,7 +208,8 @@ def has_low_activity(self) -> bool:
"""
recent_date = datetime.now(tz=pytz.UTC) - LOW_ACTIVITY_TIMEDELTA
recent_transcriptions = Submission.objects.filter(
completed_by=self, complete_time__gte=recent_date,
completed_by=self,
complete_time__gte=recent_date,
).count()

return recent_transcriptions <= LOW_ACTIVITY_THRESHOLD
Expand Down
8 changes: 0 additions & 8 deletions blossom/strings/en_US.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,6 @@ Here's a status update on everything!
```
"""

user_info="""
User info for {0}:
```
{1}
```
"""

github_sponsor_update="{0} GitHub Sponsors: [{1}] - {2} | {3} {0}"

[slack.errors]
Expand Down

0 comments on commit 98a35f6

Please sign in to comment.