{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Administration: Manage inactive users\n",
    "\n",
    "> * ✏️ Needs Configuration\n",
    "* 🔒 Requires Administrator Privileges\n",
    "* 🗄️ Administration\n",
    "* 👤 User Management\n",
    "* 🔔 Notifications\n",
    "\n",
    "Over time, an Organization can become cluttered with many users, some of whom haven't logged in for several months. This notebook will search through all `Users` in an Organization and disable those users that haven't logged in for a certain amount of days. This notebook will also email users a warning that they haven't logged in for a certain amount of days. This notebook can be a powerful tool to automatically manage these inactive users."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To get started, import the necessary libraries and connect to our GIS:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import csv\n",
    "import smtplib\n",
    "import time\n",
    "import pandas\n",
    "import datetime as dt\n",
    "from datetime import timedelta\n",
    "from IPython.display import display, HTML\n",
    "import logging\n",
    "log = logging.getLogger()\n",
    "\n",
    "from arcgis.gis import GIS\n",
    "\n",
    "gis = GIS(\"home\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next, we will create our function that sends out notifications."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Notifications\n",
    "\n",
    "An important part of this process is notifying the user about their inactivity. We will achieve this by connecting to an external SMTP server and sending out emails, but you can write _any_ code that connects to _any_ external service to send out notifications. __View the 'Notifications' notebook in the examples gallery for more information__.\n",
    "\n",
    "__<span style=\"color:red;\">You MUST modify the below cell</span>__ to update `secret_csv_item_id`, `smtp_server_url`, `username`, and any other information needed to connect to your external smtp server. This includes making sure your _secrets.csv_ file item contains the `smtp_email_password` entry."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Helper function for using your private secrets.csv file\n",
    "def get_secrets(gis=gis,\n",
    "                secret_csv_item_id = '<YOUR_ITEM_ID_HERE>'):\n",
    "    \"\"\"Returns a dict of { secret_key : secret_value } from the \n",
    "    secrets.csv item. See the 'Notifications' notebook in the \n",
    "    examples gallery for more information.\n",
    "    \"\"\"\n",
    "    try:\n",
    "        item = gis.content.get(secret_csv_item_id)\n",
    "        with open(item.download(), 'r') as local_item_file:\n",
    "            reader = csv.DictReader(local_item_file)\n",
    "            return { rows['secret_key'] : rows['secret_value'] \\\n",
    "                     for rows in reader }\n",
    "    except Exception:\n",
    "        return {}\n",
    "\n",
    "SECRETS = get_secrets()\n",
    "\n",
    "# Helper function to send out emails through an SMTP server\n",
    "def send_email_smtp(recipients, message,\n",
    "                    subject=\"Message from your Notebook\"):\n",
    "    \"\"\"Sends the `message` string to all of the emails in the \n",
    "    `recipients` list using the configured SMTP email server. \n",
    "    See the 'Notifications' notebook in the examples gallery\n",
    "    for more information.\n",
    "    \"\"\"\n",
    "    try:\n",
    "        # Set up server and credential variables\n",
    "        smtp_server_url = \"smtp.example.com\"\n",
    "        smtp_server_port = 587\n",
    "        sender = \"your_sender@example.com\"\n",
    "        username = \"your_username\"\n",
    "        password = SECRETS[\"smtp_email_password\"]\n",
    "\n",
    "        # Instantiate our server, configure the necessary security\n",
    "        server = smtplib.SMTP(smtp_server_url, smtp_server_port)\n",
    "        server.ehlo()\n",
    "        server.starttls() # Needed if TLS is required w/ SMTP server\n",
    "        server.login(username, password)\n",
    "    except Exception as e:\n",
    "        log.warning(\"Error setting up SMTP server, couldn't send \" +\n",
    "                    f\"message to {recipients}\")\n",
    "        raise e\n",
    "\n",
    "    # For each recipient, construct the message and attempt to send\n",
    "    did_succeed = True\n",
    "    for recipient in recipients:\n",
    "        try:\n",
    "            message_body = '\\r\\n'.join(['To: {}'.format(recipient),\n",
    "                                        'From: {}'.format(sender),\n",
    "                                        'Subject: {}'.format(subject),\n",
    "                                        '{}'.format(message)])\n",
    "            message_body = message_body.encode(\"utf-8\")\n",
    "            server.sendmail(sender, [recipient], message_body)\n",
    "            print(f\"SMTP server returned success for sending email \"\\\n",
    "                  f\"to {recipient}\")\n",
    "        except Exception as e:\n",
    "            log.warning(f\"Failed sending message to {recipient}\")\n",
    "            log.warning(e)\n",
    "            did_succeed = False\n",
    "    \n",
    "    # Cleanup and return\n",
    "    server.quit()\n",
    "    return did_succeed"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Make sure to independently test that your notifications function works before proceeding forward with this notebook."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Configure Behavior\n",
    "\n",
    "Now that we have a function that sends out notifications, let's configure some variables specific to our organization that will tell our notebook how we want it to run. First, let's specify our time windows for disabling users. The default behavior will email users warning them that they have not logged in for 60 days (2 months), and then notify them that their account might be deleted once they have not logged in for 90 days (3 months).\n",
    "\n",
    "Modify the below cell to change this default behavior."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "# The number of days a user is inactive for before...\n",
    "NUM_INACTIVE_DAYS_TO_NOTIFY = 60 # we notify about their inactivity\n",
    "NUM_INACTIVE_DAYS_TO_DISABLE = 90 # we delete their account"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next, we will specify if we want to email a summary email of the users notified/disabled during this notebook run. The default behavior is to send this report to whomever is specified in the `NOTEBOOK_REPORT_RECIPIENTS` list of strings. Add email addresses to that list, like this:\n",
    "\n",
    "```python\n",
    "NOTEBOOK_REPORT_RECIPIENTS = ['somebody@example.com',\n",
    "                              'somebody_else@example.com']\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Email these people a report about who this notebook notified/disabled\n",
    "EMAIL_NOTEBOOK_REPORT = True\n",
    "NOTEBOOK_REPORT_RECIPIENTS = []"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally, this notebook has a `USER_DISABLE_NOTIFY_PROTECTION` flag that by default is set to `True`. This safety flag will prevent this notebook from physically calling `user.disable()` or sending out any emails. This notebook is powerful and potentially dangerous, since it can disable many user accounts and send out emails to _everyone_ in an organization. Proceed with caution. \n",
    "\n",
    "__<span style=\"color:red;\">Only modify the below cell if you understand the implications</span>__."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "USER_DISABLE_NOTIFY_PROTECTION = True"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Datetimes\n",
    "\n",
    "A core component of this notebook will be testing how many days since a user logged in. The below code cell creates a helper function that can calculate this value using the `user.lastLogin` property and the builtin `datetime` library."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "def datetime_of_last_login(user) -> dt.datetime:\n",
    "    \"\"\"Returns a datetime instance of when the user last logged in.\n",
    "    \n",
    "    The `user.lastLogin` property returns the milliseconds since the \n",
    "    epoch, whereas a standard timestamp is the seconds since the epoch. \n",
    "    Therefore, we must divide by 1000.\n",
    "    \"\"\"\n",
    "    timestamp_of_last_login = int(user.lastLogin / 1000)\n",
    "    last_login = dt.datetime.fromtimestamp(timestamp_of_last_login)\n",
    "    return last_login"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This helper function returns the number of days between the last login and today and an `int`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "def num_days_since_last_login(user) -> int:\n",
    "    \"\"\"Returns the number of days since a user last logged in. \n",
    "\n",
    "    Subtracting the `last_login` and `now` datetime instances will\n",
    "    yield a timedelta instance. This timedelta instance has a \n",
    "    `.days` property, which is what we want to return\n",
    "    \"\"\"\n",
    "    last_login = datetime_of_last_login(user)\n",
    "    now = dt.datetime.now()\n",
    "    return (now - last_login).days"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This helper function calculates the planned datetime for deleting a user."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "def datetime_of_account_deletion(user) -> dt.datetime:\n",
    "    \"\"\"Returns the datetime of when the account is planned to be deleted\n",
    "    \n",
    "    This is calculated based off of NUM_INACTIVE_DAYS_TO_DELETE variable,\n",
    "    by adding a timedelta to a datetime (returns a datetime)\n",
    "    \"\"\"\n",
    "    last_login = datetime_of_last_login(user)\n",
    "    return last_login + timedelta(days=NUM_INACTIVE_DAYS_TO_DISABLE)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Notify/Disable\n",
    "\n",
    "Now, let's create a function that will notify a user when their account is scheduled to be disabled."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "def notify_user(user):\n",
    "    \"\"\"Called when in the 'notify' range. Will send an email to the user,\n",
    "    notifying them that their account will be disabled.\n",
    "    \"\"\"\n",
    "    num_days_last_login = num_days_since_last_login(user)\n",
    "    num_days_delete = NUM_INACTIVE_DAYS_TO_DISABLE - num_days_last_login\n",
    "    date_of_deletion = datetime_of_account_deletion(user).date()\n",
    "    if num_days_delete > 0:\n",
    "        delete_warning = \"could potentially be deleted in \"\\\n",
    "            f\"{num_days_delete} days on {date_of_deletion}\"\n",
    "    else:\n",
    "        delete_warning = \"could potentially be deleted\"\n",
    "    message = f\"Hello {user.username},\\n\"\\\n",
    "              f\"\\n\"\\\n",
    "              f\"Your account at {gis.url} is hasn't been logged in \"\\\n",
    "              f\"for {num_days_last_login} days, and {delete_warning}. \"\\\n",
    "              f\"Please login to your account to prevent deletion, \"\\\n",
    "              f\"or contact your org admin for more information.\\n\"\\\n",
    "              f\"\\n\"\\\n",
    "              f\"Account URL: {user.homepage}\"\n",
    "    send_email_smtp([user.email], message,\n",
    "                    subject=\"Your Account Could Be Disabled\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then, let's create a function that will disable a user and email them a notification."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "def disable_user(user):\n",
    "    \"\"\"Called when in the 'disable' range. Will disable the user, and \n",
    "    will send them an email that their account is disabled.\n",
    "    \"\"\"\n",
    "    if not user.disabled:\n",
    "        print(f\"Disabling user {user.username}\")\n",
    "        user.disable()\n",
    "        message = f\"Hello {user.username},\\n\"\\\n",
    "              f\"\\n\"\\\n",
    "              f\"Your account at {gis.url} has been disabled since you \"\\\n",
    "              f\"haven't logged in in {NUM_INACTIVE_DAYS_TO_DISABLE} \"\\\n",
    "              f\"days.\\n\\nContact your org admin to regain access.\"\n",
    "        send_email_smtp([user.email], message,\n",
    "                        subject=\"Your Account is Disabled\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Miscellanous Functionality\n",
    "\n",
    "Let's create a function that writes a .csv file of all users, and if they are in the notify or disable range of inactivity. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "workspace = \"/arcgis/home/\"\n",
    "\n",
    "def write_csv(users_notify_range, users_disable_range):\n",
    "    \"\"\"Given two lists of users to notify/disable, write a CSV\n",
    "    with 1 row per user, stating if in the notify/disable range \"\"\"\n",
    "    file_path = f'{workspace}/INACTIVE_USERS--{time.ctime()}.csv'\n",
    "    with open(file_path, 'w') as file:\n",
    "        writer = csv.DictWriter(file, ['user', 'in_notify_range',\n",
    "                                       'in_disable_range'])\n",
    "        writer.writeheader()\n",
    "        for user in gis.users.search():\n",
    "            writer.writerow({\n",
    "                'user' : user.username,\n",
    "                'in_notify_range' : bool(user in users_notify_range),\n",
    "                'in_disable_range' : bool(user in users_disable_range)})\n",
    "    return gis.content.add({}, file_path)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then, let's create a function that emails specific individuals a notebook report of notified/disabled users."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "def stringify_users(users):\n",
    "    \"\"\"Takes a list of users, and returns a string representation\"\"\"\n",
    "    if users:\n",
    "        return \", \".join(user.username for user in users)\n",
    "    else:\n",
    "        return \"No Users\"\n",
    "\n",
    "def email_notebook_report_summary(notified_users, disabled_users,\n",
    "                                  csv_item):\n",
    "    \"\"\"After the notebook runs, send out a summary report email to\n",
    "    the specified people about who was notified/disabled\n",
    "    \"\"\"\n",
    "    message = f\"'Manage Inactive Users' notebook finished running.\\n\"\\\n",
    "              f\"-----\\n\"\\\n",
    "              f\"CSV output: {csv_item.homepage}\\n\"\\\n",
    "              f\"-----\\n\"\\\n",
    "              f\"Users notified: {stringify_users(notified_users)}\\n\"\\\n",
    "              f\"-----\\n\"\\\n",
    "              f\"Disabled users: {stringify_users(disabled_users)}\"\n",
    "    send_email_smtp(NOTEBOOK_REPORT_RECIPIENTS, message,\n",
    "                    subject=\"'Manage Inactive Users' notebook run\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally, let's create a simple function that tests to make sure this notebook is running against a GIS object with the correct admin credentials."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "def precondition_check():\n",
    "    \"\"\"Checks if running as admin by checking 'lastLogin' prop of User\"\"\"\n",
    "    if 'lastLogin' not in gis.users.me:\n",
    "        raise Exception(\"You don't have the proper permissions to \"\\\n",
    "                        \"run this notebook; you must be an admin.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## main()\n",
    "\n",
    "Finally, let's create our `main()` function that links together all our previously defined functions that search through users, notify/disable them if they haven't logged in for the specified number of days, and display the proper notebook outputs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "users_to_notify = []\n",
    "users_to_disable = []\n",
    "csv_item = None\n",
    "\n",
    "def main():\n",
    "    # Tell user we're running, initialize\n",
    "    print(\"Notebook is now running, please wait...\\n-----\")\n",
    "    global users_to_notify, users_to_disable, csv_item\n",
    "    precondition_check()\n",
    "    users_to_notify = []\n",
    "    users_to_disable = []\n",
    "\n",
    "    # Stage an in-memory list of all users to notify/delete\n",
    "    for user in gis.users.search():\n",
    "        num_days = num_days_since_last_login(user)\n",
    "        if (num_days > NUM_INACTIVE_DAYS_TO_NOTIFY) and \\\n",
    "           (num_days < NUM_INACTIVE_DAYS_TO_DISABLE):\n",
    "            # If in the 'notify' timeframe, but not long enough to delete\n",
    "            users_to_notify.append(user)\n",
    "        elif (num_days >= NUM_INACTIVE_DAYS_TO_DISABLE):\n",
    "            # If in the 'delete' timeframe\n",
    "            users_to_disable.append(user)\n",
    "\n",
    "    # Write the in-memory lists to a CSV\n",
    "    csv_item = write_csv(users_to_notify, users_to_disable)\n",
    "    display(HTML(\"<h3>CSV of all users in notify/disable range</h3>\"))\n",
    "    display(csv_item)\n",
    "\n",
    "    # Check if we should notify/delete the users in the in-memory lists\n",
    "    if USER_DISABLE_NOTIFY_PROTECTION:\n",
    "        log.warning(f\"Not disabling/notifying any users, since \"\\\n",
    "                    f\"USER_DISABLE_NOTIFY_PROTECTION config variable \"\\\n",
    "                    f\"is set to `True`: Set to `False` to have \"\\\n",
    "                    f\"this notebook actually notify/disable users.\")\n",
    "    else:\n",
    "        # If configured, notify/disable the users in the in-memory lists\n",
    "        print(f\"Notifying {stringify_users(users_to_notify)}\")\n",
    "        print(f\"Disabling {stringify_users(users_to_disable)}\")\n",
    "        for user in users_to_notify:\n",
    "            notify_user(user)\n",
    "        for user in users_to_disable:\n",
    "            disable_user(user)\n",
    "\n",
    "        # If configured, email a report of notified/deleted users\n",
    "        if EMAIL_NOTEBOOK_REPORT:\n",
    "            email_notebook_report_summary(users_to_notify,\n",
    "                                          users_to_disable,\n",
    "                                          csv_item)\n",
    "\n",
    "    # Tell user we're finished\n",
    "    print(\"-----\\nNotebook completed running.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We have just defined a `main()` function, but we haven't called it yet. If you've modified the notebook, follow these steps:\n",
    "1. __Double check the notebook content__. Make sure no secrets are visible in the notebook, delete unused code, refactor, etc.\n",
    "2. Save the notebook\n",
    "3. In the 'Kernel' menu, press 'Restart and Run All' to run the whole notebook from top to bottom\n",
    "\n",
    "Now, `main()` can be called."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Notebook is now running, please wait...\n",
      "-----\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<h3>CSV of all users in notify/disable range</h3>"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "<div class=\"item_container\" style=\"height: auto; overflow: hidden; border: 1px solid #cfcfcf; border-radius: 2px; background: #f6fafa; line-height: 1.21429em; padding: 10px;\">\n",
       "                    <div class=\"item_left\" style=\"width: 210px; float: left;\">\n",
       "                        \n",
       "                        \n",
       "                       </a>\n",
       "                    </div>\n",
       "\n",
       "                    <div class=\"item_right\"     style=\"float: none; width: auto; overflow: hidden;\">\n",
       "                        <b>INACTIVE_USERS--Thu Oct 25 16:45:30 2018</b>\n",
       "                        </a>\n",
       "                        <br/>CSV by portaladmin\n",
       "                        <br/>Last Modified: October 25, 2018\n",
       "                        <br/>0 comments, 0 views\n",
       "                    </div>\n",
       "                </div>\n",
       "                "
      ],
      "text/plain": [
       "<Item title:\"INACTIVE_USERS--Thu Oct 25 16:45:30 2018\" type:CSV owner:portaladmin>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Not disabling/notifying any users, since USER_DISABLE_NOTIFY_PROTECTION config variable is set to `True`: Set to `False` to have this notebook actually notify/disable users.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "-----\n",
      "Notebook completed running.\n"
     ]
    }
   ],
   "source": [
    "main()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Congratulations! If all is setup correctly, emails should be sent out for users in the 'notify' range, users in the 'disable' range should be disabled, and the notebook should output the correct artifacts."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Post Processing\n",
    "\n",
    "The `INACTIVE_USERS.csv` file/item contains a list of all users, if they are in the `notify_range`, if they are in the `in_disable_range`, or if they are in neither range. This file can be viewed and analyzed using the `pandas` package. This code cell will convert any `.csv` Item to a pandas `DataFrame`; we will be converting the `INACTIVE_USERS.csv` file."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>user</th>\n",
       "      <th>in_notify_range</th>\n",
       "      <th>in_disable_range</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>andrew</td>\n",
       "      <td>False</td>\n",
       "      <td>True</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>api_data_owner</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Jane Doe</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>John Smith</td>\n",
       "      <td>False</td>\n",
       "      <td>False</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>leslie</td>\n",
       "      <td>False</td>\n",
       "      <td>True</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "             user  in_notify_range  in_disable_range\n",
       "0          andrew            False              True\n",
       "1  api_data_owner            False             False\n",
       "2   arcgis_python            False             False\n",
       "3       atma.mani            False             False\n",
       "4      bill.major            False              True"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def csv_item_to_dataframe(item):\n",
    "    \"\"\"Takes in an Item instance of a `.csv` file,\n",
    "    returns a pandas DataFrame\n",
    "    \"\"\"\n",
    "    downloaded_csv_file_path = item.download()\n",
    "    return pandas.read_csv(downloaded_csv_file_path)\n",
    "\n",
    "df = csv_item_to_dataframe(csv_item)\n",
    "df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Conclusion\n",
    "\n",
    "This notebook provided you the workflow for searching through all Users in an Organization, and will notify/disable them if they haven't logged in for a certain amount of days. This notebook can be a powerful tool to automatically manage these inactive users since Organizations can become cluttered with many users over time.\n",
    "\n",
    "### Related Notebooks\n",
    "For related notebooks, search for the following in your samples notebook gallery:\n",
    "\n",
    "- Notifications\n",
    "- Validate User Profiles"
   ]
  }
 ],
 "metadata": {
  "esriNotebookRuntime": {
   "notebookRuntimeName": "ArcGIS Notebook Python 3 Standard",
   "notebookRuntimeVersion": "10.7.1"
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "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.6.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
