/// <summary>
        /// Get all Workspaces and it filters by "Url = items" and "BadgeCount > 0"
        /// If there are Items in the workspace, it updates the BadgeCount.
        /// If there are no Items in the workspace, it resets the BadgeCount.
        /// </summary>
        /// <param name="issueManager"></param>
        public void GetWorkspaceItems(IssueManager issueManager)
        {
            try
            {
                NavigationCardsManager navigationCardsManager = new NavigationCardsManager(issueManager);
                List <NavigationCard>  workspaces             = navigationCardsManager.GetAll();
                foreach (NavigationCard workspace in workspaces.ToList())
                {
                    if (workspace.Url == "items" && workspace.BadgeCount > 0)
                    {
                        IssuesFilter    filter         = ChangeSystemFilterTypesMe(workspace.Filter, (int)workspace.UserId, false);
                        List <IssueDto> workspaceItems = issueManager.GetFiltered(filter, true);

                        if (workspaceItems.Count() > 0)
                        {
                            UpdateBadgeCount(workspace, workspaceItems, navigationCardsManager, false);
                        }
                        else
                        {
                            UpdateBadgeCount(workspace, workspaceItems, navigationCardsManager, true);
                        }
                    }
                }
            }
            catch (Exception exception)
            {
                GeminiApp.LogException(exception, false, string.Concat("Run Method GetWorkspaceItems: ", exception.Message));
            }
        }
Beispiel #2
0
        private List <UserDto> GetCardSubscribers(NavigationCard card, NavigationCardsManager cardManager, UserManager userManager, UserDto owner)
        {
            if (!owner.Entity.Active)
            {
                return(new List <UserDto>());
            }

            Dictionary <int, UserDto> subscribers = new Dictionary <int, UserDto>();

            subscribers.Add(owner.Entity.Id, owner);

            foreach (var user in card.CardData.Subscription.Users)
            {
                var userDto = userManager.Get(user);

                if (user != owner.Entity.Id && userDto != null && userDto.Entity.Active)
                {
                    subscribers.Add(user, userDto);
                }
            }

            var groupUsers = cardManager.GetUsersFromGroups(card, card.CardData.Subscription.Groups);

            foreach (var user in groupUsers)
            {
                if (!subscribers.ContainsKey(user.Entity.Id))
                {
                    subscribers.Add(user.Entity.Id, user);
                }
            }

            return(new List <UserDto>(subscribers.Values));
        }
Beispiel #3
0
        /// <summary>
        /// Get all Workspaces and it filters by "Url = items" and "BadgeCount > 0"
        /// If there are Items in the workspace, it updates the BadgeCount.
        /// If there are no Items in the workspace, it resets the BadgeCount.
        /// </summary>
        /// <param name="issueManager"></param>
        public void GetWorkspaceItems(IssueManager issueManager)
        {
            NavigationCardsManager navigationCardsManager = new NavigationCardsManager(issueManager);
            List <NavigationCard>  workspaces             = navigationCardsManager.GetAll();

            foreach (NavigationCard workspace in workspaces)
            {
                if (workspace.Url == "items" && workspace.BadgeCount > 0)
                {
                    List <IssueDto> workspaceItems = issueManager.GetItems(workspace);

                    if (workspaceItems.Count() > 0)
                    {
                        UpdateBadgeCount(workspace, workspaceItems, navigationCardsManager);
                    }
                    else if (workspaceItems.Count() == 0)
                    {
                        ResetBadgeCount(workspace, navigationCardsManager);
                    }
                }
            }
        }
Beispiel #4
0
        /// <summary>
        /// This method updates the BadgeCount.
        /// In the first foreach, every items that should not be deleted, will be in the list "elementsNotToDelete".
        /// The second foreach, compares the elements in BadgeCount with the list "elementsNotToDelete"
        /// and filters the items that should be deleted in the "elementsToDelete".
        /// In the last foreach, the items in the BadgeCount, that exist in "elementsToDelete", will be deleted.
        /// </summary>
        /// <param name="workspace"></param>
        /// <param name="workspaceItems"></param>
        /// <param name="navigationCardsManager"></param>
        public void UpdateBadgeCount(NavigationCard workspace, List <IssueDto> workspaceItems, NavigationCardsManager navigationCardsManager)
        {
            List <int> badges = workspace.CardData.Badges;
            List <int> elementsNotToDelete = new List <int>();
            List <int> elementsToDelete    = new List <int>();

            foreach (IssueDto item in workspaceItems)
            {
                int isInList = badges.IndexOf(item.Id);
                if (isInList != -1)
                {
                    elementsNotToDelete.Add(item.Id);
                }
            }

            foreach (int badge in badges)
            {
                if (!elementsNotToDelete.Exists(element => element == badge))
                {
                    elementsToDelete.Add(badge);
                }
            }

            foreach (int badgetoDelete in elementsToDelete)
            {
                workspace.CardData.Badges.Remove(badgetoDelete);
                navigationCardsManager.Update(workspace);
                LogDebugMessage("Update Badge Count in Workspace: " + workspace.Id + " " + workspace.Title);
            }
        }
Beispiel #5
0
 /// <summary>
 /// Resets the BadgeCount from Workspace if "Badges > 0"
 /// </summary>
 /// <param name="workspace">Workspace properties</param>
 /// <param name="navigationCardsManager"></param>
 public void ResetBadgeCount(NavigationCard workspace, NavigationCardsManager navigationCardsManager)
 {
     workspace.CardData.Badges.RemoveAll(item => item > 0);
     navigationCardsManager.Update(workspace);
     LogDebugMessage("Reset Badge Count in Workspace: " + workspace.Id + " " + workspace.Title);
 }
        /// <summary>
        /// This method updates the BadgeCount.
        /// In the first foreach, every items that should not be deleted, will be in the list "elementsNotToDelete".
        /// The second foreach, compares the elements in BadgeCount with the list "elementsNotToDelete"
        /// and filters the items that should be deleted in the "elementsToDelete".
        /// In the last foreach, the items in the BadgeCount, that exist in "elementsToDelete", will be deleted.
        /// </summary>
        /// <param name="workspace"></param>
        /// <param name="workspaceItems"></param>
        /// <param name="navigationCardsManager"></param>
        /// <param name="reset"></param>
        public void UpdateBadgeCount(NavigationCard workspace, List <IssueDto> workspaceItems, NavigationCardsManager navigationCardsManager, bool reset)
        {
            try
            {
                bool change = false;

                workspace = navigationCardsManager.Get(workspace.Id);
                if (reset)
                {
                    change = true;
                    workspace.CardData.Badges.RemoveAll(item => item > 0);
                    LogDebugMessage("Reset Badge Count in Workspace: " + workspace.Id + " " + workspace.Title);
                }
                else
                {
                    foreach (int badge in workspace.CardData.Badges.ToList())
                    {
                        if (!workspaceItems.Exists(item => item.Entity.Id == badge))
                        {
                            change = true;
                            workspace.CardData.Badges.RemoveAll(id => id == badge);
                            LogDebugMessage(string.Concat("Update Badge Count in Workspace: ", workspace.Key, " (", workspace.Id, ") ", workspace.Title, " >> Issue: ", badge));
                        }
                    }
                }


                if (change)
                {
                    //it's possible workspace change at time of runing timerapp get new data and don't overwrite user changes.
                    List <int> newBadges = workspace.CardData.Badges;
                    workspace.Filter          = ChangeSystemFilterTypesMe(workspace.Filter, (int)workspace.UserId, true);
                    workspace                 = navigationCardsManager.Get(workspace.Id);
                    workspace.CardData.Badges = newBadges;
                    navigationCardsManager.Update(workspace, true, false);
                }
            }
            catch (Exception exception)
            {
                GeminiApp.LogException(exception, false, string.Concat("Run Method UpdateBadgeCount: ", exception.Message));
            }
        }
Beispiel #7
0
        private void ProcessAppNavCardAlerts()
        {
            var navigationCardsManager = new NavigationCardsManager(_issueManager);

            List <NavigationCard> cards = navigationCardsManager.GetPendingAlertCards();

            LogDebugMessage("Email templates loaded: " + _templates.Count);

            LogDebugMessage("Pending card alerts: " + cards.Count);

            // ? We need to store user id and issue id for every email we send out -- avoids dupes?
            List <string> issuesEmailedToUsers = new List <string>(50);

            List <string> individualIssuesEmailedToUsers = new List <string>(50);

            AlertsTemplateHelper alerts = new AlertsTemplateHelper(_templates, GetUrl(_issueManager), _languages);

            UserManager userManager = new UserManager(_issueManager);

            bool refreshCache = false;

            var allOptOuts = navigationCardsManager.GetOptOuts();

            foreach (NavigationCard card in cards)
            {
                List <IssueDto> individualIssues = new List <IssueDto>();

                // Safety checks
                if (!card.UserId.HasValue)
                {
                    continue;
                }

                if (card.CardData.Alerts.Count == 0)
                {
                    continue;
                }

                UserDto recepient = userManager.Get(card.UserId.Value);

                // Safety check
                if (!recepient.Entity.EmailMe || recepient.Entity.Email.IsEmpty())
                {
                    continue;
                }

                DateTime lastChecked = card.CardData.AlertsLastSent.HasValue ? card.CardData.AlertsLastSent.Value : card.Created;

                DateTime lastCheckedLocal = lastChecked.ToLocal(_issueManager.UserContext.User.TimeZone);

                AlertTypeAppNavAlertsTemplateModel model = new AlertTypeAppNavAlertsTemplateModel();

                model.TheRecipient = recepient;

                model.Version = GeminiVersion.Version;

                model.GeminiUrl = alerts.GeminiUrl;

                List <int> issuesToAlert = new List <int>(card.CardData.Alerts);

                foreach (int issueId in issuesToAlert)
                {
                    IssueDto issue = null;
                    try
                    {
                        issue = _issueManager.Get(issueId);
                    }
                    catch (Exception ex)
                    {
                        LogException(ex);
                    }

                    // Safety check
                    if (issue == null || issue.Entity.IsNew)
                    {
                        continue;
                    }

                    // Dupe check
                    string dupeKey = string.Format("{0}-{1}-{2}", recepient.Entity.Id, issueId, card.Id);

                    if (issuesEmailedToUsers.Contains(dupeKey))
                    {
                        continue;
                    }

                    var permissionManager = new PermissionsManager(recepient, _types, _permissionSets, _organizations, _issueManager.UserContext.Config.HelpDeskModeGroup, false);

                    if (!permissionManager.CanSeeItem(issue.Project, issue))
                    {
                        continue;
                    }

                    foreach (var comment in issue.Comments)
                    {
                        if (!permissionManager.CanSeeComment(issue, comment))
                        {
                            issue.Comments.RemoveAll(c => !permissionManager.CanSeeComment(issue, c));
                            break;
                        }
                    }

                    // Remove the reported by first entry!
                    if (issue.History.Count > 0 && issue.History[issue.History.Count - 1].Entity.AttributeChanged == ItemAttributeVisibility.ReportedBy)
                    {
                        issue.History.RemoveAt(issue.History.Count - 1);
                    }
                    issue.ChangeLog = _issueManager.GetChangeLog(issue, _issueManager.UserContext.User, recepient, lastCheckedLocal);

                    // Populate model for email template
                    if (card.CardData.Subscription.IndividualAlert)
                    {
                        individualIssues.Add(issue);
                    }

                    if (card.CardData.Subscription.Created && issue.Created.ToUtc(_issueManager.UserContext.User.TimeZone) >= lastChecked)
                    {
                        model.TheItemsCreated.Add(issue);
                    }
                    else
                    {
                        List <IssueAuditDto> allChanges = issue.History.FindAll(h => h.Entity.Created.ToUtc(_issueManager.UserContext.User.TimeZone) >= lastChecked);

                        List <IssueAuditDto> commentChanges = allChanges.FindAll(a => !a.Entity.IsCustom && a.Entity.AttributeChanged == ItemAttributeVisibility.AssociatedComments);

                        List <IssueAuditDto> nonCommentChanges = allChanges.FindAll(a => a.Entity.IsCustom || a.Entity.AttributeChanged != ItemAttributeVisibility.AssociatedComments);

                        // Add comments and updates
                        if (card.CardData.Subscription.Updated && nonCommentChanges.Count > 0 || card.CardData.Subscription.Commented && commentChanges.Count > 0 && issue.Comments.Count > 0)
                        {
                            model.TheItemsUpdated.Add(issue);
                        }

                        if (card.CardData.Subscription.Commented && commentChanges.Count > 0 && issue.Comments.Count > 0)
                        {
                            model.TheItemsCommented.Add(issue);
                        }
                    }

                    // Record the fact that we have processed this issue for this recepient (to prevent dupes)
                    issuesEmailedToUsers.Add(dupeKey);
                }

                model.CardTitle = string.Format("{0} {1}", card.Key, card.Title);

                model.CardKey = card.Key;

                model.CardDescription = card.Title;

                model.CardComment = card.CardData.Comment;

                model.CardUrl = string.Concat(model.GeminiUrl, "workspace/", card.Id, '/', card.Url);

                // Safety check!
                if (model.ChangeCount > 0)
                {
                    List <UserDto> subscribers = GetCardSubscribers(card, navigationCardsManager, userManager, recepient);

                    //if (!subscribers.Contains(recepient) && subscribers.Find(u => u.Entity.Id == recepient.Entity.Id) == null) subscribers.Insert(0, recepient);
                    if (card.CardData.Subscription.IndividualAlert)
                    {
                        foreach (var user in subscribers)
                        {
                            if (allOptOuts.Any(s => s.UserId == user.Entity.Id && s.CardId == card.Id && s.OptOutType == OptOutEmails.OptOutTypes.Alert))
                            {
                                continue;
                            }

                            foreach (var issue in individualIssues)
                            {
                                string individualDupeKey = string.Format("{0}-{1}", user.Entity.Id, issue.Entity.Id);

                                if (individualIssuesEmailedToUsers.Contains(individualDupeKey))
                                {
                                    continue;
                                }

                                if (user != recepient)
                                {
                                    var permissionManager = new PermissionsManager(user, _types, _permissionSets, _organizations, _issueManager.UserContext.Config.HelpDeskModeGroup, false);

                                    if (!permissionManager.CanSeeItem(issue.Project, issue))
                                    {
                                        continue;
                                    }

                                    issue.ChangeLog = _issueManager.GetChangeLog(issue, _issueManager.UserContext.User, user, lastCheckedLocal);
                                }

                                var indModel = new AlertTypeIndividualTemplateModel();

                                indModel.GeminiUrl = model.GeminiUrl;

                                indModel.LinkViewItem = NavigationHelper.GetIssueUrl(_issueManager.UserContext, issue.Entity.ProjectId, issue.EscapedProjectCode, issue.Entity.Id);

                                indModel.TheItem = issue;

                                indModel.TheRecipient = user;

                                indModel.Version = GeminiVersion.Version;

                                indModel.IsNewItem = model.TheItemsCreated.Contains(issue);

                                indModel.CardKey = model.CardKey;

                                indModel.CardDescription = model.CardDescription;

                                indModel.CardComment = model.CardComment;

                                indModel.CardUrl = model.CardUrl;

                                if (!indModel.IsNewItem && issue.ChangeLog.Count == 0)
                                {
                                    continue;
                                }

                                var template = alerts.FindTemplateForProject(
                                    indModel.IsNewItem ? AlertTemplateType.Created : AlertTemplateType.Updated,
                                    issue.Entity.ProjectId, GetLanguageId(user));

                                string html = alerts.GenerateHtml(template, indModel);

                                if (GeminiApp.GeminiLicense.IsFree)
                                {
                                    html = alerts.AddSignature(html);
                                }

                                // Send email
                                string log;

                                string subject = template.Options.Subject.HasValue() ? alerts.GenerateHtml(template, indModel, true) : string.Format("[{0}] {1} {2} ({3})", issue.IssueKey, issue.Type, model.TheItemsCreated.Contains(issue) ? "Created" : "Updated", issue.Title, issue.IsClosed ? "Closed" : string.Empty);

                                EmailHelper.Send(_issueManager.UserContext.Config, subject, html, user.Entity.Email, user.Fullname, true, out log);

                                individualIssuesEmailedToUsers.Add(individualDupeKey);
                            }
                        }
                    }
                    else
                    {
                        var cloneCreated = new List <IssueDto>(model.TheItemsCreated);

                        var cloneUpdated = new List <IssueDto>(model.TheItemsUpdated);

                        var cloneCommented = new List <IssueDto>(model.TheItemsCommented);

                        // Find email template to use (for this project or fall back to default template)

                        foreach (var user in subscribers)
                        {
                            AlertTemplate template = alerts.FindTemplateForProject(AlertTemplateType.AppNavAlerts, 0, GetLanguageId(user));


                            if (allOptOuts.Any(s => s.UserId == user.Entity.Id && s.CardId == card.Id && s.OptOutType == OptOutEmails.OptOutTypes.Alert))
                            {
                                continue;
                            }

                            model.TheItemsCreated = new List <IssueDto>(cloneCreated);

                            model.TheItemsUpdated = new List <IssueDto>(cloneUpdated);

                            model.TheItemsCommented = new List <IssueDto>(cloneCommented);

                            if (user != recepient)
                            {
                                var permissionManager = new PermissionsManager(user, _types, _permissionSets, _organizations, _issueManager.UserContext.Config.HelpDeskModeGroup, false);

                                model.TheItemsCreated.RemoveAll(i => !permissionManager.CanSeeItem(i.Project, i));

                                model.TheItemsUpdated.RemoveAll(i => !permissionManager.CanSeeItem(i.Project, i));

                                model.TheItemsCommented.RemoveAll(i => !permissionManager.CanSeeItem(i.Project, i));

                                foreach (var issue in model.TheItemsCreated.Concat(model.TheItemsUpdated).Concat(model.TheItemsCommented))
                                {
                                    issue.ChangeLog = _issueManager.GetChangeLog(issue, _issueManager.UserContext.User, user, lastCheckedLocal);
                                }
                            }

                            //model.TheItemsCreated.RemoveAll(i => i.ChangeLog.Count == 0);
                            model.TheItemsUpdated.RemoveAll(i => i.ChangeLog.Count == 0);

                            model.TheItemsCommented.RemoveAll(i => i.ChangeLog.Count == 0);

                            if (model.ChangeCount == 0)
                            {
                                continue;
                            }

                            // Generate email template
                            string html = alerts.GenerateHtml(template, model);

                            if (GeminiApp.GeminiLicense.IsFree)
                            {
                                html = alerts.AddSignature(html);
                            }

                            string subject = template.Options.Subject.HasValue() ? alerts.GenerateHtml(template, model, true) : string.Format("{0} {1}", card.Key, card.Title);

                            // Send email
                            string log;

                            EmailHelper.Send(_issueManager.UserContext.Config, subject, html, user.Entity.Email, user.Fullname, true, out log);
                        }
                    }
                }

                // Remove the alert notifications and update the database
                lock (card.CardData.Alerts)
                {
                    card.CardData.Alerts.RemoveAll(a => issuesToAlert.Contains(a));
                }

                card.CardData.AlertsLastSent = DateTime.UtcNow;

                refreshCache = true;

                navigationCardsManager.Update(card, false, false);
            }

            if (refreshCache)
            {
                navigationCardsManager.Cache.NavigationCards.Invalidate();
                var webNodes = GeminiApp.Container.Resolve <IWebNodes>();
                webNodes.AddDataOnAllNodesButMe(new WebNodeData()
                {
                    NodeGuid = GeminiApp.GUID, Key = "cache", Value = navigationCardsManager.Cache.NavigationCards.CacheKey
                });
            }
        }
        private List<UserDto> GetCardSubscribers(NavigationCard card, NavigationCardsManager cardManager, UserManager userManager, UserDto owner)
        {
            if (!owner.Entity.Active) return new List<UserDto>();

            Dictionary<int, UserDto> subscribers = new Dictionary<int, UserDto>();

            subscribers.Add(owner.Entity.Id, owner);

            foreach (var user in card.CardData.Subscription.Users)
            {
                var userDto = userManager.Get(user);

                if(user != owner.Entity.Id && userDto != null && userDto.Entity.Active) subscribers.Add(user, userDto);
            }

            var groupUsers = cardManager.GetUsersFromGroups(card, card.CardData.Subscription.Groups);

            foreach (var user in groupUsers)
            {
                if (!subscribers.ContainsKey(user.Entity.Id)) subscribers.Add(user.Entity.Id, user);
            }

            return new List<UserDto>(subscribers.Values);
        }
        private void ProcessAppNavCardAlerts()
        {
            var navigationCardsManager = new NavigationCardsManager(_issueManager);

            List<NavigationCard> cards = navigationCardsManager.GetPendingAlertCards();

            LogDebugMessage("Email templates loaded: " + _templates.Count);

            LogDebugMessage("Pending card alerts: " + cards.Count);

            // ? We need to store user id and issue id for every email we send out -- avoids dupes?
            List<string> issuesEmailedToUsers = new List<string>(50);

            List<string> individualIssuesEmailedToUsers = new List<string>(50);

            AlertsTemplateHelper alerts = new AlertsTemplateHelper(_templates, GetUrl(_issueManager));

            UserManager userManager = new UserManager(_issueManager);

            bool refreshCache = false;

            var allOptOuts = navigationCardsManager.GetOptOuts();

            foreach (NavigationCard card in cards)
            {
                List<IssueDto> individualIssues = new List<IssueDto>();

                // Safety checks
                if (!card.UserId.HasValue) continue;

                if (card.CardData.Alerts.Count == 0) continue;

                UserDto recepient = userManager.Get(card.UserId.Value);

                // Safety check
                if (!recepient.Entity.EmailMe || recepient.Entity.Email.IsEmpty()) continue;

                DateTime lastChecked = card.CardData.AlertsLastSent.HasValue ? card.CardData.AlertsLastSent.Value : card.Created;

                DateTime lastCheckedLocal = lastChecked.ToLocal(_issueManager.UserContext.User.TimeZone);

                AlertTypeAppNavAlertsTemplateModel model = new AlertTypeAppNavAlertsTemplateModel();

                model.TheRecipient = recepient;

                model.Version = GeminiVersion.Version;

                model.GeminiUrl = alerts.GeminiUrl;

                List<int> issuesToAlert = new List<int>(card.CardData.Alerts);

                foreach (int issueId in issuesToAlert)
                {
                    IssueDto issue = _issueManager.Get(issueId);

                    // Safety check
                    if (issue == null || issue.Entity.IsNew) continue;

                    // Dupe check
                    string dupeKey = string.Format("{0}-{1}-{2}", recepient.Entity.Id, issueId, card.Id);

                    if (issuesEmailedToUsers.Contains(dupeKey)) continue;

                    var permissionManager = new PermissionsManager(recepient, _types, _permissionSets, _organizations, _issueManager.UserContext.Config.HelpDeskModeGroup, false);

                    if (!permissionManager.CanSeeItem(issue.Project, issue)) continue;

                    foreach (var comment in issue.Comments)
                    {
                        if (!permissionManager.CanSeeComment(issue, comment))
                        {
                            issue.Comments.RemoveAll(c => !permissionManager.CanSeeComment(issue, c));
                            break;
                        }
                    }

                    issue.ChangeLog = _issueManager.GetChangeLog(issue, _issueManager.UserContext.User, recepient, lastCheckedLocal);

                    // Populate model for email template
                    if (card.CardData.Subscription.IndividualAlert) individualIssues.Add(issue);

                    if (card.CardData.Subscription.Created && issue.Created.ToUtc(_issueManager.UserContext.User.TimeZone) >= lastChecked)
                    {
                        model.TheItemsCreated.Add(issue);
                    }
                    else
                    {
                        List<IssueAuditDto> allChanges = issue.History.FindAll(h => h.Entity.Created.ToUtc(_issueManager.UserContext.User.TimeZone) >= lastChecked);

                        List<IssueAuditDto> commentChanges = allChanges.FindAll(a => !a.Entity.IsCustom && a.Entity.AttributeChanged == ItemAttributeVisibility.AssociatedComments);

                        List<IssueAuditDto> nonCommentChanges = allChanges.FindAll(a => a.Entity.IsCustom || a.Entity.AttributeChanged != ItemAttributeVisibility.AssociatedComments);

                        // Add comments and updates
                        if (card.CardData.Subscription.Updated && nonCommentChanges.Count > 0 || card.CardData.Subscription.Commented && commentChanges.Count > 0 && issue.Comments.Count > 0)
                        {
                            model.TheItemsUpdated.Add(issue);
                        }

                        if (card.CardData.Subscription.Commented && commentChanges.Count > 0 && issue.Comments.Count > 0)
                        {
                            model.TheItemsCommented.Add(issue);
                        }
                    }

                    // Record the fact that we have processed this issue for this recepient (to prevent dupes)
                    issuesEmailedToUsers.Add(dupeKey);
                }

                model.CardTitle = string.Format("{0} {1}", card.Key, card.Title);

                model.CardKey = card.Key;

                model.CardDescription = card.Title;

                model.CardComment = card.CardData.Comment;

                model.CardUrl = string.Concat(model.GeminiUrl, "workspace/", card.Id, '/', card.Url);

                // Safety check!
                if (model.ChangeCount > 0)
                {
                    List<UserDto> subscribers = GetCardSubscribers(card, navigationCardsManager, userManager, recepient);

                    //if (!subscribers.Contains(recepient) && subscribers.Find(u => u.Entity.Id == recepient.Entity.Id) == null) subscribers.Insert(0, recepient);
                    if (card.CardData.Subscription.IndividualAlert)
                    {
                        foreach (var user in subscribers)
                        {
                            if (allOptOuts.Any(s => s.UserId == user.Entity.Id && s.CardId == card.Id && s.OptOutType == OptOutEmails.OptOutTypes.Alert)) continue;

                            foreach (var issue in individualIssues)
                            {
                                string individualDupeKey = string.Format("{0}-{1}", user.Entity.Id, issue.Entity.Id);

                                if (individualIssuesEmailedToUsers.Contains(individualDupeKey)) continue;

                                if (user != recepient)
                                {
                                    var permissionManager = new PermissionsManager(user, _types, _permissionSets, _organizations, _issueManager.UserContext.Config.HelpDeskModeGroup, false);

                                    if (!permissionManager.CanSeeItem(issue.Project, issue)) continue;

                                    issue.ChangeLog = _issueManager.GetChangeLog(issue, _issueManager.UserContext.User, user, lastCheckedLocal);
                                }

                                var indModel = new AlertTypeIndividualTemplateModel();

                                indModel.GeminiUrl = model.GeminiUrl;

                                indModel.LinkViewItem = NavigationHelper.GetIssueUrl(_issueManager.UserContext, issue.Entity.ProjectId, issue.EscapedProjectCode, issue.Entity.Id);

                                indModel.TheItem = issue;

                                indModel.TheRecipient = user;

                                indModel.Version = GeminiVersion.Version;

                                indModel.IsNewItem = model.TheItemsCreated.Contains(issue);

                                indModel.CardKey = model.CardKey;

                                indModel.CardDescription = model.CardDescription;

                                indModel.CardComment = model.CardComment;

                                indModel.CardUrl = model.CardUrl;

                                if (!indModel.IsNewItem && issue.ChangeLog.Count == 0) continue;

                                var template = alerts.FindTemplateForProject(indModel.IsNewItem ? AlertTemplateType.Created : AlertTemplateType.Updated, issue.Entity.ProjectId);

                                string html = alerts.GenerateHtml(template, indModel);

                                if (GeminiApp.GeminiLicense.IsFree) html = alerts.AddSignature(html);

                                // Send email
                                string log;

                                string subject = template.Options.Subject.HasValue() ? alerts.GenerateHtml(template, indModel, true) : string.Format("[{0}] {1} {2} ({3})", issue.IssueKey, issue.Type, model.TheItemsCreated.Contains(issue) ? "Created" : "Updated", issue.Title, issue.IsClosed ? "Closed" : string.Empty);

                                EmailHelper.Send(_issueManager.UserContext.Config, subject, html, user.Entity.Email, user.Fullname, true, out log);

                                individualIssuesEmailedToUsers.Add(individualDupeKey);
                            }
                        }
                    }
                    else
                    {
                        var cloneCreated = new List<IssueDto>(model.TheItemsCreated);

                        var cloneUpdated = new List<IssueDto>(model.TheItemsUpdated);

                        var cloneCommented = new List<IssueDto>(model.TheItemsCommented);

                        // Find email template to use (for this project or fall back to default template)
                        AlertTemplate template = alerts.FindTemplateForProject(AlertTemplateType.AppNavAlerts, 0);

                        foreach (var user in subscribers)
                        {
                            if (allOptOuts.Any(s => s.UserId == user.Entity.Id && s.CardId == card.Id && s.OptOutType == OptOutEmails.OptOutTypes.Alert)) continue;

                            model.TheItemsCreated = new List<IssueDto>(cloneCreated);

                            model.TheItemsUpdated = new List<IssueDto>(cloneUpdated);

                            model.TheItemsCommented = new List<IssueDto>(cloneCommented);

                            if (user != recepient)
                            {
                                var permissionManager = new PermissionsManager(user, _types, _permissionSets, _organizations, _issueManager.UserContext.Config.HelpDeskModeGroup, false);

                                model.TheItemsCreated.RemoveAll(i => !permissionManager.CanSeeItem(i.Project, i));

                                model.TheItemsUpdated.RemoveAll(i => !permissionManager.CanSeeItem(i.Project, i));

                                model.TheItemsCommented.RemoveAll(i => !permissionManager.CanSeeItem(i.Project, i));

                                foreach (var issue in model.TheItemsCreated.Concat(model.TheItemsUpdated).Concat(model.TheItemsCommented))
                                {
                                    issue.ChangeLog = _issueManager.GetChangeLog(issue, _issueManager.UserContext.User, user, lastCheckedLocal);
                                }
                            }

                            //model.TheItemsCreated.RemoveAll(i => i.ChangeLog.Count == 0);
                            model.TheItemsUpdated.RemoveAll(i => i.ChangeLog.Count == 0);

                            model.TheItemsCommented.RemoveAll(i => i.ChangeLog.Count == 0);

                            if (model.ChangeCount == 0) continue;

                            // Generate email template
                            string html = alerts.GenerateHtml(template, model);

                            if (GeminiApp.GeminiLicense.IsFree) html = alerts.AddSignature(html);

                            string subject = template.Options.Subject.HasValue() ? alerts.GenerateHtml(template, model, true) : string.Format("{0} {1}", card.Key, card.Title);

                            // Send email
                            string log;

                            EmailHelper.Send(_issueManager.UserContext.Config, subject, html, user.Entity.Email, user.Fullname, true, out log);
                        }
                    }
                }

                // Remove the alert notifications and update the database
                lock (card.CardData.Alerts)
                {
                    card.CardData.Alerts.RemoveAll(a => issuesToAlert.Contains(a));
                }

                card.CardData.AlertsLastSent = DateTime.UtcNow;

                refreshCache = true;

                navigationCardsManager.Update(card, false, false);
            }

            if (refreshCache)
            {
                navigationCardsManager.Cache.NavigationCards.Invalidate();
                var webNodes = GeminiApp.Container.Resolve<IWebNodes>();
                webNodes.AddDataOnAllNodesButMe(new WebNodeData() { NodeGuid = GeminiApp.GUID, Key = "cache", Value = navigationCardsManager.Cache.NavigationCards.CacheKey });
            }
        }