NovaBryan/EVE_Dump
Log | Files | 📦 下载最新源码 (.zip) | Refs | README

job.py (27541B)


      1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\packages\jobboard\client\features\campaigns\job.py
      2 import caching
      3 import eveformat
      4 import eveicon
      5 import gametime
      6 import mathext
      7 import uthread2
      8 from brennivin.itertoolsext import Bundle
      9 from campaigns.client.v1.data_model.contribution_method import MissingParameterConfiguration
     10 from campaigns.client.v1.data_model.objective import FutureObjective, ObjectiveState
     11 from campaigns.client.errors import ObjectiveStateError, ParticipantStateError, CampaignForbiddenError, CampaignRequestError
     12 from campaigns.client.parameter_types.const import ParameterType
     13 from campaigns.client.parameter_types.parameter_controller import get_parameter_class
     14 from campaigns.client.v1.signals import on_objective_contributed_notice, on_campaigns_objective_stats_updated, on_commited_state_changed
     15 from campaigns.client.ui_controllers.campaign_controller import CampaignController
     16 from campaigns.client.validation import check_can_commit, check_can_resign
     17 from carbon.common.script.util.format import FmtDate
     18 from carbonui import ButtonVariant
     19 from carbonui.control.contextMenu.menuData import MenuData
     20 from datetimeutils import datetime_to_filetime
     21 from eve.client.script.ui import eveColor
     22 from eve.client.script.ui.const.eveIconConst import CORP_ICONS
     23 from eve.client.script.ui.control.eveIcon import Icon
     24 from jobboard.client.features.campaigns.card import CampaignJobCard, FutureCampaignJobCard
     25 from jobboard.client.features.campaigns.icons_by_corp_id import FLAT_ICONS_BY_CORP_ID
     26 from jobboard.client.features.campaigns.info_panel_entry import CampaignInfoPanelEntry
     27 from jobboard.client.features.campaigns.job_dynamic_data import DynamicCampaignJobData
     28 from jobboard.client.features.campaigns.job_restrictions import get_restricted_message_id
     29 from jobboard.client.features.campaigns.list_entry import CampaignListEntry, CampaignsRewardListEntry
     30 from jobboard.client.features.campaigns.page import CampaignObjectivePage
     31 from jobboard.client.features.campaigns.qa_tools import get_qa_menu_for_job
     32 from jobboard.client.job import BaseJob
     33 from jobboard.client.job_board_signals import on_job_state_changed
     34 from jobboard.client.util import check_content_tags, get_closest_solarsystem_in_locations_from
     35 from localization import GetByLabel, GetByMessageID
     36 from metadata.common.content_tags.const import CAREER_PATH_TO_CONTENT_TAG_ID
     37 from player_messaging.client.ui_message import message_player
     38 from utillib import KeyVal
     39 CAMPAIGN_OBJECTIVE_CHAIN_ID = 145
     40 
     41 class BaseCampaignJob(BaseJob):
     42     CARD_CLASS = CampaignJobCard
     43     PAGE_CLASS = CampaignObjectivePage
     44     INFO_PANEL_ENTRY_CLASS = CampaignInfoPanelEntry
     45     LIST_ENTRY_CLASS = CampaignListEntry
     46     REWARD_LIST_ENTRY_CLASS = CampaignsRewardListEntry
     47 
     48     def __init__(self, job_id, provider, project):
     49         self._repository = None
     50         self._campaign_controller = None
     51         self._objective = project
     52         self._objectives_context = None
     53         content_id = self.content_id
     54         super(BaseCampaignJob, self).__init__(job_id, content_id, provider)
     55 
     56     @property
     57     def campaign_id(self):
     58         return self._objective.campaign_id
     59 
     60     @property
     61     def objective(self):
     62         return self._objective
     63 
     64     @property
     65     def repository(self):
     66         if self._repository is None:
     67             from campaigns.client.api import get_campaigns_repository_v1
     68             self._repository = get_campaigns_repository_v1()
     69         return self._repository
     70 
     71     @property
     72     def campaign_controller(self):
     73         if self._campaign_controller is None:
     74             campaign = self.repository.get_campaign(self.campaign_id)
     75             controller = CampaignController(campaign, self.repository)
     76             from campaigns.client.api import get_campaigns_repository_v1
     77             self._campaign_controller = controller
     78         return self._campaign_controller
     79 
     80     @property
     81     def content_id(self):
     82         return None
     83 
     84     @property
     85     def is_active(self):
     86         return False
     87 
     88     @property
     89     def icon_path(self):
     90         return None
     91 
     92     @property
     93     def is_future_objective(self):
     94         return False
     95 
     96     def _get_contribution_definition_for_contribution_method(self, config_method_identifier):
     97         campaign_data = self.campaign_controller.campaign
     98         version = campaign_data.version
     99         definitions = self.repository.get_contribution_method_definitions(version.definition_semantic_verison)
    100         for definition in definitions:
    101             if definition.contribution_method == config_method_identifier:
    102                 return definition
    103 
    104     @property
    105     def contribution_method_name(self):
    106         return GetByMessageID(self.contribution_method_definition.title)
    107 
    108     @property
    109     def contribution_method_progress_description_text(self):
    110         return GetByMessageID(self.contribution_method_definition.progress_description)
    111 
    112     @property
    113     def contribution_method_icon(self):
    114         icon_data = eveicon.get(self.contribution_method_definition.icon or '', None)
    115         return icon_data
    116 
    117     @property
    118     def restrictions_text(self):
    119         return ''
    120 
    121     def on_system_change(self):
    122         pass
    123 
    124     @property
    125     def objective_background(self):
    126         campaign_controller = self.campaign_controller
    127         return campaign_controller.textures.objective_background
    128 
    129     @property
    130     def campaign_icon_path(self):
    131         return self.campaign_controller.textures.icon_path
    132 
    133     @property
    134     def card_icon_path(self):
    135         return self.campaign_icon_path
    136 
    137     @property
    138     def is_committed(self):
    139         return False
    140 
    141     @property
    142     def total_progress_percentage(self):
    143         return 0
    144 
    145     @property
    146     def personal_progress_percentage(self):
    147         return 0
    148 
    149     @property
    150     def has_remaining_personal_progress(self):
    151         return True
    152 
    153     def update(self, objective = None):
    154         if objective is not None:
    155             self._objective = objective
    156         return super(BaseCampaignJob, self).update(objective)
    157 
    158     @property
    159     def total_remaining_progress(self):
    160         return None
    161 
    162 
    163 class CampaignJob(BaseCampaignJob):
    164     CARD_CLASS = CampaignJobCard
    165     PAGE_CLASS = CampaignObjectivePage
    166 
    167     def __init__(self, job_id, provider, project):
    168         self._closest_solar_system_id = None
    169         self._dynamic_data = None
    170         super(CampaignJob, self).__init__(job_id, provider, project)
    171 
    172     @property
    173     def dynamic_data(self):
    174         if self._dynamic_data is None:
    175             self._dynamic_data = DynamicCampaignJobData(self.repository, self.campaign_id, self.objective_id, self._isk_reward, self._lp_reward)
    176         return self._dynamic_data
    177 
    178     @property
    179     def content_id(self):
    180         return self._objective.id
    181 
    182     @property
    183     def objective_id(self):
    184         return self._objective.id
    185 
    186     @property
    187     def title(self):
    188         return GetByMessageID(self.title_message_id)
    189 
    190     @property
    191     def title_message_id(self):
    192         return self._objective.title
    193 
    194     @property
    195     def description(self):
    196         return GetByMessageID(self._objective.subtitle)
    197 
    198     @property
    199     def contribution_method_definition(self):
    200         config = self.contribution_method_configuration
    201         config_method_identifier = config.contribution_method
    202         return self._get_contribution_definition_for_contribution_method(config_method_identifier)
    203 
    204     def get_contribution_method_info(self):
    205         definition = self.contribution_method_definition
    206         configuration = self.contribution_method_configuration
    207         job_def = definition.to_job_format()
    208         param_def = job_def.parameter_def_by_key
    209         job_config = configuration.to_job_format()
    210         param_config = job_config.parameter_configs_by_key
    211         param_def_and_config = {}
    212         for k, param_d in param_def.iteritems():
    213             config = param_config.get(k, None)
    214             if not config:
    215                 config = MissingParameterConfiguration(k, [])
    216             param_def_and_config[k] = (param_d, config)
    217 
    218         return Bundle(job_contribution_definition=job_def, param_def_and_config=param_def_and_config)
    219 
    220     @property
    221     def contribution_method_configuration(self):
    222         return self._objective.contribution_method_configuration
    223 
    224     @property
    225     def career_path(self):
    226         return self._objective.career_path
    227 
    228     @property
    229     def theme_pack(self):
    230         return self.campaign_controller.theme_pack
    231 
    232     @property
    233     def target_progress(self):
    234         return self._objective.target_progress
    235 
    236     @property
    237     def current_total_progress(self):
    238         return self.dynamic_data.current_total_progress
    239 
    240     @property
    241     def total_progress_percentage(self):
    242         if self.current_total_progress is 0:
    243             return 0.0
    244         return self.current_total_progress / float(self.target_progress)
    245 
    246     @property
    247     def total_progress_percentage_with_contribution_text(self):
    248         total_progress = self.total_progress_percentage
    249         return self._get_progress_with_contribution_text(total_progress)
    250 
    251     @property
    252     def total_remaining_progress(self):
    253         remaining = max(0, self.target_progress - self.current_total_progress)
    254         return remaining
    255 
    256     @property
    257     def committed_participants(self):
    258         return self.dynamic_data.committed_participants
    259 
    260     @property
    261     def personal_progress(self):
    262         return self.dynamic_data.personal_progress
    263 
    264     @property
    265     def personal_progress_percentage(self):
    266         if self.personal_progress is 0:
    267             return 0.0
    268         if self.contribution_limit:
    269             return self.personal_progress / float(self.contribution_limit)
    270         return self.personal_progress / float(self.target_progress)
    271 
    272     def _get_progress_with_contribution_text(self, progress):
    273         ratio = eveformat.percent(mathext.clamp(progress, 0, 1.0), decimal_places=1)
    274         return '%s %s' % (ratio, self.contribution_method_progress_description_text)
    275 
    276     @property
    277     def personal_progress_percentage_with_contribution_text(self):
    278         progress = self.personal_progress_percentage
    279         return self._get_progress_with_contribution_text(progress)
    280 
    281     @property
    282     def remaining_personal_progress(self):
    283         return min(self.total_remaining_progress, max(0, self.contribution_limit - self.personal_progress))
    284 
    285     @property
    286     def has_remaining_personal_progress(self):
    287         return self.remaining_personal_progress > 0
    288 
    289     @property
    290     def personal_progress_limit(self):
    291         if self.contribution_limit:
    292             return self.contribution_limit
    293         return self.target_progress
    294 
    295     def claim_rewards(self):
    296         self.repository.redeem_rewards(self.objective_id)
    297 
    298     @property
    299     def has_claimable_rewards(self):
    300         return self.dynamic_data.has_claimable_rewards
    301 
    302     @property
    303     def unclaimed_isk_info(self):
    304         return self.dynamic_data.unclaimed_isk_info
    305 
    306     @property
    307     def unclaimed_lp_info(self):
    308         return self.dynamic_data.unclaimed_lp_info
    309 
    310     @property
    311     def _isk_reward(self):
    312         rewards = self._objective.rewards
    313         if not rewards:
    314             return None
    315         isk_reward = rewards.isk_reward
    316         if isk_reward:
    317             return isk_reward
    318 
    319     @property
    320     def total_available_amount_isk(self):
    321         isk_reward = self._isk_reward
    322         if not isk_reward:
    323             return 0
    324         num_intervals = self.max_progress_per_participant / isk_reward.interval_size
    325         total_isk = isk_reward.amount_per_interval * num_intervals
    326         return total_isk
    327 
    328     @property
    329     def isk_amount_per_interval(self):
    330         isk_reward = self._isk_reward
    331         if not isk_reward:
    332             return None
    333         return isk_reward.amount_per_interval
    334 
    335     @property
    336     def isk_interval_size(self):
    337         isk_reward = self._isk_reward
    338         if not isk_reward:
    339             return None
    340         return isk_reward.interval_size
    341 
    342     @property
    343     def _lp_reward(self):
    344         rewards = self._objective.rewards
    345         if not rewards:
    346             return None
    347         lp_reward = rewards.lp_reward
    348         if lp_reward:
    349             return lp_reward
    350 
    351     @property
    352     def total_available_amount_lp(self):
    353         lp_reward = self._lp_reward
    354         if not lp_reward:
    355             return 0
    356         num_intervals = self.max_progress_per_participant / lp_reward.interval_size
    357         total_lp = lp_reward.amount_per_interval * num_intervals
    358         return total_lp
    359 
    360     @property
    361     def lp_amount_per_interval(self):
    362         lp_reward = self._lp_reward
    363         if not lp_reward:
    364             return None
    365         return lp_reward.amount_per_interval
    366 
    367     @property
    368     def lp_interval_size(self):
    369         lp_reward = self._lp_reward
    370         if not lp_reward:
    371             return None
    372         return lp_reward.interval_size
    373 
    374     @property
    375     def lp_issuer_id(self):
    376         lp_reward = self._lp_reward
    377         if not lp_reward:
    378             return ''
    379         issuing_corporation = lp_reward.issuing_corporation
    380         return issuing_corporation
    381 
    382     @property
    383     def lp_issuer_text(self):
    384         issuing_corporation = self.lp_issuer_id
    385         if issuing_corporation:
    386             return cfg.eveowners.Get(issuing_corporation).name
    387         return ''
    388 
    389     @property
    390     def card_icon_path(self):
    391         issuer_id = self.lp_issuer_id
    392         if issuer_id in FLAT_ICONS_BY_CORP_ID:
    393             return FLAT_ICONS_BY_CORP_ID[issuer_id]
    394         if issuer_id in CORP_ICONS:
    395             path = CORP_ICONS[issuer_id]
    396             res_path, _ = Icon.ConvertIconNoToResPath(path)
    397             return res_path
    398         return super(CampaignJob, self).card_icon_path
    399 
    400     @property
    401     def _standings(self):
    402         rewards = self._objective.rewards
    403         if not rewards:
    404             return None
    405         standings_reward = rewards.standings_reward
    406         if standings_reward:
    407             return standings_reward
    408 
    409     @property
    410     def has_standings_boost(self):
    411         standings_rewards = self._standings
    412         if standings_rewards:
    413             return True
    414         return False
    415 
    416     @property
    417     def standing_booster_issuer_name(self):
    418         standings_rewards = self._standings
    419         if not standings_rewards:
    420             return None
    421         issuer_id = standings_rewards.issuer
    422         if not issuer_id:
    423             return None
    424         return cfg.eveowners.Get(issuer_id).name
    425 
    426     @property
    427     def issuer_id(self):
    428         return self._objective._issuer
    429 
    430     @property
    431     def issuer_name(self):
    432         try:
    433             return cfg.eveowners.Get(self.issuer_id).name
    434         except Exception:
    435             pass
    436 
    437         return ''
    438 
    439     @property
    440     def state(self):
    441         return self._objective.objective_state
    442 
    443     @property
    444     def finished_timestamp(self):
    445         if self._objective.is_finished:
    446             return datetime_to_filetime(self._objective.finished_datetime)
    447 
    448     @property
    449     def is_finished(self):
    450         return self._objective.is_finished
    451 
    452     @property
    453     def is_active(self):
    454         return self.state == ObjectiveState.ACTIVE
    455 
    456     @property
    457     def is_expired(self):
    458         return self.state == ObjectiveState.EXPIRED
    459 
    460     @property
    461     def is_completed(self):
    462         return self.state == ObjectiveState.COMPLETED
    463 
    464     @property
    465     def is_participant_state_unknown(self):
    466         return self.dynamic_data.is_participant_state_unknown
    467 
    468     @property
    469     def is_committed(self):
    470         return self.dynamic_data.is_committed
    471 
    472     @property
    473     def is_resigned(self):
    474         return self.dynamic_data.is_resigned
    475 
    476     @property
    477     def contribution_limit(self):
    478         return self._objective.max_progress_per_participant
    479 
    480     @property
    481     def is_available_in_browse(self):
    482         if not super(CampaignJob, self).is_available_in_browse:
    483             return False
    484         if self.is_expired:
    485             return False
    486         if not self._can_contribute_for_browse():
    487             return False
    488         if self.campaign_controller.is_expired:
    489             return False
    490         return True
    491 
    492     def check_filters(self, content_tag_ids = None, keywords = None, **kwargs):
    493         if not content_tag_ids:
    494             return True
    495         if self.campaign_id in content_tag_ids:
    496             return True
    497         if not check_content_tags(content_tag_ids, self.content_tag_ids):
    498             return False
    499         if not self._check_keywords(keywords):
    500             return False
    501         return True
    502 
    503     @property
    504     def is_available_in_active(self):
    505         if not self.is_active:
    506             return False
    507         if self.is_tracked:
    508             return True
    509         return self.is_committed
    510 
    511     @property
    512     def is_trackable(self):
    513         return self.is_active and self.is_committed
    514 
    515     @property
    516     def is_linkable(self):
    517         return not self.is_removed
    518 
    519     @property
    520     def max_progress_per_participant(self):
    521         return self._objective.max_progress_per_participant
    522 
    523     def _register(self):
    524         on_objective_contributed_notice.connect(self._on_objective_contributed)
    525         on_campaigns_objective_stats_updated.connect(self._on_objective_stats_updated)
    526         on_commited_state_changed.connect(self._on_participation_state_changed)
    527 
    528     def _unregister(self):
    529         on_objective_contributed_notice.disconnect(self._on_objective_contributed)
    530         on_campaigns_objective_stats_updated.disconnect(self._on_objective_stats_updated)
    531         on_commited_state_changed.disconnect(self._on_participation_state_changed)
    532 
    533     def _update_job(self, state_changed = False):
    534         self.on_job_updated()
    535         if state_changed:
    536             on_job_state_changed(self)
    537 
    538     def get_cta_buttons(self):
    539         buttons = []
    540         if self.is_active:
    541             if self.is_committed:
    542                 buttons.append({'label': GetByLabel('UI/Campaigns/ResignFromJob'),
    543                  'hint': GetByLabel('UI/Campaigns/ResignFromJobHint'),
    544                  'on_click': self.on_resign,
    545                  'variant': ButtonVariant.GHOST,
    546                  'is_enabled': self.can_resign()})
    547             else:
    548                 buttons.append({'label': GetByLabel('UI/Campaigns/ParticipateNow'),
    549                  'hint': self._get_commit_tooltip(),
    550                  'on_click': self.on_commit,
    551                  'is_enabled': self.can_commit()})
    552         if self.is_committed or self.has_claimable_rewards:
    553             pass
    554         return buttons
    555 
    556     def _get_commit_tooltip(self):
    557         label_path = 'UI/Campaigns/ParticipateNowHint'
    558         try:
    559             check_can_commit(self)
    560         except ObjectiveStateError as e:
    561             label_path = 'UI/Campaigns/ParticipateNowHintError'
    562         except ParticipantStateError as e:
    563             message_id = get_restricted_message_id(self)
    564             if message_id:
    565                 return GetByMessageID(message_id)
    566             label_path = 'UI/Campaigns/ParticipateNowHintError'
    567 
    568         return GetByLabel(label_path)
    569 
    570     def _can_contribute_for_browse(self):
    571         if not self.is_active:
    572             return False
    573         if not self.dynamic_data.has_valid_stats_caches:
    574             self.repository.prime_all_stats_for_campaign(self.campaign_id)
    575             return False
    576         return self.remaining_personal_progress > 0
    577 
    578     def can_commit(self):
    579         try:
    580             check_can_commit(self)
    581             return True
    582         except Exception as e:
    583             return False
    584 
    585     def can_resign(self):
    586         try:
    587             check_can_resign(self)
    588             return True
    589         except Exception as e:
    590             return False
    591 
    592     def on_commit(self, button = None):
    593         if button is None:
    594             uthread2.start_tasklet(self._do_commit)
    595             return
    596         if button.busy:
    597             return
    598         button.busy = True
    599         button.disabled = True
    600         uthread2.start_tasklet(self._commit_sequence, button)
    601 
    602     def _commit_sequence(self, button):
    603         self._do_commit()
    604         button.busy = False
    605         button.hint = None
    606 
    607     def _do_commit(self):
    608         try:
    609             self.repository.commit(self.objective_id)
    610         except CampaignForbiddenError:
    611             message_player(GetByLabel('UI/Campaigns/UnableToCommitCampaignForbiddenError', numObjectives=10))
    612             return
    613         except CampaignRequestError:
    614             message_player(GetByLabel('UI/Campaigns/CannotCommitCampaignRequestError'))
    615             return
    616 
    617     def on_resign(self, button = None):
    618         if button is None:
    619             uthread2.start_tasklet(self._do_resign)
    620             return
    621         if button.busy:
    622             return
    623         button.busy = True
    624         button.disabled = True
    625         uthread2.start_tasklet(self._resign_sequence, button)
    626 
    627     def _resign_sequence(self, button):
    628         self._do_resign()
    629         button.busy = False
    630         button.disabled = False
    631 
    632     def _do_resign(self):
    633         self.repository.resign(self.objective_id)
    634 
    635     def _on_participation_state_changed(self, objective_id):
    636         if objective_id != self.objective_id:
    637             return
    638         if self.is_committed and not self.is_tracked:
    639             self.add_tracked()
    640         elif self.is_tracked and not self.is_trackable:
    641             self.remove_tracked()
    642         self._closest_solar_system_id = None
    643         self._update_job(state_changed=True)
    644 
    645     def _get_content_tag_ids(self):
    646         return self.objective.content_tags
    647 
    648     @property
    649     def campaign_name(self):
    650         return self.campaign_controller.title_text
    651 
    652     @property
    653     def campaign_issuer_name(self):
    654         return self.campaign_controller.issuer_name
    655 
    656     @property
    657     def campaign_description(self):
    658         return self.campaign_controller.subtitle_text
    659 
    660     @property
    661     def time_left_text(self):
    662         return self.campaign_controller.time_left_text
    663 
    664     @property
    665     def expiration_time(self):
    666         return datetime_to_filetime(self.campaign_controller.expiry_datetime)
    667 
    668     @property
    669     def presenting_character_id(self):
    670         return self._objective.presenting_character
    671 
    672     @property
    673     def icon_path(self):
    674         return self.campaign_controller.textures.icon_path
    675 
    676     def get_menu(self):
    677         data = super(BaseCampaignJob, self).get_menu()
    678         data.extend(get_qa_menu_for_job(self))
    679         return data
    680 
    681     def on_system_change(self):
    682         self._closest_solar_system_id = None
    683 
    684     @property
    685     def solar_system_id(self):
    686         if self._closest_solar_system_id is None:
    687             self._update_closest_solar_system()
    688         return self._closest_solar_system_id
    689 
    690     def _update_closest_solar_system(self):
    691         if not session.solarsystemid2:
    692             return
    693         location_ids = self.get_location_ids()
    694         closest_solar_system_id = get_closest_solarsystem_in_locations_from(session.solarsystemid2, set(location_ids), sm.GetService('map'), sm.GetService('clientPathfinderService'))[0]
    695         if closest_solar_system_id is not None:
    696             self._closest_solar_system_id = closest_solar_system_id
    697 
    698     def get_location_ids(self):
    699         contribution_method_info = self.get_contribution_method_info()
    700         return self.get_contribution_method_locations(contribution_method_info.param_def_and_config)
    701 
    702     def get_contribution_method_locations(self, param_def_and_config):
    703         locations = set()
    704         for param_def, param_config in param_def_and_config.itervalues():
    705             if param_def.parameter_type in (ParameterType.LOCATION, ParameterType.DOCKABLE_LOCATION):
    706                 values = param_config.get_values()
    707                 parm_class = get_parameter_class(param_def.parameter_type)
    708                 if parm_class:
    709                     locations.update(parm_class.get_solarsystem_id_from_locations(values))
    710 
    711         return locations
    712 
    713     @property
    714     def annotations(self):
    715         return self._objective.annotations
    716 
    717     @property
    718     def restrictions_text(self):
    719         return self._GetTextFromAnnotations('restriction_tooltip_message_id')
    720 
    721     def get_warning_texts(self):
    722         warning_texts = []
    723         for x in xrange(1, 10):
    724             name = 'warning_%s_message_id' % x
    725             text = self._GetTextFromAnnotations(name)
    726             if text:
    727                 warning_texts.append(text)
    728             else:
    729                 break
    730 
    731         return warning_texts
    732 
    733     def _GetTextFromAnnotations(self, field_name, default_value = ''):
    734         message_id = self.annotations.get(field_name, None)
    735         if message_id is None:
    736             return default_value
    737         return GetByMessageID(int(message_id))
    738 
    739     def _on_objective_contributed(self, objective_id, total_participant_progress):
    740         if objective_id != self.objective_id:
    741             return
    742         self._update_project()
    743 
    744     def _on_objective_stats_updated(self, objective_ids):
    745         if self.objective_id not in objective_ids:
    746             return
    747         self._update_project()
    748 
    749     def _update_project(self, state_changed = False):
    750         self._update_context()
    751         self.on_job_updated()
    752         if state_changed:
    753             on_job_state_changed(self)
    754 
    755     @caching.lazy_property
    756     def objective_chain(self):
    757         from objectives.client.objective_chain import ObjectiveChain
    758         from objectives.common.objective_context import ObjectivesContext
    759         self._objectives_context = ObjectivesContext()
    760         self._update_context()
    761         objective_chain = ObjectiveChain(content_id=CAMPAIGN_OBJECTIVE_CHAIN_ID, context=self._objectives_context, overrides={'progress': KeyVal(title=self.contribution_method_definition.title)})
    762         objective_chain.start()
    763         return objective_chain
    764 
    765     def _update_context(self):
    766         if not self._objectives_context:
    767             return
    768         self._objectives_context.update_value(key='global_target_progress', value=self.target_progress)
    769         self._objectives_context.update_value(key='global_current_progress', value=self.current_total_progress)
    770         self._objectives_context.update_value(key='display_global_as_percentage', value=True)
    771         self._objectives_context.update_value(key='personal_progress', value=self.personal_progress)
    772         self._objectives_context.update_value(key='personal_target_progress', value=self.contribution_limit)
    773         self._objectives_context.update_value(key='progress_bar_hex_color', value=eveColor.SUCCESS_GREEN_HEX)
    774 
    775 
    776 class FutureCampaignJob(BaseCampaignJob):
    777     CARD_CLASS = FutureCampaignJobCard
    778     PAGE_CLASS = CampaignObjectivePage
    779 
    780     @property
    781     def content_id(self):
    782         return (self.start_datetime, self._objective.career_path, self._objective.contribution_method_name)
    783 
    784     @property
    785     def start_datetime(self):
    786         return self._objective.start_datetime
    787 
    788     @property
    789     def title(self):
    790         return self.contribution_method_name
    791 
    792     @property
    793     def is_future_objective(self):
    794         return True
    795 
    796     def _get_content_tag_ids(self):
    797         career_content_path = CAREER_PATH_TO_CONTENT_TAG_ID.get(self.objective.career_path)
    798         if career_content_path:
    799             return [career_content_path]
    800         return []
    801 
    802     def get_menu(self):
    803         data = MenuData()
    804         return data
    805 
    806     @property
    807     def contribution_method_definition(self):
    808         config_method_identifier = self._objective.contribution_method_name
    809         return self._get_contribution_definition_for_contribution_method(config_method_identifier)
    810 
    811     @property
    812     def time_until_start_text(self):
    813         start_date = datetime_to_filetime(self.start_datetime)
    814         diff = max(0, start_date - gametime.GetWallclockTime())
    815         time_left = FmtDate(diff, 'ss')
    816         return time_left
    817 
    818     def check_filters(self, content_tag_ids = None, keywords = None, **kwargs):
    819         return False