diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 657cc38..02472ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -133,8 +133,7 @@ repos: .min(.js|.js.map)| static/(.*)/libs/ ) - #args: - # - --fix + args: [--fix, --color, --max-warnings, '0'] - repo: https://github.com/thibaudcolas/pre-commit-stylelint rev: v16.10.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 17942d3..dbe8fc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## \[0.3.1\] - 2024-10-23 + +### Added + +- Skill Extraction Checker +- FInished Skill Checker +- Notification System +- Activity Switcher +- Inactive Character Table + +### Changed + +- Skilltraining Bool now handled by geuthuris_active property +- JS ´No Active Training´ function to new is_active property +- Add Character init force update +- CSS Improvments +- Update only active characters + +### Fixed + +- ChararcterQueue is_active property not working correctly +- Last Update was not implemented to new system +- Skillfarm Overview error +- Missing Models in init +- Empty Queue not be deleted + ## \[0.3.0\] - 2024-10-21 ### Fixed diff --git a/README.md b/README.md index 77f2f28..081558e 100644 --- a/README.md +++ b/README.md @@ -32,12 +32,10 @@ ______________________________________________________________________ - Filtered Skill Queue - Filtered Skills - Highlight finished Skills + - No Active Training hint - Filter Skills for each Character -- No Active Training hint - -## Upcoming - -- Notififcation System +- Notification System +- Enable/Disable Characters ## Installation @@ -72,6 +70,11 @@ CELERYBEAT_SCHEDULE["skillfarm_update_all_skillfarm"] = { "task": "skillfarm.tasks.update_all_skillfarm", "schedule": crontab(minute=0, hour="*/1"), } + +CELERYBEAT_SCHEDULE["skillfarm_check_skillfarm_notifications"] = { + "task": "skillfarm.tasks.check_skillfarm_notifications", + "schedule": crontab(minute=0, hour="*/12"), +} ``` ### Step 4 - Migration to AA @@ -95,11 +98,12 @@ With the Following IDs you can set up the permissions for the Skillfarm The Following Settings can be setting up in the `local.py` -- SKILLFARM_APP_NAME: `"YOURNAME"` - Set the name of the APP - -- SKILLFARM_LOGGER_USE: `True / False` - Set to use own Logger File - -- SKILLFARM_STALE_STATUS: `3` - Set the Stale Status for Skillfarm Character in hours +| Setting Name | Descriptioon | Default | +| --------------------------------- | ------------------------------------------------------ | ------------- | +| `SKILLFARM_APP_NAME` | Set the name of the APP | `"Skillfarm"` | +| `SKILLFARM_LOGGER_USE` | Set to use own Logger File `True/False` | `False` | +| `SKILLFARM_STALE_STATUS` | Set the Stale Status for Skillfarm Character in hours | `3` | +| `SKILLFARM_NOTIFICATION_COOLDOWN` | Number of days to wait before resending a notification | `3` | If you set up SKILLFARM_LOGGER_USE to `True` you need to add the following code below: diff --git a/skillfarm/__init__.py b/skillfarm/__init__.py index 9c3ab0b..f7dc143 100644 --- a/skillfarm/__init__.py +++ b/skillfarm/__init__.py @@ -2,7 +2,7 @@ from skillfarm.app_settings import SKILLFARM_APP_NAME -__version__ = "0.3.0" +__version__ = "0.3.1" __title__ = "Skillfarm" USER_AGENT_TEXT = f"{SKILLFARM_APP_NAME} v{__version__}" diff --git a/skillfarm/api/character/skillfarm.py b/skillfarm/api/character/skillfarm.py index abc0782..a55b959 100644 --- a/skillfarm/api/character/skillfarm.py +++ b/skillfarm/api/character/skillfarm.py @@ -16,7 +16,8 @@ ) from skillfarm.hooks import get_extension_logger from skillfarm.models.characterskill import CharacterSkill -from skillfarm.models.skillfarmaudit import SkillFarmAudit, SkillFarmSetup +from skillfarm.models.skillfarmaudit import SkillFarmAudit +from skillfarm.models.skillfarmsetup import SkillFarmSetup from skillfarm.models.skillqueue import CharacterSkillqueueEntry logger = get_extension_logger(__name__) @@ -105,6 +106,7 @@ def get_character_skillfarm(request, character_id: int): ).select_related( "eve_type", ) + skillsqueue_filtered = skillsqueue.filter(character_filters) def process_skill_queue_entry(entry): @@ -147,6 +149,12 @@ def process_skill_queue_entry(entry): "skillqueuefiltered": skillqueuefiltered_data, "skillqueue": skillqueue_data, "skills": skills_data, + "is_active": any(entry.is_active for entry in skillsqueue), + "extraction_ready": ( + any(entry.is_exc_ready for entry in skills) + if skillset + else False + ), } ) @@ -160,7 +168,7 @@ def process_skill_queue_entry(entry): tags=self.tags, ) def get_character_admin(request): - chars_visible = SkillFarmAudit.objects.visible_characters(request.user) + chars_visible = SkillFarmAudit.objects.visible_eve_characters(request.user) if chars_visible is None: return 403, "Permission Denied" diff --git a/skillfarm/api/helpers.py b/skillfarm/api/helpers.py index 196084d..8aacdb1 100644 --- a/skillfarm/api/helpers.py +++ b/skillfarm/api/helpers.py @@ -3,7 +3,7 @@ from allianceauth.eveonline.models import EveCharacter from skillfarm.hooks import get_extension_logger -from skillfarm.models import SkillFarmAudit +from skillfarm.models.skillfarmaudit import SkillFarmAudit logger = get_extension_logger(__name__) diff --git a/skillfarm/api/schema.py b/skillfarm/api/schema.py index 336efe6..7ac6b1d 100644 --- a/skillfarm/api/schema.py +++ b/skillfarm/api/schema.py @@ -38,6 +38,8 @@ class SkillFarm(Schema): skillset: Any skills: Any skill_names: Any + is_active: Optional[bool] + extraction_ready: Optional[bool] class SkillFarmFilter(Schema): diff --git a/skillfarm/app_settings.py b/skillfarm/app_settings.py index a0c5cbf..4a54a9e 100644 --- a/skillfarm/app_settings.py +++ b/skillfarm/app_settings.py @@ -43,3 +43,6 @@ # Update Period for Skillfarm in Hours SKILLFARM_STALE_STATUS = clean_setting("SKILLFARM_STALE_STATUS", 3) + +# Set Notification Cooldown in Days +SKILLFARM_NOTIFICATION_COOLDOWN = clean_setting("SKILLFARM_NOTIFICATION_COOLDOWN", 3) diff --git a/skillfarm/managers/characterskill.py b/skillfarm/managers/characterskill.py index d35156e..fae22e5 100644 --- a/skillfarm/managers/characterskill.py +++ b/skillfarm/managers/characterskill.py @@ -10,7 +10,7 @@ from skillfarm.task_helper import NotModifiedError, etag_results if TYPE_CHECKING: - from skillfarm.models import SkillFarmAudit + from skillfarm.models.skillfarmaudit import SkillFarmAudit logger = get_extension_logger(__name__) diff --git a/skillfarm/managers/skillqueue.py b/skillfarm/managers/skillqueue.py index 3a7f2ba..d2900c0 100644 --- a/skillfarm/managers/skillqueue.py +++ b/skillfarm/managers/skillqueue.py @@ -7,7 +7,7 @@ from skillfarm.task_helper import NotModifiedError, etag_results if TYPE_CHECKING: - from skillfarm.models import SkillFarmAudit + from skillfarm.models.skillfarmaudit import SkillFarmAudit from skillfarm.hooks import get_extension_logger @@ -21,7 +21,7 @@ def update_or_create_esi( """Update or create skills queue for a character from ESI.""" skillqueue = self._fetch_data_from_esi(character, force_refresh=force_refresh) - if not skillqueue: + if skillqueue is None: return False entries = [] @@ -52,7 +52,7 @@ def _fetch_data_from_esi( ) -> list[dict]: logger.debug("%s: Fetching skill queue from ESI", character) - skillqueue = [] + skillqueue = None token = character.get_token() try: skillqueue_data = esi.client.Skills.get_characters_character_id_skillqueue( diff --git a/skillfarm/migrations/0002_skillfarmaudit_last_notification_and_more.py b/skillfarm/migrations/0002_skillfarmaudit_last_notification_and_more.py new file mode 100644 index 0000000..03d9723 --- /dev/null +++ b/skillfarm/migrations/0002_skillfarmaudit_last_notification_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.11 on 2024-10-22 21:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("skillfarm", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="skillfarmaudit", + name="last_notification", + field=models.DateTimeField(blank=True, default=None, null=True), + ), + migrations.AddField( + model_name="skillfarmaudit", + name="notification_sent", + field=models.BooleanField(default=False), + ), + migrations.DeleteModel( + name="SkillFarmNotification", + ), + ] diff --git a/skillfarm/models/__init__.py b/skillfarm/models/__init__.py index a0148b4..5df9d6b 100644 --- a/skillfarm/models/__init__.py +++ b/skillfarm/models/__init__.py @@ -1,4 +1,5 @@ from .characterskill import CharacterSkill from .general import General from .skillfarmaudit import SkillFarmAudit +from .skillfarmsetup import SkillFarmSetup from .skillqueue import CharacterSkillqueueEntry diff --git a/skillfarm/models/characterskill.py b/skillfarm/models/characterskill.py index f9b2f5b..1bb754b 100644 --- a/skillfarm/models/characterskill.py +++ b/skillfarm/models/characterskill.py @@ -33,3 +33,24 @@ class Meta: def __str__(self) -> str: return f"{self.character}-{self.eve_type.name}" + + @property + def is_exc_ready(self) -> bool: + """Check if skill extraction is ready.""" + # pylint: disable=import-outside-toplevel + from skillfarm.models.skillfarmsetup import SkillFarmSetup + + try: + character = SkillFarmSetup.objects.get(character=self.character) + except SkillFarmSetup.DoesNotExist: + character = None + + if character and character.skillset is not None: + skills = CharacterSkill.objects.filter( + character=self.character, + eve_type__name__in=character.skillset, + ) + for skill in skills: + if skill.trained_skill_level == 5: + return True + return False diff --git a/skillfarm/models/skillfarmaudit.py b/skillfarm/models/skillfarmaudit.py index 394b26c..43e6122 100644 --- a/skillfarm/models/skillfarmaudit.py +++ b/skillfarm/models/skillfarmaudit.py @@ -26,9 +26,10 @@ class SkillFarmAudit(models.Model): ) notification = models.BooleanField(default=False) + notification_sent = models.BooleanField(default=False) + last_notification = models.DateTimeField(null=True, default=None, blank=True) last_update_skills = models.DateTimeField(null=True, default=None, blank=True) - last_update_skillqueue = models.DateTimeField(null=True, default=None, blank=True) objects = SkillFarmManager() @@ -59,6 +60,30 @@ def get_token(self) -> Token: return token return False + def finished_skills(self) -> list[str]: + """Check if a character has a skill finished from filter.""" + # pylint: disable=import-outside-toplevel + from skillfarm.models.characterskill import CharacterSkill + from skillfarm.models.skillfarmsetup import SkillFarmSetup + + skill_names = [] + try: + character = SkillFarmSetup.objects.get(character=self) + except SkillFarmSetup.DoesNotExist: + character = None + + if character and character.skillset is not None: + skills = CharacterSkill.objects.filter( + character=self, + eve_type__name__in=character.skillset, + ) + + for skill in skills: + if skill.trained_skill_level == 5: + skill_names.append(skill.eve_type.name) + return skill_names + + @property def is_active(self): time_ref = timezone.now() - datetime.timedelta( days=app_settings.SKILLFARM_CHAR_MAX_INACTIVE_DAYS @@ -77,45 +102,17 @@ def is_active(self): except Exception: # pylint: disable=broad-exception-caught return False + @property + def is_cooldown(self) -> bool: + """Check if a character has a notification cooldown.""" + if self.last_notification is None: + return False -class SkillFarmSetup(models.Model): - id = models.AutoField(primary_key=True) - - character = models.OneToOneField( - SkillFarmAudit, on_delete=models.CASCADE, related_name="skillfarm_setup" - ) - - skillset = models.JSONField(default=dict, blank=True, null=True) - - def __str__(self): - return f"{self.skillset}'s Skill Setup" - - objects = SkillFarmManager() - - class Meta: - default_permissions = () - - -class SkillFarmNotification(models.Model): - """Skillfarm Notification model for app""" - - id = models.AutoField(primary_key=True) - - character = models.OneToOneField( - SkillFarmAudit, on_delete=models.CASCADE, related_name="skillfarm_notification" - ) - - message = models.TextField() - - timestamp = models.DateTimeField(auto_now_add=True) - - objects = SkillFarmManager() - - def __str__(self): - return f"{self.character.character.character_name}'s Notification" - - class Meta: - default_permissions = () - ordering = ["-timestamp"] - verbose_name = "Skillfarm Notification" - verbose_name_plural = "Skillfarm Notifications" + if self.last_notification < timezone.now() - datetime.timedelta( + days=app_settings.SKILLFARM_NOTIFICATION_COOLDOWN + ): + self.last_notification = None + self.notification_sent = False + self.save() + return False + return True diff --git a/skillfarm/models/skillfarmsetup.py b/skillfarm/models/skillfarmsetup.py new file mode 100644 index 0000000..677ead7 --- /dev/null +++ b/skillfarm/models/skillfarmsetup.py @@ -0,0 +1,26 @@ +"""Models for Skillfarm.""" + +from django.db import models + +from skillfarm.hooks import get_extension_logger +from skillfarm.managers.skillfarmaudit import SkillFarmManager + +logger = get_extension_logger(__name__) + + +class SkillFarmSetup(models.Model): + id = models.AutoField(primary_key=True) + + character = models.OneToOneField( + "SkillFarmAudit", on_delete=models.CASCADE, related_name="skillfarm_setup" + ) + + skillset = models.JSONField(default=dict, blank=True, null=True) + + def __str__(self): + return f"{self.skillset}'s Skill Setup" + + objects = SkillFarmManager() + + class Meta: + default_permissions = () diff --git a/skillfarm/models/skillqueue.py b/skillfarm/models/skillqueue.py index aff0ed3..8429b33 100644 --- a/skillfarm/models/skillqueue.py +++ b/skillfarm/models/skillqueue.py @@ -41,4 +41,4 @@ def __str__(self) -> str: @property def is_active(self) -> bool: """Returns true when this skill is currently being trained""" - return bool(self.finish_date) and self.queue_position == 0 + return bool(self.finish_date) and self.finish_date > self.start_date diff --git a/skillfarm/static/skillfarm/js/skillfarm.js b/skillfarm/static/skillfarm/js/skillfarm.js index 4b91e3d..8a68775 100644 --- a/skillfarm/static/skillfarm/js/skillfarm.js +++ b/skillfarm/static/skillfarm/js/skillfarm.js @@ -5,17 +5,25 @@ document.addEventListener('DOMContentLoaded', function() { var csrfToken = skillfarmSettings.csrfToken; var urlAlarm = skillfarmSettings.switchAlarmUrl; var urlSkillset = skillfarmSettings.switchSkillSetpUrl; + var urlStatus = skillfarmSettings.switchStatusUrl; var urlDeleteChar = skillfarmSettings.deleteCharUrl; var url = skillfarmSettings.skillfarmUrl; var characterPk = skillfarmSettings.characterPk; // Translations var switchAlarmText = skillfarmSettings.switchAlarmConfirmText; var switchAlarm = skillfarmSettings.switchAlarmText; + var switchSkillset = skillfarmSettings.switchSkillsetText; + + var switchStatus = skillfarmSettings.switchStatusText; + var switchStatusText = skillfarmSettings.switchStatusConfirmText; + var deleteChar = skillfarmSettings.deleteCharText; var deleteCharConfirm = skillfarmSettings.deleteCharConfirmText; + var alarmActivated = skillfarmSettings.alarmActivatedText; var alarmDeactivated = skillfarmSettings.alarmDeactivatedText; + var notupdated = skillfarmSettings.notUpdatedText; var noActiveTraining = skillfarmSettings.noActiveTrainingText; @@ -29,6 +37,11 @@ document.addEventListener('DOMContentLoaded', function() { .replace('1337', characterId); } + function switchStatusUrl(characterId) { + return urlStatus + .replace('1337', characterId); + } + function deleteCharUrl(characterId) { return urlDeleteChar .replace('1337', characterId); @@ -64,6 +77,18 @@ document.addEventListener('DOMContentLoaded', function() { } }); + // Initialize DataTable + var inactive = $('#skillfarm-inactive').DataTable({ + order: [[0, 'asc']], + pageLength: 50, + columnDefs: [ + { 'orderable': false, 'targets': 'no-sort' } + ], + createdRow: function(row, data, dataIndex) { + $('td:eq(5)', row).addClass('text-end'); + } + }); + function totalProgressbar (skillqueue) { var skillJson = JSON.parse(skillqueue); var totalSP = 0; @@ -83,35 +108,45 @@ document.addEventListener('DOMContentLoaded', function() { var skillqueueJson = JSON.parse(skillqueue); var skillsJson = JSON.parse(skills); + // If there are no skills, return the total progress bar if (skillsJson.length === 0) { return totalProgressbar(skillqueue); } - // Extract unique skill names without levels and optional hyphen from the skillqueue - var uniqueSkillNames = [...new Set(skillqueueJson.map(skill => skill.skill.replace(/\s[IV-]*$/, '')))]; + // Calculate the progress percentage for each skill individually + var totalProgressPercent = 0; + var skillCount = skillqueueJson.length; + var currentDate = new Date(); - // Find the highest end_sp for each unique skill name - var totalEndSp = uniqueSkillNames.reduce((total, skillName) => { - var highestSkill = skillqueueJson - .filter(skill => skill.skill.replace(/\s[IV-]*$/, '') === skillName) - .reduce((prev, current) => (prev.end_sp > current.end_sp) ? prev : current); - return total + highestSkill.end_sp; - }, 0); + skillqueueJson.forEach(skill => { + var startDate = new Date(skill.start_date); + var endDate = new Date(skill.finish_date); + var skillDuration = endDate - startDate; + var skillTrainedDuration = Math.min(currentDate - startDate, skillDuration); - // Sum the skillpoints of all skills in the skills array - var totalSkillpoints = skillsJson.reduce((total, skill) => total + skill.skillpoints, 0); + var skillProgressPercent = (skillTrainedDuration / skillDuration) * 100; - // Calculate the progress percentage - var progressPercent = (totalSkillpoints / totalEndSp) * 100; + // Ensure the progress percentage is between 0 and 100 + if (!isFinite(skillProgressPercent) || skillProgressPercent < 0) { + skillProgressPercent = 0; + } else if (skillProgressPercent > 100) { + skillProgressPercent = 100; + } - // Handle cases where progressPercent is Infinity or exceeds 100% - if (!isFinite(progressPercent)) { - progressPercent = 100; - } else if (progressPercent > 100) { - progressPercent = 100; + totalProgressPercent += skillProgressPercent; + }); + + // Calculate the average progress percentage + var averageProgressPercent = totalProgressPercent / skillCount; + + // Ensure the final progress percentage is between 0 and 100 + if (!isFinite(averageProgressPercent) || averageProgressPercent < 0) { + averageProgressPercent = 0; + } else if (averageProgressPercent > 100) { + averageProgressPercent = 100; } - return progressPercent; + return averageProgressPercent; } function hasActiveTraining(skillqueueJson) { @@ -143,7 +178,8 @@ document.addEventListener('DOMContentLoaded', function() { const skillqueueFilteredJson = JSON.stringify(character.skillqueuefiltered); const skillqueueJson = JSON.stringify(character.skillqueue); const skillsJson = JSON.stringify(character.skills); - const hasSkillLevel5 = JSON.parse(skillsJson).some(skill => skill.level === 5); + + const is_extraction_ready = character.extraction_ready; const progressBarHtml = `
@@ -161,13 +197,13 @@ document.addEventListener('DOMContentLoaded', function() { const skillCell = `
- ${hasActiveTraining(skillqueueJson) ? progressBarHtml : noActiveTrainingHtml} + ${character.is_active ? progressBarHtml : noActiveTrainingHtml}
`; - const skillStatus = JSON.parse(skillsJson).some(skill => skill.level === 5) ? '' : ''; - const skillStatusWarning = JSON.parse(skillsJson).some(skill => skill.level === 5) ? 'bg-warning' : ''; + const skillStatus = is_extraction_ready ? '' : ''; + const skillStatusWarning = is_extraction_ready ? 'bg-warning' : ''; const skillListHtml = ` +
+ ${csrfToken} + + + +
+
${csrfToken} @@ -218,7 +263,13 @@ document.addEventListener('DOMContentLoaded', function() { `; row.push(characterCell, skillCell, skillListHtml, lastUpdatedCell, filterstatusCell, actionsCell); - table.row.add(row).draw(); + + // Add row to the table + if (character.active){ + table.row.add(row).draw(); + } else { + inactive.row.add(row).draw(); + } }); }); @@ -486,6 +537,10 @@ function showskillQueueModal(button) { progressPercent = 0; } + if (!skill.is_active && !skill.start_date) { + progressPercent = 0; + } + const tr = document.createElement('tr'); tr.innerHTML = ` ${skill.skill} @@ -495,11 +550,11 @@ function showskillQueueModal(button) {
${progressPercent.toFixed(0)}%
- - ${skill.start_date ? new Date(skill.start_date).toLocaleString() : 'No Active Training'} + + ${skill.start_date ? new Date(skill.start_date).toLocaleString() : '-'} - - ${skill.finish_date ? new Date(skill.finish_date).toLocaleString() : 'No Active Training'} + + ${skill.start_date ? new Date(skill.finish_date).toLocaleString() : '-'} `; skillQueueTbody.appendChild(tr); diff --git a/skillfarm/tasks.py b/skillfarm/tasks.py index ceae2dd..c9a170e 100644 --- a/skillfarm/tasks.py +++ b/skillfarm/tasks.py @@ -7,8 +7,11 @@ from celery import shared_task from django.utils import timezone +from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ +from allianceauth.authentication.models import CharacterOwnership +from allianceauth.notifications import notify from allianceauth.services.tasks import QueueOnce from skillfarm.app_settings import SKILLFARM_STALE_STATUS @@ -23,7 +26,7 @@ @shared_task @when_esi_is_available def update_all_skillfarm(runs: int = 0): - characters = SkillFarmAudit.objects.select_related("character").all() + characters = SkillFarmAudit.objects.select_related("character").filter(active=True) for character in characters: update_character_skillfarm.apply_async(args=[character.character.character_id]) runs = runs + 1 @@ -32,7 +35,7 @@ def update_all_skillfarm(runs: int = 0): @shared_task(bind=True, base=QueueOnce) def update_character_skillfarm( - self, character_id, force_refresh=True + self, character_id, force_refresh=False ): # pylint: disable=unused-argument character = SkillFarmAudit.objects.get(character__character_id=character_id) skip_date = timezone.now() - datetime.timedelta(hours=SKILLFARM_STALE_STATUS) @@ -63,9 +66,11 @@ def update_char_skillqueue( self, character_id, force_refresh=False, chain=[] ): # pylint: disable=unused-argument, dangerous-default-value character = SkillFarmAudit.objects.get(character__character_id=character_id) - return CharacterSkillqueueEntry.objects.update_or_create_esi( + CharacterSkillqueueEntry.objects.update_or_create_esi( character, force_refresh=force_refresh ) + character.last_update_skillqueue = timezone.now() + character.save() @shared_task( @@ -79,6 +84,83 @@ def update_char_skills( self, character_id, force_refresh=False, chain=[] ): # pylint: disable=unused-argument, dangerous-default-value character = SkillFarmAudit.objects.get(character__character_id=character_id) - return CharacterSkill.objects.update_or_create_esi( - character, force_refresh=force_refresh - ) + CharacterSkill.objects.update_or_create_esi(character, force_refresh=force_refresh) + character.last_update_skills = timezone.now() + character.save() + + +# pylint: disable=unused-argument, too-many-locals +@shared_task(bind=True, base=QueueOnce) +def check_skillfarm_notifications(self, runs: int = 0): + characters = SkillFarmAudit.objects.select_related("character").all() + owner_ids = {} + warnings = {} + notified_characters = [] + + for character in characters: + skill_names = character.finished_skills() + + if skill_names and character.notification and not character.is_cooldown: + character_id = character.character.character_id + + # Determine if the character_id is part of any main character's alts + main_id = None + for main, alts in owner_ids.items(): + if character_id in alts: + main_id = main + break + + if main_id is None: + try: + owner = CharacterOwnership.objects.get( + character__character_id=character_id + ) + main = owner.user.profile.main_character + alts = main.character_ownership.user.character_ownerships.all() + + owner_ids[main.character_id] = alts.values_list( + "character__character_id", flat=True + ) + + main_id = main.character_id + except CharacterOwnership.DoesNotExist: + continue + except AttributeError: + continue + + msg = _("%(charname)s: %(skillname)s") % { + "charname": character.character.character_name, + "skillname": ", ".join(skill_names), + } + + if main_id not in warnings: + warnings[main_id] = [] + + warnings[main_id].append(msg) + notified_characters.append(character) + + if warnings: + for main_id, warnings in warnings.items(): + msg = "\n".join(warnings) + owner = CharacterOwnership.objects.get(character__character_id=main_id) + title = _("Skillfarm Notifications") + full_message = format_html( + "Following Skills have finished training: \n{}", msg + ) + notify( + title=title, + message=full_message, + user=owner.user, + level="warning", + ) + + # Set notification_sent to True for all characters that were notified + for character in notified_characters: + if character.character.character_id in owner_ids[main_id]: + character.notification_sent = True + character.last_notification = timezone.now() + character.save() + + runs = runs + 1 + + logger.info("Queued %s Skillfarm Notifications", runs) diff --git a/skillfarm/templates/skillfarm/partials/table/inactive.html b/skillfarm/templates/skillfarm/partials/table/inactive.html new file mode 100644 index 0000000..8194c66 --- /dev/null +++ b/skillfarm/templates/skillfarm/partials/table/inactive.html @@ -0,0 +1,37 @@ +{% load i18n %} +{% load humanize %} +{% load static %} + + + + + + + + + + + + +
{% translate "Character" %}{% translate "Progress" %}{% translate "Skill Info" %}{% translate "Last Updated" %}{% translate "Filter" %}{% translate "Actions" %}
+ + + diff --git a/skillfarm/templates/skillfarm/skillfarm.html b/skillfarm/templates/skillfarm/skillfarm.html index c8e1e2e..d3bafdd 100644 --- a/skillfarm/templates/skillfarm/skillfarm.html +++ b/skillfarm/templates/skillfarm/skillfarm.html @@ -7,7 +7,7 @@ {% block page_topic %}

{% translate "Skillfarm Details" %}

{% endblock page_topic %} {% block skillfarm_block %} -
+

{% translate "Skillfarm Details" %}

+
- + Notification ON/OFF -
- -

-
{% include 'skillfarm/partials/table/skillfarm.html' %}
+
+
+

{% translate "Inactive Characters" %}

+
+
+
+ {% trans "Characters here are excluded from beeing updated" %} +
+
+ {% include 'skillfarm/partials/table/inactive.html' %} +
+
+
+ {% include 'skillfarm/partials/modals/skillqueue.html' %} {% include 'skillfarm/partials/modals/skillfilter.html' %} @@ -55,6 +66,7 @@

{% translate "Skillfarm Details" %}

skillfarmUrl: skillfarmUrl, switchSkillSetpUrl: '{% url "skillfarm:skillset" character_id=1337 %}', switchAlarmUrl: '{% url "skillfarm:switch_alarm" character_id=1337 %}', + switchStatusUrl: '{% url "skillfarm:switch_activity" character_id=1337 %}', deleteCharUrl: '{% url "skillfarm:remove_char" character_id=1337 %}', csrfToken: '{% csrf_token %}', characterPk: '{{ character_pk }}', @@ -62,6 +74,8 @@

{% translate "Skillfarm Details" %}

// Translations switchAlarmConfirmText: '{% translate "Are you sure to Switch Alarm" %}', switchAlarmText: '{% translate "Toggle Alarm" %}', + switchStatusText: '{% translate "Toggle Activity" %}', + switchStatusConfirmText: '{% translate "Are you sure to Switch Activity" %}', notUpdatedText: '{% translate "Not Updated Yet" %}', noActiveTrainingText: '{% translate "No Active Training" %}', diff --git a/skillfarm/urls.py b/skillfarm/urls.py index be9a4fb..5de0e9c 100644 --- a/skillfarm/urls.py +++ b/skillfarm/urls.py @@ -26,6 +26,11 @@ views.switch_alarm, name="switch_alarm", ), + path( + "switch_activity//", + views.switch_activity, + name="switch_activity", + ), path( "skillset//", views.skillset, diff --git a/skillfarm/views.py b/skillfarm/views.py index 8e5ad56..34973c4 100644 --- a/skillfarm/views.py +++ b/skillfarm/views.py @@ -16,7 +16,8 @@ from skillfarm.api.helpers import get_alts_queryset, get_character from skillfarm.hooks import get_extension_logger -from skillfarm.models.skillfarmaudit import SkillFarmAudit, SkillFarmSetup +from skillfarm.models.skillfarmaudit import SkillFarmAudit +from skillfarm.models.skillfarmsetup import SkillFarmSetup from skillfarm.tasks import update_character_skillfarm logger = get_extension_logger(__name__) @@ -85,7 +86,9 @@ def add_char(request, token): char, _ = SkillFarmAudit.objects.update_or_create( character=character, defaults={"character": character} ) - update_character_skillfarm.apply_async(args=[char.character.character_id]) + update_character_skillfarm.apply_async( + args=[char.character.character_id], kwargs={"force_refresh": True} + ) except SkillFarmAudit.DoesNotExist: msg = trans("Character not found") messages.error(request, msg) @@ -171,6 +174,39 @@ def switch_alarm(request, character_id: list): return redirect("skillfarm:skillfarm", character_pk=character_pk) +@login_required +@permission_required("skillfarm.basic_access") +@require_POST +def switch_activity(request, character_id: list): + # Retrieve character_pk from GET parameters + character_pk = int(request.POST.get("character_pk", 0)) + + # Check Permission + perm, _ = get_character(request, character_id) + + if not perm: + msg = trans("Permission Denied") + messages.error(request, msg) + return redirect("skillfarm:skillfarm", character_pk=character_pk) + + try: + character = SkillFarmAudit.objects.get(character__character_id=character_id) + character.active = not character.active + character.save() + except SkillFarmAudit.DoesNotExist: + msg = trans("Character/s not found") + messages.error(request, msg) + return redirect("skillfarm:skillfarm", character_pk=character_pk) + + msg = trans("{character_name} successfully switched {mode}").format( + character_name=character.character.character_name, + mode="Active" if character.active else "Inactive", + ) + messages.success(request, msg) + + return redirect("skillfarm:skillfarm", character_pk=character_pk) + + @login_required @permission_required("skillfarm.basic_access") @require_POST