{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8f13badb",
   "metadata": {},
   "outputs": [],
   "source": [
    "# This Notebook is used in conjunction with Email Notifier.ipynb as an Add-on Tool in Seeq Workbench.\n",
    "# See https://seeq.atlassian.net/wiki/spaces/SQ/pages/961675391/Add-on+Tools for installation details. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8ad71f48",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Hide the \"Edit App\" button in AppMode:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "389a4db3",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%javascript\n",
    "$('#header-container').hide();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "47012078",
   "metadata": {},
   "outputs": [],
   "source": [
    "# These variables must be set according to the location and name of your Email Notifier notebook.\n",
    "# Typical installation via the Install script will put all of the notebooks in the same location, \n",
    "# so this is assumed for a customization-free installation.\n",
    "import re\n",
    "p = re.compile(r'\\/(notebooks|apps|addon)\\/(.*)\\/.+$')\n",
    "notifier_notebook_url = p.sub(r'/\\1/\\2/Email%20Notifier.ipynb', jupyter_notebook_url)\n",
    "unsubscriber_notebook_url = p.sub(r'/\\1/\\2/Email%20Unsubscriber.ipynb', jupyter_notebook_url)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "69e26015",
   "metadata": {
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "from IPython.display import HTML, display, clear_output\n",
    "from bs4 import BeautifulSoup\n",
    "import pandas as pd\n",
    "import inspect\n",
    "import ipywidgets as w\n",
    "import ipyvuetify as v\n",
    "import pytz\n",
    "import os\n",
    "import requests\n",
    "import urllib.parse as urlparse\n",
    "\n",
    "from seeq import spy\n",
    "from seeq.sdk.rest import ApiException\n",
    "\n",
    "class NotificationScheduler:\n",
    "    \n",
    "    v.theme.themes.light.success = '#007960'\n",
    "    v.theme.themes.light.primary = '#007960'\n",
    "    v.theme.themes.light.info = '#2a5c84'\n",
    "    \n",
    "    TEMPLATING_EXPLANATION = \"\"\"\n",
    "    <p style=\"line-height:1.5; margin-bottom:10px; font-size:14px\">\n",
    "      Enter your template HTML below. Note that not all HTML tags or CSS attributes\n",
    "      will be rendered by email clients. You can validate your template by sending\n",
    "      yourself test emails or using an online checker like \n",
    "      <a href=\"https://www.htmlemailcheck.com/check/\" target=\"_blank\">https://www.htmlemailcheck.com/check/</a>.\n",
    "      Some advanced editors, e.g., VSCode, will also validate HTML.\n",
    "    </p>\n",
    "    <p style=\"line-height:1.5; margin-bottom:10px; font-size:14px\">\n",
    "      Within your template, you can also access variables specific to this notifier job.\n",
    "      The fields Condition Name, Condition ID, Schedule, Lookback Interval, Topic Document URL,\n",
    "      and Workbook ID are supported within the <code>job</code> context. You can insert these special\n",
    "      variables into your email by referencing the <code>job</code> context, followed by the name of\n",
    "      the name of the variable enclosed by double-quotes and square brackets.\n",
    "    </p>\n",
    "    <p style=\"line-height:1.5; margin-bottom:10px; font-size:14px\">For example:</p>\n",
    "    <p style=\"line-height:1.5; margin-bottom:10px; font-size:14px\">\n",
    "      <strong>Condition Name:</strong> {job[\"Condition Name\"]}\n",
    "      <br>\n",
    "      <strong>Topic Document URL:</strong> {job[\"Topic Document URL\"]}\n",
    "    </p>\n",
    "    <p style=\"line-height:1.5; margin-bottom:10px; font-size:14px\">\n",
    "      You can access information about the triggering capsule in a similar manner -\n",
    "      for example, capsule start or end and custom capsule properties.\n",
    "    </p>\n",
    "    <p style=\"line-height:1.5; margin-bottom:10px; font-size:14px\">\n",
    "      <strong>Capsule Start:</strong> {capsule[\"Capsule Start\"]}\n",
    "      <br>\n",
    "      <strong>Capsule Property (ex. \"Batch ID\"):</strong> {capsule[\"Batch ID\"]}\n",
    "    </p>\n",
    "    <p style=\"line-height:1.5; margin-bottom:10px; font-size:14px\">\n",
    "      Any elements that have a class of \"seeq-auto-update\" may be modified by this Notebook upon scheduling.\n",
    "      For example, the Topic Document URL field is optional, so if one is not provided, the paragraph element\n",
    "      with class <code>\"seeq-auto-update\"</code> and id <code>\"tdu\"</code> will be removed from the default\n",
    "      template.\n",
    "    </p>\n",
    "    \"\"\"\n",
    "    \n",
    "    SUBJECT_TEMPLATE_DEFAULT = 'Capsule alert for Condition {job[\"Condition Name\"]}'\n",
    "    HTML_TEMPLATE_DEFAULT = \"\"\"\n",
    "        <html>\n",
    "            <head></head>\n",
    "            <body>\n",
    "                <b>Capsule alert!</b>\n",
    "                <p>A new capsule was identified in the Scheduled Email Sender with Capsule Start {capsule[\"Capsule Start\"]}</p>\n",
    "                <p>Capsule details:</p>\n",
    "                <p><b>Start Time: </b>{capsule[\"Capsule Start\"]}</p>\n",
    "                <p><b>End Time: </b>{capsule[\"Capsule End\"]}</p>\n",
    "                <p class=\"seeq-auto-update\" id=\"tdu\">Topic Document: <a href='{job[\"Topic Document URL\"]}'>link</a></p>\n",
    "                <p>Regards,</p>\n",
    "                <p>Seeq Data Lab&nbsp;<img src=\"cid:sdl\"/></p>\n",
    "                <a class=\"seeq-auto-update\" id=\"usu\" href='unsubscriber_notebook_url'>Unsubscribe</a>\n",
    "            </body>\n",
    "        </html>\n",
    "    \"\"\".replace('unsubscriber_notebook_url', unsubscriber_notebook_url)\n",
    "    TO_DEFAULT = spy.user.email\n",
    "    SCHEDULE_DEFAULT = 'Every 6 hours'\n",
    "    LOOKBACK_DEFAULT = '1.5'\n",
    "    TIME_ZONE_DEFAULT = 'UTC'\n",
    "    CC_DEFAULT = ''\n",
    "    BCC_DEFAULT = ''\n",
    "    TOPIC_DEFAULT = ''\n",
    "    \n",
    "    DEFAULT_JOB_PARAMETERS = {\n",
    "        'Condition ID': '',\n",
    "        'Condition Name': '',\n",
    "        'Schedule': SCHEDULE_DEFAULT,\n",
    "        'Time Zone': TIME_ZONE_DEFAULT,\n",
    "        'Lookback Interval': LOOKBACK_DEFAULT,\n",
    "        'To': TO_DEFAULT,\n",
    "        'Cc': CC_DEFAULT,\n",
    "        'Bcc': BCC_DEFAULT,\n",
    "        'Topic Document URL': TOPIC_DEFAULT,\n",
    "        'Subject Template': SUBJECT_TEMPLATE_DEFAULT,\n",
    "        'Html Template': HTML_TEMPLATE_DEFAULT,\n",
    "        'Workbook ID': '',\n",
    "        'Scheduled': False,\n",
    "        'Stopped': False,\n",
    "        'Inception': ''\n",
    "    }\n",
    "    \n",
    "    JOB_PARAMETERS = pd.Series(DEFAULT_JOB_PARAMETERS.keys(), dtype='string')\n",
    "    \n",
    "    # The character limit on job indices is too small to use guids - use a shortened guid instead.\n",
    "    def short_id(self, guid):\n",
    "        return guid[-12:]\n",
    "    \n",
    "    def get_source_worksheet_url(self):\n",
    "        host = spy.client.host[:-4]\n",
    "        return f'{host}/workbook/{self.workbook_id}/worksheet/{self.worksheet_id}'\n",
    "    \n",
    "    \n",
    "    def check_email_configured(self):\n",
    "        p = re.compile(r'\\/(notebooks|apps|addon)\\/(.*)$')\n",
    "        notifier_notebook_contents_url = p.sub(r'/api/contents/\\2', notifier_notebook_url)\n",
    "        notifier_notebook = requests.get(notifier_notebook_contents_url, cookies = { 'sq-auth': spy.client.auth_token })\n",
    "        default_email_text = \"\"\"'Email Address': 'email.sender@mycompany.com'\"\"\"\n",
    "        if not notifier_notebook.text.find(default_email_text) == -1:\n",
    "            edit_notifier_url = notifier_notebook_url.replace('/addon/', '/notebooks/')\n",
    "            error_html = v.Html(tag='p',\n",
    "                                children=[\n",
    "                                    'Configure SMTP settings in ',\n",
    "                                    v.Html(tag='a', attributes={'href': edit_notifier_url, 'target': '_blank'}, children=['Notifier Notebook']\n",
    "                                           , style_='color: #B1D7CF'),\n",
    "                                    ' before using Scheduler'\n",
    "                                ])\n",
    "            self.on_error(error_html)\n",
    "    \n",
    "    def get_existing_jobs(self):\n",
    "        try:\n",
    "            pulled_jobs = spy.jobs.pull(datalab_notebook_url=notifier_notebook_url, label=self.workbook_id, all=True)\n",
    "        except:\n",
    "            pulled_jobs = None\n",
    "        existing_jobs = pd.DataFrame(columns=self.DEFAULT_JOB_PARAMETERS.keys()) \\\n",
    "            if pulled_jobs is None or pulled_jobs.empty \\\n",
    "            else pulled_jobs\n",
    "        if not existing_jobs.empty:\n",
    "            existing_jobs['Scheduled'] = True\n",
    "        return existing_jobs.astype({'Stopped': 'bool'})\n",
    "    \n",
    "    def get_conditions_from_worksheet(self):\n",
    "        worksheet_items = None\n",
    "        with self.output:\n",
    "            worksheet_items = spy.search(self.get_source_worksheet_url(), quiet=True)\n",
    "            clear_output()\n",
    "        conditions_from_worksheet = None\n",
    "        if worksheet_items.empty:\n",
    "            conditions_from_worksheet = pd.DataFrame()\n",
    "        else:\n",
    "            conditions_from_worksheet = worksheet_items[worksheet_items['Type'].str.contains('Condition')]\n",
    "            conditions_from_worksheet.index = [\n",
    "                self.short_id(guid) for guid in conditions_from_worksheet['ID'].to_list()\n",
    "            ]\n",
    "        return conditions_from_worksheet\n",
    "    \n",
    "    def initialize_jobs(self):\n",
    "        self.jobs = self.get_existing_jobs()\n",
    "        for key, condition in self.get_conditions_from_worksheet().iterrows():\n",
    "            if key in self.jobs.index:\n",
    "                # Rename from worksheet in case the name in the pickled job DataFrame is stale\n",
    "                self.jobs.loc[key, 'Condition Name'] = condition['Name']\n",
    "            else:\n",
    "                new_job = pd.Series(self.DEFAULT_JOB_PARAMETERS)\n",
    "                new_job.name = self.short_id(condition['ID'])\n",
    "                new_job['Condition ID'] = condition['ID']\n",
    "                new_job['Condition Name'] = condition['Name']\n",
    "                new_job['Stopped'] = False\n",
    "                new_job['Scheduled'] = False\n",
    "                self.jobs = self.jobs.append(new_job)     \n",
    "\n",
    "    \n",
    "    def displayable_conditions(self):\n",
    "        unscheduled = [\n",
    "            {'text': job['Condition Name'], 'value': index} for index, job \n",
    "             in self.jobs[self.jobs['Scheduled'] == False].iterrows()\n",
    "        ]\n",
    "        previously_scheduled =  [\n",
    "            {'text': job['Condition Name'], 'value': index} for index, job\n",
    "             in self.jobs[self.jobs['Scheduled'] == True].iterrows()]\n",
    "        \n",
    "        formatted_conditions = []\n",
    "        if unscheduled:\n",
    "            formatted_conditions += [{'header': 'From Worksheet or Unscheduled'}] + unscheduled\n",
    "        if previously_scheduled:\n",
    "            formatted_conditions += [{'header': 'Scheduled'}] + previously_scheduled\n",
    "\n",
    "        return formatted_conditions\n",
    "    \n",
    "    def displayable_jobs(self):\n",
    "        exclude_columns = [\n",
    "            'Condition ID',\n",
    "            'Topic Document URL',\n",
    "            'Lookback Interval',\n",
    "            'Subject Template',\n",
    "            'Html Template',\n",
    "            'Workbook ID',\n",
    "            'Scheduled'\n",
    "        ]\n",
    "        return self.jobs[self.jobs['Scheduled'] == True].replace(\"\", float(\"NaN\")) \\\n",
    "            .drop(columns = exclude_columns).dropna(axis='columns', how='all')\n",
    "\n",
    "    # In some cases, a template will need to be adjusted before sending it to the notifier.\n",
    "    # For example, the Topic Document URL is an optional job parameter, so if a user doesn't\n",
    "    # provide it, the HTML should not include the associated element.\n",
    "    def soup_up_html_template(self):\n",
    "        condition_id = self.jobs.loc[self.selected_condition]['Condition ID']\n",
    "        soup = BeautifulSoup(self.html.v_model, 'html.parser')\n",
    "        for sauce in soup.find_all(attrs={'class': 'seeq-auto-update', 'id': 'usu'}):\n",
    "            sauce['href'] = f'{unsubscriber_notebook_url}?workbookId={self.workbook_id}&conditionId={condition_id}'\n",
    "        if not self.topic.v_model.strip():\n",
    "            for sauce in soup.find_all(attrs={'class': 'seeq-auto-update', 'id': 'tdu'}):\n",
    "                sauce.replace_with('')            \n",
    "        return soup.prettify()\n",
    "    \n",
    "    def set_conditions(self):\n",
    "        displayable_conditions = self.displayable_conditions()\n",
    "        displayable_selected_condition = next(filter(lambda row: not 'header' in row, displayable_conditions), None)\n",
    "        self.selected_condition = displayable_selected_condition['value'] if displayable_selected_condition else None\n",
    "        \n",
    "        self.condition_select.v_model = displayable_selected_condition\n",
    "        self.condition_select.items = displayable_conditions\n",
    "    \n",
    "    def set_data_table(self):\n",
    "        displayable_jobs = self.displayable_jobs()\n",
    "        headers = [{'text': header, 'value': header } for header in displayable_jobs.keys()]        \n",
    "        self.data_table.headers = headers\n",
    "        self.data_table.items = displayable_jobs.to_dict('records')\n",
    "    \n",
    "    def handle_selection_change(self, data):\n",
    "        self.set_selected_job_from_models(data)\n",
    "        self.selected_condition = data\n",
    "        self.set_models_from_selected_job()                         \n",
    "    \n",
    "    def set_selected_job_from_models(self, data):\n",
    "        if not self.selected_condition or self.selected_condition == data:\n",
    "            return\n",
    "        self.jobs.loc[self.selected_condition, 'To'] = self.to.v_model\n",
    "        self.jobs.loc[self.selected_condition, 'Cc'] = self.cc.v_model\n",
    "        self.jobs.loc[self.selected_condition, 'Bcc'] = self.bcc.v_model\n",
    "        self.jobs.loc[self.selected_condition, 'Schedule'] = self.schedule.v_model\n",
    "        self.jobs.loc[self.selected_condition, 'Topic Document URL'] = self.topic.v_model\n",
    "        self.jobs.loc[self.selected_condition, 'Lookback Interval'] = self.lookback_period.v_model\n",
    "        self.jobs.loc[self.selected_condition, 'Subject Template'] = self.subject.v_model\n",
    "        self.jobs.loc[self.selected_condition, 'Html Template'] = self.html.v_model\n",
    "        self.jobs.loc[self.selected_condition, 'Time Zone'] = self.time_zone.v_model        \n",
    "    \n",
    "    def set_models_from_selected_job(self):\n",
    "        self.to.v_model = self.jobs.loc[self.selected_condition]['To']\n",
    "        self.schedule.v_model = self.jobs.loc[self.selected_condition]['Schedule']\n",
    "        self.topic.v_model = self.jobs.loc[self.selected_condition]['Topic Document URL']\n",
    "        self.lookback_period.v_model = self.jobs.loc[self.selected_condition]['Lookback Interval']\n",
    "        self.time_zone.v_model = self.jobs.loc[self.selected_condition]['Time Zone']\n",
    "        self.cc.v_model = self.jobs.loc[self.selected_condition]['Cc']\n",
    "        self.bcc.v_model = self.jobs.loc[self.selected_condition]['Bcc']\n",
    "        self.subject.v_model = self.jobs.loc[self.selected_condition]['Subject Template']\n",
    "        self.html.v_model = self.jobs.loc[self.selected_condition]['Html Template']\n",
    "        self.unschedule_button.disabled = not self.jobs.loc[self.selected_condition]['Scheduled']\n",
    "        self.stopped_job_warning.children = ['This job is not currently running.'] if self.jobs.loc[self.selected_condition]['Stopped'] else ['']     \n",
    "    \n",
    "    def set_validations(self):\n",
    "        required_fields = ['to', 'schedule', 'subject', 'html']\n",
    "    \n",
    "        for field in required_fields:\n",
    "            getattr(self, f'{field}').on_event('change', self.validate_required_field)\n",
    "        self.lookback_period.on_event('change', self.validate_lookback_period)\n",
    "    \n",
    "    def valid_lookback_period(self):\n",
    "        try:\n",
    "            return float(self.lookback_period.v_model) > 0\n",
    "        except Exception as ex:\n",
    "            return False\n",
    "    \n",
    "    def validate_all(self):\n",
    "        return self.to.v_model and self.schedule.v_model and self.subject.v_model and self.html.v_model and self.valid_lookback_period()\n",
    "    \n",
    "    def validate_required_field(self, widget, event, data):\n",
    "        if widget.v_model:\n",
    "            widget.rules= [] \n",
    "            if self.validate_all():\n",
    "                self.schedule_button.disabled = False\n",
    "        else:\n",
    "            widget.rules = ['is required.']\n",
    "            self.schedule_button.disabled = True        \n",
    "    \n",
    "    def validate_lookback_period(self, widget, event, data):\n",
    "        if not widget.v_model:\n",
    "            widget.rules = ['is required.']\n",
    "            self.schedule_button.disabled = True \n",
    "            return\n",
    "        \n",
    "        if self.valid_lookback_period():\n",
    "            widget.rules= []\n",
    "            if self.validate_all():\n",
    "                self.schedule_button.disabled = False\n",
    "        else:\n",
    "            widget.rules = ['must be a positive number.']\n",
    "            self.schedule_button.disabled = True \n",
    "            \n",
    "    def on_error(self, e):\n",
    "        if type(e) is type(v.Html()):\n",
    "            message = e\n",
    "        else:\n",
    "            error_text = str(e.message) if hasattr(e, 'message') else str(e)\n",
    "            message = f'Something went wrong: {error_text}'\n",
    "\n",
    "        self.snackbar.color = 'red darken-2'\n",
    "        self.snackbar.children = [message, self.close_snackbar]\n",
    "        self.snackbar.timeout = 0\n",
    "        self.snackbar.v_model = True\n",
    "\n",
    "    def on_success(self, message):\n",
    "        self.snackbar.color = 'success'\n",
    "        self.snackbar.children = [message, self.close_snackbar]\n",
    "        self.snackbar.timeout = 5000\n",
    "        self.snackbar.v_model = True\n",
    "    \n",
    "    def on_close_snackbar(self, widget, event, data):\n",
    "        self.snackbar.v_model = False\n",
    "\n",
    "    def on_condition_select(self, widget, event, data):\n",
    "        self.handle_selection_change(data)\n",
    "    \n",
    "    def on_reset_templates(self, widget, event, data):\n",
    "        if not self.selected_condition:\n",
    "            return\n",
    "        self.subject.v_model = self.SUBJECT_TEMPLATE_DEFAULT\n",
    "        self.subject.rules= []\n",
    "        self.html.v_model = self.HTML_TEMPLATE_DEFAULT\n",
    "        self.html.rules= []\n",
    "        if self.validate_all():\n",
    "            self.schedule_button.disabled = False\n",
    "    \n",
    "    def on_schedule(self, widget, event, data):\n",
    "        if not self.validate_all():\n",
    "            self.snackbar.color = 'error'\n",
    "            self.snackbar.children = ['Please fix the errors above.', self.close_snackbar]\n",
    "            self.snackbar.v_model = True\n",
    "            return\n",
    "        \n",
    "        self.schedule_button.loading = True        \n",
    "        self.jobs.loc[self.selected_condition] = pd.Series(\n",
    "            {\n",
    "                'Condition ID': self.jobs.loc[self.selected_condition]['Condition ID'],\n",
    "                'Condition Name': self.jobs.loc[self.selected_condition]['Condition Name'],\n",
    "                'Schedule': self.schedule.v_model,\n",
    "                'Lookback Interval': self.lookback_period.v_model,\n",
    "                'Time Zone': self.time_zone.v_model,\n",
    "                'To': self.to.v_model,\n",
    "                'Cc': self.cc.v_model,\n",
    "                'Bcc': self.bcc.v_model,\n",
    "                'Topic Document URL': self.topic.v_model,\n",
    "                'Subject Template': self.subject.v_model,\n",
    "                'Html Template': self.soup_up_html_template(),\n",
    "                'Workbook ID': self.workbook_id,\n",
    "                'Stopped': False,\n",
    "                'Scheduled': True,\n",
    "                'Inception': self.jobs.loc[self.selected_condition]['Inception'] or pd.Timestamp.now('UTC').isoformat()\n",
    "            },\n",
    "            name = self.selected_condition\n",
    "        )\n",
    "        new_jobs = self.jobs[self.jobs['Scheduled'] == True].sort_index()  \n",
    "        try:\n",
    "            spy.jobs.push(\n",
    "                new_jobs,\n",
    "                datalab_notebook_url=notifier_notebook_url,\n",
    "                label=self.workbook_id,\n",
    "                quiet = True\n",
    "            )\n",
    "        except Exception as e:\n",
    "            msg = str(e)\n",
    "            if 'Features/DataLab/ScheduledNotebooks/Enabled'  in msg and 'Forbidden' in msg:\n",
    "                self.on_error('ScheduledNotebooks are not enabled.  Contact an administrator to change th Features/DataLab/ScheduledNotebooks/Enabled setting.')\n",
    "            elif 'Features/DataLab/ScheduledNotebooks/MinimumScheduleFrequency' in msg:\n",
    "                self.on_error(f'The schedule {self.jobs.loc[self.selected_condition][\"Schedule\"]} is too frequent.'\n",
    "                              f'Contact an administrator to change the Features/DataLab/ScheduledNotebooks/MinimumScheduleFrequency setting.')\n",
    "            else:\n",
    "                self.on_error(e)\n",
    "            self.schedule_button.loading = False\n",
    "            return\n",
    "\n",
    "        self.condition_select.items=self.displayable_conditions()\n",
    "        self.set_data_table()\n",
    "        self.unschedule_button.disabled = False\n",
    "        self.schedule_button.loading = False\n",
    "        self.on_success('Scheduled!')\n",
    "            \n",
    "    def on_unschedule(self, widget, event, data):\n",
    "        self.unschedule_button.loading = False\n",
    "        self.jobs.loc[self.selected_condition, 'Scheduled'] = False\n",
    "        remaining_jobs = self.jobs[self.jobs['Scheduled'] == True].sort_index()\n",
    "        try:\n",
    "            if remaining_jobs.empty:\n",
    "                spy.jobs.unschedule(\n",
    "                    datalab_notebook_url=notifier_notebook_url,\n",
    "                    label=self.workbook_id,\n",
    "                    quiet=True\n",
    "                )\n",
    "            else:\n",
    "                status_df = spy.jobs.push(\n",
    "                    remaining_jobs,\n",
    "                    datalab_notebook_url=notifier_notebook_url,\n",
    "                    label=self.workbook_id,\n",
    "                    quiet=True\n",
    "                )\n",
    "        except Exception as e:\n",
    "            self.on_error(e)\n",
    "            self.unschedule_button.loading = False\n",
    "            return\n",
    "\n",
    "        self.set_conditions()\n",
    "        self.set_data_table()\n",
    "        # self.set_selected_condition()\n",
    "        self.unschedule_button.loading = False\n",
    "        self.unschedule_button.disabled = True\n",
    "        self.on_success('Unscheduled!')\n",
    "    \n",
    "    def __init__(self, workbook_id, worksheet_id):\n",
    "        self.workbook_id = workbook_id\n",
    "        self.worksheet_id = worksheet_id\n",
    "        self.output = w.Output()\n",
    "        \n",
    "        self.close_snackbar = v.Btn(color='white', icon=True, children=[v.Icon(children=['mdi-window-close'])])\n",
    "        self.snackbar = v.Snackbar(v_model=False, app=True, shaped=True, children=[])\n",
    "        \n",
    "        \n",
    "        self.check_email_configured()\n",
    "        \n",
    "        self.initialize_jobs()\n",
    "               \n",
    "        self.selected_condition = None\n",
    "        self.condition_select = v.Select(v_model=[], items=[], style_='margin-right: 8px; margin-top:0; padding-top:0', no_data_text='No conditions available')\n",
    "        self.set_conditions()\n",
    "        self.condition = v.Container(class_='text--primary',\n",
    "                                     style_='width: 100%; max-width: 100%; padding: 5px 25px;',\n",
    "                                     children=[v.Row(children=[\n",
    "                                         v.Html(tag='p', style_='margin-right: 8px;font-size: 17px;margin-top: 4px; color: #007960;', children=['Condition']),\n",
    "                                         self.condition_select\n",
    "                                     ])])\n",
    "        \n",
    "        self.scheduling_tab = v.Tab(children=['Scheduling'])\n",
    "        self.to = v.Textarea(v_model = None, label='To', hint='Separate email addresses with commas', rows=1, auto_grow = True)\n",
    "        self.schedule = v.TextField(v_model = None, label='Schedule')\n",
    "        self.topic = v.Textarea(v_model = None, label='Topic Document URL (optional)', rows=1, auto_grow = True)\n",
    "        self.time_zone = v.Select(v_model=None, items=pytz.all_timezones, label='Time Zone')\n",
    "        self.lookback_period = v.TextField(v_model = None, label='Check for new capsule starts within the last N days:')\n",
    "        self.cc = v.Textarea(v_model = None, label='Cc', hint='Separate email addresses with commas', rows=1, auto_grow = True)\n",
    "        self.bcc = v.Textarea(v_model = None, label='Bcc', hint='Separate email addresses with commas', rows=1, auto_grow = True)\n",
    "        self.advanced_content = v.ExpansionPanelContent(children=[self.time_zone, self.lookback_period, self.cc, self.bcc])\n",
    "        self.advanced = v.ExpansionPanels(children=[v.ExpansionPanel(children=[v.ExpansionPanelHeader(children=['Advanced']), self.advanced_content])])\n",
    "        self.scheduling_content = v.TabItem(children = [self.to, self.schedule, self.topic, self.advanced], style_='padding: 15px')\n",
    "        \n",
    "        self.templates_tab = v.Tab(children=['Templates'])\n",
    "        self.instructions_content = v.ExpansionPanelContent(children=[w.HTML(self.TEMPLATING_EXPLANATION)])\n",
    "        self.instructions = v.ExpansionPanels(children=[v.ExpansionPanel(children=[v.ExpansionPanelHeader(children=['Instructions'], style_='font-size:16px'), self.instructions_content])])\n",
    "        self.subject = v.Textarea(v_model = None, label='Subject Template', rows=1, auto_grow = True)\n",
    "        self.html = v.Textarea(v_model = None, label='HTML Template', auto_grow = True)\n",
    "        self.reset_templates_button = v.Btn(children=['Reset'], class_='mx-2')\n",
    "        self.reset_templates_button.on_event('click', self.on_reset_templates)\n",
    "        self.template_tools = v.Toolbar(children=[v.Spacer(), self.reset_templates_button], style_='box-shadow: none')\n",
    "        self.templates_content = v.TabItem(children = [self.instructions, self.subject, self.html, self.template_tools], style_='padding: 15px')\n",
    "            \n",
    "        self.jobs_tab = v.Tab(children=['Jobs'])\n",
    "        self.data_table = v.DataTable(headers= [], items=[], v_model=[], item_key='Condition Name', class_='elevation-1', no_data_text='No notifications scheduled.')\n",
    "        self.data_content = v.TabItem(children = [self.data_table], class_='pa4')\n",
    "        \n",
    "        if self.jobs.empty or self.selected_condition not in self.jobs.index:\n",
    "            unschedule_button_disabled = True\n",
    "        else:\n",
    "            unschedule_button_disabled = pd.Series([self.jobs.loc[self.selected_condition, 'Scheduled']]).bool()\n",
    "        \n",
    "        self.stopped_job_warning = v.Subheader(children=[])\n",
    "        self.schedule_button = v.Btn(children=['Schedule'],\n",
    "                                     color='success',\n",
    "                                     class_='mx-2',\n",
    "                                     disabled=self.selected_condition == None)        \n",
    "        self.unschedule_button = v.Btn(children=['Unschedule'],\n",
    "                                       class_='mx-2',\n",
    "                                       disabled=unschedule_button_disabled)\n",
    "        \n",
    "        if self.selected_condition:\n",
    "            self.set_models_from_selected_job()\n",
    "        self.reset_templates_button.disabled = self.selected_condition == None\n",
    "        self.set_data_table()\n",
    "        self.set_validations()\n",
    "        \n",
    "        self.condition_select.on_event('change', self.on_condition_select)\n",
    "        self.schedule_button.on_event('click', self.on_schedule)\n",
    "        self.unschedule_button.on_event('click', self.on_unschedule)\n",
    "        self.close_snackbar.on_event('click', self.on_close_snackbar)\n",
    "        \n",
    "        self.app = v.App(id='notification-scheduler')        \n",
    "        self.appBar = v.AppBar(\n",
    "            color='white',\n",
    "            dense=True,\n",
    "            children=[v.ToolbarTitle(children=['Email Notification Scheduler'])])\n",
    "        self.tabs = v.Tabs(children = [self.scheduling_tab, self.scheduling_content, self.templates_tab, self.templates_content, self.jobs_tab, self.data_content])\n",
    "        self.footer = v.Toolbar(children=[v.Spacer(), self.stopped_job_warning, self.unschedule_button, self.schedule_button], style_='box-shadow: none')\n",
    "        \n",
    "    def run(self):\n",
    "        display(HTML(\"<style> div.output_subarea { max-width:100% !important; } .mdi-checkbox-marked, .mdi-minus-box { color: #007960 !important; } .v-toolbar__content { padding: 0 !important } .v-snack {position: absolute !important;top: -300px;right: 0 !important; left: unset !important;} </style>\"))\n",
    "        self.app.children = [self.appBar, self.condition, v.Divider(), self.tabs, self.footer, self.snackbar]\n",
    "        return self.app\n",
    "\n",
    "url = jupyter_notebook_url\n",
    "query_parameters = urlparse.parse_qs(urlparse.urlparse(url).query)\n",
    "workbook_id = query_parameters['workbookId'][0] if 'workbookId' in query_parameters else None\n",
    "worksheet_id = query_parameters['worksheetId'][0] if 'worksheetId' in query_parameters else None\n",
    "\n",
    "if not workbook_id or not worksheet_id:\n",
    "    raise Exception('Workbook and Worksheet IDs must be supplied as query parameters in the URL for this Notebook')\n",
    "    \n",
    "notification_scheduler = NotificationScheduler(workbook_id, worksheet_id)\n",
    "notification_scheduler.run()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2a79656e",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3.11",
   "language": "python",
   "name": "python311"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}