From 4b4845511ee8a8b17339fd489975089e247b324e Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Tue, 13 May 2025 09:02:01 -0700 Subject: [PATCH 01/29] moved JS from the template into main.js --- README.md | 4 +- loveapp/themes/default/static/js/main.js | 146 +++++++++++++++++++++ loveapp/themes/default/templates/home.html | 134 ------------------- 3 files changed, 148 insertions(+), 136 deletions(-) diff --git a/README.md b/README.md index fb4d9a3..90913dd 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ to set up billing and give Cloud Build permission to deploy your app. ### Prepare for deployment -Copy the [example config](config-example.py) file to config.py and change the +Copy the [example config](loveapp/config-example.py) file to config.py and change the settings. Don't forget to specify your own SECRET_KEY. ### Initial deployment @@ -85,7 +85,7 @@ using the [Secret](models/secret.py) model. Locally, you can temporarily add an def create_secrets(): from loveapp.models import Secret Secret(id='AWS_ACCESS_KEY_ID', value='change-me').put() - Secret(id='AWS_SECRET_ACCESS_KEY', value='change-me').put() + Secret(id='AWS_SECRET_ACCESS_KEY', value='change-me').put() return "please delete me now" ``` diff --git a/loveapp/themes/default/static/js/main.js b/loveapp/themes/default/static/js/main.js index e0a332b..34b9f5e 100644 --- a/loveapp/themes/default/static/js/main.js +++ b/loveapp/themes/default/static/js/main.js @@ -24,7 +24,153 @@ function setupLinkify() { $('.love-message').linkify(); } +/** + * Initialize autocomplete and other functionality for the love form + */ +function initLoveForm() { + if (!$('#send-love-form').length) { + return; // Exit if we're not on a page with the love form + } + + $('#nav-send').addClass('active'); + $('#secret-love-label').tooltip(); + $('input[name="recipients"]').focus(); + if ($('input[name="recipients"]').val() != '') { + var messageText = $('textarea[name="message"]').val(); + $('textarea[name="message"]').focus().val('').val(messageText); + } + $('#love-error').hide(); + + // Hashtags autocomplete - code heavily sampled from http://jqueryui.com/autocomplete/#multiple-remote + $('textarea[name="message"]') + .bind('keydown', function(event) { + if (event.keyCode === $.ui.keyCode.TAB && + $(this).data("ui-autocomplete").menu.active) { + event.preventDefault(); + } + }) + .autocomplete({ + source: function(request, response) { + $.getJSON('/values/autocomplete', { + term: request.term.split(/\s/).pop() + }, response); + }, + search: function() { + var term = this.value.split(/\s/).pop(); + if (!term.startsWith("#")) { + return false; + } + return term; + }, + focus: function() { return false; }, + select: function(event, ui) { + var terms = this.value.split(/\s/); + terms.pop(); + terms.push(ui.item.value + " "); + this.value = terms.join(" "); + return false; + }, + minLength: 1, + delay: 5, + autoFocus: true + }); + + // Recipients autocomplete + $('input[name="recipients"]') + // don't navigate away from the field on tab when selecting an item + .bind('keydown', function(event) { + if (event.keyCode === $.ui.keyCode.TAB && + $(this).autocomplete().menu.active) { + event.preventDefault(); + } + }) + .autocomplete({ + source: function(request, response) { + $.getJSON('/user/autocomplete', { + term: extractLast(request.term) + }, response); + }, + search: function() { + // set minLength before attempting autocomplete + var term = extractLast(this.value); + if (term.length < 2) { + return false; + } + }, + focus: function() { + return false; + }, + select: function(event, ui) { + var terms = split(this.value); + // remove the current input + terms.pop(); + // add the selected item + terms.push(ui.item.value); + // add placeholder to get the comma-and-space at the end + terms.push(''); + this.value = terms.join(', '); + return false; + }, + }).data('ui-autocomplete')._renderItem = function(ul, item) { + return $("
  • ") + .attr('class', 'ui-menu-item avatar-autocomplete') + .attr('role', 'presentation') + .attr("data-value", item.value) + .append( + $("") + .attr('class', 'ui-corner-all') + .attr('tabindex', '-1') + .append( + '' + + item.label + + '') + ) + .appendTo(ul); + }; + + // Set up shareable link functionality + var loveLinkBlock = $('.love-link-block'); + if (loveLinkBlock.length) { + $('.create-link-btn').hide(); + hideLoveLinkBlockOnInputChange(loveLinkBlock); + setCopyToClipboardBtnAction(); + } +} + +// Helper functions for the love form +function split(val) { + return val.split(/,\s*/); +} + +function extractLast(term) { + return split(term).pop(); +} + +function hideLoveLinkBlockOnInputChange(loveLinkBlock) { + $('input[name="recipients"], textarea[name="message"]').change(function() { + $('input[name="recipients"], textarea[name="message"]').off('change'); + loveLinkBlock.hide(); + $('.create-link-btn').show(); + }); +} + +function setCopyToClipboardBtnAction() { + var copyBtn = document.querySelector('.copybtn'); + copyBtn.addEventListener('click', function() { + window.getSelection().removeAllRanges(); + var linkText = document.querySelector('.love-link'); + var range = document.createRange(); + range.selectNode(linkText); + window.getSelection().addRange(range); + document.execCommand('copy'); + window.getSelection().removeAllRanges(); + }); +} + $(document).ready(function () { setupDateFormatting(); setupLinkify(); + initLoveForm(); // Add the initialization of the love form }); diff --git a/loveapp/themes/default/templates/home.html b/loveapp/themes/default/templates/home.html index 22af9df..6b42522 100644 --- a/loveapp/themes/default/templates/home.html +++ b/loveapp/themes/default/templates/home.html @@ -7,138 +7,4 @@ {% endblock %} {% block javascript %} - {% endblock %} From bce9e314e07600ee679f9b4573f42d80971e2b82 Mon Sep 17 00:00:00 2001 From: Billy Montgomery Date: Tue, 13 May 2025 12:39:46 -0700 Subject: [PATCH 02/29] add message tags with yelpiversary highlight --- loveapp/models/love.py | 17 ++++++++ loveapp/themes/default/templates/explore.html | 10 ++++- loveapp/themes/default/templates/me.html | 8 +++- .../default/templates/parts/love_message.html | 7 +++- loveapp/views/web.py | 42 +++++++++++++++---- 5 files changed, 70 insertions(+), 14 deletions(-) diff --git a/loveapp/models/love.py b/loveapp/models/love.py index eafa84a..c31cfef 100644 --- a/loveapp/models/love.py +++ b/loveapp/models/love.py @@ -18,3 +18,20 @@ class Love(ndb.Model): @property def seconds_since_epoch(self): return int(mktime(self.timestamp.timetuple())) + + @property + def tags(self): + return self._tags + + @tags.setter + def tags(self, value): + self._tags = value + + def __init__(self, *args, **kwargs): + super(Love, self).__init__(*args, **kwargs) + self._tags = [] + + def add_tag(self, tag): + """Helper method to append a tag to the tags list.""" + if tag not in self._tags: + self._tags.append(tag) diff --git a/loveapp/themes/default/templates/explore.html b/loveapp/themes/default/templates/explore.html index a269cd2..e4d6600 100644 --- a/loveapp/themes/default/templates/explore.html +++ b/loveapp/themes/default/templates/explore.html @@ -48,7 +48,10 @@

    Love Sent

    recipient.username, recipient, love.message, - love.seconds_since_epoch) + love.seconds_since_epoch, + love.secret, + tags + ) }} {% endfor %} @@ -72,7 +75,10 @@

    Love Received

    recipient.username, sender, love.message, - love.seconds_since_epoch) + love.seconds_since_epoch, + love.secret, + love.tags + ) }} {% endfor %} diff --git a/loveapp/themes/default/templates/me.html b/loveapp/themes/default/templates/me.html index 65a1c8c..9785326 100644 --- a/loveapp/themes/default/templates/me.html +++ b/loveapp/themes/default/templates/me.html @@ -21,7 +21,9 @@

    Love Received

    sender, love.message, love.seconds_since_epoch, - love.secret) + love.secret, + love.tags + ) }} {% endfor %} @@ -48,7 +50,9 @@

    Love Sent

    recipient, love.message, love.seconds_since_epoch, - love.secret) + love.secret, + love.tags + ) }} {% endfor %} diff --git a/loveapp/themes/default/templates/parts/love_message.html b/loveapp/themes/default/templates/parts/love_message.html index a19b28f..f23e6c5 100644 --- a/loveapp/themes/default/templates/parts/love_message.html +++ b/loveapp/themes/default/templates/parts/love_message.html @@ -1,8 +1,11 @@ {% import theme("parts/photobox.html") as photobox %} -{% macro love(sender_name, sender_username, recipient_name, recipient_username, icon_user, message, ts, secret) %} -
    +{% macro love(sender_name, sender_username, recipient_name, recipient_username, icon_user, message, ts, secret, tags) %} +
    {{ photobox.user_icon(icon_user) }} + {% if 'yelpiversary' in tags %} +
    πŸ₯³
    + {% endif %}
    {{ message|linkify_company_values }} {% if secret %} diff --git a/loveapp/views/web.py b/loveapp/views/web.py index d4e5d11..fe58c86 100644 --- a/loveapp/views/web.py +++ b/loveapp/views/web.py @@ -67,15 +67,19 @@ def home(): def me(): current_employee = Employee.get_current_employee() - sent_love = loveapp.logic.love.recent_sent_love(current_employee.key, limit=20) - received_love = loveapp.logic.love.recent_received_love(current_employee.key, limit=20) + sent_love = handle_love( + loveapp.logic.love.recent_sent_love(current_employee.key, limit=20).get_result() + ) + received_love = handle_love( + loveapp.logic.love.recent_received_love(current_employee.key, limit=20).get_result() + ) return render_template( 'me.html', current_time=datetime.utcnow(), current_user=current_employee, - sent_loves=sent_love.get_result(), - received_loves=received_love.get_result() + sent_loves=sent_love, + received_loves=received_love ) @@ -192,14 +196,14 @@ def explore(): flash('Sorry, "{}" is not a valid user.'.format(username), 'error') return redirect(url_for('web_app.explore')) - sent_love = loveapp.logic.love.recent_sent_love(user_key, include_secret=False, limit=20) - received_love = loveapp.logic.love.recent_received_love(user_key, include_secret=False, limit=20) + sent_love = handle_love(loveapp.logic.love.recent_sent_love(user_key, include_secret=False, limit=20).get_result()) + received_love = handle_love(loveapp.logic.love.recent_received_love(user_key, include_secret=False, limit=20).get_result()) return render_template( 'explore.html', current_time=datetime.utcnow(), - sent_loves=sent_love.get_result(), - received_loves=received_love.get_result(), + sent_loves=sent_love, + received_loves=received_love, user=user_key.get() ) @@ -437,3 +441,25 @@ def import_employees(): flash('We started importing employee data in the background. Refresh the page to see it.', 'info') taskqueue.add(url='/tasks/employees/load/csv') return redirect(url_for('web_app.employees')) + + +def _is_yelpiversary_message(message: str) -> bool: + """Helper function to check if a message contains 'happy yelpiversary'.""" + return bool(message and 'happy yelpiversary' in message.lower()) + + +def handle_love(loves: list) -> list: + """ + Handles formatting the love response data to the caller. + Currently add tags to each love record based on message content and + may be extended to add other message details + """ + if not loves: + return loves + + for love in loves: + # Add yelpiversary tag if applicable + if _is_yelpiversary_message(love.message): + love.add_tag('yelpiversary') + + return loves \ No newline at end of file From 0634687552182a8588a7cf35bdb6767b448ced53 Mon Sep 17 00:00:00 2001 From: Billy Montgomery Date: Tue, 13 May 2025 14:02:13 -0700 Subject: [PATCH 03/29] move tag handling to love model --- loveapp/config-example.py | 6 +++ loveapp/models/love.py | 21 ++++++++- .../default/templates/parts/love_message.html | 2 +- loveapp/views/web.py | 44 ++++--------------- 4 files changed, 36 insertions(+), 37 deletions(-) diff --git a/loveapp/config-example.py b/loveapp/config-example.py index 36fd889..c3b5e08 100644 --- a/loveapp/config-example.py +++ b/loveapp/config-example.py @@ -53,3 +53,9 @@ CompanyValue('BE_EXCELLENT', 'Be excellent to each other', ('excellent', 'BeExcellent', 'WyldStallyns')), CompanyValue('DUST_IN_THE_WIND', 'All we are is dust in the wind, dude.', ('woah', 'whoa', 'DustInTheWind')) ] + +MESSAGE_TAG_CONFIG = { + # Work anniversary message string add a πŸ₯³ to the sender's avatar to highlight work anniversary shout outs + # Set to None to disable + "work_anniversary_tag_substring": "happy workiversary", +} \ No newline at end of file diff --git a/loveapp/models/love.py b/loveapp/models/love.py index c31cfef..aefe1c8 100644 --- a/loveapp/models/love.py +++ b/loveapp/models/love.py @@ -5,6 +5,8 @@ from loveapp.models import Employee +import loveapp.config as config + class Love(ndb.Model): """Models an instance of sent love.""" @@ -31,7 +33,24 @@ def __init__(self, *args, **kwargs): super(Love, self).__init__(*args, **kwargs) self._tags = [] - def add_tag(self, tag): + # Initialize tags based on message content + self._init_tags() + + def _init_tags(self): + """Initialize tags based on message content.""" + if self.message and self._is_work_anniversary_message(self.message): + self._add_tag('work_anniversary') + + def _add_tag(self, tag): """Helper method to append a tag to the tags list.""" if tag not in self._tags: self._tags.append(tag) + + def _is_work_anniversary_message(self, message: str) -> bool: + """Helper function to check if a message contains work anniversary text.""" + work_anniversary_finder = config.MESSAGE_TAG_CONFIG.get("work_anniversary_tag_substring") + if work_anniversary_finder is None: + self._tags = [] + return False + + return bool(message and work_anniversary_finder in message.lower()) \ No newline at end of file diff --git a/loveapp/themes/default/templates/parts/love_message.html b/loveapp/themes/default/templates/parts/love_message.html index f23e6c5..81ffb24 100644 --- a/loveapp/themes/default/templates/parts/love_message.html +++ b/loveapp/themes/default/templates/parts/love_message.html @@ -3,7 +3,7 @@ {% macro love(sender_name, sender_username, recipient_name, recipient_username, icon_user, message, ts, secret, tags) %}
    {{ photobox.user_icon(icon_user) }} - {% if 'yelpiversary' in tags %} + {% if 'work_anniversary' in tags %}
    πŸ₯³
    {% endif %}
    diff --git a/loveapp/views/web.py b/loveapp/views/web.py index fe58c86..a4dd5f3 100644 --- a/loveapp/views/web.py +++ b/loveapp/views/web.py @@ -67,19 +67,15 @@ def home(): def me(): current_employee = Employee.get_current_employee() - sent_love = handle_love( - loveapp.logic.love.recent_sent_love(current_employee.key, limit=20).get_result() - ) - received_love = handle_love( - loveapp.logic.love.recent_received_love(current_employee.key, limit=20).get_result() - ) + sent_love = loveapp.logic.love.recent_sent_love(current_employee.key, limit=20) + received_love = loveapp.logic.love.recent_received_love(current_employee.key, limit=20) return render_template( 'me.html', current_time=datetime.utcnow(), current_user=current_employee, - sent_loves=sent_love, - received_loves=received_love + sent_loves=sent_love.get_result(), + received_loves=received_love.get_result(), ) @@ -196,14 +192,14 @@ def explore(): flash('Sorry, "{}" is not a valid user.'.format(username), 'error') return redirect(url_for('web_app.explore')) - sent_love = handle_love(loveapp.logic.love.recent_sent_love(user_key, include_secret=False, limit=20).get_result()) - received_love = handle_love(loveapp.logic.love.recent_received_love(user_key, include_secret=False, limit=20).get_result()) + sent_love = loveapp.logic.love.recent_sent_love(user_key, include_secret=False, limit=20) + received_love = loveapp.logic.love.recent_received_love(user_key, include_secret=False, limit=20) return render_template( 'explore.html', current_time=datetime.utcnow(), - sent_loves=sent_love, - received_loves=received_love, + sent_loves=sent_love.get_result(), + received_loves=received_love.get_result(), user=user_key.get() ) @@ -440,26 +436,4 @@ def import_employees_form(): def import_employees(): flash('We started importing employee data in the background. Refresh the page to see it.', 'info') taskqueue.add(url='/tasks/employees/load/csv') - return redirect(url_for('web_app.employees')) - - -def _is_yelpiversary_message(message: str) -> bool: - """Helper function to check if a message contains 'happy yelpiversary'.""" - return bool(message and 'happy yelpiversary' in message.lower()) - - -def handle_love(loves: list) -> list: - """ - Handles formatting the love response data to the caller. - Currently add tags to each love record based on message content and - may be extended to add other message details - """ - if not loves: - return loves - - for love in loves: - # Add yelpiversary tag if applicable - if _is_yelpiversary_message(love.message): - love.add_tag('yelpiversary') - - return loves \ No newline at end of file + return redirect(url_for('web_app.employees')) \ No newline at end of file From 3934379fd251ca01e948a348c259de39a62be163 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Tue, 13 May 2025 14:45:44 -0700 Subject: [PATCH 04/29] grouped love works! --- loveapp/logic/love.py | 80 ++++++++++++++++++- loveapp/themes/default/static/css/style.css | 23 ++++++ loveapp/themes/default/templates/explore.html | 45 +++++------ loveapp/themes/default/templates/me.html | 49 +++++------- .../templates/parts/grouped_love_message.html | 32 ++++++++ loveapp/themes/default/templates/values.html | 46 ++++------- loveapp/views/web.py | 26 +++--- 7 files changed, 198 insertions(+), 103 deletions(-) create mode 100644 loveapp/themes/default/templates/parts/grouped_love_message.html diff --git a/loveapp/logic/love.py b/loveapp/logic/love.py index b5db5a2..c058d58 100644 --- a/loveapp/logic/love.py +++ b/loveapp/logic/love.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- -from datetime import datetime +from collections import defaultdict +from datetime import datetime, timedelta from google.appengine.api import taskqueue @@ -27,8 +28,85 @@ def _love_query(start_dt, end_dt, include_secret): query = query.filter(Love.secret == False) # noqa return query +def cluster_loves_by_time(loves, time_window_days=1): + # Phase 1: strict group by content + groups = defaultdict(list) + for love in loves: + groups[love.message.strip().lower()].append(love) + + # Phase 2: further group by secret status + secret_groups = {} + for content, content_loves in groups.items(): + # Split into secret and non-secret groups + secret_loves = [love for love in content_loves if love.secret] + non_secret_loves = [love for love in content_loves if not love.secret] + + # Only add groups that have at least one love + if secret_loves: + secret_groups[(content, True)] = secret_loves + if non_secret_loves: + secret_groups[(content, False)] = non_secret_loves + + # Phase 3: within each group, temporal clustering + clustered_groups = [] + threshold = timedelta(days=time_window_days) + + for (content, is_secret), content_loves in secret_groups.items(): + # Sort by timestamp + content_loves.sort(key=lambda l: l.timestamp) + current_cluster = [] + last_time = None + for love in content_loves: + if not current_cluster: + current_cluster.append(love) + last_time = love.timestamp + else: + if love.timestamp - last_time > threshold: + # Finish old cluster, start new one + clustered_groups.append(current_cluster) + current_cluster = [love] + else: + current_cluster.append(love) + last_time = love.timestamp + if current_cluster: + clustered_groups.append(current_cluster) + + # Annotate each cluster with its representative timestamp and collect unique senders and recipients + results = [] + for cluster in clustered_groups: + newest_time = max(love.seconds_since_epoch for love in cluster) + + # Get all unique senders and recipients from this cluster + unique_sender_keys = set() + unique_recipient_keys = set() + + # First pass - collect unique keys + for love in cluster: + unique_sender_keys.add(love.sender_key) + unique_recipient_keys.add(love.recipient_key) + + # Second pass - get the actual objects + senders = [key.get() for key in unique_sender_keys if key] + recipients = [key.get() for key in unique_recipient_keys if key] + + results.append({ + 'content': cluster[0].message, + 'is_secret': cluster[0].secret, # Add is_secret flag to result + 'senders': senders, + 'recipients': recipients, + 'sender_count': len(senders), + 'recipient_count': len(recipients), + 'most_recent_love_timestamp': newest_time, + }) + + # Sort by most recent timestamp + results.sort(key=lambda x: x['most_recent_love_timestamp'], reverse=True) + # Return the sorted list of clusters + return results + def _sent_love_query(employee_key, start_dt, end_dt, include_secret): + return _love_query(start_dt, end_dt, include_secret).filter(Love.sender_key == employee_key) diff --git a/loveapp/themes/default/static/css/style.css b/loveapp/themes/default/static/css/style.css index a46e0cc..d392a0d 100644 --- a/loveapp/themes/default/static/css/style.css +++ b/loveapp/themes/default/static/css/style.css @@ -51,6 +51,13 @@ h2 { .love-message { margin-left: 60px; + background-color: #f8f8f8; + padding: 10px 20px; + border-radius: 5px; +} + +.user-list { + margin-top: 10px; } .love-byline { @@ -352,3 +359,19 @@ h4 { -2px 0px 0px #000, -2px -1px 0px #000; } + +.badge { + vertical-align: text-bottom; + border-radius: 5px;; +} + +.loves-columns { + column-count: 2; + column-gap: 2em; +} + +.love-message-container { + break-inside: avoid; + margin-bottom: 1.5em; + display: block; +} \ No newline at end of file diff --git a/loveapp/themes/default/templates/explore.html b/loveapp/themes/default/templates/explore.html index a269cd2..2132fe6 100644 --- a/loveapp/themes/default/templates/explore.html +++ b/loveapp/themes/default/templates/explore.html @@ -1,6 +1,7 @@ {% extends theme("layout.html") %} {% import theme("parts/love_message.html") as love_message %} +{% import theme("parts/grouped_love_message.html") as grouped_love_message %} " {% import theme("parts/photobox.html") as photobox %} {% import theme("parts/flash.html") as flash %} @@ -36,20 +37,16 @@

    {{ user.full_name }}

    Love Sent

    - {% if sent_loves %} + {% if sent_grouped_loves %}
    - {% for love in sent_loves %} - {% set sender = love.sender_key.get() %} - {% set recipient = love.recipient_key.get() %} - {{ love_message.love( - sender.first_name, - sender.username, - recipient.full_name, - recipient.username, - recipient, - love.message, - love.seconds_since_epoch) - }} + {% for grouped_love in sent_grouped_loves %} + {{ grouped_love_message.love( + "To", + grouped_love.recipients, + grouped_love.content, + grouped_love.most_recent_love_timestamp, + grouped_love.is_secret + )}} {% endfor %}
    {% else %} @@ -60,20 +57,16 @@

    Love Sent

    Love Received

    - {% if received_loves %} + {% if received_grouped_loves %}
    - {% for love in received_loves %} - {% set sender = love.sender_key.get() %} - {% set recipient = love.recipient_key.get() %} - {{ love_message.love( - sender.full_name, - sender.username, - recipient.first_name, - recipient.username, - sender, - love.message, - love.seconds_since_epoch) - }} + {% for grouped_love in received_grouped_loves %} + {{ grouped_love_message.love( + "From", + grouped_love.senders, + grouped_love.content, + grouped_love.most_recent_love_timestamp, + grouped_love.is_secret + )}} {% endfor %}
    {% else %} diff --git a/loveapp/themes/default/templates/me.html b/loveapp/themes/default/templates/me.html index 65a1c8c..fd1bca4 100644 --- a/loveapp/themes/default/templates/me.html +++ b/loveapp/themes/default/templates/me.html @@ -1,6 +1,7 @@ {% extends theme("layout.html") %} {% import theme("parts/love_message.html") as love_message %} +{% import theme("parts/grouped_love_message.html") as grouped_love_message %} {% block title %}My Love{% endblock %} @@ -8,21 +9,16 @@

    Love Received

    - {% if received_loves %} + {% if received_grouped_loves %}
    - {% for love in received_loves %} - {% set sender = love.sender_key.get() %} - {% set recipient = love.recipient_key.get() %} - {{ love_message.love( - sender.full_name, - sender.username, - 'you', - '', - sender, - love.message, - love.seconds_since_epoch, - love.secret) - }} + {% for grouped_love in received_grouped_loves %} + {{ grouped_love_message.love( + "From", + grouped_love.senders, + grouped_love.content, + grouped_love.most_recent_love_timestamp, + grouped_love.is_secret + )}} {% endfor %}
    {% else %} @@ -35,22 +31,17 @@

    Love Received

    Love Sent

    - {% if sent_loves %} + {% if sent_grouped_loves %}
    - {% for love in sent_loves %} - {% set sender = love.sender_key.get() %} - {% set recipient = love.recipient_key.get() %} - {{ love_message.love( - 'You', - '', - recipient.full_name, - recipient.username, - recipient, - love.message, - love.seconds_since_epoch, - love.secret) - }} - {% endfor %} + {% for grouped_love in sent_grouped_loves %} + {{ grouped_love_message.love( + "To", + grouped_love.recipients, + grouped_love.content, + grouped_love.most_recent_love_timestamp, + grouped_love.is_secret + )}} + {% endfor %}
    {% else %}
    - {% if loves_first_list %} -
    -
    - {% for love in loves_first_list %} - {% set sender = love.sender_key.get() %} - {% set recipient = love.recipient_key.get() %} - {{ love_message.love( - sender.full_name, - sender.username, - recipient.first_name, - recipient.username, - sender, - love.message, - love.seconds_since_epoch) - }} - {% endfor %} -
    -
    -
    - {% for love in loves_second_list %} - {% set sender = love.sender_key.get() %} - {% set recipient = love.recipient_key.get() %} - {{ love_message.love( - sender.full_name, - sender.username, - recipient.first_name, - recipient.username, - sender, - love.message, - love.seconds_since_epoch) - }} - {% endfor %} + {% if grouped_loves %} +
    +
    + {% for grouped_love in grouped_loves %} + {{ grouped_love_message.love( + "To", + grouped_love.recipients, + grouped_love.content, + grouped_love.most_recent_love_timestamp, + grouped_love.is_secret + )}} + {% endfor %}
    {% else %} diff --git a/loveapp/views/web.py b/loveapp/views/web.py index d4e5d11..60a7ab3 100644 --- a/loveapp/views/web.py +++ b/loveapp/views/web.py @@ -15,7 +15,6 @@ import loveapp.logic.employee import loveapp.logic.event import loveapp.logic.love -import loveapp.logic.love_count import loveapp.logic.love_link import loveapp.logic.subscription from errors import NoSuchEmployee @@ -67,15 +66,15 @@ def home(): def me(): current_employee = Employee.get_current_employee() - sent_love = loveapp.logic.love.recent_sent_love(current_employee.key, limit=20) - received_love = loveapp.logic.love.recent_received_love(current_employee.key, limit=20) + sent_love = loveapp.logic.love.recent_sent_love(current_employee.key, limit=200).get_result() + received_love = loveapp.logic.love.recent_received_love(current_employee.key, limit=200).get_result() return render_template( 'me.html', current_time=datetime.utcnow(), current_user=current_employee, - sent_loves=sent_love.get_result(), - received_loves=received_love.get_result() + sent_grouped_loves=loveapp.logic.love.cluster_loves_by_time(sent_love), + received_grouped_loves=loveapp.logic.love.cluster_loves_by_time(received_love), ) @@ -106,14 +105,12 @@ def single_company_value(company_value_id): current_employee = Employee.get_current_employee() loves = loveapp.logic.love.recent_loves_by_company_value(None, company_value.id, limit=100).get_result() - loves_list_one, loves_list_two = format_loves(loves) return render_template( 'values.html', current_time=datetime.utcnow(), current_user=current_employee, - loves_first_list=loves_list_one, - loves_second_list=loves_list_two, + grouped_loves=loveapp.logic.love.cluster_loves_by_time(loves), values=get_company_value_link_pairs(), company_value_string=company_value.display_string ) @@ -126,7 +123,7 @@ def company_values(): abort(404) loves = loveapp.logic.love.recent_loves_with_any_company_value(None, limit=100).get_result() - loves_list_one, loves_list_two = format_loves(loves) + grouped_loves = loveapp.logic.love.cluster_loves_by_time(loves) current_employee = Employee.get_current_employee() @@ -134,8 +131,7 @@ def company_values(): 'values.html', current_time=datetime.utcnow(), current_user=current_employee, - loves_first_list=loves_list_one, - loves_second_list=loves_list_two, + grouped_loves=grouped_loves, values=get_company_value_link_pairs(), company_value_string=None ) @@ -192,14 +188,14 @@ def explore(): flash('Sorry, "{}" is not a valid user.'.format(username), 'error') return redirect(url_for('web_app.explore')) - sent_love = loveapp.logic.love.recent_sent_love(user_key, include_secret=False, limit=20) - received_love = loveapp.logic.love.recent_received_love(user_key, include_secret=False, limit=20) + sent_love = loveapp.logic.love.recent_sent_love(user_key, include_secret=False, limit=20).get_result() + received_love = loveapp.logic.love.recent_received_love(user_key, include_secret=False, limit=200).get_result() return render_template( 'explore.html', current_time=datetime.utcnow(), - sent_loves=sent_love.get_result(), - received_loves=received_love.get_result(), + received_grouped_loves=loveapp.logic.love.cluster_loves_by_time(received_love), + sent_grouped_loves=loveapp.logic.love.cluster_loves_by_time(sent_love), user=user_key.get() ) From bb81824b5d084794bed45914fd59bae78e65e4ab Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Tue, 13 May 2025 15:17:52 -0700 Subject: [PATCH 05/29] cleanup plus make tests pass --- loveapp/logic/love.py | 21 ++++++++++++------- loveapp/themes/default/templates/explore.html | 9 ++++---- loveapp/themes/default/templates/keys.html | 1 - loveapp/themes/default/templates/me.html | 9 ++++---- .../default/templates/parts/love_message.html | 21 ------------------- loveapp/themes/default/templates/values.html | 1 - loveapp/util/formatting.py | 12 ----------- loveapp/views/web.py | 16 ++++++++------ tests/util/formatting_test.py | 13 ------------ tests/views/web_test.py | 9 ++++---- 10 files changed, 36 insertions(+), 76 deletions(-) delete mode 100644 loveapp/themes/default/templates/parts/love_message.html delete mode 100644 loveapp/util/formatting.py delete mode 100644 tests/util/formatting_test.py diff --git a/loveapp/logic/love.py b/loveapp/logic/love.py index c058d58..1081b73 100644 --- a/loveapp/logic/love.py +++ b/loveapp/logic/love.py @@ -76,18 +76,23 @@ def cluster_loves_by_time(loves, time_window_days=1): for cluster in clustered_groups: newest_time = max(love.seconds_since_epoch for love in cluster) - # Get all unique senders and recipients from this cluster - unique_sender_keys = set() - unique_recipient_keys = set() + # Use ordered data structures to preserve the order of appearance + unique_sender_keys = [] + unique_recipient_keys = [] - # First pass - collect unique keys + # First pass - collect unique keys while preserving order for love in cluster: - unique_sender_keys.add(love.sender_key) - unique_recipient_keys.add(love.recipient_key) + # Add sender key if not already in the list + if love.sender_key not in unique_sender_keys and love.sender_key: + unique_sender_keys.insert(0, love.sender_key) + + # Add recipient key if not already in the list + if love.recipient_key not in unique_recipient_keys and love.recipient_key: + unique_recipient_keys.insert(0, love.recipient_key) # Second pass - get the actual objects - senders = [key.get() for key in unique_sender_keys if key] - recipients = [key.get() for key in unique_recipient_keys if key] + senders = [key.get() for key in unique_sender_keys] + recipients = [key.get() for key in unique_recipient_keys] results.append({ 'content': cluster[0].message, diff --git a/loveapp/themes/default/templates/explore.html b/loveapp/themes/default/templates/explore.html index 2132fe6..4ba7090 100644 --- a/loveapp/themes/default/templates/explore.html +++ b/loveapp/themes/default/templates/explore.html @@ -1,6 +1,5 @@ {% extends theme("layout.html") %} -{% import theme("parts/love_message.html") as love_message %} {% import theme("parts/grouped_love_message.html") as grouped_love_message %} " {% import theme("parts/photobox.html") as photobox %} {% import theme("parts/flash.html") as flash %} @@ -37,9 +36,9 @@

    {{ user.full_name }}

    Love Sent

    - {% if sent_grouped_loves %} + {% if grouped_sent_loves %}
    - {% for grouped_love in sent_grouped_loves %} + {% for grouped_love in grouped_sent_loves %} {{ grouped_love_message.love( "To", grouped_love.recipients, @@ -57,9 +56,9 @@

    Love Sent

    Love Received

    - {% if received_grouped_loves %} + {% if grouped_received_loves %}
    - {% for grouped_love in received_grouped_loves %} + {% for grouped_love in grouped_received_loves %} {{ grouped_love_message.love( "From", grouped_love.senders, diff --git a/loveapp/themes/default/templates/keys.html b/loveapp/themes/default/templates/keys.html index 9f64d96..4260d5d 100644 --- a/loveapp/themes/default/templates/keys.html +++ b/loveapp/themes/default/templates/keys.html @@ -1,6 +1,5 @@ {% extends theme("layout.html") %} -{% import theme("parts/love_message.html") as love_message %} {% import theme("parts/flash.html") as flash %} {% block title %}API Keys{% endblock %} diff --git a/loveapp/themes/default/templates/me.html b/loveapp/themes/default/templates/me.html index fd1bca4..7eb8446 100644 --- a/loveapp/themes/default/templates/me.html +++ b/loveapp/themes/default/templates/me.html @@ -1,6 +1,5 @@ {% extends theme("layout.html") %} -{% import theme("parts/love_message.html") as love_message %} {% import theme("parts/grouped_love_message.html") as grouped_love_message %} {% block title %}My Love{% endblock %} @@ -9,9 +8,9 @@

    Love Received

    - {% if received_grouped_loves %} + {% if grouped_received_loves %}
    - {% for grouped_love in received_grouped_loves %} + {% for grouped_love in grouped_received_loves %} {{ grouped_love_message.love( "From", grouped_love.senders, @@ -31,9 +30,9 @@

    Love Received

    Love Sent

    - {% if sent_grouped_loves %} + {% if grouped_sent_loves %}
    - {% for grouped_love in sent_grouped_loves %} + {% for grouped_love in grouped_sent_loves %} {{ grouped_love_message.love( "To", grouped_love.recipients, diff --git a/loveapp/themes/default/templates/parts/love_message.html b/loveapp/themes/default/templates/parts/love_message.html deleted file mode 100644 index a19b28f..0000000 --- a/loveapp/themes/default/templates/parts/love_message.html +++ /dev/null @@ -1,21 +0,0 @@ -{% import theme("parts/photobox.html") as photobox %} - -{% macro love(sender_name, sender_username, recipient_name, recipient_username, icon_user, message, ts, secret) %} -
    - {{ photobox.user_icon(icon_user) }} -
    - {{ message|linkify_company_values }} - {% if secret %} - SECRET - {% endif %} -
    - -
    -{% endmacro %} diff --git a/loveapp/themes/default/templates/values.html b/loveapp/themes/default/templates/values.html index 9906fd0..21b8aba 100644 --- a/loveapp/themes/default/templates/values.html +++ b/loveapp/themes/default/templates/values.html @@ -1,6 +1,5 @@ {% extends theme("layout.html") %} -{% import theme("parts/love_message.html") as love_message %} {% import theme("parts/grouped_love_message.html") as grouped_love_message %} diff --git a/loveapp/util/formatting.py b/loveapp/util/formatting.py deleted file mode 100644 index fad52fa..0000000 --- a/loveapp/util/formatting.py +++ /dev/null @@ -1,12 +0,0 @@ -def format_loves(loves): - # organise loves into two roughly equal lists for displaying - if len(loves) < 20: - loves_list_one = loves - loves_list_two = [] - else: - loves_list_one = loves[:len(loves)//2] - loves_list_two = loves[len(loves)//2:] - - if len(loves_list_one) < len(loves_list_two): - loves_list_one.append(loves_list_two.pop()) - return loves_list_one, loves_list_two diff --git a/loveapp/views/web.py b/loveapp/views/web.py index 60a7ab3..d2abb35 100644 --- a/loveapp/views/web.py +++ b/loveapp/views/web.py @@ -35,7 +35,6 @@ from loveapp.util.decorators import admin_required from loveapp.util.decorators import csrf_protect from loveapp.util.decorators import user_required -from loveapp.util.formatting import format_loves from loveapp.util.recipient import sanitize_recipients from loveapp.util.render import make_json_response from loveapp.util.render import render_template @@ -67,14 +66,16 @@ def me(): current_employee = Employee.get_current_employee() sent_love = loveapp.logic.love.recent_sent_love(current_employee.key, limit=200).get_result() + grouped_sent_love = loveapp.logic.love.cluster_loves_by_time(sent_love) received_love = loveapp.logic.love.recent_received_love(current_employee.key, limit=200).get_result() + grouped_received_love = loveapp.logic.love.cluster_loves_by_time(received_love) return render_template( 'me.html', current_time=datetime.utcnow(), current_user=current_employee, - sent_grouped_loves=loveapp.logic.love.cluster_loves_by_time(sent_love), - received_grouped_loves=loveapp.logic.love.cluster_loves_by_time(received_love), + grouped_sent_loves=grouped_sent_love, + grouped_received_loves=grouped_received_love ) @@ -105,12 +106,13 @@ def single_company_value(company_value_id): current_employee = Employee.get_current_employee() loves = loveapp.logic.love.recent_loves_by_company_value(None, company_value.id, limit=100).get_result() + grouped_loves = loveapp.logic.love.cluster_loves_by_time(loves) return render_template( 'values.html', current_time=datetime.utcnow(), current_user=current_employee, - grouped_loves=loveapp.logic.love.cluster_loves_by_time(loves), + grouped_loves=grouped_loves, values=get_company_value_link_pairs(), company_value_string=company_value.display_string ) @@ -189,13 +191,15 @@ def explore(): return redirect(url_for('web_app.explore')) sent_love = loveapp.logic.love.recent_sent_love(user_key, include_secret=False, limit=20).get_result() + grouped_sent_love = loveapp.logic.love.cluster_loves_by_time(sent_love) received_love = loveapp.logic.love.recent_received_love(user_key, include_secret=False, limit=200).get_result() + grouped_received_love = loveapp.logic.love.cluster_loves_by_time(received_love) return render_template( 'explore.html', current_time=datetime.utcnow(), - received_grouped_loves=loveapp.logic.love.cluster_loves_by_time(received_love), - sent_grouped_loves=loveapp.logic.love.cluster_loves_by_time(sent_love), + grouped_received_loves=grouped_received_love, + grouped_sent_loves=grouped_sent_love, user=user_key.get() ) diff --git a/tests/util/formatting_test.py b/tests/util/formatting_test.py deleted file mode 100644 index 1b11c41..0000000 --- a/tests/util/formatting_test.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -import pytest - -from loveapp.util.formatting import format_loves - - -@pytest.mark.parametrize('loves, expected', [ - ([], ([], [])), - (list(range(5)), (list(range(5)), [])), - (list(range(31)), (list(range(15)) + [30], list(range(15, 30)))), -]) -def test_format_loves(loves, expected): - assert format_loves(loves) == expected diff --git a/tests/views/web_test.py b/tests/views/web_test.py index e9fe00d..b6907a1 100644 --- a/tests/views/web_test.py +++ b/tests/views/web_test.py @@ -279,9 +279,9 @@ def test_me(self, client, recorded_templates): assert response_context['current_time'] is not None assert response_context['current_user'] == self.logged_in_employee - assert response_context['sent_loves'] == [] + assert response_context['grouped_sent_loves'] == [] assert 'Give and ye shall receive!' in response.data.decode() - assert response_context['received_loves'] == [] + assert response_context['grouped_received_loves'] == [] assert 'You haven\'t sent any love yet.' in response.data.decode() def test_me_with_loves(self, client, recorded_templates): @@ -299,9 +299,10 @@ def test_me_with_loves(self, client, recorded_templates): response = client.get('/me') _, response_context = recorded_templates[0] - assert response_context['sent_loves'] == [sent_love] + assert response_context['grouped_sent_loves'][0]["content"] == sent_love.message + assert response_context['grouped_received_loves'][0]["content"] == received_love.message + assert 'Well done.' in response.data.decode() - assert response_context['received_loves'] == [received_love] assert 'Awesome work.' in response.data.decode() dude.key.delete() From 2fca154c4a12856eef83a28d3d12af9d06f1c1cc Mon Sep 17 00:00:00 2001 From: Billy Montgomery Date: Tue, 13 May 2025 15:20:23 -0700 Subject: [PATCH 06/29] change tags to emote --- loveapp/config-example.py | 8 ++-- loveapp/models/love.py | 39 ++++--------------- loveapp/themes/default/templates/explore.html | 4 +- loveapp/themes/default/templates/me.html | 4 +- .../default/templates/parts/love_message.html | 6 +-- 5 files changed, 18 insertions(+), 43 deletions(-) diff --git a/loveapp/config-example.py b/loveapp/config-example.py index c3b5e08..da0e797 100644 --- a/loveapp/config-example.py +++ b/loveapp/config-example.py @@ -54,8 +54,8 @@ CompanyValue('DUST_IN_THE_WIND', 'All we are is dust in the wind, dude.', ('woah', 'whoa', 'DustInTheWind')) ] -MESSAGE_TAG_CONFIG = { - # Work anniversary message string add a πŸ₯³ to the sender's avatar to highlight work anniversary shout outs - # Set to None to disable - "work_anniversary_tag_substring": "happy workiversary", +# Messages that find any the test in this list will render the emoji for the sender's avatar +MESSAGE_EMOTES = { + "happy workiversary": "πŸ₯³", + "happy birthday": "πŸŽ‰", } \ No newline at end of file diff --git a/loveapp/models/love.py b/loveapp/models/love.py index aefe1c8..a860723 100644 --- a/loveapp/models/love.py +++ b/loveapp/models/love.py @@ -22,35 +22,10 @@ def seconds_since_epoch(self): return int(mktime(self.timestamp.timetuple())) @property - def tags(self): - return self._tags - - @tags.setter - def tags(self, value): - self._tags = value - - def __init__(self, *args, **kwargs): - super(Love, self).__init__(*args, **kwargs) - self._tags = [] - - # Initialize tags based on message content - self._init_tags() - - def _init_tags(self): - """Initialize tags based on message content.""" - if self.message and self._is_work_anniversary_message(self.message): - self._add_tag('work_anniversary') - - def _add_tag(self, tag): - """Helper method to append a tag to the tags list.""" - if tag not in self._tags: - self._tags.append(tag) - - def _is_work_anniversary_message(self, message: str) -> bool: - """Helper function to check if a message contains work anniversary text.""" - work_anniversary_finder = config.MESSAGE_TAG_CONFIG.get("work_anniversary_tag_substring") - if work_anniversary_finder is None: - self._tags = [] - return False - - return bool(message and work_anniversary_finder in message.lower()) \ No newline at end of file + def emote(self) -> str | None: + message = self.message + for message_substring, emoji in config.MESSAGE_EMOTES.items(): + if message and message_substring.lower() in message.lower(): + return emoji + + return None \ No newline at end of file diff --git a/loveapp/themes/default/templates/explore.html b/loveapp/themes/default/templates/explore.html index e4d6600..c7fcc6c 100644 --- a/loveapp/themes/default/templates/explore.html +++ b/loveapp/themes/default/templates/explore.html @@ -50,7 +50,7 @@

    Love Sent

    love.message, love.seconds_since_epoch, love.secret, - tags + love.emote ) }} {% endfor %} @@ -77,7 +77,7 @@

    Love Received

    love.message, love.seconds_since_epoch, love.secret, - love.tags + love.emote ) }} {% endfor %} diff --git a/loveapp/themes/default/templates/me.html b/loveapp/themes/default/templates/me.html index 9785326..e032098 100644 --- a/loveapp/themes/default/templates/me.html +++ b/loveapp/themes/default/templates/me.html @@ -22,7 +22,7 @@

    Love Received

    love.message, love.seconds_since_epoch, love.secret, - love.tags + love.emote ) }} {% endfor %} @@ -51,7 +51,7 @@

    Love Sent

    love.message, love.seconds_since_epoch, love.secret, - love.tags + love.emote ) }} {% endfor %} diff --git a/loveapp/themes/default/templates/parts/love_message.html b/loveapp/themes/default/templates/parts/love_message.html index 81ffb24..08e07d2 100644 --- a/loveapp/themes/default/templates/parts/love_message.html +++ b/loveapp/themes/default/templates/parts/love_message.html @@ -1,10 +1,10 @@ {% import theme("parts/photobox.html") as photobox %} -{% macro love(sender_name, sender_username, recipient_name, recipient_username, icon_user, message, ts, secret, tags) %} +{% macro love(sender_name, sender_username, recipient_name, recipient_username, icon_user, message, ts, secret, emote) %}
    {{ photobox.user_icon(icon_user) }} - {% if 'work_anniversary' in tags %} -
    πŸ₯³
    + {% if emote %} +
    {{ emote }}
    {% endif %}
    {{ message|linkify_company_values }} From 83ee0d2280dee423373f737874471520357a3881 Mon Sep 17 00:00:00 2001 From: Billy Montgomery Date: Tue, 13 May 2025 15:32:33 -0700 Subject: [PATCH 07/29] fix MESSAGE_EMOTES comments --- loveapp/config-example.py | 5 +++-- loveapp/views/web.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/loveapp/config-example.py b/loveapp/config-example.py index da0e797..e5d3bbd 100644 --- a/loveapp/config-example.py +++ b/loveapp/config-example.py @@ -54,8 +54,9 @@ CompanyValue('DUST_IN_THE_WIND', 'All we are is dust in the wind, dude.', ('woah', 'whoa', 'DustInTheWind')) ] -# Messages that find any the test in this list will render the emoji for the sender's avatar +# Highlight a user's avatar when their message contains a word or phrase that matches one of the options. +# If a match is found, the emoji you provide will be added on the user's avatar. MESSAGE_EMOTES = { "happy workiversary": "πŸ₯³", "happy birthday": "πŸŽ‰", -} \ No newline at end of file +} diff --git a/loveapp/views/web.py b/loveapp/views/web.py index a4dd5f3..3c52b8c 100644 --- a/loveapp/views/web.py +++ b/loveapp/views/web.py @@ -436,4 +436,4 @@ def import_employees_form(): def import_employees(): flash('We started importing employee data in the background. Refresh the page to see it.', 'info') taskqueue.add(url='/tasks/employees/load/csv') - return redirect(url_for('web_app.employees')) \ No newline at end of file + return redirect(url_for('web_app.employees')) From 6450262634ed3a93b6b32a6728fab44784d81ea1 Mon Sep 17 00:00:00 2001 From: Billy Montgomery Date: Tue, 13 May 2025 15:48:06 -0700 Subject: [PATCH 08/29] add test --- tests/models/love_test.py | 129 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 tests/models/love_test.py diff --git a/tests/models/love_test.py b/tests/models/love_test.py new file mode 100644 index 0000000..a37c181 --- /dev/null +++ b/tests/models/love_test.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +from time import mktime + +import mock +import pytest +from google.appengine.ext import ndb + +from loveapp.models import Love +from testing.factories import create_employee, create_love + + +def test_create_love(gae_testbed): + """Test creating a basic Love instance""" + sender = create_employee(username='sender') + recipient = create_employee(username='recipient') + + love = create_love( + sender_key=sender.key, + recipient_key=recipient.key, + message="Great job on the project!", + secret=False + ) + + assert love.sender_key == sender.key + assert love.recipient_key == recipient.key + assert love.message == "Great job on the project!" + assert not love.secret + assert isinstance(love.timestamp, datetime) + assert love.company_values == [] + + +def test_seconds_since_epoch(gae_testbed): + """Test the seconds_since_epoch property""" + sender = create_employee(username='sender') + recipient = create_employee(username='recipient') + + love = create_love( + sender_key=sender.key, + recipient_key=recipient.key, + message="Test message" + ) + + expected_seconds = int(mktime(love.timestamp.timetuple())) + assert love.seconds_since_epoch == expected_seconds + + +@mock.patch('loveapp.models.love.config') +def test_emote_with_matching_message(mock_love_config, gae_testbed): + """Test emote property returns correct emoji when message matches""" + sender = create_employee(username='sender') + recipient = create_employee(username='recipient') + + mock_love_config.MESSAGE_EMOTES = { + 'great': 'πŸ‘', + 'awesome': 'πŸŽ‰' + } + + love = create_love( + sender_key=sender.key, + recipient_key=recipient.key, + message="You did a GREAT job!" + ) + + assert love.emote == 'πŸ‘' + + +@mock.patch('loveapp.models.love.config') +def test_emote_with_no_matching_message(mock_love_config, gae_testbed): + """Test emote property returns None when no message matches""" + sender = create_employee(username='sender') + recipient = create_employee(username='recipient') + + mock_love_config.MESSAGE_EMOTES = { + 'great': 'πŸ‘', + 'awesome': 'πŸŽ‰' + } + + love = create_love( + sender_key=sender.key, + recipient_key=recipient.key, + message="You did a good job!" + ) + + assert love.emote is None + + +def test_emote_with_none_message(gae_testbed): + """Test emote property handles None message""" + sender = create_employee(username='sender') + recipient = create_employee(username='recipient') + + love = create_love( + sender_key=sender.key, + recipient_key=recipient.key, + message=None + ) + + assert love.emote is None + + +def test_love_with_company_values(gae_testbed): + """Test creating Love with company values""" + sender = create_employee(username='sender') + recipient = create_employee(username='recipient') + company_values = ['Integrity', 'Innovation'] + + love = create_love( + sender_key=sender.key, + recipient_key=recipient.key, + message="Great demonstration of our values!", + company_values=company_values + ) + + assert love.company_values == company_values + + +def test_secret_love_default_value(gae_testbed): + """Test that secret defaults to False""" + sender = create_employee(username='sender') + recipient = create_employee(username='recipient') + + love = create_love( + sender_key=sender.key, + recipient_key=recipient.key, + message="Test message" + ) + + assert not love.secret \ No newline at end of file From 200ac1a9681f16e189dcc884ea003ad930e36924 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Tue, 13 May 2025 16:24:12 -0700 Subject: [PATCH 09/29] tests for clustering --- tests/logic/cluster_loves_test.py | 174 ++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 tests/logic/cluster_loves_test.py diff --git a/tests/logic/cluster_loves_test.py b/tests/logic/cluster_loves_test.py new file mode 100644 index 0000000..5738cb4 --- /dev/null +++ b/tests/logic/cluster_loves_test.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +import unittest +from datetime import datetime, timedelta + +import mock +import pytest + +from loveapp.logic.love import cluster_loves_by_time +from loveapp.models import Love +from testing.factories import create_employee + + +class MockLove: + """A mock Love object for testing cluster_loves_by_time.""" + def __init__(self, message, timestamp, secret=False, sender_key=None, recipient_key=None): + self.message = message + self.timestamp = timestamp + self.seconds_since_epoch = int(timestamp.timestamp()) + self.secret = secret + self.sender_key = sender_key + self.recipient_key = recipient_key + + +@pytest.mark.usefixtures('gae_testbed') +class TestClusterLoves(unittest.TestCase): + def setUp(self): + self.alice = create_employee(username='alice') + self.bob = create_employee(username='bob') + self.carol = create_employee(username='carol') + self.dave = create_employee(username='dave') + + # Base timestamp for creating love objects + self.base_time = datetime.now() + + def test_basic_content_clustering(self): + """Test that loves with identical content are clustered together.""" + # Create loves with identical content but different timestamps + loves = [ + MockLove("Great job!", self.base_time, sender_key=self.alice.key, recipient_key=self.bob.key), + MockLove("Great job!", self.base_time + timedelta(minutes=30), sender_key=self.carol.key, recipient_key=self.dave.key), + ] + + clusters = cluster_loves_by_time(loves) + + # Should have one cluster with both loves + self.assertEqual(len(clusters), 1) + self.assertEqual(clusters[0]['content'], "Great job!") + self.assertEqual(clusters[0]['sender_count'], 2) + self.assertEqual(clusters[0]['recipient_count'], 2) + + def test_secret_status_separation(self): + """Test that loves are separated by secret status.""" + + # Create loves with identical content but different secret status + loves = [ + MockLove("Great job!", self.base_time, secret=False, sender_key=self.alice.key, recipient_key=self.bob.key), + MockLove("Great job!", self.base_time + timedelta(minutes=30), secret=True, sender_key=self.carol.key, recipient_key=self.dave.key), + ] + + clusters = cluster_loves_by_time(loves) + + # Should have two clusters, one secret and one not + self.assertEqual(len(clusters), 2) + # Check that we have one secret and one non-secret cluster + secret_clusters = [c for c in clusters if c['is_secret']] + non_secret_clusters = [c for c in clusters if not c['is_secret']] + self.assertEqual(len(secret_clusters), 1) + self.assertEqual(len(non_secret_clusters), 1) + + def test_temporal_clustering(self): + """Test that loves are clustered by time window.""" + # Create loves with identical content but timestamps outside the default window + loves = [ + MockLove("Great job!", self.base_time, sender_key=self.alice.key, recipient_key=self.bob.key), + # This one is 2 days later, should be in a separate cluster with default window of 1 day + MockLove("Great job!", self.base_time + timedelta(days=2), sender_key=self.carol.key, recipient_key=self.dave.key), + ] + + clusters = cluster_loves_by_time(loves) + + # Should have two clusters due to time separation + self.assertEqual(len(clusters), 2) + + # Test with custom time window that would include both + clusters_with_larger_window = cluster_loves_by_time(loves, time_window_days=3) + self.assertEqual(len(clusters_with_larger_window), 1) + + def test_sender_recipient_aggregation(self): + """Test that unique senders and recipients are properly aggregated.""" + # Create loves with same content but overlapping senders and recipients + loves = [ + # Alice loves Bob + MockLove("Great team!", self.base_time, sender_key=self.alice.key, recipient_key=self.bob.key), + # Alice loves Carol too + MockLove("Great team!", self.base_time + timedelta(minutes=10), sender_key=self.alice.key, recipient_key=self.carol.key), + # Dave also loves Bob + MockLove("Great team!", self.base_time + timedelta(minutes=20), sender_key=self.dave.key, recipient_key=self.bob.key), + ] + + clusters = cluster_loves_by_time(loves) + + # Should have one cluster with 2 unique senders and 2 unique recipients + self.assertEqual(len(clusters), 1) + self.assertEqual(clusters[0]['sender_count'], 2) # Alice and Dave + self.assertEqual(clusters[0]['recipient_count'], 2) # Bob and Carol + + def test_timestamp_sorting(self): + """Test that clusters are sorted by most recent timestamp.""" + # Create different loves with different timestamps + loves = [ + # Older message + MockLove("Good work!", self.base_time - timedelta(days=1), sender_key=self.alice.key, recipient_key=self.bob.key), + # Newer message + MockLove("Great job!", self.base_time, sender_key=self.carol.key, recipient_key=self.dave.key), + ] + + clusters = cluster_loves_by_time(loves) + + # Should have two clusters, with the newer one first + self.assertEqual(len(clusters), 2) + self.assertEqual(clusters[0]['content'], "Great job!") + self.assertEqual(clusters[1]['content'], "Good work!") + + def test_complex_clustering(self): + """Test a complex scenario with multiple clustering criteria.""" + # Create various loves with different properties + loves = [ + + # Same content but 2 days later (separate cluster) + MockLove("Great job!", self.base_time + timedelta(days=2), sender_key=self.alice.key, recipient_key=self.carol.key), + + # Same content but secret + MockLove("Great job!", self.base_time + timedelta(hours=3), secret=True, sender_key=self.dave.key, recipient_key=self.alice.key), + + # Same content, same day + MockLove("Great job!", self.base_time, sender_key=self.alice.key, recipient_key=self.bob.key), + MockLove("Great job!", self.base_time + timedelta(hours=2), sender_key=self.carol.key, recipient_key=self.dave.key), + + # Different content + MockLove("Nice presentation!", self.base_time + timedelta(hours=1), sender_key=self.bob.key, recipient_key=self.alice.key) + + ] + + clusters = cluster_loves_by_time(loves) + + # pretty sure this is wrong. + # Should have 4 clusters: + # 1. "Great job!" (non-secret, recent) + # 2. "Great job!" (secret) + # 3. "Nice presentation!" + # 4. "Great job!" (non-secret, older) + self.assertEqual(len(clusters), 4) + print(clusters) + + # Verify they're in the right order (most recent first) + self.assertEqual(clusters[0]['content'], "Great job!") + self.assertFalse(clusters[0]['is_secret']) # The recent non-secret one + self.assertEqual(clusters[0]['sender_count'], 1) # Alice + + self.assertEqual(clusters[1]['content'], "Great job!") + self.assertTrue(clusters[1]['is_secret']) + self.assertEqual(clusters[1]['sender_count'], 1) # Dave + + self.assertEqual(clusters[2]['content'], "Great job!") # The older one + self.assertFalse(clusters[2]['is_secret']) + self.assertEqual(clusters[2]['sender_count'], 2) # Alice and Carol + + self.assertEqual(clusters[3]['content'], "Nice presentation!") + self.assertFalse(clusters[3]['is_secret']) + self.assertEqual(clusters[3]['sender_count'], 1) # Bob + + + + From 44679ada956b6d7d592bf3cd75d84e18675155f6 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Wed, 14 May 2025 13:28:57 -0700 Subject: [PATCH 10/29] improve clustering performance and add a config for clustering threshold --- loveapp/config-example.py | 5 +++++ loveapp/logic/love.py | 33 ++++++++++++--------------------- loveapp/views/web.py | 24 ++++++++++++------------ tests/views/web_test.py | 2 ++ 4 files changed, 31 insertions(+), 33 deletions(-) diff --git a/loveapp/config-example.py b/loveapp/config-example.py index e5d3bbd..21d0bca 100644 --- a/loveapp/config-example.py +++ b/loveapp/config-example.py @@ -60,3 +60,8 @@ "happy workiversary": "πŸ₯³", "happy birthday": "πŸŽ‰", } + +# Temporaral separation of clustered loves. +# i.e., if two loves have the same content but are sent more than this time apart, +# they will be considered separate clusters. +LOVE_CLUSTERING_TIME_WINDOW_DAYS = 7 diff --git a/loveapp/logic/love.py b/loveapp/logic/love.py index 4d50070..ad7f907 100644 --- a/loveapp/logic/love.py +++ b/loveapp/logic/love.py @@ -28,35 +28,26 @@ def _love_query(start_dt, end_dt, include_secret): query = query.filter(Love.secret == False) # noqa return query -def cluster_loves_by_time(loves, time_window_days=1): - # Phase 1: strict group by content +def cluster_loves_by_time(loves, time_window_days=None): + time_window_days = time_window_days or config.LOVE_CLUSTERING_TIME_WINDOW_DAYS + # Phase 1: group by content and secrecy groups = defaultdict(list) for love in loves: - groups[love.message.strip().lower()].append(love) - - # Phase 2: further group by secret status - secret_groups = {} - for content, content_loves in groups.items(): - # Split into secret and non-secret groups - secret_loves = [love for love in content_loves if love.secret] - non_secret_loves = [love for love in content_loves if not love.secret] - - # Only add groups that have at least one love - if secret_loves: - secret_groups[(content, True)] = secret_loves - if non_secret_loves: - secret_groups[(content, False)] = non_secret_loves - - # Phase 3: within each group, temporal clustering + # key is a combo of message and secret status, since secret loves + # shouldn't be grouped with non-secret ones + key = love.message.strip().lower() + f"_secret={love.secret}" + groups[key].append(love) + + # Phase 2: within each group, do temporal clustering clustered_groups = [] threshold = timedelta(days=time_window_days) - for (content, is_secret), content_loves in secret_groups.items(): + for loves in groups.values(): # Sort by timestamp - content_loves.sort(key=lambda l: l.timestamp) + loves.sort(key=lambda l: l.timestamp) current_cluster = [] last_time = None - for love in content_loves: + for love in loves: if not current_cluster: current_cluster.append(love) last_time = love.timestamp diff --git a/loveapp/views/web.py b/loveapp/views/web.py index d2abb35..8c2f799 100644 --- a/loveapp/views/web.py +++ b/loveapp/views/web.py @@ -65,10 +65,10 @@ def home(): def me(): current_employee = Employee.get_current_employee() - sent_love = loveapp.logic.love.recent_sent_love(current_employee.key, limit=200).get_result() - grouped_sent_love = loveapp.logic.love.cluster_loves_by_time(sent_love) - received_love = loveapp.logic.love.recent_received_love(current_employee.key, limit=200).get_result() - grouped_received_love = loveapp.logic.love.cluster_loves_by_time(received_love) + sent_love = loveapp.logic.love.recent_sent_love(current_employee.key, limit=1000).get_result() + grouped_sent_love = loveapp.logic.love.cluster_loves_by_time(sent_love)[:20] + received_love = loveapp.logic.love.recent_received_love(current_employee.key, limit=1000).get_result() + grouped_received_love = loveapp.logic.love.cluster_loves_by_time(received_love)[:20] return render_template( 'me.html', @@ -105,8 +105,8 @@ def single_company_value(company_value_id): current_employee = Employee.get_current_employee() - loves = loveapp.logic.love.recent_loves_by_company_value(None, company_value.id, limit=100).get_result() - grouped_loves = loveapp.logic.love.cluster_loves_by_time(loves) + loves = loveapp.logic.love.recent_loves_by_company_value(None, company_value.id, limit=1000).get_result() + grouped_loves = loveapp.logic.love.cluster_loves_by_time(loves)[:100] return render_template( 'values.html', @@ -124,8 +124,8 @@ def company_values(): if not config.COMPANY_VALUES: abort(404) - loves = loveapp.logic.love.recent_loves_with_any_company_value(None, limit=100).get_result() - grouped_loves = loveapp.logic.love.cluster_loves_by_time(loves) + loves = loveapp.logic.love.recent_loves_with_any_company_value(None, limit=1000).get_result() + grouped_loves = loveapp.logic.love.cluster_loves_by_time(loves)[:100] current_employee = Employee.get_current_employee() @@ -190,10 +190,10 @@ def explore(): flash('Sorry, "{}" is not a valid user.'.format(username), 'error') return redirect(url_for('web_app.explore')) - sent_love = loveapp.logic.love.recent_sent_love(user_key, include_secret=False, limit=20).get_result() - grouped_sent_love = loveapp.logic.love.cluster_loves_by_time(sent_love) - received_love = loveapp.logic.love.recent_received_love(user_key, include_secret=False, limit=200).get_result() - grouped_received_love = loveapp.logic.love.cluster_loves_by_time(received_love) + sent_love = loveapp.logic.love.recent_sent_love(user_key, limit=1000, include_secret=False).get_result() + grouped_sent_love = loveapp.logic.love.cluster_loves_by_time(sent_love)[:20] + received_love = loveapp.logic.love.recent_received_love(user_key, limit=1000, include_secret=False).get_result() + grouped_received_love = loveapp.logic.love.cluster_loves_by_time(received_love)[:20] return render_template( 'explore.html', diff --git a/tests/views/web_test.py b/tests/views/web_test.py index b6907a1..ce6030b 100644 --- a/tests/views/web_test.py +++ b/tests/views/web_test.py @@ -584,6 +584,7 @@ def test_single_value_page(self, mock_util_config, mock_logic_config, client, re CompanyValue('AWESOME', 'awesome', ['awesome']), CompanyValue('COOL', 'cool', ['cool']) ] + mock_logic_config.LOVE_CLUSTERING_TIME_WINDOW_DAYS = mock_util_config.LOVE_CLUSTERING_TIME_WINDOW_DAYS = 1 response = client.get('/value/cool') template, response_context = recorded_templates[0] @@ -606,6 +607,7 @@ def test_all_values_page(self, mock_util_config, mock_logic_config, client, reco CompanyValue('AWESOME', 'awesome', ['awesome']), CompanyValue('COOL', 'cool', ['cool']) ] + mock_logic_config.LOVE_CLUSTERING_TIME_WINDOW_DAYS = mock_util_config.LOVE_CLUSTERING_TIME_WINDOW_DAYS = 1 response = client.get('/values') template, response_context = recorded_templates[0] From 27c5dd9606ddba0058065bc07fb8d25300678526 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Wed, 14 May 2025 14:22:13 -0700 Subject: [PATCH 11/29] pagination --- loveapp/themes/default/static/css/style.css | 35 ++++++++++++++++++++- loveapp/themes/default/templates/me.html | 28 +++++++++++++++++ loveapp/views/web.py | 29 +++++++++++++---- 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/loveapp/themes/default/static/css/style.css b/loveapp/themes/default/static/css/style.css index d392a0d..7946758 100644 --- a/loveapp/themes/default/static/css/style.css +++ b/loveapp/themes/default/static/css/style.css @@ -374,4 +374,37 @@ h4 { break-inside: avoid; margin-bottom: 1.5em; display: block; -} \ No newline at end of file +} + +.pagination { + display: flex; + justify-content: center; + gap: 2px; + align-items: center; + } + .pagination a, .pagination span { + padding: 6px 12px; + margin: 0 2px; + border-radius: 3px; + text-decoration: none; + color: #555; + background: #fff; + font-size: 1em; + } + .pagination a:hover { + background: #f0f0f0; + } + .pagination span.active { + background: #e00707; + color: #fff; + border-color: #e00707; + font-weight: bold; + pointer-events: none; + } + .pagination span.disabled { + color: #ccc; + border-color: #eee; + background: #f9f9f9; + cursor: not-allowed; + pointer-events: none; + } \ No newline at end of file diff --git a/loveapp/themes/default/templates/me.html b/loveapp/themes/default/templates/me.html index 52a4bbc..912e883 100644 --- a/loveapp/themes/default/templates/me.html +++ b/loveapp/themes/default/templates/me.html @@ -51,6 +51,34 @@

    Love Sent

    {% endif %}
    + +
    + + +
    {% endblock %} {% block javascript %} diff --git a/loveapp/views/web.py b/loveapp/views/web.py index 8c2f799..9728557 100644 --- a/loveapp/views/web.py +++ b/loveapp/views/web.py @@ -63,19 +63,36 @@ def home(): @web_app.route('/me', methods=['GET']) @user_required def me(): + # Get pagination params + page = int(request.args.get('page', 1)) + page_size = 2 + current_employee = Employee.get_current_employee() - sent_love = loveapp.logic.love.recent_sent_love(current_employee.key, limit=1000).get_result() - grouped_sent_love = loveapp.logic.love.cluster_loves_by_time(sent_love)[:20] - received_love = loveapp.logic.love.recent_received_love(current_employee.key, limit=1000).get_result() - grouped_received_love = loveapp.logic.love.cluster_loves_by_time(received_love)[:20] + sent_love = loveapp.logic.love.recent_sent_love(current_employee.key, limit=5000).get_result() + grouped_sent_love = loveapp.logic.love.cluster_loves_by_time(sent_love) + received_love = loveapp.logic.love.recent_received_love(current_employee.key, limit=5000).get_result() + grouped_received_love = loveapp.logic.love.cluster_loves_by_time(received_love) + + total_items = max(len(grouped_sent_love), len(grouped_received_love)) + total_pages = (total_items + page_size - 1) // page_size # Calculate total pages + + # Calculate paged results + page = max(1, min(page, total_pages)) # Ensure page is within bounds + start = (page - 1) * page_size + end = start + page_size + grouped_sent_love_page = grouped_sent_love[start:end] + grouped_received_love_page = grouped_received_love[start:end] + return render_template( 'me.html', current_time=datetime.utcnow(), current_user=current_employee, - grouped_sent_loves=grouped_sent_love, - grouped_received_loves=grouped_received_love + grouped_sent_loves=grouped_sent_love_page, + grouped_received_loves=grouped_received_love_page, + page=page, + total_pages=total_pages, ) From c8fc74f6e02bcfed6e160a932e9bf69b2e94cbaa Mon Sep 17 00:00:00 2001 From: Billy Montgomery Date: Wed, 14 May 2025 14:29:42 -0700 Subject: [PATCH 12/29] add plus one action --- loveapp/themes/default/static/css/style.css | 51 +++++++++++++++++++ .../default/templates/parts/love_message.html | 31 +++++++++++ 2 files changed, 82 insertions(+) diff --git a/loveapp/themes/default/static/css/style.css b/loveapp/themes/default/static/css/style.css index a46e0cc..e008663 100644 --- a/loveapp/themes/default/static/css/style.css +++ b/loveapp/themes/default/static/css/style.css @@ -352,3 +352,54 @@ h4 { -2px 0px 0px #000, -2px -1px 0px #000; } + +.love-plus-one { + background: none; + border: none; + padding: 0; + margin-left: 8px; + cursor: pointer; + font-weight: bold; +} + +.love-plus-one .circle { + display: inline-block; + width: 20px; + height: 18px; + position: relative; + transform: rotate(-45deg); + margin: 2px 4px; + + &:before, + &:after { + content: ''; + width: 18px; + height: 18px; + border-radius: 50%; + background: inherit; + position: absolute; + z-index: -1; + } + + &:before { + top: -10px; + left: 0; + } + + &:after { + top: 0; + right: -10px; + } + background: #ea050b; + color: white; + text-align: center; + font-size: 12px; + transition: transform 0.2s ease-in-out, background 0.2s ease-in-out; + transform: scale(1); +} + +.love-plus-one:hover .circle { + background: #c41200; + transition: transform 0.3s ease-in-out, background 0.3s ease-in-out; + transform: scale(1.1); +} \ No newline at end of file diff --git a/loveapp/themes/default/templates/parts/love_message.html b/loveapp/themes/default/templates/parts/love_message.html index a19b28f..2653614 100644 --- a/loveapp/themes/default/templates/parts/love_message.html +++ b/loveapp/themes/default/templates/parts/love_message.html @@ -16,6 +16,37 @@ loved {% if display_recipient_link %}{% endif %}{{ recipient_name }} {{ ts * 1000 }} +
    + + +
    +
    {% endmacro %} From 6fa2e5affc7bf83dcf6f475824431c8887bb1299 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Wed, 14 May 2025 15:02:51 -0700 Subject: [PATCH 13/29] pagination and tests --- loveapp/themes/default/templates/me.html | 6 +- loveapp/views/web.py | 58 +++++++++++------ tests/views/web_test.py | 81 ++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 23 deletions(-) diff --git a/loveapp/themes/default/templates/me.html b/loveapp/themes/default/templates/me.html index 912e883..edae209 100644 --- a/loveapp/themes/default/templates/me.html +++ b/loveapp/themes/default/templates/me.html @@ -56,7 +56,7 @@

    Love Sent

    {% if user_list|length > 1 %} -
    - {% for user in user_list %} - {{ user.full_name }} - {% endfor %} -
    +
    + {% for user in user_list %} + {{ user.full_name }} + {% endfor %} + +
    +
    {% endif %}
    - -
    + + {% endmacro %} From 90ee7ebd2031ff290bf828d55a1a06b7dcaf5250 Mon Sep 17 00:00:00 2001 From: Billy Montgomery Date: Thu, 15 May 2025 11:23:38 -0700 Subject: [PATCH 16/29] make +1 a redirect to the love form --- .../templates/parts/grouped_love_message.html | 44 +++++-------------- 1 file changed, 11 insertions(+), 33 deletions(-) diff --git a/loveapp/themes/default/templates/parts/grouped_love_message.html b/loveapp/themes/default/templates/parts/grouped_love_message.html index 9417971..505ea31 100644 --- a/loveapp/themes/default/templates/parts/grouped_love_message.html +++ b/loveapp/themes/default/templates/parts/grouped_love_message.html @@ -22,43 +22,21 @@
    {{ message|linkify_company_values }} {% if user_list|length > 1 %} -
    - {% for user in user_list %} - {{ user.full_name }} - {% endfor %} - -
    -
    +
    + {% for user in user_list %} + {{ user.full_name }} + {% endfor %} +
    {% endif %}
    - - {% endmacro %} From cb53fc14c0e4ff960e77513783411e832e11e61b Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Thu, 15 May 2025 11:50:39 -0700 Subject: [PATCH 17/29] fixed animation mistiming --- loveapp/themes/default/static/css/style.css | 6 ++++-- loveapp/themes/default/static/js/main.js | 18 ++++++++++++++---- .../templates/parts/grouped_love_message.html | 16 +++++++--------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/loveapp/themes/default/static/css/style.css b/loveapp/themes/default/static/css/style.css index 6790ef2..60cfbc6 100644 --- a/loveapp/themes/default/static/css/style.css +++ b/loveapp/themes/default/static/css/style.css @@ -486,7 +486,6 @@ h4 { left: 0; right: 0; bottom: 0; height: 30px; background: linear-gradient(to bottom, rgba(250,250,251,0), #f8f8f8); - transition: opacity 0.3s; display: none; } .user-list.is-collapsible .user-list-gradient { display: block; } @@ -512,7 +511,8 @@ h4 { .user-list.user-list-expanded .user-list-expander .chevron { transform: rotate(225deg); } - .chevron { + + .user-list-expander .chevron { display: inline-block; width: 0.4em; height: 0.4em; @@ -521,9 +521,11 @@ h4 { border-bottom: 2px solid #888; padding: 3px; margin: 0; + /* default: down */ transform: rotate(45deg); transition: transform 0.2s; } + /* visually hidden text for screen readers */ .sr-only { position: absolute; diff --git a/loveapp/themes/default/static/js/main.js b/loveapp/themes/default/static/js/main.js index 500e7b1..67b9f11 100644 --- a/loveapp/themes/default/static/js/main.js +++ b/loveapp/themes/default/static/js/main.js @@ -206,10 +206,20 @@ function checkUserListCollapsible(userList) { } } -function toggleUserList(button) { - const userList = button.closest('.user-list'); - userList.classList.toggle('user-list-expanded'); - button.setAttribute('aria-expanded', userList.classList.contains('user-list-expanded')); +function toggleUserList(expanderBtn) { + const userList = expanderBtn.closest('.user-list'); + const expanded = userList.classList.toggle('user-list-expanded'); + if (expanded) { + expanderBtn.classList.add('chevron-rotated'); + expanderBtn.setAttribute('aria-expanded', 'true'); + userList.style.maxHeight = ''; // reset for measuring actual height + let fullHeight = userList.scrollHeight; + userList.style.maxHeight = fullHeight + "px"; // animate open! + } else { + expanderBtn.classList.remove('chevron-rotated'); + expanderBtn.setAttribute('aria-expanded', 'false'); + userList.style.maxHeight = ''; // REMOVE so it falls back to CSS (collapsed) height + } } window.addEventListener('DOMContentLoaded', function() { diff --git a/loveapp/themes/default/templates/parts/grouped_love_message.html b/loveapp/themes/default/templates/parts/grouped_love_message.html index 9417971..a039f4d 100644 --- a/loveapp/themes/default/templates/parts/grouped_love_message.html +++ b/loveapp/themes/default/templates/parts/grouped_love_message.html @@ -44,15 +44,13 @@ {{ user.full_name }} {% endfor %}
    From 93921aac32a08eb81d023a1a7251267eb69bf800 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Thu, 15 May 2025 13:16:04 -0700 Subject: [PATCH 18/29] being a little nicer to grouped_love --- loveapp/themes/default/templates/explore.html | 2 + loveapp/themes/default/templates/me.html | 2 + .../templates/parts/grouped_love_message.html | 98 ++++++++++--------- loveapp/themes/default/templates/values.html | 1 + 4 files changed, 57 insertions(+), 46 deletions(-) diff --git a/loveapp/themes/default/templates/explore.html b/loveapp/themes/default/templates/explore.html index 5eed959..878d7f5 100644 --- a/loveapp/themes/default/templates/explore.html +++ b/loveapp/themes/default/templates/explore.html @@ -41,6 +41,7 @@

    Love Sent

    {% for grouped_love in grouped_sent_loves %} {{ grouped_love_message.love( "To", + grouped_love.senders, grouped_love.recipients, grouped_love.content, grouped_love.most_recent_love_timestamp, @@ -63,6 +64,7 @@

    Love Received

    {{ grouped_love_message.love( "From", grouped_love.senders, + grouped_love.recipients, grouped_love.content, grouped_love.most_recent_love_timestamp, grouped_love.is_secret, diff --git a/loveapp/themes/default/templates/me.html b/loveapp/themes/default/templates/me.html index edae209..9048d3a 100644 --- a/loveapp/themes/default/templates/me.html +++ b/loveapp/themes/default/templates/me.html @@ -14,6 +14,7 @@

    Love Received

    {{ grouped_love_message.love( "From", grouped_love.senders, + grouped_love.recipients, grouped_love.content, grouped_love.most_recent_love_timestamp, grouped_love.is_secret, @@ -37,6 +38,7 @@

    Love Sent

    {{ grouped_love_message.love( "To", grouped_love.recipients, + grouped_love.recipients, grouped_love.content, grouped_love.most_recent_love_timestamp, grouped_love.is_secret, diff --git a/loveapp/themes/default/templates/parts/grouped_love_message.html b/loveapp/themes/default/templates/parts/grouped_love_message.html index f95f2e7..8c00c2c 100644 --- a/loveapp/themes/default/templates/parts/grouped_love_message.html +++ b/loveapp/themes/default/templates/parts/grouped_love_message.html @@ -1,52 +1,58 @@ {% import theme("parts/photobox.html") as photobox %} -{% macro love(to_from, user_list, message, ts, secret, emote) %} -
    - {% set first_user = user_list[0] %} - {{ photobox.user_icon(first_user) }} - {% if emote %} -
    {{ emote }}
    - {% endif %} - {% endmacro %} diff --git a/loveapp/themes/default/templates/values.html b/loveapp/themes/default/templates/values.html index 21b8aba..a82f85a 100644 --- a/loveapp/themes/default/templates/values.html +++ b/loveapp/themes/default/templates/values.html @@ -27,6 +27,7 @@

    Who's repping the Values?

    {% for grouped_love in grouped_loves %} {{ grouped_love_message.love( "To", + grouped_love.senders, grouped_love.recipients, grouped_love.content, grouped_love.most_recent_love_timestamp, From 8eea49fe60a344c63acb3ca921b5a98af1a173ca Mon Sep 17 00:00:00 2001 From: Billy Montgomery Date: Thu, 15 May 2025 13:51:45 -0700 Subject: [PATCH 19/29] hide +1 when current user is a sender --- loveapp/themes/default/templates/explore.html | 2 ++ loveapp/themes/default/templates/me.html | 2 ++ .../templates/parts/grouped_love_message.html | 22 ++++++++++--------- loveapp/views/web.py | 4 +++- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/loveapp/themes/default/templates/explore.html b/loveapp/themes/default/templates/explore.html index 878d7f5..0967ed2 100644 --- a/loveapp/themes/default/templates/explore.html +++ b/loveapp/themes/default/templates/explore.html @@ -41,6 +41,7 @@

    Love Sent

    {% for grouped_love in grouped_sent_loves %} {{ grouped_love_message.love( "To", + current_user, grouped_love.senders, grouped_love.recipients, grouped_love.content, @@ -63,6 +64,7 @@

    Love Received

    {% for grouped_love in grouped_received_loves %} {{ grouped_love_message.love( "From", + current_user, grouped_love.senders, grouped_love.recipients, grouped_love.content, diff --git a/loveapp/themes/default/templates/me.html b/loveapp/themes/default/templates/me.html index 9048d3a..5b5b089 100644 --- a/loveapp/themes/default/templates/me.html +++ b/loveapp/themes/default/templates/me.html @@ -13,6 +13,7 @@

    Love Received

    {% for grouped_love in grouped_received_loves %} {{ grouped_love_message.love( "From", + current_user, grouped_love.senders, grouped_love.recipients, grouped_love.content, @@ -37,6 +38,7 @@

    Love Sent

    {% for grouped_love in grouped_sent_loves %} {{ grouped_love_message.love( "To", + current_user, grouped_love.recipients, grouped_love.recipients, grouped_love.content, diff --git a/loveapp/themes/default/templates/parts/grouped_love_message.html b/loveapp/themes/default/templates/parts/grouped_love_message.html index 8c00c2c..f9711e4 100644 --- a/loveapp/themes/default/templates/parts/grouped_love_message.html +++ b/loveapp/themes/default/templates/parts/grouped_love_message.html @@ -1,6 +1,6 @@ {% import theme("parts/photobox.html") as photobox %} -{% macro love(to_from_flag, senders, recipients, message, ts, secret, emote) %} +{% macro love(to_from_flag, current_user, senders, recipients, message, ts, secret, emote) %}
    {% if to_from_flag == "To" %} {% set user_list = recipients %} @@ -27,15 +27,17 @@
    {{ message|linkify_company_values }} - + {% if current_user.username not in senders | map(attribute='username') | list %} + + {% endif %} {% if user_list|length > 1 %}
    {% for user in user_list %} diff --git a/loveapp/views/web.py b/loveapp/views/web.py index dafe530..c596cdd 100644 --- a/loveapp/views/web.py +++ b/loveapp/views/web.py @@ -229,13 +229,15 @@ def explore(): grouped_sent_love = loveapp.logic.love.cluster_loves_by_time(sent_love)[:page_size] received_love = loveapp.logic.love.recent_received_love(user_key, limit=love_lookback_limit, include_secret=False).get_result() grouped_received_love = loveapp.logic.love.cluster_loves_by_time(received_love)[:page_size] + current_user = Employee.get_current_employee() return render_template( 'explore.html', current_time=datetime.utcnow(), grouped_received_loves=grouped_received_love, grouped_sent_loves=grouped_sent_love, - user=user_key.get() + user=user_key.get(), + current_user=current_user, ) From cc41c06d475245c6830ac5eb8afed63df825bd2b Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Thu, 15 May 2025 13:56:25 -0700 Subject: [PATCH 20/29] minor UI adjustment --- loveapp/themes/default/static/css/style.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/loveapp/themes/default/static/css/style.css b/loveapp/themes/default/static/css/style.css index 60cfbc6..93c78c8 100644 --- a/loveapp/themes/default/static/css/style.css +++ b/loveapp/themes/default/static/css/style.css @@ -486,6 +486,7 @@ h4 { left: 0; right: 0; bottom: 0; height: 30px; background: linear-gradient(to bottom, rgba(250,250,251,0), #f8f8f8); + transition: opacity 0.2s; display: none; } .user-list.is-collapsible .user-list-gradient { display: block; } @@ -493,7 +494,7 @@ h4 { .user-list-expander { position: absolute; - right: 8px; /* tweak as needed */ + right: 0px; /* tweak as needed */ bottom: 8px; z-index: 2; background: rgba(255,255,255,0.98); From 11d4ef54246f5dbf247be4f6d39250c8aef81390 Mon Sep 17 00:00:00 2001 From: Billy Montgomery Date: Thu, 15 May 2025 14:07:25 -0700 Subject: [PATCH 21/29] hide +1 when current user is the only recipient --- .../default/templates/parts/grouped_love_message.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/loveapp/themes/default/templates/parts/grouped_love_message.html b/loveapp/themes/default/templates/parts/grouped_love_message.html index f9711e4..50b4e64 100644 --- a/loveapp/themes/default/templates/parts/grouped_love_message.html +++ b/loveapp/themes/default/templates/parts/grouped_love_message.html @@ -27,7 +27,12 @@
    {{ message|linkify_company_values }} - {% if current_user.username not in senders | map(attribute='username') | list %} + {# + Checks if the current user's username is not present in the list of senders' usernames. + This is typically used to determine whether the current user has not sent a message in the grouped love message context. + #} + {% if current_user.username not in senders | map(attribute='username') | list + and not (recipients|length == 1 and recipients[0].username == current_user.username) %}
    Date: Thu, 15 May 2025 14:21:48 -0700 Subject: [PATCH 22/29] forgot to switch recipients for senders in me.html --- loveapp/themes/default/templates/me.html | 2 +- .../themes/default/templates/parts/grouped_love_message.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/loveapp/themes/default/templates/me.html b/loveapp/themes/default/templates/me.html index 5b5b089..335d5dc 100644 --- a/loveapp/themes/default/templates/me.html +++ b/loveapp/themes/default/templates/me.html @@ -39,7 +39,7 @@

    Love Sent

    {{ grouped_love_message.love( "To", current_user, - grouped_love.recipients, + grouped_love.senders, grouped_love.recipients, grouped_love.content, grouped_love.most_recent_love_timestamp, diff --git a/loveapp/themes/default/templates/parts/grouped_love_message.html b/loveapp/themes/default/templates/parts/grouped_love_message.html index 50b4e64..c7ff0d1 100644 --- a/loveapp/themes/default/templates/parts/grouped_love_message.html +++ b/loveapp/themes/default/templates/parts/grouped_love_message.html @@ -31,8 +31,8 @@ Checks if the current user's username is not present in the list of senders' usernames. This is typically used to determine whether the current user has not sent a message in the grouped love message context. #} - {% if current_user.username not in senders | map(attribute='username') | list - and not (recipients|length == 1 and recipients[0].username == current_user.username) %} + {% set sender_usernames = senders | map(attribute='username') | list %} + {% if current_user.username not in sender_usernames and not (recipients|length == 1 and recipients[0].username == current_user.username) %}
    Date: Thu, 15 May 2025 14:40:12 -0700 Subject: [PATCH 23/29] add +1 disabled state --- loveapp/themes/default/static/css/style.css | 56 +++++++++++-------- .../templates/parts/grouped_love_message.html | 28 +++++----- 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/loveapp/themes/default/static/css/style.css b/loveapp/themes/default/static/css/style.css index 93c78c8..74cfc36 100644 --- a/loveapp/themes/default/static/css/style.css +++ b/loveapp/themes/default/static/css/style.css @@ -381,7 +381,9 @@ h4 { margin: -15px -25px 0 0; } -.love-plus-one { +a.love-plus-one, +a.love-plus-one:active { + outline: none; background: none; border: none; padding: 0; @@ -390,6 +392,10 @@ h4 { font-weight: bold; } +.love-plus-one.disabled { + cursor: not-allowed; +} + .love-plus-one .circle { display: inline-block; width: 20px; @@ -397,27 +403,6 @@ h4 { position: relative; transform: rotate(-45deg); margin: 2px 4px; - - &:before, - &:after { - content: ''; - width: 18px; - height: 18px; - border-radius: 50%; - background: inherit; - position: absolute; - z-index: -1; - } - - &:before { - top: -10px; - left: 0; - } - - &:after { - top: 0; - right: -10px; - } background: #ea050b; color: white; text-align: center; @@ -425,8 +410,33 @@ h4 { transition: transform 0.2s ease-in-out, background 0.2s ease-in-out; transform: scale(1); } +.love-plus-one .circle:before, +.love-plus-one .circle:after { + content: ''; + width: 18px; + height: 18px; + border-radius: 50%; + background: inherit; + position: absolute; + z-index: -1; +} +.love-plus-one .circle:before { + top: -10px; + left: 0; +} +.love-plus-one .circle:after { + top: 0; + right: -10px; +} + +/* Disabled state */ +.love-plus-one.disabled .circle { + background: #aaa; + cursor: default; + pointer-events: none; +} -.love-plus-one:hover .circle { +.love-plus-one:not(.disabled):hover .circle { background: #c41200; transition: transform 0.3s ease-in-out, background 0.3s ease-in-out; transform: scale(1.1); diff --git a/loveapp/themes/default/templates/parts/grouped_love_message.html b/loveapp/themes/default/templates/parts/grouped_love_message.html index c7ff0d1..ef043fa 100644 --- a/loveapp/themes/default/templates/parts/grouped_love_message.html +++ b/loveapp/themes/default/templates/parts/grouped_love_message.html @@ -28,21 +28,21 @@
    {{ message|linkify_company_values }} {# - Checks if the current user's username is not present in the list of senders' usernames. - This is typically used to determine whether the current user has not sent a message in the grouped love message context. + Checks if the current user's username is not present in the list of senders' usernames and they are not the only recipient. + If this is the case, disable the plus one button. #} - {% set sender_usernames = senders | map(attribute='username') | list %} - {% if current_user.username not in sender_usernames and not (recipients|length == 1 and recipients[0].username == current_user.username) %} - - {% endif %} + {% set disable_plus_one = current_user.username in senders | map(attribute='username') | list + or (recipients|length == 1 and recipients[0].username == current_user.username) %} + {% if user_list|length > 1 %}
    {% for user in user_list %} From f533ba9f59076ba7f013f3425491f1da6bab8eda Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Thu, 15 May 2025 15:34:05 -0700 Subject: [PATCH 24/29] tooltips --- loveapp/themes/default/static/css/style.css | 51 ++++++++++++++++++- .../templates/parts/grouped_love_message.html | 27 ++++++++-- loveapp/themes/default/templates/values.html | 1 + 3 files changed, 74 insertions(+), 5 deletions(-) diff --git a/loveapp/themes/default/static/css/style.css b/loveapp/themes/default/static/css/style.css index 74cfc36..616f0dd 100644 --- a/loveapp/themes/default/static/css/style.css +++ b/loveapp/themes/default/static/css/style.css @@ -481,7 +481,7 @@ a.love-plus-one:active { max-height: 45px; transition: max-height 0.4s cubic-bezier(0.4,0,0.2,1); margin-top: 7px; - padding-bottom: 10px; + padding-bottom: 2px; padding-right: 36px; background: #f8f8f8; } @@ -544,4 +544,53 @@ a.love-plus-one:active { padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; + } + + .love-plus-one-container { + position: relative; + display: inline-block; + } + + .love-plus-one .custom-tooltip { + visibility: hidden; + width: 140px; + background-color: #222; + color: #fff; + text-align: center; + border-radius: 0.25rem; + padding: 0.5em; + position: absolute; + z-index: 1000; + /* Position from top right */ + bottom: 100%; /* above the element */ + right: 0; /* align to the element's right edge */ + margin-bottom: 0.5em; /* space between tooltip and element */ + opacity: 0; + pointer-events: none; + transition: opacity 0.15s; + font-size: 0.5em; + box-shadow: 0px 3px 12px rgba(0,0,0,0.15); + } + + .love-plus-one .custom-tooltip::after { + content: ''; + position: absolute; + top: 100%; + right: 0.75em; + border-width: 6px; + border-style: solid; + border-color: #222 transparent transparent transparent; + } + + .love-plus-one:hover .custom-tooltip, + .love-plus-one:focus .custom-tooltip { + visibility: visible; + opacity: 1; + pointer-events: auto; + } + + /* Optionally: show "not-allowed" cursor for disabled state */ + .love-plus-one.disabled { + cursor: not-allowed; + pointer-events: auto; } \ No newline at end of file diff --git a/loveapp/themes/default/templates/parts/grouped_love_message.html b/loveapp/themes/default/templates/parts/grouped_love_message.html index ef043fa..0f53a30 100644 --- a/loveapp/themes/default/templates/parts/grouped_love_message.html +++ b/loveapp/themes/default/templates/parts/grouped_love_message.html @@ -31,16 +31,29 @@ Checks if the current user's username is not present in the list of senders' usernames and they are not the only recipient. If this is the case, disable the plus one button. #} - {% set disable_plus_one = current_user.username in senders | map(attribute='username') | list - or (recipients|length == 1 and recipients[0].username == current_user.username) %} + {% set already_sent_this_love = current_user.username in senders | map(attribute='username') | list %} + {% set already_received_this_love = (recipients|length == 1 and recipients[0].username == current_user.username) %} + {% set tooltip_text = '' %} + {% if already_sent_this_love %} + {% set tooltip_text = "You already sent this!" %} + {% elif already_received_this_love %} + {% set tooltip_text = "You can't send love to yourself!" %} + {% else %} + {% set tooltip_text = "Add your love!" %} + {% endif %} + {% set disable_plus_one = already_sent_this_love or already_received_this_love %} {% if user_list|length > 1 %} @@ -62,4 +75,10 @@ {% endif %}
    + {% endmacro %} diff --git a/loveapp/themes/default/templates/values.html b/loveapp/themes/default/templates/values.html index a82f85a..04b4915 100644 --- a/loveapp/themes/default/templates/values.html +++ b/loveapp/themes/default/templates/values.html @@ -27,6 +27,7 @@

    Who's repping the Values?

    {% for grouped_love in grouped_loves %} {{ grouped_love_message.love( "To", + current_user, grouped_love.senders, grouped_love.recipients, grouped_love.content, From ccc149af7b4372791f1cb401e8473546e9ae69fb Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Thu, 15 May 2025 15:43:20 -0700 Subject: [PATCH 25/29] only populate the love-link if the +1 is not disabled --- .../themes/default/templates/parts/grouped_love_message.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/loveapp/themes/default/templates/parts/grouped_love_message.html b/loveapp/themes/default/templates/parts/grouped_love_message.html index 0f53a30..be400e7 100644 --- a/loveapp/themes/default/templates/parts/grouped_love_message.html +++ b/loveapp/themes/default/templates/parts/grouped_love_message.html @@ -44,12 +44,13 @@ {% set disable_plus_one = already_sent_this_love or already_received_this_love %}
    +1 From bb6b7deab1028e91f2a741ddef4c3b003f92b6e0 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Thu, 15 May 2025 16:00:28 -0700 Subject: [PATCH 26/29] update README.md --- README.md | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 90913dd..9738326 100644 --- a/README.md +++ b/README.md @@ -110,11 +110,67 @@ your favorite packet manager. ### Running the application locally -* Check out the application code: git clone git@github.com:Yelp/love.git -* Follow the [Prepare for deployment](#prepare-for-deployment) section -* Run the app: make run-dev will start both [Yelp Love](http://localhost:8080) as well as the [Admin server](http://localhost:8000) -* Follow the [CSV import](#csv) section to locally import user data -* Make your changes +Check out the application code and cd to the directory +``` +$ git clone git@github.com:Yelp/love.git +$ cd love +``` + +Create the virtual environment and activate it +``` +$ python3 -m venv env +$ source env/bin/activate +``` + +Install the requisite libraries +``` +$ pip install -r requirements-dev.txt +``` + +Make a copy of loveapp/config.py +``` +$ cp loveapp/config-example.py loveapp/config.py +``` + +Make a copy of employees.csv.example +``` +cp import/employees.csv.example import/employees.csv +``` + + +Edit employees.csv to add your own test data +``` +username,first_name,last_name,department,office,photo_url +michael,Michael,Scott,,,https://placehold.co/100x100 +jim,Jim,Halpert,,,https://placehold.co/100x100 +pam,Pamela,Beesly,,,https://placehold.co/100x100 +dwight,Dwight,Schrute,,,https://placehold.co/100x100 +angela,Angela,Martin,,,https://placehold.co/100x100 +ryan,Ryan,Howard,,,https://placehold.co/100x100 +stanley,Stanley,Hudson,,,https://placehold.co/100x100 +kelly,Kelly,Kapoor,,,https://placehold.co/100x100 +oscar,Oscar,Martinez,,,https://placehold.co/100x100 +creed,Creed,Bratton,,,https://placehold.co/100x100 +kevin,Kevin,Malone,,,https://placehold.co/100x100 +toby,Toby,Flenderson,,,https://placehold.co/100x100 +phyllis,Phyllis,Lapin,,,https://placehold.co/100x100 +meredith,Meredith,Palmer,,,https://placehold.co/100x100 +andy,Andy,Bernard,,,https://placehold.co/100x100 +bobvance,Vance,Refrigeration,,,https://placehold.co/100x100 +``` + +Run the application +``` +$ make run-dev +``` + +Go to http://localhost:8080/ and login as michael@example.com (or something matching a username in employees.csv), check the "Sign in as administrator" box. +You'll probably get an error, but that's ok. + +Go to http://localhost:8080/employees/import +click "Import" + +You're done! ## Deployment From fd74eea9ebc55fb45cc9412ec075f64ae4bf887f Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Thu, 15 May 2025 16:02:59 -0700 Subject: [PATCH 27/29] add billyxs to the AUTHORS.md file --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index b121c61..163641c 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -24,3 +24,4 @@ * Matthew Bentley [matthewbentley](https://github.com/matthewbentley) * Kevin Hock [KevinHock](https://github.com/KevinHock) * Duncan Cook [theletterd](https://github.com/theletterd) +* Billy Montgomery [billyxs](https://github.com/billyxs) \ No newline at end of file From 6f3a3eadcc6b28e87fac070415ca8d2cca135d9d Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Thu, 15 May 2025 16:10:11 -0700 Subject: [PATCH 28/29] syncing the authors in README.md with AUTHORS.md --- README.md | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9738326..353fe33 100644 --- a/README.md +++ b/README.md @@ -274,14 +274,25 @@ curl "https://project_id.appspot.com/api/autocomplete?term=ha&api_key=secret" ## Original Authors and Contributors -* [adamrothman](https://github.com/adamrothman) -* [amartinezfonts](https://github.com/amartinezfonts) -* [benasher44](https://github.com/benasher44) -* [jetze](https://github.com/jetze) -* [KurtisFreedland](https://github.com/KurtisFreedland) -* [mesozoic](https://github.com/mesozoic) -* [michalczapko](https://github.com/michalczapko) -* [wuhuwei](https://github.com/wuhuwei) +* Adam Rothman [adamrothman](https://github.com/adamrothman) +* Ben Asher [benasher44](https://github.com/benasher44) +* Andrew Martinez-Fonts [amartinezfonts](https://github.com/amartinezfonts) +* Wei Wu [wuhuwei](https://github.com/wuhuwei) +* Alex Levy [mesozoic](https://github.com/mesozoic) +* Anthony Sottile [asottile](https://github.com/asottile) +* Jenny Lemmnitz [jetze](https://github.com/jetze) +* Kurtis Freedland [KurtisFreedland](https://github.com/KurtisFreedland) +* MichaΕ‚ Czapko [michalczapko](https://github.com/michalczapko) +* Prayag Verma [pra85](https://github.com/pra85) +* Stephen Brennan [brenns10](https://github.com/brenns10) +* Wayne Crasta [waynecrasta](https://github.com/waynecrasta) +* Dennis Coldwell [dencold](https://github.com/dencold) +* Andrew Lau [ajlau](https://github.com/ajlau) +* Alina Rada [transcedentalia](https://github.com/transcedentalia) +* Matthew Bentley [matthewbentley](https://github.com/matthewbentley) +* Kevin Hock [KevinHock](https://github.com/KevinHock) +* Duncan Cook [theletterd](https://github.com/theletterd) +* Billy Montgomery [billyxs](https://github.com/billyxs) For more info check out the [Authors](AUTHORS.md) file. From ba19655a4f3ce4f6bf0b47035ca73e8603b94c27 Mon Sep 17 00:00:00 2001 From: Duncan Cook Date: Sat, 24 May 2025 06:46:27 -0700 Subject: [PATCH 29/29] cleanup and fix a styling issue --- loveapp/themes/default/static/css/style.css | 37 +++++++++----- loveapp/themes/default/static/js/main.js | 4 ++ .../templates/parts/grouped_love_message.html | 51 +++++++++---------- 3 files changed, 53 insertions(+), 39 deletions(-) diff --git a/loveapp/themes/default/static/css/style.css b/loveapp/themes/default/static/css/style.css index 616f0dd..a1e2752 100644 --- a/loveapp/themes/default/static/css/style.css +++ b/loveapp/themes/default/static/css/style.css @@ -47,6 +47,7 @@ h2 { .love-message-container { margin-bottom: 20px; clear: left; + position: relative; } .love-message { @@ -376,11 +377,16 @@ h4 { display: block; } -.love-plus-one-container { - float: right; - margin: -15px -25px 0 0; +.loves-columns .love-message-container { + width: 100%; + box-sizing: border-box; } +.love-message-container .emote { + position: absolute; + top: -10px; left: 25px; + } + a.love-plus-one, a.love-plus-one:active { outline: none; @@ -548,9 +554,11 @@ a.love-plus-one:active { .love-plus-one-container { position: relative; - display: inline-block; + float: right; + margin: -15px -15px 0 0; } - + + .love-plus-one .custom-tooltip { visibility: hidden; width: 140px; @@ -561,10 +569,11 @@ a.love-plus-one:active { padding: 0.5em; position: absolute; z-index: 1000; - /* Position from top right */ - bottom: 100%; /* above the element */ - right: 0; /* align to the element's right edge */ - margin-bottom: 0.5em; /* space between tooltip and element */ + /* Position from the right side */ + top: 50%; /* vertically center */ + left: 100%; /* to the right of the element */ + transform: translateY(-50%); /* perfect vertical centering */ + margin-left: 2em; /* increased spacing to move tooltip further right */ opacity: 0; pointer-events: none; transition: opacity 0.15s; @@ -575,11 +584,12 @@ a.love-plus-one:active { .love-plus-one .custom-tooltip::after { content: ''; position: absolute; - top: 100%; - right: 0.75em; + top: 50%; + right: 100%; /* at the left edge of tooltip */ + transform: translateY(-50%); border-width: 6px; border-style: solid; - border-color: #222 transparent transparent transparent; + border-color: transparent #222 transparent transparent; /* arrow pointing left */ } .love-plus-one:hover .custom-tooltip, @@ -593,4 +603,5 @@ a.love-plus-one:active { .love-plus-one.disabled { cursor: not-allowed; pointer-events: auto; - } \ No newline at end of file + } + diff --git a/loveapp/themes/default/static/js/main.js b/loveapp/themes/default/static/js/main.js index 67b9f11..5e68fca 100644 --- a/loveapp/themes/default/static/js/main.js +++ b/loveapp/themes/default/static/js/main.js @@ -229,6 +229,10 @@ window.addEventListener('resize', function() { document.querySelectorAll('.user-list').forEach(checkUserListCollapsible); }); +var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) +tooltipTriggerList.forEach(function (tooltipTriggerEl) { + new bootstrap.Tooltip(tooltipTriggerEl) +}) $(document).ready(function () { setupDateFormatting(); diff --git a/loveapp/themes/default/templates/parts/grouped_love_message.html b/loveapp/themes/default/templates/parts/grouped_love_message.html index be400e7..55c98ee 100644 --- a/loveapp/themes/default/templates/parts/grouped_love_message.html +++ b/loveapp/themes/default/templates/parts/grouped_love_message.html @@ -1,7 +1,7 @@ {% import theme("parts/photobox.html") as photobox %} {% macro love(to_from_flag, current_user, senders, recipients, message, ts, secret, emote) %} -
    +
    {% if to_from_flag == "To" %} {% set user_list = recipients %} {% else %} @@ -24,9 +24,10 @@ {% if secret %} SECRET {% endif %} -
    -
    - {{ message|linkify_company_values }} +
    + + +
    + +
    + {{ message|linkify_company_values }} + {% if user_list|length > 1 %}
    {% for user in user_list %} @@ -76,10 +80,5 @@ {% endif %}
    - + {% endmacro %}