`, and ``.
@font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
@font-family-base: @font-family-sans-serif;
@@ -93,11 +88,11 @@
//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
//** Load fonts from this directory.
-@icon-font-path: "../fonts/";
+// '@icon-font-path' automatically set by Bootstrap package.
//** File name for all font files.
-@icon-font-name: "glyphicons-halflings-regular";
+// '@icon-font-name' automatically set by Bootstrap package.
//** Element ID within SVG icon file.
-@icon-font-svg-id: "glyphicons_halflingsregular";
+// '@icon-font-svg-id' automatically set by Bootstrap package.
//== Components
@@ -116,7 +111,7 @@
@padding-xs-vertical: 1px;
@padding-xs-horizontal: 5px;
-@line-height-large: 1.33;
+@line-height-large: 1.3333333; // extra decimals for Win 8.1 Chrome
@line-height-small: 1.5;
@border-radius-base: 4px;
@@ -187,6 +182,11 @@
@btn-link-disabled-color: @gray-light;
+// Allows for customizing button radius independently from global border radius
+@btn-border-radius-base: @border-radius-base;
+@btn-border-radius-large: @border-radius-large;
+@btn-border-radius-small: @border-radius-small;
+
//== Forms
//
@@ -204,6 +204,7 @@
// TODO: Rename `@input-border-radius` to `@input-border-radius-base` in v4
//** Default `.form-control` border radius
+// This has no effect on ``s in some browsers, due to the limited stylability of ``s in CSS.
@input-border-radius: @border-radius-base;
//** Large `.form-control` border radius
@input-border-radius-large: @border-radius-large;
@@ -211,7 +212,7 @@
@input-border-radius-small: @border-radius-small;
//** Border color for inputs on focus
-@input-border-focus: @brand-primary;
+@input-border-focus: #66afe9;
//** Placeholder text color
@input-color-placeholder: #999;
@@ -223,6 +224,9 @@
//** Small `.form-control` height
@input-height-small: (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
+//** `.form-group` margin
+@form-group-margin-bottom: 15px;
+
@legend-color: @gray-dark;
@legend-border-color: #e5e5e5;
@@ -282,7 +286,8 @@
@zindex-popover: 1060;
@zindex-tooltip: 1070;
@zindex-navbar-fixed: 1030;
-@zindex-modal: 1040;
+@zindex-modal-background: 1040;
+@zindex-modal: 1050;
//== Media queries breakpoints
@@ -334,7 +339,7 @@
@grid-gutter-width: 30px;
// Navbar collapse
//** Point at which the navbar becomes uncollapsed.
-@grid-float-breakpoint: @screen-desktop;
+@grid-float-breakpoint: @screen-sm-min;
//** Point at which the navbar begins collapsing.
@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
@@ -395,7 +400,7 @@
@navbar-default-toggle-border-color: #ddd;
-// Inverted navbar
+//=== Inverted navbar
// Reset inverted navbar basics
@navbar-inverse-color: lighten(@gray-light, 15%);
@navbar-inverse-bg: #222;
@@ -496,6 +501,7 @@
@jumbotron-bg: @gray-lighter;
@jumbotron-heading-color: inherit;
@jumbotron-font-size: ceil((@font-size-base * 1.5));
+@jumbotron-heading-font-size: ceil((@font-size-base * 4.5));
//== Form states and alerts
@@ -514,9 +520,9 @@
@state-warning-bg: #fcf8e3;
@state-warning-border: darken(spin(@state-warning-bg, -10), 5%);
-@state-danger-text: #444;
-@state-danger-bg: rgba(255,255,255,0.4);
-@state-danger-border: @brand-secondary;
+@state-danger-text: #a94442;
+@state-danger-bg: #f2dede;
+@state-danger-border: darken(spin(@state-danger-bg, -10), 5%);
//== Tooltips
@@ -869,5 +875,7 @@
@page-header-border-color: @gray-lighter;
//** Width of horizontal description list titles
@dl-horizontal-offset: @component-offset-horizontal;
+//** Point at which .dl-horizontal becomes horizontal
+@dl-horizontal-breakpoint: @grid-float-breakpoint;
//** Horizontal line color.
@hr-border: @gray-lighter;
diff --git a/client/stylesheets/sites/_style.less b/client/stylesheets/sites/_style.less
old mode 100644
new mode 100755
index c0764fb..fc76511
--- a/client/stylesheets/sites/_style.less
+++ b/client/stylesheets/sites/_style.less
@@ -1,5 +1,4 @@
-@import "../vendor/custom.bootstrap.import.less";
-@import "_variables.import.less";
+@import "../bootstrap-variables.less";
body {
background: @main-background;
@@ -36,17 +35,13 @@ input:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px white inset;
}
-.application-content {
- @media (max-width: @screen-sm-min) {
+.application-content {
+ @media (max-width: @screen-sm-min) {
margin: 15px inherit;
}
@media (min-width: @screen-sm-min) {
padding: 30px inherit;
- }
-}
-
-.btn-secondary {
- .button-variant(@btn-primary-color; @brand-secondary; @brand-secondary);
+ }
}
.container-fluid {
@@ -69,6 +64,16 @@ input:not([type="submit"]):focus {
box-shadow: 0 0 0px 1000px white inset;
}
+input.input-clear[type="checkbox"]:focus {
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+
+input[type="checkbox"]:focus {
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+
.full-width {
width: 100%;
}
@@ -151,3 +156,27 @@ input:not([type="submit"]):focus {
.search-icon {
padding-right: 10px;
}
+
+.tag-box{
+ padding-top: 15px;
+}
+
+.setting-padding{
+ padding:15px;
+}
+
+.btn-margin{
+ margin-bottom: 15px;
+}
+
+.long-string-fix{
+ word-wrap: break-word;
+}
+
+.card-tag{
+ display: inline-block;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ max-width: 100%;
+ white-space: nowrap;
+}
diff --git a/client/stylesheets/sites/_variables.import.less b/client/stylesheets/sites/_variables.import.less
deleted file mode 100644
index 8e874aa..0000000
--- a/client/stylesheets/sites/_variables.import.less
+++ /dev/null
@@ -1 +0,0 @@
-@import "../vendor/custom.bootstrap.import.less";
diff --git a/client/stylesheets/sites/about.less b/client/stylesheets/sites/about.less
index 1708b84..c683894 100644
--- a/client/stylesheets/sites/about.less
+++ b/client/stylesheets/sites/about.less
@@ -1,4 +1,4 @@
-@import "_variables.import.less";
+@import "../bootstrap-variables.less";
.row-same-height [class*="col-"] {
@media (min-width: @screen-sm-min) {
diff --git a/client/stylesheets/sites/carousel.less b/client/stylesheets/sites/carousel.less
index 29878af..d6792dd 100644
--- a/client/stylesheets/sites/carousel.less
+++ b/client/stylesheets/sites/carousel.less
@@ -1,25 +1,34 @@
-@import "_variables.import.less";
+@import "../bootstrap-variables.less";
.carousel {
+ @media (min-width: @screen-sm-min) { height: 500px; }
+ @media (min-width: @screen-md-min) { height: 550px; }
+ @media (min-width: @screen-lg-min) { height: 600px; }
background-position: center;
background-size: cover;
color: #fff;
- padding: 50px 0;
- @media (min-width: @screen-sm-min) {
- min-height: 500px;
- }
- @media (min-width: @screen-md-min) {
- min-height: 550px;
- }
- @media (min-width: @screen-lg-min) {
- min-height: 600px;
- }
+}
+
+.campus-carousel {
+ background-position: center;
+ background-size: cover;
+ color: #fff;
+ height: 200px;
+}
+
+.v-center {
+ position: relative;
+ top: 50%;
+ -webkit-transform: translateY(-50%);
+ -moz-transform: translateY(-50%);
+ -ms-transform: translateY(-50%);
+ -o-transform: translateY(-50%);
+ transform: translateY(-50%);
}
.carousel-content {
@media (min-width: @screen-sm-min) {
position: absolute;
- top: 25%;
}
}
@@ -31,6 +40,17 @@
overflow: hidden;
margin: 0;
}
+.carousel-title-rounded {
+ font-weight: 900;
+ color: #fff;
+ padding: 15px;
+ line-height: 38px;
+ overflow: hidden;
+ margin: 0;
+ border: 4px solid @brand-primary;
+ background-color: transparent;
+ border-radius: 10px!important;
+}
.carousel-description {
margin: 40px 0;
diff --git a/client/stylesheets/sites/footer.less b/client/stylesheets/sites/footer.less
index 12eb167..abcd484 100644
--- a/client/stylesheets/sites/footer.less
+++ b/client/stylesheets/sites/footer.less
@@ -1,4 +1,4 @@
-@import "_variables.import.less";
+@import "../bootstrap-variables.less";
.email-arrow {
background-color: #333333;
@@ -28,10 +28,10 @@
.footer-left, .footer-right {
padding: 30px 0;
- @media (min-width: @screen-sm-min) {
+ @media (min-width: @screen-sm-min) {
height: 320px;
}
- @media (min-width: @screen-md-min) {
+ @media (min-width: @screen-md-min) {
height: 340px;
}
}
diff --git a/client/stylesheets/sites/loading_icon.less b/client/stylesheets/sites/loading_icon.less
index e4ce2d9..62fc6ea 100644
--- a/client/stylesheets/sites/loading_icon.less
+++ b/client/stylesheets/sites/loading_icon.less
@@ -1,4 +1,4 @@
-@import "_variables.import.less";
+@import "../bootstrap-variables.less";
.input-symbol {
position: absolute;
diff --git a/client/stylesheets/sites/navbar.less b/client/stylesheets/sites/navbar.less
index 67bf764..9a9a685 100644
--- a/client/stylesheets/sites/navbar.less
+++ b/client/stylesheets/sites/navbar.less
@@ -1,4 +1,4 @@
-@import "_variables.import.less";
+@import "../bootstrap-variables.less";
.navbar .navbar-default {
margin-bottom: 0;
@@ -13,7 +13,7 @@
background: white;
border: none;
margin-bottom: 0;
- @media (min-width: @screen-sm-min) {
+ @media (min-width: @screen-sm-min) {
margin: 0;
}
}
diff --git a/client/stylesheets/sites/petition.less b/client/stylesheets/sites/petition.less
old mode 100644
new mode 100755
index d8ee7f7..8184a6d
--- a/client/stylesheets/sites/petition.less
+++ b/client/stylesheets/sites/petition.less
@@ -1,4 +1,19 @@
-@import "_variables.import.less";
+@import "../bootstrap-variables.less";
+
+.container h1 {
+ color: @brand-primary;
+}
+
+.filter-checkboxes {
+ text-overflow: ellipsis;
+ overflow:hidden;
+ white-space:nowrap;
+ max-width: 300px;
+ .filter-checkbox {
+ padding:5px 10px;
+
+ }
+}
.author {
font-weight: 300;
@@ -30,7 +45,7 @@
.input-search {
margin: 0 auto;
@media (min-width: @screen-sm-min) {
- max-width: 400px;
+ max-width: 400px;
}
}
@@ -39,9 +54,7 @@
text-align: left;
vertical-align: top;
width: 100%;
- @media (min-width: @screen-sm-min) {
- margin-top: 25px;
- }
+ margin-top: 25px;
}
.module-row {
@@ -59,7 +72,7 @@
.module-white {
background: #ffffff;
- border: 0.25em solid transparent;
+ border: 0.25em solid transparent;
}
.petition-graph {
@@ -72,10 +85,13 @@
.petition-signature-module {
@media (max-width: @screen-xs-max) {
- display: none;
}
}
+.petition-signature-module {
+ margin-bottom: 30px;
+}
+
.petition-initials {
-webkit-column-count: 5;
-moz-column-count: 5;
@@ -101,10 +117,17 @@
.petition-sign-count {
font-size: 18px;
+ display:inline-block;
+}
+
+.signed-badge {
+ padding: 7px 7px 9px 7px;
+ margin-left: 10px;
+ position: relative;
}
.petition-social-wrapper {
- margin-bottom: 50px;
+ margin-bottom: 0px;
}
.petition-subtitle {
@@ -132,7 +155,7 @@
}
}
-.post-title-graph {
+.petition-title-graph {
svg {
@media (max-width: @screen-sm-min) {
left: 90%;
@@ -162,25 +185,9 @@
fill: #fff;
}
-@keyframes popin {
- from { opacity: 0; transform: scale(0.5);}
- to { opacity: 1; transform: scale(1);}
-}
-@-webkit-keyframes popin {
- from { opacity: 0; -webkit-transform: scale(0.5);}
- to { opacity: 1; -webkit-transform: scale(1);}
-}
-@-moz-keyframes popin {
- from { opacity: 0; -moz-transform: scale(0.5);}
- to { opacity: 1; -moz-transform: scale(1);}
-}
-
.square {
padding-bottom: 15px;
padding-top: 15px;
- animation: popin 0.5s;
- -webkit-animation: popin 0.5s;
- -moz-animation: popin 0.5s;
}
.square-content {
@@ -189,13 +196,13 @@
@media (max-width: @screen-sm-min) {
min-height: 100px;
}
- @media (min-width: @screen-sm-min) {
+ @media (min-width: @screen-sm-min) {
height: 275px;
}
- @media (min-width: @screen-md-min) {
+ @media (min-width: @screen-md-min) {
height: 275px;
}
- @media (min-width: @screen-lg-min) {
+ @media (min-width: @screen-lg-min) {
height: 250px;
}
}
@@ -268,5 +275,10 @@
position: absolute;
font-style: italic;
margin-top: 10px;
- bottom: 25px;
+ bottom: 25px;
+}
+
+.petition-align-fix {
+ margin-right: 15px;
+ margin-left: 15px;
}
diff --git a/client/stylesheets/sites/select2.less b/client/stylesheets/sites/select2.less
index ea408e3..4911168 100644
--- a/client/stylesheets/sites/select2.less
+++ b/client/stylesheets/sites/select2.less
@@ -1,4 +1,4 @@
-@import "_variables.import.less";
+@import "../bootstrap-variables.less";
.select2-results .select2-no-results, .select2-results .select2-searching, .select2-results .select2-selection-limit {
background: none;
diff --git a/client/stylesheets/sites/updates.less b/client/stylesheets/sites/updates.less
index a73a931..3725b12 100644
--- a/client/stylesheets/sites/updates.less
+++ b/client/stylesheets/sites/updates.less
@@ -1,4 +1,4 @@
-@import "_variables.import.less";
+@import "../bootstrap-variables.less";
.update-title {
color: @brand-primary;
diff --git a/client/stylesheets/style.less b/client/stylesheets/style.less
index 77da354..ca180be 100644
--- a/client/stylesheets/style.less
+++ b/client/stylesheets/style.less
@@ -1,7 +1,3 @@
-// Import Vendors
-@import "vendor/custom.bootstrap.mixins.import.less";
-@import "vendor/custom.bootstrap.import.less";
-
// Sites
@import "sites/_style.less";
@import "sites/about.less";
diff --git a/client/stylesheets/vendor/.gitignore b/client/stylesheets/vendor/.gitignore
deleted file mode 100644
index 7650e7a..0000000
--- a/client/stylesheets/vendor/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-custom.bootstrap.less
-custom.bootstrap.mixins.import.less
diff --git a/client/stylesheets/vendor/custom.bootstrap.json b/client/stylesheets/vendor/custom.bootstrap.json
deleted file mode 100644
index b497657..0000000
--- a/client/stylesheets/vendor/custom.bootstrap.json
+++ /dev/null
@@ -1,51 +0,0 @@
-{"modules": {
- "normalize": true,
- "print": true,
-
- "scaffolding": true,
- "type": true,
- "code": true,
- "grid": true,
- "tables": true,
- "forms": true,
- "buttons": true,
-
- "glyphicons": true,
- "button-groups": true,
- "input-groups": true,
- "navs": true,
- "navbar": true,
- "breadcrumbs": true,
- "pagination": true,
- "pager": true,
- "labels": true,
- "badges": true,
- "jumbotron": true,
- "thumbnails": true,
- "alerts": true,
- "progress-bars": true,
- "media": true,
- "list-group": true,
- "panels": true,
- "responsive-embed": true,
- "wells": true,
- "close": true,
-
- "component-animations": true,
- "dropdowns": true,
- "modals": true,
- "tooltip": true,
- "popovers": true,
- "carousel": true,
-
- "affix": true,
- "alert": true,
- "button": true,
- "collapse": true,
- "scrollspy": true,
- "tab": true,
- "transition": true,
-
- "utilities": true,
- "responsive-utilities": true
-}}
diff --git a/client/views/admin/admin.html b/client/views/admin/admin.html
old mode 100644
new mode 100755
index aaef51d..740423b
--- a/client/views/admin/admin.html
+++ b/client/views/admin/admin.html
@@ -9,6 +9,7 @@
Users
Settings
Petition tags
+ Interface
@@ -16,6 +17,7 @@
{{> users}}
{{> settings}}
{{> tags}}
+ {{> interface}}
@@ -24,6 +26,11 @@
+ {{#if publicSettings.ui.roles_locked}}
+
+ Heads up! Users are automatically added and removed from groups when they log into the site. Change someone's LDAP groups if you want them to be an admin or a moderator.
+
+ {{/if}}
Admins
Administrators can manage user roles and petitions.
@@ -43,9 +50,9 @@
Notifiers
-
+
Settings
-
+
Minimum Signature Threshold: Changing the threshold only affects future petitions.
+ {{> moderationButton}}
@@ -78,7 +86,7 @@
Petition Tags
value="{{this.name}}"
disabled>
- X
+ ×
@@ -100,47 +108,108 @@
Petition Tags
+
+
+
User Interface
+
Manage UI settings.
+
+
+
+
+
+
{{#each users}}
+ {{#if publicSettings.ui.roles_locked}}
+ {{profile.name}} - {{username}}
+ {{else}}
+
+ {{/if}}
+ {{/each}}
+ {{#unless publicSettings.ui.roles_locked}}
- {{/each}}
-
-
\ No newline at end of file
+ {{/unless}}
+
diff --git a/client/views/admin/admin.js b/client/views/admin/admin.js
old mode 100644
new mode 100755
index eeea2e8..e2cbeda
--- a/client/views/admin/admin.js
+++ b/client/views/admin/admin.js
@@ -1,9 +1,7 @@
Template.admin.events({
'submit #thresholdForm': function (e) {
e.preventDefault();
-
var threshold = $(e.target).find('[name=minimumThreshold]').val();
-
Meteor.call('changeMinimumThreshold', threshold, function (error) {
if (error) {
throwError(error.reason);
@@ -32,7 +30,7 @@ Template.admin.events({
}
})
},
- 'submit form': function(e) {
+ 'submit #users form': function(e) {
e.preventDefault();
var username,
@@ -58,4 +56,43 @@ Template.admin.events({
}
});
}
-});
\ No newline at end of file
+});
+
+Template.interface.events({
+ 'submit #changeParallax': function(e){
+ e.preventDefault();
+ Meteor.call('toggleParallax');
+ },
+ 'submit #changePetitionHistoryDisplay': function(e){
+ e.preventDefault();
+ Meteor.call('togglePetitionHistoryDisplay');
+ },
+ 'submit #changeUpdateAuthorDisplay': function(e){
+ e.preventDefault();
+ Meteor.call('toggleUpdateAuthorDisplay');
+ }
+});
+
+Template.interface.helpers({
+ 'parallaxStyle' : function(){
+ if(Singleton.findOne().parallax){
+ return 'Enabled'
+ }else{
+ return 'Disabled'
+ }
+ },
+ 'petitionHistoryDisplayStyle' : function(){
+ if(Singleton.findOne().petitionHistoryDisplay){
+ return 'Enabled'
+ }else{
+ return 'Disabled'
+ }
+ },
+ 'updateAuthorDisplayStyle' : function(){
+ if(Singleton.findOne().updateAuthorDisplay){
+ return 'Enabled'
+ }else{
+ return 'Disabled'
+ }
+ }
+});
diff --git a/client/views/admin/moderatePage.html b/client/views/admin/moderatePage.html
new file mode 100644
index 0000000..cf1220b
--- /dev/null
+++ b/client/views/admin/moderatePage.html
@@ -0,0 +1,15 @@
+
+
+ {{> moderationButton}}
+
+ {{> petitionGroup petitions=petitions
+ petitionOrder=petitionOrder
+ title="Pending Approval"
+ description="These petitions are currently being evaluated for submission"
+ nopetitions="No petitions are currently pending."
+ pending=true
+ }}
+
+
+
+
diff --git a/client/views/admin/moderationButton.html b/client/views/admin/moderationButton.html
new file mode 100644
index 0000000..ecc9919
--- /dev/null
+++ b/client/views/admin/moderationButton.html
@@ -0,0 +1,25 @@
+
+
+ {{#if isInRole 'admin'}}
+
+
Moderation Enabled/Disabled: Changing the moderation only affects future petitions.
+
+
+ {{/if}}
+
diff --git a/client/views/admin/moderationButton.js b/client/views/admin/moderationButton.js
new file mode 100644
index 0000000..69be7d5
--- /dev/null
+++ b/client/views/admin/moderationButton.js
@@ -0,0 +1,16 @@
+Template.moderationButton.events({
+ 'submit #changeModeration': function(e){
+ e.preventDefault();
+ Meteor.call('toggleModeration');
+ }
+});
+
+Template.moderationButton.helpers({
+ 'moderationStyle' : function(){
+ if(Singleton.findOne().moderation){
+ return 'Enabled'
+ }else{
+ return 'Disabled'
+ }
+ }
+});
diff --git a/client/views/application/index.html b/client/views/application/index.html
index 49ed595..68e6699 100644
--- a/client/views/application/index.html
+++ b/client/views/application/index.html
@@ -1,4 +1,4 @@
{{> carousel}}
- {{> postsList}}
-
\ No newline at end of file
+ {{> petitionsList}}
+
diff --git a/client/views/application/layout.html b/client/views/application/layout.html
old mode 100644
new mode 100755
index 4818402..add668f
--- a/client/views/application/layout.html
+++ b/client/views/application/layout.html
@@ -1,8 +1,9 @@
{{> header}}
- {{> yield}}
- {{> footer}}
- {{> login}}
- {{> postSearch}}
- {{> errors}}
-
\ No newline at end of file
+
+ {{> yield}}
+ {{> footer}}
+ {{> login}}
+ {{> errors}}
+
+
diff --git a/client/views/includes/carousel.html b/client/views/includes/carousel.html
index e5bcc8e..113fe77 100644
--- a/client/views/includes/carousel.html
+++ b/client/views/includes/carousel.html
@@ -1,23 +1,23 @@
-
+
- MAKE YOUR MARK.
+ PawPrints
Create and vote on petitions.
-
Receive official responses from Student Government.
+
Track their progress and share with friends.
+
Receive responses and updates.
+
+ Search Petitions
+
+
-
-
-
- Search Petitions
-
-
-
+
+
-
\ No newline at end of file
+
diff --git a/client/views/includes/carousel.js b/client/views/includes/carousel.js
old mode 100644
new mode 100755
index 57aa25c..bb2d824
--- a/client/views/includes/carousel.js
+++ b/client/views/includes/carousel.js
@@ -8,8 +8,11 @@ Template.carousel.events = {
}
Template.carousel.rendered = function onCarouselRendered() {
- var stock_images = ['carousel_1.png', 'carousel_2.png', 'carousel_3.png'];
+ var stock_images = _.get(Meteor, 'settings.public.ui.carousel_images', ['/carousel_1.png', '/carousel_2.png', '/carousel_3.png']);
var random = stock_images[Math.floor(Math.random() * stock_images.length)];
$('.carousel').css('background-image', 'url(' + random + ')');
+ if(Singleton.findOne().parallax){
+ $('.carousel').css('background-attachment', 'fixed');
+ }
return;
-}
\ No newline at end of file
+}
diff --git a/client/views/includes/footer.html b/client/views/includes/footer.html
index 863f8e8..29c58af 100644
--- a/client/views/includes/footer.html
+++ b/client/views/includes/footer.html
@@ -7,11 +7,11 @@
About
- PawPrints is a place for sparking change at RIT. Share ideas with the RIT community and influence decision making.
+ PawPrints is the place to spark positive change in your community. Developed by students at the Rochester Institute of Technology .
- © Student Government 2014.
+ © RIT Student Government 2016.
@@ -19,6 +19,11 @@
Available on GitHub.
+
+
+ Get it for your community.
+
+
@@ -44,10 +49,10 @@
Petitions
- {{ singleton.postsCount }}
+ {{ singleton.petitionsCount }}
-
\ No newline at end of file
+
diff --git a/client/views/includes/header.html b/client/views/includes/header.html
index b0750b7..4895d39 100644
--- a/client/views/includes/header.html
+++ b/client/views/includes/header.html
@@ -1,5 +1,5 @@
-
\ No newline at end of file
+
diff --git a/client/views/includes/header.js b/client/views/includes/header.js
old mode 100644
new mode 100755
index 43377bc..1ab924f
--- a/client/views/includes/header.js
+++ b/client/views/includes/header.js
@@ -1,8 +1,25 @@
-Template.header.events({
- 'click .navbar-search': function () {
- $('#modal-search').modal();
- setTimeout( function() {
- $('#search').focus();
- }, 500);
+Template.header.helpers({
+
+ 'moderationEnabled' : function(){
+ var enabled = false;
+ if(Singleton.findOne()){
+ enabled = Singleton.findOne().moderation;
+ }
+ var admin = Roles.userIsInRole(Meteor.user(), ['admin']);
+ var moderator = Roles.userIsInRole(Meteor.user(), ['moderator']);
+ if(enabled &&(admin || moderator)){
+ return true;
+ }else{
+ return admin;
+ }
}
-});
\ No newline at end of file
+})
+
+//Collapses the navbar when navigating.
+//In a traditional application, the state would be reset on navigation.
+//Becuase the page is not reloaded, navbar needs to be manually collapsed.
+$(document).on('click.nav','.navbar-collapse.in',function(e){
+ if( $(e.target).is('a') ) {
+ $(this).collapse('hide');
+ }
+});
diff --git a/client/views/petitions/action_bar.html b/client/views/petitions/action_bar.html
new file mode 100644
index 0000000..5186322
--- /dev/null
+++ b/client/views/petitions/action_bar.html
@@ -0,0 +1,105 @@
+
+
+ {{#if petition.pending}}
+
Approve Petition
+
+ Reject petition
+ {{/if}}
+ {{#if petition.response}}
+
Great Work!
+
This petition has received an official response.
+ {{else}}
+
Support
+
+ {{/if}}
+
Share
+
+
+
+
+
+
+
+ {{#if petition.response}}
+
Signatures
+
+ {{petition.votes}} of
+
+
+
+ {{petition.minimumVotes}} signatures needed
+
+ {{else}}
+
Track
+
+ {{#if updates}}
+
Student Government is aware of and actively working on responding to this petition.
+ {{/if}}
+
+ {{petition.votes}} of
+
+
+
+ {{petition.minimumVotes}} signatures needed
+
+ {{/if}}
+
+
Details
+
+
+
+ Status
+
+
+
+ {{ petitionStatus.title }}
+
+
+
+
+ {{#if equal this.petition.status undefined}}
+
+
{{mustReachDate}}
+ {{/if}}
+
+
+
SUBMITTED
+
{{submittedDate}}
+ {{#if equal this.petition.status undefined}}
+
+
{{mustReachDate}}
+ {{/if}}
+
+
+
+
+
diff --git a/client/views/petitions/action_bar.js b/client/views/petitions/action_bar.js
new file mode 100644
index 0000000..fd4b0e2
--- /dev/null
+++ b/client/views/petitions/action_bar.js
@@ -0,0 +1,65 @@
+var social_links = {
+ 'facebook': 'https://www.facebook.com/sharer/sharer.php?u=',
+ 'twitter': 'https://twitter.com/intent/tweet?url=',
+ 'reddit': 'http://www.reddit.com/submit?url=',
+ 'plus': 'https://plus.google.com/share?url=',
+ 'linkedin': 'https://www.linkedin.com/cws/share?url='
+};
+
+Template.actionBar.helpers({
+ 'progress': function () {
+ if (this.petition.votes > this.petition.minimumVotes) {
+ return 100;
+ } else {
+ return (this.petition.votes / this.petition.minimumVotes) * 100;
+ }
+ },
+ 'goalReachedClass': function () {
+ return this.petition.votes >= this.petition.minimumVotes ? 'goal-reached' : '';
+ },
+ 'mustReachDate': function() {
+ return new moment(this.petition.submitted).add(1, 'month').format('ll');
+ },
+ 'submittedDate': function(){
+ return moment(this.petition.submitted).format('ll');
+ },
+ 'petitionStatus': function () {
+ var petition = Petitions.findOne();
+ if (petition.status == "waiting-for-reply") {
+ return {
+ title: "In Progress",
+ description: "This petition is being reviewed by Student Government."
+ };
+ } else if (petition.status == "responded") {
+ return {
+ title: "Responded",
+ description: "This petition has recieved an official response."
+ };
+ } else {
+ if (petition.votes >= petition.minimumVotes) {
+ return {
+ title: "Goal Met",
+ description: "This petition has met its signature goal, but has not yet been reviewed by Student Government."
+ };
+ } else if (moment(petition.submitted).isBefore(moment().subtract(1, 'month'))) {
+ return {
+ title: "Expired",
+ description: "This petition didn't meet it's minimum signature goal within one month."
+ };
+ } else {
+ return {
+ title: "Goal Not Met",
+ description: "This petition is below its signature threshold of " + petition.minimumVotes + "."
+ };
+ }
+ }
+ }
+});
+Template.actionBar.events({
+ 'click *[social]': function (e) {
+ var network = $(e.currentTarget).attr("social");
+ var url = social_links[network] + this.url;
+ GAnalytics.event("petition", "share", network);
+ window.open(url);
+ }
+});
diff --git a/client/views/petitions/header_carousel.html b/client/views/petitions/header_carousel.html
new file mode 100644
index 0000000..dd12349
--- /dev/null
+++ b/client/views/petitions/header_carousel.html
@@ -0,0 +1,11 @@
+
+
+
+
+
{{title}}
+ {{description}}
+
+
+
+
+
diff --git a/client/views/petitions/header_carousel.js b/client/views/petitions/header_carousel.js
new file mode 100755
index 0000000..9a3888f
--- /dev/null
+++ b/client/views/petitions/header_carousel.js
@@ -0,0 +1,10 @@
+Template.headerCarousel.rendered = function onHeaderCarouselRendered() {
+ var stock_images = _.get(Meteor, 'settings.public.ui.carousel_images', ['/carousel_1.png', '/carousel_2.png', '/carousel_3.png']);
+ var random = stock_images[Math.floor(Math.random() * stock_images.length)];
+ $('.campus-carousel').css('background-image', 'url(' + random + ')');
+ if(Singleton.findOne().parallax){
+ $('.campus-carousel').css('background-attachment', 'fixed');
+ }
+ console.log("background selected");
+ return;
+}
diff --git a/client/views/petitions/petition_card.html b/client/views/petitions/petition_card.html
new file mode 100755
index 0000000..6cffd26
--- /dev/null
+++ b/client/views/petitions/petition_card.html
@@ -0,0 +1,48 @@
+
+
+
+
+ {{pluralize petition.votes "Signature"}}
+
+ {{#if signedByUser petition}}
+
signed
+ {{/if}}
+
+
+ {{#each petition.tag_ids}}
+ {{#with tag this}}
+ {{#if isExpired ../..}}
+ {{#if isResponded ../../..}}
+ {{upcase this.name}}
+ {{else}}
+ {{upcase this.name}}
+ {{/if}}
+ {{else}}
+ {{upcase this.name}}
+ {{/if}}
+ {{/with}}
+ {{/each}}
+
+ {{#if petition.pending}}
+
Pending Moderation
+ {{/if}}
+
+
+
diff --git a/client/views/petitions/petition_card.js b/client/views/petitions/petition_card.js
new file mode 100755
index 0000000..a25fac3
--- /dev/null
+++ b/client/views/petitions/petition_card.js
@@ -0,0 +1,26 @@
+Template.petitionCard.events({
+ 'click .card-tag': function(e){
+ Session.set('activeTag', e.target.name);
+ },
+});
+
+Template.petitionCard.helpers({
+ isExpired: function(e) {
+ if(moment(e.petition.submitted).isBefore(moment().subtract(1, 'month'))) {
+ return true;
+ }
+ else {
+ return false;
+ }
+ },
+ signedByUser: function(petition) {
+ return Meteor.user() && _.contains(petition.upvoters, Meteor.user()._id);
+ },
+ isResponded: function(e) {
+ if (e.status == "responded"){
+ return true;
+ }else{
+ return false;
+ }
+ }
+});
diff --git a/client/views/posts/post_edit.html b/client/views/petitions/petition_edit.html
similarity index 76%
rename from client/views/posts/post_edit.html
rename to client/views/petitions/petition_edit.html
index 5c4f844..51e57c1 100644
--- a/client/views/posts/post_edit.html
+++ b/client/views/petitions/petition_edit.html
@@ -1,4 +1,4 @@
-
+
@@ -7,9 +7,9 @@
Edit Petition
Back to Petition
+ href="{{pathFor 'petitionPage' _id=petition._id}}">Back to Petition
-
Petition "{{post.title}}" currently has {{pluralize post.votes "signature"}}.
+
Petition "{{petition.title}}" currently has {{pluralize petition.votes "signature"}}.
@@ -52,53 +52,41 @@
maxlength="70"
placeholder="My Petition's Name"
type="text"
- value="{{ post.title }}">
+ value="{{ petition.title }}">
Tags
Description
-
+
Status Updates
Use status updates for information regarding in-progress activities, e.g., committees formed as a result of the petition, or reachout efforts to Administrators.
-
If a new status update is created, an e-mail will be dispatched to petition signers who subscribe to status updates.
+
If a new status update is created, an e-mail will be dispatched to petition signatories who subscribe to status updates.
{{#each updates}}
- {{> createUpdate update=this post=../post }}
+ {{> createUpdate update=this petition=../petition }}
{{/each}}
- {{> createUpdate update=newUpdate post=post user=user }}
+ {{> createUpdate update=newUpdate petition=petition user=user }}
Response
Use response for the final petition update, e.g., the final decision reached by Administration, the consensus reached by the committee formed for the petition, etc.
-
When a response is initially posted, an e-mail is dispatched to petition signers who subscribe to updates.
+
When a response is initially petitioned, an e-mail is dispatched to petition signatories who subscribe to updates.
-
-
+
+
- {{#with post}}
+ {{#with petition}}
Publish Status
This petition is currently {{publishStatus}} . Published petitions are publicly viewable and accessible via the API; unpublished petitions are only visible to administrators and not accessible via the API.
@@ -114,7 +102,7 @@
-
+
Save Petition
diff --git a/client/views/posts/post_edit.js b/client/views/petitions/petition_edit.js
similarity index 58%
rename from client/views/posts/post_edit.js
rename to client/views/petitions/petition_edit.js
index ef4ff29..d7ff62b 100644
--- a/client/views/posts/post_edit.js
+++ b/client/views/petitions/petition_edit.js
@@ -1,40 +1,50 @@
-Template.postEdit.events({
+Template.petitionEdit.events({
'submit #petitionForm': function(e) {
e.preventDefault();
- var postProperties = {
+ var styled_description = $('#summernote1').summernote('code');
+ styled_response = null;
+ if ($('#summernote3').summernote('isEmpty') == false){
+ styled_response = $('#summernote3').summernote('code');
+ }
+ var petitionProperties = {
title: $(e.target).find('[name=title]').val(),
- description: $(e.target).find('[name=description]').val(),
- response: $(e.target).find('[name=response]').val(),
+ description: styled_description,
+ response: styled_response,
tag_ids: _.pluck($('#s2id_tags').select2('data'), '_id')
};
- Meteor.call('edit', this.post._id, postProperties, function (err) {
+ Meteor.call('edit', this.petition._id, petitionProperties, function (err) {
if (err) {
- GAnalytics.event("post", "edit", err.reason);
+ GAnalytics.event("petition", "edit", err.reason);
throwError(err.reason);
} else {
throwError("Petition saved.");
- GAnalytics.event("post", "edit");
+ GAnalytics.event("petition", "edit");
}
});
},
'click .delete-petition': function(e) {
e.preventDefault();
if (confirm("Delete this petition? Consider unpublishing it instead.")) {
- Meteor.call('delete', this.post._id, function (err) {
+ Meteor.call('delete', this.petition._id, function (err) {
if (err) {
- GAnalytics.event("post", "delete", err.reason);
+ GAnalytics.event("petition", "delete", err.reason);
throwError(err.reason);
} else {
- GAnalytics.event("post", "delete");
- Router.go('postsList');
+ GAnalytics.event("petition", "delete");
+ Router.go('petitionsList');
}
});
}
}
});
-Template.postEdit.rendered = function () {
+Template.petitionEdit.rendered = function () {
var _this = this;
+ $('#summernote1').summernote();
+ $('#summernote2').summernote();
+ $('#summernote3').summernote();
+ $('#summernote1').summernote('code', this.data.petition.description);
+ $('#summernote3').summernote('code', this.data.petition.response);
Deps.autorun(function () {
$('#tags').select2({
placeholder: "Petition Tags",
@@ -47,7 +57,7 @@ Template.postEdit.rendered = function () {
formatResult: function (object, container, query) { return object.name; },
formatNoMatches: function (object) { return "No tags found." },
formatSelectionTooBig: function (object) { return "You can only select up to 3 tags." }
- }).select2('val', _.pluck(Tags.find({_id: {$in: this.data.post.tag_ids}}).fetch(), '_id'));
+ }).select2('val', _.pluck(Tags.find({_id: {$in: this.data.petition.tag_ids}}).fetch(), '_id'));
$('.select2-search-field>input').addClass("input");
}.bind(this));
};
@@ -57,10 +67,10 @@ Template.petitionPublish.events({
e.preventDefault();
Meteor.call('changePublishStatus', this._id, function (err) {
if (err) {
- GAnalytics.event("post", "changePublishStatus", err.reason);
+ GAnalytics.event("petition", "changePublishStatus", err.reason);
throwError(err.reason);
} else {
- GAnalytics.event("post", "changePublishStatus");
+ GAnalytics.event("petition", "changePublishStatus");
}
});
}
diff --git a/client/views/petitions/petition_page.html b/client/views/petitions/petition_page.html
new file mode 100755
index 0000000..bf3ef52
--- /dev/null
+++ b/client/views/petitions/petition_page.html
@@ -0,0 +1,92 @@
+
+
+
+
{{petition.title}}
+
+ {{#each petition.tag_ids}}
+ {{#with tag this}}
+ {{upcase this.name}}
+ {{/with}}
+ {{/each}}
+ {{#if isInRole 'admin' 'moderator'}}
+ EDIT
+ {{/if}}
+ {{#if petition.pending}}
+ Pending Moderation
+ {{/if}}
+
+
+
Petition by {{petition.author}}
+ {{#if show_history}}
+ {{#if updates}}
+ {{#each updates}}
+
Update by {{this.author}}
+ {{/each}}
+ {{/if}}
+ {{/if}}
+
+
+
+
+
+
+ {{#if petition.response}}
+
+
+
+
Official Response
+
{{ responded_at }}
+
+
{{{breaklines petition.response }}}
+
+
+
+ {{/if}}
+
+
+
+ {{#if updates}}
+ {{#each updates}}
+
+ {{> updateBlurb}}
+
+ {{/each}}
+ {{/if}}
+
+
+
+ Description
+ {{ submitted_at }}
+
+
+
{{{breaklines petition.description}}}
+
+
+
+ {{> actionBar}}
+
+
+
+
Signed by
+
+
+
+
+ {{#each initials}}
+ {{ this }}
+ {{/each}}
+
+
+
+
+
+
+
+ {{> actionBar}}
+
+
+
+ {{> petitionReport}}
+ {{> petitionReject}}
+ {{> petitionShareModal}}
+
diff --git a/client/views/petitions/petition_page.js b/client/views/petitions/petition_page.js
new file mode 100644
index 0000000..e003534
--- /dev/null
+++ b/client/views/petitions/petition_page.js
@@ -0,0 +1,52 @@
+var timeTick = new Deps.Dependency();
+
+Meteor.setInterval(function () {
+ timeTick.changed();
+}, 1000);
+
+Template.petitionPage.helpers({
+ 'responded_at': function () {
+ timeTick.depend();
+ return new moment(this.petition.responded_at).fromNow();
+ },
+ 'responded_date':function(){
+ return new moment(this.petition.responded_at).format('MMMM Do YYYY, h:mm a');
+ },
+ 'submitted_at': function () {
+ timeTick.depend();
+ return new moment(this.petition.submitted).fromNow();
+ },
+ 'submitted_date': function () {
+ return new moment(this.petition.submitted).format('MMMM Do YYYY, h:mm a');
+ },
+ 'initials': function () {
+ return Meteor.users.find({
+ '_id': {
+ $in: this.petition.upvoters
+ }
+ }).fetch().map(function (user) {
+ if (!user.profile.initials) {
+ return "Unknown";
+ } else {
+ return user.profile.initials;
+ }
+ });
+ },
+ 'show_history': function () {
+ return Singleton.findOne().petitionHistoryDisplay;
+ }
+});
+
+Template.petitionPage.events({
+ 'click #approve' : function(e){
+ e.preventDefault();
+ var _id = this.petition._id
+ Meteor.call('changePendingPetition', _id, true, function(error) {
+ if (error) {
+ throwError(error.reason);
+ } else {
+ throwError(result);
+ }
+ });
+ }
+});
diff --git a/client/views/petitions/petition_reject.html b/client/views/petitions/petition_reject.html
new file mode 100644
index 0000000..b98b6e6
--- /dev/null
+++ b/client/views/petitions/petition_reject.html
@@ -0,0 +1,39 @@
+
+
+
diff --git a/client/views/petitions/petition_reject.js b/client/views/petitions/petition_reject.js
new file mode 100644
index 0000000..4a15982
--- /dev/null
+++ b/client/views/petitions/petition_reject.js
@@ -0,0 +1,20 @@
+Template.petitionReject.events({
+ 'submit form': function(e){
+ e.preventDefault();
+ var reason = $('textarea[name=rejectReason]').val();
+ if(reason.length == 0){
+ throwError("Must have a message to reject!");
+ }else{
+ var _id = this.petition._id
+ Meteor.call('changePendingPetition', _id, false, reason, function(error, result) {
+ if (error) {
+ throwError(error.reason);
+ } else {
+ setTimeout( function() { $('#petitionRejectModal').modal("hide") }, 1000);
+ throwError(result);
+ }
+ });
+ }
+
+ }
+});
diff --git a/client/views/posts/post_report.html b/client/views/petitions/petition_report.html
similarity index 96%
rename from client/views/posts/post_report.html
rename to client/views/petitions/petition_report.html
index 143f1d7..8182f1f 100644
--- a/client/views/posts/post_report.html
+++ b/client/views/petitions/petition_report.html
@@ -1,6 +1,6 @@
-
+
Why are you reporting this petition?
-
\ No newline at end of file
+
diff --git a/client/views/posts/post_report.js b/client/views/petitions/petition_report.js
similarity index 58%
rename from client/views/posts/post_report.js
rename to client/views/petitions/petition_report.js
index 8ab6660..cb3e0b4 100644
--- a/client/views/posts/post_report.js
+++ b/client/views/petitions/petition_report.js
@@ -1,10 +1,10 @@
-Template.postReport.events({
+Template.petitionReport.events({
'submit form': function (e) {
e.preventDefault();
Session.set("waiting", true);
- var _id = this.post._id,
+ var _id = this.petition._id,
reason = $("input[name='report-reason']:checked").val();
Meteor.call('report', _id, reason, function(error) {
@@ -13,12 +13,12 @@ Template.postReport.events({
if (error) {
throwError(error.reason);
- GAnalytics.event("post", "report", error.reason);
+ GAnalytics.event("petition", "report", error.reason);
} else {
throwError("Petition reported.");
- setTimeout( function() { $('#postReportModal').modal("hide") }, 1000);
- GAnalytics.event("post", "report", _id);
+ setTimeout( function() { $('#petitionReportModal').modal("hide") }, 1000);
+ GAnalytics.event("petition", "report", _id);
}
});
}
-});
\ No newline at end of file
+});
diff --git a/client/views/posts/post_share_modal.html b/client/views/petitions/petition_share_modal.html
similarity index 91%
rename from client/views/posts/post_share_modal.html
rename to client/views/petitions/petition_share_modal.html
index afca4ad..a9e82bf 100644
--- a/client/views/posts/post_share_modal.html
+++ b/client/views/petitions/petition_share_modal.html
@@ -1,6 +1,6 @@
-
+
More Signatures Needed
You're only {{pluralize signaturesRemaining "signature"}}{{percentRemaining}} away from an official response!
-
Share this petition to increase your chances of reaching {{pluralize post.minimumVotes "signature"}}.
+
Share this petition to increase your chances of reaching {{pluralize petition.minimumVotes "signature"}}.
@@ -35,4 +35,4 @@
More Signatures Needed
-
\ No newline at end of file
+
diff --git a/client/views/petitions/petition_share_modal.js b/client/views/petitions/petition_share_modal.js
new file mode 100644
index 0000000..a61ab67
--- /dev/null
+++ b/client/views/petitions/petition_share_modal.js
@@ -0,0 +1,9 @@
+Template.petitionShareModal.helpers({
+ signaturesRemaining: function () {
+ return this.petition.minimumVotes - this.petition.votes;
+ },
+ percentRemaining: function () {
+ var percent = parseInt((this.petition.minimumVotes - this.petition.votes) / this.petition.minimumVotes * 100);
+ return percent < 40 ? " (" + percent + "%)" : "";
+ }
+});
diff --git a/client/views/petitions/petition_sign.html b/client/views/petitions/petition_sign.html
new file mode 100644
index 0000000..5c5abf3
--- /dev/null
+++ b/client/views/petitions/petition_sign.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/views/petitions/petition_sign.js b/client/views/petitions/petition_sign.js
new file mode 100644
index 0000000..17b86bf
--- /dev/null
+++ b/client/views/petitions/petition_sign.js
@@ -0,0 +1,92 @@
+Template.petitionSign.events({
+ 'submit form': function(e) {
+ e.preventDefault();
+ var petition = this.petition;
+ var sign = function () {
+ Meteor.call('sign', petition._id, function(error) {
+ if (error)
+ throwError(error.reason);
+ else {
+ var signaturesNeeded = petition.minimumVotes - petition.votes;
+ if (signaturesNeeded >= 1) {
+ $('#petitionShareModal').modal('show');
+ }
+ }
+ });
+ };
+ if (Meteor.userId()) {
+ sign();
+ } else {
+ Session.set("loginMsg", "Please login to sign.");
+ $('#loginModal').modal('show');
+ $('#loginModal').on('hidden.bs.modal', function () {
+ if (Meteor.userId())
+ sign();
+ });
+ }
+ }
+});
+
+Template.petitionSub.events({
+ 'submit form': function(e) {
+ e.preventDefault();
+ var petition = this.petition;
+ var sub = function () {
+ Meteor.call('subscribe', petition._id, function(error) {
+ if (error)
+ throwError(error.reason);
+ });
+ };
+ if (Meteor.userId()) {
+ sub();
+ } else {
+ Session.set("loginMsg", "Please login to subscribe.");
+ $('#loginModal').modal('show');
+ $('#loginModal').on('hidden.bs.modal', function () {
+ if (Meteor.userId())
+ sub();
+ });
+ }
+ }
+});
+
+Template.petitionSub.helpers({
+ subscribedClass: function() {
+ var userId = Meteor.userId();
+ if (userId && this.petition && _.include(this.petition.subscribers, userId)) {
+ return 'disabled';
+ } else {
+ return '';
+ }
+ },
+ subscribeBtnText: function() {
+ var userId = Meteor.userId();
+ if (userId && this.petition && _.include(this.petition.subscribers, userId)) {
+ return 'Subscribed';
+ } else {
+ return 'Subscribe';
+ }
+ }
+})
+
+Template.petitionSign.helpers({
+ signedClass: function() {
+ var userId = Meteor.userId();
+ if (userId && this.petition && _.include(this.petition.upvoters, userId) ||
+ moment(this.petition.submitted).isBefore(moment().subtract(1, 'month'))) {
+ return 'disabled';
+ } else {
+ return '';
+ }
+ },
+ btnText: function() {
+ var userId = Meteor.userId();
+ if (userId && this.petition && _.include(this.petition.upvoters, userId)) {
+ return 'Signed';
+ } else if (moment(this.petition.submitted).isBefore(moment().subtract(1, 'month'))) {
+ return 'Expired';
+ } else {
+ return 'Add My Name';
+ }
+ }
+});
diff --git a/client/views/petitions/petition_submit.html b/client/views/petitions/petition_submit.html
new file mode 100644
index 0000000..2966256
--- /dev/null
+++ b/client/views/petitions/petition_submit.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
Compose Petition
+
+
We recommend reading our one-minute guide on the Petition Process prior to making a submission.
+
Be careful! You can't edit or delete a petition once it is submitted.
+
+
+
+
Explanation and reasoning behind petition. Why should someone sign? How will it improve the community?
+
Your full name will be made public with this petition. If you aren't comfortable with that, consider working with your SG senator/rep or a relevant administrator to have it posted from another account.
+
+
+
+
Preview
+ {{> petitionCard petition=emptyPetition }}
+ {{> petitionCard petition=inProgressPetition }}
+ {{> petitionCard petition=respondedPetition }}
+
+
+
Your full name will be made public with this petition. If you aren't comfortable with that, consider working with your PawPrints provider to have it posted from another account.
+
+
+
+
Your full name will be made public with this petition. If you aren't comfortable with that, consider working with your PawPrints provider to have it posted from another account.
+
+
+
+
+
+
diff --git a/client/views/petitions/petition_submit.js b/client/views/petitions/petition_submit.js
new file mode 100755
index 0000000..8171646
--- /dev/null
+++ b/client/views/petitions/petition_submit.js
@@ -0,0 +1,124 @@
+//This function is called by event handlers and is debounced by a few milliseconds. Since the Session is reactive,
+//if this is not debounced, a user could have their text overwritten by what was just put in the session a few milliseconds ago.
+var savePetitionSessionState = function() {
+ Session.set('petition.title', $('*[name=title]').val());
+ Session.set('petition.description', $('textarea[name=description]').val());
+ Session.set('petition.tag_ids', _.pluck($('#s2id_tags').select2('data'), '_id'));
+};
+
+Template.petitionSubmit.helpers({
+ 'emptyPetition': function() {
+
+ return {
+ votes: 1,
+ author: Meteor.user().profile.name,
+ title: Session.get('petition.title'),
+ tag_ids: Session.get('petition.tag_ids')
+ }
+ },
+ 'title': function() {
+ return Session.get('petition.title');
+ },
+ 'author': function() {
+ return Meteor.user().profile.displayName;
+ }
+});
+
+Template.petitionSubmit.helpers({
+ 'respondedPetition': function() {
+
+ return {
+ votes: 1,
+ author: Meteor.user().profile.name,
+ title: Session.get('petition.title'),
+ status: 'responded',
+ tag_ids: Session.get('petition.tag_ids')
+ }
+ },
+ 'title': function() {
+ return Session.get('petition.title');
+ },
+ 'author': function() {
+ return Meteor.user().profile.displayName;
+ }
+});
+Template.petitionSubmit.helpers({
+ 'inProgressPetition': function() {
+
+ return {
+ votes: 1,
+ author: Meteor.user().profile.name,
+ title: Session.get('petition.title'),
+ status: 'waiting-for-reply',
+ tag_ids: Session.get('petition.tag_ids')
+ }
+ },
+ 'title': function() {
+ return Session.get('petition.title');
+ },
+ 'author': function() {
+ return Meteor.user().profile.displayName;
+ }
+});
+
+Template.petitionSubmit.events({
+ 'submit form': function(e) {
+ //Since this function is debounced by event handlers, it may not have run yet. This is to ensure someone doesn't
+ //quickly write up a petition and hammer the enter key before the debounced function is run.
+ savePetitionSessionState();
+ e.preventDefault();
+
+ var markupStr = $('#summernote').summernote('code');
+ var petition = {
+ title: Session.get('petition.title'),
+ description: markupStr,
+ tag_ids: Session.get('petition.tag_ids')
+ }
+
+ Meteor.call('petition', petition, function(error, id) {
+ if (error) {
+ // display the error to the user
+ throwError(error.reason);
+ if (error.error === 302)
+ Router.go('petitionPage', {_id: error.details})
+ } else {
+
+ Session.set('petition.title', '');
+ Session.set('petition.description', '');
+ if(Singleton.findOne().moderation){
+ Router.go('index');
+ throwError("Petition is pending approval, you will receive an email once it has gone thorugh the approval process.");
+ }else{
+ Router.go('petitionPage', {_id: id});
+ }
+ }
+ });
+ },
+ 'keyup *[name=title]': _.debounce(savePetitionSessionState, 250),
+ 'keyup *[name=description]': _.debounce(savePetitionSessionState, 250)
+});
+
+Template.petitionSubmit.rendered = function () {
+ Session.set('petition.tag_ids', []);
+
+ $('#summernote').summernote();
+
+ Deps.autorun(function () {
+ $('#tags').select2({
+ placeholder: "Petition Tags",
+ data: {results: Tags.find({}, {sort: {name: 1}}).fetch()},
+ multiple: true,
+ maximumSelectionSize: 3,
+ id: function (object) { return object._id; },
+ matcher: function(term, text, option) { return option.name.toUpperCase().indexOf(term.toUpperCase()) > -1; },
+ formatSelection: function (object, container) { return object.name.toUpperCase(); },
+ formatResult: function (object, container, query) { return object.name; },
+ formatNoMatches: function (object) { return "No tags found." },
+ formatSelectionTooBig: function (object) { return "You can only select up to 3 tags." }
+ });
+ $('.select2-search-field>input').addClass("input");
+ });
+ $('#tags').on("change", savePetitionSessionState);
+ // Accessing selected tags
+ // $('#s2id_tags').select2('data');
+};
diff --git a/client/views/petitions/petitions_list.html b/client/views/petitions/petitions_list.html
new file mode 100755
index 0000000..fc986a4
--- /dev/null
+++ b/client/views/petitions/petitions_list.html
@@ -0,0 +1,106 @@
+
+
+
+
Open Petitions
+ Petitions that haven't been recognized or resolved.
+
+ {{> petitionGroup petitions=petitions
+ petitionOrder=petitionOrder
+ title="Open/Expired Petitions"
+ tags=tags
+ nopetitions="Be the first to write a petition."
+ displayheader=false}}
+
+
+
+
+ {{> petitionGroup petitions=petitions
+ petitionOrder=petitionOrder
+ tag=this.tag
+ tags=tags
+ nopetitions="There are no petitions with that tag."
+ displayheader=true }}
+
+
+
+ {{> petitionGroup petitions=petitions
+ petitionOrder=petitionOrder
+ title="Recognized"
+ description="These petitions are currently being worked on. Status updates may be available on the petition page."
+ tags=tags
+ nopetitions="No petitions are currently in progress."
+ displayheader=true }}
+
+
+
+ {{> petitionGroup petitions=petitions
+ petitionOrder=petitionOrder
+ title="Responded"
+ description="These petitions have received responses."
+ tags=tags
+ nopetitions="No petitions have received responses."
+ displayheader=true}}
+
+
+
+
+ {{#if displayheader}}
+ {{> headerCarousel title=tagTitle description=description}}
+ {{/if}}
+
+
+
+ {{#unless pending}}
+
+
+
Sort
+
+ {{#if equal title "Responded"}}
+ Most Recent Responded
+ {{/if}}
+ Most Recent Submitted
+ Last Signed
+ Most Signatures
+
+
Filter
+ {{# if loggedIn}}
+
+ {{/if}}
+
+
+
+ {{/unless}}
+
+ {{#each petitions}}
+ {{> petitionCard petition=this }}
+ {{/each}}
+ {{#if empty petitions}}
+
+ {{/if}}
+
+
+
+
+
diff --git a/client/views/petitions/petitions_lists.js b/client/views/petitions/petitions_lists.js
new file mode 100755
index 0000000..642ce02
--- /dev/null
+++ b/client/views/petitions/petitions_lists.js
@@ -0,0 +1,60 @@
+Template.petitionGroup.events({
+ 'change #petition-order': function(evt) {
+ Session.set("petitionOrder", evt.currentTarget.value);
+ },
+ 'click #load-more': function () {
+ Session.set('petitionsLimit', Session.get('petitionsLimit') + 12);
+ },
+ 'click .tag-item': function (e) {
+ if(e.target.id == 'all'){
+ Session.set('activeTag', null);
+ }else{
+ Session.set('activeTag', e.target.id);
+ }
+ },
+ 'change #show-signed': function(e) {
+ Session.set('showSigned', $(e.target).is(':checked'));
+ },
+ 'change #show-created': function(e) {
+ Session.set('showCreated', $(e.target).is(':checked'));
+ }
+});
+
+Template.petitionGroup.helpers({
+ 'isActiveTag' : function(name){
+ if((name == Session.get('activeTag')) ||
+ (!Session.get('activeTag') && (name == 'all'))){
+ return 'active';
+ }else{
+ return '';
+ }
+ },
+ 'tagTitle' : function() {
+ if(Session.get('activeTag')){
+ return Session.get('activeTag') + ' Petitions';
+ }else{
+ return this.title;
+ }
+ },
+ 'moderation' : function(){
+ if(Singleton.findOne().moderation){
+ return true;
+ }else{
+ return false;
+ }
+ }
+});
+
+Template.petitionGroup.helpers({
+ 'sortType': function(title){
+ if(title == "Responses"){
+ return "responded_at"
+ }else{
+ return "submitted"
+ }
+ }
+});
+
+Template.petitionGroup.rendered = function () {
+ $('[data-toggle="tooltip"]').tooltip();
+};
diff --git a/client/views/posts/post_card.html b/client/views/posts/post_card.html
deleted file mode 100644
index c7a258a..0000000
--- a/client/views/posts/post_card.html
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
{{pluralize post.votes "Signature"}}
-
-
- {{#each post.tag_ids}}
- {{#with tag this}}
- {{upcase this.name}}
- {{/with}}
- {{/each}}
-
-
-
-
diff --git a/client/views/posts/post_page.html b/client/views/posts/post_page.html
deleted file mode 100644
index c17190d..0000000
--- a/client/views/posts/post_page.html
+++ /dev/null
@@ -1,124 +0,0 @@
-
-
-
-
-
-
-
{{post.title}}
-
- {{#each post.tag_ids}}
- {{#with tag this}}
- {{upcase this.name}}
- {{/with}}
- {{/each}}
- {{#if isInRole 'admin' 'moderator'}}
- EDIT
- {{/if}}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{#if post.response}}
-
-
-
-
OFFICIAL RESPONSE - LAST UPDATED {{ responded_at }}
-
{{{breaklines post.response }}}
-
-
-
- {{/if}}
-
- {{#if updates}}
-
-
-
-
STATUS UPDATES
- {{#each updates}}
- {{> updateBlurb}}
- {{/each}}
-
-
-
- {{/if}}
-
-
-
-
-
-
{{{breaklines post.description}}}
-
-
-
-
SIGNED BY
-
{{#each initials}}
- {{ this }}
- {{/each}}
-
-
-
-
-
-
-
TAKE ACTION
-
-
-
- SIGNATURES ({{post.votes}} / {{post.minimumVotes}})
-
-
-
-
-
-
- {{ petitionStatus.title }}
-
-
-
- {{#if equal this.post.status undefined}}
-
-
{{mustReachDate}}
- {{/if}}
-
-
- Report petition
-
-
-
-
-
-
- {{> postReport}}
- {{> postShareModal}}
-
\ No newline at end of file
diff --git a/client/views/posts/post_page.js b/client/views/posts/post_page.js
deleted file mode 100644
index a7c9ed0..0000000
--- a/client/views/posts/post_page.js
+++ /dev/null
@@ -1,100 +0,0 @@
-var social_links = {
- 'facebook': 'https://www.facebook.com/sharer/sharer.php?u=',
- 'twitter': 'https://twitter.com/intent/tweet?url=',
- 'reddit': 'http://www.reddit.com/submit?url=',
- 'plus': 'https://plus.google.com/share?url=',
- 'linkedin': 'https://www.linkedin.com/cws/share?url='
-};
-
-var timeTick = new Deps.Dependency();
-
-Meteor.setInterval(function () {
- timeTick.changed();
-}, 1000);
-
-
-Template.postPage.helpers({
- 'responded_at': function () {
- timeTick.depend();
- return new moment(this.post.responded_at).fromNow().toUpperCase();
- },
- 'submitted_at': function () {
- timeTick.depend();
- return new moment(this.post.submitted).fromNow().toUpperCase();
- },
- 'initials': function () {
- return Meteor.users.find({
- '_id': {
- $in: this.post.upvoters
- }
- }).fetch().map(function (user) {
- if (!user.profile.initials) {
- return "XYZ";
- } else {
- return user.profile.initials;
- }
- });
- },
- 'progress': function () {
- if (this.post.votes > this.post.minimumVotes) {
- return 100;
- } else {
- return (this.post.votes / this.post.minimumVotes) * 100;
- }
- },
- 'goalReachedClass': function () {
- return this.post.votes >= this.post.minimumVotes ? 'goal-reached' : '';
- },
- 'mustReachDate': function() {
- return new moment(this.post.submitted).add(1, 'month').format('ll');
- },
- 'petitionStatus': function () {
- var post = Posts.findOne();
- if (post.status == "waiting-for-reply") {
- return {
- title: "In Progress",
- description: "This petition is being reviewed by Student Government."
- };
- } else if (post.status == "responded") {
- return {
- title: "Responded",
- description: "This petition has recieved an official response."
- };
- } else {
- if (post.votes >= post.minimumVotes) {
- return {
- title: "Goal Met",
- description: "This petition has met its signature goal, but has not yet been reviewed by Student Government."
- };
- } else if (moment(post.submitted).isBefore(moment().subtract(1, 'month'))) {
- return {
- title: "Expired",
- description: "This petition didn't meet it's minimum signature goal within one month."
- };
- } else {
- return {
- title: "Goal Not Met",
- description: "This petition is below its signature threshold of " + post.minimumVotes + "."
- };
- }
- }
- }
-});
-
-Template.postPage.events({
- 'click *[social]': function (e) {
- var network = $(e.currentTarget).attr("social");
- var url = social_links[network] + this.url;
- GAnalytics.event("post", "share", network);
- window.open(url);
- }
-});
-
-Template.postPage.rendered = function () {
- Deps.autorun( function () {
- var post = Posts.findOne();
- $('.petition-status').tooltip('destroy');
- $('.petition-status').tooltip({title: Template.postPage.__helpers[" petitionStatus"]().description});
- $('.petition-expires').tooltip();
- });
-}
\ No newline at end of file
diff --git a/client/views/posts/post_search.html b/client/views/posts/post_search.html
deleted file mode 100644
index 704827d..0000000
--- a/client/views/posts/post_search.html
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
-
-
-
-
- ×
-
- Close
-
-
-
-
-
- {{> esInput id="search" index="posts" class="form-control input-lg input-clear" placeholder="Search (3 character minimum)" }}
-
-
- {{#if session 'waiting'}}
- {{> loading}}
- {{/if}}
-
- {{#each session 'results'}}
- {{> postCard post=this }}
- {{/each}}
-
- {{#if noResults}}
-
- {{/if}}
-
-
-
-
-
-
diff --git a/client/views/posts/post_search.js b/client/views/posts/post_search.js
deleted file mode 100644
index 55b7d5b..0000000
--- a/client/views/posts/post_search.js
+++ /dev/null
@@ -1,41 +0,0 @@
-Template.postSearch.rendered = function () {
- $('#modal-search').on('shown.bs.modal', function () {
- $('body').css('overflow', 'hidden');
- $('.modal-backdrop').css('opacity', '0.5');
- });
- $('#modal-search').on('hidden.bs.modal', function () {
- $('body').css('overflow', 'visible');
- $('.modal-backdrop').css('opacity', '0');
- $('#search').val("");
- Session.set("results", null);
- });
-};
-
-Template.postSearch.events({
- 'keyup #search': function (e) {
- var query = $(e.target).val();
- if (query.length >= 3) {
- setTimeout( function() {
- Session.set("waiting", true);
- EasySearch.search('posts', query, function (err, data) {
- Session.set("waiting", false);
- Session.set("results", data.results);
- });
- GAnalytics.event("post", "search", query);
- }, 100);
- }
- },
- 'click a': function (e) {
- $('#modal-search').modal("hide");
- }
-});
-
-Template.postSearch.helpers({
- noResults: function() {
- return Session.get('results') && Session.get('results').length == 0;
- }
-});
-
-Template.postSearch.rendered = function () {
- $("#search").attr('autocomplete', 'off');
-}
\ No newline at end of file
diff --git a/client/views/posts/post_share_modal.js b/client/views/posts/post_share_modal.js
deleted file mode 100644
index 8b01efe..0000000
--- a/client/views/posts/post_share_modal.js
+++ /dev/null
@@ -1,9 +0,0 @@
-Template.postShareModal.helpers({
- signaturesRemaining: function () {
- return this.post.minimumVotes - this.post.votes;
- },
- percentRemaining: function () {
- var percent = parseInt((this.post.minimumVotes - this.post.votes) / this.post.minimumVotes * 100);
- return percent < 40 ? " (" + percent + "%)" : "";
- }
-});
\ No newline at end of file
diff --git a/client/views/posts/post_sign.html b/client/views/posts/post_sign.html
deleted file mode 100644
index f397ba0..0000000
--- a/client/views/posts/post_sign.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
diff --git a/client/views/posts/post_sign.js b/client/views/posts/post_sign.js
deleted file mode 100644
index ed64f2d..0000000
--- a/client/views/posts/post_sign.js
+++ /dev/null
@@ -1,50 +0,0 @@
-Template.postSign.events({
- 'submit form': function(e) {
- e.preventDefault();
- var post = this.post;
- var sign = function () {
- Meteor.call('sign', post._id, function(error) {
- if (error)
- throwError(error.reason);
- else {
- var signaturesNeeded = post.minimumVotes - post.votes;
- if (signaturesNeeded >= 1) {
- $('#postShareModal').modal('show');
- }
- }
- });
- };
- if (Meteor.userId()) {
- sign();
- } else {
- Session.set("loginMsg", "Please login to sign.");
- $('#loginModal').modal('show');
- $('#loginModal').on('hidden.bs.modal', function () {
- if (Meteor.userId())
- sign();
- });
- }
- }
-});
-
-Template.postSign.helpers({
- signedClass: function() {
- var userId = Meteor.userId();
- if (userId && this.post && _.include(this.post.upvoters, userId) ||
- moment(this.post.submitted).isBefore(moment().subtract(1, 'month'))) {
- return 'disabled';
- } else {
- return '';
- }
- },
- btnText: function() {
- var userId = Meteor.userId();
- if (userId && this.post && _.include(this.post.upvoters, userId)) {
- return 'Signed';
- } else if (moment(this.post.submitted).isBefore(moment().subtract(1, 'month'))) {
- return 'Expired';
- } else {
- return 'Sign';
- }
- }
-});
diff --git a/client/views/posts/post_submit.html b/client/views/posts/post_submit.html
deleted file mode 100644
index f224470..0000000
--- a/client/views/posts/post_submit.html
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
-
Create a New Petition
-
-
-
We recommend reading our one-minute guide on the Petition Process prior to making a submission.
-
Be careful! You can't edit or delete a petition once it is submitted.
-
-
-
-
-
-
Preview Petition Card
-
-
- {{> postCard post=emptyPost }}
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/client/views/posts/post_submit.js b/client/views/posts/post_submit.js
deleted file mode 100644
index 4288c84..0000000
--- a/client/views/posts/post_submit.js
+++ /dev/null
@@ -1,81 +0,0 @@
-Template.postSubmit.helpers({
- 'emptyPost': function() {
-
- return {
- votes: 1,
- author: Meteor.user().profile.name,
- title: Session.get('post.title'),
- tag_ids: Session.get('post.tag_ids')
- }
- },
- 'title': function() {
- return Session.get('post.title');
- },
- 'author': function() {
- return Meteor.user().profile.displayName;
- }
-});
-
-Template.postSubmit.events({
- 'submit form': function(e) {
- e.preventDefault();
-
- var post = {
- title: Session.get('post.title'),
- description: Session.get('post.description'),
- tag_ids: Session.get('post.tag_ids')
- }
-
- Meteor.call('post', post, function(error, id) {
- if (error) {
- // display the error to the user
- throwError(error.reason);
- if (error.error === 302)
- Router.go('postPage', {_id: error.details})
- } else {
- Session.set('post.title', '');
- Session.set('post.description', '');
- Router.go('postPage', {_id: id});
- }
- });
- },
- 'keyup *[name=title]': function (e) {
- Session.set('post.title', $('*[name=title]').val());
- },
- 'keyup *[name=description]': function (e) {
- Session.set('post.description', $('textarea[name=description]').val());
- }
-});
-
-Template.postSubmit.rendered = function () {
- Session.set('post.tag_ids', []);
- Deps.autorun(function () {
- $('#tags').select2({
- placeholder: "Petition Tags",
- data: {results: Tags.find({}, {sort: {name: 1}}).fetch()},
- multiple: true,
- maximumSelectionSize: 3,
- id: function (object) { return object._id; },
- matcher: function(term, text, option) { return option.name.toUpperCase().indexOf(term.toUpperCase()) > -1; },
- formatSelection: function (object, container) { return object.name.toUpperCase(); },
- formatResult: function (object, container, query) { return object.name; },
- formatNoMatches: function (object) { return "No tags found." },
- formatSelectionTooBig: function (object) { return "You can only select up to 3 tags." }
- });
- $('.select2-search-field>input').addClass("input");
- });
- $('#tags').on("change", function (e) {
- var tag_ids = Session.get('post.tag_ids')
- if (e.added) {
- tag_ids.push(e.added._id);
- Session.set('post.tag_ids', tag_ids);
- } else if (e.removed) {
- var tag_ids = _.without(tag_ids, e.removed._id);
- Session.set('post.tag_ids', tag_ids);
- } else {
- // to-do
- }
- });
- // Accessing selected tags
- // $('#s2id_tags').select2('data');
-};
diff --git a/client/views/posts/posts_list.html b/client/views/posts/posts_list.html
deleted file mode 100644
index 4e83a95..0000000
--- a/client/views/posts/posts_list.html
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
- {{> petitionGroup posts=posts
- postOrder=postOrder
- title="All Petitions"
- noposts="Be the first to petition your government." }}
-
-
-
-
-
- {{> petitionGroup posts=posts
- postOrder=postOrder
- tag=this.tag
- noposts="There are no petitions with that tag." }}
-
-
-
-
-
- {{> petitionGroup posts=posts
- postOrder=postOrder
- title="In Progress"
- description="These petitions are currently being worked on by Student Government. Status updates may be available on the petition page."
- noposts="No petitions are currently in progress." }}
-
-
-
-
-
- {{> petitionGroup posts=posts
- postOrder=postOrder
- title="Responses"
- description="These petitions have recieved official responses."
- noposts="No petitions have received responses." }}
-
-
-
-
-
-
- {{#if empty posts}}
- {{else}}
-
-
- {{#if this.tag}}
- {{this.tag}} Petitions
- {{else}}
- {{title}}
- {{/if}}
- {{#if description}}
-
- {{/if}}
-
-
-
-
- Sort By
-
- Most Recent
- Most Signatures
-
-
- {{/if}}
- {{#each posts}}
- {{> postCard post=this }}
- {{/each}}
- {{#if empty posts}}
-
- {{/if}}
-
-
\ No newline at end of file
diff --git a/client/views/posts/posts_lists.js b/client/views/posts/posts_lists.js
deleted file mode 100644
index e803335..0000000
--- a/client/views/posts/posts_lists.js
+++ /dev/null
@@ -1,12 +0,0 @@
-Template.petitionGroup.events({
- 'change #petition-order': function(evt) {
- Session.set("postOrder", evt.currentTarget.value);
- },
- 'click #load-more': function () {
- Session.set('postsLimit', Session.get('postsLimit') + 12);
- }
-});
-
-Template.petitionGroup.rendered = function () {
- $('[data-toggle="tooltip"]').tooltip();
-};
\ No newline at end of file
diff --git a/client/views/search/search.html b/client/views/search/search.html
new file mode 100755
index 0000000..1410da5
--- /dev/null
+++ b/client/views/search/search.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+ {{#if session 'waiting'}}
+ {{> loading}}
+ {{/if}}
+
+ {{#each session 'results'}}
+ {{> petitionCard petition=this title="In Progress"}}
+ {{/each}}
+
+ {{#if noResults}}
+
+ {{/if}}
+
+
+
+
diff --git a/client/views/search/search.js b/client/views/search/search.js
new file mode 100755
index 0000000..b935522
--- /dev/null
+++ b/client/views/search/search.js
@@ -0,0 +1,33 @@
+Template.search.events({
+ 'keyup #search': _.debounce(function (e) {
+ console.log('search');
+ var query = $(e.target).val();
+ if (query.length >= 0) {
+ Session.set("waiting", true);
+ var cursor = PetitionsIndex.search(query);
+ Session.set("results", cursor.fetch());
+ Session.set("waiting", false);
+
+ GAnalytics.event("petition", "search", query);
+ }
+ }, 500),
+ 'click a': function (e) {
+ $('#modal-search').modal("hide");
+ },
+ 'submit form': function(event) {
+ event.preventDefault();
+ }
+
+});
+
+Template.search.helpers({
+ noResults: function() {
+ return Session.get('results') && Session.get('results').length == 0;
+ }
+});
+
+Template.search.rendered = function () {
+ $('#search').focus();
+ Session.set("results", null);
+ Session.set("waiting", false);
+}
diff --git a/client/views/static/about.html b/client/views/static/about.html
index eaff9d2..4b8d1d2 100644
--- a/client/views/static/about.html
+++ b/client/views/static/about.html
@@ -1,8 +1,6 @@
-
About
-
@@ -10,9 +8,9 @@
-
Student Government President Ashley Carrington and Vice President Tyler Pierce (2014-2015) sought to improve engagement of RIT Students through a Petition site. The site creates a place for the RIT community to converse on important issues.
-
To remain transparent, this site is an open source project available on GitHub . Additionally, API Access gives the community a convenient way to access the information available through this service.
-
+
PawPrints was developed as free software at the Rochester Institute of Technology by Student Government in 2014.
+
Since 2014, PawPrints has grown to all of RIT's international campuses, RPI, and many more organizations to come.
+
@@ -25,4 +23,4 @@
-
\ No newline at end of file
+
diff --git a/client/views/static/about_template.html b/client/views/static/about_template.html
index 4f14e53..0c826aa 100644
--- a/client/views/static/about_template.html
+++ b/client/views/static/about_template.html
@@ -1,10 +1,11 @@
-
+ {{> headerCarousel title=title description=""}}
+
-
-
\ No newline at end of file
+
diff --git a/client/views/static/about_template.js b/client/views/static/about_template.js
new file mode 100644
index 0000000..a023944
--- /dev/null
+++ b/client/views/static/about_template.js
@@ -0,0 +1,5 @@
+Template.aboutTemplate.helpers({
+ title: function(){
+ return document.title;
+ }
+});
diff --git a/client/views/static/api.html b/client/views/static/api.html
index 5911791..2c8f25a 100644
--- a/client/views/static/api.html
+++ b/client/views/static/api.html
@@ -1,7 +1,4 @@
-
- API Access
-
@@ -13,16 +10,12 @@
-
Student Government provides a read-only JSON REST API for retreiving SG Petition information.
+
This service provides a read-only JSON REST API for retreiving petition information.
-
Developers utilizing the API should keep the stability of the API's infrastructure and their own applications in mind. We may impose usage limits at any time or degrade performance for developers who create an unreasonable number of requests.
+
Developers utilizing the API should keep the stability of the API's infrastructure and their own applications in mind. This API is not intended for production use.
-
Availability: While we strive to maintain a highly available API, there may be times where it is unavailable for unanticipated causes. We encourage developers to write code that can handle these situations.
-
-
Unpublished Petitions: At any time, administrators may choose to unpublish a petition (a decision that can be later reversed). Unpublished petitions are not visible publicly on the site, nor are they available via the API.
-
Developers: Claim an API Key by going to your profile .
@@ -57,7 +50,7 @@
Summary
Includes the petition's name, submission time, title, description, minimum signature threshold, and signature count. The petition's response and responded at time are also included, if a response has been made. There is a limit of 500 petitions.
-
+
Path Parameters
@@ -114,8 +107,8 @@ Summary
-
Provides identical information as the Petition List, but also includes the initials of petition signers and up to a 7-day historical reading of the petition count (smaller for petitions less than one week in age).
-
+
Provides identical information as the Petition List, but also includes the initials of petition signatories and up to a 7-day historical reading of the petition count (smaller for petitions less than one week in age).
+
Path Parameters
@@ -146,4 +139,4 @@ Example Response Object
-
\ No newline at end of file
+
diff --git a/client/views/static/api.js b/client/views/static/api.js
index 6ca8021..d48969c 100644
--- a/client/views/static/api.js
+++ b/client/views/static/api.js
@@ -25,7 +25,7 @@ Template.api.helpers({
return JSON.stringify(response, undefined, 2);
},
example_individual: function makeExamplePetition () {
- var response = {
+ var response = {
author: "Pete Mikitsh",
submitted: 1410112945911,
title: "Extend hours for RIT Computer Labs at peak times.",
@@ -33,24 +33,24 @@ Template.api.helpers({
votes: 4,
_id: "bDA8ynBMErm9NLaqj",
minimumVotes: 25,
- signers:[
+ signatories:[
"PAM",
"ALC",
"TSP",
"NXC"
],
- history:[
- {
+ history:[
+ {
created_at: 1410112945912,
votes: 1,
_id: "ibdMPcGaS5gGmBEha"
},
- {
+ {
created_at: 1410199345913,
votes: 2,
_id: "5bw2TeyDK48XcqSiw"
},
- {
+ {
created_at: 1410285745914,
votes: 4,
_id: "jZu2D26gwRcZExFy2"
@@ -59,4 +59,4 @@ Template.api.helpers({
};
return JSON.stringify(response, undefined, 2);
}
-});
\ No newline at end of file
+});
diff --git a/client/views/static/moderation.html b/client/views/static/moderation.html
index c66d567..58bbbbd 100644
--- a/client/views/static/moderation.html
+++ b/client/views/static/moderation.html
@@ -1,17 +1,9 @@
-
- Moderation Policy
-
- "Since free and civil discourse is at the heart of a university community, users should communicate in a manner that advances the cause of learning and mutual understanding."
- RIT Code of Conduct for Computer and Network Use
-
-
Use of this site falls under the RIT Code of Conduct for Computer and Network Use .
-
Student Government reserves the right to edit or remove any petition at any time for violating the Code of Conduct. This includes, but is not limited to, creating an intimidating, hostile or abusive environment for any member of the RIT community, or posting of any obscene, defamatory, threatening, or otherwise harassing petitions.
-
When using this service, you agree to sign petitions from only one RIT Computer Account. Should you have access to more than one account, you will only sign from your primary student, faculty, or staff account.
-
Please exercise good judgment when using this service.
+
PawPrints supports moderation! Administrators may remove petitions at their discretion. Optionally, the application may be configured to put petitions into a moderation queue before they are made public.
+
Admins: Use this space to describe your moderation policy. Short and sweet is ideal!
-
\ No newline at end of file
+
diff --git a/client/views/static/petition_process.html b/client/views/static/petition_process.html
index 246af48..df0c27f 100644
--- a/client/views/static/petition_process.html
+++ b/client/views/static/petition_process.html
@@ -1,7 +1,4 @@
-
- The Petition Process
-
@@ -10,7 +7,7 @@
Search for petitions and if a petition similar to yours already exists, sign that petition.
-
This will help raise support for issue and help you better reach the minimum threshold of {{singleton.minimumThreshold}} signatures (as of {{ threshold_last_updated }}) for receiving an official Student Government response.
+
This will help raise support for issue and help you better reach the minimum threshold of {{singleton.minimumThreshold}} signatures (as of {{ threshold_last_updated }}) for receiving an official response.
@@ -36,7 +33,7 @@
Use social media to spread awareness of your issue.
-
A petition has one month to reach its threshold. If the threshold is not reached, then the petition is archived and available via the search. Student Government may elect to respond to a petition prior to reaching its threshold.
+
Make sure you get enough signatures before the petition expires!
Submit a petition.
diff --git a/client/views/static/petition_process.js b/client/views/static/petition_process.js
index 8f22169..6bd48af 100644
--- a/client/views/static/petition_process.js
+++ b/client/views/static/petition_process.js
@@ -2,4 +2,4 @@ Template.petitionProcess.helpers({
threshold_last_updated: function thresholdLastUpdatedAt () {
return new moment(Singleton.findOne().threshold_updated_at).format("L");
}
-});
\ No newline at end of file
+});
diff --git a/client/views/updates/create_update.html b/client/views/updates/create_update.html
index f44eed3..c9e6b96 100644
--- a/client/views/updates/create_update.html
+++ b/client/views/updates/create_update.html
@@ -27,14 +27,7 @@
{{fromNow null}} BY {{upcase user.profile.name}}
{{/if}}
-
+
-
- {{this.title}}
-
- {{fromNow this.created_at}} BY {{upcase this.author}}
+ {{this.title}}
+ {{#if showAuthor}}Update by {{this.author}} - {{/if}}{{fromNow this.created_at}}
+
{{{breaklines this.description }}}
-
\ No newline at end of file
+
diff --git a/client/views/updates/update_blurb.js b/client/views/updates/update_blurb.js
new file mode 100644
index 0000000..fc7ecff
--- /dev/null
+++ b/client/views/updates/update_blurb.js
@@ -0,0 +1,8 @@
+Template.updateBlurb.helpers({
+ 'showAuthor': function () {
+ return Singleton.findOne().updateAuthorDisplay;
+ },
+ 'submitted_date':function(){
+ return new moment(this.created_at).format('MMMM Do YYYY, h:mm a');
+ }
+});
diff --git a/client/views/users/user_edit.html b/client/views/users/user_edit.html
index a498d1c..8daad99 100644
--- a/client/views/users/user_edit.html
+++ b/client/views/users/user_edit.html
@@ -17,9 +17,27 @@ Profile
About Me
-
Username: {{ user.username }}
-
Name: {{ user.profile.name }}
-
Initials: {{ user.profile.initials }}
+
+ Username: {{ user.username }}
+ Name: {{ user.profile.name }}
+
+ Initials:
+ {{#if publicSettings.ui.initials_locked}}
+ {{ user.profile.initials }}
+ {{else}}
+
+ {{/if}}
+
+ Email: {{ user.profile.mail }}
+
+ {{#unless publicSettings.ui.initials_locked}}
+
+
+
+ {{/unless}}
+
@@ -30,13 +48,13 @@ Profile
Notification Settings
-
+
diff --git a/client/views/users/user_edit.js b/client/views/users/user_edit.js
old mode 100644
new mode 100755
index 879c165..7248979
--- a/client/views/users/user_edit.js
+++ b/client/views/users/user_edit.js
@@ -1,5 +1,5 @@
Template.userEdit.events({
- 'submit form': function(e) {
+ 'submit #notification-form': function(e) {
e.preventDefault();
var notificationPrefs = {
@@ -16,6 +16,22 @@ Template.userEdit.events({
}
});
},
+ 'submit #profile-form': function(e) {
+ e.preventDefault();
+
+ var profilePrefs = {
+ initials: $(e.target).find('[name=initials]').val()
+ };
+
+ Meteor.call('editProfile', profilePrefs, function(error) {
+ if (error) {
+ // display the error to the user
+ throwError(error.reason);
+ } else {
+ throwError("Profile saved.");
+ }
+ });
+ },
'click .get-key': function (e) {
e.preventDefault();
Meteor.call('createApiKey', function(error, key) {
diff --git a/collections/apiKeys.js b/collections/apiKeys.js
index 7f93021..eb6320c 100644
--- a/collections/apiKeys.js
+++ b/collections/apiKeys.js
@@ -20,4 +20,4 @@ Meteor.methods({
return key;
}
-});
\ No newline at end of file
+});
diff --git a/collections/petitions.js b/collections/petitions.js
new file mode 100755
index 0000000..8076f6c
--- /dev/null
+++ b/collections/petitions.js
@@ -0,0 +1,219 @@
+Petitions = new Meteor.Collection('petitions');
+
+if (Meteor.isServer)
+ Petitions._ensureIndex({title: 1}, {unique: 1});
+
+PetitionsIndex = new EasySearch.Index({
+ collection: Petitions,
+ fields: ['title', 'description', 'author'],
+ engine: new EasySearch.Minimongo()
+}, {
+ limit: 50
+})
+
+var validatePetitionOnCreate = function validatePetitionOnCreate (petitionAttributes) {
+
+ // ensure title is unique
+ if (Petitions.findOne({title: petitionAttributes.title}))
+ throw new Meteor.Error(422, 'This title has already been used. Write a different one.');
+
+};
+
+var validatePetition = function validatePetition (petitionAttributes) {
+
+ // ensure the user is logged in
+ if (!Meteor.user())
+ throw new Meteor.Error(401, "You need to login to do that.");
+
+ // ensure the petition has a title
+ if (!petitionAttributes.title || !petitionAttributes.title.trim())
+ throw new Meteor.Error(422, 'Please fill in a \n title.');
+
+ var titleLength = petitionAttributes.title.length;
+ if (titleLength > 70)
+ throw new Meteor.Error(422, 'Title must not exceed 70 characters. Currently: ' + titleLength );
+
+ // ensure the petition has at least one tag
+ if (!petitionAttributes.tag_ids || petitionAttributes.tag_ids.length == 0)
+ throw new Meteor.Error(422, 'Please add at least one tag to the petition.');
+
+ if (petitionAttributes.tag_ids.length > 3)
+ throw new Meteor.Error(422, 'Petitions are limited to at most 3 tags.');
+
+ // ensure the petition has a description
+ if (!petitionAttributes.description || !petitionAttributes.description.trim())
+ throw new Meteor.Error(422, 'Please fill in a description.');
+
+};
+
+Meteor.methods({
+ petition: function(petitionAttributes) {
+
+ validatePetition(petitionAttributes);
+ validatePetitionOnCreate(petitionAttributes);
+
+ var user = Meteor.user();
+ var publishByDefault = !(Singleton.findOne().moderation);
+
+ // pick out the whitelisted keys
+ var petition = _.extend(_.pick(petitionAttributes, 'title', 'description', 'tag_ids'), {
+ userId: user._id,
+ author: user.profile.name,
+ submitted: new Date().getTime(),
+ upvoters: [user._id],
+ subscribers: [user._id],
+ votes: 1,
+ minimumVotes: Singleton.findOne().minimumThreshold,
+ published: publishByDefault,
+ pending: Singleton.findOne().moderation,
+ lastSignedAt: new Date().getTime()
+ });
+
+ var petitionId = Petitions.insert(petition);
+
+ Singleton.update({}, {$inc: {petitionsCount: 1}});
+
+ return petitionId;
+ },
+
+ sign: function(petitionId) {
+ var user = Meteor.user();
+
+ if (!user)
+ throw new Meteor.Error(401, "You need to login to sign a petition.");
+
+ var petition = Petitions.findOne(petitionId);
+
+ if (!petition.published)
+ throw new Meteor.Error(401, "This petition is not published.");
+
+ if (moment(petition.submitted).isBefore(moment().subtract(1, 'month')))
+ throw new Meteor.Error(401, "This petition has expired.");
+
+ Petitions.update({
+ _id: petitionId,
+ upvoters: {$ne: user._id}
+ }, {
+ $addToSet: {upvoters: user._id},
+ $inc: {votes: 1},
+ $set: {lastSignedAt: new Date().getTime()}
+ });
+ Petitions.update({
+ _id: petitionId,
+ subscribers: {$ne: user._id}
+ }, {
+ $addToSet: {subscribers: user._id}
+ });
+ if(Meteor.isServer){
+ if (petition.votes === petition.minimumVotes && Meteor.isServer) {
+ var users = Meteor.users.find({roles: {$in: ['notify-threshold-reached']}});
+ var emails = users.map(function (user) { return user.profile.mail || user.username + Meteor.settings.MAIL.default_domain; });
+
+ if (!_.isEmpty(emails)) {
+ Mailer.sendTemplatedEmail(
+ "petition_threshold_reached",
+ {
+ to: emails
+ },
+ {
+ petition: petition
+ }
+ );
+ }
+ }
+ }
+
+ },
+ subscribe: function(petitionId) {
+ var user = Meteor.user();
+
+ if (!user)
+ throw new Meteor.Error(401, "You need to login to subscribe to a petition.");
+
+ var petition = Petitions.findOne(petitionId);
+
+ if (!petition.published)
+ throw new Meteor.Error(401, "This petition is not published.");
+
+ Petitions.update({
+ _id: petitionId,
+ subscribers: {$ne: user._id}
+ }, {
+ $addToSet: {subscribers: user._id}
+ });
+ },
+ edit: function (petitionId, petitionAttributes) {
+
+ var oldPetition = Petitions.findOne(petitionId, {
+ fields: { response: 1,
+ upvoters: 1,
+ subscribers: 1,
+ author: 1 }});
+
+ validatePetition(petitionAttributes);
+
+ var user = Meteor.user();
+
+ if (!Roles.userIsInRole(user, ['admin', 'moderator']))
+ throw new Meteor.Error(403, "You are not authorized to edit petitions.");
+
+ // pick out the whitelisted keys
+ var petition = _.extend(_.pick(petitionAttributes, 'title', 'description', 'response', 'status', 'tag_ids'));
+ if (_.isEmpty(petition.response)) {
+ petition.response = null;
+ petition.responded_at = null;
+ petition.status = "waiting-for-reply";
+ } else {
+ petition.responded_at = new Date().getTime();
+ }
+
+ Petitions.update(petitionId, {$set: petition });
+
+ if (_.isEmpty(oldPetition.response) && !_.isEmpty(petition.response)) {
+
+ Petitions.update(petitionId, {$set: {status: "responded"}});
+
+ this.unblock();
+
+ if(Meteor.isServer){
+ var notifyees = Meteor.users.find({$and: [{'notify.response': true},
+ {_id: {$in: oldPetition.subscribers}}]},
+ {fields: {username: 1}});
+
+ var emails = notifyees.map(function (user) { return user.username + Meteor.settings.MAIL.default_domain; });
+
+ Mailer.sendTemplatedEmail("petition_response_received", {
+ bcc: emails
+ },{
+ petition: petition,
+ oldPetition: oldPetition
+ }
+ );
+ }
+ }
+ },
+ delete: function (petitionId) {
+
+ var user = Meteor.user();
+
+ if (!Roles.userIsInRole(user, ['admin']))
+ throw new Meteor.Error(403, "You are not authorized to delete petitions.");
+
+ Petitions.remove(petitionId);
+
+ Singleton.update({}, {$inc: {petitionsCount: -1}});
+
+ },
+
+ changePublishStatus: function (petitionId) {
+
+ var user = Meteor.user();
+
+ if (!Roles.userIsInRole(user, ['admin']))
+ throw new Meteor.Error(403, "You are not authorized to change publishing status.");
+
+ var petition = Petitions.findOne(petitionId);
+ Petitions.update(petitionId, {$set: {published: !petition.published}});
+
+ }
+});
diff --git a/collections/posts.js b/collections/posts.js
deleted file mode 100644
index 3d17ccc..0000000
--- a/collections/posts.js
+++ /dev/null
@@ -1,191 +0,0 @@
-Posts = new Meteor.Collection('posts');
-
-if (Meteor.isServer)
- Posts._ensureIndex({title: 1}, {unique: 1});
-
-Posts.initEasySearch(
- [ 'title', 'description', 'author' ],
- {
- 'limit' : 50,
- 'use': 'mongo-db'
- }
-);
-
-var validatePostOnCreate = function validatePostOnCreate (postAttributes) {
-
- // ensure title is unique
- if (Posts.findOne({title: postAttributes.title}))
- throw new Meteor.Error(422, 'This title has already been used. Write a different one.');
-
-};
-
-var validatePost = function validatePost (postAttributes) {
-
- // ensure the user is logged in
- if (!Meteor.user())
- throw new Meteor.Error(401, "You need to login to do that.");
-
- // ensure the post has a title
- if (!postAttributes.title || !postAttributes.title.trim())
- throw new Meteor.Error(422, 'Please fill in a \n title.');
-
- var titleLength = postAttributes.title.length;
- if (titleLength > 70)
- throw new Meteor.Error(422, 'Title must not exceed 70 characters. Currently: ' + titleLength );
-
- // ensure the post has at least one tag
- if (!postAttributes.tag_ids || postAttributes.tag_ids.length == 0)
- throw new Meteor.Error(422, 'Please add at least one tag to the petition.');
-
- if (postAttributes.tag_ids.length > 3)
- throw new Meteor.Error(422, 'Petitions are limited to at most 3 tags.');
-
- // ensure the post has a description
- if (!postAttributes.description || !postAttributes.description.trim())
- throw new Meteor.Error(422, 'Please fill in a description.');
-
- var descriptionLength = postAttributes.title.length;
- if (descriptionLength > 4000)
- throw new Meteor.Error(422, 'Description must not exceed 4000 characters. Currently: ' + descriptionLength );
-};
-
-Meteor.methods({
- post: function(postAttributes) {
-
- validatePost(postAttributes);
- validatePostOnCreate(postAttributes);
-
- var user = Meteor.user();
-
- // pick out the whitelisted keys
- var post = _.extend(_.pick(postAttributes, 'title', 'description', 'tag_ids'), {
- userId: user._id,
- author: user.profile.name,
- submitted: new Date().getTime(),
- upvoters: [user._id],
- votes: 1,
- minimumVotes: Singleton.findOne().minimumThreshold,
- published: true
- });
-
- var postId = Posts.insert(post);
-
- Singleton.update({}, {$inc: {postsCount: 1}});
-
- return postId;
- },
-
- sign: function(postId) {
- var user = Meteor.user();
-
- if (!user)
- throw new Meteor.Error(401, "You need to login to sign a petition.");
-
- var post = Posts.findOne(postId);
-
- if (!post.published)
- throw new Meteor.Error(401, "This petition is not published.");
-
- if (moment(post.submitted).isBefore(moment().subtract(1, 'month')))
- throw new Meteor.Error(401, "This petition has expired.");
-
- Posts.update({
- _id: postId,
- upvoters: {$ne: user._id}
- }, {
- $addToSet: {upvoters: user._id},
- $inc: {votes: 1}
- });
-
- if (post.votes === post.minimumVotes && Meteor.isServer) {
- var users = Meteor.users.find({roles: {$in: ['notify-threshold-reached']}});
- var emails = users.map(function (user) { return user.username + "@rit.edu"; });
-
- if (!_.isEmpty(emails)) {
- Email.send({
- to: emails,
- from: "sgnoreply@rit.edu",
- subject: "PawPrints - Petition Reaches Signature Threshold",
- text: "Petition \"" + post.title + "\" by " + post.author + " has reached its minimum signature goal: \n\n" +
- Meteor.settings.public.root_url + "/petitions/" + postId +
- "\n\nThanks, \nRIT Student Government"
- });
- }
- }
-
- },
-
- edit: function (postId, postAttributes) {
-
- var oldPost = Posts.findOne(postId, {
- fields: { response: 1,
- upvoters: 1,
- author: 1 }});
-
- validatePost(postAttributes);
-
- var user = Meteor.user();
-
- if (!Roles.userIsInRole(user, ['admin', 'moderator']))
- throw new Meteor.Error(403, "You are not authorized to edit petitions.");
-
- // pick out the whitelisted keys
- var post = _.extend(_.pick(postAttributes, 'title', 'description', 'response', 'status', 'tag_ids'));
-
- if (_.isEmpty(post.response)) {
- delete post.response;
- } else {
- post.responded_at = new Date().getTime();
- }
-
- Posts.update(postId, {$set: post });
-
- if (_.isEmpty(oldPost.response) && !_.isEmpty(post.response)) {
-
- Posts.update(postId, {$set: {status: "responded"}});
-
- this.unblock();
-
- var users = Meteor.users.find({$and: [{'notify.response': true},
- {_id: {$in: oldPost.upvoters}}]},
- {fields: {username: 1}});
-
- var emails = users.map(function (user) { return user.username + "@rit.edu"; });
-
- Email.send({
- bcc: emails,
- to: "sgnoreply@rit.edu",
- from: "sgnoreply@rit.edu",
- subject: "PawPrints - A petition you signed has received a response",
- text: "Hello, \n\n" +
- "Petition \"" + post.title + "\" by " + oldPost.author + " has recieved a response: \n\n" +
- Meteor.settings.public.root_url + "/petitions/" + oldPost._id +
- "\n\nThanks, \nRIT Student Government"
- });
- }
- },
- delete: function (postId) {
-
- var user = Meteor.user();
-
- if (!Roles.userIsInRole(user, ['admin']))
- throw new Meteor.Error(403, "You are not authorized to delete petitions.");
-
- Posts.remove(postId);
-
- Singleton.update({}, {$inc: {postsCount: -1}});
-
- },
-
- changePublishStatus: function (postId) {
-
- var user = Meteor.user();
-
- if (!Roles.userIsInRole(user, ['admin']))
- throw new Meteor.Error(403, "You are not authorized to change publishing status.");
-
- var post = Posts.findOne(postId);
- Posts.update(postId, {$set: {published: !post.published}});
-
- }
-});
diff --git a/collections/scores.js b/collections/scores.js
deleted file mode 100644
index 003f65d..0000000
--- a/collections/scores.js
+++ /dev/null
@@ -1 +0,0 @@
-Scores = new Meteor.Collection('scores');
diff --git a/collections/singleton.js b/collections/singleton.js
index 76dff4a..54447e9 100644
--- a/collections/singleton.js
+++ b/collections/singleton.js
@@ -1,9 +1,9 @@
/** Site-wide, global information, including denormalized data.
*
- * Structure:
+ * Structure:
*
* {
- * postsCount: , // Petition count
+ * petitionsCount: , // Petition count
* minimumThreshold: , // Current minimum threshold
* threshold_updated_at: // Last datetime threshold changed
* }
@@ -29,5 +29,49 @@ Meteor.methods({
Singleton.update({}, {$set: { minimumThreshold: thresholdInt,
threshold_updated_at: new Date().getTime()}});
+ },
+ 'toggleModeration': function(){
+ var user = Meteor.user()
+ var current = Singleton.findOne().moderation;
+ if (!Roles.userIsInRole(user, ['admin']))
+ throw new Meteor.Error(403, "You are not authorized to change the moderation.");
+ if(current){
+ Singleton.update({}, {$set: { moderation: false}});
+ }else{
+ Singleton.update({}, {$set: { moderation: true}});
+ }
+ },
+ 'toggleParallax': function(){
+ var user = Meteor.user()
+ var current = Singleton.findOne().parallax;
+ if (!Roles.userIsInRole(user, ['admin']))
+ throw new Meteor.Error(403, "You are not authorized to change the parallax setting.");
+ if(current){
+ Singleton.update({}, {$set: { parallax: false}});
+ }else{
+ Singleton.update({}, {$set: { parallax: true}});
+ }
+ },
+ 'togglePetitionHistoryDisplay': function(){
+ var user = Meteor.user()
+ var current = Singleton.findOne().petitionHistoryDisplay;
+ if (!Roles.userIsInRole(user, ['admin']))
+ throw new Meteor.Error(403, "You are not authorized to change the petition history display setting.");
+ if(current){
+ Singleton.update({}, {$set: { petitionHistoryDisplay: false}});
+ }else{
+ Singleton.update({}, {$set: { petitionHistoryDisplay: true}});
+ }
+ },
+ 'toggleUpdateAuthorDisplay': function(){
+ var user = Meteor.user()
+ var current = Singleton.findOne().updateAuthorDisplay;
+ if (!Roles.userIsInRole(user, ['admin']))
+ throw new Meteor.Error(403, "You are not authorized to change the update author display setting.");
+ if(current){
+ Singleton.update({}, {$set: { updateAuthorDisplay: false}});
+ }else{
+ Singleton.update({}, {$set: { updateAuthorDisplay: true}});
+ }
}
-});
\ No newline at end of file
+});
diff --git a/collections/updates.js b/collections/updates.js
old mode 100644
new mode 100755
index 00f9afd..0870ed8
--- a/collections/updates.js
+++ b/collections/updates.js
@@ -11,19 +11,16 @@
Updates = new Meteor.Collection('updates');
-var validateUpdate = function (updateAttrs, post) {
+var validateUpdate = function (updateAttrs, petition) {
if (!updateAttrs.title || updateAttrs.title.length > 80)
throw new Meteor.Error(422, "Title is longer than 80 characters or not present.");
- if (!updateAttrs.description || updateAttrs.description.length > 4000)
- throw new Meteor.Error(422, "Description is longer than 4000 characters or not present.");
+ if (!updateAttrs.description)
+ throw new Meteor.Error(422, "Description is not present.");
- if (!updateAttrs.postId)
- throw new Meteor.Error(422, "The title's postId is missing.");
-
- if (post.status == "responded")
- throw new Meteor.Error(422, "Updates can't be added to petitions with responses.");
+ if (!updateAttrs.petitionId)
+ throw new Meteor.Error(422, "The title's petitionId is missing.");
};
@@ -35,35 +32,11 @@ Meteor.methods({
if (!Roles.userIsInRole(user, ['admin', 'moderator']))
throw new Meteor.Error(403, "You are not authorized to create updates.");
- var post = Posts.findOne(updateAttrs.postId);
- validateUpdate(updateAttrs, post);
-
- var existingUpdates = Updates.find({postId: updateAttrs.postId});
-
- if (_.isEmpty(post.response)) {
+ var petition = Petitions.findOne(updateAttrs.petitionId);
+ validateUpdate(updateAttrs, petition);
- Posts.update(updateAttrs.postId, {$set: {status: "waiting-for-reply"}});
-
- var users = Meteor.users.find({$and: [{'notify.updates': true},
- {_id: {$in: post.upvoters}}]},
- {fields: {username: 1}});
-
- var emails = users.map(function (user) { return user.username + "@rit.edu"; });
-
- Email.send({
- bcc: emails,
- to: "sgnoreply@rit.edu",
- from: "sgnoreply@rit.edu",
- subject: "PawPrints - A petition you signed has a status update",
- text: "Hello, \n\n" +
- "Petition \"" + post.title + "\" by " + post.author + " has a status update: \n\n" +
- Meteor.settings.public.root_url + "/petitions/" + post._id +
- "\n\nThanks, \nRIT Student Government"
- });
-
- }
-
- var update = _.extend(_.pick(updateAttrs, 'title', 'description', 'postId'), {
+ var existingUpdates = Updates.find({petitionId: updateAttrs.petitionId});
+ var update = _.extend(_.pick(updateAttrs, 'title', 'description', 'petitionId'), {
created_at: new Date().getTime(),
updated_at: new Date().getTime(),
author: user.profile.name,
@@ -71,7 +44,24 @@ Meteor.methods({
});
var updateId = Updates.insert(update);
+ if(Meteor.isServer){
+ if (_.isEmpty(petition.response)) {
+ Petitions.update(updateAttrs.petitionId, {$set: {status: "waiting-for-reply"}});
+ }
+
+ var users = Meteor.users.find({$and: [{'notify.updates': true},
+ {_id: {$in: petition.subscribers}}]},
+ {fields: {username: 1}});
+ var emails = users.map(function (user) { return user.username + Meteor.settings.MAIL.default_domain; });
+
+ Mailer.sendTemplatedEmail("petition_status_update", {
+ bcc: emails
+ }, {
+ petition: petition
+ });
+ }
+ return updateId;
},
'editUpdate': function (updateAttrs) {
@@ -80,17 +70,17 @@ Meteor.methods({
if (!Roles.userIsInRole(user, ['admin', 'moderator']))
throw new Meteor.Error(403, "You are not authorized to edit updates.");
- var post = Posts.findOne(updateAttrs.postId);
- validateUpdate(updateAttrs, post);
+ var petition = Petitions.findOne(updateAttrs.petitionId);
+ validateUpdate(updateAttrs, petition);
- var update = _.extend(_.pick(updateAttrs, 'title', 'description', 'postId'), {
+ var update = _.extend(_.pick(updateAttrs, 'title', 'description', 'petitionId'), {
updated_at: new Date().getTime(),
author: user.profile.name,
userId: user._id
});
Updates.update(updateAttrs._id, {$set: update });
-
+
},
'deleteUpdate': function (updateAttrs) {
diff --git a/collections/users.js b/collections/users.js
old mode 100644
new mode 100755
index a9f0022..70d79a7
--- a/collections/users.js
+++ b/collections/users.js
@@ -1,5 +1,14 @@
Meteor.methods({
editUserRole: function(username, role, actionType) {
+ var editingLocked;
+ try {
+ editingLocked = Meteor.settings.public.ui.roles_locked;
+ }catch(Exception){
+ editingLocked = false;
+ }
+
+ if(editingLocked)
+ throw new Meteor.Error(403, "Roles are not editable.");
var loggedInUser = Meteor.user();
var action;
@@ -29,5 +38,38 @@ Meteor.methods({
}
Meteor.users.update(user._id, {$set: {notify: notifyAttributes}});
+ },
+ editProfile: function(profilePrefs) {
+ var editingLocked;
+ try {
+ editingLocked = Meteor.settings.public.ui.initials_locked;
+ }catch(Exception){
+ editingLocked = false;
+ }
+
+ //Right now the only thing you can edit on your profile is your initials.
+ if(editingLocked)
+ throw new Meteor.Error(403, "Initials are not editable.");
+
+ var user = Meteor.user();
+
+ if (!user)
+ throw new Meteor.Error(401, "You need to login to update your profile.");
+
+ var initials = profilePrefs.initials;
+
+ if (typeof initials != "string") {
+ throw new Meteor.Error(422, 'Profile preferences could not be saved.');
+ }
+
+ if (initials.trim().length < 1) {
+ throw new Meteor.Error(422, 'Profile initials are too short.');
+ }
+
+ if (initials.trim().length > 5) {
+ throw new Meteor.Error(422, 'Profile initials are too long.');
+ }
+
+ Meteor.users.update(user._id, {$set: {'profile.initials': initials }});
}
});
\ No newline at end of file
diff --git a/lib/infinite_scroll.js b/lib/infinite_scroll.js
index a9d81ea..3acc2c6 100644
--- a/lib/infinite_scroll.js
+++ b/lib/infinite_scroll.js
@@ -17,7 +17,7 @@ infiniteScroll = function() {
didScroll = false;
var atBottom = (windowDom.scrollTop() >= documentDom.height() - windowDom.height() - bufferBottom);
if (atBottom) {
- Session.set('postsLimit', Session.get('postsLimit') + 12);
+ Session.set('petitionsLimit', Session.get('petitionsLimit') + 12);
}
}
}, 500);
diff --git a/lib/seo.js b/lib/main.seo.js
old mode 100644
new mode 100755
similarity index 76%
rename from lib/seo.js
rename to lib/main.seo.js
index 3fbae67..1842fb4
--- a/lib/seo.js
+++ b/lib/main.seo.js
@@ -1,4 +1,4 @@
-var seoData = {
+var seoData = _.extend({
admin: {
title: "Admin",
description: "Change administrative settings."
@@ -7,31 +7,31 @@ var seoData = {
title: "Profile",
description: "Edit profile information."
},
- postSubmit: {
+ petitionSubmit: {
title: "Submit Petition",
description: "Submit a petition to PawPrints."
},
- postsInProgress: {
- title: "In Progress Petitions",
+ petitionsInProgress: {
+ title: "Recognized Petitions",
description: "View petitions being actively worked on by Student Government."
},
- postsList: {
+ petitionsList: {
title: "Petitions",
description: "PawPrints is a place for sparking change at RIT. Share ideas with the RIT community and influence decision making."
},
- postsResponses: {
- title: "Responsed Petitions",
+ petitionsResponses: {
+ title: "Petition Responses",
description: "View petitions that have received responses from RIT Student Government and Administration."
},
- postsTagList: {
+ petitionsTagList: {
title: "Tagged Petitions",
description: "View tagged petitions on PawPrints."
},
- postPage: {
+ petitionPage: {
title: "View Petition",
description: "View petition on PawPrints."
},
- postEdit: {
+ petitionEdit: {
title: "Edit Petition",
description: "Edit petition."
},
@@ -39,6 +39,10 @@ var seoData = {
title: "About PawPrints",
description: "Learn more about the history of PawPrints, its goal and vision."
},
+ search: {
+ title: "Search",
+ description: "Search petitions"
+ },
api: {
title: "API Access",
description: "Student Government provides a read-only JSON REST API for retreiving petition information."
@@ -58,8 +62,16 @@ var seoData = {
pageNotFound: {
title: "Page Not Found",
description: "The page you requested is not found."
+ },
+ moderate: {
+ title: "Moderate",
+ description: "A page where petitions can be moderated."
+ },
+ pendingPetition:{
+ title: "Pending",
+ description: "Your petition is pending approval!."
}
-};
+}, _.get(Meteor, 'settings.public.seo_overrides', {}));
Router.configure({
onBeforeAction: function () {
@@ -80,13 +92,13 @@ setSEO = function (data) {
SEO.set({
title: data.title,
meta: {
- 'description': data.description
+ 'description': data.description.replace(/<(?:.|\n)*?>/gm, '')
},
og: {
'title': data.title,
- 'description': data.description,
+ 'description': data.description.replace(/<(?:.|\n)*?>/gm, ''),
'image': image_url
}
});
}
-};
\ No newline at end of file
+};
diff --git a/lib/router.js b/lib/router.js
old mode 100644
new mode 100755
index 2359998..eef7529
--- a/lib/router.js
+++ b/lib/router.js
@@ -17,44 +17,70 @@ Router.configure({
}
});
-var postSort = function() {
+var petitionSort = function() {
var sort = {};
- sort[Session.get('postOrder')] = -1;
- sort.submitted = -1;
+ sort[Session.get('petitionOrder')] = -1;
return {
- posts: Posts.find({}, {sort: sort}).fetch(),
- postOrder: Session.get('postOrder'),
- tag: Session.get('tagName')
+ petitions: Petitions.find({}, {sort: sort}).fetch(),
+ petitionOrder: Session.get('petitionOrder'),
+ tag: Session.get('tagName'),
+ tags: Tags.find().fetch()
};
};
-PostsListController = RouteController.extend({
+PetitionsListController = RouteController.extend({
+ onRun : function(){
+ Session.set('activeTag', null);
+ this.next();
+ },
onBeforeAction: function() {
- Meteor.subscribe('posts', Session.get('postsLimit'), Session.get('postOrder'), this.params.tagName);
+ Meteor.subscribe('petitions', Session.get('petitionsLimit'), Session.get('petitionOrder'), Session.get('activeTag'), Session.get('showSigned'), Session.get('showCreated'));
+ Meteor.subscribe('petitionsSignedByMe');
infiniteScroll();
this.next();
},
- data: postSort
+ data: petitionSort
});
-PostsWithResponsesController = RouteController.extend({
+PetitionsWithResponsesController = RouteController.extend({
+ onRun : function(){
+ Session.set('activeTag', null);
+ this.next();
+ },
onBeforeAction: function () {
- Meteor.subscribe('postsWithResponses', Session.get('postsLimit'), Session.get('postOrder'));
+ Meteor.subscribe('petitionsWithResponses', Session.get('petitionsLimit'), Session.get('petitionOrder'), Session.get('activeTag'), Session.get('showSigned'), Session.get('showCreated'));
infiniteScroll();
this.next();
},
- data: postSort
+ data: petitionSort
});
-PostsInProgressController = RouteController.extend({
+PetitionsInProgressController = RouteController.extend({
+ onRun : function(){
+ Session.set('activeTag', null);
+ this.next();
+ },
onBeforeAction: function () {
- Meteor.subscribe('postsInProgress', Session.get('postsLimit'), Session.get('postOrder'));
+ Meteor.subscribe('petitionsInProgress', Session.get('petitionsLimit'), Session.get('petitionOrder'), Session.get('activeTag'), Session.get('showSigned'), Session.get('showCreated'));
infiniteScroll();
this.next();
},
- data: postSort
+ data: petitionSort
});
+PendingPetitionsController = RouteController.extend({
+ onRun : function(){
+ Session.set('activeTag', null);
+ this.next();
+ },
+ onBeforeAction: function () {
+ Meteor.subscribe('pendingPetitions');
+ infiniteScroll();
+ this.next();
+ },
+ data: petitionSort
+})
+
Router.map(function() {
// Privileged Routes
@@ -80,7 +106,7 @@ Router.map(function() {
this.route('userEdit', {
path: '/users/edit',
waitOn: function() {
- return [Meteor.subscribe('apiKeys')];
+ return [Meteor.subscribe('apiKeys')];
},
data: function() {
return {
@@ -90,93 +116,86 @@ Router.map(function() {
}
});
- // Post Routes
+ // Petition Routes
- this.route('postSubmit', {
+ this.route('petitionSubmit', {
path: '/petitions/create'
});
+ this.route('hello', {
+ path: '/hello',
+ template: 'helloWorld'
+ });
- this.route('postsList', {
+
+ this.route('petitionsList', {
path: '/petitions/list',
- template: 'postsList',
- controller: PostsListController
+ template: 'petitionsList',
+ controller: PetitionsListController
});
- this.route('postsInProgress', {
+ this.route('petitionsInProgress', {
path: '/petitions/in-progress',
- template: 'postsInProgressList',
- controller: PostsInProgressController
+ template: 'petitionsInProgressList',
+ controller: PetitionsInProgressController
});
- this.route('postsResponses', {
+ this.route('petitionsResponses', {
path: '/petitions/responses',
- template: 'postsWithResponsesList',
- controller: PostsWithResponsesController
+ template: 'petitionsWithResponsesList',
+ controller: PetitionsWithResponsesController
});
- this.route('postsTagList', {
- path: '/petitions/tagged/:tagName',
- template: 'postsTagList',
- onBeforeAction: function () {
- infiniteScroll();
- Session.set('tagName', this.params.tagName);
- Meteor.subscribe('posts', Session.get('postsLimit'), Session.get('postOrder'), Session.get('tagName'));
- this.next();
- },
- data: postSort,
- onAfterAction: function() {
- setSEO({title: Session.get('tagName') + " Petitions",
- description: "View " + Session.get('tagName') + " petitions on PawPrints."});
- return;
- }
+ this.route('moderate', {
+ path: '/moderate',
+ template: 'moderatePage',
+ controller: PendingPetitionsController
});
- this.route('postPage', {
+ this.route('petitionPage', {
path: '/petitions/:_id',
- template: 'postPage',
+ template: 'petitionPage',
waitOn: function() {
- return [Meteor.subscribe('singlePost', this.params._id),
- Meteor.subscribe('signers', this.params._id),
+ return [Meteor.subscribe('singlePetition', this.params._id),
+ Meteor.subscribe('signatories', this.params._id),
Meteor.subscribe('updates', this.params._id)];
},
data: function() {
- var post = Posts.findOne(this.params._id);
- if (this.ready() && !post)
+ var petition = Petitions.findOne(this.params._id);
+ if (this.ready() && !petition)
this.render('pageNotFound');
- else
+ else
return {
- post: Posts.findOne(),
+ petition: Petitions.findOne(),
updates: Updates.find({}, {sort: {created_at: 1}}).fetch(),
- scores: Scores.find().fetch(),
url: window.location.href
};
},
onAfterAction: function() {
- if (this.data() && this.data().post)
- setSEO({title: this.data().post.title, description: this.data().post.description});
- return;
+ if (this.data() && this.data().petition)
+ setSEO({title: this.data().petition.title, description: this.data().petition.description});
+ return;
}
});
- this.route('postEdit', {
+ this.route('petitionEdit', {
path: '/petitions/:_id/edit',
- template: 'postEdit',
+ template: 'petitionEdit',
onBeforeAction: function () {
if (Meteor.user() && !_.contains(Meteor.user().roles, "moderator") && !_.contains(Meteor.user().roles, "admin"))
this.render('pageNotFound');
this.next();
},
waitOn: function () {
- return [Meteor.subscribe('singlePost', this.params._id),
+ return [Meteor.subscribe('singlePetition', this.params._id),
Meteor.subscribe('updates', this.params._id)];
},
data: function() {
- var post = Posts.findOne(this.params._id);
- if (this.ready() && !post)
+ var petition = Petitions.findOne(this.params._id);
+ if (this.ready() && !petition)
this.render('pageNotFound');
else
return {
- post: Posts.findOne(this.params._id),
+ petition: Petitions.findOne(this.params._id),
updates: Updates.find({}, {sort: {created_at: 1}}).fetch(),
newUpdate: {},
user: Meteor.user()
@@ -196,6 +215,21 @@ Router.map(function() {
}
});
+
+ this.route('search', {
+ path: '/search',
+ template: 'search',
+ waitOn: function() {
+ return [Meteor.subscribe('petitionsSearch')]
+ }
+ });
+
+ this.route('pendingPetition', {
+ path: '/pendingPetition',
+ template: 'pendingPetition'
+
+ });
+
this.route('api', {
path: '/api-access',
template: 'aboutTemplate',
@@ -231,7 +265,7 @@ Router.map(function() {
this.route('index', {
path: '/',
template: 'index',
- controller: PostsListController
+ controller: PetitionsListController
});
this.route('pageNotFound', {
@@ -261,4 +295,4 @@ var clearLoginMsg = function() {
Router.onBeforeAction('loading');
Router.onAfterAction(clearLoginMsg);
-Router.onBeforeAction(requireLogin, {only: ['admin', 'postSubmit', 'userEdit', 'postEdit']});
+Router.onBeforeAction(requireLogin, {only: ['admin', 'petitionSubmit', 'userEdit', 'petitionEdit']});
diff --git a/lib/underscore-mixins.js b/lib/underscore-mixins.js
new file mode 100755
index 0000000..486eed4
--- /dev/null
+++ b/lib/underscore-mixins.js
@@ -0,0 +1,286 @@
+/**
+ * lodash 3.0.0 (Custom Build)
+ * Build: `lodash modularize exports="npm" -o ./`
+ * Copyright 2012-2016 The Dojo Foundation
+ * Based on Underscore.js 1.8.3
+ * Copyright 2009-2016 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+ * Available under MIT license
+ */
+
+/** Used to determine if values are of the language type `Object`. */
+var objectTypes = {
+ 'function': true,
+ 'object': true
+};
+
+/** Detect free variable `exports`. */
+var freeExports = (objectTypes[typeof exports] && exports && !exports.nodeType) ? exports : null;
+
+/** Detect free variable `module`. */
+var freeModule = (objectTypes[typeof module] && module && !module.nodeType) ? module : null;
+
+/** Detect free variable `global` from Node.js. */
+var freeGlobal = checkGlobal(freeExports && freeModule && typeof global == 'object' && global);
+
+/** Detect free variable `self`. */
+var freeSelf = checkGlobal(objectTypes[typeof self] && self);
+
+/** Detect free variable `window`. */
+var freeWindow = checkGlobal(objectTypes[typeof window] && window);
+
+/** Detect `this` as the global object. */
+var thisGlobal = checkGlobal(objectTypes[typeof this] && this);
+
+/**
+ * Used as a reference to the global object.
+ *
+ * The `this` value is used if it's the global object to avoid Greasemonkey's
+ * restricted `window` object, otherwise the `window` object is used.
+ */
+var root = freeGlobal || ((freeWindow !== (thisGlobal && thisGlobal.window)) && freeWindow) || freeSelf || thisGlobal || Function('return this')();
+
+/**
+ * Checks if `value` is a global object.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @returns {null|Object} Returns `value` if it's a global object, else `null`.
+ */
+function checkGlobal(value) {
+ return (value && value.Object === Object) ? value : null;
+}
+
+/** Used as references for various `Number` constants. */
+var INFINITY = 1 / 0;
+
+/** `Object#toString` result references. */
+var symbolTag = '[object Symbol]';
+
+/** Used to match property names within property paths. */
+var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,
+ reIsPlainProp = /^\w*$/,
+ rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]/g;
+
+/** Used to match backslashes in property paths. */
+var reEscapeChar = /\\(\\)?/g;
+
+/** Used for built-in method references. */
+var objectProto = Object.prototype;
+
+/**
+ * Used to resolve the [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring)
+ * of values.
+ */
+var objectToString = objectProto.toString;
+
+/** Built-in value references. */
+var Symbol = root.Symbol;
+
+/** Used to convert symbols to primitives and strings. */
+var symbolProto = Symbol ? Symbol.prototype : undefined,
+ symbolToString = Symbol ? symbolProto.toString : undefined;
+
+/**
+ * The base implementation of `_.get` without support for default values.
+ *
+ * @private
+ * @param {Object} object The object to query.
+ * @param {Array|string} path The path of the property to get.
+ * @returns {*} Returns the resolved value.
+ */
+function baseGet(object, path) {
+ path = isKey(path, object) ? [path + ''] : baseToPath(path);
+
+ var index = 0,
+ length = path.length;
+
+ while (object != null && index < length) {
+ object = object[path[index++]];
+ }
+ return (index && index == length) ? object : undefined;
+}
+
+/**
+ * The base implementation of `_.toPath` which only converts `value` to a
+ * path if it's not one.
+ *
+ * @private
+ * @param {*} value The value to process.
+ * @returns {Array} Returns the property path array.
+ */
+function baseToPath(value) {
+ return isArray(value) ? value : stringToPath(value);
+}
+
+/**
+ * Checks if `value` is a property name and not a property path.
+ *
+ * @private
+ * @param {*} value The value to check.
+ * @param {Object} [object] The object to query keys on.
+ * @returns {boolean} Returns `true` if `value` is a property name, else `false`.
+ */
+function isKey(value, object) {
+ if (typeof value == 'number') {
+ return true;
+ }
+ return !isArray(value) &&
+ (reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||
+ (object != null && value in Object(object)));
+}
+
+/**
+ * Converts `string` to a property path array.
+ *
+ * @private
+ * @param {string} string The string to convert.
+ * @returns {Array} Returns the property path array.
+ */
+function stringToPath(string) {
+ var result = [];
+ toString(string).replace(rePropName, function(match, number, quote, string) {
+ result.push(quote ? string.replace(reEscapeChar, '$1') : (number || match));
+ });
+ return result;
+}
+
+/**
+ * Checks if `value` is classified as an `Array` object.
+ *
+ * @static
+ * @memberOf _
+ * @type Function
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+ * @example
+ *
+ * _.isArray([1, 2, 3]);
+ * // => true
+ *
+ * _.isArray(document.body.children);
+ * // => false
+ *
+ * _.isArray('abc');
+ * // => false
+ *
+ * _.isArray(_.noop);
+ * // => false
+ */
+var isArray = Array.isArray;
+
+/**
+ * Checks if `value` is object-like. A value is object-like if it's not `null`
+ * and has a `typeof` result of "object".
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
+ * @example
+ *
+ * _.isObjectLike({});
+ * // => true
+ *
+ * _.isObjectLike([1, 2, 3]);
+ * // => true
+ *
+ * _.isObjectLike(_.noop);
+ * // => false
+ *
+ * _.isObjectLike(null);
+ * // => false
+ */
+function isObjectLike(value) {
+ return !!value && typeof value == 'object';
+}
+
+/**
+ * Checks if `value` is classified as a `Symbol` primitive or object.
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to check.
+ * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.
+ * @example
+ *
+ * _.isSymbol(Symbol.iterator);
+ * // => true
+ *
+ * _.isSymbol('abc');
+ * // => false
+ */
+function isSymbol(value) {
+ return typeof value == 'symbol' ||
+ (isObjectLike(value) && objectToString.call(value) == symbolTag);
+}
+
+/**
+ * Converts `value` to a string if it's not one. An empty string is returned
+ * for `null` and `undefined` values. The sign of `-0` is preserved.
+ *
+ * @static
+ * @memberOf _
+ * @category Lang
+ * @param {*} value The value to process.
+ * @returns {string} Returns the string.
+ * @example
+ *
+ * _.toString(null);
+ * // => ''
+ *
+ * _.toString(-0);
+ * // => '-0'
+ *
+ * _.toString([1, 2, 3]);
+ * // => '1,2,3'
+ */
+function toString(value) {
+ // Exit early for strings to avoid a performance hit in some environments.
+ if (typeof value == 'string') {
+ return value;
+ }
+ if (value == null) {
+ return '';
+ }
+ if (isSymbol(value)) {
+ return Symbol ? symbolToString.call(value) : '';
+ }
+ var result = (value + '');
+ return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;
+}
+
+/**
+ * Gets the value at `path` of `object`. If the resolved value is
+ * `undefined` the `defaultValue` is used in its place.
+ *
+ * @static
+ * @memberOf _
+ * @category Object
+ * @param {Object} object The object to query.
+ * @param {Array|string} path The path of the property to get.
+ * @param {*} [defaultValue] The value returned if the resolved value is `undefined`.
+ * @returns {*} Returns the resolved value.
+ * @example
+ *
+ * var object = { 'a': [{ 'b': { 'c': 3 } }] };
+ *
+ * _.get(object, 'a[0].b.c');
+ * // => 3
+ *
+ * _.get(object, ['a', '0', 'b', 'c']);
+ * // => 3
+ *
+ * _.get(object, 'a.b.c', 'default');
+ * // => 'default'
+ */
+function get(object, path, defaultValue) {
+ var result = object == null ? undefined : baseGet(object, path);
+ return result === undefined ? defaultValue : result;
+}
+
+_.mixin({
+ get: get
+})
\ No newline at end of file
diff --git a/packages/npm-container/index.js b/packages/npm-container/index.js
index 43c108b..c3fc862 100644
--- a/packages/npm-container/index.js
+++ b/packages/npm-container/index.js
@@ -1,9 +1,9 @@
- Meteor.npmRequire = function(moduleName) { // 79
- var module = Npm.require(moduleName); // 80
- return module; // 81
- }; // 82
- // 83
- Meteor.require = function(moduleName) { // 84
- console.warn('Meteor.require is deprecated. Please use Meteor.npmRequire instead!'); // 85
- return Meteor.npmRequire(moduleName); // 86
- }; // 87
\ No newline at end of file
+Meteor.npmRequire = function(moduleName) {
+ var module = Npm.require(moduleName);
+ return module;
+};
+
+Meteor.require = function(moduleName) {
+ console.warn('Meteor.require is deprecated. Please use Meteor.npmRequire instead!');
+ return Meteor.npmRequire(moduleName);
+};
\ No newline at end of file
diff --git a/packages/npm-container/package.js b/packages/npm-container/package.js
index 4a766fa..9fab3ce 100644
--- a/packages/npm-container/package.js
+++ b/packages/npm-container/package.js
@@ -1,21 +1,30 @@
- var path = Npm.require('path'); // 91
- var fs = Npm.require('fs'); // 92
- // 93
- Package.describe({ // 94
- summary: 'Contains all your npm dependencies', // 95
- version: '1.0.0', // 96
- name: 'npm-container' // 97
- }); // 98
- // 99
- var packagesJsonFile = path.resolve('./packages.json'); // 100
- try { // 101
- var fileContent = fs.readFileSync(packagesJsonFile); // 102
- var packages = JSON.parse(fileContent.toString()); // 103
- Npm.depends(packages); // 104
- } catch(ex) { // 105
- console.error('ERROR: packages.json parsing error [ ' + ex.message + ' ]'); // 106
- } // 107
- // 108
- Package.onUse(function(api) { // 109
- api.add_files(['index.js', '../../packages.json'], 'server'); // 110
- }); // 111
\ No newline at end of file
+var path = Npm.require('path');
+var fs = Npm.require('fs');
+
+Package.describe({
+ summary: 'Contains all your npm dependencies',
+ version: '1.2.0',
+ name: 'npm-container'
+});
+
+var packagesJsonFile = path.resolve('./packages.json');
+try {
+ var fileContent = fs.readFileSync(packagesJsonFile);
+ var packages = JSON.parse(fileContent.toString());
+ Npm.depends(packages);
+} catch (ex) {
+ console.error('ERROR: packages.json parsing error [ ' + ex.message + ' ]');
+}
+
+// Adding the app's packages.json as a used file for this package will get
+// Meteor to watch it and reload this package when it changes
+Package.onUse(function(api) {
+ api.addFiles('index.js', 'server');
+ if (api.addAssets) {
+ api.addAssets('../../packages.json', 'server');
+ } else {
+ api.addFiles('../../packages.json', 'server', {
+ isAsset: true
+ });
+ }
+});
\ No newline at end of file
diff --git a/private/email_templates/petition_approved.html b/private/email_templates/petition_approved.html
new file mode 100755
index 0000000..3a44932
--- /dev/null
+++ b/private/email_templates/petition_approved.html
@@ -0,0 +1,8 @@
+Hello,
+
+Petition {{ petition.title }} by {{ petition.author }} has been approved:
+{{ settings.public.root_url }}/petitions/{{ petition._id }}
+
+
+Thanks,
+RIT Student Government
\ No newline at end of file
diff --git a/private/email_templates/petition_rejected.html b/private/email_templates/petition_rejected.html
new file mode 100755
index 0000000..a6a0517
--- /dev/null
+++ b/private/email_templates/petition_rejected.html
@@ -0,0 +1,9 @@
+Hello,
+
+Petition {{ petition.title }} by {{ petition.author }} has been rejected for the following reasons:
+
+{{ message }}
+
+
+Thanks,
+RIT Student Government"
\ No newline at end of file
diff --git a/private/email_templates/petition_response_received.html b/private/email_templates/petition_response_received.html
new file mode 100755
index 0000000..7cbf638
--- /dev/null
+++ b/private/email_templates/petition_response_received.html
@@ -0,0 +1,9 @@
+Hello,
+
+Petition {{ petition.title }} by {{ oldPetition.author }} has received a response:
+
+{{ settings.public.root_url }}/petitions/{{ oldPetition._id }}
+
+
+Thanks,
+RIT Student Government
diff --git a/private/email_templates/petition_status_update.html b/private/email_templates/petition_status_update.html
new file mode 100755
index 0000000..4300c6d
--- /dev/null
+++ b/private/email_templates/petition_status_update.html
@@ -0,0 +1,9 @@
+Hello,
+
+Petition {{ petition.title }} by {{ petition.author }} has a status update:
+
+{{ settings.public.root_url }}/petitions/{{ petition._id }}
+
+
+Thanks,
+RIT Student Government"
\ No newline at end of file
diff --git a/private/email_templates/petition_threshold_reached.html b/private/email_templates/petition_threshold_reached.html
new file mode 100755
index 0000000..24cbe4c
--- /dev/null
+++ b/private/email_templates/petition_threshold_reached.html
@@ -0,0 +1,6 @@
+Petition {{ petition.title }} by {{ petition.author }} has reached its minimum signature goal:
+{{ settings.public.root_url }}/petitions/{{ petition._id }}
+
+
+Thanks,
+RIT Student Government
\ No newline at end of file
diff --git a/private/email_templates/report_petition.html b/private/email_templates/report_petition.html
new file mode 100755
index 0000000..aad667a
--- /dev/null
+++ b/private/email_templates/report_petition.html
@@ -0,0 +1 @@
+Petition {{ petition.title }} by {{ petition.author }} was reported as {{ reason }}.
\ No newline at end of file
diff --git a/public/carousel_1.png b/public/carousel_1.png
index ace8366..aa13f43 100644
Binary files a/public/carousel_1.png and b/public/carousel_1.png differ
diff --git a/public/carousel_2.png b/public/carousel_2.png
index 83497c7..675ecbc 100644
Binary files a/public/carousel_2.png and b/public/carousel_2.png differ
diff --git a/public/carousel_3.png b/public/carousel_3.png
index 6c1ac53..7ac6eca 100644
Binary files a/public/carousel_3.png and b/public/carousel_3.png differ
diff --git a/server/cron.js b/server/cron.js
deleted file mode 100644
index 2025907..0000000
--- a/server/cron.js
+++ /dev/null
@@ -1,44 +0,0 @@
-SyncedCron.add({
- name: 'Cache daily signature counts.',
- schedule: function (parser) {
- return parser.recur().on('00:00:00').time();
- },
- job: function() {
-
- // @petitions petitions with no response submitted in the last year
- var petitions = Posts.find({
- response: { $exists: false },
- submitted: { $gte: moment().subtract(1, 'year').valueOf() }},
- {fields: {votes: 1, submitted: 1, score: 1}}).fetch();
-
- petitions.forEach(function (petition) {
-
- /**
- * @daysOld number of days since petition created (floored)
- * @newScore calculated use time decay formula
- * @newChange day-over-day change in score
- */
- var daysOld = moment().diff(petition.submitted, 'days');
- var oldScore = petition.score || 0;
- var newScore = petition.votes / Math.pow(daysOld + 1, 1.5);
- var newChange = -1 * (oldScore - newScore);
-
- Posts.update(
- petition._id,
- {$set: {score: newScore, change: newChange}}
- );
-
- Scores.insert({
- postId: petition._id,
- created_at: new Date().getTime(),
- score: newScore,
- votes: petition.votes
- });
- });
-
- return "Job done.";
-
- }
-});
-
-SyncedCron.start();
\ No newline at end of file
diff --git a/server/fixtures.js b/server/fixtures.js
index 16ddadb..6e81cb2 100644
--- a/server/fixtures.js
+++ b/server/fixtures.js
@@ -1,4 +1,4 @@
-// Initialize Post Count singleton
+// Initialize Petition Count singleton
if (Singleton.find().count() === 0) {
Singleton.insert({
@@ -7,9 +7,9 @@ if (Singleton.find().count() === 0) {
});
}
-// Add test posts to non-production instances
+// Add test petitions to non-production instances
-if (Posts.find().count() === 0 && process.env.NODE_ENV != "production" ) {
+if (false){//Petitions.find().count() === 0 && process.env.NODE_ENV != "production" ) {
console.log("Adding test tags...");
@@ -18,7 +18,9 @@ if (Posts.find().count() === 0 && process.env.NODE_ENV != "production" ) {
var tagId_dining = Tags.insert({name: "Dining"});
var tagId_test = Tags.insert({name: "Test"});
- console.log("Adding test posts...");
+ console.log("Adding test petitions...");
+
+
var peteId = Meteor.users.insert({
username: 'pam3961',
@@ -34,11 +36,12 @@ if (Posts.find().count() === 0 && process.env.NODE_ENV != "production" ) {
response: true
}
});
+
var pete = Meteor.users.findOne(peteId);
-
- // Post with 7-day history
- var postId_seven_day = Posts.insert({
+ // Petition with 7-day history
+
+ var petitionId_seven_day = Petitions.insert({
userId: pete._id,
author: pete.profile.displayName,
submitted: moment().subtract(7, 'days').valueOf(),
@@ -49,59 +52,9 @@ if (Posts.find().count() === 0 && process.env.NODE_ENV != "production" ) {
minimumVotes: Singleton.findOne().minimumThreshold,
tag_ids: [tagId_housing]
});
+ // Petition with 3-day history
- Scores.insert({
- postId: postId_seven_day,
- created_at: moment().subtract(7, 'days').valueOf(),
- score: 1,
- votes: 1
- });
-
- Scores.insert({
- postId: postId_seven_day,
- created_at: moment().subtract(6, 'days').valueOf(),
- score: 10,
- votes: 10
- });
-
- Scores.insert({
- postId: postId_seven_day,
- created_at: moment().subtract(5, 'days').valueOf(),
- score: 22,
- votes: 22
- });
-
- Scores.insert({
- postId: postId_seven_day,
- created_at: moment().subtract(4, 'days').valueOf(),
- score: 30,
- votes: 30
- });
-
- Scores.insert({
- postId: postId_seven_day,
- created_at: moment().subtract(3, 'days').valueOf(),
- score: 44,
- votes: 44
- });
-
- Scores.insert({
- postId: postId_seven_day,
- created_at: moment().subtract(2, 'days').valueOf(),
- score: 47,
- votes: 47
- });
-
- Scores.insert({
- postId: postId_seven_day,
- created_at: moment().subtract(1, 'day').valueOf(),
- score: 50,
- votes: 50
- });
-
- // Post with 3-day history
-
- var postId_three_day = Posts.insert({
+ var petitionId_three_day = Petitions.insert({
userId: pete._id,
author: pete.profile.displayName,
submitted: moment().subtract(3, 'days').valueOf(),
@@ -113,30 +66,9 @@ if (Posts.find().count() === 0 && process.env.NODE_ENV != "production" ) {
tag_ids: [tagId_technology]
});
- Scores.insert({
- postId: postId_three_day,
- created_at: moment().subtract(3, 'days').valueOf(),
- score: 1,
- votes: 1
- });
-
- Scores.insert({
- postId: postId_three_day,
- created_at: moment().subtract(2, 'days').valueOf(),
- score: 2,
- votes: 2
- });
-
- Scores.insert({
- postId: postId_three_day,
- created_at: moment().subtract(1, 'day').valueOf(),
- score: 4,
- votes: 4
- });
-
- // Post with 0-day history
+ // Petition with 0-day history
- var postId_no_history = Posts.insert({
+ var petitionId_no_history = Petitions.insert({
userId: pete._id,
author: pete.profile.displayName,
submitted: new Date().getTime(),
@@ -148,9 +80,9 @@ if (Posts.find().count() === 0 && process.env.NODE_ENV != "production" ) {
tag_ids: [tagId_test, tagId_dining]
});
- // Expired post
+ // Expired petition
- var postId_expired = Posts.insert({
+ var petitionId_expired = Petitions.insert({
userId: pete._id,
author: pete.profile.displayName,
submitted: moment().subtract(2, 'months').valueOf(),
@@ -162,14 +94,14 @@ if (Posts.find().count() === 0 && process.env.NODE_ENV != "production" ) {
tag_ids: [tagId_housing]
});
- // Extra posts for testing scalability and pagination
+ // Extra petitions for testing scalability and pagination
for (var i = 0; i < 10; i++) {
- Posts.insert({
+ Petitions.insert({
userId: pete._id,
author: pete.profile.displayName,
submitted: new Date().getTime(),
- title: 'Test post #' + i,
+ title: 'Test petition #' + i,
description: "Foo",
upvoters: [pete._id],
votes: 1,
@@ -201,6 +133,6 @@ if (Meteor.users.find({username: "sgweb"}).count() === 0) {
Roles.addUsersToRoles(sgweb, ['admin']);
}
-// Update post count
+// Update petition count
-Singleton.update({}, {$set: {postsCount: Posts.find().count()}});
\ No newline at end of file
+Singleton.update({}, {$set: {petitionsCount: Petitions.find().count()}});
diff --git a/server/ldap.js b/server/ldap.js
index f79cbb4..8b0d2dc 100644
--- a/server/ldap.js
+++ b/server/ldap.js
@@ -1,105 +1,168 @@
-var assert, ldap, Future, LDAP;
+var assert, ldap, LDAP;
ldap = Meteor.npmRequire('ldapjs');
assert = Npm.require('assert');
-Future = Npm.require('fibers/future');
LDAP = {};
-LDAP.searchOu = 'ou=People,dc=rit,dc=edu';
+LDAP.searchOu = Meteor.settings.LDAP.search_ou;
LDAP.searchQuery = function(user){
return {
- filter: "(uid=" + user + ")",
+ filter: "(" + Meteor.settings.LDAP.username_attribute + "=" + user + ")",
scope: 'sub'
};
};
LDAP.checkAccount = function(options) {
- var dn, future;
- LDAP.client = ldap.createClient({
- url: Meteor.settings.LDAP_URL,
- maxConnections: 2,
- bindDN: 'uid=' + options.username + ',ou=People,dc=rit,dc=edu',
- bindCredentials: options.password
- });
- options = options || {};
- dn = [];
- future = new Future();
- if (options.password.length === 0 || options.username.length === 0) {
- future['return'](void 8);
- return;
- }
- LDAP.client.search(LDAP.searchOu, LDAP.searchQuery(options.username), function(err, search) {
- if (err) {
- future['return'](false);
- return false;
- } else {
- search.on('searchEntry', function(entry) {
- dn.push(entry.objectName);
- LDAP.displayName = entry.object.displayName;
- LDAP.givenName = entry.object.givenName;
- LDAP.initials = entry.object.initials;
- LDAP.sn = entry.object.sn;
- LDAP.ou = entry.object.ou;
- return LDAP.displayName = entry.object.displayName;
- });
- search.on('error', function(err){
- throw new Meteor.Error(500, "LDAP server error");
- });
- return search.on('end', function() {
- if (dn.length === 0) {
- future['return'](false);
- return false;
- }
- return LDAP.client.bind(dn[0], options.password, function(err) {
- if (err) {
- future['return'](false);
- return false;
- }
- return LDAP.client.unbind(function(err) {
- assert.ifError(err);
- return future['return'](!err);
- });
- });
- });
- }
- });
- return future.wait();
-};
+ var dn;
+
+ LDAP.client = ldap.createClient({
+ url: Meteor.settings.LDAP.url,
+ maxConnections: 2,
+ bindDN: Meteor.settings.LDAP.bind_dn_prefix + options.username + ',' + Meteor.settings.LDAP.search_ou,
+ bindCredentials: options.password
+ });
+
+ options = options || {};
+ dn = [];
+
+ var exec = Meteor.sync(function(done){
+
+ // Check if user leaves any of the fields empty.
+ if(options.username.length === 0 || options.password.length === 0){
+
+ err = true;
+ done(err);
+ return exec;
+ }
+
+ // Attempt connection with LDAP.
+ LDAP.client.search(LDAP.searchOu, LDAP.searchQuery(options.username), function(err, search) {
+
+ // Check if authentication failed (Invalid credentials).
+ if(err){
+
+ done(err);
+ return exec;
+ }
+ else{
+
+ // Grab user info when authentication succeeds.
+ search.on('searchEntry', function(entry){
+
+ dn.push(entry.objectName);
+ LDAP.displayName = entry.object.displayName;
+ LDAP.givenName = entry.object.givenName;
+ LDAP.initials = entry.object.initials;
+ LDAP.sn = entry.object.sn;
+ LDAP.ou = entry.object.ou;
+ LDAP.memberOf = entry.object.memberOf;
+ LDAP.mail = entry.object.mail;
+ LDAP.displayName = entry.object.displayName;
+ done(null);
+ });
+
+ // Catch any user data error.
+ search.on('error', function(err){
+ throw new Meteor.Error(500, "LDAP server error");
+ done(err);
+ });
+
+ search.on('end', function() {
+
+ if(dn.length === 0){
+
+ err = true;
+ done(err);
+ }
+
+ LDAP.client.bind(dn[0], options.password, function(err) {
+
+ if (err) {
+ done(err);
+ }
+
+ LDAP.client.unbind(function(err) {
+ assert.ifError(err);
+ return(null);
+ });
+ });
+ });
+ }
+ });
+ });
+
+ return exec;
+}
+
+// Register loginHandler for LDAP.
Accounts.registerLoginHandler('ldap', function(loginRequest) {
- var user, userId, profile;
- if (LDAP.checkAccount(loginRequest)) {
- user = Meteor.users.findOne({
- username: loginRequest.username.trim().toLowerCase()
- });
- var name = (LDAP.givenName && LDAP.sn) ? LDAP.givenName + " " + LDAP.sn : null,
- profile = {
- displayName: LDAP.displayName || null,
- givenName: LDAP.givenName || null,
- initials: LDAP.initials || null,
- sn: LDAP.sn || null,
- name: name
+
+ var user, userId, profile;
+ var auth = LDAP.checkAccount(loginRequest);
+
+ if(!auth.error){
+
+ user = Meteor.users.findOne({
+ username: loginRequest.username.trim().toLowerCase()
+ });
+
+ var name = (LDAP.givenName && LDAP.sn) ? LDAP.givenName + " " + LDAP.sn : null;
+ var profile = {
+ displayName: LDAP.displayName || null,
+ givenName: LDAP.givenName || null,
+ initials: LDAP.initials || null,
+ sn: LDAP.sn || null,
+ name: name,
+ memberOf: LDAP.memberOf || null,
+ mail: LDAP.mail || null
};
- if (user) {
- userId = user._id;
- profile.displayName = profile.displayName || user.profile.displayName || null;
- profile.givenName = profile.givenName || user.profile.givenName || null;
- profile.initials = profile.initials || user.profile.initials || null;
- profile.sn = profile.sn || user.profile.sn || null;
- profile.name = profile.name || user.profile.name || null;
- Meteor.users.update(userId, {$set: {profile: profile}});
- } else {
- userId = Meteor.users.insert({
- username: loginRequest.username.trim().toLowerCase(),
- notify: {
- updates: true,
- response: true
- },
- profile: profile
- });
- }
- return {
- userId: userId
- };
- }
-});
+
+ if (user) {
+
+ userId = user._id;
+ profile.displayName = profile.displayName || user.profile.displayName || null;
+ profile.givenName = profile.givenName || user.profile.givenName || null;
+ profile.sn = profile.sn || user.profile.sn || null;
+ profile.name = profile.name || user.profile.name || null;
+ profile.email = profile.mail || user.profile.mail || null;
+ //When a user changes their initials or has it set for the first time, keep it.
+ profile.initials = user.profile.initials || profile.initials || null;
+
+ Meteor.users.update(userId, {$set: {profile: profile}});
+ }
+ else{
+
+ userId = Meteor.users.insert({
+ username: loginRequest.username.trim().toLowerCase(),
+ notify: {
+ updates: true,
+ response: true
+ },
+ profile: profile
+ });
+ }
+
+
+ if(Meteor.settings.LDAP.auto_group){
+ _.each(Meteor.settings.LDAP.auto_group, function(group, role) {
+
+ var autogroupAction = null;
+ if(profile.memberOf.indexOf(group) !== -1) {
+
+ autogroupAction = {$addToSet: {roles: role}};
+ }
+ else{
+
+ autogroupAction = {$pull: {roles: role}};
+ }
+ Meteor.users.update({_id: userId}, autogroupAction);
+ });
+ }
+
+ return {userId: userId};
+ }
+ else{
+ return {error: auth.error};
+ }
+});
\ No newline at end of file
diff --git a/server/mailer.js b/server/mailer.js
new file mode 100755
index 0000000..ff519aa
--- /dev/null
+++ b/server/mailer.js
@@ -0,0 +1,70 @@
+/*
+ * Uses the settings file to create default emails. Thus if we don't have at least the MAIL
+ * key set up this module is unusable and the site won't really work.
+ */
+if(!Meteor.settings.MAIL)
+ throw new Error("You must have a Meteor.settings.MAIL attribute, please refer to the settings.json.sample file.");
+
+//Pre-compile all the email templates at startup, that way we know immediately if there's an error.
+_.each(Meteor.settings.MAIL.templates, function(settings, templateKey) {
+ if(settings.template)
+ SSR.compileTemplate(templateKey, Assets.getText('email_templates/{0}.html'.format(settings.template)));
+});
+
+Mailer = {
+ sendTemplatedEmail: function(templateKey, overrides, context) {
+ //Setting up default variables available in the template, so you don't have to pass them in every time.
+ context = _.extend({}, {
+ settings: Meteor.settings
+ }, context || {});
+
+
+ //You can override the templateSettings.template variable to render a different template in a different package.
+ templateSettings = Meteor.settings.MAIL.templates[templateKey];
+ //If the template doesn't exist we shouldn't go any further.
+ if(!templateSettings)
+ throw new Meteor.Error(500, 'Tried to send an email with nonexistent template {0}.'.format(templateKey));
+
+ try {
+ /*
+ * The settings for an email trickle down from most to least important:
+ * 1. Override template settings
+ * 2. Overrides passed in
+ * 3. Individual template settings
+ * 4. Default template settings
+ * 5. Settings in this file (like text)
+ */
+ template = _.extend(
+ { text: SSR.render(templateKey, context) },
+ Meteor.settings.MAIL.template_defaults,
+ templateSettings,
+ overrides,
+ Meteor.settings.MAIL.template_overrides
+ );
+ }catch(e) {
+ //This means there was a general error either finding the template passed in or in rendering the compiled template.
+ throw new Meteor.Error(500, 'There was an error rendering the email template for {0} template. {1}'.format(templateKey, e.toString()));
+ }
+
+ //Apply string formatting to every string variable in the template, this allows you to have a variable subject for example.
+ template = _.object(_.map(template, function(value, key) {
+ var finalValue = value;
+ if(typeof value === "string") {
+ finalValue = value.format(context);
+ }
+
+ return [ key, finalValue ];
+ }));
+ try{
+ template.subject = template.subject.replace(/_/g, " "); //Convert _ to space to accommodate environment + JSON config (e.g. meteor unit file)
+}catch(e){
+ //ignore
+}
+ try {
+ Email.send(template);
+ }catch(e) {
+ //May not be the best error message, I'm not sure if including the error string could expose any sensitive information.
+ throw new Meteor.Error(500, 'There was a problem sending an email using the {0} template. {1}.'.format(templateKey, e.toString()));
+ }
+ }
+}
diff --git a/server/methods.js b/server/methods.js
old mode 100644
new mode 100755
index 562de72..9de0723
--- a/server/methods.js
+++ b/server/methods.js
@@ -1,5 +1,5 @@
Meteor.methods({
- report: function(postId, reason) {
+ report: function(petitionId, reason) {
var user = Meteor.user();
@@ -9,19 +9,52 @@ Meteor.methods({
if (!reason)
throw new Meteor.Error(401, "You need to specify a reason for reporting the petition.");
- var petition = Posts.findOne(postId);
+ var petition = Petitions.findOne(petitionId);
if (petition) {
this.unblock();
- Email.send({
- to: "sgweb@rit.edu",
- from: "sgnoreply@rit.edu",
- subject: "[petitions] Petition Reported",
- text: "Petition \"" + petition.title + "\" by " + petition.author + " was reported as " + reason + "."
+ Mailer.sendTemplatedEmail("report_petition", {}, {
+ petition: petition,
+ reason: reason
});
}
+ },
+ changePendingPetition: function(petitionId, approved, message){
+ var user = Meteor.user()
+ if (!Roles.userIsInRole(user, ['admin', 'moderator']))
+ throw new Meteor.Error(403, "You are not authorized to approve this petition.");
+
+ var petition = Petitions.findOne(petitionId);
+ Petitions.update(petitionId, {$set: {pending: false}});
+ var users = Meteor.users.find({_id: {$in: petition.upvoters}},
+ {fields: {username: 1}});
+ var emails = users.map(function (user) { return user.username + Meteor.settings.MAIL.default_domain; });
+ if(approved){
+ Petitions.update(petitionId, {$set: {published: true,
+ submitted: new Date().getTime()}});
+
+ Mailer.sendTemplatedEmail("petition_approved", {
+ bcc: emails
+ }, {
+ petition: petition
+ }
+ );
+
+ return "Petition Approved!";
+
+ }else{
+ Mailer.sendTemplatedEmail("petition_rejected", {
+ bcc: emails
+ },{
+ petition: petition,
+ message: message
+ }
+ );
+
+ return "Petition Rejected.";
+ }
}
-});
\ No newline at end of file
+});
diff --git a/server/migrations.js b/server/migrations.js
index d614db5..db5b7e5 100644
--- a/server/migrations.js
+++ b/server/migrations.js
@@ -22,11 +22,26 @@ Meteor.startup(function () {
sn: null,
name: null
}}});
- }
+ }
});
Migrations.insert({name: "ensureProfilePropertyExistsForUsers"});
}
+ if(!Migrations.findOne({name: "renamePostToPetition"})){
+ Posts = new Meteor.Collection('posts');
+ Posts.find().forEach(function (post){
+ Petitions.insert(post);
+ Posts.remove(post);
+ });
+ Migrations.insert({name: "renamePostToPetition"});
+ }
+
+ if(!Migrations.findOne({name: "fixSingletonPetitionCount"})){
+ single = Singleton.findOne();
+ single.petitionsCount += single.postsCount;
+ Migrations.insert({name: "fixSingletonPetitionCount"});
+ }
+
// auto-subscribe users to status update e-mails *if* they have official responses enabled
if (!Migrations.findOne({name: "enableStatusUpdateEmailsIfOfficialResponsesEnabled"})) {
Meteor.users.update({'notify.response': true}, {$set: {'notify.updates': true}}, {multi: true});
@@ -35,8 +50,37 @@ Meteor.startup(function () {
// mark all existing petitions as published by default
if (!Migrations.findOne({name: "markAllPreviousPetitionsAsPublished"})) {
- Posts.update({}, {$set: {published: true}}, {multi: true});
+ Petitions.update({}, {$set: {published: true}}, {multi: true});
Migrations.insert({name: "markAllPreviousPetitionsAsPublished"});
}
+ // update all petitions to have a lastSignedAt field
+ if (!Migrations.findOne({name: "addLastSignedAtField"})){
+ Petitions.update({}, {$set: {lastSignedAt: new Date().getTime()}});
+ Migrations.insert({name: "addLastSignedAtField"});
+ }
+
+ // change updates to reference petitionId rather than postId
+ if(!Migrations.findOne({name: "fixUpdatesPetitionIdField"})){
+ Updates.update({}, {$rename: { 'postId' : 'petitionId'}}, {multi: true});
+ Migrations.insert({name: "fixUpdatesPetitionIdField"});
+ }
+
+ // Adds all upvoters to subscribers for every petition.
+ if(!Migrations.findOne({name: "addUpvotersToSubscribers"})){
+ //Petitions.update({}, {$set: {subscribers: upvoters}});
+ Petitions.find().forEach(function (petition){
+ Meteor.users.find({_id: {$in: petition.upvoters}},{fields: {username: 1}}).forEach(function (u){
+ Petitions.update({
+ _id: petition.petitionId,
+ subscribers: {$ne: u}
+ }, {
+ $addToSet: {subscribers: u}
+ });
+ });
+
+ });
+ Migrations.insert({name: "addUpvotersToSubscribers"});
+ }
+
});
diff --git a/server/publications.js b/server/publications.js
old mode 100644
new mode 100755
index 7567837..2e71a6e
--- a/server/publications.js
+++ b/server/publications.js
@@ -1,21 +1,31 @@
-var findPosts = function (options) {
+var findPetitions = function (options) {
var sort = {},
selector = {};
// configure sort parameters
+
sort[options.sortBy] = -1;
- sort.submitted = -1;
// configure query selector
- selector.status = {$in: [options.status]};
+ if (options.ignoreStatus == null || options.ignoreStatus == false){
+ selector.status = {$in: [options.status]};
+ }
if (options.tagName) {
selector.tag_ids = {$in: [Tags.findOne({name: options.tagName})._id]}
}
if (!Roles.userIsInRole(options.userId, ['admin'])) {
selector['published'] = true;
}
-
- return Posts.find(selector, {
+
+ if (options.showSigned) {
+ selector.upvoters = options.userId;
+ }
+
+ if (options.showCreated) {
+ selector.userId = options.userId;
+ }
+
+ return Petitions.find(selector, {
limit: options.limit,
sort: sort,
fields: {
@@ -24,45 +34,79 @@ var findPosts = function (options) {
votes: 1,
submitted: 1,
status: 1,
- tag_ids: 1
+ tag_ids: 1,
+ lastSignedAt: 1,
+ upvoters: 1,
+ pending: 1
}
});
};
-Meteor.publish('posts', function (limit, sortBy, tagName) {
- return findPosts({
+Meteor.publish('petitions', function (limit, sortBy, tagName, showSigned, showCreated) {
+ return findPetitions.call(this, {
+ limit: limit,
+ sortBy: sortBy,
+ tagName: tagName,
+ userId: this.userId,
+ showSigned: showSigned,
+ showCreated: showCreated,
+ status: null
+ });
+});
+
+Meteor.publish('petitionsSearch', function (limit, sortBy, tagName, showSigned, showCreated) {
+ return findPetitions.call(this, {
limit: limit,
sortBy: sortBy,
tagName: tagName,
- userId: this.userId
+ userId: this.userId,
+ showSigned: showSigned,
+ showCreated: showCreated,
+ ignoreStatus: true
});
});
-Meteor.publish('postsInProgress', function (limit, sortBy) {
- return findPosts({
+
+Meteor.publish('petitionsInProgress', function (limit, sortBy, tagName, showSigned, showCreated) {
+ return findPetitions.call(this, {
limit: limit,
sortBy: sortBy,
+ tagName: tagName,
status: "waiting-for-reply",
- userId: this.userId
+ userId: this.userId,
+ showSigned: showSigned,
+ showCreated: showCreated
});
});
-Meteor.publish('postsWithResponses', function (limit, sortBy) {
- return findPosts({
+Meteor.publish('petitionsWithResponses', function (limit, sortBy, tagName, showSigned, showCreated) {
+ return findPetitions.call(this, {
limit: limit,
sortBy: sortBy,
+ tagName: tagName,
status: "responded",
- userId: this.userId
+ userId: this.userId,
+ showSigned: showSigned,
+ showCreated: showCreated
});
});
-Meteor.publish('singlePost', function (id) {
+Meteor.publish('pendingPetitions', function(){
+ if (Roles.userIsInRole(this.userId, ['admin', 'moderator'])) {
+ return Petitions.find({pending: true});
+ }else{
+ this.stop();
+ return;
+ }
+});
+
+Meteor.publish('singlePetition', function (id) {
var selector = {};
selector["_id"] = id;
- if (!Roles.userIsInRole(this.userId, ['admin'])) {
+ if ((!Roles.userIsInRole(this.userId, ['admin', 'moderator']))) {
selector['published'] = true;
}
- return Posts.find(selector, {
+ return Petitions.find(selector, {
fields: {
author: 1,
title: 1,
@@ -72,10 +116,12 @@ Meteor.publish('singlePost', function (id) {
response: 1,
responded_at: 1,
upvoters: 1,
+ subscribers: 1,
minimumVotes: 1,
status: 1,
tag_ids: 1,
- published: 1
+ published: 1,
+ pending: 1
}
});
});
@@ -103,20 +149,10 @@ Meteor.publish('privilegedUsers', function () {
}
});
-Meteor.publish('singleScore', function (postId) {
- return Scores.find({
- postId: postId,
- created_at: { $gte: moment().startOf('day').subtract(1, 'week').valueOf() }
- }, {
- limit: 7,
- sort: {created_at: 1}
- });
-});
-
-Meteor.publish('signers', function (postId) {
- var post = Posts.findOne(postId);
- if (post) {
- return Meteor.users.find({_id: {$in: post.upvoters}}, {
+Meteor.publish('signatories', function (petitionId) {
+ var petition = Petitions.findOne(petitionId);
+ if (petition) {
+ return Meteor.users.find({_id: {$in: petition.upvoters}}, {
fields: {
"profile.initials": 1
}
@@ -126,9 +162,9 @@ Meteor.publish('signers', function (postId) {
}
});
-Meteor.publish('updates', function (postId) {
+Meteor.publish('updates', function (petitionId) {
return Updates.find(
- { postId: postId },
+ { petitionId: petitionId },
{ fields:
{
title: 1,
@@ -141,10 +177,10 @@ Meteor.publish('updates', function (postId) {
});
Meteor.publish('tags', function () {
- return Tags.find();
+ return Tags.find({},{sort: {name: 1} } );
});
-// Expose individual users' notification preferences
+// Expose individual users' notification preferences
Meteor.publish(null, function() {
return Meteor.users.find({_id: this.userId}, {fields: {'notify.updates': 1, 'notify.response': 1}});
});
diff --git a/server/startup.js b/server/startup.js
old mode 100644
new mode 100755
index be00154..0732e3d
--- a/server/startup.js
+++ b/server/startup.js
@@ -1,4 +1,9 @@
Meteor.startup(function () {
Singleton.update({}, {$set: { version: "v1.2.3.1" }});
- process.env.MAIL_URL = Meteor.settings.MAIL_URL;
+ console.log(Singleton.findOne().moderation);
+ if(Singleton.findOne().moderation === undefined){
+ console.log("Setting Default");
+ Singleton.update({}, {$set: { moderation: false}});
+ }
+ process.env.MAIL_URL = Meteor.settings.MAIL.gateway_url;
});
diff --git a/settings.json.sample b/settings.json.sample
old mode 100644
new mode 100755
index a80ab03..8343a7c
--- a/settings.json.sample
+++ b/settings.json.sample
@@ -1,10 +1,81 @@
{
- "LDAP_URL": "ldaps://ldap.rit.edu",
- "MAIL_URL": "smtp://username@main.ad.rit.edu:password@mymail.ad.rit.edu:587",
+ "LDAP": {
+ "__comments": [
+ " username_attribute: Change to sAMAccountName if you are using Active Directory ",
+ " bind_dn_prefix: This is the prefix to your DN, change to something like cn= if you are using Active Directory. ",
+ " auto_group: This is optional, it will place users in the admin or moderator role if they are ",
+ " in the specified groups. This means memberOf is also available in the Meteor.user().profile "
+ ],
+ "url": "ldaps://ldap.rit.edu/",
+ "search_ou": "ou=LDAP-Users,dc=rit,dc=edu",
+ "username_attribute": "uid",
+ "bind_dn_prefix": "uid=",
+ "auto_group": {
+ "admin": "CN=Admins,OU=Petitions,OU=Application-Specific-Access,DC=rit,DC=edu",
+ "moderator": "CN=Moderators,OU=Petitions,OU=Application-Specific-Access,DC=rit,DC=edu"
+ }
+ },
+ "MAIL": {
+ "__comments": [
+ " default_domain: Appended to the username to construct an email address if no email address is present ",
+ " gateway_url: The MAIL_URL environment variable, more information here: http://docs.meteor.com/#/full/email ",
+ " templates: used by the Mailer object and provide defaults for the Email.send function. Example: ",
+ " { ",
+ " 'say_hello': { ",
+ " 'subject': 'Hello!', Default subject for the email, can be overridden by the caller ",
+ " 'to': 'hello@rit.edu', Default to address for the email, could also be an array like ['hello@rit.edu', 'goodbye@rit.edu'] ",
+ " 'template': 'say_hello' Required key for the template, a template should exist at the path /private/email_templates/say_hello.html ",
+ " } ",
+ " } ",
+ " template_defaults: Optional settings that are applied last to every template, so they are easily overridden ",
+ " template_overrides: Optional settings that are applied first to every template, they are most useful for local testing. "
+ ],
+
+ "default_domain": "@rit.edu",
+ "gateway_url": "smtp://username:password@smtp.rit.edu:25",
+ "templates": {
+ "report_petition": {
+ "subject": "[petitions] Petition Reported",
+ "template": "report_petition"
+ },
+ "petition_approved": {
+ "subject": "PawPrints - A petition you created has been approved",
+ "template": "petition_approved"
+ },
+ "petition_rejected": {
+ "subject": "PawPrints - A petition you created has been rejected",
+ "template": "petition_rejected"
+ },
+ "petition_threshold_reached": {
+ "subject": "PawPrints - Petition Reaches Signature Threshold",
+ "template": "petition_threshold_reached"
+ },
+ "petition_response_received": {
+ "subject": "PawPrints - A petition you signed has received a response",
+ "template": "petition_response_received"
+ },
+ "petition_status_update": {
+ "subject": "PawPrints - A petition you signed has a status update",
+ "template": "petition_status_update"
+ }
+ },
+ "template_defaults": {
+ "from": "web@rit.edu",
+ "to": "web@rit.edu"
+ },
+ "template_overrides": {
+ "to": "web@rit.edu"
+ }
+ },
"public" : {
+ "ui": {
+ "roles_locked": false,
+ "initials_locked": true,
+ "carousel_images": ["/carousel_1.png", "/carousel_2.png", "/carousel_3.png"]
+ },
"ga": {
"account":"UA-XXXXXXXX-Y"
},
"root_url": "http://localhost:3000"
}
-}
\ No newline at end of file
+}