diff --git a/.gitignore b/.gitignore index 02cf3c1..f8ec4c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .meteor/local .meteor/meteorite node_modules/* -settings.json \ No newline at end of file +settings.json +custom.branding.import.less diff --git a/.meteor/.finished-upgraders b/.meteor/.finished-upgraders index 68df3d8..dacc2c0 100644 --- a/.meteor/.finished-upgraders +++ b/.meteor/.finished-upgraders @@ -5,3 +5,9 @@ notices-for-0.9.0 notices-for-0.9.1 0.9.4-platform-file +notices-for-facebook-graph-api-2 +1.2.0-standard-minifiers-package +1.2.0-meteor-platform-split +1.2.0-cordova-changes +1.2.0-breaking-changes +1.3.0-split-minifiers-package diff --git a/.meteor/cordova-plugins b/.meteor/cordova-plugins index 8b13789..e69de29 100644 --- a/.meteor/cordova-plugins +++ b/.meteor/cordova-plugins @@ -1 +0,0 @@ - diff --git a/.meteor/packages b/.meteor/packages index c53e2cd..7fb381e 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -11,7 +11,6 @@ email spiderable iron:router cfs:http-methods -matteodem:easy-search percolatestudio:synced-cron meteorhacks:npm datariot:ganalytics @@ -21,6 +20,14 @@ manuelschoebel:ms-seo mrt:moment sacha:spin alanning:roles + + npm-container -nemo64:bootstrap +easy:search +fileer:string-format +meteorhacks:ssr +summernote:standalone +standard-minifier-css +standard-minifier-js +huttonr:bootstrap3 diff --git a/.meteor/release b/.meteor/release index fdc6583..20ea255 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@1.0.2.1 +METEOR@1.3.4 diff --git a/.meteor/versions b/.meteor/versions index bfb704b..73428ad 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -1,81 +1,110 @@ -accounts-base@1.1.3 -alanning:roles@1.2.13 -application-configuration@1.0.4 -autoupdate@1.1.4 -base64@1.0.2 -binary-heap@1.0.2 -blaze@2.0.4 -blaze-tools@1.0.2 -boilerplate-generator@1.0.2 -callback-hook@1.0.2 -cfs:http-methods@0.0.27 -check@1.0.3 -coffeescript@1.0.5 +accounts-base@1.2.8 +alanning:roles@1.2.15 +allow-deny@1.0.5 +autoupdate@1.2.10 +babel-compiler@6.8.3 +babel-runtime@0.1.9 +base64@1.0.9 +binary-heap@1.0.9 +blaze@2.1.8 +blaze-tools@1.0.9 +boilerplate-generator@1.0.9 +caching-compiler@1.0.5 +caching-html-compiler@1.0.6 +callback-hook@1.0.9 +cfs:http-methods@0.0.32 +check@1.2.3 +coffeescript@1.1.2 datariot:ganalytics@0.2.1 -ddp@1.0.13 -deps@1.0.6 -ejson@1.0.5 -email@1.0.5 -fastclick@1.0.2 -follower-livedata@1.0.3 -geojson-utils@1.0.2 -html-tools@1.0.3 -htmljs@1.0.3 -http@1.0.9 -id-map@1.0.2 -iron:controller@1.0.6 -iron:core@1.0.6 -iron:dynamic-template@1.0.6 -iron:layout@1.0.6 -iron:location@1.0.6 -iron:middleware-stack@1.0.6 -iron:router@1.0.6 -iron:url@1.0.6 -jquery@1.0.2 -json@1.0.2 -launch-screen@1.0.1 -less@1.0.12 -livedata@1.0.12 -localstorage@1.0.2 -logging@1.0.6 +ddp@1.2.5 +ddp-client@1.2.8 +ddp-common@1.2.6 +ddp-rate-limiter@1.0.5 +ddp-server@1.2.8 +deps@1.0.12 +diff-sequence@1.0.6 +easy:search@2.0.9 +easysearch:components@2.0.9 +easysearch:core@2.0.9 +ecmascript@0.4.6 +ecmascript-runtime@0.2.11 +ejson@1.0.12 +email@1.0.14 +fastclick@1.0.12 +fileer:string-format@0.0.1 +geojson-utils@1.0.9 +html-tools@1.0.10 +htmljs@1.0.10 +http@1.1.7 +huttonr:bootstrap3@3.3.6_12 +huttonr:bootstrap3-assets@3.3.6_3 +id-map@1.0.8 +iron:controller@1.0.12 +iron:core@1.0.11 +iron:dynamic-template@1.0.12 +iron:layout@1.0.12 +iron:location@1.0.11 +iron:middleware-stack@1.1.0 +iron:router@1.0.13 +iron:url@1.0.11 +jquery@1.11.9 +launch-screen@1.0.12 +less@2.6.3 +livedata@1.0.18 +localstorage@1.0.11 +logging@1.0.13 manuelschoebel:ms-seo@0.4.1 -matteodem:easy-search@1.4.5 -meteor@1.1.4 -meteor-platform@1.2.1 +meteor@1.1.15 +meteor-platform@1.2.6 meteorhacks:async@1.0.0 -meteorhacks:npm@1.2.2 -minifiers@1.1.3 -minimongo@1.0.6 -mobile-status-bar@1.0.2 -mongo@1.0.11 -mongo-livedata@1.0.7 +meteorhacks:npm@1.5.0 +meteorhacks:ssr@2.2.0 +minifier-css@1.1.12 +minifier-js@1.1.12 +minimongo@1.0.17 +mobile-status-bar@1.0.12 +modules@0.6.4 +modules-runtime@0.6.4 +mongo@1.1.9 +mongo-id@1.0.5 mrt:moment@2.8.1 natestrauser:select2@3.5.1 -nemo64:bootstrap@3.3.1_1 -nemo64:bootstrap-data@3.3.1_1 -npm-container@1.0.0 -observe-sequence@1.0.4 -ordered-dict@1.0.2 +npm-container@1.2.0 +npm-mongo@1.4.44 +observe-sequence@1.0.12 +ordered-dict@1.0.8 +peerlibrary:assert@0.2.5 +peerlibrary:base-component@0.14.0 +peerlibrary:blaze-components@0.16.2 +peerlibrary:computed-field@0.3.1 +peerlibrary:data-lookup@0.1.0 +peerlibrary:reactive-field@0.1.0 percolatestudio:synced-cron@1.1.0 -random@1.0.2 -reactive-dict@1.0.5 -reactive-var@1.0.4 -reload@1.1.2 -retry@1.0.2 -routepolicy@1.0.3 -sacha:spin@2.0.4 -service-configuration@1.0.3 -session@1.0.5 -spacebars@1.0.4 -spacebars-compiler@1.0.4 -spiderable@1.0.6 -standard-app-packages@1.0.4 -templating@1.0.10 +promise@0.7.2 +random@1.0.10 +rate-limit@1.0.5 +reactive-dict@1.1.8 +reactive-var@1.0.10 +reload@1.1.10 +retry@1.0.8 +routepolicy@1.0.11 +sacha:spin@2.3.1 +service-configuration@1.0.10 +session@1.1.6 +spacebars@1.0.12 +spacebars-compiler@1.0.12 +spiderable@1.0.13 +standard-app-packages@1.0.9 +standard-minifier-css@1.0.7 +standard-minifier-js@1.0.7 +summernote:standalone@0.8.1 +templating@1.1.12 +templating-tools@1.0.4 tmeasday:crypto-base@3.1.2 tmeasday:crypto-md5@3.1.2 -tracker@1.0.4 -ui@1.0.5 -underscore@1.0.2 -url@1.0.3 -webapp@1.1.5 -webapp-hashing@1.0.2 +tracker@1.0.14 +ui@1.0.11 +underscore@1.0.9 +url@1.0.10 +webapp@1.2.9 +webapp-hashing@1.0.9 diff --git a/LICENSE b/LICENSE index 366e462..0f99df0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 RIT Student Government +Copyright (c) 2016 RIT Student Government Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ba8cef5..2c07bad 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,11 @@ Usage (Production Environment) Contributing ============ -- The [roadmap] details planned features by core project maintainers. +- See GitHub Issues for work to be done. If you have any questions, just ask! - We are open to pull requests! Please follow the coding conventions currently in place. [Node]:http://nodejs.org/ [Meteor]:https://www.meteor.com/ -[roadmap]:https://trello.com/b/b6Kyx395/petition-roadmap [config]:https://github.com/ritstudentgovernment/config [article]:https://gentlenode.com/journal/meteor-1-deploy-and-manage-a-meteor-application-on-ubuntu-with-nginx/1 diff --git a/api/v1/posts.js b/api/v1/posts.js index 45c140c..5dd1361 100644 --- a/api/v1/posts.js +++ b/api/v1/posts.js @@ -20,7 +20,7 @@ if (Meteor.isServer) { this.setContentType('application/json'); var limit = Math.min(parseInt(this.query.limit) || 500, 500); var selector = {published: true}; - var posts = Posts.find(selector, {fields: { title: 1, + var petitions = Petitions.find(selector, {fields: { title: 1, votes: 1, author: 1, description: 1, @@ -28,7 +28,7 @@ if (Meteor.isServer) { response: 1, responded_at: 1, minimumVotes: 1}, limit: limit}).fetch(); - return JSON.stringify(posts); + return JSON.stringify(petitions); } else { this.setStatusCode(401); } @@ -41,7 +41,7 @@ if (Meteor.isServer) { var selector = {}; selector['_id'] = this.params.petitionId; selector['published'] = true; - var post = Posts.findOne(selector, {fields: { title: 1, + var petition = Petitions.findOne(selector, {fields: { title: 1, votes: 1, author: 1, description: 1, @@ -50,22 +50,11 @@ if (Meteor.isServer) { response: 1, responded_at: 1, minimumVotes: 1}}); - if (post) { + if (petition) { this.setContentType('application/json'); - post.signers = Meteor.users.find({'_id': {$in: post.upvoters}}).map(function (signer) { return signer.profile.initials }); - post.history = Scores.find({ - postId: post._id, - created_at: { $gte: moment().startOf('day').subtract(1, 'week').valueOf() } - }, { - fields: { - created_at: 1, - votes: 1 - }, - limit: 7, - sort: {created_at: 1} - }).fetch(); - delete post.upvoters; - return JSON.stringify(post); + petition.signatories = Meteor.users.find({'_id': {$in: petition.upvoters}}).map(function (signer) { return signer.profile.initials }); + delete petition.upvoters; + return JSON.stringify(petition); } else this.setStatusCode(404); } else { @@ -74,4 +63,4 @@ if (Meteor.isServer) { } } }); -} \ No newline at end of file +} diff --git a/client/helpers/handlebars.js b/client/helpers/handlebars.js old mode 100644 new mode 100755 index 4549d67..f07d278 --- a/client/helpers/handlebars.js +++ b/client/helpers/handlebars.js @@ -57,7 +57,7 @@ Handlebars.registerHelper('fromNow', function (time) { var timeTick = new Deps.Dependency(); Meteor.setInterval(function () { timeTick.changed(); }, 1000); timeTick.depend(); - return new moment(time).fromNow().toUpperCase(); + return new moment(time).fromNow(); }); Handlebars.registerHelper('singleton', function () { @@ -83,4 +83,8 @@ var routeUtils = { Handlebars.registerHelper('isActiveRoute', function(route) { return routeUtils.testRoutes(route) ? 'active' : ''; -}); \ No newline at end of file +}); + +Handlebars.registerHelper('publicSettings', function(route) { + return Meteor.settings.public; +}); diff --git a/client/helpers/init.js b/client/helpers/init.js index 54b7d91..eb1c72b 100644 --- a/client/helpers/init.js +++ b/client/helpers/init.js @@ -1,2 +1,2 @@ -Session.setDefault('postsLimit', 12); -Session.setDefault('postOrder', 'submitted'); \ No newline at end of file +Session.setDefault('petitionsLimit', 12); +Session.setDefault('petitionOrder', 'submitted'); diff --git a/client/main.html b/client/main.html old mode 100644 new mode 100755 index e19f508..6dac29f --- a/client/main.html +++ b/client/main.html @@ -1,5 +1,5 @@ - PawPrints + Petitions diff --git a/client/stylesheets/bootstrap-settings.json b/client/stylesheets/bootstrap-settings.json new file mode 100644 index 0000000..ea5404b --- /dev/null +++ b/client/stylesheets/bootstrap-settings.json @@ -0,0 +1,70 @@ +{ + "less": { + "customVariables": true, + + "exposeMixins": false, + + "compile": true, + + "modules": { + "alerts": true, + "badges": true, + "breadcrumbs": true, + "button-groups": true, + "buttons": true, + "carousel": true, + "close": true, + "code": true, + "component-animations": true, + "dropdowns": true, + "forms": true, + "glyphicons": true, + "grid": true, + "input-groups": true, + "jumbotron": true, + "labels": true, + "list-group": true, + "media": true, + "modals": true, + "navbar": true, + "navs": true, + "normalize": true, + "pager": true, + "pagination": true, + "panels": true, + "popovers": true, + "print": true, + "progress-bars": true, + "responsive-embed": true, + "responsive-utilities": true, + "scaffolding": true, + "tables": true, + "thumbnails": true, + "tooltip": true, + "type": true, + "utilities": true, + "wells": true + } + }, + + "javascript": { + "expose": false, + + "modules": { + "affix": true, + "alert": true, + "button": true, + "carousel": true, + "collapse": true, + "dropdown": true, + "modal": true, + "popover": true, + "scrollspy": true, + "tab": true, + "tooltip": true, + "transition": true + } + }, + + "version": 2 +} diff --git a/client/stylesheets/vendor/custom.bootstrap.import.less b/client/stylesheets/bootstrap-variables.less similarity index 94% rename from client/stylesheets/vendor/custom.bootstrap.import.less rename to client/stylesheets/bootstrap-variables.less index 8e7a38f..52b5db1 100644 --- a/client/stylesheets/vendor/custom.bootstrap.import.less +++ b/client/stylesheets/bootstrap-variables.less @@ -1,16 +1,14 @@ -// This File is for you to modify! -// It won't be overwritten as long as it exists. -// You may include this file into your less files to benefit from -// mixins and variables that bootstrap provides. -@import "custom.bootstrap.mixins.import.less"; +// These are custom bootstrap variables for you to edit. +// These simply override any default bootstrap variables. +// This means that you may delete anything in this file +// and the default bootstrap values will be used instead. -// @import "bootstrap/less/variables.less" -// -// Variables -// -------------------------------------------------- - +//== PawPrints Custom +@light-gray: #F5F5F5; +@dark-gray: #333; +@main-background: #eee; //== Colors // @@ -23,12 +21,8 @@ @gray-light: lighten(@gray-base, 46.7%); // #777 @gray-lighter: lighten(@gray-base, 93.5%); // #eee -@light-gray: #F5F5F5; -@dark-gray: #333; -@main-background: #eee; - -@brand-primary: #F36E21; -@brand-secondary: #513127; +@brand-primary: #428bca; +@brand-secondary: @brand-info; //PawPrints custom @brand-success: #5cb85c; @brand-info: #5bc0de; @brand-warning: #f0ad4e; @@ -61,6 +55,7 @@ @import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:600); @font-family-sans-serif: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif; @font-family-serif: Georgia, "Times New Roman", Times, serif; + //** Default monospace fonts for ``, ``, 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 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 @@ @@ -78,7 +86,7 @@

    Petition Tags

    value="{{this.name}}" disabled> - + @@ -100,47 +108,108 @@

    Petition Tags

    + + \ 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 @@ + 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 @@ + 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 @@ \ 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 @@ \ 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 @@ \ 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. + +
    {{ singleton.version }}

    @@ -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 @@ + 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 @@ + 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 @@ + 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 @@ - 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 @@ \ 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 @@ \ 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 @@ \ 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 +}