Merged in feature/user_recommendations_email (pull request #41)

WIP: Feature/user recommendations email
This commit is contained in:
Seth Call 2023-02-14 15:56:01 +00:00
commit 84e3609602
54 changed files with 1344 additions and 133 deletions

1
.ruby-version Normal file
View File

@ -0,0 +1 @@
2.4.1

View File

@ -2,7 +2,7 @@
const showSidePanelContent = () => {
cy.get('[data-testid=profileSidePanel] h4').should('have.text', 'Test User1');
cy.get('[data-testid=profileSidePanel] .modal-body p').within(() => {
cy.get('[data-testid=profileSidePanel] .modal-body p').first().within(() => {
cy.contains('Location: Denver, CO, US')
.and('contain', 'Skill Level: Professional')
.and('contain', 'Joined JamKazam: 08-26-2021')
@ -11,7 +11,7 @@ const showSidePanelContent = () => {
cy.get('.latency-badge').contains('UNKNOWN');
});
cy.get('[data-testid=profileSidePanel] .modal-body').within(() => {
cy.get('[data-testid=profileSidePanel] .modal-body').first().within(() => {
cy.get('[data-testid=biography]').contains('Biography of Test User1');
//instruments
@ -116,7 +116,7 @@ describe('Friends page with data', () => {
cy.viewport('macbook-13');
});
it.only('paginate', () => {
it('paginate', () => {
cy.get('[data-testid=peopleListTable] > tbody tr').should('have.length', 10);
cy.wait('@getPeople_page2')
cy.get('[data-testid=paginate-next-page]').click();
@ -139,9 +139,10 @@ describe('Friends page with data', () => {
it('click profile name', () => {
//open side panel by clicking name
cy.get('[data-testid=peopleListTable]').within(() => {
cy.contains('Test User1').click();
});
cy.get('[data-testid=peopleListTable]').find('.person-link').first().within(() => {
cy.contains("Test User1").click()
})
//cy.get('[data-testid=peopleListTable]').find('.person-link').first().click()
showSidePanelContent();
closeSidePanel();
});
@ -260,12 +261,13 @@ describe('Friends page with data', () => {
//it.skip('click message button', () => {});
it('paginate', () => {
it.skip('paginate', () => {
cy.get('[data-testid=peopleSwiper] .swiper-button-prev').should('have.class', 'swiper-button-disabled');
for (let i = 0; i < 19; i++) {
cy.get('[data-testid=peopleSwiper] .swiper-button-next').click();
cy.wait(500);
}
cy.wait(500);
cy.get('[data-testid=peopleSwiper] .swiper-button-next').should('have.class', 'swiper-button-disabled');
});
@ -341,11 +343,11 @@ describe('Friends page with data', () => {
cy.contains('Send a message').should('exist');
});
it('is disabled for non friends', () => {
it('is not disabled for non friends', () => {
cy.get('[data-testid=peopleListTable] > tbody tr')
.eq(1)
.find('[data-testid=message]')
.should('be.disabled');
.should('not.be.disabled');
//cy.contains('You can message this user once you are friends').should('exist')
});
@ -446,6 +448,31 @@ describe('Friends page with data', () => {
});
});
describe('coming from email links', () => {
it.only("opens details sidebar", () => {
cy.visit('/friends?open=details&id=1');
showSidePanelContent();
});
it.only("opens chat window", () => {
cy.visit('/friends?open=message&id=1');
cy.get('[data-testid=textMessageModal]')
.should('be.visible')
cy.contains('Send Message to Test User1').should('exist');
});
it.only("sends friend request", () => {
cy.intercept('GET', /\S+\/profile\S+/, { fixture: 'person' });
cy.intercept('POST', /\S+\/friend_requests/, { statusCode: 201, body: { ok: true } });
cy.visit('/friends?open=connect&id=1');
cy.get('[data-testid=profileSidePanel]')
.find('[data-testid=connect]')
.should('be.disabled');
cy.contains('Success! Your friend request has been sent to Test User1.');
});
})
describe('filter', () => {
const fillFilterForm = () => {
//cy.get('[data-testid=btnUpdateSearch]').click();
@ -529,7 +556,7 @@ describe('Friends page with data', () => {
cy.get('[data-testid=btnUpdateSearch]').click();
cy.wait(1000);
cy.get('[data-testid=btnSubmitSearch]').click();
//wait for stubbed request sent by submitting search form without filling any form field
cy.wait('@getPeople_page1')
.then(interception => {
@ -580,4 +607,6 @@ describe('Friends page with data', () => {
});
});
});

View File

@ -0,0 +1,23 @@
/// <reference types="cypress" />
describe('Unsubscribe from email link', () => {
beforeEach(() =>{
cy.intercept('POST', /\S+\/unsubscribe_user_match\/\S+/, { statusCode: 200, body: { ok: true } });
})
it("redirect to home page if tok is not provided", () => {
cy.visit('/unsubscribe');
cy.location('pathname').should('eq', '/');
});
it.only("show unsubscribed message", () => {
cy.visit('/unsubscribe?tok=123');
cy.location('search')
.should('equal', '?tok=123')
.then((s) => new URLSearchParams(s))
.invoke('get', 'tok')
.should('equal', '123')
cy.contains("successfully unsubscribed")
});
})

View File

@ -26,6 +26,7 @@ import JKPrivacy from '../page/JKPrivacy';
import JKPeopleFilter from '../page/JKPeopleFilter';
import JKNotifications from '../page/JKNotifications';
import JKMessageModal from '../profile/JKMessageModal';
import JKUnsubscribe from '../page/JKUnsubscribe';
//import loadable from '@loadable/component';
//const DashboardRoutes = loadable(() => import('../../layouts/JKDashboardRoutes'));
@ -174,6 +175,7 @@ function JKDashboardMain() {
<Route path="/" exact component={HomePage} />
<Route path="/privacy" component={JKPrivacy} />
<Route path="/help" component={JKHelp} />
<Route path="/unsubscribe" exact component={JKUnsubscribe} />
<PrivateRoute path="/friends" component={JKPeopleFilter} />
<PrivateRoute path="/notifications" component={JKNotifications} />
{/*Redirect*/}

View File

@ -22,7 +22,6 @@ const JKPeople = ({ className, onPageChange }) => {
useEffect(() => {
try {
console.log("DEBUG======", page, hasNext);
onPageChange(page, hasNext)
} catch (error) {
console.log('Error fetching people', error);

View File

@ -26,8 +26,6 @@ const JKPeopleList = ({ people, goNextPage, hasNext, isLoading }) => {
))}
</tbody>
</Table>
{hasNext && (
<Button color="primary" outline={true} onClick={() => goNextPage()} disabled={isLoading} data-testid="paginate-next-page">

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Row, Col } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@ -8,6 +8,7 @@ import { fetchPerson } from '../../store/features/peopleSlice';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { useResponsive } from '@farfetch/react-context-responsive';
import { useBrowserQuery } from '../../context/BrowserQuery';
import JKProfileSidePanel from '../profile/JKProfileSidePanel';
import JKProfileAvatar from '../profile/JKProfileAvatar';
@ -28,22 +29,36 @@ const JKPerson = props => {
const { greaterThan } = useResponsive()
const toggleMoreDetails = async e => {
e.preventDefault();
if(e)
e.preventDefault();
try {
await dispatch(fetchPerson({ userId: id })).unwrap();
} catch (error) {
console.log(error);
}
setShowSidePanel(prev => !prev);
};
const queryString = useBrowserQuery();
useEffect(() => {
const openWin = queryString.get('open');
const userId = queryString.get('id')
//showing user more details if directly reqested to do so
//by query string params (coming from weekly new user match email link)
if(openWin && userId && userId === id){
if(openWin === 'details' || openWin === 'connect'){
toggleMoreDetails()
}
}
}, [])
return (
<>
{greaterThan.sm ? (
<tr className="align-middle" key={`people-list-item-${id}`}>
<td className="text-nowrap">
<a href="/#" onClick={toggleMoreDetails} className="d-flex align-items-center mb-1 fs-0">
<a href="/#" onClick={toggleMoreDetails} className="d-flex align-items-center mb-1 fs-0 person-link">
<div className="avatar avatar-xl">
<JKProfileAvatar url={photo_url} />
</div>

View File

@ -0,0 +1,65 @@
import React, { useEffect, useState } from 'react';
import { Card, CardBody, CardText, CardTitle } from 'reactstrap';
import { useTranslation } from "react-i18next";
import { useBrowserQuery } from '../../context/BrowserQuery';
import { useHistory } from "react-router-dom";
const unsubscribeFromNewUsersWeeklyEmail = (token) => {
const baseUrl = process.env.REACT_APP_LEGACY_BASE_URL
return new Promise((resolve, reject) => {
fetch(`${baseUrl}/unsubscribe_user_match/${token}`,
{ method: 'POST' }
).then(response => {
if (response.ok) {
resolve(response);
} else {
reject(response)
}
})
})
}
function JKUnsubscribe() {
const {t} = useTranslation()
const queryObj = useBrowserQuery();
const history = useHistory()
const [success, setSuccess] = useState(false)
useEffect(() => {
const token = queryObj.get('tok')
if(token){
unsubscribeFromNewUsersWeeklyEmail(token)
.then((resp) => {
if(resp.ok){
setSuccess(true)
}
})
.catch(error => console.error(error))
}else{
history.push('/')
}
}, [])
return (
<Card color={ success ? 'success' : 'light' } style={{ width: '25rem', margin: '2rem auto' }}>
<CardBody>
<CardTitle className="mb-2">Unsubscribe From Weekly Email</CardTitle>
<CardText>
{
success?
'You have successfully unsubscribed from weekly emails on newly joined musicians having low internet latency to you.' :
'Unsubscribing...'
}
</CardText>
</CardBody>
</Card>
)
}
export default JKUnsubscribe

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { addFriend as connect, removeFriend as disconnect } from '../../helpers/rest';
import { toast } from 'react-toastify';
import { Modal, ModalBody, ModalHeader, ModalFooter, Button } from 'reactstrap';
import { useBrowserQuery } from '../../context/BrowserQuery';
const JKConnectButton = props => {
const { user, currentUser, addContent, removeContent, cssClasses } = props;
@ -15,6 +16,21 @@ const JKConnectButton = props => {
setPendingFriendRequest(user.pending_friend_request);
}, [user]);
const queryString = useBrowserQuery();
useEffect(() => {
const openWin = queryString.get('open');
const userId = queryString.get('id')
//sending friend request if directly reqested to do so
//by query string params (coming from weekly new user match email link)
if(openWin && userId && userId === user.id && !user.isFriend && !user.pending_friend_request){
if(openWin === 'connect'){
addFriend();
}
}
}, [])
const addFriend = () => {
setShowConfirmModal(!showConfirmModal);
setPendingFriendRequest(true);

View File

@ -1,21 +1,38 @@
import React, { useState, useEffect } from 'react';
import { Button, Tooltip } from "reactstrap";
import JKMessageModal from './JKMessageModal';
import { useBrowserQuery } from '../../context/BrowserQuery';
const JKMessageButton = props => {
const { currentUser, user, cssClasses, children, size, color, outline } = props;
const [showModal, setShowModal] = useState(false);
const [isFriend, setIsFriend] = useState(false);
//const [isFriend, setIsFriend] = useState(false);
const [pendingFriendRequest, setPendingFriendRequest] = useState(false);
const [tooltipOpen, setTooltipOpen] = useState(false);
const toggleTooltip = () => setTooltipOpen(!tooltipOpen);
useEffect(() => {
setIsFriend(user.is_friend);
//setIsFriend(user.is_friend);
setPendingFriendRequest(user.pending_friend_request);
}, [user]);
const queryString = useBrowserQuery();
useEffect(() => {
const openWin = queryString.get('open');
const userId = queryString.get('id')
//openning chat window if directly reqested to do so
//by query string params (coming from weekly new user match email link)
if(openWin && userId && userId === user.id){
if(openWin === 'message'){
setShowModal(!showModal)
}
}
}, [])
return (
<>
<JKMessageModal show={showModal} setShow={setShowModal} user={user} currentUser={currentUser} />
@ -27,7 +44,7 @@ const JKMessageButton = props => {
outline={outline}
className={cssClasses}
data-testid="message"
disabled={!isFriend}
//disabled={!isFriend}
>
{children}
</Button>
@ -37,9 +54,10 @@ const JKMessageButton = props => {
target={"text-message-user-" + user.id}
toggle={toggleTooltip}
>
{
{/* {
isFriend ? 'Send a message' : 'You can message this user once you are friends'
}
} */}
Send a message
</Tooltip>
</>
);

View File

@ -0,0 +1,21 @@
import React from 'react'
import { useLocation } from "react-router-dom";
const BrowserQueryContext = React.createContext(null)
export const BrowserQueryProvider = ({children}) => {
function useQuery() {
return new URLSearchParams(useLocation().search);
}
const queryObj = useQuery();
return(
<BrowserQueryContext.Provider value={ queryObj }>
{ children }
</BrowserQueryContext.Provider>
)
}
export const useBrowserQuery = () => React.useContext(BrowserQueryContext)

View File

@ -128,4 +128,4 @@ export const deleteNotification = (userId, notificationId) => {
.then(response => resolve(response))
.catch(error => reject(error))
})
}
}

View File

@ -5,11 +5,13 @@ import commonTranslationsEN from './locales/en/common.json'
import homeTranslationsEN from './locales/en/home.json'
import peopleTranslationsEN from './locales/en/people.json'
import authTranslationsEN from './locales/en/auth.json'
import unsubscribeTranslationsEN from './locales/en/unsubscribe.json'
import commonTranslationsES from './locales/es/common.json'
import homeTranslationsES from './locales/es/home.json'
import peopleTranslationsES from './locales/es/people.json'
import authTranslationsES from './locales/es/auth.json'
import unsubscribeTranslationsES from './locales/es/unsubscribe.json'
i18n.use(initReactI18next).init({
fallbackLng: 'en',
@ -20,14 +22,16 @@ i18n.use(initReactI18next).init({
common: commonTranslationsEN,
home: homeTranslationsEN,
people: peopleTranslationsEN,
auth: authTranslationsEN
auth: authTranslationsEN,
unsubscribe: unsubscribeTranslationsEN,
},
es: {
//translations: require('./locales/es/translations.json')
common: commonTranslationsES,
home: homeTranslationsES,
people: peopleTranslationsES,
auth: authTranslationsES
auth: authTranslationsES,
unsubscribe: unsubscribeTranslationsES,
}
},
//ns: ['translations'],

View File

@ -0,0 +1,3 @@
{
"page_title": "Unsubscribe"
}

View File

@ -0,0 +1,3 @@
{
"page_title": "Unsubscribe"
}

View File

@ -1,8 +1,9 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, createContext } from 'react';
import PropTypes from 'prop-types';
import DashboardMain from '../components/dashboard/JKDashboardMain';
import UserAuth from '../context/UserAuth';
import { BrowserQueryProvider } from '../context/BrowserQuery';
const DashboardLayout = ({ location }) => {
useEffect(() => {
@ -11,7 +12,9 @@ const DashboardLayout = ({ location }) => {
return (
<UserAuth path={location.pathname}>
<DashboardMain />
<BrowserQueryProvider>
<DashboardMain />
</BrowserQueryProvider>
</UserAuth>
);
};

View File

@ -31,6 +31,7 @@ end
gem 'activerecord', '= 4.2.8'
gem 'railties', '= 4.2.8'
gem 'actionmailer', '= 4.2.8'
gem 'actionview', '= 4.2.8'
gem 'rails-observers', '0.1.2'
gem 'protected_attributes' # needed to support attr_accessible

View File

@ -87,7 +87,6 @@ gem 'sendgrid_toolkit', '>= 1.1.1'
gem 'stripe'
gem 'zip-codes'
gem 'elasticsearch'
gem 'logging', '1.7.2'

View File

@ -0,0 +1,11 @@
class UserMatchEmailSendingsDefaultSentUserIds < ActiveRecord::Migration
def self.up
execute("ALTER TABLE public.user_match_email_sendings
ALTER COLUMN sent_user_ids SET DEFAULT array[]::varchar[];")
end
def self.down
execute("ALTER TABLE public.user_match_email_sendings
ALTER COLUMN sent_user_ids DROP DEFAULT;")
end
end

View File

@ -0,0 +1,9 @@
class AddSubscribeEmailForUserMatch < ActiveRecord::Migration
def self.up
execute("ALTER TABLE users ADD COLUMN subscribe_email_for_user_match BOOLEAN; UPDATE users SET subscribe_email_for_user_match = TRUE;")
end
def self.down
execute("ALTER TABLE users DROP COLUMN subscribe_email_for_user_match;")
end
end

View File

@ -0,0 +1,18 @@
class CreateUserMatchEmailSendings < ActiveRecord::Migration
def self.up
execute(<<-SQL
CREATE TABLE public.user_match_email_sendings (
id character varying(64) DEFAULT public.uuid_generate_v4() PRIMARY KEY NOT NULL,
sent_user_ids text,
total_recipients integer,
created_at timestamp without time zone DEFAULT now() NOT NULL,
completed_at timestamp without time zone
);
SQL
)
end
def self.down
execute("DROP TABLE public.user_match_email_sendings")
end
end

View File

@ -0,0 +1,9 @@
class AddUserMatchEmailSentAt < ActiveRecord::Migration
def self.up
execute("ALTER TABLE users ADD COLUMN user_match_email_sent_at timestamp without time zone;")
end
def self.down
execute("ALTER TABLE users DROP COLUMN user_match_email_sent_at;")
end
end

View File

@ -0,0 +1,11 @@
class AddFailCountAndExceptionDetailToUserMatchEmailSendings < ActiveRecord::Migration
def self.up
execute("ALTER TABLE public.user_match_email_sendings ADD COLUMN fail_count INTEGER DEFAULT 0;")
execute("ALTER TABLE public.user_match_email_sendings ADD COLUMN exception_detail VARCHAR;")
end
def self.down
execute("ALTER TABLE public.user_match_email_sendings DROP COLUMN fail_count;")
execute("ALTER TABLE public.user_match_email_sendings DROP COLUMN exception_detail;")
end
end

View File

@ -30,6 +30,7 @@ require 'tzinfo'
require 'stripe'
require 'zip-codes'
require 'email_validator'
require 'action_view'
ActiveRecord::Base.raise_in_transactional_callbacks = true
require "jam_ruby/lib/timezone"
@ -73,6 +74,7 @@ require "jam_ruby/resque/scheduled/hourly_job"
require "jam_ruby/resque/scheduled/minutely_job"
require "jam_ruby/resque/scheduled/daily_session_emailer"
require "jam_ruby/resque/scheduled/new_musician_emailer"
require "jam_ruby/resque/scheduled/new_musician_match_emailer"
require "jam_ruby/resque/scheduled/music_session_reminder"
require "jam_ruby/resque/scheduled/music_session_scheduler"
require "jam_ruby/resque/scheduled/active_music_session_cleaner"
@ -116,6 +118,8 @@ require "jam_ruby/lib/desk_multipass"
require "jam_ruby/lib/ip"
require "jam_ruby/lib/subscription_message"
require "jam_ruby/lib/stats.rb"
require "jam_ruby/lib/email_new_musician_match"
require "jam_ruby/lib/musician_filter"
require "jam_ruby/amqp/amqp_connection_manager"
require "jam_ruby/database"
require "jam_ruby/message_factory"
@ -339,6 +343,7 @@ require "jam_ruby/models/mobile_recording_upload"
require "jam_ruby/models/temp_token"
require "jam_ruby/models/ad_campaign"
require "jam_ruby/models/user_asset"
require "jam_ruby/models/user_match_email_sending"
include Jampb

View File

@ -3,4 +3,30 @@ module MailerHelper
@vars = Hash.new unless @vars
@vars[arg1] = arg2[0]
end
def latency_info(latency_data)
latency_scores = {
good: { label: 'GOOD', min: 0, max: 40 },
fair: { label: 'FAIR', min: 40, max: 60 },
high: { label: 'HIGH', min: 60, max: 10000000 },
me: { label: 'ME', min: -1, max: -1 },
unknown: { label: 'UNKNOWN', min: -2, max: -2 }
}
total_latency = latency_data[:ars_internet_latency].round + latency_data[:audio_latency].round;
lbl = if (total_latency >= latency_scores[:good][:min] && total_latency <= latency_scores[:good][:max])
latency_scores[:good][:label]
elsif (total_latency > latency_scores[:fair][:min] && total_latency <= latency_scores[:fair][:max])
latency_scores[:fair][:label]
elsif (total_latency > latency_scores[:fair][:min] && total_latency <= latency_scores[:fair][:max])
latency_scores[:fair][:label]
elsif (total_latency > latency_scores[:high][:min])
latency_scores[:high][:label]
else
latency_scores[:unknown][:label]
end
lbl
end
end

View File

@ -10,6 +10,8 @@ module JamRuby
include SendGrid
include MailerHelper
helper MailerHelper
layout "user_mailer"
DEFAULT_SENDER = "JamKazam <noreply@jamkazam.com>"
@ -399,6 +401,23 @@ module JamRuby
end
end
def new_musicians_match(user, musicians_data)
@user, @musicians_data = user, musicians_data
@instrument_proficiencies = {
'1': 'Beginner',
'2': 'Intermediate',
'3': 'Expert'
}
sendgrid_recipients([user.email])
sendgrid_substitute('@USERID', [user.id])
sendgrid_unique_args :type => "new_musicians_match"
mail(:to => user.email, :subject => EmailNewMusicianMatch.subject) do |format|
format.text
format.html { render layout: "user_mailer_beta" }
end
end
#################################### NOTIFICATION EMAILS ####################################
def friend_request(user, msg, friend_request_id)
return if !user.subscribe_email

View File

@ -0,0 +1,113 @@
<style>
.container{
width: 65%;
margin: 0 auto;
padding: 2em;
background-color: #fff;
font-size: 1.2rem;
}
.logo{
display: flex;
justify-content: center;
margin-bottom: 1em;
}
.search-btn{
display: flex;
justify-content: center;
}
.search-btn a{
color: #fff;
background-color: #2c7be5;
border-color: #2c7be5;
border: 1px solid transparent;
padding: 0.3125rem 1rem;
line-height: 2.5;
font-size: 1em;
border-radius: 0.25rem;
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.row {
display: flex;
align-items: stretch;
justify-content: flex-start;
row-gap: 1em;
column-gap: 1em;
margin-top: 2em;
}
.row > div{
flex-grow: 1;
}
.row .photo{
flex-grow: 1;
}
.row .details{
flex-grow: 2;
}
.row .instruments{
flex-grow: 2;
}
.row .links{
flex-grow: 1;
}
</style>
<section class="container">
<div class="logo">
<img src="<%= image_url("JK_Logo_blue-2021.png", host: APP_CONFIG.action_mailer.assets_host ) -%>" alt="JamKazam Logo" />
</div>
<p>
Hi <%= @user.first_name -%>,
</p>
<p>
The following musicians have joined JamKazam within the last week and have low internet
latency to you that will support enjoyable sessions. If you'd like to make more musical connections,
we encourage you to use the links below to send these new users a welcome message and
perhaps arrange a session to play together.
</p>
<%
@musicians_data.each do | data | -%>
<%
musicians = data[:musicians]
latencies = data[:latencies]
musicians.each do |musician|
latency = latencies.find{|l| l[:user_id] == musician.id }
-%>
<div class="row">
<div class="photo">
<%= image_tag musician.photo_url.blank?? "avatar.png" : musician.photo_url, height: '32', width: '32', host: APP_CONFIG.action_mailer.assets_host -%>
</div>
<div class="details">
<div><strong><%= musician.first_name %> <%= musician.last_name %></strong></div>
<div>Latency To You: <%= latency_info(latency) %></div>
<% if musician.last_active_timestamp -%>
<div>Last Active On: <%= time_ago_in_words(Time.at(musician.last_active_timestamp)) %> ago</div>
<% end -%>
</div>
<div class="instruments">
<% musician.musician_instruments.each do |mi| -%>
<div>
<%= mi.description %>:&nbsp;<%= @instrument_proficiencies[mi.proficiency_level.to_s.to_sym] %>
</div>
<% end -%>
</div>
<div class="links">
<div><a href="<%= APP_CONFIG.spa_origin -%>/friends?id=<%= musician.id %>&open=details" target="_blank">View Profile</a></div>
<div><a href="<%= APP_CONFIG.spa_origin -%>/friends?id=<%= musician.id %>&open=message" target="_blank">Send Message</a></div>
<div><a href="<%= APP_CONFIG.spa_origin -%>/friends?id=<%= musician.id %>&open=connect" target="_blank">Send Friend Request</a></div>
</div>
</div>
<% end -%>
<% end -%>
<br />
<p>
To find great musical matches across the entire JamKazam commiunity and make new connections, use the button below to access our musician search feature.
This let you filter JamKazammers by latency, instruments, skill level, genre interests, last active day and more.
</p>
<br />
<div class="search-btn">
<a class="button" href="<%= APP_CONFIG.spa_origin -%>/friends" target="_blank">Search JamKazam Musicians</a>
</div>
</section>

View File

@ -0,0 +1,32 @@
<% if !@user.anonymous? %>
Hi <%= @user.first_name %>,
<% end %>
The following musicians have joined JamKazam within the last week and have low internet latency to you that will support enjoyable sessions. If you'd like to make more musical connections, we encourage you to use the links below to send these new users a welcome message and perhaps arrange a session to play together.
<%
@musicians_data.each do | data | -%>
<%
musicians = data[:musicians]
latencies = data[:latencies]
musicians.each do |musician|
latency = latencies.find{|l| l[:user_id] == musician.id }
-%>
<%= musician.first_name %> <%= musician.last_name %>
Latency To You: <%= latency_info(latency) %>
<% if musician.last_active_timestamp -%>
Last Active On: <%= time_ago_in_words(Time.at(musician.last_active_timestamp)) %> ago
<% end -%>
<% musician.musician_instruments.each do |mi| -%>
<%= mi.description %> (<%= @instrument_proficiencies[mi.proficiency_level.to_s.to_sym] %>)
<% end -%>
View Profile: <%= APP_CONFIG.spa_origin -%>/friends?id=<%= musician.id %>&open=details
Send Message: <%= APP_CONFIG.spa_origin -%>/friends?id=<%= musician.id %>&open=message
Send Friend Request: <%= APP_CONFIG.spa_origin -%>/friends?id=<%= musician.id %>&open=connect
<% end -%>
<% end -%>
To find great musical matches across the entire JamKazam commiunity and make new connections, use the link below to access our musician search feature. This let you filter JamKazammers by latency, instruments, skill level, genre interests, last active day and more.
Search JamKazam Musicians: <%= APP_CONFIG.spa_origin -%>/friends

View File

@ -0,0 +1,38 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>JamKazam</title>
<style>
body{
margin-top:10px;
font-family:Arial, Helvetica, sans-serif;
}
p {
margin-bottom:0px;
line-height:140%;
}
footer{
text-align: center;
padding-top: 1em;
color: #5e6e82;
}
</style>
</head>
<body bgcolor="#eee" >
<%= yield -%>
<body>
<footer>
<p>
Copyright &copy; <%= Time.now.year %> JamKazam, Inc. All rights reserved.
</p>
<p style="text-decoration:none;">
You are receiving this email because you created a JamKazam account with the email address <%= @user.email -%>
</p>
<p><a href="<%= APP_CONFIG.spa_origin -%>/unsubscribe?tok=<%= @user.unsubscribe_token %>">Unsubscribe from this weekly new musician notification</a></p>
</footer>
</html>

View File

@ -0,0 +1,11 @@
<% if @batch_body %>
<%= Nokogiri::HTML(@batch_body).text %>
<% else %>
<%= yield %>
<% end %>
<% unless @user.nil? || @suppress_user_has_account_footer == true %>
This email was sent to you because you have an account at JamKazam / https://www.jamkazam.com. To unsubscribe: https://www.jamkazam.com/unsubscribe/<%=@user.unsubscribe_token%>.
<% end %>
Copyright <%= Time.now.year %> JamKazam, Inc. All rights reserved.

View File

@ -0,0 +1,99 @@
module JamRuby
class EmailNewMusicianMatch
PER_PAGE = 150
LIMIT = 20
JOINED_WITHIN_DAYS = 7
ACTIVE_WITHIN_DAYS = 30
PRIORITY_RECIPIENTS = %w(seth@jamkazam.com david@jamkazam.com peter@jamkazam.com nuwan@jamkazam.com).freeze
def self.subject
"New musicians with good Internet connections to you have joined JamKazam!"
end
def self.send_new_musicians
params = {
latency_good: true,
latency_fair: true,
latency_high: false,
proficiency_beginner: true,
proficiency_intermediate: true,
proficiency_expert: true,
from_location: false,
joined_within_days: JOINED_WITHIN_DAYS,
active_within_days: ACTIVE_WITHIN_DAYS,
limit: PER_PAGE
}
email_sending = UserMatchEmailSending.most_recent
if email_sending.nil? || email_sending.completed?
email_sending = UserMatchEmailSending.create
end
begin
recipients = User.where("users.subscribe_email = ? AND
users.subscribe_email_for_user_match = ?
AND NOT COALESCE(users.user_match_email_sent_at, ?) > ?",
true, true, 7.days.ago, 6.days.ago).where.not(id: email_sending.sent_user_ids).order("
CASE WHEN users.email IN ('#{PRIORITY_RECIPIENTS.map {|str| "\"#{str}\""}.join(',')}')
THEN 0 ELSE 1 END, last_active_at DESC").select("users.*,
GREATEST(updated_at, last_jam_updated_at) AS last_active_at").limit(LIMIT)
AdminMailer.ugly({to: APP_CONFIG.user_match_monitoring_email,
subject:"Weekly user match email sending job started.",
body: "#{email_sending.sent_user_ids.any?? "This job is resuming. It was originally started at #{email_sending.created_at} and has been sent to #{email_sending.sent_user_ids.size} user(s) so far." : "This job was started at #{email_sending.created_at}" }. It will send to total of #{ recipients.size } users."}).deliver_now
recipients.find_each do |user|
ip_address = user.last_jam_addr.blank?? '127.0.0.1' : IPAddr.new(user.last_jam_addr, Socket::AF_INET).to_s
matched_musician_data = []
nextOffset = 0
while !nextOffset.nil? && nextOffset >= 0 do
params.merge!({ offset: nextOffset })
results, latency_data, nextOffset = JamRuby::MusicianFilter.filter(user, ip_address, params)
matched_musician_data << [{ musicians: results, latencies: latency_data }] if results && results.size > 0
end
if matched_musician_data.size > 0
UserMailer.new_musicians_match(user, matched_musician_data).deliver_now
user.update_column(:user_match_email_sent_at, Time.now)
email_sending.sent_user_ids.push(user.id)
email_sending.save!
end
end
email_sending.total_recipients = email_sending.sent_user_ids.size
email_sending.completed_at = Time.now
email_sending.save!
AdminMailer.ugly({
to: APP_CONFIG.user_match_monitoring_email,
subject:"Weekly user match email sending completed.",
body: "Weekly email sending job was completed at #{Time.now}. It was sent to #{email_sending.sent_user_ids.size} user(s)."
}).deliver_now
rescue => exception
begin
fail_count = email_sending.fail_count
email_sending.update_attributes(fail_count: fail_count + 1, exception_detail: exception.message)
AdminMailer.ugly({to: APP_CONFIG.user_match_monitoring_email,
subject:"Error occured when sending weekly user match email.",
body: "An error was encountered at #{Time.now} while sending weekly user match email - #{exception.message}."}).deliver_now
rescue
Bugsnag.notify(exception)
end
end
end
end
end

View File

@ -0,0 +1,159 @@
module JamRuby
class MusicianFilter
LATENCY_SCORES = {
good: { label: 'GOOD', min: 0, max: 40 },
fair: { label: 'FAIR', min: 40, max: 60 },
high: { label: 'HIGH', min: 60, max: 10000000 },
me: { label: 'ME', min: -1, max: -1 },
unknown: { label: 'UNKNOWN', min: -2, max: -2 }
};
def self.filter(user, remote_ip, params)
#debugger
latency_good = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:latency_good])
latency_fair = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:latency_fair])
latency_high = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:latency_high])
offset = [params[:offset].to_i, 0].max
limit = [params[:limit].to_i, 20].max
filter_params = {}
filter_params.merge!(from_location: params[:from_location] ? '1' : '0')
genres = params[:genres]
filter_params.merge!(genres: genres) if genres
beginner = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:proficiency_beginner])
intermediate = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:proficiency_intermediate])
expert = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:proficiency_expert])
proficiency_levels = []
proficiency_levels.push(1) if beginner
proficiency_levels.push(2) if intermediate
proficiency_levels.push(3) if expert
instruments = params[:instruments]
if instruments && instruments.any? && proficiency_levels.any?
inst = []
instruments.each do |ii|
proficiency_levels.each do |pl|
inst << { id: ii[:value], proficiency: pl}
end
end
filter_params.merge!(instruments: inst)
end
filter_params.merge!(joined_within_days: params[:joined_within_days]) unless params[:joined_within_days].blank?
filter_params.merge!(active_within_days: params[:active_within_days]) unless params[:active_within_days].blank?
begin
#bm = Benchmark.measure do
result = JamRuby::MusicianFilter.users_latency_data(user, remote_ip, latency_good, latency_fair, latency_high, filter_params, offset, limit)
latency_data = result[:data]
nextOffset = result[:next]
user_ids = latency_data.map{ |l_data| l_data[:user_id] }
#end
# Bugsnag.notify("search_users_benchmark") do |report|
# report.severity = "info"
# report.add_tab(:benchmark, benchmark: bm.to_s)
# end if Rails.env.production?
sobj = JamRuby::MusicianSearch.user_search_filter(user)
search = sobj.user_search_results(user_ids)
[search, latency_data, nextOffset]
rescue => exception
logger.debug("Latency exception: #{exception.message}")
Bugsnag.notify(exception) do |report|
report.severity = "error"
report.add_tab(:latency, {
params: params,
user_id: user.id,
name: user.name,
url: filter_latency_url,
})
end
raise exception
end
end
def self.users_latency_data(user_obj, remote_ip, latency_good, latency_fair, latency_high, filter_opts, offset, limit)
filter_latency_url = "#{APP_CONFIG.latency_data_host}/search_users"
uri = URI(filter_latency_url)
begin
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if APP_CONFIG.latency_data_host.start_with?("https://")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Basic #{APP_CONFIG.latency_data_host_auth_code}"
req["Content-Type"] = "application/json"
req_params = {
my_user_id: user_obj.id,
my_public_ip: remote_ip,
my_device_id: nil,
my_client_id: nil,
from_location: filter_opts[:from_location] || "0",
offset: offset,
limit: limit
}
req_params.merge!(instruments: filter_opts[:instruments]) if filter_opts[:instruments]
req_params.merge!(genres: filter_opts[:genres]) if filter_opts[:genres]
req_params.merge!(joined_within_days: filter_opts[:joined_within_days]) if filter_opts[:joined_within_days]
req_params.merge!(active_within_days: filter_opts[:active_within_days]) if filter_opts[:active_within_days]
req.body = req_params.to_json
response = http.request(req)
if response.is_a?(Net::HTTPOK) || response.is_a?(Net::HTTPSuccess)
json_body = JSON.parse(response.body)
graph_db_users = json_body['users']
nextOffset = json_body['next']
if latency_good || latency_fair || latency_high
#fiter by latency params
graph_db_users.select! do |user|
total_latency = user["ars"]["total_latency"].to_f
(total_latency >= LATENCY_SCORES[:good][:min] && total_latency <= LATENCY_SCORES[:good][:max] && latency_good) ||
(total_latency > LATENCY_SCORES[:fair][:min] && total_latency <= LATENCY_SCORES[:fair][:max] && latency_fair) ||
(total_latency > LATENCY_SCORES[:high][:min] && latency_high)
end
end
latency_data = graph_db_users.map { | user |
{
user_id: user["user_id"],
audio_latency: user["audio_latency"].to_f,
ars_total_latency: user["ars"]["total_latency"].to_f,
ars_internet_latency: user["ars"]["internet_latency"].to_f
}
}.uniq
return { data: latency_data, next: nextOffset }
else
logger.debug("Latency response failed: #{response}")
Bugsnag.notify("LatencyResponseFailed") do |report|
report.severity = "faliure"
report.add_tab(:latency, {
user_id: user_obj.id,
name: user_obj.name,
params: params,
url: filter_latency_url,
code: response.code,
body: response.body,
})
end
end
rescue => exception
raise exception
end
end
end
end

View File

@ -2936,10 +2936,17 @@ module JamRuby
def recurly_link_to_account
"https://#{APP_CONFIG.recurly_subdomain}.recurly.com/accounts/#{id}"
end
def recurly_link_to_subscription
"https://#{APP_CONFIG.recurly_subdomain}.recurly.com/subscriptions/#{recurly_subscription_id}"
end
def last_active_timestamp
if updated_at || last_jam_updated_at
[updated_at, last_jam_updated_at].compact.max.to_i
end
end
private
def create_remember_token
self.remember_token = SecureRandom.urlsafe_base64

View File

@ -0,0 +1,19 @@
module JamRuby
class UserMatchEmailSending < ActiveRecord::Base
serialize :sent_user_ids, Array
def sent_user_ids=(ids)
ids = ids.split(',') if ids.is_a?(String)
super(ids)
end
def completed?
!completed_at.nil?
end
def self.most_recent
UserMatchEmailSending.order(created_at: :desc).first
end
end
end

View File

@ -0,0 +1,15 @@
module JamRuby
class NewMusicianMatchEmailer
extend Resque::Plugins::JamLonelyJob
@queue = :scheduled_new_musician_match_emailer
@@log = Logging.logger[NewMusicianMatchEmailer]
def self.perform
@@log.debug("waking up")
EmailNewMusicianMatch.send_new_musicians
@@log.debug("done")
end
end
end

View File

@ -0,0 +1,81 @@
require 'spec_helper'
describe EmailNewMusicianMatch do
let(:user1) { FactoryGirl.create(:user, subscribe_email: true, subscribe_email_for_user_match: true, user_match_email_sent_at: 7.days.ago) }
let(:user2) { FactoryGirl.create(:user, subscribe_email: true, subscribe_email_for_user_match: true, user_match_email_sent_at: 7.days.ago) }
let(:user3) { FactoryGirl.create(:user, subscribe_email: true, subscribe_email_for_user_match: true, user_match_email_sent_at: 7.days.ago) }
let(:user4) { FactoryGirl.create(:user, subscribe_email: true, subscribe_email_for_user_match: true, user_match_email_sent_at: 7.days.ago) }
let(:user5) { FactoryGirl.create(:user, subscribe_email: true, subscribe_email_for_user_match: true, user_match_email_sent_at: 7.days.ago) }
let(:user6) { FactoryGirl.create(:user, subscribe_email: true, subscribe_email_for_user_match: true, user_match_email_sent_at: 7.days.ago) }
let(:user7) { FactoryGirl.create(:user, subscribe_email: false, user_match_email_sent_at: 7.days.ago) }
let(:user8) { FactoryGirl.create(:user, subscribe_email: false, subscribe_email_for_user_match: false, user_match_email_sent_at: 7.days.ago) }
let(:user9) { FactoryGirl.create(:user, email: 'seth@jamkazam.com', subscribe_email: true, subscribe_email_for_user_match: true, user_match_email_sent_at: 7.days.ago) } #a priority user
let(:user10) { FactoryGirl.create(:user, email: 'david@jamkazam.com', subscribe_email: false) } #a priority user. but not included as he has marked not to receive email notifications
let(:search_result){ [[user1, user2, user3, user4, user5], [user6, user7, user8, user9, user10] ] }
let (:mail) { double("Mail") }
let (:admin_mail) { double("Admin Mail") }
before(:each) do
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
User.delete_all
allow(JamRuby::MusicianFilter).to receive(:filter).and_return(search_result)
end
after(:each) do
ActionMailer::Base.deliveries.clear
end
it "notify admin" do
allow(AdminMailer).to receive(:ugly).and_return(admin_mail)
expect(admin_mail).to receive(:deliver_now).exactly(2).times
JamRuby::EmailNewMusicianMatch.send_new_musicians
end
it "does not sent to whom have not been opted to receive emails" do
JamRuby::EmailNewMusicianMatch.send_new_musicians
ActionMailer::Base.deliveries.map{|d| d['to'].to_s }.include?("david@example.com").should be_falsey
end
it "delivers the new musicians notification email" do
allow(UserMailer).to receive(:new_musicians_match).and_return(mail)
expect(mail).to receive(:deliver_now).exactly(7).times
JamRuby::EmailNewMusicianMatch.send_new_musicians
end
xit "delivers to priority recipients first" do
JamRuby::EmailNewMusicianMatch.send_new_musicians
ActionMailer::Base.deliveries[1]['to'].to_s.should == "seth@jamkazam.com" #NOTE: the first email is sent to user_match_monitoring_email. The second email should be sent to the first priority user in the priority user list
end
describe "halfway done job" do
before(:each) do
UserMailer.deliveries.clear
allow_any_instance_of(UserMatchEmailSending).to receive(:sent_user_ids).and_return([user1.id, user2.id])
end
it "does not deliver to already delivered users" do
allow(UserMailer).to receive(:new_musicians_match).and_return(mail)
expect(mail).to receive(:deliver_now).exactly(5).times
JamRuby::EmailNewMusicianMatch.send_new_musicians
end
end
describe "catching errors" do
before do
JamRuby::MusicianFilter.stub(:filter).and_raise
end
it 'notifies admin about the error' do
JamRuby::EmailNewMusicianMatch.send_new_musicians
ActionMailer::Base.deliveries.length.should == 2
ActionMailer::Base.deliveries[1]['subject'].to_s.should == "Error occured when sending weekly user match email."
end
end
end

View File

@ -0,0 +1,223 @@
require 'spec_helper'
require 'webmock/rspec'
describe MusicianFilter do
let(:latency_data_uri) { /\S+\/search_users/ }
let(:user) { FactoryGirl.create(:user) }
let(:remote_ip) { "127.0.0.1" }
let(:user1) { FactoryGirl.create(:user) }
let(:user2) { FactoryGirl.create(:user) }
let(:user3) { FactoryGirl.create(:user) }
let(:user4) { FactoryGirl.create(:user) }
let(:user5) { FactoryGirl.create(:user) }
let(:user6) { FactoryGirl.create(:user) }
let(:user7) { FactoryGirl.create(:user) }
let(:user8) { FactoryGirl.create(:user) }
let(:response_body) { mock_latency_response([
{ user: user1, ars_total_latency: 1.0, ars_internet_latency: 0.4, audio_latency: 0.6 }, #GOOD
{ user: user2, ars_total_latency: 40.0, ars_internet_latency: 25.0, audio_latency: 15.0 }, #GOOD
{ user: user3, ars_total_latency: 40.1, ars_internet_latency: 25, audio_latency: 15.1 }, #FAIR
{ user: user4, ars_total_latency: 60.0, ars_internet_latency: 30, audio_latency: 30.0 }, #FAIR
{ user: user5, ars_total_latency: 60.1, ars_internet_latency: 30.1, audio_latency: 30 }, #HIGH
{ user: user6, ars_total_latency: 100.0, ars_internet_latency: 50.0, audio_latency: 50.0 }, #HIGH
{ user: user7, ars_total_latency: -2, ars_internet_latency: -1, audio_latency: -1 }, #UNKNOWN
{ user: user8, ars_total_latency: 10, ars_internet_latency: 5, audio_latency: 0 } #GOOD (NOTE: audio_latency from neo4j is 0 here)
])
}
let(:response_body_pop) { mock_latency_response([
{ user: user1, ars_total_latency: 1.0, ars_internet_latency: 0.4, audio_latency: 0.6 }, #GOOD
{ user: user2, ars_total_latency: 40.0, ars_internet_latency: 25.0, audio_latency: 15.0 }, #GOOD
{ user: user3, ars_total_latency: 40.1, ars_internet_latency: 25, audio_latency: 15.1 }, #FAIR
])
}
let(:response_body_pop_and_rap) { mock_latency_response([
{ user: user1, ars_total_latency: 1.0, ars_internet_latency: 0.4, audio_latency: 0.6 }, #GOOD
])
}
let(:response_body_drums_intermediate) { mock_latency_response([
{ user: user1, ars_total_latency: 1.0, ars_internet_latency: 0.4, audio_latency: 0.6 },
{ user: user2, ars_total_latency: 40.0, ars_internet_latency: 25.0, audio_latency: 15.0 },
{ user: user3, ars_total_latency: 40.1, ars_internet_latency: 25, audio_latency: 15.1 },
{ user: user4, ars_total_latency: 60.0, ars_internet_latency: 30, audio_latency: 30.0 }
])
}
let(:response_body_drums_violin_expert) { mock_latency_response([
{ user: user2, ars_total_latency: 40.0, ars_internet_latency: 25.0, audio_latency: 15.0 },
{ user: user3, ars_total_latency: 40.1, ars_internet_latency: 25, audio_latency: 15.1 },
])
}
let(:response_body_active_within_one_day) { mock_latency_response([
{ user: user4, ars_total_latency: 60.0, ars_internet_latency: 30, audio_latency: 30.0 }, #FAIR
])
}
let(:response_body_joined_within_one_day) { mock_latency_response([
{ user: user4, ars_total_latency: 60.0, ars_internet_latency: 30, audio_latency: 30.0 }, #FAIR
{ user: user5, ars_total_latency: 60.1, ars_internet_latency: 30.1, audio_latency: 30 }, #HIGH
])
}
before(:each) do
User.delete_all
stub_request(:post, latency_data_uri)
.with(:headers => {'Accept'=>'*/*', 'Content-Type'=>'application/json', 'User-Agent'=>'Ruby'})
.to_return( body: response_body, status: 200)
stub_request(:post, latency_data_uri).
with(
body: hash_including({ genres: ["pop"]}),
:headers => {'Accept'=>'*/*', 'Content-Type'=>'application/json', 'User-Agent'=>'Ruby'}
)
.to_return( body: response_body_pop, status: 200)
stub_request(:post, latency_data_uri).
with(
body: hash_including({ genres: ["pop", "rap"]}),
:headers => {'Accept'=>'*/*', 'Content-Type'=>'application/json', 'User-Agent'=>'Ruby'}
)
.to_return( body: response_body_pop_and_rap, status: 200)
stub_request(:post, latency_data_uri).
with(
body: hash_including({ instruments: [{id: 'drums', proficiency: 2}]}),
:headers => {'Accept'=>'*/*', 'Content-Type'=>'application/json', 'User-Agent'=>'Ruby'}
)
.to_return( body: response_body_drums_intermediate, status: 200)
stub_request(:post, latency_data_uri).
with(
body: hash_including({ instruments: [{id: 'drums', proficiency: 3}, {id: 'violin', proficiency: 3}]}),
:headers => {'Accept'=>'*/*', 'Content-Type'=>'application/json', 'User-Agent'=>'Ruby'}
)
.to_return( body: response_body_drums_violin_expert, status: 200)
stub_request(:post, latency_data_uri).
with(
body: hash_including({ active_within_days: 1}),
:headers => {'Accept'=>'*/*', 'Content-Type'=>'application/json', 'User-Agent'=>'Ruby'}
)
.to_return( body: response_body_active_within_one_day, status: 200)
stub_request(:post, latency_data_uri).
with(
body: hash_including({ joined_within_days: 1 }),
:headers => {'Accept'=>'*/*', 'Content-Type'=>'application/json', 'User-Agent'=>'Ruby'}
)
.to_return( body: response_body_joined_within_one_day, status: 200)
end
it "when no latency option is selected" do
opts = { latency_good: false, latency_fair: false, latency_high: false }
search, latency_data, nextOffset = JamRuby::MusicianFilter.filter(user, remote_ip, opts)
puts search.results
puts "===="
puts latency_data
puts "===="
puts nextOffset
expect(search.results.size).to eq(8)
expect(latency_data).not_to eq(nil)
expect(latency_data[0][:audio_latency]).not_to eq(nil)
expect(latency_data[0][:ars_total_latency]).not_to eq(nil)
expect(latency_data[0][:ars_internet_latency]).not_to eq(nil)
end
it "filter musicians for all latency options" do
opts = { latency_good: true, latency_fair: true, latency_high: true }
search, latency_data, nextOffset = JamRuby::MusicianFilter.filter(user, remote_ip, opts)
expect(search.results.size).to eq(7)
expect(latency_data).not_to eq(nil)
expect(latency_data[0][:audio_latency]).not_to eq(nil)
expect(latency_data[0][:ars_total_latency]).not_to eq(nil)
expect(latency_data[0][:ars_internet_latency]).not_to eq(nil)
#sort by latency
expect(search.results[0][:id]).to eq(user1.id)
expect(search.results[1][:id]).to eq(user2.id)
expect(search.results[2][:id]).to eq(user3.id)
expect(search.results[3][:id]).to eq(user4.id)
expect(search.results[4][:id]).to eq(user5.id)
expect(search.results[5][:id]).to eq(user6.id)
expect(search.results[6][:id]).to eq(user8.id)
end
it "filter GOOD latency users" do
opts = { latency_good: true, latency_fair: false, latency_high: false }
search, latency_data, nextOffset = JamRuby::MusicianFilter.filter(user, remote_ip, opts)
expect(search.results.size).to eq(3)
end
it "filter FAIR latency musicians" do
opts = { latency_good: false, latency_fair: true, latency_high: false }
search, latency_data, nextOffset = JamRuby::MusicianFilter.filter(user, remote_ip, opts)
expect(search.results.size).to eq(2)
end
it "filter HIGH latency musicians" do
opts = { latency_good: false, latency_fair: false, latency_high: true }
search, latency_data, nextOffset = JamRuby::MusicianFilter.filter(user, remote_ip, opts)
expect(search.results.size).to eq(2)
end
it "filter GOOD and FAIR latency musicians" do
opts = { latency_good: true, latency_fair: true, latency_high: false }
search, latency_data, nextOffset = JamRuby::MusicianFilter.filter(user, remote_ip, opts)
expect(search.results.size).to eq(5)
end
it "filter GOOD and HIGH latency musicians" do
opts = { latency_good: true, latency_fair: false, latency_high: true }
search, latency_data, nextOffset = JamRuby::MusicianFilter.filter(user, remote_ip, opts)
expect(search.results.size).to eq(5)
end
it "filter GOOD, FAIR and HIGH latency musicians" do
opts = { latency_good: true, latency_fair: true, latency_high: true }
search, latency_data, nextOffset = JamRuby::MusicianFilter.filter(user, remote_ip, opts)
expect(search.results.size).to eq(7)
end
it "filter musicians by genres" do
opts = { latency_good: true, latency_fair: true, latency_high: true, genres: ['pop'] }
search, latency_data, nextOffset = JamRuby::MusicianFilter.filter(user, remote_ip, opts)
expect(search.results.size).to eq(3)
opts = { latency_good: true, latency_fair: true, latency_high: true, genres: ['pop', 'rap'] }
search, latency_data, nextOffset = JamRuby::MusicianFilter.filter(user, remote_ip, opts)
expect(search.results.size).to eq(1)
end
it "filter musicians by instruments they play" do
opts = { latency_good: true, latency_fair: true, latency_high: true, instruments: [{value: "drums", label: "Drums"}], proficiency_intermediate: true }
search, latency_data, nextOffset = JamRuby::MusicianFilter.filter(user, remote_ip, opts)
expect(search.results.size).to eq(4)
opts = { latency_good: true, latency_fair: true, latency_high: true, instruments: [{value: "drums", label: "Drums"}, {value: 'violin', label: 'Violin'}], proficiency_expert: true }
search, latency_data, nextOffset = JamRuby::MusicianFilter.filter(user, remote_ip, opts)
expect(search.results.size).to eq(2)
end
it "filter musicians by days ago that they joined" do
opts = { latency_good: true, latency_fair: true, latency_high: true, joined_within_days: 1 }
search, latency_data, nextOffset = JamRuby::MusicianFilter.filter(user, remote_ip, opts)
expect(search.results.size).to eq(2)
end
it "finds user updated_at is within a day ago" do
opts = { latency_good: true, latency_fair: true, latency_high: true, active_within_days: 1 }
search, latency_data, nextOffset = JamRuby::MusicianFilter.filter(user, remote_ip, opts)
expect(search.results.size).to eq(1)
end
end

View File

@ -4,7 +4,7 @@
require "spec_helper"
describe "RenderMailers", :slow => true do
describe "RenderMailers" do
let(:user) { FactoryGirl.create(:user) }
let(:school) {FactoryGirl.create(:school, education:true)}
@ -16,7 +16,7 @@ describe "RenderMailers", :slow => true do
describe "UserMailer emails" do
before(:each) do
user.update_email = "my_new_email@jamkazam.com"
#user.update_email = "my_new_email@jamkazam.com"
UserMailer.deliveries.clear
end
@ -53,6 +53,32 @@ describe "RenderMailers", :slow => true do
it { @filename="friend_request"; UserMailer.friend_request(user, 'So and so has sent you a friend request.', friend_request.id).deliver_now }
end
# describe "sending about new musicians with good latency to the user", focus: true do
# let(:user) { User.find_by(email: "nuwan@jamkazam.com") }
# let(:params) {
# {latency_good: true,
# latency_fair: true,
# latency_high: false,
# proficiency_beginner: true,
# proficiency_intermediate: true,
# proficiency_expert: true,
# from_location: false,
# joined_within_days: "",
# active_within_days: "",
# limit: 20,
# offset: 0}
# }
# let(:ip){ "127.0.0.1" }
# it{
# @filename="new_musicians_match"
# search, latency_data, nextOffset = JamRuby::MusicianFilter.filter(user, ip, params)
# matched_musician_data = []
# matched_musician_data << [search, latency_data]
# UserMailer.new_musicians_match(user, matched_musician_data).deliver_now
# }
# end
=begin
describe "student/teacher" do
let(:teacher) { u = FactoryGirl.create(:teacher); u.user }
@ -497,6 +523,36 @@ describe "RenderMailers", :slow => true do
@filename="daily_sessions"; scheduled_batch.deliver_batch
end
end
describe "New Musician Match email" do
let(:user) { FactoryGirl.create(:user) }
let(:user1) { FactoryGirl.create(:user, first_name: 'David', last_name: 'Macmillan', updated_at: 2.day.ago.to_i) }
let(:user2) { FactoryGirl.create(:user, first_name: 'Tom', last_name: 'Cluff', last_jam_updated_at: 1.days.ago.to_i) }
let(:matched_musician_data){
[
{
musicians: [user1, user2],
latencies: [
{:user_id=> user1.id, :audio_latency=>4, :ars_total_latency=>12, :ars_internet_latency=>8},
{:user_id=> user2.id, :audio_latency=>4, :ars_total_latency=>12, :ars_internet_latency=>8}
]
}
]
}
before(:each) do
User.delete_all
ActionMailer::Base.deliveries.clear
end
after(:each) do
ActionMailer::Base.deliveries.length.should == 1
mail = ActionMailer::Base.deliveries[0]
save_emails_to_disk(mail, @filename)
end
fit { @filename="new_musicians_match"; UserMailer.new_musicians_match(user, matched_musician_data).deliver_now }
end
end
def save_emails_to_disk(mail, filename)

View File

@ -12,11 +12,11 @@ describe UserMailer do
let(:user) { FactoryGirl.create(:user) }
before(:each) do
stub_const("APP_CONFIG", app_config)
#stub_const("APP_CONFIG", app_config)
UserMailer.deliveries.clear
end
describe "should send confirm email" do
describe "should send confirm email", focus: true do
let (:mail) { UserMailer.deliveries[0] }
let (:signup_confirmation_url) { "/confirm" }
@ -26,7 +26,6 @@ describe UserMailer do
UserMailer.confirm_email(user, signup_confirmation_url_with_token).deliver_now
end
it { UserMailer.deliveries.length.should == 1 }
it { mail['from'].to_s.should == UserMailer::DEFAULT_SENDER }
it { mail['to'].to_s.should == user.email }
@ -181,7 +180,16 @@ describe UserMailer do
end
end
describe "sends new musicians email" do
let(:mail) { UserMailer.deliveries[0] }
before(:each) do
UserMailer.new_musicians_match(user, []).deliver_now
end
it { UserMailer.deliveries.length.should == 1 }
it { mail['from'].to_s.should == UserMailer::DEFAULT_SENDER }
it { mail['to'].to_s.should == user.email }
it { mail.multipart?.should == true } # because we send plain + html
end
# describe "sends new musicians email" do

View File

@ -182,6 +182,8 @@ end
# gem 'rack-timeout'
#end
gem 'ffi', '1.14.0'
group :development, :test do
gem 'rspec-rails' #, require: "rspec/rails" #, '2.14.2'
gem 'rspec-collection_matchers'
@ -248,4 +250,4 @@ end
group :package do
#gem 'fpm'
end
end

View File

@ -459,7 +459,9 @@ GEM
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2021.0212)
mimemagic (0.3.5)
mimemagic (0.4.3)
nokogiri (~> 1)
rake
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.14.3)
@ -517,7 +519,7 @@ GEM
paypal-sdk-merchant-jk (1.118.1)
paypal-sdk-core (~> 0.3.0)
pdf-core (0.7.0)
pg (0.17.1)
pg (0.21.0)
pg_array_parser (0.0.9)
pleaserun (0.0.31)
cabin (> 0)
@ -554,6 +556,8 @@ GEM
rabl (0.13.1)
activesupport (>= 2.3.14)
rack (1.6.13)
rack-cors (1.0.6)
rack (>= 1.6.0)
rack-oauth2 (1.12.0)
activesupport
attr_required
@ -864,7 +868,7 @@ DEPENDENCIES
omniauth-stripe-connect
omniauth-twitter
paypal-sdk-merchant-jk (= 1.118.1)
pg (= 0.17.1)
pg (= 0.21.0)
postgres-copy
postgres_ext
prawn-table
@ -873,6 +877,7 @@ DEPENDENCIES
puma
quiet_assets
rabl (= 0.13.1)
rack-cors (~> 1.0, >= 1.0.6)
rack-test
rails (= 4.2.8)
rails-assets-bluebird!

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -5,7 +5,7 @@ class ApiSearchController < ApiController
respond_to :json
include LatencyHelper
#include LatencyHelper
def index
if 1 == params[Search::PARAM_MUSICIAN].to_i || 1 == params[Search::PARAM_BAND].to_i
@ -95,94 +95,98 @@ class ApiSearchController < ApiController
end
end
def filter
# def filter
latency_good = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:latency_good])
latency_fair = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:latency_fair])
latency_high = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:latency_high])
offset = [params[:offset].to_i, 0].max
limit = [params[:limit].to_i, 20].max
filter_params = {}
# latency_good = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:latency_good])
# latency_fair = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:latency_fair])
# latency_high = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:latency_high])
# offset = [params[:offset].to_i, 0].max
# limit = [params[:limit].to_i, 20].max
# filter_params = {}
filter_params.merge!(from_location: params[:from_location] ? '1' : '0')
# filter_params.merge!(from_location: params[:from_location] ? '1' : '0')
genres = params[:genres]
filter_params.merge!(genres: genres) if genres
# if genres && genres.any?
# genres.map!{|genre| {id: genre} }
# filter_params.merge!(genres: genres)
# end
# genres = params[:genres]
# filter_params.merge!(genres: genres) if genres
# # if genres && genres.any?
# # genres.map!{|genre| {id: genre} }
# # filter_params.merge!(genres: genres)
# # end
beginner = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:proficiency_beginner])
intermediate = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:proficiency_intermediate])
expert = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:proficiency_expert])
# beginner = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:proficiency_beginner])
# intermediate = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:proficiency_intermediate])
# expert = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:proficiency_expert])
proficiency_levels = []
proficiency_levels.push(1) if beginner
proficiency_levels.push(2) if intermediate
proficiency_levels.push(3) if expert
# proficiency_levels = []
# proficiency_levels.push(1) if beginner
# proficiency_levels.push(2) if intermediate
# proficiency_levels.push(3) if expert
instruments = params[:instruments]
# instruments = params[:instruments]
#debugger
# #debugger
if instruments && instruments.any? && proficiency_levels.any?
inst = []
instruments.each do |ii|
proficiency_levels.each do |pl|
inst << { id: ii[:value], proficiency: pl}
end
end
filter_params.merge!(instruments: inst)
end
# if instruments && instruments.any? && proficiency_levels.any?
# inst = []
# instruments.each do |ii|
# proficiency_levels.each do |pl|
# inst << { id: ii[:value], proficiency: pl}
# end
# end
# filter_params.merge!(instruments: inst)
# end
filter_params.merge!(joined_within_days: params[:joined_within_days]) unless params[:joined_within_days].blank?
filter_params.merge!(active_within_days: params[:active_within_days]) unless params[:active_within_days].blank?
# filter_params.merge!(joined_within_days: params[:joined_within_days]) unless params[:joined_within_days].blank?
# filter_params.merge!(active_within_days: params[:active_within_days]) unless params[:active_within_days].blank?
@latency_data = []
begin
# @latency_data = []
# begin
#bm = Benchmark.measure do
result = users_latency_data(latency_good, latency_fair, latency_high, filter_params, offset, limit)
@latency_data = result[:data]
@nextOffset = result[:next]
# #bm = Benchmark.measure do
# result = JamRuby::MusicianFilter.users_latency_data(current_user, request.remote_ip, latency_good, latency_fair, latency_high, filter_params, offset, limit)
# @latency_data = result[:data]
# @nextOffset = result[:next]
user_ids = @latency_data.map{ |l_data| l_data[:user_id] }
#end
# user_ids = @latency_data.map{ |l_data| l_data[:user_id] }
# #end
# Bugsnag.notify("search_users_benchmark") do |report|
# report.severity = "info"
# report.add_tab(:benchmark, benchmark: bm.to_s)
# end if Rails.env.production?
# # Bugsnag.notify("search_users_benchmark") do |report|
# # report.severity = "info"
# # report.add_tab(:benchmark, benchmark: bm.to_s)
# # end if Rails.env.production?
sobj = MusicianSearch.user_search_filter(current_user)
#@search = sobj.search_results_page(filter_params, page, user_ids)
#debugger
@search = sobj.user_search_results(user_ids)
# sobj = MusicianSearch.user_search_filter(current_user)
# #@search = sobj.search_results_page(filter_params, page, user_ids)
# debugger
# @search = sobj.user_search_results(user_ids)
# respond_with @search, responder: ApiResponder, status: 201, template: 'api_search/filter'
# rescue => exception
# logger.debug("Latency exception: #{exception.message}")
# Bugsnag.notify(exception) do |report|
# report.severity = "error"
# report.add_tab(:latency, {
# params: params,
# user_id: current_user.id,
# name: current_user.name,
# url: filter_latency_url,
# })
# end
# render json: {}, status: 500
# end
# end
def filter
begin
@search, @latency_data, @nextOffset = JamRuby::MusicianFilter.filter(current_user, request.remote_ip, params)
Rails.logger.debug("=====SEARCH : #{@search.results.inspect}")
Rails.logger.debug("=====LATENCY : #{@latency_data}")
respond_with @search, responder: ApiResponder, status: 201, template: 'api_search/filter'
rescue => exception
logger.debug("Latency exception: #{exception.message}")
Bugsnag.notify(exception) do |report|
report.severity = "error"
report.add_tab(:latency, {
params: params,
user_id: current_user.id,
name: current_user.name,
url: filter_latency_url,
})
end
rescue
render json: {}, status: 500
end
end
end
private
def filter_latency_url
"#{Rails.application.config.latency_data_host}/search_users"
end
end

View File

@ -445,6 +445,16 @@ JS
render text: 'You have been unsubscribed.'
end
def unsubscribe_user_match
if params[:user_token].present? && @user = User.read_access_token(params[:user_token])
@user.subscribe_email_for_user_match = false
@user.save!
render json: { head: :ok }
else
render json: { status: :unprocessable_entity }
end
end
private
def _render(action)

View File

@ -86,7 +86,6 @@ module LatencyHelper
raise exception
end
latency_data
end
end

View File

@ -25,7 +25,7 @@ node :is_blank_filter do |foo|
end
child(:results => :musicians) {
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :online, :musician, :photo_url, :biography, :regionname, :score, :full_score, :is_friend, :is_following, :pending_friend_request
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :online, :musician, :photo_url, :biography, :regionname, :score, :full_score, :is_friend, :is_following, :pending_friend_request, :last_active_timestamp
# node :is_friend do |musician|
# @search.is_friend?(musician)
@ -82,11 +82,11 @@ child(:results => :musicians) {
end if @latency_data
end
node :last_active_timestamp do |musician|
if musician.updated_at || musician.last_jam_updated_at
[musician.updated_at, musician.last_jam_updated_at].compact.max.to_i
end
end
# node :last_active_timestamp do |musician|
# if musician.updated_at || musician.last_jam_updated_at
# [musician.updated_at, musician.last_jam_updated_at].compact.max.to_i
# end
# end
child :genres => :genres do
attributes :genre_id, :description

View File

@ -24,7 +24,7 @@ if @search.is_a?(BaseSearch)
end
child(:results => :musicians) {
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :online, :musician, :photo_url, :biography, :regionname, :score, :full_score
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :online, :musician, :photo_url, :biography, :regionname, :score, :full_score, :last_active_timestamp
node :is_friend do |musician|
@search.is_friend?(musician)
@ -72,11 +72,11 @@ if @search.is_a?(BaseSearch)
end if @latency_data
end
node :last_active_timestamp do |musician|
if musician.updated_at || musician.last_jam_updated_at
[musician.updated_at, musician.last_jam_updated_at].compact.max.to_i
end
end
# node :last_active_timestamp do |musician|
# if musician.updated_at || musician.last_jam_updated_at
# [musician.updated_at, musician.last_jam_updated_at].compact.max.to_i
# end
# end
child :genres => :genres do
attributes :genre_id, :description

View File

@ -1,7 +1,7 @@
object @profile
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :follower_count, :following_count, :recording_count, :session_count, :biography, :favorite_count, :audio_latency, :upcoming_session_count, :age, :website, :skill_level, :concert_count, :studio_session_count, :virtual_band, :virtual_band_commitment, :traditional_band, :traditional_band_commitment, :traditional_band_touring, :paid_sessions, :paid_sessions_hourly_rate,
:paid_sessions_daily_rate, :free_sessions, :cowriting, :cowriting_purpose, :subscribe_email, :is_a_teacher, :is_a_student
:paid_sessions_daily_rate, :free_sessions, :cowriting, :cowriting_purpose, :subscribe_email, :is_a_teacher, :is_a_student, :last_active_timestamp
child :online_presences => :online_presences do
attributes :id, :service_type, :username
@ -32,11 +32,11 @@ child :musician_instruments => :instruments do
attributes :description, :proficiency_level, :priority, :instrument_id
end
node :last_active_timestamp do |user|
if user.updated_at || user.last_jam_updated_at
[user.updated_at, user.last_jam_updated_at].compact.max.to_i
end
end
# node :last_active_timestamp do |user|
# if user.updated_at || user.last_jam_updated_at
# [user.updated_at, user.last_jam_updated_at].compact.max.to_i
# end
# end
node :created_at_timestamp do |user|
user.created_at.to_i

View File

@ -515,7 +515,7 @@ if defined?(Bundler)
config.latency_data_host = "http://localhost:4001"
config.latency_data_host_auth_code = "c2VydmVyOnBhc3N3b3Jk"
config.manual_override_installer_ends_with = "JamKazam-1.0.3776.dmg"
config.spa_origin = "http://beta.jamkazam.local:3000"
config.spa_origin = "http://beta.jamkazam.local:4000"
config.user_match_monitoring_email = "user_match_monitoring_email@jamkazam.com"
end
end

View File

@ -120,4 +120,7 @@ SampleApp::Application.configure do
config.spa_origin = "http://beta.jamkazam.local:4000"
config.session_cookie_domain = ".jamkazam.local"
config.action_controller.asset_host = 'http://localhost:3000'
config.action_mailer.asset_host = config.action_controller.asset_host
end

View File

@ -145,6 +145,7 @@ Rails.application.routes.draw do
get '/reset_password_complete' => 'users#reset_password_complete', :as => 'reset_password_complete'
match '/unsubscribe/:user_token' => 'users#unsubscribe', via: [:get, :post]
match '/unsubscribe_user_match/:user_token' => 'users#unsubscribe_user_match', via: [:post]
# email update
get '/confirm_email' => 'users#finalize_update_email', :as => 'confirm_email' # NOTE: if you change this, you break outstanding email changes because links in user inboxes are broken

View File

@ -65,6 +65,11 @@ NewMusicianEmailer:
class: "JamRuby::NewMusicianEmailer"
description: "Sends weekly emails of new users with good latency"
NewMusicianMatchEmailer:
cron: "0 2 * * 6"
class: "JamRuby::NewMusicianMatchEmailer"
description: "Sends weekly emails of new users with good latency - (User latency data from neo4j)"
MusicSessionScheduler:
cron: "0 * * * *"
class: "JamRuby::MusicSessionScheduler"

View File

@ -135,7 +135,6 @@ describe "Musician Filter API", type: :request do
it "filter musicians when no latency option is selected" do
post '/api/filter.json', { latency_good: false, latency_fair: false, latency_high: false }
expect(JSON.parse(response.body)["musicians"].size).to eq(8)
expect(JSON.parse(response.body)["musicians"][0]["latency_data"]).not_to eq(nil)
expect(JSON.parse(response.body)["musicians"][0]["latency_data"]["audio_latency"]).not_to eq(nil)
@ -223,7 +222,7 @@ describe "Musician Filter API", type: :request do
expect(JSON.parse(response.body)["musicians"].size).to eq(2)
end
it "filter musicians by days ago that they joined" do
fit "filter musicians by days ago that they joined" do
post '/api/filter.json', { latency_good: true, latency_fair: true, latency_high: true, joined_within_days: 1 }
expect(JSON.parse(response.body)["musicians"].size).to eq(2)
end

View File

@ -28,6 +28,20 @@ describe UsersController, :type => :api do
user.subscribe_email.should eql false
end
fit "unsubscribe_user_match" do
user.subscribe_email_for_user_match.should eql false #default value is false
user.subscribe_email_for_user_match = true #we make it true here for this test
user.save!
get '/unsubscribe_user_match/' + user.unsubscribe_token
expect(last_response).to have_http_status(200)
user.reload
user.subscribe_email_for_user_match.should eql false
end
describe "track_origin" do
describe "logged out" do