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 = `
{% translate "Character" %} | +{% translate "Progress" %} | +{% translate "Skill Info" %} | +{% translate "Last Updated" %} | +{% translate "Filter" %} | +{% translate "Actions" %} | + + + + +
---|