diff --git a/.fabmanager-version b/.fabmanager-version
index 158349812d..a6c4b4a24a 100644
--- a/.fabmanager-version
+++ b/.fabmanager-version
@@ -1 +1 @@
-2.4.9
\ No newline at end of file
+2.4.10
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f8cbe8c549..6239fda7fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog Fab Manager
+## v2.4.10 2017 January 9
+
+- Optimized notifications system
+- Fix a bug: when many users with too many unread notifications are connected at the same time, the system kill the application due to memory overflow
+- Fix a bug: ReservationReminderWorker crash with undefined method find_by
+- Fix a bug: navigation to about page duplicates admin's links in left menu
+- Fix a bug: changing the price of a plan lost its past statistics
+- [TODO DEPLOY] `rake fablab:set_plans_slugs`
+
## v2.4.9 2017 January 4
- Mask new notifications alerts when more than 3
@@ -14,7 +23,7 @@
- Fix a bug: when deleting an availability just after its creation, the indexer workers crash and retries for a month
- [TODO DEPLOY] remove possible value `application/` in `ALLOWED_MIME_TYPES` list, in environment variable
- [TODO DEPLOY] `rails runner StatisticCustomAggregation.destroy_all`, then `rake db:seed`, then `rake fablab:es_build_availabilities_index` (1)
-- [TODO DEPLOY] `fablab:generate_stats[1095]` if you already has regenerated the statistics in the past, then they are very likely corrupted. Run this task to fix (2)
+- [TODO DEPLOY] `rake fablab:generate_stats[1095]` if you already has regenerated the statistics in the past, then they are very likely corrupted. Run this task to fix (2)
## v2.4.8 2016 December 15
diff --git a/app/assets/javascripts/controllers/application.coffee.erb b/app/assets/javascripts/controllers/application.coffee.erb
index 645b6e6cb9..683c2e3ffb 100644
--- a/app/assets/javascripts/controllers/application.coffee.erb
+++ b/app/assets/javascripts/controllers/application.coffee.erb
@@ -60,7 +60,9 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
Session.destroy()
$rootScope.currentUser = null
$rootScope.toCheckNotifications = false
- $scope.notifications = []
+ $scope.notifications =
+ total: 0
+ unread: 0
$state.go('app.public.home')
, (error) ->
# An error occurred logging out.
@@ -261,33 +263,30 @@ Application.Controllers.controller 'ApplicationController', ["$rootScope", "$sco
$rootScope.toCheckNotifications = true
unless $rootScope.checkNotificationsIsInit or !$rootScope.currentUser
setTimeout ->
- $scope.notifications = Notification.query {is_read: false}
- , 2000
- $scope.$watch 'notifications', (newValue, oldValue) ->
- diff = []
- angular.forEach newValue, (value) ->
- find = false
- for i in [0..oldValue.length] by 1
- if oldValue[i] and (value.id is oldValue[i].id)
- find = true
- break
-
- unless find
- diff.push(value)
+ # we request the most recent notifications
+ Notification.last_unread (notifications) ->
+ $rootScope.lastCheck = new Date()
+ $scope.notifications = notifications.totals
- remain = 3
- if diff.length >= remain
- diff.splice(remain, (diff.length - remain), {message: {description: _t('and_NUMBER_other_notifications', {NUMBER: diff.length - remain})}})
+ toDisplay = []
+ angular.forEach notifications.notifications, (n) ->
+ toDisplay.push(n)
- angular.forEach diff, (notification, key) ->
- growl.info(notification.message.description)
+ if toDisplay.length < notifications.totals.unread
+ toDisplay.push({message: {description: _t('and_NUMBER_other_notifications', {NUMBER: notifications.totals.unread - toDisplay.length}, "messageformat")}})
- , true
+ angular.forEach toDisplay, (notification) ->
+ growl.info(notification.message.description)
+ , 2000
checkNotifications = ->
if $rootScope.toCheckNotifications
- Notification.query({is_read: false}).$promise.then (data) ->
- $scope.notifications = data;
+ Notification.polling({last_poll: $rootScope.lastCheck}).$promise.then (data) ->
+ $rootScope.lastCheck = new Date()
+ $scope.notifications = data.totals
+
+ angular.forEach data.notifications, (notification) ->
+ growl.info(notification.message.description)
$interval(checkNotifications, NOTIFICATIONS_CHECK_PERIOD)
$rootScope.checkNotificationsIsInit = true
diff --git a/app/assets/javascripts/controllers/main_nav.coffee.erb b/app/assets/javascripts/controllers/main_nav.coffee.erb
index b1938b850f..0b83d9ce37 100644
--- a/app/assets/javascripts/controllers/main_nav.coffee.erb
+++ b/app/assets/javascripts/controllers/main_nav.coffee.erb
@@ -50,7 +50,7 @@ Application.Controllers.controller "MainNavController", ["$scope", "$location",
Fablab.adminNavLinks = Fablab.adminNavLinks || []
- Fablab.adminNavLinks = [
+ adminNavLinks = [
{
state: 'app.admin.trainings'
linkText: 'trainings_monitoring'
@@ -108,5 +108,5 @@ Application.Controllers.controller "MainNavController", ["$scope", "$location",
}
].concat(Fablab.adminNavLinks)
- $scope.adminNavLinks = Fablab.adminNavLinks
+ $scope.adminNavLinks = adminNavLinks
]
diff --git a/app/assets/javascripts/controllers/members.coffee b/app/assets/javascripts/controllers/members.coffee
index f71b8292c8..3c2c96e56e 100644
--- a/app/assets/javascripts/controllers/members.coffee
+++ b/app/assets/javascripts/controllers/members.coffee
@@ -225,7 +225,9 @@ Application.Controllers.controller "EditProfileController", ["$scope", "$rootSco
Session.destroy()
$rootScope.currentUser = null
$rootScope.toCheckNotifications = false
- $scope.notifications = []
+ $scope.notifications =
+ total: 0
+ unread: 0
$window.location.href = $scope.activeProvider.link_to_sso_connect
diff --git a/app/assets/javascripts/controllers/notifications.coffee b/app/assets/javascripts/controllers/notifications.coffee
index 3426aa1d08..9c2e3ade28 100644
--- a/app/assets/javascripts/controllers/notifications.coffee
+++ b/app/assets/javascripts/controllers/notifications.coffee
@@ -2,7 +2,7 @@
##
# Controller used in notifications page
-# inherits $scope.$parent.notifications (unread notifications) from ApplicationController
+# inherits $scope.$parent.notifications (global notifications state) from ApplicationController
##
Application.Controllers.controller "NotificationsController", ["$scope", 'Notification', ($scope, Notification) ->
@@ -20,6 +20,15 @@ Application.Controllers.controller "NotificationsController", ["$scope", 'Notifi
## Array containg the archived notifications (already read)
$scope.notificationsRead = []
+ ## Array containg the new notifications (not read)
+ $scope.notificationsUnread = []
+
+ ## Total number of notifications for the current user
+ $scope.total = 0
+
+ ## Total number of unread notifications for the current user
+ $scope.totalUnread = 0
+
## By default, the pagination mode is activated to limit the page size
$scope.paginateActive = true
@@ -39,10 +48,15 @@ Application.Controllers.controller "NotificationsController", ["$scope", 'Notifi
Notification.update {id: notification.id},
id: notification.id
is_read: true
- , ->
- index = $scope.$parent.notifications.indexOf(notification)
- $scope.$parent.notifications.splice(index,1)
- $scope.notificationsRead.push notification
+ , (updatedNotif) ->
+ # remove notif from unreads
+ index = $scope.notificationsUnread.indexOf(notification)
+ $scope.notificationsUnread.splice(index,1)
+ # add update notif to read
+ $scope.notificationsRead.push updatedNotif
+ # update counters
+ $scope.$parent.notifications.unread -= 1
+ $scope.totalUnread -= 1
@@ -52,21 +66,32 @@ Application.Controllers.controller "NotificationsController", ["$scope", 'Notifi
$scope.markAllAsRead = ->
Notification.update {}
, -> # success
- angular.forEach $scope.$parent.notifications, (n)->
+ # add notifs to read
+ angular.forEach $scope.notificationsUnread, (n)->
+ n.is_read = true
$scope.notificationsRead.push n
-
- $scope.$parent.notifications.splice(0, $scope.$parent.notifications.length)
+ # clear unread
+ $scope.notificationsUnread = []
+ # update counters
+ $scope.$parent.notifications.unread = 0
+ $scope.totalUnread = 0
##
- # Request the server to retrieve the next undisplayed notifications and add them
- # to the archived notifications list.
+ # Request the server to retrieve the next notifications and add them
+ # to their corresponding notifications list (read or unread).
##
- $scope.addMoreNotificationsReaded = ->
- Notification.query {is_read: true, page: $scope.page}, (notifications) ->
- $scope.notificationsRead = $scope.notificationsRead.concat notifications
- $scope.paginateActive = false if notifications.length < NOTIFICATIONS_PER_PAGE
+ $scope.addMoreNotifications = ->
+ Notification.query {page: $scope.page}, (notifications) ->
+ $scope.total = notifications.totals.total
+ $scope.totalUnread = notifications.totals.unread
+ angular.forEach notifications.notifications, (notif) ->
+ if notif.is_read
+ $scope.notificationsRead.push(notif)
+ else
+ $scope.notificationsUnread.push(notif)
+ $scope.paginateActive = (notifications.totals.total > ($scope.notificationsRead.length + $scope.notificationsUnread.length))
$scope.page += 1
@@ -78,7 +103,7 @@ Application.Controllers.controller "NotificationsController", ["$scope", 'Notifi
# Kind of constructor: these actions will be realized first when the controller is loaded
##
initialize = ->
- $scope.addMoreNotificationsReaded()
+ $scope.addMoreNotifications()
diff --git a/app/assets/javascripts/controllers/profile.coffee.erb b/app/assets/javascripts/controllers/profile.coffee.erb
index 781f12995f..c5ca54280c 100644
--- a/app/assets/javascripts/controllers/profile.coffee.erb
+++ b/app/assets/javascripts/controllers/profile.coffee.erb
@@ -170,7 +170,9 @@ Application.Controllers.controller "CompleteProfileController", ["$scope", "$roo
Session.destroy()
$rootScope.currentUser = null
$rootScope.toCheckNotifications = false
- $scope.notifications = []
+ $scope.notifications =
+ total: 0
+ unread: 0
$window.location.href = activeProviderPromise.link_to_sso_connect
diff --git a/app/assets/javascripts/services/notification.coffee b/app/assets/javascripts/services/notification.coffee
index 56709c43cd..42eb9e9e29 100644
--- a/app/assets/javascripts/services/notification.coffee
+++ b/app/assets/javascripts/services/notification.coffee
@@ -3,6 +3,14 @@
Application.Services.factory 'Notification', ["$resource", ($resource)->
$resource "/api/notifications/:id",
{id: "@id"},
+ query:
+ isArray: false
update:
method: 'PUT'
+ polling:
+ url: '/api/notifications/polling'
+ method: 'GET'
+ last_unread:
+ url: '/api/notifications/last_unread'
+ method: 'GET'
]
diff --git a/app/assets/templates/notifications/index.html.erb b/app/assets/templates/notifications/index.html.erb
index e4d3f00032..1d244d4cdb 100644
--- a/app/assets/templates/notifications/index.html.erb
+++ b/app/assets/templates/notifications/index.html.erb
@@ -19,7 +19,7 @@
-
+
@@ -31,7 +31,7 @@
-
+
|
-
+
{{ 'no_new_notifications' }} |
-
{{ 'archives' }}
+
+
{{ 'archives' }}
-
-
-
- |
- |
- |
+
+
+
+ |
+ |
+ |
-
-
-
+
+
+
-
-
- |
- {{ n.created_at | amDateFormat:'LLL' }} |
- |
+
+
+ |
+ {{ n.created_at | amDateFormat:'LLL' }} |
+ |
-
+
-
- {{ 'no_archived_notifications' }} |
-
+
+ {{ 'no_archived_notifications' }} |
+
-
-
+
+
+
-
{{ 'load_the_next_notifications' }}
+
{{ 'load_the_next_notifications' }}
diff --git a/app/assets/templates/shared/header.html.erb b/app/assets/templates/shared/header.html.erb
index ed5bfef099..3a2c59502c 100644
--- a/app/assets/templates/shared/header.html.erb
+++ b/app/assets/templates/shared/header.html.erb
@@ -23,7 +23,7 @@
-
- {{notifications.length}}
+ {{notifications.unread}}
-
diff --git a/app/controllers/api/notifications_controller.rb b/app/controllers/api/notifications_controller.rb
index 0c242ff6cf..02fb14ccf2 100644
--- a/app/controllers/api/notifications_controller.rb
+++ b/app/controllers/api/notifications_controller.rb
@@ -3,14 +3,49 @@ class API::NotificationsController < API::ApiController
before_action :authenticate_user!
def index
- if params[:is_read]
- if params[:is_read] == 'true'
- @notifications = current_user.notifications.where(is_read: true).page(params[:page]).per(15).order('created_at DESC')
- else
- @notifications = current_user.notifications.where(is_read: false).order('created_at DESC')
+ loop do
+ @notifications = current_user.notifications.page(params[:page]).per(15).order('created_at DESC')
+ # we delete obsolete notifications on first access
+ break unless delete_obsoletes(@notifications)
+ end
+ @totals = {
+ total: current_user.notifications.count,
+ unread: current_user.notifications.where(is_read: false).count
+ }
+ render :index
+ end
+
+ def last_unread
+ loop do
+ @notifications = current_user.notifications.where(is_read: false).limit(3).order('created_at DESC')
+ # we delete obsolete notifications on first access
+ break unless delete_obsoletes(@notifications)
+ end
+ @totals = {
+ total: current_user.notifications.count,
+ unread: current_user.notifications.where(is_read: false).count
+ }
+ render :index
+ end
+
+ def polling
+ @notifications = current_user.notifications.where('is_read = false AND created_at >= :date', date: params[:last_poll]).order('created_at DESC')
+ @totals = {
+ total: current_user.notifications.count,
+ unread: current_user.notifications.where(is_read: false).count
+ }
+ render :index
+ end
+
+ private
+ def delete_obsoletes(notifications)
+ cleaned = false
+ notifications.each do |n|
+ if !Module.const_get(n.attached_object_type) or !n.attached_object
+ n.destroy!
+ cleaned = true
end
- else
- @notifications = current_user.notifications.order('created_at DESC')
end
+ cleaned
end
end
diff --git a/app/models/plan.rb b/app/models/plan.rb
index 176488348b..2248fa343d 100644
--- a/app/models/plan.rb
+++ b/app/models/plan.rb
@@ -9,6 +9,9 @@ class Plan < ActiveRecord::Base
has_one :plan_file, as: :viewable, dependent: :destroy
has_many :prices, dependent: :destroy
+ extend FriendlyId
+ friendly_id :name, use: :slugged
+
accepts_nested_attributes_for :prices
accepts_nested_attributes_for :plan_file, allow_destroy: true, reject_if: :all_blank
@@ -94,7 +97,7 @@ def create_stripe_plan
end
def create_statistic_subtype
- StatisticSubType.create!({key: self.stp_plan_id, label: self.name})
+ StatisticSubType.create!({key: self.slug, label: self.name})
end
def create_statistic_association(stat_type, stat_subtype)
diff --git a/app/services/statistic_service.rb b/app/services/statistic_service.rb
index f19736ee6e..4fa428ac2f 100644
--- a/app/services/statistic_service.rb
+++ b/app/services/statistic_service.rb
@@ -12,7 +12,7 @@ def generate_statistic(options = default_options)
Stats::Subscription.create({
date: format_date(s.date),
type: s.duration,
- subType: s.stp_plan_id,
+ subType: s.slug,
stat: 1,
ca: s.ca,
planId: s.plan_id,
@@ -138,7 +138,7 @@ def subscriptions_list(options = default_options)
plan_interval: p.interval,
plan_interval_count: p.interval_count,
plan_group_name: p.group.name,
- stp_plan_id: p.stp_plan_id,
+ slug: p.slug,
duration: p.duration.to_i,
subscription_id: sub.id,
invoice_item_id: i.id,
diff --git a/app/views/api/notifications/index.json.jbuilder b/app/views/api/notifications/index.json.jbuilder
index 8a1380a64f..aafcdb02dd 100644
--- a/app/views/api/notifications/index.json.jbuilder
+++ b/app/views/api/notifications/index.json.jbuilder
@@ -1,13 +1,12 @@
-json.array!(@notifications) do |notification|
- if Module.const_get(notification.attached_object_type) and notification.attached_object # WHY WERE WE DOING Object.const_defined?(notification.attached_object_type) ??? Why not just deleting obsolete notifications ?! Object.const_defined? was introducing a bug! Module.const_get is a TEMPORARY fix, NOT a solution
- json.extract! notification, :id, :notification_type_id, :notification_type, :created_at, :is_read
- json.attached_object notification.attached_object
- json.message do
- if notification.notification_type.nil?
- json.partial! 'api/notifications/undefined_notification', notification: notification
- else
- json.partial! "/api/notifications/#{notification.notification_type}", notification: notification
- end
+json.totals @totals
+json.notifications(@notifications) do |notification|
+ json.extract! notification, :id, :notification_type_id, :notification_type, :created_at, :is_read
+ json.attached_object notification.attached_object
+ json.message do
+ if notification.notification_type.nil?
+ json.partial! 'api/notifications/undefined_notification', notification: notification
+ else
+ json.partial! "/api/notifications/#{notification.notification_type}", notification: notification
end
end
end.delete_if {|n| n['id'] == nil }
diff --git a/app/workers/reservation_reminder_worker.rb b/app/workers/reservation_reminder_worker.rb
index 68d723dfce..5aff73febc 100644
--- a/app/workers/reservation_reminder_worker.rb
+++ b/app/workers/reservation_reminder_worker.rb
@@ -16,7 +16,7 @@ def perform
already_sent = Notification.where(
attached_object_type: Reservation.name,
attached_object_id: r.id,
- notification_type_id: NotificationType.find_by(name: 'notify_member_reservation_reminder')
+ notification_type_id: NotificationType.find_by_name('notify_member_reservation_reminder')
).count
unless already_sent > 0
NotificationCenter.call type: 'notify_member_reservation_reminder',
diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml
index 7221927ab3..4eae93c2dd 100644
--- a/config/locales/app.public.en.yml
+++ b/config/locales/app.public.en.yml
@@ -104,7 +104,7 @@ en:
version: "Version:"
# Notifications
- and_NUMBER_other_notifications: "and {{NUMBER}} other notifications..." # angular interpolation
+ and_NUMBER_other_notifications: "and {NUMBER, plural, =0{no other notifications} =1{one other notification} other{{NUMBER} other notifications}}..." # messageFormat interpolation
about:
# about page
diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml
index ad2eaeeabe..a6e7be6c8f 100644
--- a/config/locales/app.public.fr.yml
+++ b/config/locales/app.public.fr.yml
@@ -104,7 +104,7 @@ fr:
version: "Version :"
# Notifications
- and_NUMBER_other_notifications: "et {{NUMBER}} autres notifications ..." # angular interpolation
+ and_NUMBER_other_notifications: "et {NUMBER, plural, =0{aucune autre notification} =1{une autre notification} other{{NUMBER} autres notifications}} ..." # messageFormat interpolation
about:
# page à propos
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 26b8f7d830..c5c2b44cc2 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -260,7 +260,7 @@ fr:
your_invoice_is_ready_html: "Votre facture n°%{REFERENCE}, d'un montant de %{AMOUNT}, est prête. Cliquez ici pour la télécharger."
undefined_notification:
unknown_notification: "Notification inconnue"
- notification_ID_wrong_type_TYPE_unknown: "Notification {ID} erronée (type {TYPE} inconnu)."
+ notification_ID_wrong_type_TYPE_unknown: "Notification %{ID} erronée (type %{TYPE} inconnu)."
notify_user_wallet_is_credited:
your_wallet_is_credited: "Votre porte-monnaie a bien été crédité de %{AMOUNT} par l'administrateur"
notify_admin_user_wallet_is_credited:
diff --git a/config/routes.rb b/config/routes.rb
index 496ba1594c..fc122e5ad9 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -53,6 +53,8 @@
resources :reservations, only: [:show, :create, :index, :update]
resources :notifications, only: [:index, :show, :update] do
match :update_all, path: '/', via: [:put, :patch], on: :collection
+ get 'polling', action: 'polling', on: :collection
+ get 'last_unread', action: 'last_unread', on: :collection
end
resources :wallet, only: [] do
get '/by_user/:user_id', action: 'by_user', on: :collection
diff --git a/db/migrate/20170109085345_add_slug_to_plan.rb b/db/migrate/20170109085345_add_slug_to_plan.rb
new file mode 100644
index 0000000000..301a8b6f17
--- /dev/null
+++ b/db/migrate/20170109085345_add_slug_to_plan.rb
@@ -0,0 +1,5 @@
+class AddSlugToPlan < ActiveRecord::Migration
+ def change
+ add_column :plans, :slug, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 1b7f84d7f4..9f233830ce 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,12 +11,12 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20161123104604) do
+ActiveRecord::Schema.define(version: 20170109085345) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
- enable_extension "unaccent"
enable_extension "pg_trgm"
+ enable_extension "unaccent"
create_table "abuses", force: :cascade do |t|
t.integer "signaled_id"
@@ -289,7 +289,7 @@
t.boolean "is_read", default: false
t.datetime "created_at"
t.datetime "updated_at"
- t.string "receiver_type", limit: 255
+ t.string "receiver_type"
t.boolean "is_send", default: false
t.jsonb "meta_data", default: {}
end
@@ -374,6 +374,7 @@
t.string "base_name"
t.integer "ui_weight", default: 0
t.integer "interval_count", default: 1
+ t.string "slug"
end
add_index "plans", ["group_id"], name: "index_plans_on_group_id", using: :btree
@@ -466,7 +467,7 @@
t.datetime "published_at"
end
- add_index "projects", ["slug"], name: "index_projects_on_slug", unique: true, using: :btree
+ add_index "projects", ["slug"], name: "index_projects_on_slug", using: :btree
create_table "projects_components", force: :cascade do |t|
t.integer "project_id"
@@ -535,8 +536,8 @@
t.datetime "updated_at"
t.integer "availability_id"
t.datetime "ex_start_at"
- t.datetime "canceled_at"
t.datetime "ex_end_at"
+ t.datetime "canceled_at"
t.boolean "offered", default: false
end
@@ -718,7 +719,6 @@
add_index "user_trainings", ["user_id"], name: "index_user_trainings_on_user_id", using: :btree
create_table "users", force: :cascade do |t|
- t.string "username", limit: 255
t.string "email", limit: 255, default: "", null: false
t.string "encrypted_password", limit: 255, default: "", null: false
t.string "reset_password_token", limit: 255
@@ -741,6 +741,7 @@
t.boolean "is_allow_contact", default: true
t.integer "group_id"
t.string "stp_customer_id", limit: 255
+ t.string "username", limit: 255
t.string "slug", limit: 255
t.boolean "is_active", default: true
t.boolean "invoicing_disabled", default: false
diff --git a/lib/tasks/fablab.rake b/lib/tasks/fablab.rake
index 13b0d58909..1ee489b0c5 100644
--- a/lib/tasks/fablab.rake
+++ b/lib/tasks/fablab.rake
@@ -269,4 +269,14 @@ namespace :fablab do
StatisticService.new.generate_statistic({start_date: i.day.ago.beginning_of_day,end_date: i.day.ago.end_of_day})
end
end
+
+
+ desc 'set slugs to plans'
+ task set_plans_slugs: :environment do
+ # this will maintain compatibility with existing statistics
+ Plan.all.each do |p|
+ p.slug = p.stp_plan_id
+ p.save
+ end
+ end
end