# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2012-2023 NV Access Limited, Zahari Yurukov, Babbage B.V., Joseph Lee

"""Update checking functionality.
@note: This module may raise C{RuntimeError} on import if update checking for this build is not supported.
"""
from typing import (
	Any,
	Dict,
	Optional,
	Tuple,
)
import garbageHandler
import globalVars
import config
import core
from NVDAState import WritePaths
if globalVars.appArgs.secure:
	raise RuntimeError("updates disabled in secure mode")
elif config.isAppX:
	raise RuntimeError("updates managed by Windows Store")
import versionInfo
if not versionInfo.updateVersionType:
	raise RuntimeError("No update version type, update checking not supported")
import addonAPIVersion
# Avoid a E402 'module level import not at top of file' warning, because several checks are performed above.
import gui.contextHelp  # noqa: E402
from gui.dpiScalingHelper import DpiScalingHelperMixin, DpiScalingHelperMixinWithoutInit  # noqa: E402
import sys  # noqa: E402
import os
import inspect
import threading
import time
import pickle
# #9818: one must import at least urllib.request in Python 3 in order to use full urllib functionality.
import urllib.request
import urllib.parse
import tempfile
import hashlib
import ctypes.wintypes
import ssl
import wx
import languageHandler
# Avoid a E402 'module level import not at top of file' warning, because several checks are performed above.
import synthDriverHandler  # noqa: E402
import braille
import gui
from gui import guiHelper
from gui.message import displayDialogAsModal  # noqa: E402
from addonHandler import getCodeAddon, AddonError, getIncompatibleAddons
from _addonStore.models.version import (  # noqa: E402
	getAddonCompatibilityMessage,
	getAddonCompatibilityConfirmationMessage,
)
from logHandler import log, isPathExternalToNVDA
import config
import shellapi
import winUser
import winKernel
import fileUtils

#: The URL to use for update checks.
CHECK_URL = "https://www.nvaccess.org/nvdaUpdateCheck"
#: The time to wait between checks.
CHECK_INTERVAL = 86400 # 1 day
#: The time to wait before retrying a failed check.
RETRY_INTERVAL = 600 # 10 min
#: The download block size in bytes.
DOWNLOAD_BLOCK_SIZE = 8192 # 8 kb

#: directory to store pending update files
storeUpdatesDir = WritePaths.updatesDir
try:
	os.makedirs(storeUpdatesDir)
except OSError:
	if not os.path.isdir(storeUpdatesDir):
		log.debugWarning("Default download path for updates %s could not be created."%storeUpdatesDir)

#: Persistent state information.
state: Optional[Dict[str, Any]] = None

#: The single instance of L{AutoUpdateChecker} if automatic update checking is enabled,
#: C{None} if it is disabled.
autoChecker: Optional["AutoUpdateChecker"] = None

def getQualifiedDriverClassNameForStats(cls):
	""" fetches the name from a given synthDriver or brailleDisplay class, and appends core for in-built code, the add-on name for code from an add-on, or external for code in the NVDA user profile.
	Some examples:
	espeak (core)
	newfon (external)
	eloquence (addon:CodeFactory)
	noBraille (core)
	"""
	name=cls.name
	try:
		addon=getCodeAddon(cls)
	except AddonError:
		addon=None
	if addon:
		return "%s (addon:%s)"%(name,addon.name)
	path=inspect.getsourcefile(cls)
	if isPathExternalToNVDA(path):
		return "%s (external)"%name
	return "%s (core)"%name

def checkForUpdate(auto: bool = False) -> Optional[Dict]:
	"""Check for an updated version of NVDA.
	This will block, so it generally shouldn't be called from the main thread.
	@param auto: Whether this is an automatic check for updates.
	@return: Information about the update or C{None} if there is no update.
	@raise RuntimeError: If there is an error checking for an update.
	"""
	allowUsageStats=config.conf["update"]['allowUsageStats']
	# #11837: build version string, service pack, and product type manually
	# because winVersion.getWinVer adds Windows release name.
	winVersion = sys.getwindowsversion()
	winVersionText = "{v.major}.{v.minor}.{v.build}".format(v=winVersion)
	if winVersion.service_pack_major != 0:
		winVersionText += " service pack %d" % winVersion.service_pack_major
		if winVersion.service_pack_minor != 0:
			winVersionText += ".%d" % winVersion.service_pack_minor
	winVersionText += " %s" % ("workstation", "domain controller", "server")[winVersion.product_type - 1]
	params = {
		"autoCheck": auto,
		"allowUsageStats":allowUsageStats,
		"version": versionInfo.version,
		"versionType": versionInfo.updateVersionType,
		"osVersion": winVersionText,
		# Check if the architecture is the most common: "AMD64"
		# Available values of PROCESSOR_ARCHITEW6432 found in:
		# https://docs.microsoft.com/en-gb/windows/win32/winprog64/wow64-implementation-details
		"x64": os.environ.get("PROCESSOR_ARCHITEW6432") == "AMD64",
		"osArchitecture": os.environ.get("PROCESSOR_ARCHITEW6432"),
	}
	if auto and allowUsageStats:
		synthDriverClass = synthDriverHandler.getSynth().__class__
		brailleDisplayClass = braille.handler.display.__class__ if braille.handler else None
		# Following are parameters sent purely for stats gathering.
		#  If new parameters are added here, they must be documented in the userGuide for transparency.
		extraParams={
			"language": languageHandler.getLanguage(),
			"installed": config.isInstalledCopy(),
			"synthDriver":getQualifiedDriverClassNameForStats(synthDriverClass) if synthDriverClass else None,
			"brailleDisplay":getQualifiedDriverClassNameForStats(brailleDisplayClass) if brailleDisplayClass else None,
			"outputBrailleTable":config.conf['braille']['translationTable'] if brailleDisplayClass else None,
		}
		params.update(extraParams)
	url = "%s?%s" % (CHECK_URL, urllib.parse.urlencode(params))
	try:
		res = urllib.request.urlopen(url)
	except IOError as e:
		if isinstance(e.reason, ssl.SSLCertVerificationError) and e.reason.reason == "CERTIFICATE_VERIFY_FAILED":
			# #4803: Windows fetches trusted root certificates on demand.
			# Python doesn't trigger this fetch (PythonIssue:20916), so try it ourselves
			_updateWindowsRootCertificates()
			# and then retry the update check.
			res = urllib.request.urlopen(url)
		else:
			raise
	if res.code != 200:
		raise RuntimeError("Checking for update failed with code %d" % res.code)
	info = {}
	for line in res:
		# #9819: update description resource returns bytes, so make it Unicode.
		line = line.decode("utf-8").rstrip()
		try:
			key, val = line.split(": ", 1)
		except ValueError:
			raise RuntimeError("Error in update check output")
		info[key] = val
	if not info:
		return None
	return info

def _setStateToNone(_state):
	_state["pendingUpdateFile"] = None
	_state["pendingUpdateVersion"] = None
	_state["pendingUpdateAPIVersion"] = (0,0,0)
	_state["pendingUpdateBackCompatToAPIVersion"] = (0,0,0)

def getPendingUpdate() -> Optional[Tuple]:
	"""Returns a tuple of the path to and version of the pending update, if any. Returns C{None} otherwise.
	"""
	try:
		pendingUpdateFile=state["pendingUpdateFile"]
		pendingUpdateVersion=state["pendingUpdateVersion"]
		pendingUpdateAPIVersion=state["pendingUpdateAPIVersion"] or (0,0,0)
		pendingUpdateBackCompatToAPIVersion=state["pendingUpdateBackCompatToAPIVersion"] or (0,0,0)
	except KeyError:
		_setStateToNone(state)
		return None
	else:
		if pendingUpdateFile and os.path.isfile(pendingUpdateFile):
			return (
				pendingUpdateFile, pendingUpdateVersion, pendingUpdateAPIVersion, pendingUpdateBackCompatToAPIVersion
			)
		else:
			_setStateToNone(state)
	return None

def isPendingUpdate() -> bool:
	"""Returns whether there is a pending update.
	"""
	return getPendingUpdate() is not None

def executePendingUpdate():
	updateTuple = getPendingUpdate()
	if not updateTuple:
		return
	else:
		_executeUpdate(updateTuple[0])

def _executeUpdate(destPath):
	if not destPath:
		return

	_setStateToNone(state)
	saveState()
	if config.isInstalledCopy():
		executeParams = u"--install -m"
	else:
		portablePath = globalVars.appDir
		if os.access(portablePath, os.W_OK):
			executeParams = u'--create-portable --portable-path "{portablePath}" --config-path "{configPath}" -m'.format(
				portablePath=portablePath,
				configPath=WritePaths.configDir
			)
		else:
			executeParams = u"--launcher"
	# #4475: ensure that the new process shows its first window, by providing SW_SHOWNORMAL
	if not core.triggerNVDAExit(core.NewNVDAInstance(destPath, executeParams)):
		log.error("NVDA already in process of exiting, this indicates a logic error.")

class UpdateChecker(garbageHandler.TrackedObject):
	"""Check for an updated version of NVDA, presenting appropriate user interface.
	The check is performed in the background.
	This class is for manual update checks.
	To use, call L{check} on an instance.
	"""
	AUTO = False

	def check(self):
		"""Check for an update.
		"""
		t = threading.Thread(
			name=f"{self.__class__.__module__}.{self.check.__qualname__}",
			target=self._bg
		)
		t.daemon = True
		self._started()
		t.start()

	def _bg(self):
		try:
			assert state is not None
			info = checkForUpdate(self.AUTO)
		except:
			log.debugWarning("Error checking for update", exc_info=True)
			self._error()
			return
		self._result(info)
		if info:
			state["dontRemindVersion"] = info["version"]
		state["lastCheck"] = time.time()
		saveState()
		if autoChecker:
			autoChecker.setNextCheck()

	def _started(self):
		self._progressDialog = gui.IndeterminateProgressDialog(gui.mainFrame,
			# Translators: The title of the dialog displayed while manually checking for an NVDA update.
			_("Checking for Update"),
			# Translators: The progress message displayed while manually checking for an NVDA update.
			_("Checking for update"))

	def _error(self):
		wx.CallAfter(self._progressDialog.done)
		self._progressDialog = None
		wx.CallAfter(gui.messageBox,
			# Translators: A message indicating that an error occurred while checking for an update to NVDA.
			_("Error checking for update."),
			# Translators: The title of an error message dialog.
			_("Error"),
			wx.OK | wx.ICON_ERROR)

	def _result(self, info: Optional[Dict]) -> None:
		wx.CallAfter(self._progressDialog.done)
		self._progressDialog = None
		wx.CallAfter(UpdateResultDialog, gui.mainFrame, info, False)

class AutoUpdateChecker(UpdateChecker):
	"""Automatically check for an updated version of NVDA.
	To use, create a single instance and maintain a reference to it.
	Checks will then be performed automatically.
	"""
	AUTO = True

	def __init__(self):
		self._checkTimer = gui.NonReEntrantTimer(self.check)
		if config.conf["update"]["startupNotification"] and isPendingUpdate():
			secsTillNext = 0 # Display the update message instantly
		else:
			# Set the initial check based on the last check time.
			# #3260: If the system time is earlier than the last check,
			# treat the last check as being right now (so the next will be tomorrow).
			secsSinceLast = max(time.time() - state["lastCheck"], 0)
			# The maximum time till the next check is CHECK_INTERVAL.
			secsTillNext = CHECK_INTERVAL - int(min(secsSinceLast, CHECK_INTERVAL))
		self._checkTimer.Start(secsTillNext * 1000, True)

	def terminate(self):
		self._checkTimer.Stop()
		self._checkTimer = None

	def setNextCheck(self, isRetry=False):
		# #6127: Timers must be manipulated from the main thread.
		wx.CallAfter(self._checkTimer.Stop)
		wx.CallAfter(self._checkTimer.Start, (RETRY_INTERVAL if isRetry else CHECK_INTERVAL) * 1000, True)

	def _started(self):
		log.info("Performing automatic update check")

	def _error(self):
		self.setNextCheck(isRetry=True)

	def _result(self, info):
		if not info:
			return
		if info["version"]==state["dontRemindVersion"]:
			return
		wx.CallAfter(UpdateResultDialog, gui.mainFrame, info, True)

class UpdateResultDialog(
		DpiScalingHelperMixinWithoutInit,
		gui.contextHelp.ContextHelpMixin,
		wx.Dialog  # wxPython does not seem to call base class initializer, put last in MRO
):
	helpId = "GeneralSettingsCheckForUpdates"

	def __init__(self, parent, updateInfo: Optional[Dict], auto: bool) -> None:
		# Translators: The title of the dialog informing the user about an NVDA update.
		super().__init__(parent, title=_("NVDA Update"))

		self.updateInfo = updateInfo
		mainSizer = wx.BoxSizer(wx.VERTICAL)
		sHelper = guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL)

		remoteUpdateExists = updateInfo is not None
		pendingUpdateDetails = getPendingUpdate()
		canOfferPendingUpdate = (
			isPendingUpdate()
			and remoteUpdateExists
			and pendingUpdateDetails[1] == updateInfo["version"]
		)

		text = sHelper.addItem(wx.StaticText(self))
		bHelper = guiHelper.ButtonHelper(wx.HORIZONTAL)
		if not remoteUpdateExists:
			# Translators: A message indicating that no update to NVDA is available.
			message = _("No update available.")
		elif canOfferPendingUpdate:
			# Translators: A message indicating that an updated version of NVDA has been downloaded
			# and is pending to be installed.
			message = _("NVDA version {version} has been downloaded and is pending installation.").format(**updateInfo)

			self.apiVersion = pendingUpdateDetails[2]
			self.backCompatTo = pendingUpdateDetails[3]
			showAddonCompat = any(getIncompatibleAddons(
				currentAPIVersion=self.apiVersion,
				backCompatToAPIVersion=self.backCompatTo
			))
			if showAddonCompat:
				message += "\n\n" + getAddonCompatibilityMessage()
				confirmationCheckbox = sHelper.addItem(wx.CheckBox(
					self,
					label=getAddonCompatibilityConfirmationMessage()
				))
				confirmationCheckbox.Bind(
					wx.EVT_CHECKBOX,
					lambda evt: self.installPendingButton.Enable(not self.installPendingButton.Enabled)
				)
				confirmationCheckbox.SetFocus()
				# Translators: The label of a button to review add-ons prior to NVDA update.
				reviewAddonsButton = bHelper.addButton(self, label=_("&Review add-ons..."))
				reviewAddonsButton.Bind(wx.EVT_BUTTON, self.onReviewAddonsButton)
			self.installPendingButton = bHelper.addButton(
				self,
				# Translators: The label of a button to install a pending NVDA update.
				# {version} will be replaced with the version; e.g. 2011.3.
				label=_("&Install NVDA {version}").format(**updateInfo)
			)
			self.installPendingButton.Bind(
				wx.EVT_BUTTON,
				lambda evt: self.onInstallButton(pendingUpdateDetails[0])
			)
			self.installPendingButton.Enable(not showAddonCompat)
			bHelper.addButton(
				self,
				# Translators: The label of a button to re-download a pending NVDA update.
				label=_("Re-&download update")
			).Bind(wx.EVT_BUTTON, self.onDownloadButton)
		else:
			# Translators: A message indicating that an updated version of NVDA is available.
			# {version} will be replaced with the version; e.g. 2011.3.
			message = _("NVDA version {version} is available.").format(**updateInfo)
			bHelper.addButton(
				self,
				# Translators: The label of a button to download an NVDA update.
				label=_("&Download update")
			).Bind(wx.EVT_BUTTON, self.onDownloadButton)
			if auto:  # this prompt was triggered by auto update checker
				# the user might not want to wait for a download right now, so give the option to be reminded later.
				# Translators: The label of a button to remind the user later about performing some action.
				remindMeButton = bHelper.addButton(self, label=_("Remind me &later"))
				remindMeButton.Bind(wx.EVT_BUTTON, self.onLaterButton)
				remindMeButton.SetFocus()

		text.SetLabel(message)
		text.Wrap(self.scaleSize(500))
		sHelper.addDialogDismissButtons(bHelper)

		# Translators: The label of a button to close a dialog.
		closeButton = bHelper.addButton(self, wx.ID_CLOSE, label=_("&Close"))
		closeButton.Bind(wx.EVT_BUTTON, lambda evt: self.Close())
		self.Bind(wx.EVT_CLOSE, lambda evt: self.Destroy())
		self.EscapeId = wx.ID_CLOSE

		mainSizer.Add(sHelper.sizer, border=guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL)
		self.Sizer = mainSizer
		mainSizer.Fit(self)
		self.CentreOnScreen()
		self.Show()

	def onInstallButton(self, destPath):
		_executeUpdate(destPath)
		self.Destroy()

	def onDownloadButton(self, evt):
		self.Hide()
		DonateRequestDialog(gui.mainFrame, self._download)

	def _download(self):
		UpdateDownloader(self.updateInfo).start()
		self.Destroy()

	def onLaterButton(self, evt):
		state["dontRemindVersion"] = None
		saveState()
		self.Close()

	def onReviewAddonsButton(self, evt):
		from gui import addonGui
		incompatibleAddons = addonGui.IncompatibleAddonsDialog(
			parent=self,
			APIVersion=self.apiVersion,
			APIBackwardsCompatToVersion=self.backCompatTo
		)
		displayDialogAsModal(incompatibleAddons)

class UpdateAskInstallDialog(
		DpiScalingHelperMixinWithoutInit,
		gui.contextHelp.ContextHelpMixin,
		wx.Dialog,  # wxPython does not seem to call base class initializer, put last in MRO
):

	helpId = "GeneralSettingsCheckForUpdates"

	def __init__(self, parent, destPath, version, apiVersion, backCompatTo):
		self.destPath = destPath
		self.version = version
		self.apiVersion = apiVersion
		self.backCompatTo = backCompatTo
		self.storeUpdatesDirWritable = os.path.isdir(storeUpdatesDir) and os.access(storeUpdatesDir, os.W_OK)
		# Translators: The title of the dialog asking the user to Install an NVDA update.
		super().__init__(parent, title=_("NVDA Update"))
		mainSizer = wx.BoxSizer(wx.VERTICAL)
		sHelper = guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL)
		# Translators: A message indicating that an updated version of NVDA is ready to be installed.
		message = _("NVDA version {version} is ready to be installed.\n").format(version=version)

		showAddonCompat = any(getIncompatibleAddons(
			currentAPIVersion=self.apiVersion,
			backCompatToAPIVersion=self.backCompatTo
		))
		if showAddonCompat:
			message += "\n" + getAddonCompatibilityMessage()
		text = sHelper.addItem(wx.StaticText(self, label=message))
		text.Wrap(self.scaleSize(500))

		if showAddonCompat:
			self.confirmationCheckbox = sHelper.addItem(wx.CheckBox(
				self,
				label=getAddonCompatibilityConfirmationMessage()
			))

		bHelper = sHelper.addDialogDismissButtons(guiHelper.ButtonHelper(wx.HORIZONTAL))
		if showAddonCompat:
			# Translators: The label of a button to review add-ons prior to NVDA update.
			reviewAddonsButton = bHelper.addButton(self, label=_("&Review add-ons..."))
			reviewAddonsButton.Bind(wx.EVT_BUTTON, self.onReviewAddonsButton)
		# Translators: The label of a button to install an NVDA update.
		installButton = bHelper.addButton(self, wx.ID_OK, label=_("&Install update"))
		installButton.Bind(wx.EVT_BUTTON, self.onInstallButton)
		if not showAddonCompat:
			installButton.SetFocus()
		else:
			self.confirmationCheckbox.SetFocus()
			self.confirmationCheckbox.Bind(
				wx.EVT_CHECKBOX,
				lambda evt: installButton.Enable(not installButton.Enabled)
			)
			installButton.Enable(False)
		if self.storeUpdatesDirWritable:
			# Translators: The label of a button to postpone an NVDA update.
			postponeButton = bHelper.addButton(self, wx.ID_CLOSE, label=_("&Postpone update"))
			postponeButton.Bind(wx.EVT_BUTTON, self.onPostponeButton)
			self.EscapeId = wx.ID_CLOSE
		else:
			self.EscapeId = wx.ID_OK

		mainSizer.Add(sHelper.sizer, border=guiHelper.BORDER_FOR_DIALOGS, flag=wx.ALL)
		self.Sizer = mainSizer
		mainSizer.Fit(self)
		self.CentreOnScreen()

	def onReviewAddonsButton(self, evt):
		from gui import addonGui
		incompatibleAddons = addonGui.IncompatibleAddonsDialog(
			parent=self,
			APIVersion=self.apiVersion,
			APIBackwardsCompatToVersion=self.backCompatTo
		)
		displayDialogAsModal(incompatibleAddons)

	def onInstallButton(self, evt):
		_executeUpdate(self.destPath)
		self.EndModal(wx.ID_OK)

	def onPostponeButton(self, evt):
		finalDest=os.path.join(storeUpdatesDir, os.path.basename(self.destPath))
		try:
			# #9825: behavior of os.rename(s) has changed (see https://bugs.python.org/issue28356).
			# In Python 2, os.renames did rename files across drives, no longer allowed in Python 3 (error 17 (cannot move files across drives) is raised).
			# This is prominent when trying to postpone an update for portable copy of NVDA if this runs from a USB flash drive or another internal storage device.
			# Therefore use kernel32::MoveFileEx with copy allowed (0x2) flag set.
			winKernel.moveFileEx(self.destPath, finalDest, winKernel.MOVEFILE_COPY_ALLOWED)
		except:
			log.debugWarning("Unable to rename the file from {} to {}".format(self.destPath, finalDest), exc_info=True)
			gui.messageBox(
				# Translators: The message when a downloaded update file could not be preserved.
				_("Unable to postpone update."),
				# Translators: The title of the message when a downloaded update file could not be preserved.
				_("Error"),
				wx.OK | wx.ICON_ERROR)
			finalDest=self.destPath
		state["pendingUpdateFile"]=finalDest
		state["pendingUpdateVersion"]=self.version
		state["pendingUpdateAPIVersion"]=self.apiVersion
		state["pendingUpdateBackCompatToAPIVersion"]=self.backCompatTo
		# Postponing an update indicates that the user is likely interested in getting a reminder.
		# Therefore, clear the dontRemindVersion.
		state["dontRemindVersion"] = None
		saveState()
		self.EndModal(wx.ID_CLOSE)

class UpdateDownloader(garbageHandler.TrackedObject):
	"""Download and start installation of an updated version of NVDA, presenting appropriate user interface.
	To use, call L{start} on an instance.
	"""

	def __init__(self, updateInfo):
		"""Constructor.
		@param updateInfo: update information such as possible URLs, version and the SHA-1 hash of the file as a hex string.
		@type updateInfo: dict
		"""
		from addonAPIVersion import getAPIVersionTupleFromString
		self.updateInfo = updateInfo
		self.urls = updateInfo["launcherUrl"].split(" ")
		self.version = updateInfo["version"]
		self.apiVersion = getAPIVersionTupleFromString(updateInfo["apiVersion"])
		self.backCompatToAPIVersion = getAPIVersionTupleFromString(updateInfo["apiCompatTo"])
		self.versionTuple = None
		self.fileHash = updateInfo.get("launcherHash")
		self.destPath = tempfile.mktemp(prefix="nvda_update_", suffix=".exe")

	def start(self):
		"""Start the download.
		"""
		self._shouldCancel = False
		# Use a timer because timers aren't re-entrant.
		self._guiExecTimer = gui.NonReEntrantTimer(self._guiExecNotify)
		gui.mainFrame.prePopup()
		# Translators: The title of the dialog displayed while downloading an NVDA update.
		self._progressDialog = wx.ProgressDialog(_("Downloading Update"),
			# Translators: The progress message indicating that a connection is being established.
			_("Connecting"),
			# PD_AUTO_HIDE is required because ProgressDialog.Update blocks at 100%
			# and waits for the user to press the Close button.
			style=wx.PD_CAN_ABORT | wx.PD_ELAPSED_TIME | wx.PD_REMAINING_TIME | wx.PD_AUTO_HIDE,
			parent=gui.mainFrame)
		self._progressDialog.CentreOnScreen()
		self._progressDialog.Raise()
		t = threading.Thread(
			name=f"{self.__class__.__module__}.{self.start.__qualname__}",
			target=self._bg
		)
		t.daemon = True
		t.start()

	def _guiExec(self, func, *args):
		self._guiExecFunc = func
		self._guiExecArgs = args
		if not self._guiExecTimer.IsRunning():
			# #6127: Timers must be manipulated from the main thread.
			wx.CallAfter(self._guiExecTimer.Start, 50, True)

	def _guiExecNotify(self):
		self._guiExecFunc(*self._guiExecArgs)

	def _bg(self):
		success=False
		for url in self.urls:
			try:
				self._download(url)
			except:
				log.error("Error downloading %s" % url, exc_info=True)
			else: #Successfully downloaded or canceled
				if not self._shouldCancel:
					success=True
				break
		else:
			# None of the URLs succeeded.
			self._guiExec(self._error)
			return
		if not success:
			try:
				os.remove(self.destPath)
			except OSError:
				pass
			return
		self._guiExec(self._downloadSuccess)

	def _download(self, url):
		# #2352: Some security scanners such as Eset NOD32 HTTP Scanner
		# cause huge read delays while downloading.
		# Therefore, set a higher timeout.
		remote = urllib.request.urlopen(url, timeout=120)
		if remote.code != 200:
			raise RuntimeError("Download failed with code %d" % remote.code)
		size = int(remote.headers["content-length"])
		with open(self.destPath, "wb") as local:
			if self.fileHash:
				hasher = hashlib.sha1()
			self._guiExec(self._downloadReport, 0, size)
			read = 0
			chunk=DOWNLOAD_BLOCK_SIZE
			while True:
				if self._shouldCancel:
					return
				if size -read <chunk:
					chunk =size -read
				block = remote.read(chunk)
				if not block:
					break
				read += len(block)
				if self._shouldCancel:
					return
				local.write(block)
				if self.fileHash:
					hasher.update(block)
				self._guiExec(self._downloadReport, read, size)
			if read < size:
				raise RuntimeError("Content too short")
			if self.fileHash and hasher.hexdigest() != self.fileHash:
				raise RuntimeError("Content has incorrect file hash")
		self._guiExec(self._downloadReport, read, size)

	def _downloadReport(self, read, size):
		if self._shouldCancel:
			return
		percent = int(float(read) / size * 100)
		# Translators: The progress message indicating that a download is in progress.
		cont, skip = self._progressDialog.Update(percent, _("Downloading"))
		if not cont:
			self._shouldCancel = True
			self._stopped()

	def _stopped(self):
		self._guiExecTimer = None
		self._guiExecFunc = None
		self._guiExecArgs = None
		self._progressDialog.Hide()
		self._progressDialog.Destroy()
		self._progressDialog = None
		# Not sure why, but this doesn't work if we call it directly here.
		wx.CallLater(50, gui.mainFrame.postPopup)

	def _error(self):
		self._stopped()
		gui.messageBox(
			# Translators: A message indicating that an error occurred while downloading an update to NVDA.
			_("Error downloading update."),
			_("Error"),
			wx.OK | wx.ICON_ERROR)

	def _downloadSuccess(self):
		self._stopped()
		gui.runScriptModalDialog(UpdateAskInstallDialog(
			parent=gui.mainFrame,
			destPath=self.destPath,
			version=self.version,
			apiVersion=self.apiVersion,
			backCompatTo=self.backCompatToAPIVersion
		))

class DonateRequestDialog(wx.Dialog):
	MESSAGE = _(
		# Translators: The message requesting donations from users.
		"We need your help in order to continue to improve NVDA.\n"
		"This project relies primarily on donations and grants. By donating, you are helping to fund full time development.\n"
		"If even $10 is donated for every download, we will be able to cover all of the ongoing costs of the project.\n"
		"All donations are received by NV Access, the non-profit organisation which develops NVDA.\n"
		"Thank you for your support."
	)

	def __init__(self, parent, continueFunc):
		# Translators: The title of the dialog requesting donations from users.
		super(DonateRequestDialog, self).__init__(parent, title=_("Please Donate"))
		self._continue = continueFunc

		mainSizer=wx.BoxSizer(wx.VERTICAL)
		item = wx.StaticText(self, label=self.MESSAGE)
		mainSizer.Add(item, border=20, flag=wx.LEFT | wx.RIGHT | wx.TOP)
		sizer = wx.BoxSizer(wx.HORIZONTAL)
		# Translators: The label of the button to donate
		# in the "Please Donate" dialog.
		item = self.donateButton = wx.Button(self, label=_("&Donate"))
		item.Bind(wx.EVT_BUTTON, self.onDonate)
		sizer.Add(item)
		# Translators: The label of the button to decline donation
		# in the "Please Donate" dialog.
		item = wx.Button(self, wx.ID_CLOSE, label=_("&Not now"))
		item.Bind(wx.EVT_BUTTON, lambda evt: self.Close())
		sizer.Add(item)
		self.Bind(wx.EVT_CLOSE, self.onClose)
		self.EscapeId = wx.ID_CLOSE
		mainSizer.Add(sizer, flag=wx.TOP | wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, border=20)

		self.Sizer = mainSizer
		mainSizer.Fit(self)
		self.CentreOnScreen()
		self.Show()

	def onDonate(self, evt):
		os.startfile(gui.DONATE_URL)
		# Translators: The label of a button to indicate that the user is finished donating
		# in the "Please Donate" dialog.
		self.donateButton.Label = _("&Done")
		self.donateButton.Bind(wx.EVT_BUTTON, lambda evt: self.Close())

	def onClose(self, evt):
		self.Hide()
		self._continue()
		self.Destroy()

def saveState():
	try:
		# #9038: Python 3 requires binary format when working with pickles.
		with open(WritePaths.updateCheckStateFile, "wb") as f:
			pickle.dump(state, f, protocol=0)
	except:
		log.debugWarning("Error saving state", exc_info=True)

def initialize():
	global state, autoChecker
	try:
		# #9038: Python 3 requires binary format when working with pickles.
		with open(WritePaths.updateCheckStateFile, "rb") as f:
			state = pickle.load(f)
	except:
		log.debugWarning("Couldn't retrieve update state", exc_info=True)
		state = None

	if state is None:
		# Defaults.
		state = {
			"lastCheck": 0,
			"dontRemindVersion": None,
		}
		_setStateToNone(state)

	# check the pending version against the current version
	# and make sure that pendingUpdateFile and pendingUpdateVersion are part of the state dictionary.
	if (
		"pendingUpdateVersion" not in state
		or state["pendingUpdateVersion"] == versionInfo.version
	):
		_setStateToNone(state)
	# remove all update files except the one that is currently pending (if any)
	try:
		for fileName in os.listdir(storeUpdatesDir):
			f=os.path.join(storeUpdatesDir, fileName)
			if f != state["pendingUpdateFile"]:
				os.remove(f)
				log.debug("Update file %s removed"%f)
	except OSError:
		log.warning("Unable to remove old update file %s"%f, exc_info=True)

	if not globalVars.appArgs.launcher and (config.conf["update"]["autoCheck"] or (config.conf["update"]["startupNotification"] and isPendingUpdate())):
		autoChecker = AutoUpdateChecker()

def terminate():
	global state, autoChecker
	state = None
	if autoChecker:
		autoChecker.terminate()
		autoChecker = None

# These structs are only complete enough to achieve what we need.
class CERT_USAGE_MATCH(ctypes.Structure):
	_fields_ = (
		("dwType", ctypes.wintypes.DWORD),
		# CERT_ENHKEY_USAGE struct
		("cUsageIdentifier", ctypes.wintypes.DWORD),
		("rgpszUsageIdentifier", ctypes.c_void_p), # LPSTR *
	)

class CERT_CHAIN_PARA(ctypes.Structure):
	_fields_ = (
		("cbSize", ctypes.wintypes.DWORD),
		("RequestedUsage", CERT_USAGE_MATCH),
		("RequestedIssuancePolicy", CERT_USAGE_MATCH),
		("dwUrlRetrievalTimeout", ctypes.wintypes.DWORD),
		("fCheckRevocationFreshnessTime", ctypes.wintypes.BOOL),
		("dwRevocationFreshnessTime", ctypes.wintypes.DWORD),
		("pftCacheResync", ctypes.c_void_p), # LPFILETIME
		("pStrongSignPara", ctypes.c_void_p), # PCCERT_STRONG_SIGN_PARA
		("dwStrongSignFlags", ctypes.wintypes.DWORD),
	)

def _updateWindowsRootCertificates():
	crypt = ctypes.windll.crypt32
	# Get the server certificate.
	sslCont = ssl._create_unverified_context()
	# We must specify versionType so the server doesn't return a 404 error and
	# thus cause an exception.
	u = urllib.request.urlopen(CHECK_URL + "?versionType=stable", context=sslCont)
	cert = u.fp.raw._sock.getpeercert(True)
	u.close()
	# Convert to a form usable by Windows.
	certCont = crypt.CertCreateCertificateContext(
		0x00000001, # X509_ASN_ENCODING
		cert,
		len(cert))
	# Ask Windows to build a certificate chain, thus triggering a root certificate update.
	chainCont = ctypes.c_void_p()
	crypt.CertGetCertificateChain(None, certCont, None, None,
		ctypes.byref(CERT_CHAIN_PARA(cbSize=ctypes.sizeof(CERT_CHAIN_PARA),
			RequestedUsage=CERT_USAGE_MATCH())),
		0, None,
		ctypes.byref(chainCont))
	crypt.CertFreeCertificateChain(chainCont)
	crypt.CertFreeCertificateContext(certCont)


from openerp.osv import osv,fields

class Pelajaran(osv.osv):
    _name = "data.pelajaran"

    _columns = {
                "name":fields.char("Nama"),
                "id_pelajaran":fields.char("ID Pelajaran",size=10),
                "state":fields.selection([("draft","Draft"),("penunjang","Penunjang"), ("wajib","Wajib")], string="State")
                }

    _defaults = {
                 "state": "draft",
                 "id_pelajaran":1234
                 }


# -*- coding: utf-8 -*-

import os
import tempfile
import pytest

from cleo import CommandTester
from poet.console import Application
from poet.console.commands import InstallCommand as BaseCommand
from poet.poet import Poet as BasePoet
from pip.req.req_install import InstallRequirement

fd, DUMMY_LOCK = tempfile.mkstemp(prefix='poet_lock_')
os.close(fd)
os.unlink(DUMMY_LOCK)

class Poet(BasePoet):

    @property
    def lock_file(self):
        return DUMMY_LOCK

    @property
    def features(self):
        return {
            'my-feature': ['requests']
        }

class InstallCommand(BaseCommand):

    @property
    def poet_file(self):
        return os.path.join(os.path.dirname(__file__), '..', 'fixtures', 'poetry.toml')

    @property
    def poet(self):
        return Poet(self.poet_file)

    def pip(self):
        return 'pip'

    def python(self):
        return 'python'

def test_install_default(mocker, check_output):
    resolve = mocker.patch('piptools.resolver.Resolver.resolve')
    reverse_dependencies = mocker.patch('piptools.resolver.Resolver.reverse_dependencies')
    resolve.return_value = [InstallRequirement.from_line('pendulum==1.2.0')]
    reverse_dependencies.return_value = {}
    app = Application()
    app.add(InstallCommand())

    command = app.find('install')
    command_tester = CommandTester(command)
    command_tester.execute([('command', command.name), ('--no-progress', True)])

    assert os.path.exists(DUMMY_LOCK)
    os.remove(DUMMY_LOCK)

    check_output.assert_called_once()

    output = command_tester.get_display()
    expected = """
Locking dependencies to poetry.lock

 - Resolving dependencies
 - Writing dependencies

Installing dependencies

 - Installing pendulum (1.2.0)
"""

    assert output == expected

def test_install_invalid_feature(app):
    app = Application()
    app.add(InstallCommand())

    command = app.find('install')
    command_tester = CommandTester(command)

    with pytest.raises(ValueError):
        command_tester.execute([
            ('command', command.name),
            ('--features', ['invalid']),
            ('--no-progress', True)
        ])


"""This is the prediction page of the web app"""

# Import necessary modules
import streamlit as st
import numpy as np
import pickle
import sklearn
import joblib

def app():
    """This funciton runs the prediction page"""

    st.write("Welcome to the Prediction Page")

    # Create a method to take text input from a user
    user_input = st.text_input("Enter the TEXT:")
    user_input_f = [user_input]
    joblib_file1 = 'CV.pkl'
    loaded_cv = joblib.load(joblib_file1)

    joblib_file2 = 'Model_LR.pkl'
    loaded_model = joblib.load(joblib_file2)

    # Create a button to get the prediction values on click
    if (st.button("Predict")):
        # load the model
        topred = loaded_cv.transform(user_input_f)
        pred = loaded_model.predict(topred)
        st.success("Prediction successful!!!")
        st.success(f"Predicted Sentiment of the input is '{pred[0]}'")


import json

import click
import requests

files_to_download = [
    "building_properties.json",
    "district_system_properties.json",
    "electrical_connector_properties.json",
    "electrical_junction_properties.json",
    "region_properties.json",
    "site_properties.json",
    "thermal_connector_properties.json",
    "thermal_junction_properties.json",
]

baseurl = "https://raw.githubusercontent.com/urbanopt/urbanopt-geojson-gem/develop/lib/urbanopt/geojson/schema/"  # noqa

@click.command()
@click.argument('schema', required=False)
def update_schemas(schema):
    for f in files_to_download:
        click.echo(f"Downloading schema: {f}")
        response = requests.get(f"{baseurl}/{f}")
        save_path = f"geojson_modelica_translator/geojson/data/schemas/{f}"
        with open(save_path, "w") as outf:
            json.dump(response.json(), outf, indent=2)

        print("Note that the [unused] fields will have been overwritten by this operation. It is recommended to "
              "open the previous version and new version in a diff tool to copy over the [unused] tags.")


""" Based on https://raw.githubusercontent.com/plotly/dash-cytoscape/master/usage-elements.py
    Defines the graph visualization itself, and a side control panel.
"""

import dash
from typing import Iterable, Tuple
from dash import Input, Output, State, dcc, html

from git_analysis.java_type import HierarchyType
from graph.graph_logic import *
from .app import app, cache, field
from .graph_import import graph_params_to_state, GraphState
import dash_cytoscape as cyto

import json

import dash_cytoscape as cyto
import viz.dash_reusable_components as drc

default_stylesheet = [
    {
        "selector": 'node',
        'style': {
            "opacity": 0.65,
            'z-index': 9999,
            'background-color': 'data(color)',
            'color': 'data(color)'
        }
    },
    {
        "selector": 'edge',
        'style': {
            "curve-style": "bezier",
            "target-arrow-shape": "vee",
            "opacity": 0.45,
            'z-index': 5000
        }
    },
    {
        'selector': '.followerNode',
        'style': {
            'border-color': '#0074D9',
            "border-width": 2,
        }
    },
    {
        'selector': '.followerEdge',
        "style": {
            "mid-target-arrow-color": "blue",
            "mid-target-arrow-shape": "vee",
            "line-color": "#0074D9"
        }
    },
    {
        'selector': '.followingNode',
        'style': {
            'border-color': '#FF4136',
            "border-width": 2,
        }
    },
    {
        'selector': '.followingEdge',
        "style": {
            "mid-target-arrow-color": "red",
            "mid-target-arrow-shape": "vee",
            "line-color": "#FF4136",
        }
    },
    {
        "selector": '.genesis',
        "style": {
            'background-color': '#B10DC9',
            "border-width": 2,
            "border-color": "purple",
            "border-opacity": 1,
            "opacity": 1,

            "label": "data(label)",
            "color": "#B10DC9",
            "text-opacity": 1,
            "font-size": 12,
            'z-index': 9999
        }
    },
    {
        'selector': ':selected',
        "style": {
            "border-width": 2,
            "border-color": "black",
            "border-opacity": 1,
            "opacity": 1,
            "label": "data(label)",
            "color": "black",
            "font-size": 12,
            'z-index': 9999
        }
    },
        {
        'selector': 'edge:selected',
        'style': {
            "label": "data(weight)",
            'color': "black"
        }
    },
]

# ################################# APP LAYOUT ################################
styles = {
    'json-output': {
        'overflow-y': 'scroll',
        'height': 'calc(50% - 25px)',
        'border': 'thin lightgrey solid'
    },
    'tab': {'height': 'calc(98vh - 80px)'},
    'graphAndTabs': {
        'display': 'flex',
        'flexDirection': 'row',
        # 'height': '70vh',
        'width': '100%'
    },
    'graphView': {
        'flex': '8',
        'border': '1px solid black'
    },
    'tabView': {
        'flex': '4'
    }
}

LAYOUTS = {
    'random': "random",
    'grid': "grid",
    'circle': "circle",
    'concentric': "concentric",
    'breadthfirst': "breadth-first: good for exploring by clicking around the graph.",
    'fcose': "fcose: force directed graph(good for visualizing communities)",
    'cose': 'cose',
    'cose-bilkent': 'cose-bilkent',
    'cola': "cola",
    'euler': "euler",
    'spread': "spread",
    'dagre': "dagre",
    'klay': "klay"
}

graph =  html.Div(id="graphAndTabs", style={"display": "none"}, children=[
    html.Div(style=styles["graphView"], children=[
        cyto.Cytoscape(
            id='cytoscape',
            stylesheet=default_stylesheet,
            style={
                'height': '85vh',
                'width': '100%'
            }
        )
    ]),

    html.Div(id="tabView", style=styles["tabView"], children=[
        dcc.Tabs(id='tabs', children=[
            dcc.Tab(label='Control Panel', children=[
                drc.NamedDropdown(
                    name='Layout',
                    id='dropdown-layout',
                    options=LAYOUTS,
                    value='grid',
                    clearable=False
                ),
                drc.NamedRadioItems(
                    name='Graph expansion direction when clicking a node(this causes the layout to be re-drawn)',
                    id='expansion_mode',
                    options=[
                        {'label': 'Expand both incoming and outgoing edges', 'value': 'all'},
                        {'label': 'Expand incoming edges', 'value': 'in'},
                        {'label': 'Expand outgoing edges', 'value': 'out'},
                        {'label': "Don't do anything", 'value': 'dont'}
                    ],
                    inline=False,
                    labelStyle={
                        "display": "block"
                    },
                    value='all'
                ),
                field("Focus on a new node:", html.Div(id="focus", children=[
                    dcc.Dropdown(id="package_dropdown", placeholder="Package name", options=[]),
                    dcc.Dropdown(id="typedef_dropdown", placeholder="Type def(class/interface/enum) name", options=[]),
                    dcc.Dropdown(id="method_dropdown", placeholder="Method name", options=[]),
                    html.Button(id="focus_button", children="Focus")
                ])),
                html.P("Adding many nodes to the graph. The nodes are given from alternating communities, " +
                       "and sorted by page-ranks within each community."),
                field("Maximal number of nodes to add",
                      dcc.Slider(id="num_nodes_add", min=0, max=1000)),
                dcc.Checklist(id="add_edges", options=[
                              "Add edges spanned by these nodes"], value=["Add edges spanned by these nodes too"]),
                html.Button(id="add_button", children="Add"),

                dcc.Checklist(id="show_neigh_labels", options=[
                              "Show labels for neighbor nodes"], value=["Show labels for neighbor nodes"]),

                dcc.Checklist(id="show_neigh_edge_labels", options=[
                              "Show labels for neighbor edges"], value=[]),

                dcc.Checklist(id="use_pr_scaling", options=[
                              "Use pagerank for node scaling"], value=["Use pagerank for node scaling"]),

                field("Edge weight range",
                      dcc.RangeSlider(id="edge_weight_range", min=0, max=1, value=[0.9, 1])),

                field("Pagerank range",
                      dcc.RangeSlider(id="pagerank_range", min=0, max=1, value=[0, 1]))
            ]),

            dcc.Tab(label='JSON', children=[
                html.Div(style=styles['tab'], children=[
                    html.P('Node Object JSON:'),
                    html.Pre(
                        id='tap-node-json-output',
                        style=styles['json-output']
                    ),
                    html.P('Edge Object JSON:'),
                    html.Pre(
                        id='tap-edge-json-output',
                        style=styles['json-output']
                    )
                ])
            ])
        ]),

    ])
])

@app.callback(
    Output("graphAndTabs", "style"),
    Input("graph_active", "data")
)
def render_graph(graph_active):
    if graph_active:
        return styles["graphAndTabs"]
    return { "display": "none"}

# ############################## CALLBACKS ####################################

@app.callback(Output("cytoscape", "stylesheet"), Input("show_neigh_labels", "value"), Input("show_neigh_edge_labels", "value"),
              Input("use_pr_scaling", "value"), Input("edge_weight_range", "value"),
              Input("pagerank_range", "value"))
def graph_stylesheet(show_neigh_labels, show_neigh_edge_labels, use_pr_scaling, edge_weight_range,
                     pagerank_range):
    # shallow copy the list as we are only adding items to the list
    stylesheet = default_stylesheet.copy()
    if len(show_neigh_labels) > 0:
        stylesheet.append({
            'selector': '.selneighbor',
            "style": {
                "label": "data(label)",
                "color": "black",
                "font-size": 8,
                'z-index': 9999

            }
        })
    if len(show_neigh_edge_labels) > 0:
        stylesheet.append({
            'selector': '.selneighedge',
            "style": {
                "label": "data(weight)",
                "color": "black",
                "font-size": 8,
            }
        })
    if len(use_pr_scaling) > 0:
        stylesheet.append({
            'selector': 'node',
            "style": {
                'width': 'data(size)',
                'height': 'data(size)',
            }
        })
    if edge_weight_range:
        [min_weight, max_weight] = edge_weight_range
        stylesheet.append({
            'selector': f'edge[weight < {min_weight}],edge[weight > {max_weight}]',
            "style": {
                "display": "none"
            }
        })
    if pagerank_range:
        [min_pr, max_pr] = pagerank_range
        stylesheet.append({
            'selector': f'node[pr < {min_pr}],node[pr > {max_pr}]',
            "style": {
                "display": "none"
            }
        })
    return stylesheet

@app.callback(Output('tap-node-json-output', 'children'),
              [Input('cytoscape', 'tapNode')])
def display_tap_node(data):
    return json.dumps(data, indent=2)

@app.callback(Output('tap-edge-json-output', 'children'),
              [Input('cytoscape', 'tapEdge')])
def display_tap_edge(data):
    return json.dumps(data, indent=2)

@app.callback(Output('cytoscape', 'layout'),
              [Input('dropdown-layout', 'value')])
def update_cytoscape_layout(layout):
    return {'name': layout}

@app.callback(
    dict(
        new_options=dict(
            package=Output("package_dropdown", "options"),
            typedef=Output("typedef_dropdown", "options"),
            method=Output("method_dropdown", "options")
        ),
        focus_disabled=Output("focus_button", "disabled")
    ),
    inputs=dict(
        graph_active=State("graph_active", "data"),
        package=Input("package_dropdown", "value"),
        typedef=Input("typedef_dropdown", "value"),
        method=Input("method_dropdown", "value"),
        graph_params=Input("graph_params", "data"),
        old_options=dict(
            package=State("package_dropdown", "options"),
            typedef=State("typedef_dropdown", "options"),
            method=State("method_dropdown", "options")
        ),
    )
)
def update_search_options(graph_active, package, typedef, method, graph_params, old_options):
    """ Callback for updating dropdowns for searching a class/method/interface """
    new_options = old_options or { "package": [], "typedef": [], "method": []}
    if not graph_params or not graph_active:
        return { "new_options": new_options, "focus_disabled": True }
    state = graph_params_to_state(graph_params)
    verts = state.vertices_by_communities_prs

    def dfv_to_options(dfv: pd.Series):
        return [{
            "label": str(item),
            "value": str(item)
        } for item in dfv.unique()]

    new_options["package"] = dfv_to_options(
        verts["package"])
    if package and "class" in verts.columns:
        new_options["typedef"] = dfv_to_options(
            verts[verts["package"] == package]["class"])
    if typedef and "method" in verts.columns:
        new_options["method"] = dfv_to_options(
            verts[verts["class"] == typedef]["method"])

    can_focus = ((state.raw.hierch == HierarchyType.package and package) or
                 (state.raw.hierch == HierarchyType.type_def and typedef) or
                 (state.raw.hierch == HierarchyType.method and method)
    )

    return { "new_options": new_options, "focus_disabled": not can_focus }

@app.callback(Output("num_nodes_add", "max"), Input("graph_active", "data"), Input("graph_params", "data"),
              State("cytoscape", "elements"))
def update_max_nodes_to_add(graph_active, graph_params, elements):
    if not graph_active or not graph_active:
        return 0

    n_cur_nodes = sum(1 for el in elements if el.get("data").get("name"))
    state = graph_params_to_state(graph_params)
    return len(state.vertices_by_communities_prs) - n_cur_nodes

@app.callback(Output("edge_weight_range", "max"), Output("pagerank_range", "max"),
              Input("graph_active", "data"),Input("graph_params", "data"))
def update_max_edge_weight_and_pr(is_active, graph_params):
    if not is_active:
        return 1, 1
    state = graph_params_to_state(graph_params)
    return max(state.graph.es["weight"]), max(state.graph.vs["pr"])

def handle_add_nodes(state: GraphState, elements, num_nodes_to_add, add_edges_opt):
    """ Adds nodes(and possibly their edges) to the graph.
        The number of nodes being added is divided evenly among all communities,
        and within each community, added in descending order of page-rank values.
        This should ensure that the nodes added to the visualization are more
        representative, without biasing towards a particular dominant community.
    """
    if not num_nodes_to_add:
        return elements

    # first, determine candidate nodes -
    # those not already present in the graph
    existing_node_names = set(
        el.get("data").get("name") for el in elements
    )

    df = state.vertices_by_communities_prs
    df = df[~(df["name"].isin(existing_node_names))]

    if len(df) == 0:
        return elements

    # split the number of nodes added within each community,
    # evenly.
    communities = df.index.get_level_values(0).unique()
    n_communities = len(communities)
    take_per_comm = num_nodes_to_add // n_communities
    take_remainder = num_nodes_to_add % n_communities

    node_names = set()
    for ix, comm_ix in enumerate(communities):
        to_take = take_per_comm
        if ix == 0:
            to_take += take_remainder
        node_names.update(df.loc[comm_ix].iloc[:to_take]["name"])

    new_node_names = node_names - existing_node_names
    new_nodes = state.graph.vs.select(name_in=new_node_names)

    new_elements = elements + [igraph_vert_to_cyto(state.graph, node, []) for node in new_nodes ]
    if add_edges_opt:
        subgraph = state.graph.induced_subgraph(new_nodes)
        new_edges = get_filtered_edges(elements, subgraph.es)
        new_elements.extend(igraph_edge_to_cyto(subgraph, edge, []) for edge in new_edges)

    return new_elements

def handle_focus_node(state: GraphState, elements, focus_values):
    """ Focuses on a node, given an array of values for package, class and method
        dropdown inputs. """
    name = ".".join(str(val) for val in focus_values if val)
    found_genesis = False
    graph = state.graph
    # check if the node we're looking for already exists in the graph
    for element in elements:
        element["classes"] = element["classes"].replace("genesis", "")
        if element.get("data").get("name") == name:
            found_genesis = True
            element["classes"] = element["classes"] + " genesis"
            break
    # otherwise, add it
    if not found_genesis:
        # TODO: sometimes this might fail to find, how is it possible?
        elements.append(igraph_vert_to_cyto(graph, graph.vs.find(name=name), classes=["genesis"]))

    return elements

def handle_tap_node(state: GraphState, elements, nodeData, expansion_mode):
    """ """
    graph = state.graph
    # tapped a node, try to expand
    if expansion_mode == "dont":
        return elements
    do_expand = True
    # If the node has already been expanded, we don't expand it again
    if nodeData.get(f'expanded-{expansion_mode}') or nodeData.get('expanded-all'):
        do_expand = False

    # This retrieves the currently selected element, and tag it as expanded
    for element in elements:
        if nodeData['id'] == element.get('data').get('id'):
            element['data'][f'expanded-{expansion_mode}'] = True
            break

    neigh_nodes = graph.neighbors(nodeData['id'], expansion_mode)
    neigh_names = set(graph.vs[ix]["name"] for ix in neigh_nodes)
    neigh_edges = get_filtered_edges(elements, (graph.es[ix]
                                        for ix in graph.incident(nodeData['id'], expansion_mode)))
    node_class, edge_class = "", ""
    if expansion_mode == "in":
        node_class, edge_class = "followerNode", "followerEdge"
    elif expansion_mode == "out":
        node_class, edge_class = "followingNode", "followingEdge"

    if do_expand:
        elements.extend(igraph_vert_to_cyto(graph, graph.vs[node_ix], classes=[node_class, "selneighbor"]) for node_ix in neigh_nodes)
        elements.extend(igraph_edge_to_cyto(graph, edge, classes=[edge_class, "selneighedge"])
                        for edge in neigh_edges)

    for element in elements:
        el_id = element.get('data').get('id')
        source, tgt = element.get("data").get("source"), element.get("data").get("target")
        if el_id not in neigh_names:
            element["classes"] = element["classes"].replace("selneighbor", "")
        if source and tgt:
            if source not in neigh_names and tgt not in neigh_names:
                element["classes"] = element["classes"].replace("selneighedge", "")
    return elements

@app.callback(Output('cytoscape', 'elements'),
              inputs=dict(
                  graph_active=Input("graph_active", "data"),
                  graph_params=Input("graph_params", "data"),
                  nodeData=Input("cytoscape", "tapNodeData"),
                  focus=Input("focus_button", "n_clicks"),
                  add_edge=Input("add_button", "n_clicks")
              ),
              state=dict(
                  focus_values=[State("package_dropdown", "value"),
                                State("typedef_dropdown", "value"),
                                State("method_dropdown", "value")],
                  elements=State("cytoscape", "elements"),
                  expansion_mode=State("expansion_mode", "value"),
                  num_nodes_to_add=State("num_nodes_add", "value"),
                  add_edges_opt=State("add_edges", "value"),
              ))
def generate_elements(graph_active, graph_params, nodeData, focus, add_edge, focus_values, elements, expansion_mode,
                      num_nodes_to_add, add_edges_opt):
    """ This callback is responsible for generating the graph's elements, therefore
        it needs to respond to every input that can affect the graph - hence
        the huge amount of inputs.
    """
    if not graph_active:
        return []
    state = graph_params_to_state(graph_params)
    graph = state.graph

    ctx = dash.callback_context
    if not ctx.triggered:
        return []

    changed_inputs =set(
        prop['prop_id'].split('.')[0] for prop in ctx.triggered
    )
    tappedANode = any("cytoscape.tapNodeData" in prop['prop_id'] for prop in ctx.triggered)

    focus_node = "focus_button" in changed_inputs
    add_edge = "add_button" in changed_inputs
    reload_graph = any(v in changed_inputs for v in ("import_dir", "graph_params"))
    if focus_node:
        return handle_focus_node(state, elements, focus_values)
    elif add_edge:
        return handle_add_nodes(state, elements, num_nodes_to_add, add_edges_opt)
    elif (not nodeData) or reload_graph:
        print("Graph is being reloaded")
        return [igraph_vert_to_cyto(graph, graph.vs[0], classes=["genesis"])]

    elif tappedANode:
        return handle_tap_node(state, elements, nodeData, expansion_mode)
    else:
        return elements


import datetime as dt
from random import randint

from django.test import SimpleTestCase
from django.urls import reverse
from django.utils import timezone
from freezegun import freeze_time

from zoo_checks.ingest import TRACKS_REQ_COLS
from zoo_checks.models import AnimalCount, Enclosure
from zoo_checks.views import (
    enclosure_counts_to_dict,
    get_accessible_enclosures,
    get_selected_role,
    redirect_if_not_permitted,
)

DST_DATETIME = dt.datetime(2021, 3, 14, 12, 0, 0)

def test_home(client, user_base):
    client.force_login(user_base)
    url = reverse("home")
    resp = client.get(url)

    assert resp.status_code == 200
    assert resp.context["selected_role"] is None
    assert user_base.first_name in resp.content.decode()
    assert (
        "No enclosures. Contact a manager to be added to a role"
        in resp.content.decode()
    )

def test_home_counts(client, create_many_counts, user_base):

    num_enc = 7
    num_anim = 4
    num_species = 5

    create_many_counts(
        num_enc=num_enc,
        num_anim=num_anim,
        num_species=num_species,
    )

    client.force_login(user_base)
    url = reverse("home")
    resp = client.get(url)

    assert resp.status_code == 200
    assert list(resp.context["roles"]) == list(user_base.roles.all())
    assert resp.context["selected_role"] is None
    assert "Individuals" in resp.content.decode()

def test_count(
    client, user_base, enclosure_base, animal_A, animal_count_A_BAR, group_B
):
    client.force_login(user_base)

    # GET

    resp = client.get(f"/count/{enclosure_base.slug}/")
    assert resp.context["enclosure"] == enclosure_base
    assert resp.context["conditions"] == AnimalCount.CONDITIONS

    # untested context:
    # "dateday": dateday,
    # "species_formset": species_formset,
    # "groups_formset": groups_formset,
    # "animals_formset": animals_formset,
    # "formset_order": formset_order,
    # "dateform": dateform,
    # "conditions": AnimalCount.CONDITIONS,

    # test a different date

    # create some counts

    # todo:
    # POST

def test_count_todays_date(
    client, user_base, enclosure_base, animal_A, animal_count_A_BAR, group_B
):
    """ whenever you ge the count, it should always have todays date in it """

    client.force_login(user_base)

    resp = client.get(f"/count/{enclosure_base.slug}/")
    assert "Today's Count" in resp.content.decode()

@freeze_time(DST_DATETIME)  # daylight savings
def test_count_todays_date_dst(
    client, user_base, enclosure_base, animal_A, animal_count_A_BAR, group_B
):
    """mock the current date to be on daylight savings (@12pm)
    we should still be seeing "todays date" -- this requires us being at 12pm currently
    because today_time utility uses localtime to generate a date
    """

    client.force_login(user_base)

    resp = client.get(f"/count/{enclosure_base.slug}/")
    assert "Today's Count" in resp.content.decode()
    assert resp.context["todays_date"] == DST_DATETIME.strftime("%Y-%m-%d")

def test_tally_date_handler(client, enclosure_base, user_base):
    client.force_login(user_base)

    yesterday = dt.date.today() - dt.timedelta(days=1)
    tomorrow = dt.date.today() + dt.timedelta(days=1)

    resp = client.post(f"/tally_date_handler/{enclosure_base.slug}")
    assert resp.status_code == 302
    SimpleTestCase().assertRedirects(resp, f"/count/{enclosure_base.slug}/")

    resp = client.get(f"/tally_date_handler/{enclosure_base.slug}")
    assert resp.status_code == 302
    SimpleTestCase().assertRedirects(resp, f"/count/{enclosure_base.slug}/")

    resp = client.post(
        f"/tally_date_handler/{enclosure_base.slug}",
        {"tally_date": yesterday.strftime("%m/%d/%Y")},
    )
    assert resp.status_code == 302
    SimpleTestCase().assertRedirects(
        resp,
        (
            "/count/"
            f"{enclosure_base.slug}/{yesterday.year}/{yesterday.month}/{yesterday.day}/"
        ),
    )

    resp = client.post(
        f"/tally_date_handler/{enclosure_base.slug}",
        {"tally_date": "not_a_date"},
    )
    assert resp.status_code == 302
    # normally this is in resp.context but context is None since we're doing a redirect
    messages = list(resp.wsgi_request._messages)
    assert messages[0].message == "Error in date entered"
    SimpleTestCase().assertRedirects(resp, f"/count/{enclosure_base.slug}/")

    resp = client.post(
        f"/tally_date_handler/{enclosure_base.slug}",
        {"tally_date": tomorrow.strftime("%m/%d/%Y")},
    )
    assert resp.status_code == 302
    messages = list(resp.wsgi_request._messages)
    assert messages[0].message == "Error in date entered"
    SimpleTestCase().assertRedirects(resp, f"/count/{enclosure_base.slug}/")

def test_edit_species_count(
    client,
    user_base,
    species_base,
    enclosure_base,
    enclosure_factory,
    species_base_count,
):
    client.force_login(user_base)

    yesterday_time = timezone.localtime() - dt.timedelta(days=1)
    yesterday = yesterday_time.date()

    enc_not_permit = enclosure_factory("not_permit", None)

    # not permitted
    resp = client.get(
        (
            "/edit_species_count/"
            f"{species_base.slug}/{enc_not_permit.name}"
            f"/{yesterday.year}/{yesterday.month}/{yesterday.day}/"
        )
    )
    assert resp.status_code == 302
    SimpleTestCase().assertRedirects(resp, "/")

    count = species_base_count(yesterday_time)

    # GET
    resp = client.get(
        (
            "/edit_species_count/"
            f"{species_base.slug}/{enclosure_base.name}/"
            f"{yesterday.year}/{yesterday.month}/{yesterday.day}/"
        )
    )
    assert resp.status_code == 200
    assert resp.context["species"] == species_base
    assert resp.context["enclosure"] == enclosure_base
    assert resp.context["dateday"].year == yesterday.year
    assert resp.context["dateday"].month == yesterday.month
    assert resp.context["dateday"].day == yesterday.day
    assert resp.context["count"] == count
    # todo: form context

    # POST
    post_data = {
        "count": randint(1, 400),
        "species": species_base.id,
        "enclosure": enclosure_base.id,
    }
    resp = client.post(
        (
            "/edit_species_count/"
            f"{species_base.slug}/{enclosure_base.name}/"
            f"{yesterday.year}/{yesterday.month}/{yesterday.day}/"
        ),
        data=post_data,
        follow=True,
    )
    assert resp.status_code == 200
    assert resp.redirect_chain == [(f"/count/{enclosure_base.slug}/", 302)]
    assert species_base.counts.latest("datetimecounted").count == post_data["count"]

def test_edit_group_count(
    client,
    user_base,
    group_B,
    group_factory,
    enclosure_base,
    enclosure_factory,
    group_B_count_datetime_factory,
):
    client.force_login(user_base)

    yesterday_time = timezone.localtime() - dt.timedelta(days=1)
    yesterday = yesterday_time.date()

    enc_not_permit = enclosure_factory("not_permit", None)
    group_not_permit = group_factory("abc123", 5, 4, 3, 12, enclosure=enc_not_permit)

    # not permitted
    resp = client.get(
        (
            "/edit_group_count/"
            f"{group_not_permit.accession_number}/"
            f"{yesterday.year}/{yesterday.month}/{yesterday.day}/"
        )
    )
    assert resp.status_code == 302
    SimpleTestCase().assertRedirects(resp, "/")

    count = group_B_count_datetime_factory(yesterday_time)

    # GET
    resp = client.get(
        (
            "/edit_group_count/"
            f"{group_B.accession_number}/"
            f"{yesterday.year}/{yesterday.month}/{yesterday.day}/"
        )
    )
    assert resp.status_code == 200
    assert resp.context["count"] == count
    assert resp.context["group"] == group_B
    assert resp.context["enclosure"] == enclosure_base
    assert resp.context["dateday"].year == yesterday.year
    assert resp.context["dateday"].month == yesterday.month
    assert resp.context["dateday"].day == yesterday.day
    # todo: form context

    # POST
    count_bar = randint(1, 400)
    count_seen = count_bar + randint(5, 25)
    count_total = count_bar + count_seen
    post_data = {
        "count_seen": count_seen,
        "enclosure": enclosure_base.id,
        "count_bar": count_bar,
        "comment": "this is a randomly generated count",
        "group": group_B.id,
        "count_total": count_total,
        "needs_attn": False,
    }
    resp = client.post(
        (
            "/edit_group_count/"
            f"{group_B.accession_number}/"
            f"{yesterday.year}/{yesterday.month}/{yesterday.day}/"
        ),
        data=post_data,
        follow=True,
    )
    assert resp.status_code == 200
    assert resp.redirect_chain == [(f"/count/{enclosure_base.slug}/", 302)]

    latest_count = group_B.counts.latest("datetimecounted")
    assert latest_count.count_seen == post_data["count_seen"]
    assert latest_count.count_bar == post_data["count_bar"]

def test_edit_animal_count(
    client,
    user_base,
    enclosure_factory,
    animal_factory,
    animal_A,
    enclosure_base,
    animal_count_A_BAR_datetime_factory,
):
    client.force_login(user_base)

    yesterday_time = timezone.localtime() - dt.timedelta(days=1)
    yesterday = yesterday_time.date()

    enc_not_permit = enclosure_factory("not_permit", None)

    anim_not_permit = animal_factory(
        name="kermitthefrog",
        identifier="green",
        sex="m",
        accession_number="abc123",
        enclosure=enc_not_permit,
    )

    # not permitted
    resp = client.get(
        (
            "/edit_animal_count/"
            f"{anim_not_permit.accession_number}/"
            f"{yesterday.year}/{yesterday.month}/{yesterday.day}/"
        )
    )
    assert resp.status_code == 302
    SimpleTestCase().assertRedirects(resp, "/")

    count = animal_count_A_BAR_datetime_factory(yesterday_time)

    # GET
    resp = client.get(
        (
            "/edit_animal_count/"
            f"{animal_A.accession_number}/"
            f"{yesterday.year}/{yesterday.month}/{yesterday.day}/"
        )
    )
    assert resp.status_code == 200
    assert resp.context["count"] == count
    assert resp.context["animal"] == animal_A
    assert resp.context["enclosure"] == enclosure_base
    assert resp.context["dateday"].year == yesterday.year
    assert resp.context["dateday"].month == yesterday.month
    assert resp.context["dateday"].day == yesterday.day
    # todo: form context

    # POST
    post_data = {
        "condition": "NA",
        "comment": "this is a test count",
        "animal": animal_A.id,
        "enclosure": enclosure_base.id,
    }
    resp = client.post(
        (
            "/edit_animal_count/"
            f"{animal_A.accession_number}/"
            f"{yesterday.year}/{yesterday.month}/{yesterday.day}/"
        ),
        data=post_data,
        follow=True,
    )
    assert resp.status_code == 200
    assert resp.redirect_chain == [(f"/count/{enclosure_base.slug}/", 302)]

    latest_count = animal_A.conditions.latest("datetimecounted")
    assert latest_count.condition == "NA"
    assert latest_count.comment == "this is a test count"
    assert latest_count.enclosure == enclosure_base

def test_animal_counts(
    client,
    animal_A,
    animal_count_factory,
    user_base,
    user_factory,
    enclosure_base,
):

    num_counts = 100

    rando_user = user_factory("rando")
    client.force_login(rando_user)
    url = reverse("animal_counts", args=[animal_A.accession_number])
    resp = client.get(url)
    assert resp.status_code == 302
    messages = list(resp.wsgi_request._messages)
    assert (
        messages[0].message
        == f"You do not have permissions to access enclosure {animal_A.enclosure}"
    )

    counts = []
    for d in range(num_counts):
        counts.append(
            animal_count_factory(
                "BA",
                timezone.localtime() - dt.timedelta(days=d),
                comment=f"test count on day {d}",
            )
        )

    # test some counts w/ context
    client.force_login(user_base)
    url = reverse("animal_counts", args=[animal_A.accession_number])
    resp = client.get(url)
    assert resp.status_code == 200
    assert resp.context["animal"] == animal_A
    assert resp.context["enclosure"] == enclosure_base
    assert list(resp.context["animal_counts"]) == counts[:10]
    assert resp.context["page_range"][0] == 1
    assert resp.context["page_range"][-1] == 1 + 5
    # chart_data
    # chart_labels

    # test pagination
    url = "{}{}".format(
        reverse("animal_counts", args=[animal_A.accession_number]), f"?page={2}"
    )
    resp = client.get(url)
    assert resp.status_code == 200
    assert list(resp.context["animal_counts"]) == counts[10:20]
    assert resp.context["page_range"][0] == 1
    assert resp.context["page_range"][-1] == 2 + 5

def test_group_counts(
    client,
    group_B,
    group_B_count_datetime_factory,
    user_base,
    user_factory,
    enclosure_base,
):
    num_counts = 120

    # test perms
    rando_user = user_factory("rando")
    client.force_login(rando_user)
    url = reverse("group_counts", args=[group_B.accession_number])
    resp = client.get(url)
    assert resp.status_code == 302  # redirect to home
    messages = list(resp.wsgi_request._messages)
    assert (
        messages[0].message
        == f"You do not have permissions to access enclosure {group_B.enclosure}"
    )

    counts = []
    for d in range(num_counts):
        counts.append(
            group_B_count_datetime_factory(timezone.localtime() - dt.timedelta(days=d))
        )

    # test some counts w/ context
    client.force_login(user_base)
    url = reverse("group_counts", args=[group_B.accession_number])
    resp = client.get(url)
    assert resp.status_code == 200
    assert resp.context["group"] == group_B
    assert resp.context["enclosure"] == enclosure_base
    assert list(resp.context["counts"]) == counts[:10]
    assert resp.context["page_range"][0] == 1
    assert resp.context["page_range"][-1] == 1 + 5

    # test pagination
    url = "{}{}".format(
        reverse("group_counts", args=[group_B.accession_number]), f"?page={2}"
    )
    resp = client.get(url)
    assert resp.status_code == 200
    assert list(resp.context["counts"]) == counts[10:20]
    assert resp.context["page_range"][0] == 1
    assert resp.context["page_range"][-1] == 2 + 5

def test_species_counts(
    client, species_count_factory, species_base, user_base, user_factory, enclosure_base
):
    num_counts = 30

    # test perms
    rando_user = user_factory("rando")
    client.force_login(rando_user)
    url = reverse("species_counts", args=[species_base.slug, enclosure_base.slug])
    # should redirect to home
    resp = client.get(url)
    assert resp.status_code == 302
    messages = list(resp.wsgi_request._messages)
    assert (
        messages[0].message
        == f"You do not have permissions to access enclosure {enclosure_base}"
    )

    # create some counts
    counts = []
    for d in range(num_counts):
        counts.append(
            species_count_factory(
                50, datetimecounted=timezone.localtime() - dt.timedelta(days=d)
            )
        )

    # test some counts w/ context
    client.force_login(user_base)
    url = reverse("species_counts", args=[species_base.slug, enclosure_base.slug])
    resp = client.get(url)
    assert resp.status_code == 200
    assert resp.context["obj"] == species_base
    assert resp.context["enclosure"] == enclosure_base
    assert list(resp.context["counts"]) == counts[:10]
    assert resp.context["page_range"][0] == 1
    assert resp.context["page_range"][-1] == 3
    # "chart_data_line_total"
    # "chart_labels_line"
    # "chart_data_pie"
    # "chart_labels_pie"

def test_ingest_form(client, user_base, user_super):
    client.force_login(user_base)

    url = reverse("ingest_form")
    resp = client.get(url)
    assert resp.status_code == 302
    SimpleTestCase().assertRedirects(resp, "/accounts/login/", target_status_code=302)

    client.force_login(user_super)
    resp = client.get(url)
    assert resp.status_code == 200
    assert resp.context["req_cols"] == TRACKS_REQ_COLS

    # todo: test session data manipulation (changesets/upload_file)

    # TODO: POST

def test_confirm_upload():
    # /confirm_upload

    # changesets and upload_file in session

    # test GET

    # test POST (writes changes to db)
    pass

def test_export(client, user_base, enclosure_base, user_factory, caplog):

    # GET

    # user w/ no enclosures empty list of enclosures
    rando_user = user_factory("rando")
    client.force_login(rando_user)
    resp = client.get("/export/")
    assert resp.status_code == 200
    assert list(resp.context["form"].fields["selected_enclosures"].queryset) == list(
        Enclosure.objects.none()
    )

    # user_base has enclosure_base in list of options
    client.force_login(user_base)
    resp = client.get("/export/")
    assert resp.status_code == 200
    assert enclosure_base.name in resp.content.decode()
    assert (
        resp.context["form"].fields["selected_enclosures"].queryset[0] == enclosure_base
    )

    # POST
    # still with user_base logged in
    yesterday = dt.date.today() - dt.timedelta(days=1)

    caplog.clear()
    # w/ no counts
    resp = client.post(
        "/export/",
        {
            "start_date": yesterday.strftime("%m/%d/%Y"),
            "end_date": dt.date.today().strftime("%m/%d/%Y"),
            "selected_enclosures": enclosure_base.id,
        },
    )
    assert resp.status_code == 200
    record = caplog.records[0]

    assert record.message == "no data to export for enclosures"
    assert record.enclosures[0]["name"] == enclosure_base.name
    assert record.start_date == yesterday.strftime("%m/%d/%Y")
    assert record.end_date == dt.date.today().strftime("%m/%d/%Y")

    # create some counts
    # assert 201 status code
    # load in excel data, convert to dataframe and check the counts

def test_get_accessible_enclosures(
    user_base, enclosure_base, enclosure_factory, user_super
):
    enclosures = get_accessible_enclosures(user_base)
    assert list(enclosures) == [enclosure_base]

    # create a bunch of enclosures, but don't assign to role
    enc_set = {enclosure_base}
    for i in range(10):
        enc_set.add(enclosure_factory(str(i), role=None))

    # super user should get all the enclosures
    enclosures_super = get_accessible_enclosures(user_super)
    assert set(enclosures_super) == enc_set

    # regular user should still have the same single enclosure
    enclosures = get_accessible_enclosures(user_base)
    assert list(enclosures) == [enclosure_base]

def test_redirect_if_not_permitted(
    rf_get_factory, enclosure_factory, enclosure_base, user_super
):
    """uses our request factory to directly test `redirect_if_not_permitted`
    without actually making requests to any endpoints
    """

    request = rf_get_factory("/count/")
    forbidden_enc = enclosure_factory("forbidden_enc", role=None)

    # test if user doesn't have role that includes enclosure (True)
    assert redirect_if_not_permitted(request, forbidden_enc)

    # test if user has role that includes enclosure (False)
    assert not redirect_if_not_permitted(request, enclosure_base)

    # test if user is superuser (False)
    request.user = user_super
    assert not redirect_if_not_permitted(request, enclosure_base)
    assert not redirect_if_not_permitted(request, forbidden_enc)

def test_selected_role(rf_get_factory, role_base):

    # test view all query param returns None and clears session
    request = rf_get_factory("/home/?view_all=true")
    request.session["selected_role"] = "role_base"
    selected_role = get_selected_role(request)
    assert selected_role is None
    assert request.session.get("selected_role") is None

    # test role in session & no query params returns session role
    request = rf_get_factory("/home/")
    request.session["selected_role"] = "role_base"
    selected_role = get_selected_role(request)
    assert selected_role == role_base
    assert request.session.get("selected_role") == "role_base"

    # test role query param returns role and sets session
    request = rf_get_factory("/home/?role=role_base")
    selected_role = get_selected_role(request)
    assert selected_role == role_base
    assert request.session.get("selected_role") == "role_base"

    # test role query param and diff session query param returns query param role and
    # sets session
    request = rf_get_factory("/home/?role=role_base")
    request.session["selected_role"] = "diff_role"
    selected_role = get_selected_role(request)
    assert selected_role == role_base
    assert request.session.get("selected_role") == "role_base"

    # test absence of all of these return None
    request = rf_get_factory("/home/")
    selected_role = get_selected_role(request)
    assert selected_role is None

def test_enclosure_counts_to_dict(create_many_counts, django_assert_num_queries):
    """
    Tests the dictionary creation from list of counts
    Tests the structure of the dict
    """
    num_enc = 7
    num_anim = 4
    num_species = num_groups = 5

    _, _, _, enc_list = create_many_counts(
        num_enc=num_enc,
        num_anim=num_anim,
        num_species=num_species,
    )

    # create a query similar to how we build it in the view
    encl_q = Enclosure.objects.filter(name__in=enc_list).prefetch_related(
        "animals", "groups"
    )
    with django_assert_num_queries(5):
        # 3 for enclosures, groups, animals (w/ prefetch related)
        # 2 for animal_counts and group_counts

        animal_counts, group_counts = Enclosure.all_counts(encl_q)
        all_counts_dict = enclosure_counts_to_dict(encl_q, animal_counts, group_counts)

    # assert dict keys present in original query
    assert list(encl_q) == list(all_counts_dict.keys())

    # assert dict structure
    for enc in enc_list:
        enc_dict = all_counts_dict[enc]

        keys = {
            "animal_count_total",
            "animal_conditions",
            "group_counts",
            "group_count_total",
            "total_animals",
            "total_groups",
        }
        assert set(enc_dict.keys()) == keys

        assert enc_dict["animal_count_total"] == num_anim
        assert enc_dict["total_animals"] == num_anim

        # (1+3) Seen/BAR from input to group_count_factory
        assert enc_dict["group_count_total"] == num_groups * (1 + 3)

        # pop total = 30
        assert enc_dict["total_groups"] == num_groups * 30

        assert list(enc_dict["animal_conditions"].keys()) == [
            c[1] for c in AnimalCount.CONDITIONS
        ]
        assert all(
            [
                all([isinstance(c, AnimalCount) for c in c_l])
                for c_l in enc_dict["animal_conditions"].values()
            ]
        )

        assert list(enc_dict["group_counts"].keys()) == ["Seen", "BAR", "Needs Attn"]
        # These values come from the factory inputs to create group_counts
        assert enc_dict["group_counts"]["Seen"] == num_groups
        assert enc_dict["group_counts"]["BAR"] == num_groups * 3
        assert enc_dict["group_counts"]["Needs Attn"] == 0


from Tkinter import *
import random

###########################################################################
# Author: Alex Herman, Andrew Fagrey
# Description: Class that contains the code for the process scheduler part
#  of the 4th CSC456 program.
###########################################################################
class processFrame(Frame):

	###########################################################################
	# Author: Alex Herman, Andrew Fagrey
	# Description: This function is the class constructor
	###########################################################################
	def __init__(self):
		self.parent = Tk()
		self.parent.title("Process Management Algorithms")
		self.parent.geometry("800x400+300+300")
		Frame.__init__(self, self.parent)
		self.grid()

		self.initUI();
		self.parent.mainloop()
	############################# END OF FUNCTION #############################

	###########################################################################
	# Author: Andrew Fagrey
	# Description: This function is responsible for sorting the process lists
	#  by their priority
	###########################################################################
	def sortProcessesByPriority(self):
		sizeOfList = int(self.processCountBox.get())
		for i in range(sizeOfList-1, 0, -1):
			for j in range(i):
				if self.processPrioriy[j] > self.processPrioriy[j+1]:
					# swap process priorities
					swap = self.processPrioriy[j]
					self.processPrioriy[j] = self.processPrioriy[j+1]
					self.processPrioriy[j+1] = swap
					# swap process burst times
					swap = self.processBurstTime[j]
					self.processBurstTime[j] = self.processBurstTime[j+1]
					self.processBurstTime[j+1] = swap
					# swap process name
					swap = self.processName[j]
					self.processName[j] = self.processName[j+1]
					self.processName[j+1] = swap
					# swap process arrival time
					swap = self.processArrival[j]
					self.processArrival[j] = self.processArrival[j+1]
					self.processArrival[j+1] = swap
	############################# END OF FUNCTION #############################

	###########################################################################
	# Author: Andrew Fagrey
	# Description: This function is responsible for sorting the process lists
	#  by their burst times
	###########################################################################
	def sortProcessesByBurstTime(self):
		sizeOfList = int(self.processCountBox.get())
		for i in range(sizeOfList-1, 0, -1):
			for j in range(i):
				if self.processBurstTime[j] > self.processBurstTime[j+1]:
					# swap process burst times
					swap = self.processBurstTime[j]
					self.processBurstTime[j] = self.processBurstTime[j+1]
					self.processBurstTime[j+1] = swap
					# swap process priorities
					swap = self.processPrioriy[j]
					self.processPrioriy[j] = self.processPrioriy[j+1]
					self.processPrioriy[j+1] = swap
					# swap process name
					swap = self.processName[j]
					self.processName[j] = self.processName[j+1]
					self.processName[j+1] = swap
					# swap process arrival time
					swap = self.processArrival[j]
					self.processArrival[j] = self.processArrival[j+1]
					self.processArrival[j+1] = swap
	############################# END OF FUNCTION #############################

	###########################################################################
	# Author: Andrew Fagrey
	# Description: This function is responsible for sorting the process lists
	#  by their arrival time
	###########################################################################
	def sortProcessesByArrivalTime(self):
		sizeOfList = int(self.processCountBox.get())
		for i in range(sizeOfList-1, 0, -1):
			for j in range(i):
				if self.processArrival[j] > self.processArrival[j+1]:
					# swap process arrival time
					swap = self.processArrival[j]
					self.processArrival[j] = self.processArrival[j+1]
					self.processArrival[j+1] = swap
					# swap process burst times
					swap = self.processBurstTime[j]
					self.processBurstTime[j] = self.processBurstTime[j+1]
					self.processBurstTime[j+1] = swap
					# swap process priorities
					swap = self.processPrioriy[j]
					self.processPrioriy[j] = self.processPrioriy[j+1]
					self.processPrioriy[j+1] = swap
					# swap process name
					swap = self.processName[j]
					self.processName[j] = self.processName[j+1]
					self.processName[j+1] = swap
	############################# END OF FUNCTION #############################

	###########################################################################
	# Author: Andrew Fagrey
	# Description: This function adds the process burst times and stores the
	#  total in a global variable
	###########################################################################
	def addBurstTimes(self):
		self.burstTimeTotal = 0
		for i in range(int(self.processCountBox.get())):
			self.burstTimeTotal = self.burstTimeTotal + self.processBurstTime[i]
	############################# END OF FUNCTION #############################

	###########################################################################
	# Author: Andrew Fagrey
	# Description: This function generates the process list
	###########################################################################
	def generateProcessesList(self):
		random.seed()
		self.processName = []
		self.processBurstTime = []
		self.processPrioriy = []
		self.processArrival = []
		spcr = "     "
		#remove all elelments in the list
		self.processListBox.delete(0, END)
		for i in range(0, int(self.processCountBox.get())): #maybe check that there is actually a number here
			self.processName.append("P" + str(i+1))
			self.processBurstTime.append(int(random.randint(1,25)))
			self.processPrioriy.append(int(random.randint(1,9)))
			self.processArrival.append(int(i+1))
			processLabel = self.processName[i] + ":"+ spcr + str(self.processBurstTime[i]).zfill(2) + spcr + str(self.processPrioriy[i]) + spcr + str(self.processArrival[i]).zfill(2)
			self.processListBox.insert(END, processLabel)
	############################# END OF FUNCTION #############################

	###########################################################################
	# Author: Andrew Fagrey
	# Description: This function checks that a copy of the burst list still has
	#  values that are greater than zero in it
	###########################################################################
	def checkBurstListForValues(self):
		count = int(self.processCountBox.get())
		for i in range(0, count):
			if self.roundRobinBurstList[i] > 0:
				return False
		return True
	############################# END OF FUNCTION #############################

	###########################################################################
	# Author: Andrew Fagrey
	# Description: A function to copy the processBurstTime list to another list
	###########################################################################
	def copyBurstList(self):
		count = int(self.processCountBox.get())
		for i in range(0, count):
			self.roundRobinBurstList.append(self.processBurstTime[i])
	############################# END OF FUNCTION #############################

	###########################################################################
	# Author: Andrew Fagrey
	# Description: Function for generating the gantt chart for Priority Sch.
	###########################################################################
	def roundRobinGanttChart(self):
		# clear the frames
		self.setProcessesFrame()

		# get time quanta
		quanta = int(self.timeQuantaBox.get())

		# make copy of burst time list
		self.roundRobinBurstList = []
		self.copyBurstList()

		# build the round robin list
		roundRobinBurstQueue = []
		roundRobinNameQueue = []

		# set up initial loop condition variable
		quitLoop = self.checkBurstListForValues()

		# variables used in the loop below
		width = 0
		row = 0
		col = 0
		totalTime = 0
		processList = []
		colT = 0

		# keep looping through the burst list till all values are less then zero
		while quitLoop == False:
			for i in range(0, int(self.processCountBox.get())):
				if self.roundRobinBurstList[i] > 0:
					leftOver = self.roundRobinBurstList[i] - quanta
					if leftOver <= 0:
						width = self.roundRobinBurstList[i]
					else:
						width = quanta
					roundRobinNameQueue.append(self.processName[i])
					self.roundRobinBurstList[i] = self.roundRobinBurstList[i] - quanta
					# display shapes and calculate size
					Label(self.processDisplay, text=roundRobinNameQueue[i]).grid(row=row, column=col+1)
					shapeCan = Canvas(self.processDisplay, width=width+1, height=40)
					shapeCan.create_rectangle(0, 0, width+1, 50, fill="green")
					shapeCan.grid(row=row+1, column=col+1)
					# display the label for burst time
					Label(self.processDisplay, text=str(totalTime)).grid(row=row+2, column=col)
					totalTime = totalTime + width

					# update row and column
					if col > 20:
						col = 0
						row = row + 3
					else:
						col = col + 2

			quitLoop = self.checkBurstListForValues()
		Label(self.processDisplay, text=str(totalTime)).grid(row=row+2, column=col)
	############################# END OF FUNCTION #############################

	###########################################################################
	# Author: Andrew Fagrey
	# Description: Function for generating the gantt chart for Priority Sch.
	###########################################################################
	def priorityGanttChart(self):
		# clear the window of other gantt charts
		self.setProcessesFrame()

		# sort list by priority
		self.sortProcessesByPriority()

		# add the burst times (used for calculating the width of the shapes)
		self.addBurstTimes()

		# some variables used below in the loop
		processList = []
		col = 0
		totalTime = 0

		# loop through the number of processes and display the gantt chart
		for i in range(0, int(self.processCountBox.get())):
			# show the process label
			Label(self.processDisplay, text=self.processName[i]).grid(row=0, column=col+1)

			# calculate the width of the shape
			width = (300 / self.burstTimeTotal) * self.processBurstTime[i]

			# add to the shape the process list and display it in the window
			shapeCan = Canvas(self.processDisplay, width=width, height=50)
			processList.append(shapeCan)
			processList[i].create_rectangle(0, 0, width, 50, fill="yellow")
			processList[i].grid(row=1, column=col+1, sticky=W)

			# display the label for burst time
			Label(self.processDisplay, text=str(totalTime)).grid(row=2, column=col)

			# get the total time so far and increase column value
			totalTime = totalTime + self.processBurstTime[i]
			col = col + 2
		# End of for loop

		# add remaining burst time label to window
		Label(self.processDisplay, text=str(totalTime)).grid(row=2, column=col)
	############################# END OF FUNCTION #############################

	###########################################################################
	# Author: Andrew Fagrey
	# Description: Function for generating the gantt chart for FCFS
	###########################################################################
	def FCFSGanttChart(self):
		# clear the window of other gantt charts
		self.setProcessesFrame()

		# sort list by arrival time
		self.sortProcessesByArrivalTime()

		# add the burst times (used for calculating the width of the shapes)
		self.addBurstTimes()

		# some variables used below in the loop
		processList = []
		col = 0
		totalTime = 0

		# loop through the number of processes and display the gantt chart
		for i in range(0, int(self.processCountBox.get())):
			# show the process label
			Label(self.processDisplay, text=self.processName[i]).grid(row=0, column=col+1)

			# calculate the width of the shape
			width = (300 / self.burstTimeTotal) * self.processBurstTime[i]

			# add to the shape the process list and display it in the window
			shapeCan = Canvas(self.processDisplay, width=width, height=50)
			processList.append(shapeCan)
			processList[i].create_rectangle(0, 0, width, 50, fill="red")
			processList[i].grid(row=1, column=col+1, sticky=W)

			# display the label for burst time
			Label(self.processDisplay, text=str(totalTime)).grid(row=2, column=col)

			# get the total time so far and increase column value
			totalTime = totalTime + self.processBurstTime[i]
			col = col + 2
		# End of for loop

		# add remaining burst time label to window
		Label(self.processDisplay, text=str(totalTime)).grid(row=2, column=col)
	############################# END OF FUNCTION #############################

	###########################################################################
	# Author: Andrew Fagrey
	# Description: Function for generating the gantt chart for SJF
	###########################################################################
	def SJFGanttChart(self):
		# clear the window of other gantt charts
		self.setProcessesFrame()

		# sort the processes by burst time and calculate total burst time
		self.sortProcessesByBurstTime()
		self.addBurstTimes()

		# vars used in loop below
		processList = []
		col = 0
		totalTime = 0

		# loop through the processes and display the gantt chart
		for i in range(0, int(self.processCountBox.get())):
			# show the process label
			Label(self.processDisplay, text=self.processName[i]).grid(row=0, column=col+1)

			# calculate the width of the shape
			width = (300 / self.burstTimeTotal) * self.processBurstTime[i]

			# add to the shape the process list and display it in the window
			shapeCan = Canvas(self.processDisplay, width=width, height=50)
			processList.append(shapeCan)
			processList[i].create_rectangle(0, 0, width, 50, fill="blue")
			processList[i].grid(row=1, column=col+1, sticky=W)

			# display the label for burst time
			Label(self.processDisplay, text=str(totalTime)).grid(row=2, column=col)

			# get the total time so far and increase column value
			totalTime = totalTime + self.processBurstTime[i]
			col = col + 2
		# End of for loop

		# add remaining burst time label to window
		Label(self.processDisplay, text=str(totalTime)).grid(row=2, column=col)
	############################# END OF FUNCTION #############################

	###########################################################################
	# Author: Andrew Fagrey
	# Description: A function to select (based on the radio button selected)
	#  which process scheduling algorithm to run
	###########################################################################
	def printSelection(self):
		if self.selectedMethod.get() == 0:
			self.roundRobinGanttChart()
		elif self.selectedMethod.get() == 1:
			self.priorityGanttChart()
		elif self.selectedMethod.get() == 2:
			self.SJFGanttChart()
		elif self.selectedMethod.get() == 3:
			self.FCFSGanttChart()
	############################# END OF FUNCTION #############################

	###########################################################################
	# Author: Andrew Fagrey
	# Description: A function to set the processes display frame
	###########################################################################
	def setProcessesFrame(self):
		self.processDisplay.grid_remove()
		self.processDisplay = Frame(self.dataColumnFrame, width=600, height=325)
		self.processDisplay.grid_propagate(False);
		self.processDisplay.grid(row=1, column=0)
	############################# END OF FUNCTION #############################

	###########################################################################
	# Author: Andrew Fagrey
	# Description: A function for removing the process display frame
	###########################################################################
	def clearProcessFrame(self):
		self.processDisplay.grid_remove()
	############################# END OF FUNCTION #############################

	###########################################################################
	# Author: Andrew Fagrey
	# Description: This function is responsible for laying out the frames for
	#  the various elements of the process scheduling UI
	###########################################################################
	def initUI(self):
		# var for padding width
		padWidth = 7

		# FRAME FOR PROCESS LIST ON THE LEFT SIDE OF THE WINDOW
		# set up left side column for processes
		self.processListColumnFrame = Frame(self, width=200, height=400, background="grey")
		self.processListColumnFrame.grid_propagate(False);
		self.processListColumnFrame.grid(row=0, column=0)

		# process list
		# show label and entry box for process count
		Label(self.processListColumnFrame, text="Enter process count: ", background="grey").grid(row=0, column=0, sticky=W, padx=padWidth)
		self.processCountBox = Entry(self.processListColumnFrame)
		self.processCountBox.grid(row=1, column=0, sticky=W, padx=padWidth)

		# show button for generating process list
		Button(self.processListColumnFrame, text="Generate Process List", command=self.generateProcessesList).grid(row=2, column=0, pady=10, sticky=W, padx=padWidth)

		# show label for list box
		Label(self.processListColumnFrame, text="Px, Burst t, Priority t, Arrival t", background="grey").grid(row=3, column=0, sticky=W, padx=padWidth)

		# show list box for showing the processes
		self.processListBox = Listbox(self.processListColumnFrame, height=17, width=22)
		self.processListBox.grid(row=4, column=0, sticky=W, padx=padWidth)

		# END PROCESS COLUMN SUTFF

		# FRAME FOR BUTTONS AND CHART ON THE RIGHT SIDE OF THE WINDOW
		# set up frame for rght side for buttons and process rectangles
		self.dataColumnFrame = Frame(self, width=600, height=400)
		self.dataColumnFrame.grid_propagate(False);
		self.dataColumnFrame.grid(row=0, column=1, padx=10)

		# FRAME INSIDE OF THE DISPLAY FRAME
		# set up frame for radio buttons and text entry
		self.frameRadioButtons = Frame(self.dataColumnFrame, width=600, height=75)
		self.frameRadioButtons.grid_propagate(False);
		self.frameRadioButtons.grid(sticky=W,row=0, column=0, pady=10)

		# variable for getting a number based on radio button choosen
		self.selectedMethod = IntVar(master=self.parent)

		# set up round robin radio button - command=self.clearProcessFrame,
		self.roundRobinButton = Radiobutton(self.frameRadioButtons, command=self.clearProcessFrame, text="Round Robin", variable=self.selectedMethod, value=0)
		self.roundRobinButton.grid(sticky=N, row=0, column=0)

		# set up shortest job first button
		self.shortestJobFirstButton = Radiobutton(self.frameRadioButtons, command=self.clearProcessFrame, text="Shortest Job First", variable=self.selectedMethod, value=2)
		self.shortestJobFirstButton.grid(sticky=N, row=0, column=1)

		# set up priority button (scheduling)
		self.priorityButton = Radiobutton(self.frameRadioButtons, command=self.clearProcessFrame, text="Priority", variable=self.selectedMethod, value=1)
		self.priorityButton.grid(sticky=N, row=0, column=2)

		# set up button for FCFS
		self.FCFSButton = Radiobutton(self.frameRadioButtons, command=self.clearProcessFrame, text="FCFS", variable=self.selectedMethod, value=3)
		self.FCFSButton.grid(sticky=N, row=0, column=3)

		# default radio button to be selected
		self.roundRobinButton.select()

		# set up frame for where the processes are going to be visually displayed
		self.processDisplay = Frame(self.dataColumnFrame, width=600, height=325)
		self.processDisplay.grid_propagate(False);
		self.processDisplay.grid(row=1, column=0)

		# label and entry for the time quanta - only for round robin
		self.timeQuanta = StringVar()
		Label(self.frameRadioButtons, text="Time Quanta (RR only): ").grid(row=1, column=0)
		self.timeQuantaBox = Entry(self.frameRadioButtons)
		self.timeQuantaBox.grid(row=1, column=1)

		# button for generating the process display
		Button(self.frameRadioButtons, text="Display Gantt Chart", command=self.printSelection).grid(row=1, column=3)
	############################# END OF FUNCTION #############################