This is an automated email from the git hooks/post-receive script. New commit to branch feature/pollen-riot-js in repository pollen. See https://gitlab.nuiton.org/chorem/pollen.git commit 978e3cf325ca709cda789905e2af7daa6c7bd738 Author: Tony CHEMIT <dev@tchemit.fr> Date: Wed Feb 1 13:01:44 2017 +0100 Meilleure organisation de la configuration + édition --- .../pollen/services/service/PollService.java | 1 + pollen-ui-riot-js/src/main/web/i18n.json | 17 +- pollen-ui-riot-js/src/main/web/js/FormHelper.js | 6 +- pollen-ui-riot-js/src/main/web/js/PollForm.js | 23 ++ pollen-ui-riot-js/src/main/web/js/PollService.js | 8 +- pollen-ui-riot-js/src/main/web/tag/Pollen.tag | 6 + pollen-ui-riot-js/src/main/web/tag/poll/Poll.tag | 17 +- .../src/main/web/tag/poll/PollChoices.tag | 2 +- .../src/main/web/tag/poll/PollSettings.tag | 241 +++++++++++++-------- 9 files changed, 217 insertions(+), 104 deletions(-) diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/service/PollService.java b/pollen-services/src/main/java/org/chorem/pollen/services/service/PollService.java index 61ae54f..7da57f8 100644 --- a/pollen-services/src/main/java/org/chorem/pollen/services/service/PollService.java +++ b/pollen-services/src/main/java/org/chorem/pollen/services/service/PollService.java @@ -381,6 +381,7 @@ public class PollService extends PollenServiceSupport { toSave.setPollType(poll.getPollType()); toSave.setMaxChoiceNumber(poll.getMaxChoiceNumber()); toSave.setTitle(poll.getTitle()); + toSave.setChoiceType(poll.getChoiceType()); toSave.setBeginDate(poll.getBeginDate()); toSave.setEndDate(poll.getEndDate()); diff --git a/pollen-ui-riot-js/src/main/web/i18n.json b/pollen-ui-riot-js/src/main/web/i18n.json index 948da27..1748088 100644 --- a/pollen-ui-riot-js/src/main/web/i18n.json +++ b/pollen-ui-riot-js/src/main/web/i18n.json @@ -6,6 +6,7 @@ "poll_reopenPoll": "Réouvrir le sondage", "poll_deletePoll": "Supprimer le sondage", "poll_votes": "Votes", + "poll_settings": "Configuration", "poll_choices": "Choix", "poll_results": "Résultats", "poll_results_title": "Résultats", @@ -109,6 +110,8 @@ "poll_settings_previous": "Précédent", "poll_settings_next": "Continuer", "poll_settings_skip": "Passer", + "poll_settings_save": "Enregistrer", + "poll_settings_pollIsClosed": "Le sondage est fermé, vous ne pouvez plus l'éditer.", "poll_settings_showOptions": "Voir les options avancées", "poll_settings_hideOptions": "Masquer les options avancées", "poll_settings_resultVisibility": "Qui peut voir les résultats ?", @@ -159,7 +162,7 @@ "poll_choices_description": "Description", "poll_choices_previous": "Précédent", "poll_choices_next": "Suivant", - "poll_choices_save": "Enregister", + "poll_choices_save": "Enregistrer", "poll_choices_moreChoices": "Ajouter des choix", "poll_choices_moreChoice": "Ajouter un choix", "poll_choices_choicePeriod": "Ajout de choix", @@ -184,6 +187,7 @@ "pagination_resultsPerPage": "Results per page", "poll_choices": "Choices", "poll_votes": "Votes", + "poll_settings": "Settings", "poll_closePoll": "Close poll", "poll_reopenPoll": "Reopen poll", "poll_deletePoll": "Delete poll", @@ -290,6 +294,8 @@ "poll_settings_previous": "Previous", "poll_settings_next": "Continue", "poll_settings_skip": "Skip", + "poll_settings_save": "Save", + "poll_settings_pollIsClosed": "Poll is closed, you can't edit it any longer.", "poll_settings_showOptions": "Show options", "poll_settings_hideOptions": "Hide options", "poll_settings_resultVisibility": "Who can see results?", @@ -306,8 +312,9 @@ "poll_settings_voteVisibility_voter": "Only voters", "poll_settings_nav_poll": "Poll", "poll_settings_nav_votePeriod": "Vote period", - "poll_settings_nav_resultVisibility": "Results visibility", - "poll_settings_nav_continuousResult": "Continuous results", + "poll_settings_nav_result": "Results", + "poll_settings_nav_resultVisibility": "Visibility", + "poll_settings_nav_continuousResult": "Continuous", "poll_settings_nav_choices": "Choices", "poll_settings_nav_addChoices": "Add choices", "poll_settings_nav_limitChoices": "Limit choices", @@ -318,6 +325,10 @@ "poll_settings_nav_comments": "Comments", "poll_settings_nav_commentVisibility": "Visibility", "poll_settings_poll_configuration": "Poll configuration", + "poll_settings_nav_pollTitle": "Title", + "poll_settings_nav_pollDescription": "Description", + + "poll_settings_resultsConfiguration": "Results configuration", "poll_settings_continuousResult": "Use continuous results", "poll_settings_choicesConfiguration": "Choices configuration", "poll_settings_addChoices": "Can user add choices?", diff --git a/pollen-ui-riot-js/src/main/web/js/FormHelper.js b/pollen-ui-riot-js/src/main/web/js/FormHelper.js index 6b13fc6..04a6e9f 100644 --- a/pollen-ui-riot-js/src/main/web/js/FormHelper.js +++ b/pollen-ui-riot-js/src/main/web/js/FormHelper.js @@ -28,9 +28,7 @@ class FormHelper { Array.prototype.forEach.call(form.elements, (e) => { if (e.name) { if (e.type === "checkbox") { - if (e.checked) { - result[e.name] = true; - } + result[e.name] = !!e.checked; } else if (e.type === "datetime-local") { if (e.value !== "") { result[e.name] = moment(e.value).format('YYYY-MM-DDTHH:mm'); @@ -81,7 +79,7 @@ class FormHelper { } } else if (e.type === "radio") { Array.prototype.forEach.call(form.elements[e.name], function(r) { - if (r.value === value) { + if (r.value == value) { r.checked = true; } }); diff --git a/pollen-ui-riot-js/src/main/web/js/PollForm.js b/pollen-ui-riot-js/src/main/web/js/PollForm.js index 4bfee2a..8e70674 100644 --- a/pollen-ui-riot-js/src/main/web/js/PollForm.js +++ b/pollen-ui-riot-js/src/main/web/js/PollForm.js @@ -26,6 +26,7 @@ class PollForm { constructor() { this.service = require('./PollService'); + this.FormHelper = require('./FormHelper'); this.step = 0; this.isInit = false; this.choiceType = null; @@ -101,6 +102,28 @@ class PollForm { this.setStep(this.step + 1); } + fromDom(form) { + let map = this.FormHelper.formToMap(form); + Object.assign(this.model, map); + console.info('dom to model'); + console.info(map); + console.info(this.model); + } + + toDom(form) { + let map = this.FormHelper.formToMap(form); + Object.assign(this.model, map); + console.info('model to dom'); + console.info(map); + console.info(this.model); + } + + fillForm(form) { + console.info('fill form from model'); + console.info(this.model); + this.FormHelper.fillForm(form, this.model); + } + fromTextChoices(form) { let choices = []; diff --git a/pollen-ui-riot-js/src/main/web/js/PollService.js b/pollen-ui-riot-js/src/main/web/js/PollService.js index 8434887..06bb3e1 100644 --- a/pollen-ui-riot-js/src/main/web/js/PollService.js +++ b/pollen-ui-riot-js/src/main/web/js/PollService.js @@ -27,8 +27,12 @@ class PollService extends FetchService { return this.getWithParams("/v1/polls/new", {choiceType: choiceType}); } - create(form, choices) { - return this.form("/v1/polls/create", {poll: form, choices: choices}); + create(poll, choices) { + return this.form("/v1/polls/create", {poll: poll, choices: choices}); + } + + save(poll) { + return this.form("/v1/polls/edit?permission="+poll.permission, {poll: poll}); } createdPolls(pagination) { diff --git a/pollen-ui-riot-js/src/main/web/tag/Pollen.tag b/pollen-ui-riot-js/src/main/web/tag/Pollen.tag index 4153e0d..32756e6 100644 --- a/pollen-ui-riot-js/src/main/web/tag/Pollen.tag +++ b/pollen-ui-riot-js/src/main/web/tag/Pollen.tag @@ -105,6 +105,12 @@ require("./poll/Polls.tag"); route("/poll/*/result/*", (pollId, permission) => { riot.mount(this.refs.content, "poll", {pollId: pollId, tabName: 'results', permission: permission}); }); + route("/poll/*/settings", (pollId) => { + riot.mount(this.refs.content, "poll", {pollId: pollId, tabName: 'settings'}); + }); + route("/poll/*/settings/*", (pollId, permission) => { + riot.mount(this.refs.content, "poll", {pollId: pollId, tabName: 'settings', permission: permission}); + }); route("/poll/*", (pollId) => { riot.mount(this.refs.content, "poll", {pollId: pollId}); diff --git a/pollen-ui-riot-js/src/main/web/tag/poll/Poll.tag b/pollen-ui-riot-js/src/main/web/tag/poll/Poll.tag index 736e76f..c8698ee 100644 --- a/pollen-ui-riot-js/src/main/web/tag/poll/Poll.tag +++ b/pollen-ui-riot-js/src/main/web/tag/poll/Poll.tag @@ -2,6 +2,7 @@ require('./PollVotes.tag'); require('./PollComments.tag'); require('./PollResults.tag'); require('./PollChoices.tag'); +require('./PollSettings.tag'); <Poll> @@ -33,6 +34,11 @@ require('./PollChoices.tag'); <i class="fa fa-bar-chart-o"></i><span>{__.results}</span> </a> </div> + <div if="{poll.permission}" class="{selectedTab=='settings'?'tab-selected':'tab-not-selected'}"> + <a href="#poll/{pollId}/settings{permission?'/' + permission : ''}"> + <i class="fa fa-gear"></i> {__.settings} + </a> + </div> <div if="{poll.permission}" class="actions tab-not-selected"> <div class="actions-dropdown"> <i class="fa fa-bars fa-2x mainColor"/> @@ -70,12 +76,15 @@ require('./PollChoices.tag'); this.poll.$canVote = this.poll.canVote; console.info("Poll::"); console.info(this.poll); + if (this.selectedTab === 'settings' && !this.poll.permission) { + this.selectedTab = false; + } if (!this.selectedTab) { - if (this.poll.status == 'VOTING' || this.poll.status == 'CREATED') { + if (this.poll.status === 'VOTING' || this.poll.status === 'CREATED') { this.selectedTab = 'votes'; - } else if (this.poll.status == 'ADDING_CHOICES') { + } else if (this.poll.status === 'ADDING_CHOICES') { this.selectedTab = 'choices'; - } else if (this.poll.status == 'CLOSED') { + } else if (this.poll.status === 'CLOSED') { this.selectedTab = 'results'; } } @@ -91,7 +100,7 @@ require('./PollChoices.tag'); session: session, permission: this.permission }); - let countName = this.selectedTab == 'results' ? '' : (this.selectedTab + 'Count'); + let countName = (this.selectedTab === 'results' || this.selectedTab === 'settings') ? '' : (this.selectedTab + 'Count'); if (countName) { tags[0].on('count', count => { this[countName] = count; diff --git a/pollen-ui-riot-js/src/main/web/tag/poll/PollChoices.tag b/pollen-ui-riot-js/src/main/web/tag/poll/PollChoices.tag index 51b0eae..51144a2 100644 --- a/pollen-ui-riot-js/src/main/web/tag/poll/PollChoices.tag +++ b/pollen-ui-riot-js/src/main/web/tag/poll/PollChoices.tag @@ -9,7 +9,7 @@ require("./PollChoicesDate.tag"); } else { this.form = require('../../js/PollForm'); this.form.model = opts.poll; - this.form.choiceType = opts.poll.choiceType || 'text'; + this.form.choiceType = (opts.poll.choiceType || 'text').toLowerCase(); this.form.mode = 'view'; if (opts.poll.choiceAddAllowed && opts.poll.status == 'ADDING_CHOICES') { diff --git a/pollen-ui-riot-js/src/main/web/tag/poll/PollSettings.tag b/pollen-ui-riot-js/src/main/web/tag/poll/PollSettings.tag index b8048a5..dc16600 100644 --- a/pollen-ui-riot-js/src/main/web/tag/poll/PollSettings.tag +++ b/pollen-ui-riot-js/src/main/web/tag/poll/PollSettings.tag @@ -20,38 +20,34 @@ */ <PollSettings> - <div class="legend"> + <div if="{mode==='create'}" class="legend"> {__.basic_usage} <a class="button skip mainColorBackground" onclick="{skip}">{__.skip}</a> </div> - <div class="actions"> - <a if="{!showOptions}" class="button" onclick="{previousStep}">{__.previous}</a> - <a if="{!showOptions}" class="button wide" onclick="{toggleShowOptions}">{__.showOptions}</a> + <div if="{!showOptions}" class="actions"> + <a class="button" onclick="{previousStep}">{__.previous}</a> + <a class="button wide" onclick="{toggleShowOptions}">{__.showOptions}</a> </div> <form show="{showOptions}" ref="form" onsubmit="{action}"> <div id="navigation"> <div class="config-navigation"> - <div class="actions"> + <div if="{mode==='create'}" class="actions"> <a if="{showOptions}" class="button wide" onclick="{toggleShowOptions}">{__.hideOptions}</a> </div> <nav class="bs-docs-sidebar"> <ul id="sidebar" class="nav nav-stacked"> - <li> + <li if="{mode==='edit'}"> <a onclick="{scrollTo}" href="#Poll">{__.nav_poll}</a> <ul class="nav nav-stacked"> <li> - <a onclick="{scrollTo}" href="#Poll_votePeriod">{__.nav_votePeriod}</a> - </li> - <li> - <a onclick="{scrollTo}" href="#Poll_resultVisibility">{__.nav_resultVisibility}</a> + <a onclick="{scrollTo}" href="#Poll_title">{__.nav_pollTitle}</a> </li> <li> - <a onclick="{scrollTo}" - href="#Poll_continuousVisibility">{__.nav_continuousResult}</a> + <a onclick="{scrollTo}" href="#Poll_description">{__.nav_pollDescription}</a> </li> </ul> </li> @@ -73,6 +69,9 @@ <a onclick="{scrollTo}" href="#Vote">{__.nav_votes}</a> <ul class="nav nav-stacked"> <li> + <a onclick="{scrollTo}" href="#Vote_period">{__.nav_votePeriod}</a> + </li> + <li> <a onclick="{scrollTo}" href="#Vote_visibility">{__.nav_voteVisibility}</a> </li> <li> @@ -81,6 +80,18 @@ </ul> </li> <li> + <a onclick="{scrollTo}" href="#Result">{__.nav_result}</a> + <ul class="nav nav-stacked"> + <li> + <a onclick="{scrollTo}" href="#Poll_resultVisibility">{__.nav_resultVisibility}</a> + </li> + <li> + <a onclick="{scrollTo}" + href="#Poll_continuousResult">{__.nav_continuousResult}</a> + </li> + </ul> + </li> + <li> <a onclick="{scrollTo}" href="#Comment">{__.nav_comments}</a> <ul class="nav nav-stacked"> <li> @@ -95,50 +106,20 @@ <div ref="content" class="content" onsubmit="{action}"> <div class="config-group" id="Poll"> <div class="config-header">{__.poll_configuration}</div> - <div id="Poll_votePeriod"> - <div class="config-subheader">{__.nav_votePeriod}<i class="fa fa-info-circle" - onclick="{help}"></i></div> - <div class="config-description">{__.votePeriod}</div> - <div class="config-form config-period"> - <div> - <label for="beginDate">{__.beginDate}</label> - <input ref="beginDate" name="beginDate" id="beginDate" type="datetime-local"> - </div> - <div> - <label for="endDate">{__.endDate}</label> - <input ref="endDate" name="endDate" id="endDate" type="datetime-local"> - </div> - </div> - </div> - <div id="Poll_resultVisibility"> - <div class="config-subheader">{__.nav_resultVisibility}<i class="fa fa-info-circle" - onclick="{help}"></i></div> - <div class="config-description"> - {__.resultVisibility} + <div id="Poll_title"> + <div class="config-subheader">{__.nav_pollTitle} + <i class="fa fa-info-circle" onclick="{help}"></i> </div> <div class="config-form"> - <ul> - <li> - <input type="radio" name="resultVisibility" value="CREATOR">{__.resultVisibility_creator} - </li> - <li> - <input type="radio" name="resultVisibility" value="VOTER">{__.resultVisibility_voter} - </li> - <li> - <input type="radio" name="resultVisibility" value="EVERYBODY">{__.resultVisibility_everybody} - </li> - </ul> + <input class="text" type="text" name="title" required> </div> </div> - <div id="Poll_continuousVisibility"> - <div class="config-subheader">{__.nav_continuousResult}<i class="fa fa-info-circle" - onclick="{help}"></i></div> - <div class="config-description"> {__.continuousResult}</div> - <div class="config-form checkbox"> - <input type="checkbox" name="continuousResult" id="continuousResult" - ref="continuousResult"> - <label for="continuousResult"></label> - + <div id="Poll_description"> + <div class="config-subheader">{__.nav_pollDescription} + <i class="fa fa-info-circle" onclick="{help}"></i> + </div> + <div class="config-form"> + <input class="text" type="text" name="description" ref="pollDescription"> </div> </div> </div> @@ -183,7 +164,7 @@ <div if="{form.model.limitChoices}"> <div> <label for="maxChoiceNumber">{__.maxChoiceNumber}</label> - <input ref="maxChoiceNumber" name="maxChoiceNumber" type="number"> + <input name="maxChoiceNumber" id="maxChoiceNumber" type="number"> </div> </div> </div> @@ -196,13 +177,32 @@ <div class="config-form"> <ul> <li each="{type in voteCountingTypes}"> - <input type="radio" name="voteCountingType" value="{type.id}">{type.name} + <input type="radio" name="voteCountingType" value="{type.id}" + ref="voteCountingType_{type.id}" + title="{type.shortHelper}" help="{type.helper}" + onclick="{showVoteCountingTypeHelp}">{type.name} </li> </ul> + <div class="voteCountingTypeHelp" ref="voteCountingTypeHelp"/> </div> </div> <div id="Vote" class="config-group"> <div class="config-header">{__.votesConfiguration}</div> + <div id="Vote_period"> + <div class="config-subheader">{__.nav_votePeriod}<i class="fa fa-info-circle" + onclick="{help}"></i></div> + <div class="config-description">{__.votePeriod}</div> + <div class="config-form config-period"> + <div> + <label for="beginDate">{__.beginDate}</label> + <input ref="beginDate" name="beginDate" id="beginDate" type="datetime-local"> + </div> + <div> + <label for="endDate">{__.endDate}</label> + <input ref="endDate" name="endDate" id="endDate" type="datetime-local"> + </div> + </div> + </div> <div id="Vote_visibility"> <div class="config-subheader">{__.nav_voteVisibility}<i class="fa fa-info-circle" onclick="{help}"></i></div> @@ -235,6 +235,39 @@ </div> </div> </div> + <div id="Result" class="config-group"> + <div class="config-header">{__.resultsConfiguration}</div> + <div id="Poll_resultVisibility"> + <div class="config-subheader">{__.nav_resultVisibility}<i class="fa fa-info-circle" + onclick="{help}"></i></div> + <div class="config-description"> + {__.resultVisibility} + </div> + <div class="config-form"> + <ul> + <li> + <input type="radio" name="resultVisibility" value="CREATOR">{__.resultVisibility_creator} + </li> + <li> + <input type="radio" name="resultVisibility" value="VOTER">{__.resultVisibility_voter} + </li> + <li> + <input type="radio" name="resultVisibility" value="EVERYBODY">{__.resultVisibility_everybody} + </li> + </ul> + </div> + </div> + <div id="Poll_continuousResult"> + <div class="config-subheader">{__.nav_continuousResult}<i class="fa fa-info-circle" + onclick="{help}"></i></div> + <div class="config-description"> {__.continuousResult}</div> + <div class="config-form checkbox"> + <input type="checkbox" name="continuousResults" id="continuousResults"> + <label for="continuousResults"></label> + + </div> + </div> + </div> <div id="Comment" class="config-group"> <div class="config-header">{__.commentsConfiguration}</div> <div id="Comment_visibility"> @@ -268,29 +301,53 @@ </div> </div> </div> - <div class="actions"> + <div if="{mode==='create'}" class="actions"> <a class="button" onclick="{previousStep}">{__.previous}</a> <input type="submit" class="button mainColorBackground" value="{__.next}"> </div> + <div if="{mode==='edit' && !form.model.isClosed}" class="actions"> + <input type="submit" class="button mainColorBackground" value="{__.save}"> + </div> + <div if="{mode==='edit' && form.model.isClosed}" class="actions"> + {__.pollIsClosed} + </div> </form> <script> - this.form = opts.form; - this.showOptions = this.form.showOptions; + if (opts.form) { + this.mode = 'create'; + this.form = opts.form; + this.showOptions = this.form.showOptions; + } else { + this.mode = 'edit'; + this.form = require('../../js/PollForm'); + this.form.model = opts.poll; + this.showOptions = true; + } + this.installBundle(opts.session, "poll_settings"); this.showHelp = false; - let FormHelper = require('../../js/FormHelper'); + let moment = require('moment'); + let pollService = require('../../js/PollService'); - this.toggleChoiceAddAllowed = (e) => { + this.toggleChoiceAddAllowed = e => { this.form.model.choiceAddAllowed = !this.form.model.choiceAddAllowed; }; - this.toggleLimitChoices = (e) => { + this.toggleLimitChoices = e => { this.form.model.limitChoices = !this.form.model.limitChoices; }; - this.help = (e) => { + this.toggleShowOptions = () => { + this.showOptions = this.form.showOptions = !this.showOptions; + }; + + this.showVoteCountingTypeHelp = e => { + this.refs.voteCountingTypeHelp.innerHTML = e.target.getAttribute('help'); + }; + + this.help = e => { let target = e.target; let helpId = 'Help_' + target.parentNode.parentNode.id; console.info(helpId); @@ -298,23 +355,26 @@ this.update({showHelp: this.showHelp}); }; - this.closeHelp = (e) => { + this.closeHelp = e => { this.showHelp = false; this.update({showHelp: this.showHelp}); }; - this.action = (e) => { + this.action = e => { e.preventDefault(); e.stopPropagation(); - let map = FormHelper.formToMap(this.refs.form); - Object.assign(this.form.model, map); - console.info('form to model'); - console.info(map); - console.info(this.form.model); - this.form.nextStep(); - }; + this.form.fromDom(this.refs.form); - let moment = require('moment'); + if (this.mode === 'create') { + this.form.nextStep(); + } + if (this.mode === 'edit') { + pollService.save(this.form.model).then(result => { + console.info("poll settings updated"); + console.info(result); + }); + } + }; this.formatDate = (date) => { if (date) { @@ -338,30 +398,22 @@ this.update({voteCountingTypes: this.voteCountingTypes}); console.info('voteCountingTypes'); console.info(this.voteCountingTypes); - let form = this.refs.form; - console.info('fill form with model'); - console.info(model); + this.form.fillForm(this.refs.form); - FormHelper.fillForm(form, model); + this.showVoteCountingTypeHelp({target: this.refs['voteCountingType_' + model.voteCountingType]}); }); - - }); - this.previousStep = (e) => { - let map = FormHelper.formToMap(this.refs.form); - Object.assign(this.form.model, map); - console.info('form to model'); - console.info(map); - console.info(this.form.model); + this.previousStep = e => { + this.form.toDom(this.refs.form); this.form.previousStep(); }; - this.skip = (e) => { + this.skip = e => { this.form.nextStep(); }; - this.scrollTo = (e) => { + this.scrollTo = e => { e.preventDefault(); e.stopPropagation(); this.closeHelp(e); @@ -372,13 +424,21 @@ document.getElementById(id).scrollIntoView(); }; - this.toggleShowOptions = () => { - this.showOptions = this.form.showOptions = !this.showOptions; - }; - </script> <style> + form { + width: 800px; + } + + .text { + width: 95%; + } + + .voteCountingTypeHelp { + margin-top: 20px; + } + [type="checkbox"]:not(:checked), [type="checkbox"]:checked { position: absolute; @@ -435,7 +495,7 @@ } input[type=datetime-local] { - width: 300px; + width: 250px; } .fa-info-circle { @@ -470,6 +530,7 @@ display: flex; flex-direction: column; align-content: flex-start; + width: 220px; } .config-header { -- To stop receiving notification emails like this one, please contact chorem.org SCM administrator <admin+scm@chorem.org>.