diff --git a/Makefile b/Makefile index f81ba58c7..b5d86e512 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,5 @@ all: cd funnel/assets; make + +build: + cd funnel/assets; make assetsonly diff --git a/funnel/assets/cypress/fixtures/profile.json b/funnel/assets/cypress/fixtures/profile.json new file mode 100644 index 000000000..489e2b593 --- /dev/null +++ b/funnel/assets/cypress/fixtures/profile.json @@ -0,0 +1,5 @@ +{ + "title": "testcypressproject", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit", + "logo_url": "https://images.hasgeek.com/embed/file/fa971c8b503941de9f6ef5c53af02be7" +} diff --git a/funnel/assets/cypress/fixtures/user.json b/funnel/assets/cypress/fixtures/user.json index c582c7648..c7c963f22 100644 --- a/funnel/assets/cypress/fixtures/user.json +++ b/funnel/assets/cypress/fixtures/user.json @@ -1,8 +1,24 @@ { + "owner": { + "username": "profile-cypress", + "password": "cypress123" + }, "admin": { "username": "admin-user", "password": "cypress129" }, + "concierge": { + "username": "concierge-user", + "password": "cypress341" + }, + "usher": { + "username": "usher-cypress", + "password": "cypress566" + }, + "editor": { + "username": "editor-cypress", + "password": "cypress900" + }, "user": { "username": "member-user", "password": "cypress341" diff --git a/funnel/assets/cypress/integration/00_addProfile.js b/funnel/assets/cypress/integration/00_addProfile.js new file mode 100644 index 000000000..6d755aef1 --- /dev/null +++ b/funnel/assets/cypress/integration/00_addProfile.js @@ -0,0 +1,20 @@ +describe('Profile', function() { + const owner = require('../fixtures/user.json').owner; + const profile = require('../fixtures/profile.json'); + + it('Create a new profile', function() { + cy.login('/organizations', owner.username, owner.password); + + cy.get('a') + .contains('new organization') + .click(); + cy.location('pathname').should('contain', '/new'); + + cy.get('#title').type(profile.title); + cy.get('#name').type(profile.title); + cy.get('#is_public_profile').click(); + cy.get('button') + .contains('Next') + .click(); + }); +}); diff --git a/funnel/assets/cypress/integration/01_addCrewtoProfile.js b/funnel/assets/cypress/integration/01_addCrewtoProfile.js new file mode 100644 index 000000000..a0b51fdc7 --- /dev/null +++ b/funnel/assets/cypress/integration/01_addCrewtoProfile.js @@ -0,0 +1,52 @@ +describe('Adding crew to profile', function() { + const owner = require('../fixtures/user.json').owner; + const admin = require('../fixtures/user.json').admin; + const profile = require('../fixtures/profile.json'); + + Cypress.on('uncaught:exception', (err, runnable) => { + return false; + }); + + it('Add new member to profile and edit roles', function() { + cy.server(); + cy.route('**/edit').as('edit-form'); + cy.route('POST', '**/edit').as('edit-member'); + cy.route('GET', '**/delete').as('delete-form'); + cy.route('POST', '**/delete').as('delete-member'); + + cy.login('/' + profile.title, owner.username, owner.password); + cy.get('a[data-cy-btn="profile-crew"]').click(); + + cy.add_member(admin.username, 'owner'); + + cy.get('[data-cy="member"]') + .contains(admin.username) + .click(); + cy.wait('@edit-form'); + cy.get('button[data-cy-btn="revoke"]').click(); + cy.wait('@delete-form'); + cy.get('button') + .contains('Delete') + .click(); + cy.wait('@delete-member'); + cy.get('[data-cy="member"]') + .contains(admin.username) + .should('not.exist'); + + cy.add_member(admin.username, 'owner'); + cy.get('[data-cy="member"]') + .contains(admin.username) + .click(); + cy.wait('@edit-form'); + cy.get('#is_owner-0').click(); + cy.get('button') + .contains('Edit membership') + .click(); + cy.wait('@edit-member'); + cy.get('[data-cy="member"]') + .contains(admin.username) + .parents('.user-box') + .find('[data-cy="role"]') + .contains('Admin'); + }); +}); diff --git a/funnel/assets/cypress/integration/02_verifyAdminRoles.js b/funnel/assets/cypress/integration/02_verifyAdminRoles.js new file mode 100644 index 000000000..61a8648ff --- /dev/null +++ b/funnel/assets/cypress/integration/02_verifyAdminRoles.js @@ -0,0 +1,29 @@ +describe('Profile admin roles', function() { + const owner = require('../fixtures/user.json').owner; + const admin = require('../fixtures/user.json').admin; + const profile = require('../fixtures/profile.json'); + + it('Check roles of profile admins', function() { + cy.login('/' + profile.title, admin.username, admin.password); + + cy.get('a[data-cy-btn="edit-details"]').click(); + cy.get('#field-description') + .find('.CodeMirror textarea') + .type(profile.description, { force: true }); + cy.get('#logo_url').type(profile.logo_url); + cy.get('button') + .contains('Save changes') + .click(); + + cy.get('a[data-cy-btn="profile-crew"]').click(); + cy.get('button[data-cy-btn="add-member"]').should('not.exist'); + cy.get('[data-cy="member"]') + .contains(admin.username) + .click(); + cy.get('#member-form', { timeout: 10000 }).should('not.be.visible'); + cy.get('[data-cy="member"]') + .contains(owner.username) + .click(); + cy.get('#member-form', { timeout: 10000 }).should('not.be.visible'); + }); +}); diff --git a/funnel/assets/cypress/integration/01_createproject.js b/funnel/assets/cypress/integration/03_createProject.js similarity index 89% rename from funnel/assets/cypress/integration/01_createproject.js rename to funnel/assets/cypress/integration/03_createProject.js index e0094d66b..994f622c0 100644 --- a/funnel/assets/cypress/integration/01_createproject.js +++ b/funnel/assets/cypress/integration/03_createProject.js @@ -1,9 +1,10 @@ describe('Project', function() { const admin = require('../fixtures/user.json').admin; + const profile = require('../fixtures/profile.json'); const project = require('../fixtures/project.json'); it('Create a new project', function() { - cy.login('/testcypressproject', admin.username, admin.password); + cy.login('/' + profile.title, admin.username, admin.password); cy.get('a[data-cy="new-project"]').click(); cy.location('pathname').should('contain', '/new'); diff --git a/funnel/assets/cypress/integration/04_addCrewtoProject.js b/funnel/assets/cypress/integration/04_addCrewtoProject.js new file mode 100644 index 000000000..431a5d67c --- /dev/null +++ b/funnel/assets/cypress/integration/04_addCrewtoProject.js @@ -0,0 +1,31 @@ +describe('Adding crew', function() { + const owner = require('../fixtures/user.json').owner; + const admin = require('../fixtures/user.json').admin; + const concierge = require('../fixtures/user.json').concierge; + const usher = require('../fixtures/user.json').usher; + const editor = require('../fixtures/user.json').editor; + const profile = require('../fixtures/profile.json'); + const project = require('../fixtures/project.json'); + + Cypress.on('uncaught:exception', (err, runnable) => { + return false; + }); + + it('Add crew to project', function() { + cy.login('/' + profile.title, admin.username, admin.password); + cy.get('[data-cy-project="' + project.title + '"]') + .first() + .click(); + cy.location('pathname').should('contain', project.url); + cy.get('a[data-cy-navbar="crew"]').click(); + cy.get('[data-cy="member"]') + .contains(admin.username) + .parents('.user-box') + .find('[data-cy="role"]') + .contains('Editor'); + + cy.add_member(concierge.username, 'concierge'); + cy.add_member(usher.username, 'usher'); + cy.add_member(editor.username, 'editor'); + }); +}); diff --git a/funnel/assets/cypress/integration/05_verifyEditorRoles.js b/funnel/assets/cypress/integration/05_verifyEditorRoles.js new file mode 100644 index 000000000..748496a35 --- /dev/null +++ b/funnel/assets/cypress/integration/05_verifyEditorRoles.js @@ -0,0 +1,33 @@ +describe('Verify roles of editor', function() { + const editor = require('../fixtures/user.json').editor; + const profile = require('../fixtures/profile.json'); + const project = require('../fixtures/project.json'); + + it('Access available for editor in project settings', function() { + // Failing now - project in draft state is not visible to editor + // cy.login('/' + profile.title +, editor.username, editor.password); + // cy.get('[data-cy-project="' + project.title + '"]') + // .first() + // .click(); + + cy.login( + '/' + profile.title + '/' + project.url, + editor.username, + editor.password + ); + cy.location('pathname').should('contain', project.url); + cy.get('a[data-cy-navbar="settings"]').click(); + cy.location('pathname').should('contain', 'settings'); + cy.get('a[data-cy="edit"]').should('exist'); + cy.get('a[data-cy="add-livestream"]').should('exist'); + cy.get('a[data-cy="manage-venues"]').should('exist'); + cy.get('a[data-cy="add-cfp"]').should('exist'); + cy.get('a[data-cy="edit-schedule"]').should('exist'); + cy.get('a[data-cy="manage-labels"]').should('exist'); + cy.get('a[data-cy="setup-events"]').should('not.exist'); + cy.get('a[data-cy="scan-checkin"]').should('not.exist'); + cy.get('a[data-cy="download-csv"]').should('exist'); + cy.get('a[data-cy="download-json"]').should('exist'); + cy.get('a[data-cy="download-schedule-json"]').should('exist'); + }); +}); diff --git a/funnel/assets/cypress/integration/06_confirmProposeBtn.js b/funnel/assets/cypress/integration/06_confirmProposeBtn.js deleted file mode 100644 index 1d261f7d0..000000000 --- a/funnel/assets/cypress/integration/06_confirmProposeBtn.js +++ /dev/null @@ -1,19 +0,0 @@ -describe('Confirm propose button', function() { - const proposal = require('../fixtures/proposal.json'); - const project = require('../fixtures/project.json'); - const labels = require('../fixtures/labels.json'); - - it('Confirm Add proposal button', function() { - cy.visit('/testcypressproject'); - - cy.get('a[data-cy-project="' + project.title + '"]').click(); - cy.location('pathname').should('contain', project.url); - cy.get('a[data-cy-navbar="proposals"]').click(); - cy.location('pathname').should('contain', 'proposals'); - cy.get('a[data-cy="propose-a-session"]').should('exist'); - - cy.get('a[data-cy="profile-link"]').click(); - cy.location('pathname').should('contain', 'testcypressproject'); - cy.get('[data-cy="profile-title"]').should('contain', 'testcypressproject'); - }); -}); diff --git a/funnel/assets/cypress/integration/02_publishproject.js b/funnel/assets/cypress/integration/06_publishProject.js similarity index 56% rename from funnel/assets/cypress/integration/02_publishproject.js rename to funnel/assets/cypress/integration/06_publishProject.js index 990ecc37d..b55cb7da7 100644 --- a/funnel/assets/cypress/integration/02_publishproject.js +++ b/funnel/assets/cypress/integration/06_publishProject.js @@ -1,13 +1,21 @@ describe('Publish project', function() { - const admin = require('../fixtures/user.json').admin; + const editor = require('../fixtures/user.json').editor; + const profile = require('../fixtures/profile.json'); const project = require('../fixtures/project.json'); it('Publish project', function() { - cy.login('/testcypressproject', admin.username, admin.password); + // Failing now - project in draft state is not visible to editor + // cy.login('/' + profile.title, editor.username, editor.password); + // cy.get('[data-cy-project="' + project.title + '"]') + // .first() + // .click(); + + cy.login( + '/' + profile.title + '/' + project.url, + editor.username, + editor.password + ); - cy.get('[data-cy-project="' + project.title + '"]') - .first() - .click(); cy.location('pathname').should('contain', project.url); cy.get('a[data-cy-navbar="settings"]').click(); cy.location('pathname').should('contain', 'settings'); diff --git a/funnel/assets/cypress/integration/03_managevenue.js b/funnel/assets/cypress/integration/07_manageVenue.js similarity index 92% rename from funnel/assets/cypress/integration/03_managevenue.js rename to funnel/assets/cypress/integration/07_manageVenue.js index 7674b23dd..247ea83e8 100644 --- a/funnel/assets/cypress/integration/03_managevenue.js +++ b/funnel/assets/cypress/integration/07_manageVenue.js @@ -1,12 +1,13 @@ describe('Manage project venue', function() { - const admin = require('../fixtures/user.json').admin; + const editor = require('../fixtures/user.json').editor; + const profile = require('../fixtures/profile.json'); const project = require('../fixtures/project.json'); it('Add venue', function() { cy.login( - '/testcypressproject/' + project.url, - admin.username, - admin.password + '/' + profile.title + '/' + project.url, + editor.username, + editor.password ); cy.get('a[data-cy-navbar="settings"]').click(); diff --git a/funnel/assets/cypress/integration/04_addCFP.js b/funnel/assets/cypress/integration/08_addCFP.js similarity index 86% rename from funnel/assets/cypress/integration/04_addCFP.js rename to funnel/assets/cypress/integration/08_addCFP.js index 2791adb76..29e5e189c 100644 --- a/funnel/assets/cypress/integration/04_addCFP.js +++ b/funnel/assets/cypress/integration/08_addCFP.js @@ -1,13 +1,14 @@ describe('Add CFP to project', function() { - const admin = require('../fixtures/user.json').admin; + const editor = require('../fixtures/user.json').editor; const cfp = require('../fixtures/cfp.json'); + const profile = require('../fixtures/profile.json'); const project = require('../fixtures/project.json'); it('Add CFP', function() { cy.login( - '/testcypressproject/' + project.url, - admin.username, - admin.password + '/' + profile.title + '/' + project.url, + editor.username, + editor.password ); cy.get('a[data-cy-navbar="settings"]').click(); diff --git a/funnel/assets/cypress/integration/05_addLabels.js b/funnel/assets/cypress/integration/09_addLabels.js similarity index 93% rename from funnel/assets/cypress/integration/05_addLabels.js rename to funnel/assets/cypress/integration/09_addLabels.js index 64e6e748d..6f030a052 100644 --- a/funnel/assets/cypress/integration/05_addLabels.js +++ b/funnel/assets/cypress/integration/09_addLabels.js @@ -1,13 +1,14 @@ describe('Add labels to project', function() { - const admin = require('../fixtures/user.json').admin; + const editor = require('../fixtures/user.json').editor; + const profile = require('../fixtures/profile.json'); const project = require('../fixtures/project.json'); const labels = require('../fixtures/labels.json'); it('Add labels', function() { cy.login( - '/testcypressproject/' + project.url, - admin.username, - admin.password + '/' + profile.title + '/' + project.url, + editor.username, + editor.password ); cy.get('a[data-cy-navbar="settings"]').click(); @@ -102,8 +103,8 @@ describe('Add labels to project', function() { .trigger('mouseover', { which: 1, force: true, view: window }) .trigger('mousedown', { which: 1, force: true, view: window }) .trigger('mousemove', { - pageX: 230, - pageY: 550, + pageX: 500, + pageY: 610, force: true, view: window, }) diff --git a/funnel/assets/cypress/integration/08_respondToAttend.js b/funnel/assets/cypress/integration/10_respondToAttend.js similarity index 100% rename from funnel/assets/cypress/integration/08_respondToAttend.js rename to funnel/assets/cypress/integration/10_respondToAttend.js diff --git a/funnel/assets/cypress/integration/07_addProposal.js b/funnel/assets/cypress/integration/11_addProposal.js similarity index 94% rename from funnel/assets/cypress/integration/07_addProposal.js rename to funnel/assets/cypress/integration/11_addProposal.js index 4f1510908..118985eeb 100644 --- a/funnel/assets/cypress/integration/07_addProposal.js +++ b/funnel/assets/cypress/integration/11_addProposal.js @@ -1,11 +1,12 @@ describe('Add a new proposal', function() { const user = require('../fixtures/user.json').user; + const profile = require('../fixtures/profile.json'); const proposal = require('../fixtures/proposal.json'); const project = require('../fixtures/project.json'); const labels = require('../fixtures/labels.json'); it('Add proposal', function() { - cy.login('/testcypressproject', user.username, user.password); + cy.login('/' + profile.title, user.username, user.password); cy.get('a[data-cy-project="' + project.title + '"]').click(); cy.location('pathname').should('contain', project.url); diff --git a/funnel/assets/cypress/integration/09_confirmProposal.js b/funnel/assets/cypress/integration/12_confirmProposal.js similarity index 89% rename from funnel/assets/cypress/integration/09_confirmProposal.js rename to funnel/assets/cypress/integration/12_confirmProposal.js index ae404f5ed..0f06a9f70 100644 --- a/funnel/assets/cypress/integration/09_confirmProposal.js +++ b/funnel/assets/cypress/integration/12_confirmProposal.js @@ -1,11 +1,12 @@ describe('Confirm proposal', function() { - const admin = require('../fixtures/user.json').admin; + const editor = require('../fixtures/user.json').editor; + const profile = require('../fixtures/profile.json'); const proposal = require('../fixtures/proposal.json'); const project = require('../fixtures/project.json'); const labels = require('../fixtures/labels.json'); it('Confirm proposal', function() { - cy.login('/testcypressproject', admin.username, admin.password); + cy.login('/' + profile.title, editor.username, editor.password); cy.get('a[data-cy-project="' + project.title + '"]').click(); cy.location('pathname').should('contain', project.url); diff --git a/funnel/assets/cypress/integration/11_addLivestream.js b/funnel/assets/cypress/integration/13_addLivestream.js similarity index 84% rename from funnel/assets/cypress/integration/11_addLivestream.js rename to funnel/assets/cypress/integration/13_addLivestream.js index 7903c8f8c..4027c2e6e 100644 --- a/funnel/assets/cypress/integration/11_addLivestream.js +++ b/funnel/assets/cypress/integration/13_addLivestream.js @@ -1,9 +1,10 @@ describe('Add livestream', function() { - const admin = require('../fixtures/user.json').admin; + const editor = require('../fixtures/user.json').editor; + const profile = require('../fixtures/profile.json'); const project = require('../fixtures/project.json'); it('Adding livestream youtube url to project', function() { - cy.login('/testcypressproject', admin.username, admin.password); + cy.login('/' + profile.title, editor.username, editor.password); cy.get('[data-cy-project="' + project.title + '"]') .first() diff --git a/funnel/assets/cypress/integration/10_updateSchedule.js b/funnel/assets/cypress/integration/14_updateSchedule.js similarity index 94% rename from funnel/assets/cypress/integration/10_updateSchedule.js rename to funnel/assets/cypress/integration/14_updateSchedule.js index 7ba9fc3c3..0900d8630 100644 --- a/funnel/assets/cypress/integration/10_updateSchedule.js +++ b/funnel/assets/cypress/integration/14_updateSchedule.js @@ -1,7 +1,8 @@ describe('Add session to schedule and publish', function() { - const admin = require('../fixtures/user.json').admin; + const editor = require('../fixtures/user.json').editor; const session = require('../fixtures/session.json'); const proposal = require('../fixtures/proposal.json'); + const profile = require('../fixtures/profile.json'); const project = require('../fixtures/project.json'); it('Update schedule', function() { @@ -11,7 +12,7 @@ describe('Add session to schedule and publish', function() { cy.route('**/schedule').as('session-form'); cy.route('POST', '**/schedule').as('add-session'); - cy.login('/testcypressproject', admin.username, admin.password); + cy.login('/' + profile.title, editor.username, editor.password); cy.get('a[data-cy-project="' + project.title + '"]').click(); cy.location('pathname').should('contain', project.url); diff --git a/funnel/assets/cypress/integration/15_verifyConciergeRoles.js b/funnel/assets/cypress/integration/15_verifyConciergeRoles.js new file mode 100644 index 000000000..2a79d3ef3 --- /dev/null +++ b/funnel/assets/cypress/integration/15_verifyConciergeRoles.js @@ -0,0 +1,26 @@ +describe('Verify roles of concierge', function() { + const concierge = require('../fixtures/user.json').concierge; + const project = require('../fixtures/project.json'); + + it('Access available for concierge in project settings', function() { + cy.login('/', concierge.username, concierge.password); + + cy.get('[data-cy-project="' + project.title + '"]') + .first() + .click(); + cy.location('pathname').should('contain', project.url); + cy.get('a[data-cy-navbar="settings"]').click(); + cy.location('pathname').should('contain', 'settings'); + cy.get('a[data-cy="edit"]').should('not.exist'); + cy.get('a[data-cy="add-livestream"]').should('not.exist'); + cy.get('a[data-cy="manage-venues"]').should('not.exist'); + cy.get('a[data-cy="add-cfp"]').should('not.exist'); + cy.get('a[data-cy="edit-schedule"]').should('not.exist'); + cy.get('a[data-cy="manage-labels"]').should('not.exist'); + cy.get('a[data-cy="setup-events"]').should('exist'); + cy.get('a[data-cy="scan-checkin"]').should('not.exist'); + cy.get('a[data-cy="download-csv"]').should('exist'); + cy.get('a[data-cy="download-json"]').should('exist'); + cy.get('a[data-cy="download-schedule-json"]').should('exist'); + }); +}); diff --git a/funnel/assets/cypress/integration/12_setupEvent.js b/funnel/assets/cypress/integration/16_setupEvent.js similarity index 79% rename from funnel/assets/cypress/integration/12_setupEvent.js rename to funnel/assets/cypress/integration/16_setupEvent.js index ad2e50699..bb6705f9b 100644 --- a/funnel/assets/cypress/integration/12_setupEvent.js +++ b/funnel/assets/cypress/integration/16_setupEvent.js @@ -1,19 +1,19 @@ describe('Setup event for checkin', function() { - const admin = require('../fixtures/user.json').admin; + const concierge = require('../fixtures/user.json').concierge; const project = require('../fixtures/project.json'); const events = require('../fixtures/events.json'); const participants = require('../fixtures/participants.json'); it('Setup event for checkin', function() { - cy.login('/testcypressproject', admin.username, admin.password); + cy.login('/', concierge.username, concierge.password); - cy.get('[data-cy-project="' + project.title + '"]') + cy.get('[data-cy-title="' + project.title + '"]') .first() .click(); cy.location('pathname').should('contain', project.url); cy.get('a[data-cy-navbar="settings"]').click(); cy.location('pathname').should('contain', 'settings'); - cy.get('a[data-cy="checkin"').click(); + cy.get('a[data-cy="setup-events"').click(); cy.location('pathname').should('contain', '/admin'); cy.fixture('events').then(events => { diff --git a/funnel/assets/cypress/integration/13_synctickets.js b/funnel/assets/cypress/integration/17_synctickets.js similarity index 90% rename from funnel/assets/cypress/integration/13_synctickets.js rename to funnel/assets/cypress/integration/17_synctickets.js index de5ddf82a..7b11dc97b 100644 --- a/funnel/assets/cypress/integration/13_synctickets.js +++ b/funnel/assets/cypress/integration/17_synctickets.js @@ -1,20 +1,20 @@ describe('Sync tickets from Boxoffice', function() { - const admin = require('../fixtures/user.json').admin; + const concierge = require('../fixtures/user.json').concierge; const user = require('../fixtures/user.json').user; const project = require('../fixtures/project.json'); const events = require('../fixtures/events.json'); const { ticket_client } = require('../fixtures/boxoffice.js'); it('Sync tickets from Boxoffice', function() { - cy.login('/testcypressproject', admin.username, admin.password); + cy.login('/', concierge.username, concierge.password); - cy.get('[data-cy-project="' + project.title + '"]') + cy.get('[data-cy-title="' + project.title + '"]') .first() .click(); cy.location('pathname').should('contain', project.url); cy.get('a[data-cy-navbar="settings"]').click(); cy.location('pathname').should('contain', 'settings'); - cy.get('a[data-cy="checkin"').click(); + cy.get('a[data-cy="setup-events"').click(); cy.location('pathname').should('contain', '/admin'); cy.get('a[data-cy="new-ticket-client"').click(); diff --git a/funnel/assets/cypress/integration/14_checkin.js b/funnel/assets/cypress/integration/18_checkin.js similarity index 90% rename from funnel/assets/cypress/integration/14_checkin.js rename to funnel/assets/cypress/integration/18_checkin.js index 0dd7417ca..21e21dedb 100644 --- a/funnel/assets/cypress/integration/14_checkin.js +++ b/funnel/assets/cypress/integration/18_checkin.js @@ -1,12 +1,13 @@ describe('Checkin of attendees', function() { - const admin = require('../fixtures/user.json').admin; + const concierge = require('../fixtures/user.json').concierge; const user = require('../fixtures/user.json').user; + const profile = require('../fixtures/profile.json'); const project = require('../fixtures/project.json'); const events = require('../fixtures/events.json'); const participants = require('../fixtures/participants.json'); it('Checkin of attendees', function() { - cy.login('/testcypressproject', admin.username, admin.password); + cy.login('/', concierge.username, concierge.password); cy.get('[data-cy-project="' + project.title + '"]') .first() @@ -14,7 +15,7 @@ describe('Checkin of attendees', function() { cy.location('pathname').should('contain', project.url); cy.get('a[data-cy-navbar="settings"]').click(); cy.location('pathname').should('contain', 'settings'); - cy.get('a[data-cy="checkin"').click(); + cy.get('a[data-cy="setup-events"').click(); cy.location('pathname').should('contain', '/admin'); cy.fixture('participants').then(participants => { diff --git a/funnel/assets/cypress/integration/19_verifyUsherRoles.js b/funnel/assets/cypress/integration/19_verifyUsherRoles.js new file mode 100644 index 000000000..7374e021d --- /dev/null +++ b/funnel/assets/cypress/integration/19_verifyUsherRoles.js @@ -0,0 +1,26 @@ +describe('Verify roles of usher', function() { + const usher = require('../fixtures/user.json').usher; + const project = require('../fixtures/project.json'); + + it('Access available for usher in project settings', function() { + cy.login('/', usher.username, usher.password); + + cy.get('[data-cy-project="' + project.title + '"]') + .first() + .click(); + cy.location('pathname').should('contain', project.url); + cy.get('a[data-cy-navbar="settings"]').click(); + cy.location('pathname').should('contain', 'settings'); + cy.get('a[data-cy="edit"]').should('not.exist'); + cy.get('a[data-cy="add-livestream"]').should('not.exist'); + cy.get('a[data-cy="manage-venues"]').should('not.exist'); + cy.get('a[data-cy="add-cfp"]').should('not.exist'); + cy.get('a[data-cy="edit-schedule"]').should('not.exist'); + cy.get('a[data-cy="manage-labels"]').should('not.exist'); + cy.get('a[data-cy="setup-events"]').should('not.exist'); + cy.get('a[data-cy="scan-checkin"]').should('exist'); + cy.get('a[data-cy="download-csv"]').should('exist'); + cy.get('a[data-cy="download-json"]').should('exist'); + cy.get('a[data-cy="download-schedule-json"]').should('exist'); + }); +}); diff --git a/funnel/assets/cypress/integration/15_badge.js b/funnel/assets/cypress/integration/20_badge.js similarity index 84% rename from funnel/assets/cypress/integration/15_badge.js rename to funnel/assets/cypress/integration/20_badge.js index fa03bda40..4fdcb089e 100644 --- a/funnel/assets/cypress/integration/15_badge.js +++ b/funnel/assets/cypress/integration/20_badge.js @@ -1,5 +1,6 @@ describe('View participant badge', function() { - const admin = require('../fixtures/user.json').admin; + const usher = require('../fixtures/user.json').usher; + const profile = require('../fixtures/profile.json'); const project = require('../fixtures/project.json'); const events = require('../fixtures/events.json'); const participants = require('../fixtures/participants.json'); @@ -9,7 +10,7 @@ describe('View participant badge', function() { cy.route('POST', '**/participants/checkin?*').as('checkin'); cy.route('**/participants/json').as('participant-list'); - cy.login('/testcypressproject', admin.username, admin.password); + cy.login('/', usher.username, usher.password); cy.get('[data-cy-project="' + project.title + '"]') .first() @@ -17,7 +18,7 @@ describe('View participant badge', function() { cy.location('pathname').should('contain', project.url); cy.get('a[data-cy-navbar="settings"]').click(); cy.location('pathname').should('contain', 'settings'); - cy.get('a[data-cy="checkin"').click(); + cy.get('a[data-cy="setup-events"').click(); cy.location('pathname').should('contain', '/admin'); cy.get('a[data-cy="' + events[1].title + '"]').click(); var firstname = participants[2].fullname.split(' ')[0]; diff --git a/funnel/assets/cypress/integration/16_printbadge.js b/funnel/assets/cypress/integration/21_printbadge.js similarity index 88% rename from funnel/assets/cypress/integration/16_printbadge.js rename to funnel/assets/cypress/integration/21_printbadge.js index b42349c25..8c9e2807b 100644 --- a/funnel/assets/cypress/integration/16_printbadge.js +++ b/funnel/assets/cypress/integration/21_printbadge.js @@ -1,5 +1,5 @@ describe('View badges to be printed', function() { - const admin = require('../fixtures/user.json').admin; + const usher = require('../fixtures/user.json').usher; const user = require('../fixtures/user.json').user; const project = require('../fixtures/project.json'); const events = require('../fixtures/events.json'); @@ -10,7 +10,7 @@ describe('View badges to be printed', function() { cy.route('POST', '**/participants/checkin?*').as('checkin'); cy.route('**/participants/json').as('participant-list'); - cy.login('/testcypressproject', admin.username, admin.password); + cy.login('/', usher.username, usher.password); cy.get('[data-cy-project="' + project.title + '"]') .first() @@ -18,7 +18,7 @@ describe('View badges to be printed', function() { cy.location('pathname').should('contain', project.url); cy.get('a[data-cy-navbar="settings"]').click(); cy.location('pathname').should('contain', 'settings'); - cy.get('a[data-cy="checkin"').click(); + cy.get('a[data-cy="setup-events"').click(); cy.location('pathname').should('contain', '/admin'); cy.get('a[data-cy="' + events[0].title + '"]').click(); var firstname1 = participants[0].fullname.split(' ')[0]; diff --git a/funnel/assets/cypress/integration/17_updatebadgeprintstatus.js b/funnel/assets/cypress/integration/22_updatebadgeprintstatus.js similarity index 96% rename from funnel/assets/cypress/integration/17_updatebadgeprintstatus.js rename to funnel/assets/cypress/integration/22_updatebadgeprintstatus.js index aa81c9be9..6c17e38ff 100644 --- a/funnel/assets/cypress/integration/17_updatebadgeprintstatus.js +++ b/funnel/assets/cypress/integration/22_updatebadgeprintstatus.js @@ -18,7 +18,7 @@ describe('View and update print status of badge', function() { cy.get('a[data-cy-navbar="settings"]').click(); cy.location('pathname').should('contain', 'settings'); - cy.get('a[data-cy="checkin"').click(); + cy.get('a[data-cy="setup-events"').click(); cy.location('pathname').should('contain', '/admin'); cy.get('a[data-cy="' + events[0].title + '"]').click(); cy.get('select#badge_printed').select('Printed', { force: true }); diff --git a/funnel/assets/cypress/integration/18_verifyattendeeList.js b/funnel/assets/cypress/integration/22_verifyattendeeList.js similarity index 81% rename from funnel/assets/cypress/integration/18_verifyattendeeList.js rename to funnel/assets/cypress/integration/22_verifyattendeeList.js index 30bf07b51..97a95810e 100644 --- a/funnel/assets/cypress/integration/18_verifyattendeeList.js +++ b/funnel/assets/cypress/integration/22_verifyattendeeList.js @@ -1,10 +1,10 @@ describe('Verify attendee list', function() { - const admin = require('../fixtures/user.json').admin; + const concierge = require('../fixtures/user.json').concierge; const user = require('../fixtures/user.json').user; const project = require('../fixtures/project.json'); it('Verify list of attendees who have responded yes to attending a project', function() { - cy.login('/testcypressproject', admin.username, admin.password); + cy.login('/testcypressproject', concierge.username, concierge.password); cy.get('[data-cy-project="' + project.title + '"]') .first() diff --git a/funnel/assets/cypress/integration/20_viewSchedule.js b/funnel/assets/cypress/integration/23_viewSchedule.js similarity index 100% rename from funnel/assets/cypress/integration/20_viewSchedule.js rename to funnel/assets/cypress/integration/23_viewSchedule.js diff --git a/funnel/assets/cypress/integration/21_addSessionVideo.js b/funnel/assets/cypress/integration/24_addSessionVideo.js similarity index 100% rename from funnel/assets/cypress/integration/21_addSessionVideo.js rename to funnel/assets/cypress/integration/24_addSessionVideo.js diff --git a/funnel/assets/cypress/integration/22_viewVideos.js b/funnel/assets/cypress/integration/25_viewVideos.js similarity index 100% rename from funnel/assets/cypress/integration/22_viewVideos.js rename to funnel/assets/cypress/integration/25_viewVideos.js diff --git a/funnel/assets/cypress/integration/23_removeSessionVideo.js b/funnel/assets/cypress/integration/26_removeSessionVideo.js similarity index 90% rename from funnel/assets/cypress/integration/23_removeSessionVideo.js rename to funnel/assets/cypress/integration/26_removeSessionVideo.js index 416edc8c9..10b68bef2 100644 --- a/funnel/assets/cypress/integration/23_removeSessionVideo.js +++ b/funnel/assets/cypress/integration/26_removeSessionVideo.js @@ -1,5 +1,5 @@ describe('Remove video to session', function() { - const admin = require('../fixtures/user.json').admin; + const editor = require('../fixtures/user.json').editor; const project = require('../fixtures/project.json'); const proposal = require('../fixtures/proposal.json'); @@ -7,7 +7,7 @@ describe('Remove video to session', function() { cy.server(); cy.route('**/viewsession-popup').as('view-session'); - cy.login('/', admin.username, admin.password); + cy.login('/', editor.username, editor.password); cy.get('.upcoming') .find('.card--upcoming') diff --git a/funnel/assets/cypress/integration/27_viewCrew.js b/funnel/assets/cypress/integration/27_viewCrew.js new file mode 100644 index 000000000..b8f45efb7 --- /dev/null +++ b/funnel/assets/cypress/integration/27_viewCrew.js @@ -0,0 +1,36 @@ +describe('View crew', function() { + const admin = require('../fixtures/user.json').admin; + const concierge = require('../fixtures/user.json').concierge; + const usher = require('../fixtures/user.json').usher; + const project = require('../fixtures/project.json'); + + it('View crew of the project', function() { + cy.visit('/'); + cy.get('.upcoming') + .find('.card--upcoming') + .contains(project.title) + .click({ force: true }); + cy.location('pathname').should('contain', project.url); + cy.get('a[data-cy-navbar="crew"]').click(); + cy.get('button[data-cy-btn="add-member"]').should('not.exist'); + cy.get('[data-cy="member"]') + .contains(admin.username) + .parents('.user-box') + .find('[data-cy="role"]') + .contains('Editor'); + cy.get('[data-cy="member"]') + .contains(concierge.username) + .parents('.user-box') + .find('[data-cy="role"]') + .contains('Concierge'); + cy.get('[data-cy="member"]') + .contains(usher.username) + .parents('.user-box') + .find('[data-cy="role"]') + .contains('Usher'); + cy.get('[data-cy="member"]') + .contains(admin.username) + .click(); + cy.get('#member-form', { timeout: 10000 }).should('not.be.visible'); + }); +}); diff --git a/funnel/assets/cypress/integration/19_search.js b/funnel/assets/cypress/integration/28_search.js similarity index 100% rename from funnel/assets/cypress/integration/19_search.js rename to funnel/assets/cypress/integration/28_search.js diff --git a/funnel/assets/cypress/support/commands.js b/funnel/assets/cypress/support/commands.js index d8393ce93..e3ff32793 100644 --- a/funnel/assets/cypress/support/commands.js +++ b/funnel/assets/cypress/support/commands.js @@ -25,19 +25,6 @@ Cypress.Commands.add('login', (route, username, password) => { cy.wait('@login', { timeout: 20000 }); }); -Cypress.Commands.add('relogin', route => { - cy.visit(route) - .get('#hgnav') - .then($header => { - if ($header.find('.header__button').length > 0) { - cy.get('#hgnav') - .find('.header__button') - .click(); - cy.location('pathname').should('include', route); - } - }); -}); - Cypress.Commands.add('logout', () => { cy.get('a[data-cy="account-link"]').click(); cy.get('a[data-cy="my-account"]').click(); @@ -48,6 +35,37 @@ Cypress.Commands.add('logout', () => { cy.clearCookies(); }); +Cypress.Commands.add('add_member', (username, role) => { + cy.server(); + cy.route('**/membership/new').as('member-form'); + cy.route('POST', '**/membership/new').as('add-member'); + + cy.get('button[data-cy-btn="add-member"]').click(); + cy.wait('@member-form'); + cy.get('.select2-selection__arrow').click({ multiple: true }); + cy.get('.select2-search__field').type(username, { + force: true, + }); + cy.get('.select2-results__option--highlighted', { timeout: 20000 }).should( + 'be.visible' + ); + cy.get('.select2-results__option') + .contains(username) + .click(); + cy.get('.select2-results__options', { timeout: 10000 }).should('not.visible'); + cy.get(`#is_${role}`).click(); + cy.get('button') + .contains('Add member') + .click(); + cy.wait('@add-member'); + var roleString = role[0].toUpperCase() + role.slice(1); + cy.get('[data-cy="member"]') + .contains(username) + .parents('.user-box') + .find('[data-cy="role"]') + .contains(roleString); +}); + Cypress.Commands.add('checkin', participant => { cy.server(); cy.route('POST', '**/participants/checkin').as('checkin'); diff --git a/funnel/assets/js/event.js b/funnel/assets/js/event.js index 19403d68e..a6047436e 100644 --- a/funnel/assets/js/event.js +++ b/funnel/assets/js/event.js @@ -87,7 +87,7 @@ const Queue = function(queueName) { }; const ParticipantTable = { - init({ isAdmin, isConcierge, checkinUrl, participantlistUrl, eventName }) { + init({ isConcierge, isUsher, checkinUrl, participantlistUrl, eventName }) { Ractive.DEBUG = false; const count = new Ractive({ @@ -107,8 +107,8 @@ const ParticipantTable = { checkinUrl, checkinQ: new Queue(`${eventName}-checkin-queue`), cancelcheckinQ: new Queue(`${eventName}-cancelcheckin-queue`), - isAdmin, isConcierge, + isUsher, svgIconUrl: window.HasGeek.config.svgIconUrl, getCsrfToken() { return $('meta[name="csrf-token"]').attr('content'); diff --git a/funnel/assets/js/membership.js b/funnel/assets/js/membership.js new file mode 100644 index 000000000..8f0ed564d --- /dev/null +++ b/funnel/assets/js/membership.js @@ -0,0 +1,161 @@ +import Vue from 'vue/dist/vue.min'; +import VS2 from 'vue-script2'; +import { Utils } from './util'; + +const Membership = { + init({ + newMemberUrl, + members = '', + roles = [], + divElem, + memberTemplate, + isUserProfileAdmin, + }) { + Vue.use(VS2); + + const memberUI = Vue.component('member', { + template: memberTemplate, + props: ['member'], + methods: { + rolesCount(member) { + let count = 0; + if (member.is_editor) count += 1; + if (member.is_concierge) count += 1; + if (member.is_usher) count += 1; + return count - 1; + }, + }, + }); + + /* eslint-disable no-new */ + new Vue({ + el: divElem, + components: { + memberUI, + }, + data() { + return { + newMemberUrl, + members: members.length > 0 ? members : '', + isUserProfileAdmin, + roles: roles, + memberForm: '', + activeMember: '', + errorMsg: '', + view: 'name', + search: '', + showInfo: false, + svgIconUrl: window.HasGeek.config.svgIconUrl, + isMobile: false, + }; + }, + methods: { + fetchForm(event, url, member = '') { + event.preventDefault(); + if (this.isUserProfileAdmin) { + this.activeMember = member; + const app = this; + $.ajax({ + type: 'GET', + url, + timeout: window.HasGeek.config.ajaxTimeout, + dataType: 'json', + success(data) { + const vueFormHtml = data.form; + app.memberForm = vueFormHtml.replace(/\bscript\b/g, 'script2'); + $('#member-form').modal('show'); + }, + }); + } + }, + activateForm() { + const formId = Utils.getElementId(this.memberForm); + const url = Utils.getActionUrl(formId); + const onSuccess = responseData => { + this.closeForm(); + if (responseData.memberships) { + this.updateMembersList(responseData.memberships); + this.onChange(); + } + }; + const onError = response => { + this.errorMsg = Utils.formErrorHandler(formId, response); + }; + window.Baseframe.Forms.handleFormSubmit( + formId, + url, + onSuccess, + onError, + {} + ); + }, + updateMembersList(membersList) { + this.members = membersList.length > 0 ? membersList : ''; + }, + filter(event, action) { + event.preventDefault(); + this.view = action; + }, + closeForm(event = '') { + if (event) event.preventDefault(); + $.modal.close(); + this.errorMsg = ''; + }, + onChange() { + if (this.search) { + this.members.filter(member => { + member.hide = + member.user.fullname + .toLowerCase() + .indexOf(this.search.toLowerCase()) === -1; + return true; + }); + } + }, + collapse(event, role) { + event.preventDefault(); + role.showMembers = !role.showMembers; + }, + showRoleDetails(event) { + event.preventDefault(); + this.showInfo = !this.showInfo; + }, + onWindowResize() { + this.isMobile = + $(window).width() < window.HasGeek.config.mobileBreakpoint; + }, + }, + computed: { + Form() { + const template = this.memberForm ? this.memberForm : '
'; + const isFormTemplate = this.memberForm ? true : ''; + return { + template, + mounted() { + if (isFormTemplate) { + this.$parent.activateForm(); + } + }, + }; + }, + deleteURL() { + return this.activeMember.urls.delete; + }, + }, + mounted() { + $('#member-form').on($.modal.CLOSE, () => { + this.closeForm(); + }); + }, + created() { + window.addEventListener('resize', this.onWindowResize); + }, + }); + }, +}; + +$(() => { + window.HasGeek.Membership = function(config) { + Membership.init(config); + }; +}); diff --git a/funnel/assets/js/util.js b/funnel/assets/js/util.js index 63b184e5a..d8f14dff1 100644 --- a/funnel/assets/js/util.js +++ b/funnel/assets/js/util.js @@ -173,6 +173,40 @@ export const Utils = { } }); }, + getElementId(htmlString) { + return htmlString.match(/id="(.*?)"/)[1]; + }, + formErrorHandler(formId, errorResponse) { + let errorMsg = ''; + // xhr readyState '4' indicates server has received the request & response is ready + if (errorResponse.readyState === 4) { + if (errorResponse.status === 500) { + errorMsg = 'Internal Server Error'; + } else { + if (errorResponse.responseJSON.errors) { + window.Baseframe.Forms.showValidationErrors( + formId, + errorResponse.responseJSON.errors + ); + } + errorMsg = errorResponse.responseJSON.message + ? errorResponse.responseJSON.message + : 'Error'; + } + } else { + errorMsg = 'Unable to connect. Please try again.'; + } + $(`#${formId}`) + .find('button[type="submit"]') + .prop('disabled', false); + $(`#${formId}`) + .find('.loading') + .addClass('mui--hide'); + return errorMsg; + }, + getActionUrl(formId) { + return $(`#${formId}`).attr('action'); + }, }; export const ScrollActiveMenu = { diff --git a/funnel/assets/package-lock.json b/funnel/assets/package-lock.json index 189a29623..c85799728 100644 --- a/funnel/assets/package-lock.json +++ b/funnel/assets/package-lock.json @@ -8347,6 +8347,29 @@ "get-value": "^2.0.6", "is-extendable": "^0.1.1", "set-value": "^2.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + } + } } }, "unique-filename": { @@ -8541,6 +8564,11 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz", "integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==" }, + "vue-script2": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vue-script2/-/vue-script2-2.1.0.tgz", + "integrity": "sha512-EDUOjQBFvhkJXwmWuUR9ijlF7/4JtmvjXSKaHSa/LNTMy9ltjgKgYB68aqlxgq8ORdSxowd5eo24P1syjZJnBA==" + }, "watchpack": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.1.tgz", diff --git a/funnel/assets/package.json b/funnel/assets/package.json index 88a5374bf..7a5c766ef 100644 --- a/funnel/assets/package.json +++ b/funnel/assets/package.json @@ -38,6 +38,7 @@ "leaflet": "^1.4.0", "ractive": "^0.7.3", "vcards-js": "^2.10.0", - "vue": "^2.6.10" + "vue": "^2.6.10", + "vue-script2": "^2.1.0" } } diff --git a/funnel/assets/service-worker-template.js b/funnel/assets/service-worker-template.js index 9f198c99d..f4496dcd0 100644 --- a/funnel/assets/service-worker-template.js +++ b/funnel/assets/service-worker-template.js @@ -3,50 +3,82 @@ workbox.core.clientsClaim(); workbox.precaching.precacheAndRoute(self.__precacheManifest); -workbox.routing.registerRoute(new RegExp('/^https?\:\/\/static.*/'), new workbox.strategies.NetworkFirst({ - "cacheName": "assets" -}), 'GET'); +workbox.routing.registerRoute( + new RegExp('/^https?://static.*/'), + new workbox.strategies.NetworkFirst({ + cacheName: 'assets', + }), + 'GET' +); -workbox.routing.registerRoute(new RegExp('/^https?:\/\/hasgeek.com\/*/'), new workbox.strategies.NetworkFirst({ - "cacheName": "assets" -}), 'GET'); +workbox.routing.registerRoute( + new RegExp('/^https?://hasgeek.com/*/'), + new workbox.strategies.NetworkFirst({ + cacheName: 'assets', + }), + 'GET' +); //For development setup caching of assets -workbox.routing.registerRoute(new RegExp('/^http:\/\/localhost:3000\/static/'), new workbox.strategies.NetworkFirst({ - "cacheName": "baseframe-local" -}), 'GET'); +workbox.routing.registerRoute( + new RegExp('/^http://localhost:3000/static/'), + new workbox.strategies.NetworkFirst({ + cacheName: 'baseframe-local', + }), + 'GET' +); -workbox.routing.registerRoute(new RegExp('/^https?:\/\/images\.hasgeek\.com\/embed\/file\/*/'), new workbox.strategies.NetworkFirst({ - "cacheName": "images" -}), 'GET'); +workbox.routing.registerRoute( + new RegExp('/^https?://images.hasgeek.com/embed/file/*/'), + new workbox.strategies.NetworkFirst({ + cacheName: 'images', + }), + 'GET' +); -workbox.routing.registerRoute(new RegExp('/^https?:\/\/fonts.googleapis.com\/*/'), new workbox.strategies.NetworkFirst({ - "cacheName": "fonts" -}), 'GET'); +workbox.routing.registerRoute( + new RegExp('/^https?://fonts.googleapis.com/*/'), + new workbox.strategies.NetworkFirst({ + cacheName: 'fonts', + }), + 'GET' +); -workbox.routing.registerRoute(new RegExp('/^https?\:\/\/ajax.googleapis.com\/*/'), new workbox.strategies.NetworkFirst({ - "cacheName": "cdn-libraries" -}), 'GET'); +workbox.routing.registerRoute( + new RegExp('/^https?://ajax.googleapis.com/*/'), + new workbox.strategies.NetworkFirst({ + cacheName: 'cdn-libraries', + }), + 'GET' +); -workbox.routing.registerRoute(new RegExp('/(.*)'), new workbox.strategies.NetworkFirst({ - "cacheName": "routes" -}), 'GET'); +workbox.routing.registerRoute( + new RegExp('/(.*)'), + new workbox.strategies.NetworkFirst({ + cacheName: 'routes', + }), + 'GET' +); -self.addEventListener('install', (event) => { - event.waitUntil(caches.open('offline').then((cache) => cache.addAll(['/api/1/template/offline']))); +self.addEventListener('install', event => { + event.waitUntil( + caches + .open('offline') + .then(cache => cache.addAll(['/api/1/template/offline'])) + ); }); -addEventListener('message', (event) => { +addEventListener('message', event => { if (event.data && event.data.type === 'SKIP_WAITING') { skipWaiting(); } }); -workbox.routing.setCatchHandler(({event}) => { +workbox.routing.setCatchHandler(({ event }) => { switch (event.request.destination) { case 'document': return caches.match('offline'); - break; + break; default: return Response.error(); diff --git a/funnel/assets/webpack.config.js b/funnel/assets/webpack.config.js index cee91ee5d..47a8ee5b2 100644 --- a/funnel/assets/webpack.config.js +++ b/funnel/assets/webpack.config.js @@ -60,6 +60,7 @@ module.exports = { scan_contact: path.resolve(__dirname, 'js/scan_contact.js'), contact: path.resolve(__dirname, 'js/contact.js'), search: path.resolve(__dirname, 'js/search.js'), + membership: path.resolve(__dirname, 'js/membership.js'), session_videos: path.resolve(__dirname, 'js/session_videos.js'), }, output: { diff --git a/funnel/forms/__init__.py b/funnel/forms/__init__.py index 44d468726..1eb4d29d0 100644 --- a/funnel/forms/__init__.py +++ b/funnel/forms/__init__.py @@ -6,6 +6,7 @@ from .comment import * from .label import * from .login import * +from .membership import * from .organization import * from .participant import * from .profile import * diff --git a/funnel/forms/account.py b/funnel/forms/account.py index f6238c7b0..4ffeb0ea9 100644 --- a/funnel/forms/account.py +++ b/funnel/forms/account.py @@ -111,7 +111,7 @@ class AccountForm(forms.Form): email = forms.EmailField( __("Email address"), description=__("Required for sending you tickets, invoices and notifications"), - validators=[forms.validators.DataRequired(), forms.ValidEmail()], + validators=[forms.validators.DataRequired(), forms.validators.ValidEmail()], widget_attrs={'autocorrect': 'none', 'autocapitalize': 'none'}, ) username = forms.AnnotatedTextField( @@ -166,7 +166,7 @@ def validate_email(self, field): class NewEmailAddressForm(forms.RecaptchaForm): email = forms.EmailField( __("Email address"), - validators=[forms.validators.DataRequired(), forms.ValidEmail()], + validators=[forms.validators.DataRequired(), forms.validators.ValidEmail()], widget_attrs={'autocorrect': 'none', 'autocapitalize': 'none'}, ) type = forms.RadioField( # NOQA: A003 @@ -201,7 +201,7 @@ def validate_email(self, field): class EmailPrimaryForm(forms.Form): email = forms.EmailField( __("Email address"), - validators=[forms.validators.DataRequired(), forms.ValidEmail()], + validators=[forms.validators.DataRequired(), forms.validators.ValidEmail()], widget_attrs={'autocorrect': 'none', 'autocapitalize': 'none'}, ) diff --git a/funnel/forms/auth_client.py b/funnel/forms/auth_client.py index 3313646c5..3886df783 100644 --- a/funnel/forms/auth_client.py +++ b/funnel/forms/auth_client.py @@ -123,7 +123,7 @@ def validate_client_owner(self, field): else: orgs = [ org - for org in self.edit_user.organizations_owned() + for org in self.edit_user.organizations_as_owner if org.buid == field.data ] if len(orgs) != 1: diff --git a/funnel/forms/membership.py b/funnel/forms/membership.py new file mode 100644 index 000000000..29bd1149e --- /dev/null +++ b/funnel/forms/membership.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +from baseframe import _, __ +from coaster.utils import getbool +import baseframe.forms as forms + + +class OrganizationMembershipForm(forms.Form): + # add a member to a project + user = forms.UserSelectField( + __("User"), + validators=[forms.validators.DataRequired(_(u"Please select a user"))], + description=__("Find a user by their name or email address"), + ) + is_owner = forms.RadioField( + __("Access level"), + coerce=getbool, + default=False, + choices=[ + ( + False, + __("Admin (can manage projects, but can't add or remove other admins)"), + ), + (True, __("Owner (can also manage other owners and admins)")), + ], + ) + + +class ProjectCrewMembershipForm(forms.Form): + # add a member to a project + user = forms.UserSelectField( + __("User"), + validators=[forms.validators.DataRequired(_(u"Please select a user"))], + description=__("Find a user by their name or email address"), + ) + is_editor = forms.BooleanField(__("Editor"), default=False) + is_concierge = forms.BooleanField(__("Concierge"), default=False) + is_usher = forms.BooleanField(__("Usher"), default=False) + + def validate(self, extra_validators=None): + is_valid = super(ProjectCrewMembershipForm, self).validate(extra_validators) + if not any([self.is_editor.data, self.is_concierge.data, self.is_usher.data]): + self.is_usher.errors.append("Please select one or more roles") + is_valid = False + return is_valid + + +class ProjectCrewMembershipInviteForm(forms.Form): + action = forms.SelectField( + __("Choice"), + choices=[('accept', __("Accept")), ('decline', __("Decline"))], + validators=[forms.validators.DataRequired(_(u"Please make a choice"))], + ) diff --git a/funnel/forms/organization.py b/funnel/forms/organization.py index 9be9853cc..4366ff76f 100644 --- a/funnel/forms/organization.py +++ b/funnel/forms/organization.py @@ -39,10 +39,11 @@ class OrganizationForm(forms.Form): is_public_profile = forms.BooleanField(__("Make profile page public")) def validate_name(self, field): - if self.edit_obj: - reason = self.edit_obj.validate_name_candidate(field.data) - else: - reason = Profile.validate_name_candidate(field.data) + if field.data and field.data == self.edit_obj.name: + # Don't validate if name is unchanged + return + + reason = Profile.validate_name_candidate(field.data) if not reason: return # name is available if reason == 'invalid': diff --git a/funnel/forms/profile.py b/funnel/forms/profile.py index 8928e53ff..3435fc51e 100644 --- a/funnel/forms/profile.py +++ b/funnel/forms/profile.py @@ -3,18 +3,12 @@ from baseframe import _, __ import baseframe.forms as forms +from .organization import OrganizationForm -class NewProfileForm(forms.Form): - """Create a new profile.""" - - profile = forms.RadioField( - __("Organization"), - validators=[forms.validators.DataRequired("Select an organization")], - description=__("Select the organization you’d like to create a Talkfunnel for"), - ) +__all__ = ['ProfileForm'] -class EditProfileForm(forms.Form): +class ProfileForm(OrganizationForm): """Edit a profile.""" description = forms.MarkdownField( diff --git a/funnel/forms/project.py b/funnel/forms/project.py index ad8fa6f74..51fe219e0 100644 --- a/funnel/forms/project.py +++ b/funnel/forms/project.py @@ -123,30 +123,6 @@ class ProjectForm(forms.Form): parent_project = QuerySelectField( __("Parent project"), get_label='title', allow_blank=True, blank_text=__("None") ) - - admin_team = QuerySelectField( - "Admin team", - validators=[forms.validators.DataRequired(__("Please select a team"))], - get_label='title', - allow_blank=False, - description=__("The administrators of this project"), - ) - review_team = QuerySelectField( - "Review team", - validators=[forms.validators.DataRequired(__("Please select a team"))], - get_label='title', - allow_blank=False, - description=__( - "Reviewers can see contact details of proposers, but can’t change settings" - ), - ) - checkin_team = QuerySelectField( - "Checkin team", - validators=[forms.validators.DataRequired(__("Please select a team"))], - get_label='title', - allow_blank=False, - description=__("Team members can check in users at an event"), - ) allow_rsvp = forms.BooleanField(__("Allow site visitors to RSVP (login required)")) buy_tickets_url = forms.URLField( __("URL to buy tickets"), @@ -160,10 +136,6 @@ class ProjectForm(forms.Form): ) def set_queries(self): - profile_teams = self.edit_parent.teams - self.admin_team.query = profile_teams - self.review_team.query = profile_teams - self.checkin_team.query = profile_teams if self.edit_obj is None: self.parent_project.query = self.edit_parent.projects else: diff --git a/funnel/forms/proposal.py b/funnel/forms/proposal.py index f24246f24..c7778dd6e 100644 --- a/funnel/forms/proposal.py +++ b/funnel/forms/proposal.py @@ -5,8 +5,6 @@ from coaster.auth import current_auth import baseframe.forms as forms -from ..models import Profile, Project - __all__ = [ 'ProposalForm', 'ProposalLabelsAdminForm', @@ -243,12 +241,4 @@ class ProposalMoveForm(forms.Form): ) def set_queries(self): - team_ids = [t.id for t in current_auth.user.teams] - self.target.query = ( - Project.query.join(Project.profile) - .filter( - (Project.admin_team_id.in_(team_ids)) - | (Profile.admin_team_id.in_(team_ids)) - ) - .order_by(Project.schedule_start_at.desc()) - ) + self.target.query = current_auth.user.projects_as_editor diff --git a/funnel/jobs/jobs.py b/funnel/jobs/jobs.py index c353af746..2e7812e60 100644 --- a/funnel/jobs/jobs.py +++ b/funnel/jobs/jobs.py @@ -9,6 +9,13 @@ from ..extapi.boxoffice import Boxoffice from ..extapi.explara import ExplaraAPI from ..models import Project, ProjectLocation, TicketClient, db +from ..views.helpers import send_mail + + +@rq.job('funnel') +def send_mail_async(sender, to, body, subject): + with app.app_context(): + send_mail(sender, to, body, subject) @rq.job('funnel') diff --git a/funnel/models/__init__.py b/funnel/models/__init__.py index d0a382917..b2b95eb11 100644 --- a/funnel/models/__init__.py +++ b/funnel/models/__init__.py @@ -45,3 +45,7 @@ from .saved import * # isort:skip from .session import * # isort:skip from .venue import * # isort:skip +from .membership import * # isort:skip +from .organization_membership import * # isort:skip +from .project_membership import * # isort:skip +from .proposal_membership import * # isort:skip diff --git a/funnel/models/auth_client.py b/funnel/models/auth_client.py index e68761ed5..e9cafe431 100644 --- a/funnel/models/auth_client.py +++ b/funnel/models/auth_client.py @@ -158,7 +158,7 @@ def owner_is(self, user): if not user: return False return self.user == user or ( - self.organization and self.organization in user.organizations_owned() + self.organization and self.organization in user.organizations_as_owner ) def permissions(self, user, inherited=None): @@ -208,7 +208,7 @@ def all_for(cls, user): return cls.query.filter( db.or_( cls.user == user, - cls.organization_id.in_(user.organizations_owned_ids()), + cls.organization_id.in_(user.organizations_as_owner_ids()), ) ).order_by(cls.title) @@ -239,7 +239,7 @@ class AuthClientCredential(BaseMixin, db.Model): primaryjoin=auth_client_id == AuthClient.id, backref=db.backref( 'credentials', - cascade='all, delete-orphan', + cascade='all', collection_class=attribute_mapped_collection('name'), ), ) @@ -288,7 +288,7 @@ class AuthCode(ScopeMixin, BaseMixin, db.Model): auth_client = db.relationship( AuthClient, primaryjoin=auth_client_id == AuthClient.id, - backref=db.backref('authcodes', cascade='all, delete-orphan'), + backref=db.backref('authcodes', cascade='all'), ) user_session_id = db.Column(None, db.ForeignKey('user_session.id'), nullable=True) user_session = db.relationship(UserSession) @@ -319,7 +319,7 @@ class AuthToken(ScopeMixin, BaseMixin, db.Model): _user = db.relationship( User, primaryjoin=user_id == User.id, - backref=db.backref('authtokens', lazy='dynamic', cascade='all, delete-orphan'), + backref=db.backref('authtokens', lazy='dynamic', cascade='all'), ) #: The session in which this token was issued, null for confidential clients user_session_id = db.Column(None, db.ForeignKey('user_session.id'), nullable=True) @@ -333,7 +333,7 @@ class AuthToken(ScopeMixin, BaseMixin, db.Model): auth_client = db.relationship( AuthClient, primaryjoin=auth_client_id == AuthClient.id, - backref=db.backref('authtokens', lazy='dynamic', cascade='all, delete-orphan'), + backref=db.backref('authtokens', lazy='dynamic', cascade='all'), ) #: The token token = db.Column(db.String(22), default=buid, nullable=False, unique=True) @@ -466,8 +466,8 @@ def get_for(cls, auth_client, user=None, user_session=None): auth_client=auth_client, user_session=user_session ).one_or_none() - @classmethod # NOQA: A003 - def all(cls, users): + @classmethod + def all(cls, users): # NOQA: A003 """ Return all AuthToken for the specified users. """ @@ -501,7 +501,7 @@ class AuthClientUserPermissions(BaseMixin, db.Model): user = db.relationship( User, primaryjoin=user_id == User.id, - backref=db.backref('client_permissions', cascade='all, delete-orphan'), + backref=db.backref('client_permissions', cascade='all'), ) #: AuthClient app they are assigned on auth_client_id = db.Column( @@ -510,7 +510,7 @@ class AuthClientUserPermissions(BaseMixin, db.Model): auth_client = db.relationship( AuthClient, primaryjoin=auth_client_id == AuthClient.id, - backref=db.backref('user_permissions', cascade='all, delete-orphan'), + backref=db.backref('user_permissions', cascade='all'), ) #: The permissions as a string of tokens access_permissions = db.Column( @@ -565,7 +565,7 @@ class AuthClientTeamPermissions(BaseMixin, db.Model): team = db.relationship( Team, primaryjoin=team_id == Team.id, - backref=db.backref('client_permissions', cascade='all, delete-orphan'), + backref=db.backref('client_permissions', cascade='all'), ) #: AuthClient app they are assigned on auth_client_id = db.Column( @@ -574,7 +574,7 @@ class AuthClientTeamPermissions(BaseMixin, db.Model): auth_client = db.relationship( AuthClient, primaryjoin=auth_client_id == AuthClient.id, - backref=db.backref('team_permissions', cascade='all, delete-orphan'), + backref=db.backref('team_permissions', cascade='all'), ) #: The permissions as a string of tokens access_permissions = db.Column( diff --git a/funnel/models/commentvote.py b/funnel/models/commentvote.py index 30edb0770..a1a6a93d0 100644 --- a/funnel/models/commentvote.py +++ b/funnel/models/commentvote.py @@ -73,13 +73,13 @@ class Vote(BaseMixin, db.Model): user = db.relationship( User, primaryjoin=user_id == User.id, - backref=db.backref('votes', lazy='dynamic', cascade="all, delete-orphan"), + backref=db.backref('votes', lazy='dynamic', cascade='all'), ) voteset_id = db.Column(None, db.ForeignKey('voteset.id'), nullable=False) voteset = db.relationship( Voteset, primaryjoin=voteset_id == Voteset.id, - backref=db.backref('votes', cascade="all, delete-orphan"), + backref=db.backref('votes', cascade='all'), ) votedown = db.Column(db.Boolean, default=False, nullable=False) @@ -119,18 +119,18 @@ class Comment(UuidMixin, BaseMixin, db.Model): user = db.relationship( User, primaryjoin=user_id == User.id, - backref=db.backref('comments', lazy='dynamic', cascade="all, delete-orphan"), + backref=db.backref('comments', lazy='dynamic', cascade='all'), ) commentset_id = db.Column(None, db.ForeignKey('commentset.id'), nullable=False) commentset = db.relationship( Commentset, primaryjoin=commentset_id == Commentset.id, - backref=db.backref('comments', cascade="all, delete-orphan"), + backref=db.backref('comments', cascade='all'), ) parent_id = db.Column(None, db.ForeignKey('comment.id'), nullable=True) children = db.relationship( - "Comment", backref=db.backref("parent", remote_side="Comment.id") + 'Comment', backref=db.backref('parent', remote_side='Comment.id') ) message = MarkdownColumn('message', nullable=False) @@ -227,5 +227,13 @@ def permissions(self, user, inherited=None): perms.update(['edit_comment', 'delete_comment']) return perms + def roles_for(self, actor=None, anchors=()): + roles = super(Comment, self).roles_for(actor, anchors) + roles.add('reader') + if actor is not None: + if actor == self.user: + roles.add('author') + return roles + add_search_trigger(Comment, 'search_vector') diff --git a/funnel/models/event.py b/funnel/models/event.py index e78064978..358efd8ee 100644 --- a/funnel/models/event.py +++ b/funnel/models/event.py @@ -3,6 +3,8 @@ import base64 import os +from sqlalchemy.ext.associationproxy import association_proxy + from . import BaseMixin, BaseScopedNameMixin, UuidMixin, db, with_roles from .project import Project from .user import User @@ -84,15 +86,24 @@ class Event(ScopedNameTitleMixin, db.Model): __tablename__ = 'event' project_id = db.Column(None, db.ForeignKey('project.id'), nullable=False) - project = db.relationship( - Project, backref=db.backref('events', cascade='all, delete-orphan') - ) + project = db.relationship(Project, backref=db.backref('events', cascade='all')) parent = db.synonym('project') ticket_types = db.relationship('TicketType', secondary=event_ticket_type) participants = db.relationship( 'Participant', secondary='attendee', backref='events', lazy='dynamic' ) badge_template = db.Column(db.Unicode(250), nullable=True) + + project_editors = with_roles( + association_proxy('project', 'editors'), grants={'project_editor'} + ) + project_concierges = with_roles( + association_proxy('project', 'concierges'), grants={'project_concierge'} + ) + project_ushers = with_roles( + association_proxy('project', 'ushers'), grants={'project_usher'} + ) + __table_args__ = ( db.UniqueConstraint('project_id', 'name'), db.UniqueConstraint('project_id', 'title'), @@ -109,10 +120,21 @@ class TicketType(ScopedNameTitleMixin, db.Model): project_id = db.Column(None, db.ForeignKey('project.id'), nullable=False) project = db.relationship( - Project, backref=db.backref('ticket_types', cascade='all, delete-orphan') + Project, backref=db.backref('ticket_types', cascade='all') ) parent = db.synonym('project') events = db.relationship('Event', secondary=event_ticket_type) + + project_editors = with_roles( + association_proxy('project', 'editors'), grants={'project_editor'} + ) + project_concierges = with_roles( + association_proxy('project', 'concierges'), grants={'project_concierge'} + ) + project_ushers = with_roles( + association_proxy('project', 'ushers'), grants={'project_usher'} + ) + __table_args__ = ( db.UniqueConstraint('project_id', 'name'), db.UniqueConstraint('project_id', 'title'), @@ -169,17 +191,23 @@ class Participant(UuidMixin, BaseMixin, db.Model): ) badge_printed = db.Column(db.Boolean, default=False, nullable=False) user_id = db.Column(None, db.ForeignKey('user.id'), nullable=True) - user = db.relationship( - User, backref=db.backref('participants', cascade='all, delete-orphan') - ) + user = db.relationship(User, backref=db.backref('participants', cascade='all')) project_id = db.Column(None, db.ForeignKey('project.id'), nullable=False) project = with_roles( - db.relationship( - Project, backref=db.backref('participants', cascade='all, delete-orphan') - ), + db.relationship(Project, backref=db.backref('participants', cascade='all')), read={'concierge', 'subject', 'scanner'}, ) + project_editors = with_roles( + association_proxy('project', 'editors'), grants={'project_editor'} + ) + project_concierges = with_roles( + association_proxy('project', 'concierges'), grants={'project_concierge'} + ) + project_ushers = with_roles( + association_proxy('project', 'ushers'), grants={'project_usher'} + ) + __table_args__ = (db.UniqueConstraint('project_id', 'email'),) def roles_for(self, actor, anchors=()): @@ -268,12 +296,10 @@ class Attendee(BaseMixin, db.Model): participant_id = db.Column(None, db.ForeignKey('participant.id'), nullable=False) participant = db.relationship( - Participant, backref=db.backref('attendees', cascade='all, delete-orphan') + Participant, backref=db.backref('attendees', cascade='all') ) event_id = db.Column(None, db.ForeignKey('event.id'), nullable=False) - event = db.relationship( - Event, backref=db.backref('attendees', cascade='all, delete-orphan') - ) + event = db.relationship(Event, backref=db.backref('attendees', cascade='all')) checked_in = db.Column(db.Boolean, default=False, nullable=False) __table_args__ = (db.UniqueConstraint('event_id', 'participant_id'),) @@ -298,7 +324,17 @@ class TicketClient(BaseMixin, db.Model): client_access_token = db.Column(db.Unicode(80), nullable=False) project_id = db.Column(None, db.ForeignKey('project.id'), nullable=False) project = db.relationship( - Project, backref=db.backref('ticket_clients', cascade='all, delete-orphan') + Project, backref=db.backref('ticket_clients', cascade='all') + ) + + project_editors = with_roles( + association_proxy('project', 'editors'), grants={'project_editor'} + ) + project_concierges = with_roles( + association_proxy('project', 'concierges'), grants={'project_concierge'} + ) + project_ushers = with_roles( + association_proxy('project', 'ushers'), grants={'project_usher'} ) def import_from_list(self, ticket_list): @@ -360,19 +396,19 @@ class SyncTicket(BaseMixin, db.Model): order_no = db.Column(db.Unicode(80), nullable=False) ticket_type_id = db.Column(None, db.ForeignKey('ticket_type.id'), nullable=False) ticket_type = db.relationship( - TicketType, backref=db.backref('sync_tickets', cascade='all, delete-orphan') + TicketType, backref=db.backref('sync_tickets', cascade='all') ) participant_id = db.Column(None, db.ForeignKey('participant.id'), nullable=False) participant = db.relationship( Participant, primaryjoin=participant_id == Participant.id, - backref=db.backref('sync_tickets', cascade="all, delete-orphan"), + backref=db.backref('sync_tickets', cascade='all'), ) ticket_client_id = db.Column( None, db.ForeignKey('ticket_client.id'), nullable=False ) ticket_client = db.relationship( - TicketClient, backref=db.backref('sync_tickets', cascade='all, delete-orphan') + TicketClient, backref=db.backref('sync_tickets', cascade='all') ) __table_args__ = (db.UniqueConstraint('ticket_client_id', 'order_no', 'ticket_no'),) diff --git a/funnel/models/label.py b/funnel/models/label.py index a81336ce5..90b4d569d 100644 --- a/funnel/models/label.py +++ b/funnel/models/label.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- +from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.orderinglist import ordering_list from sqlalchemy.sql import case, exists +from coaster.sqlalchemy import with_roles + from . import BaseScopedNameMixin, TSVectorType, db from .helpers import add_search_trigger from .project import Project @@ -106,6 +109,16 @@ class Label(BaseScopedNameMixin, db.Model): #: Proposals that this label is attached to proposals = db.relationship(Proposal, secondary=proposal_label, backref='labels') + project_editors = with_roles( + association_proxy('project', 'editors'), grants={'project_editor'} + ) + project_concierges = with_roles( + association_proxy('project', 'concierges'), grants={'project_concierge'} + ) + project_ushers = with_roles( + association_proxy('project', 'ushers'), grants={'project_usher'} + ) + __table_args__ = ( db.UniqueConstraint('project_id', 'name'), db.Index('ix_label_search_vector', 'search_vector', postgresql_using='gin'), @@ -239,11 +252,6 @@ def __repr__(self): else: return "