<commit_before># relay_clientbot.py: Clientbot extensions for Relay
import string
import collections

from pylinkirc import utils, conf, world
from pylinkirc.log import log

# TODO: document configurable styles in relay::clientbot_styles::COMMAND_NAME
# These use template strings as documented @ https://docs.python.org/3/library/string.html#template-strings
default_styles = {'MESSAGE': '\x02[$colored_netname]\x02 <$colored_sender> $text',
                  'KICK': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost has kicked $target_nick from $channel ($text)',
                  'PART': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost has left $channel ($text)',
                  'JOIN': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost has joined $channel',
                  'NICK': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost is now known as $newnick',
                  'QUIT': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost has quit ($text)',
                  'ACTION': '\x02[$colored_netname]\x02 * $colored_sender $text',
                  'NOTICE': '\x02[$colored_netname]\x02 - Notice from $colored_sender: $text',
                  'SQUIT': '\x02[$colored_netname]\x02 - Netsplit lost users: $colored_nicks',
                  'SJOIN': '\x02[$colored_netname]\x02 - Netjoin gained users: $colored_nicks',
                  }

def color_text(s):
    """
    Returns a colorized version of the given text based on a simple hash algorithm
    (sum of all characters).
    """
    colors = ('02', '03', '04', '05', '06', '07', '08', '09', '10', '11',
              '12', '13')
    num = sum([ord(char) for char in s])
    num = num % len(colors)
    return "\x03%s%s\x03" % (colors[num], s)

def cb_relay_core(irc, source, command, args):
    """
    This function takes Clientbot actions and outputs them to a channel as regular text.
    """
    real_command = command.split('_')[-1]

    relay = world.plugins.get('relay')
    if irc.pseudoclient and relay:
        try:
            sourcename = irc.getFriendlyName(source)
        except KeyError:  # User has left due to /quit
            sourcename = args['userdata'].nick

        # Special case for CTCPs.
        if real_command == 'MESSAGE':
            # CTCP action, format accordingly
            if (not args.get('is_notice')) and args['text'].startswith('\x01ACTION ') and args['text'].endswith('\x01'):
                args['text'] = args['text'][8:-1]

                real_command = 'ACTION'

            # Other CTCPs are ignored
            elif args['text'].startswith('\x01'):
                return
            elif args.get('is_notice'):  # Different syntax for notices
                real_command = 'NOTICE'

        # .get() chains are lovely. Try to fetch the format for the given command from the
        # relay:clientbot_format:$command key, falling back to one defined in default_styles
        # above, and then nothing if not found.
        text_template = conf.conf.get('relay', {}).get('clientbot_styles', {}).get(real_command,
                        default_styles.get(real_command, ''))
        text_template = string.Template(text_template)

        if text_template:
            # Get the original client that the relay client source was meant for.
            log.debug('(%s) relay_cb_core: Trying to find original sender (user) for %s', irc.name, source)
            try:
                origuser = relay.getOrigUser(irc, source) or args['userdata'].remote
            except (AttributeError, KeyError):
                log.debug('(%s) relay_cb_core: Trying to find original sender (server) for %s. serverdata=%s', irc.name, source, args.get('serverdata'))
                try:
                    origuser = ((args.get('serverdata') or irc.servers[source]).remote,)
                except (AttributeError, KeyError):
                    return

            log.debug('(%s) relay_cb_core: Original sender found as %s', irc.name, origuser)
            netname = origuser[0]
            try:  # Try to get the full network name
                netname = conf.conf['servers'][netname]['netname'].lower()
            except KeyError:
                pass

            # Figure out where the message is destined to.
            target = args.get('channel') or args.get('target')
            if target is None or not utils.isChannel(target):
                # Quit and nick messages are not channel specific. Figure out all channels that the
                # sender shares over the relay, and relay them that way.
                userdata = args.get('userdata') or irc.users.get(source)
                if not userdata:
                    # No user data given. This was probably some other global event such as SQUIT.
                    userdata = irc.pseudoclient

                channels = [channel for channel in userdata.channels if relay.getRelay((irc.name, channel))]
            else:
                # Pluralize the channel so that we can iterate over it.
                channels = [target]
            log.debug('(%s) relay_cb_core: Relaying event %s to channels: %s', irc.name, real_command, channels)

            if source in irc.users:
                try:
                    identhost = irc.getHostmask(source).split('!')[-1]
                except KeyError:  # User got removed due to quit
                    identhost = '%s@%s' % (args['olduser'].ident, args['olduser'].host)
                # This is specifically spaced so that ident@host is only shown for users that have
                # one, and not servers.
                identhost = ' (%s)' % identhost
            else:
                identhost = ''

            # $target_nick: Convert the target for kicks, etc. from a UID to a nick
            if args.get("target") in irc.users:
                args["target_nick"] = irc.getFriendlyName(args['target'])

            args.update({'netname': netname, 'sender': sourcename, 'sender_identhost': identhost,
                         'colored_sender': color_text(sourcename), 'colored_netname': color_text(netname)})

            for channel in channels:
                cargs = args.copy()  # Copy args list to manipualte them in a channel specific way

                # $nicks / $colored_nicks: used when the event affects multiple users, such as SJOIN or SQUIT.
                # For SJOIN, this is simply a list of nicks. For SQUIT, this is sent as a dict
                # mapping channels to lists of nicks, as netsplits aren't channel specific but
                # still have to be relayed as such.
                nicklist = args.get('nicks')
                if nicklist:
                    if type(nicklist) == collections.defaultdict:
                        nicklist = nicklist.get(channel, [])

                    # Ignore if no nicks are affected on the channel.
                    if not nicklist:
                        continue

                    colored_nicks = [color_text(nick) for nick in nicklist]

                    # Join both the nicks and colored_nicks fields into a comma separated string.
                    cargs['nicks'] = ', '.join(nicklist)
                    cargs['colored_nicks'] = ', '.join(colored_nicks)

                text = text_template.safe_substitute(cargs)
                irc.proto.message(irc.pseudoclient.uid, channel, text)

utils.add_hook(cb_relay_core, 'CLIENTBOT_MESSAGE')
utils.add_hook(cb_relay_core, 'CLIENTBOT_KICK')
utils.add_hook(cb_relay_core, 'CLIENTBOT_PART')
utils.add_hook(cb_relay_core, 'CLIENTBOT_JOIN')
utils.add_hook(cb_relay_core, 'CLIENTBOT_QUIT')
utils.add_hook(cb_relay_core, 'CLIENTBOT_NICK')
utils.add_hook(cb_relay_core, 'CLIENTBOT_SJOIN')
utils.add_hook(cb_relay_core, 'CLIENTBOT_SQUIT')
<commit_msg>relay_clientbot: skip relaying non-PRIVMSGs for X seconds after connect<commit_after># relay_clientbot.py: Clientbot extensions for Relay
import string
import collections
import time

from pylinkirc import utils, conf, world
from pylinkirc.log import log

# TODO: document configurable styles in relay::clientbot_styles::COMMAND_NAME
# These use template strings as documented @ https://docs.python.org/3/library/string.html#template-strings
default_styles = {'MESSAGE': '\x02[$colored_netname]\x02 <$colored_sender> $text',
                  'KICK': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost has kicked $target_nick from $channel ($text)',
                  'PART': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost has left $channel ($text)',
                  'JOIN': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost has joined $channel',
                  'NICK': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost is now known as $newnick',
                  'QUIT': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost has quit ($text)',
                  'ACTION': '\x02[$colored_netname]\x02 * $colored_sender $text',
                  'NOTICE': '\x02[$colored_netname]\x02 - Notice from $colored_sender: $text',
                  'SQUIT': '\x02[$colored_netname]\x02 - Netsplit lost users: $colored_nicks',
                  'SJOIN': '\x02[$colored_netname]\x02 - Netjoin gained users: $colored_nicks',
                  }

def color_text(s):
    """
    Returns a colorized version of the given text based on a simple hash algorithm
    (sum of all characters).
    """
    colors = ('02', '03', '04', '05', '06', '07', '08', '09', '10', '11',
              '12', '13')
    num = sum([ord(char) for char in s])
    num = num % len(colors)
    return "\x03%s%s\x03" % (colors[num], s)

def cb_relay_core(irc, source, command, args):
    """
    This function takes Clientbot actions and outputs them to a channel as regular text.
    """
    real_command = command.split('_')[-1]

    relay = world.plugins.get('relay')
    if irc.pseudoclient and relay:
        try:
            sourcename = irc.getFriendlyName(source)
        except KeyError:  # User has left due to /quit
            sourcename = args['userdata'].nick

        relay_conf = conf.conf.get('relay', {})

        # Be less floody on startup: don't relay non-PRIVMSGs for the first X seconds after connect.
        startup_delay = relay_conf.get('startup_delay', 5)

        # Special case for CTCPs.
        if real_command == 'MESSAGE':
            # CTCP action, format accordingly
            if (not args.get('is_notice')) and args['text'].startswith('\x01ACTION ') and args['text'].endswith('\x01'):
                args['text'] = args['text'][8:-1]

                real_command = 'ACTION'

            # Other CTCPs are ignored
            elif args['text'].startswith('\x01'):
                return
            elif args.get('is_notice'):  # Different syntax for notices
                real_command = 'NOTICE'
        elif (time.time() - irc.start_ts) < startup_delay:
            log.debug('(%s) relay_cb_core: Not relaying %s because of startup delay of %s.', irc.name,
                      real_command, startup_delay)
            return

        # Try to fetch the format for the given command from the relay:clientbot_styles:$command
        # key, falling back to one defined in default_styles above, and then nothing if not found
        # there.
        text_template = relay_conf.get('clientbot_styles', {}).get(real_command,
                        default_styles.get(real_command, ''))
        text_template = string.Template(text_template)

        if text_template:
            # Get the original client that the relay client source was meant for.
            log.debug('(%s) relay_cb_core: Trying to find original sender (user) for %s', irc.name, source)
            try:
                origuser = relay.getOrigUser(irc, source) or args['userdata'].remote
            except (AttributeError, KeyError):
                log.debug('(%s) relay_cb_core: Trying to find original sender (server) for %s. serverdata=%s', irc.name, source, args.get('serverdata'))
                try:
                    origuser = ((args.get('serverdata') or irc.servers[source]).remote,)
                except (AttributeError, KeyError):
                    return

            log.debug('(%s) relay_cb_core: Original sender found as %s', irc.name, origuser)
            netname = origuser[0]
            try:  # Try to get the full network name
                netname = conf.conf['servers'][netname]['netname'].lower()
            except KeyError:
                pass

            # Figure out where the message is destined to.
            target = args.get('channel') or args.get('target')
            if target is None or not utils.isChannel(target):
                # Quit and nick messages are not channel specific. Figure out all channels that the
                # sender shares over the relay, and relay them that way.
                userdata = args.get('userdata') or irc.users.get(source)
                if not userdata:
                    # No user data given. This was probably some other global event such as SQUIT.
                    userdata = irc.pseudoclient

                channels = [channel for channel in userdata.channels if relay.getRelay((irc.name, channel))]
            else:
                # Pluralize the channel so that we can iterate over it.
                channels = [target]
            log.debug('(%s) relay_cb_core: Relaying event %s to channels: %s', irc.name, real_command, channels)

            if source in irc.users:
                try:
                    identhost = irc.getHostmask(source).split('!')[-1]
                except KeyError:  # User got removed due to quit
                    identhost = '%s@%s' % (args['olduser'].ident, args['olduser'].host)
                # This is specifically spaced so that ident@host is only shown for users that have
                # one, and not servers.
                identhost = ' (%s)' % identhost
            else:
                identhost = ''

            # $target_nick: Convert the target for kicks, etc. from a UID to a nick
            if args.get("target") in irc.users:
                args["target_nick"] = irc.getFriendlyName(args['target'])

            args.update({'netname': netname, 'sender': sourcename, 'sender_identhost': identhost,
                         'colored_sender': color_text(sourcename), 'colored_netname': color_text(netname)})

            for channel in channels:
                cargs = args.copy()  # Copy args list to manipualte them in a channel specific way

                # $nicks / $colored_nicks: used when the event affects multiple users, such as SJOIN or SQUIT.
                # For SJOIN, this is simply a list of nicks. For SQUIT, this is sent as a dict
                # mapping channels to lists of nicks, as netsplits aren't channel specific but
                # still have to be relayed as such.
                nicklist = args.get('nicks')
                if nicklist:

                    # Get channel-specific nick list if relevent.
                    if type(nicklist) == collections.defaultdict:
                        nicklist = nicklist.get(channel, [])

                    # Ignore if no nicks are affected on the channel.
                    if not nicklist:
                        continue

                    colored_nicks = [color_text(nick) for nick in nicklist]

                    # Join both the nicks and colored_nicks fields into a comma separated string.
                    cargs['nicks'] = ', '.join(nicklist)
                    cargs['colored_nicks'] = ', '.join(colored_nicks)

                text = text_template.safe_substitute(cargs)
                irc.proto.message(irc.pseudoclient.uid, channel, text)

utils.add_hook(cb_relay_core, 'CLIENTBOT_MESSAGE')
utils.add_hook(cb_relay_core, 'CLIENTBOT_KICK')
utils.add_hook(cb_relay_core, 'CLIENTBOT_PART')
utils.add_hook(cb_relay_core, 'CLIENTBOT_JOIN')
utils.add_hook(cb_relay_core, 'CLIENTBOT_QUIT')
utils.add_hook(cb_relay_core, 'CLIENTBOT_NICK')
utils.add_hook(cb_relay_core, 'CLIENTBOT_SJOIN')
utils.add_hook(cb_relay_core, 'CLIENTBOT_SQUIT')
<|endoftext|>


<commit_before>import pygraphviz as pgv

class Diagram(object):
    def __init__(self, machine):
        self.machine = machine

class AGraph(Diagram):
    state_attributes = {
        'shape': 'circle',
        'height': '1.2',
    }

    machine_attributes = {
        'directed': True,
        'strict': False,
        'rankdir': 'LR',
        'ratio': '0.3'
    }

    def get_graph(self, title=None):
        """ Generate a DOT graph with pygraphviz, returns an AGraph object
        Args:
            title (string): Optional title for the graph.
        """

        if title is None:
            title = self.__class__.__name__
        elif title is False:
            title = ''

        fsm_graph = pgv.AGraph(title=title, **self.machine_attributes)
        fsm_graph.node_attr.update(self.state_attributes)

        # For each state, draw a circle
        for state in self.machine.states.items():
            shape = self.state_attributes['shape']

            # We want the first state to be a double circle (UML style)
            if state == self.machine.states.items()[0]:
                shape = 'doublecircle'
            else:
                shape = self.state_attributes['shape']

            state = state[0]
            fsm_graph.add_node(n=state, shape=shape)

        fsm_graph.add_node('null', shape='plaintext', label='')
        fsm_graph.add_edge('null', 'new')

        # For each event, add the transitions
        for event in self.machine.events.items():
            event = event[1]
            label = str(event.name)

            for transition in event.transitions.items():
                src = transition[0]
                dst = transition[1][0].dest

                fsm_graph.add_edge(src, dst, label=label)

        return fsm_graph
<commit_msg>make pygraphviz not a hard requirement, added abstractmethod<commit_after>import abc
try:
    import pygraphviz as pgv
except:
    pgv = None

class Diagram(object):
    def __init__(self, machine):
        self.machine = machine

    @abc.abstractmethod
    def get_graph(self):
        return

class AGraph(Diagram):
    state_attributes = {
        'shape': 'circle',
        'height': '1.2',
    }

    machine_attributes = {
        'directed': True,
        'strict': False,
        'rankdir': 'LR',
        'ratio': '0.3'
    }

    def get_graph(self, title=None):
        """ Generate a DOT graph with pygraphviz, returns an AGraph object
        Args:
            title (string): Optional title for the graph.
        """
        if not pgv:
            raise Exception('AGraph diagram requires pygraphviz')

        if title is None:
            title = self.__class__.__name__
        elif title is False:
            title = ''

        fsm_graph = pgv.AGraph(title=title, **self.machine_attributes)
        fsm_graph.node_attr.update(self.state_attributes)

        # For each state, draw a circle
        for state in self.machine.states.items():
            shape = self.state_attributes['shape']

            # We want the first state to be a double circle (UML style)
            if state == self.machine.states.items()[0]:
                shape = 'doublecircle'
            else:
                shape = self.state_attributes['shape']

            state = state[0]
            fsm_graph.add_node(n=state, shape=shape)

        fsm_graph.add_node('null', shape='plaintext', label='')
        fsm_graph.add_edge('null', 'new')

        # For each event, add the transitions
        for event in self.machine.events.items():
            event = event[1]
            label = str(event.name)

            for transition in event.transitions.items():
                src = transition[0]
                dst = transition[1][0].dest

                fsm_graph.add_edge(src, dst, label=label)

        return fsm_graph
<|endoftext|>


<commit_before># relay_clientbot.py: Clientbot extensions for Relay
import string
import collections
import time
import hashlib

from pylinkirc import utils, conf, world
from pylinkirc.log import log

# TODO: document configurable styles in relay::clientbot_styles::COMMAND_NAME
# These use template strings as documented @ https://docs.python.org/3/library/string.html#template-strings
default_styles = {'MESSAGE': '\x02[$colored_netname]\x02 <$colored_sender> $text',
                  'KICK': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost has kicked $target_nick from $channel ($text)',
                  'PART': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost has left $channel ($text)',
                  'JOIN': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost has joined $channel',
                  'NICK': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost is now known as $newnick',
                  'QUIT': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost has quit ($text)',
                  'ACTION': '\x02[$colored_netname]\x02 * $colored_sender $text',
                  'NOTICE': '\x02[$colored_netname]\x02 - Notice from $colored_sender: $text',
                  'SQUIT': '\x02[$colored_netname]\x02 - Netsplit lost users: $colored_nicks',
                  'SJOIN': '\x02[$colored_netname]\x02 - Netjoin gained users: $colored_nicks',
                  'PM': 'PM from $sender on $netname: $text',
                  'PNOTICE': '<$sender> $text',
                  }

def color_text(s):
    """
    Returns a colorized version of the given text based on a simple hash algorithm
    (sum of all characters).
    """
    colors = ('02', '03', '04', '05', '06', '07', '08', '09', '10', '11',
              '12', '13', '15')
    digest = hashlib.md5(s.encode()).hexdigest()
    digest = int(digest, base=16)
    num = digest % len(colors)
    return "\x03%s%s\x03" % (colors[num], s)

def cb_relay_core(irc, source, command, args):
    """
    This function takes Clientbot events and formats them as text to the target channel / user.
    """
    real_command = command.split('_')[-1]

    relay = world.plugins.get('relay')

    private = False

    if irc.pseudoclient and relay:
        try:
            sourcename = irc.getFriendlyName(source)
        except KeyError:  # User has left due to /quit
            sourcename = args['userdata'].nick

        relay_conf = conf.conf.get('relay', {})

        # Be less floody on startup: don't relay non-PRIVMSGs for the first X seconds after connect.
        startup_delay = relay_conf.get('clientbot_startup_delay', 5)

        # Special case for CTCPs.
        if real_command == 'MESSAGE':
            # CTCP action, format accordingly
            if (not args.get('is_notice')) and args['text'].startswith('\x01ACTION ') and args['text'].endswith('\x01'):
                args['text'] = args['text'][8:-1]

                real_command = 'ACTION'

            elif not utils.isChannel(args['target']):
                # Target is a user; handle this accordingly.
                if relay_conf.get('allow_clientbot_pms'):
                    real_command = 'PNOTICE' if args.get('is_notice') else 'PM'
                    private = True

            # Other CTCPs are ignored
            elif args['text'].startswith('\x01'):
                return

            elif args.get('is_notice'):  # Different syntax for notices
                real_command = 'NOTICE'
        elif (time.time() - irc.start_ts) < startup_delay:
            log.debug('(%s) relay_cb_core: Not relaying %s because of startup delay of %s.', irc.name,
                      real_command, startup_delay)
            return

        # Try to fetch the format for the given command from the relay:clientbot_styles:$command
        # key, falling back to one defined in default_styles above, and then nothing if not found
        # there.
        text_template = relay_conf.get('clientbot_styles', {}).get(real_command,
                        default_styles.get(real_command, ''))
        text_template = string.Template(text_template)

        if text_template:
            if irc.getServiceBot(source):
                # HACK: service bots are global and lack the relay state we look for.
                # just pretend the message comes from the current network.
                log.debug('(%s) relay_cb_core: Overriding network origin to local (source=%s)', irc.name, source)
                sourcenet = irc.name
            else:
                # Get the original client that the relay client source was meant for.
                log.debug('(%s) relay_cb_core: Trying to find original sender (user) for %s', irc.name, source)
                try:
                    origuser = relay.get_orig_user(irc, source) or args['userdata'].remote
                except (AttributeError, KeyError):
                    log.debug('(%s) relay_cb_core: Trying to find original sender (server) for %s. serverdata=%s', irc.name, source, args.get('serverdata'))
                    try:
                        origuser = ((args.get('serverdata') or irc.servers[source]).remote,)
                    except (AttributeError, KeyError):
                        return

                log.debug('(%s) relay_cb_core: Original sender found as %s', irc.name, origuser)
                sourcenet = origuser[0]

            try:  # Try to get the full network name
                netname = conf.conf['servers'][sourcenet]['netname']
            except KeyError:
                netname = sourcenet

            # Figure out where the message is destined to.
            target = args.get('channel') or args.get('target')
            if target is None or not (utils.isChannel(target) or private):
                # Non-channel specific message (e.g. QUIT or NICK). If this isn't a PM, figure out
                # all channels that the sender shares over the relay, and relay them to those
                # channels.
                userdata = args.get('userdata') or irc.users.get(source)
                if not userdata:
                    # No user data given. This was probably some other global event such as SQUIT.
                    userdata = irc.pseudoclient

                targets = [channel for channel in userdata.channels if relay.get_relay((irc.name, channel))]
            else:
                # Pluralize the channel so that we can iterate over it.
                targets = [target]
            log.debug('(%s) relay_cb_core: Relaying event %s to channels: %s', irc.name, real_command, targets)

            if source in irc.users:
                try:
                    identhost = irc.getHostmask(source).split('!')[-1]
                except KeyError:  # User got removed due to quit
                    identhost = '%s@%s' % (args['olduser'].ident, args['olduser'].host)
                # This is specifically spaced so that ident@host is only shown for users that have
                # one, and not servers.
                identhost = ' (%s)' % identhost
            else:
                identhost = ''

            # $target_nick: Convert the target for kicks, etc. from a UID to a nick
            if args.get("target") in irc.users:
                args["target_nick"] = irc.getFriendlyName(args['target'])

            args.update({'netname': netname, 'sender': sourcename, 'sender_identhost': identhost,
                         'colored_sender': color_text(sourcename), 'colored_netname': color_text(netname)})
            if 'channel' in args:
                # Display the real channel instead of the local name, if applicable
                args['local_channel'] = args['channel']
                args['channel'] = relay.get_remote_channel(irc, world.networkobjects[sourcenet], args['channel'])
                log.debug('(%s) relay_clientbot: coersing $channel from %s to %s', irc.name, args['local_channel'], args['channel'])

            for target in targets:
                cargs = args.copy()  # Copy args list to manipulate them in a channel specific way

                # $nicks / $colored_nicks: used when the event affects multiple users, such as SJOIN or SQUIT.
                # For SJOIN, this is simply a list of nicks. For SQUIT, this is sent as a dict
                # mapping channels to lists of nicks, as netsplits aren't channel specific but
                # still have to be relayed as such.
                nicklist = args.get('nicks')
                if nicklist:

                    # Get channel-specific nick list if relevent.
                    if type(nicklist) == collections.defaultdict:
                        nicklist = nicklist.get(target, [])

                    # Ignore if no nicks are affected on the channel.
                    if not nicklist:
                        continue

                    colored_nicks = [color_text(nick) for nick in nicklist]

                    # Join both the nicks and colored_nicks fields into a comma separated string.
                    cargs['nicks'] = ', '.join(nicklist)
                    cargs['colored_nicks'] = ', '.join(colored_nicks)

                text = text_template.safe_substitute(cargs)
                # PMs are always sent as notice - this prevents unknown command loops with bots.
                irc.msg(target, text, loopback=False, notice=private)

utils.add_hook(cb_relay_core, 'CLIENTBOT_MESSAGE')
utils.add_hook(cb_relay_core, 'CLIENTBOT_KICK')
utils.add_hook(cb_relay_core, 'CLIENTBOT_PART')
utils.add_hook(cb_relay_core, 'CLIENTBOT_JOIN')
utils.add_hook(cb_relay_core, 'CLIENTBOT_QUIT')
utils.add_hook(cb_relay_core, 'CLIENTBOT_NICK')
utils.add_hook(cb_relay_core, 'CLIENTBOT_SJOIN')
utils.add_hook(cb_relay_core, 'CLIENTBOT_SQUIT')

@utils.add_cmd
def rpm(irc, source, args):
    """<target> <text>

    Sends PMs to users over the relay, if Clientbot PMs are enabled.
    """

    try:
        target = args[0]
        text = ' '.join(args[1:])
    except IndexError:
        irc.error('Not enough arguments. Needs 2: target nick and text.')
        return

    relay = world.plugins.get('relay')
    if irc.protoname != 'clientbot':
        irc.error('This command is only supported on Clientbot networks. Try /msg %s <text>' % target)
        return
    elif relay is None:
        irc.error('PyLink Relay is not loaded.')
        return
    elif not text:
        irc.error('No text given.')
        return
    elif not conf.conf.get('relay').get('allow_clientbot_pms'):
        irc.error('Private messages with users connected via Clientbot have been '
                  'administratively disabled.')
        return

    uid = irc.nickToUid(target)
    if not uid:
        irc.error('Unknown user %s.' % target)
        return
    elif not relay.isRelayClient(irc, uid):
        irc.error('%s is not a relay user.' % target)
        return
    else:
        assert not irc.isInternalClient(source), "rpm is not allowed from PyLink bots"
        # Send the message through relay by faking a hook for its handler.
        relay.handle_messages(irc, source, 'RELAY_CLIENTBOT_PRIVMSG', {'target': uid, 'text': text})
        irc.reply('Message sent.')
<commit_msg>relay: use built-in hash() for colorizing text<commit_after># relay_clientbot.py: Clientbot extensions for Relay
import string
import collections
import time
import hashlib

from pylinkirc import utils, conf, world
from pylinkirc.log import log

# TODO: document configurable styles in relay::clientbot_styles::COMMAND_NAME
# These use template strings as documented @ https://docs.python.org/3/library/string.html#template-strings
default_styles = {'MESSAGE': '\x02[$colored_netname]\x02 <$colored_sender> $text',
                  'KICK': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost has kicked $target_nick from $channel ($text)',
                  'PART': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost has left $channel ($text)',
                  'JOIN': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost has joined $channel',
                  'NICK': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost is now known as $newnick',
                  'QUIT': '\x02[$colored_netname]\x02 - $colored_sender$sender_identhost has quit ($text)',
                  'ACTION': '\x02[$colored_netname]\x02 * $colored_sender $text',
                  'NOTICE': '\x02[$colored_netname]\x02 - Notice from $colored_sender: $text',
                  'SQUIT': '\x02[$colored_netname]\x02 - Netsplit lost users: $colored_nicks',
                  'SJOIN': '\x02[$colored_netname]\x02 - Netjoin gained users: $colored_nicks',
                  'PM': 'PM from $sender on $netname: $text',
                  'PNOTICE': '<$sender> $text',
                  }

def color_text(s):
    """
    Returns a colorized version of the given text based on a simple hash algorithm.
    """
    colors = ('02', '03', '04', '05', '06', '07', '08', '09', '10', '11',
              '12', '13', '15')
    hash_output = hash(s.encode())
    num = hash_output % len(colors)
    return "\x03%s%s\x03" % (colors[num], s)

def cb_relay_core(irc, source, command, args):
    """
    This function takes Clientbot events and formats them as text to the target channel / user.
    """
    real_command = command.split('_')[-1]

    relay = world.plugins.get('relay')

    private = False

    if irc.pseudoclient and relay:
        try:
            sourcename = irc.getFriendlyName(source)
        except KeyError:  # User has left due to /quit
            sourcename = args['userdata'].nick

        relay_conf = conf.conf.get('relay', {})

        # Be less floody on startup: don't relay non-PRIVMSGs for the first X seconds after connect.
        startup_delay = relay_conf.get('clientbot_startup_delay', 5)

        # Special case for CTCPs.
        if real_command == 'MESSAGE':
            # CTCP action, format accordingly
            if (not args.get('is_notice')) and args['text'].startswith('\x01ACTION ') and args['text'].endswith('\x01'):
                args['text'] = args['text'][8:-1]

                real_command = 'ACTION'

            elif not utils.isChannel(args['target']):
                # Target is a user; handle this accordingly.
                if relay_conf.get('allow_clientbot_pms'):
                    real_command = 'PNOTICE' if args.get('is_notice') else 'PM'
                    private = True

            # Other CTCPs are ignored
            elif args['text'].startswith('\x01'):
                return

            elif args.get('is_notice'):  # Different syntax for notices
                real_command = 'NOTICE'
        elif (time.time() - irc.start_ts) < startup_delay:
            log.debug('(%s) relay_cb_core: Not relaying %s because of startup delay of %s.', irc.name,
                      real_command, startup_delay)
            return

        # Try to fetch the format for the given command from the relay:clientbot_styles:$command
        # key, falling back to one defined in default_styles above, and then nothing if not found
        # there.
        text_template = relay_conf.get('clientbot_styles', {}).get(real_command,
                        default_styles.get(real_command, ''))
        text_template = string.Template(text_template)

        if text_template:
            if irc.getServiceBot(source):
                # HACK: service bots are global and lack the relay state we look for.
                # just pretend the message comes from the current network.
                log.debug('(%s) relay_cb_core: Overriding network origin to local (source=%s)', irc.name, source)
                sourcenet = irc.name
            else:
                # Get the original client that the relay client source was meant for.
                log.debug('(%s) relay_cb_core: Trying to find original sender (user) for %s', irc.name, source)
                try:
                    origuser = relay.get_orig_user(irc, source) or args['userdata'].remote
                except (AttributeError, KeyError):
                    log.debug('(%s) relay_cb_core: Trying to find original sender (server) for %s. serverdata=%s', irc.name, source, args.get('serverdata'))
                    try:
                        origuser = ((args.get('serverdata') or irc.servers[source]).remote,)
                    except (AttributeError, KeyError):
                        return

                log.debug('(%s) relay_cb_core: Original sender found as %s', irc.name, origuser)
                sourcenet = origuser[0]

            try:  # Try to get the full network name
                netname = conf.conf['servers'][sourcenet]['netname']
            except KeyError:
                netname = sourcenet

            # Figure out where the message is destined to.
            target = args.get('channel') or args.get('target')
            if target is None or not (utils.isChannel(target) or private):
                # Non-channel specific message (e.g. QUIT or NICK). If this isn't a PM, figure out
                # all channels that the sender shares over the relay, and relay them to those
                # channels.
                userdata = args.get('userdata') or irc.users.get(source)
                if not userdata:
                    # No user data given. This was probably some other global event such as SQUIT.
                    userdata = irc.pseudoclient

                targets = [channel for channel in userdata.channels if relay.get_relay((irc.name, channel))]
            else:
                # Pluralize the channel so that we can iterate over it.
                targets = [target]
            log.debug('(%s) relay_cb_core: Relaying event %s to channels: %s', irc.name, real_command, targets)

            if source in irc.users:
                try:
                    identhost = irc.getHostmask(source).split('!')[-1]
                except KeyError:  # User got removed due to quit
                    identhost = '%s@%s' % (args['olduser'].ident, args['olduser'].host)
                # This is specifically spaced so that ident@host is only shown for users that have
                # one, and not servers.
                identhost = ' (%s)' % identhost
            else:
                identhost = ''

            # $target_nick: Convert the target for kicks, etc. from a UID to a nick
            if args.get("target") in irc.users:
                args["target_nick"] = irc.getFriendlyName(args['target'])

            args.update({'netname': netname, 'sender': sourcename, 'sender_identhost': identhost,
                         'colored_sender': color_text(sourcename), 'colored_netname': color_text(netname)})
            if 'channel' in args:
                # Display the real channel instead of the local name, if applicable
                args['local_channel'] = args['channel']
                args['channel'] = relay.get_remote_channel(irc, world.networkobjects[sourcenet], args['channel'])
                log.debug('(%s) relay_clientbot: coersing $channel from %s to %s', irc.name, args['local_channel'], args['channel'])

            for target in targets:
                cargs = args.copy()  # Copy args list to manipulate them in a channel specific way

                # $nicks / $colored_nicks: used when the event affects multiple users, such as SJOIN or SQUIT.
                # For SJOIN, this is simply a list of nicks. For SQUIT, this is sent as a dict
                # mapping channels to lists of nicks, as netsplits aren't channel specific but
                # still have to be relayed as such.
                nicklist = args.get('nicks')
                if nicklist:

                    # Get channel-specific nick list if relevent.
                    if type(nicklist) == collections.defaultdict:
                        nicklist = nicklist.get(target, [])

                    # Ignore if no nicks are affected on the channel.
                    if not nicklist:
                        continue

                    colored_nicks = [color_text(nick) for nick in nicklist]

                    # Join both the nicks and colored_nicks fields into a comma separated string.
                    cargs['nicks'] = ', '.join(nicklist)
                    cargs['colored_nicks'] = ', '.join(colored_nicks)

                text = text_template.safe_substitute(cargs)
                # PMs are always sent as notice - this prevents unknown command loops with bots.
                irc.msg(target, text, loopback=False, notice=private)

utils.add_hook(cb_relay_core, 'CLIENTBOT_MESSAGE')
utils.add_hook(cb_relay_core, 'CLIENTBOT_KICK')
utils.add_hook(cb_relay_core, 'CLIENTBOT_PART')
utils.add_hook(cb_relay_core, 'CLIENTBOT_JOIN')
utils.add_hook(cb_relay_core, 'CLIENTBOT_QUIT')
utils.add_hook(cb_relay_core, 'CLIENTBOT_NICK')
utils.add_hook(cb_relay_core, 'CLIENTBOT_SJOIN')
utils.add_hook(cb_relay_core, 'CLIENTBOT_SQUIT')

@utils.add_cmd
def rpm(irc, source, args):
    """<target> <text>

    Sends PMs to users over the relay, if Clientbot PMs are enabled.
    """

    try:
        target = args[0]
        text = ' '.join(args[1:])
    except IndexError:
        irc.error('Not enough arguments. Needs 2: target nick and text.')
        return

    relay = world.plugins.get('relay')
    if irc.protoname != 'clientbot':
        irc.error('This command is only supported on Clientbot networks. Try /msg %s <text>' % target)
        return
    elif relay is None:
        irc.error('PyLink Relay is not loaded.')
        return
    elif not text:
        irc.error('No text given.')
        return
    elif not conf.conf.get('relay').get('allow_clientbot_pms'):
        irc.error('Private messages with users connected via Clientbot have been '
                  'administratively disabled.')
        return

    uid = irc.nickToUid(target)
    if not uid:
        irc.error('Unknown user %s.' % target)
        return
    elif not relay.isRelayClient(irc, uid):
        irc.error('%s is not a relay user.' % target)
        return
    else:
        assert not irc.isInternalClient(source), "rpm is not allowed from PyLink bots"
        # Send the message through relay by faking a hook for its handler.
        relay.handle_messages(irc, source, 'RELAY_CLIENTBOT_PRIVMSG', {'target': uid, 'text': text})
        irc.reply('Message sent.')
<|endoftext|>


<commit_before>from django import forms
from django.utils.translation import ugettext as _

from lily.accounts.models import Account
from lily.contacts.models import Contact, Function
from lily.tags.forms import TagsFormMixin
from lily.utils.forms import HelloLilyModelForm
from lily.utils.functions import get_twitter_username_from_string, validate_linkedin_url
from lily.utils.forms.widgets import ShowHideWidget, BootstrapRadioFieldRenderer, AddonTextInput
from lily.utils.forms.mixins import FormSetFormMixin

class AddContactQuickbuttonForm(HelloLilyModelForm):
    """
    Form to add an account with the absolute minimum of information.
    """
    account = forms.ModelChoiceField(label=_('Works at'), required=False,
                                     queryset=Account.objects.none(),
                                     empty_label=_('Select an account'))
    email = forms.EmailField(label=_('E-mail address'), max_length=255, required=False)
    phone = forms.CharField(label=_('Phone number'), max_length=40, required=False)

    def __init__(self, *args, **kwargs):
        """
        Overload super().__init__ to change auto_id to prevent clashing form field id's with
        other forms.
        """
        kwargs.update({
            'auto_id': 'id_contact_quickbutton_%s'
        })

        super(AddContactQuickbuttonForm, self).__init__(*args, **kwargs)

        # Provide filtered query set
        self.fields['account'].queryset = Account.objects.all()

    def clean(self):
        """
        Form validation: all fields should be unique.
        """
        cleaned_data = super(AddContactQuickbuttonForm, self).clean()

        # Check if at least first or last name has been provided.
        if not cleaned_data.get('first_name') and not cleaned_data.get('last_name'):
            self._errors['first_name'] = self._errors['last_name'] = self.error_class([_('Name can\'t be empty')])

        # Prevent multiple contacts with the same e-mailadress when adding
        if cleaned_data.get('email'):
            if Contact.objects.filter(email_addresses__email_address__iexact=cleaned_data.get('email')).exists():
                self._errors['email'] = self.error_class([_('E-mail address already in use.')])

        return cleaned_data

    class Meta:
        model = Contact
        fields = ('first_name', 'preposition', 'last_name', 'account', 'email', 'phone')

class CreateUpdateContactForm(FormSetFormMixin, TagsFormMixin):
    """
    Form to add a contact which all fields available.
    """
    account = forms.ModelChoiceField(label=_('Works at'), required=False,
                                     queryset=Account.objects.none(),
                                     empty_label=_('Select an account'))
    twitter = forms.CharField(label=_('Twitter'), required=False, widget=AddonTextInput(icon_attrs={
        'class': 'icon-twitter',
        'position': 'left',
        'is_button': False
    }))
    linkedin = forms.URLField(label=_('LinkedIn'), required=False, widget=AddonTextInput(icon_attrs={
        'class': 'icon-linkedin',
        'position': 'left',
        'is_button': False
    }))

    def __init__(self, *args, **kwargs):
        super(CreateUpdateContactForm, self).__init__(*args, **kwargs)

        # Provide filtered query set
        self.fields['account'].queryset = Account.objects.all()

        # Try providing initial account info
        is_working_at = Function.objects.filter(contact=self.instance).values_list('account_id', flat=True)
        if len(is_working_at) == 1:
            self.fields['account'].initial = is_working_at[0]

        self.fields['twitter'].initial = self.instance.get_twitter()
        self.fields['linkedin'].initial = self.instance.get_linkedin()

        self.fields['addresses'].form_attrs = {
            'exclude_address_types': ['visiting'],
            'extra_form_kwargs': {
                'initial': {
                    'type': 'home',
                }
            }
        }

    def clean(self):
        """
        Form validation: fill in at least first or last name.
        """
        cleaned_data = super(CreateUpdateContactForm, self).clean()

        # Check if at least first or last name has been provided.
        if not cleaned_data.get('first_name') and not cleaned_data.get('last_name'):
            self._errors['first_name'] = self._errors['last_name'] = self.error_class([_('Name can\'t be empty')])

        if cleaned_data.get('twitter'):
            twitter_username = get_twitter_username_from_string(cleaned_data.get('twitter'))

            if twitter_username is not None:
                cleaned_data['twitter'] = twitter_username
            else:
                # A string was given but it seems to be invalid
                self._errors['twitter'] = self.error_class([_('Please enter a valid Twitter username')])

        if cleaned_data.get('linkedin'):
            if not validate_linkedin_url(cleaned_data.get('linkedin')):
                # Profile url was invalid
                self._errors['linkedin'] = self.error_class([_('Please enter a valid LinkedIn profile url')])

        return cleaned_data

    class Meta:
        model = Contact
        fields = ('salutation', 'gender', 'first_name', 'preposition', 'last_name', 'account', 'description',
                  'email_addresses', 'phone_numbers', 'addresses', )
        widgets = {
            'description': ShowHideWidget(forms.Textarea(attrs={
                'rows': 3,
            })),
            'gender': forms.widgets.RadioSelect(renderer=BootstrapRadioFieldRenderer, attrs={
                'data-skip-uniform': 'true',
                'data-uniformed': 'true',
            }),
        }
<commit_msg>LILY-380: salutation to bootstrap style<commit_after>from django import forms
from django.utils.translation import ugettext as _

from lily.accounts.models import Account
from lily.contacts.models import Contact, Function
from lily.tags.forms import TagsFormMixin
from lily.utils.forms import HelloLilyModelForm
from lily.utils.functions import get_twitter_username_from_string, validate_linkedin_url
from lily.utils.forms.widgets import ShowHideWidget, BootstrapRadioFieldRenderer, AddonTextInput
from lily.utils.forms.mixins import FormSetFormMixin

class AddContactQuickbuttonForm(HelloLilyModelForm):
    """
    Form to add an account with the absolute minimum of information.
    """
    account = forms.ModelChoiceField(label=_('Works at'), required=False,
                                     queryset=Account.objects.none(),
                                     empty_label=_('Select an account'))
    email = forms.EmailField(label=_('E-mail address'), max_length=255, required=False)
    phone = forms.CharField(label=_('Phone number'), max_length=40, required=False)

    def __init__(self, *args, **kwargs):
        """
        Overload super().__init__ to change auto_id to prevent clashing form field id's with
        other forms.
        """
        kwargs.update({
            'auto_id': 'id_contact_quickbutton_%s'
        })

        super(AddContactQuickbuttonForm, self).__init__(*args, **kwargs)

        # Provide filtered query set
        self.fields['account'].queryset = Account.objects.all()

    def clean(self):
        """
        Form validation: all fields should be unique.
        """
        cleaned_data = super(AddContactQuickbuttonForm, self).clean()

        # Check if at least first or last name has been provided.
        if not cleaned_data.get('first_name') and not cleaned_data.get('last_name'):
            self._errors['first_name'] = self._errors['last_name'] = self.error_class([_('Name can\'t be empty')])

        # Prevent multiple contacts with the same e-mailadress when adding
        if cleaned_data.get('email'):
            if Contact.objects.filter(email_addresses__email_address__iexact=cleaned_data.get('email')).exists():
                self._errors['email'] = self.error_class([_('E-mail address already in use.')])

        return cleaned_data

    class Meta:
        model = Contact
        fields = ('first_name', 'preposition', 'last_name', 'account', 'email', 'phone')

class CreateUpdateContactForm(FormSetFormMixin, TagsFormMixin):
    """
    Form to add a contact which all fields available.
    """
    account = forms.ModelChoiceField(label=_('Works at'), required=False,
                                     queryset=Account.objects.none(),
                                     empty_label=_('Select an account'))
    twitter = forms.CharField(label=_('Twitter'), required=False, widget=AddonTextInput(icon_attrs={
        'class': 'icon-twitter',
        'position': 'left',
        'is_button': False
    }))
    linkedin = forms.URLField(label=_('LinkedIn'), required=False, widget=AddonTextInput(icon_attrs={
        'class': 'icon-linkedin',
        'position': 'left',
        'is_button': False
    }))

    def __init__(self, *args, **kwargs):
        super(CreateUpdateContactForm, self).__init__(*args, **kwargs)

        # Provide filtered query set
        self.fields['account'].queryset = Account.objects.all()

        # Try providing initial account info
        is_working_at = Function.objects.filter(contact=self.instance).values_list('account_id', flat=True)
        if len(is_working_at) == 1:
            self.fields['account'].initial = is_working_at[0]

        self.fields['twitter'].initial = self.instance.get_twitter()
        self.fields['linkedin'].initial = self.instance.get_linkedin()

        self.fields['addresses'].form_attrs = {
            'exclude_address_types': ['visiting'],
            'extra_form_kwargs': {
                'initial': {
                    'type': 'home',
                }
            }
        }

    def clean(self):
        """
        Form validation: fill in at least first or last name.
        """
        cleaned_data = super(CreateUpdateContactForm, self).clean()

        # Check if at least first or last name has been provided.
        if not cleaned_data.get('first_name') and not cleaned_data.get('last_name'):
            self._errors['first_name'] = self._errors['last_name'] = self.error_class([_('Name can\'t be empty')])

        if cleaned_data.get('twitter'):
            twitter_username = get_twitter_username_from_string(cleaned_data.get('twitter'))

            if twitter_username is not None:
                cleaned_data['twitter'] = twitter_username
            else:
                # A string was given but it seems to be invalid
                self._errors['twitter'] = self.error_class([_('Please enter a valid Twitter username')])

        if cleaned_data.get('linkedin'):
            if not validate_linkedin_url(cleaned_data.get('linkedin')):
                # Profile url was invalid
                self._errors['linkedin'] = self.error_class([_('Please enter a valid LinkedIn profile url')])

        return cleaned_data

    class Meta:
        model = Contact
        fields = ('salutation', 'gender', 'first_name', 'preposition', 'last_name', 'account', 'description',
                  'email_addresses', 'phone_numbers', 'addresses', )
        widgets = {
            'description': ShowHideWidget(forms.Textarea(attrs={
                'rows': 3,
            })),
            'salutation': forms.widgets.RadioSelect(renderer=BootstrapRadioFieldRenderer, attrs={
                'data-skip-uniform': 'true',
                'data-uniformed': 'true',
            }),
            'gender': forms.widgets.RadioSelect(renderer=BootstrapRadioFieldRenderer, attrs={
                'data-skip-uniform': 'true',
                'data-uniformed': 'true',
            }),
        }
<|endoftext|>


<commit_before>#!/usr/bin/env bash
# -*- coding: utf-8 -*-

"""Shallow Water Model

- Two dimensional shallow water in a rotating frame
- Staggered Arakawa-C lat:lon grid
- periodic in the x-dimension
- fixed boundary conditions in the y-dimension

"""

import numpy as np
from numpy import mod, floor, ceil

# Constants
# =========

pi = np.pi
twopi = 2.0 * pi
piby2 = pi / 2.0

rearth = 6371220.0          # radius of earth
twoomega = 1.4584e-4        # rotation rate
gravity = 9.80616           # gravitational force

# Model Configuration
# ===================

p = 6                       # grid scale
nx = 5*2**p
ny = 5*2**(p-1)

dt = 900.0                  # timestep in seconds
numdays = 600               # total length of simulation

phiref = 100.0              # refrence phi value for solver
                            # (should be similar to mean phi)

# Grid Setup
# ==========

dx = 2*np.pi/nx
dy = np.pi/ny
hny = ny / 2.0
hnx = nx / 2.0

i = np.arange(nx)           # index for lons
j = np.arange(ny)           # index for lats

xp = (i+0.5)*dx             # lon of phi points
xv = (i+0.5)*dx             # lon of v points
xu = i*dx                   # lon of u points

yp = (j+0.5-hny)*dy         # lat of phi points
yu = (j+0.5-hny)*dy         # lon of u points
yv = (j-hny)*dy             # lon of v points

def _periodicl(psi):
    """Copy the last column to in front of the array to make zonally periodic."""
    return np.concatenate((psi[-1, np.newaxis], psi))

def _periodicr(psi):
    """Copy the first column to after the array to make zonally periodic."""
    return np.concatenate((psi, psi[0, np.newaxis]))

def _withbcs(psi, lower_bc, upper_bc):
	"""Add the upper and lower boundary conditions to the edge of the field."""
	return np.vstack()

def halfr(psi):
    """Take a half-step right.
    i.e. psi[i+1/2, j] = 0.5*(psi[i,j] + psi[i+1, j])
    wraps around the x-domain."""
    pr = _periodicr(psi)  # make periodic
    return 0.5*(pr[:-1,:] + pr[1:,:])

def halfl(psi):
    """Take a half-step left.
    i.e. psi[i, j] = 0.5*(psi[i-1/2,j] + psi[i+1/2, j])
    wraps around the x-domain."""
    pl = _periodicl(psi)  # make periodic
    return 0.5*(pl[:-1,:] + pl[1:,:])

def halfup(psi):
    """Take a half-step up.
    i.e. psi[i, j+1/2] = 0.5*(psi[i, j] + psi[i, j+1/2]).
    As the meridional direction does not wrap, taking a half step reduces the meridional size by 1.
    Returns a grid one latitudinal level less than psi."""
    return 0.5*(psi[:,:-1] + psi[:,1:])

### Differentiation Functions
def xdiffl(psi, dx):
    """Central difference in x over one grid square.
    i.e. d/dy(psi)[i,j] = (psi[i-1/2, j] - psi[i+1/2, j]) / dx

    `dx` should be the same dimension as latitude.
    """
    pl = _periodicl(psi)
    return (pl[1:,:] - pl[:-1,:]) / dx[np.newaxis, :]

def ydiff(psi, dy):
    """Central difference in y over one grid square.
    i.e. d/dy(psi)[i,j] = (psi[i, j+1/2] - psi[i, j-1/2]) / dy
    Returns a grid one latitudinal level less than psi.
    """
    return (psi[:, 1:] - psi[:,:-1]) / dy

def coriolis(u, v, phi):
    """Calculates the Coriolis terms of the momentum equation.

    Returns (fu, fv) : ((nx x ny+1), (nx, ny))"""
    f = twoomega * sinp[np.newaxis, :]  # value of Coriolis param f at phi points

    # == Calculating fv at u points ==
    tempv = np.zeros_like(v)
    tempv[:, 1:-1] = halfup(phi)*v[:, 1:-1]*cosv[np.newaxis, 1:-1]
    # average to phi points and multiply by f / phi
    tempp = halfup(tempv) * f / phi
    fv = halfl(tempp) / cosp[np.newaxis, :]

    # == Calculating fu at v points ==
    tempu = halfl(phi)*u
    tempp = halfr(tempu) * f / phi
    fu = np.zeros_like(v)
    fu[:, 1:-1] = halfup(tempp)

    return (fu, fv)

def uvatuv(u, v):
    """Calculate the value of u at v and v at u."""
    unp, vnp, usp, vsp = polar(u, v)
    v[:, 0] = vsp
    v[:, -1] = vnp

    ur = _periodicr(u)
    ubar = np.zeros_like(v)
    ubar[:, 1:-1] = 0.25*(ur[:-1,:-1] + ur[:-1,1:] + ur[1:, :-1] + ur[1:,1:])
    ubar[:, 0] = usp
    ubar[:, -1] = unp

    vl = _periodicl(v)
    vbar = 0.25*(vl[:-1,:-1] + vl[:-1,1:] + vl[1:,:-1] + vl[1:,1:])
    return ubar, vbar

def uvatphi(u, v):
    """Calculate the value of u and v at phi points."""
    pu = halfr(u)
    pv = halfup(v)
    return pu, pv

<commit_msg>A 2-d linear shallow water code - Arakawa C-grid - f-plane - periodic in x, d/dy = 0 at top and bottom boundaries<commit_after>#!/usr/bin/env bash
# -*- coding: utf-8 -*-

"""Shallow Water Model

- Two dimensional shallow water in a rotating frame
- Staggered Arakawa-C lat:lon grid
- periodic in the x-dimension
- fixed boundary conditions in the y-dimension

h = H + η

∂/∂t[u] + fv = - g ∂/∂x[η]
∂/∂t[v] - fu = - g ∂/∂y[η]
∂/∂t[h] + H(∂/∂x[u] + ∂/∂y[v]) = 0

f = f0 + βy
"""

import numpy as np
from numpy import mod, floor, ceil

SHOW_CHART = True

class LinearShallowWater(object):
    """A model of the two-dimensional linearised shallow water equations"""
    def __init__(self, nx, ny, f=1.0, r=0.01, dt=0.1, maxt=1.0, domain=(10000.0, 10000.0), H=1000.0, g=9.8):
        super(LinearShallowWater, self).__init__()
        self.nx = nx
        self.ny = ny
        self.dt = dt
        self.f  = f
        self.maxt = maxt
        self.domain = domain
        self.dx = domain[0] / nx
        self.dy = domain[1] / ny
        self.H  = H
        self.u = np.zeros((nx+1, ny))
        self.v = np.zeros((nx, ny+1))    # v points are on horizontal edges so there exists an extra point from the last vertex
        self.h = np.zeros((nx, ny))
        self.r = r
        self.g = g

        self.prev_state = self.state

        self.t = 0.0

    def _ubc(self):
        """Returns the u velocity boundaries.
        Returns tuple (left, right, top, bottom)."""
        # periodic in x, zero derivative on y
        return (self.u[-1,:], self.u[0,:], self.u[:,-1], self.u[:,0])

    def _hbc(self):
        """Returns the h boundaries.
        Returns tuple (left, right, top, bottom)."""
        # periodic in x, zero derivative on y
        return (self.h[-1,:], self.h[0,:], self.h[:,-1], self.h[:,0])

    def __update_x_periodic(self):
        self.u[-1, :] = self.u[0, :]

    def _periodicl(self, psi):
        """Copy the last column to in front of the field to make zonally periodic."""
        return np.vstack((psi[-1, :], psi))

    def _periodicr(self, psi):
        """Copy the first column to after the array to make zonally periodic."""
        return np.vstack((psi, psi[0, :]))

    def diffx(self, psi):
        """Calculate ∂/∂x[psi] over a single grid square.

        i.e. d/dx(psi)[i,j] = (psi[i+1/2, j] - psi[i-1/2, j]) / dx

        The derivative is returned at x points at the midpoint between
        x points of the input array."""
        return (psi[1:,:] - psi[:-1,:]) / self.dx

    def diffy(self, psi):
        """Calculate ∂/∂y[psi] over a single grid square.

        i.e. d/dy(psi)[i,j] = (psi[i, j+1/2] - psi[i, j-1/2]) / dy

        The derivative is returned at y points at the midpoint between
        y points of the input array."""
        return (psi[:, 1:] - psi[:,:-1]) / self.dy

    def hdiv(self):
        """Returns the horizontal divergence at h points."""
        return self.diffx(self.u) + self.diffy(self.v)

    def uvatuv(self):
        """Calculate the value of u at v and v at u."""
        ul, ur, ut, ub = self._ubc()       # need the boundary conditions to average u at v points
        ur = np.hstack([ub[:, np.newaxis], self.u, ut[:, np.newaxis]])

        ubar = np.zeros_like(self.v)
        ubar[:,:] = 0.25*(ur[:-1,:-1] + ur[:-1,1:] + ur[1:, :-1] + ur[1:,1:])

        vl = self._periodicl(self.v)
        vbar = 0.25*(vl[:-1,:-1] + vl[:-1,1:] + vl[1:,:-1] + vl[1:,1:])
        return ubar, vbar

    def rhs(self):
        """Calculate the right hand side of the u, v and h equations."""
        # the height equation
        h_rhs = -self.H*(self.diffx(self.u) + self.diffy(self.v))

        # the u and v equations
        uu, vv = self.uvatuv()

        hx = self._periodicl(self.h)
        dhdx = self.diffx(hx)
        u_rhs = -self.f*vv - self.g*dhdx
        u_rhs = self._periodicr(u_rhs) #+ self.r*self.u

        hl, hr, ht, hb = self._hbc()
        hy = np.hstack([hb[:, np.newaxis], self.h, ht[:, np.newaxis]])
        dhdy = self.diffy(hy)
        v_rhs = self.f*uu - self.g*dhdy #+ self.r*self.v
        return np.array([u_rhs, v_rhs, h_rhs])

    def raw_filter(self, prev, curr, new, nu=0.01):
        """Robert-Asselin Filter."""
        return curr + nu*(new - 2.0*curr + prev)

    @property
    def state(self):
        return np.array([self.u, self.v, self.h])

    @state.setter
    def state(self, value):
        self.u, self.v, self.h = value

    def step(self):
        #ur, vr, hr = self.rhs()
        # take leapfrog step forward in time
        next_state = self.prev_state + 2.0*self.dt*self.rhs()
        self.prev_state = self.raw_filter(self.prev_state, self.state, next_state)
        self.state = next_state
        self.t = self.t + 2.0*self.dt

nx=320
ny=320
sw = LinearShallowWater(nx, ny, f=0.01, maxt=1000.0)

i, j = np.indices(sw.h.shape)
sw.h[:] = np.exp(-(np.sqrt((j - ny/2.0)**2 + (i - nx/2.0)**2)/20)**2)*10
#sw.h[:] = np.random.random(sw.h.shape)*sw.H*0.1

def take_step():
        sw.step()
        print('step %f: Max vel. %.3g' % (sw.t, np.max(sw.u)))
        print('max h: %.3g' % np.max(sw.h))
        im1.set_data(sw.h.T)
        im2.set_data(sw.u.T)

        #im1.set_clim((np.min(sw.h), np.max(sw.h)))
        im2.set_clim((np.min(sw.u), np.max(sw.u)))
        im1.axes.figure.canvas.draw()
        im2.axes.figure.canvas.draw()
        plt.pause(0.0001)

import time
if SHOW_CHART:
    import matplotlib
    import matplotlib.pyplot as plt
    fig = plt.figure(figsize=(12,12))
    ax = fig.add_subplot(211)
    im1 = ax.imshow(sw.state[2].T, cmap=plt.cm.seismic)
    im1.set_clim((-5, 5))
    plt.colorbar(im1)

    ax = fig.add_subplot(212)
    im2 = ax.imshow(sw.state[0].T, cmap=plt.cm.seismic)

    plt.colorbar(im2)
    fig.show()
    time.sleep(1)
    while sw.t < sw.maxt:
        take_step()<|endoftext|>


<commit_before>import os.path
import sys
from threading import Thread

from django.conf import settings
from django.db.models import signals as model_signals

try:
    from filebrowser.views import filebrowser_post_upload
    from filebrowser.views import filebrowser_post_rename
    from filebrowser.views import filebrowser_post_delete
    from filebrowser.settings import DIRECTORY
    FILEBROWSER_PRESENT = True
except ImportError:
    FILEBROWSER_PRESENT = False

from linkcheck.models import all_linklists

listeners = []

#1, register listeners for the objects that contain Links
for linklist_name, linklist_cls in all_linklists.items():

    def check_instance_links(sender, instance, linklist_cls=linklist_cls, **kwargs):
        '''
        When an object is saved:
            new Link/Urls are created, checked
        When an object is modified:
            new Link/Urls are created, checked
            existing Link/Urls are checked
            disappering Links are deleted
        '''
        def do_check_instance_links(sender, instance, linklist_cls=linklist_cls):

            from linkcheck.models import Url
            from linkcheck.models import Link
            import linkcheck.notifications

            try:
                linkcheck.notifications.still_updating = True
                content_type = linklist_cls.content_type()
                new_links = []
                old_links = Link.objects.filter(content_type=content_type, object_id=instance.pk)

                linklists = linklist_cls().get_linklist(extra_filter={'pk':instance.pk,})

                if not linklists:
                    # This object is no longer watched by linkcheck according to object_filter
                    links = []
                else:
                    linklist = linklists[0]
                    links = linklist['urls']+linklist['images']

                for link in links:
                    # url structure = (field, link text, url)
                    url = link[2]
                    if url.startswith('#'):
                        url = instance.get_absolute_url() + url
                    u, created = Url.objects.get_or_create(url=url)
                    l, created = Link.objects.get_or_create(url=u, field=link[0], text=link[1], content_type=content_type, object_id=instance.pk)
                    new_links.append(l.id)
                    u.still_exists = True
                    u.check()

                gone_links = old_links.exclude(id__in=new_links)
                gone_links.delete()

            finally:
                linkcheck.notifications.still_updating = False

        # Don't run in a separate thread if we are running tests
        if len(sys.argv)>1 and sys.argv[1] == 'test':
            do_check_instance_links(sender, instance, linklist_cls,)
        else:
            t = Thread(target=do_check_instance_links, args=(sender, instance, linklist_cls,))
            t.start()

    listeners.append(check_instance_links)
    model_signals.post_save.connect(listeners[-1], sender=linklist_cls.model)

    def delete_instance_links(sender, instance, linklist_cls=linklist_cls, **kwargs):
        '''delete all its links when an object is deleted'''
        from linkcheck.models import Link
        content_type = linklist_cls.content_type()
        old_links = Link.objects.filter(content_type=content_type, object_id=instance.pk)
        old_links.delete()
    listeners.append(delete_instance_links)
    model_signals.post_delete.connect(listeners[-1], sender=linklist_cls.model)

#2, register listeners for the objects that are targets of Links
    def instance_pre_save(sender, instance, ModelCls=linklist_cls.model, **kwargs):
        from linkcheck.models import Url
        current_url = instance.get_absolute_url()
        try:
            previous = ModelCls.objects.get(pk=instance.pk)
            #log.debug('instance exists modifying')
            previous_url = previous.get_absolute_url()
            if previous_url == current_url:
                #log.debug('url did not change, return')
                return
            else:
                #log.debug('url changed')
                old_urls = Url.objects.filter(url__startswith=previous_url)
                if old_urls:
                    old_urls.update(status=False, message='Broken internal link')
                new_urls = Url.objects.filter(url__startswith=current_url)
                if new_urls:
                    # mark these urls' status as False, so that post_save will check them
                    new_urls.update(status=False, message='Should be checked now!')
        except:
            #log.debug('new instance, post_save is in charge of this')
            pass

    listeners.append(instance_pre_save)
    model_signals.pre_save.connect(listeners[-1], sender=linklist_cls.model)

    def instance_post_save(sender, instance, ModelCls=linklist_cls.model, linklist=linklist_cls, **kwargs):

        from linkcheck.models import Url

        current_url = instance.get_absolute_url()

        # We assume returning None from get_absolute_url means that this instance doesn't have a URL
        # Not sure if we should do the same for '' as this could refer to '/'
        if current_url!=None:

            active = linklist.objects().filter(pk=instance.pk).count()

            if kwargs['created'] or (not active):
                new_urls = Url.objects.filter(url__startswith=current_url)
            else:
                new_urls = Url.objects.filter(status=False).filter(url__startswith=current_url)

            if new_urls:
                for url in new_urls:
                    url.check()

    listeners.append(instance_post_save)
    model_signals.post_save.connect(listeners[-1], sender=linklist_cls.model)

    def instance_pre_delete(sender, instance, ModelCls=linklist_cls.model,  **kwargs):
        from linkcheck.models import Url
        instance.linkcheck_deleting = True
        deleted_url = instance.get_absolute_url()
        if deleted_url:
            old_urls = Url.objects.filter(url__startswith=deleted_url).exclude(status=False)
            if old_urls:
                old_urls.update(status=False, message='Broken internal link')
    listeners.append(instance_pre_delete)
    model_signals.pre_delete.connect(listeners[-1], sender=linklist_cls.model)

################################################
# Integrate with django-filebrowser if present #
################################################

def handle_upload(sender, path=None, **kwargs):
    from linkcheck.models import Url
    url = os.path.join(settings.RELATIVE_MEDIA_URL, kwargs['file'].url_relative)
    url_qs = Url.objects.filter(url=url).filter(status=False)
    count = url_qs.count()
    if count:
        url_qs.update(status=True, message='Working document link')
        msg = "Please note. Uploading %s has corrected %s broken link%s. See the Link Manager for more details" % (url, count, 's' if count>1 else '')
        sender.user.message_set.create(message=msg)

def handle_rename(sender, path=None, **kwargs):
    from linkcheck.models import Url
    old_url = os.path.join(settings.RELATIVE_MEDIA_URL, DIRECTORY, path, kwargs['filename'])
    new_url = os.path.join(settings.RELATIVE_MEDIA_URL, DIRECTORY, path, kwargs['new_filename'])
    # rename a file will cause the urls to it invalid
    # rename a directory will cause the urls to its files invalid
    old_url_qs = Url.objects.filter(url=old_url).filter(status=True)
    if isdir(kwargs['filename']):
        old_url_qs = Url.objects.filter(url__startswith=old_url).filter(status=True)
    old_count = old_url_qs.count()
    if old_count:
        old_url_qs.update(status=False, message='Missing Document')
        msg = "Warning. Renaming %s has caused %s link%s to break. Please use the Link Manager to fix them" % (old_url, old_count, 's' if old_count>1 else '')
        sender.user.message_set.create(message=msg)

    # the new directory may fix some invalid links, so we make a check here.
    if isdir(kwargs['new_filename']):
        new_count = 0
        new_url_qs = Url.objects.filter(url__startswith=new_url).filter(status=False)
        for url in new_url_qs:
            if url.check():
                new_count += 1
    else:
        new_url_qs = Url.objects.filter(url=new_url).filter(status=False)
        new_count = new_url_qs.count()
        if new_count:
            new_url_qs.update(status=True, message='Working document link')
    if new_count:
        msg = "Please note. Renaming %s has corrected %s broken link%s. See the Link Manager for more details" % (new_url, new_count, 's' if new_count>1 else '')
        sender.user.message_set.create(message=msg)

def handle_delete(sender, path=None, **kwargs):
    from linkcheck.models import Url
    url = os.path.join(settings.RELATIVE_MEDIA_URL, DIRECTORY, path, kwargs['filename'])
    url_qs = Url.objects.filter(url=url).filter(status=True)
    count = url_qs.count()
    if count:
        url_qs.update(status=False, message='Missing Document')
        msg = "Warning. Deleting %s has caused %s link%s to break. Please use the Link Manager to fix them" % (url, count, 's' if count>1 else '')
        sender.user.message_set.create(message=msg)

if FILEBROWSER_PRESENT:
    filebrowser_post_upload.connect(handle_upload)
    filebrowser_post_rename.connect(handle_rename)
    filebrowser_post_delete.connect(handle_delete)

def isdir(filename):
    '''!!!only used for filebrowser'''
    if filename.count('.'):
        return False
    else:
        return True<commit_msg>Python 2.4 compatible syntax in listeners.py<commit_after>import os.path
import sys
from threading import Thread

from django.conf import settings
from django.db.models import signals as model_signals

try:
    from filebrowser.views import filebrowser_post_upload
    from filebrowser.views import filebrowser_post_rename
    from filebrowser.views import filebrowser_post_delete
    from filebrowser.settings import DIRECTORY
    FILEBROWSER_PRESENT = True
except ImportError:
    FILEBROWSER_PRESENT = False

from linkcheck.models import all_linklists

listeners = []

#1, register listeners for the objects that contain Links
for linklist_name, linklist_cls in all_linklists.items():

    def check_instance_links(sender, instance, linklist_cls=linklist_cls, **kwargs):
        '''
        When an object is saved:
            new Link/Urls are created, checked
        When an object is modified:
            new Link/Urls are created, checked
            existing Link/Urls are checked
            disappering Links are deleted
        '''
        def do_check_instance_links(sender, instance, linklist_cls=linklist_cls):

            from linkcheck.models import Url
            from linkcheck.models import Link
            import linkcheck.notifications

            try:
                linkcheck.notifications.still_updating = True
                content_type = linklist_cls.content_type()
                new_links = []
                old_links = Link.objects.filter(content_type=content_type, object_id=instance.pk)

                linklists = linklist_cls().get_linklist(extra_filter={'pk':instance.pk,})

                if not linklists:
                    # This object is no longer watched by linkcheck according to object_filter
                    links = []
                else:
                    linklist = linklists[0]
                    links = linklist['urls']+linklist['images']

                for link in links:
                    # url structure = (field, link text, url)
                    url = link[2]
                    if url.startswith('#'):
                        url = instance.get_absolute_url() + url
                    u, created = Url.objects.get_or_create(url=url)
                    l, created = Link.objects.get_or_create(url=u, field=link[0], text=link[1], content_type=content_type, object_id=instance.pk)
                    new_links.append(l.id)
                    u.still_exists = True
                    u.check()

                gone_links = old_links.exclude(id__in=new_links)
                gone_links.delete()

            finally:
                linkcheck.notifications.still_updating = False

        # Don't run in a separate thread if we are running tests
        if len(sys.argv)>1 and sys.argv[1] == 'test':
            do_check_instance_links(sender, instance, linklist_cls,)
        else:
            t = Thread(target=do_check_instance_links, args=(sender, instance, linklist_cls,))
            t.start()

    listeners.append(check_instance_links)
    model_signals.post_save.connect(listeners[-1], sender=linklist_cls.model)

    def delete_instance_links(sender, instance, linklist_cls=linklist_cls, **kwargs):
        '''delete all its links when an object is deleted'''
        from linkcheck.models import Link
        content_type = linklist_cls.content_type()
        old_links = Link.objects.filter(content_type=content_type, object_id=instance.pk)
        old_links.delete()
    listeners.append(delete_instance_links)
    model_signals.post_delete.connect(listeners[-1], sender=linklist_cls.model)

#2, register listeners for the objects that are targets of Links
    def instance_pre_save(sender, instance, ModelCls=linklist_cls.model, **kwargs):
        from linkcheck.models import Url
        current_url = instance.get_absolute_url()
        try:
            previous = ModelCls.objects.get(pk=instance.pk)
            #log.debug('instance exists modifying')
            previous_url = previous.get_absolute_url()
            if previous_url == current_url:
                #log.debug('url did not change, return')
                return
            else:
                #log.debug('url changed')
                old_urls = Url.objects.filter(url__startswith=previous_url)
                if old_urls:
                    old_urls.update(status=False, message='Broken internal link')
                new_urls = Url.objects.filter(url__startswith=current_url)
                if new_urls:
                    # mark these urls' status as False, so that post_save will check them
                    new_urls.update(status=False, message='Should be checked now!')
        except:
            #log.debug('new instance, post_save is in charge of this')
            pass

    listeners.append(instance_pre_save)
    model_signals.pre_save.connect(listeners[-1], sender=linklist_cls.model)

    def instance_post_save(sender, instance, ModelCls=linklist_cls.model, linklist=linklist_cls, **kwargs):

        from linkcheck.models import Url

        current_url = instance.get_absolute_url()

        # We assume returning None from get_absolute_url means that this instance doesn't have a URL
        # Not sure if we should do the same for '' as this could refer to '/'
        if current_url!=None:

            active = linklist.objects().filter(pk=instance.pk).count()

            if kwargs['created'] or (not active):
                new_urls = Url.objects.filter(url__startswith=current_url)
            else:
                new_urls = Url.objects.filter(status=False).filter(url__startswith=current_url)

            if new_urls:
                for url in new_urls:
                    url.check()

    listeners.append(instance_post_save)
    model_signals.post_save.connect(listeners[-1], sender=linklist_cls.model)

    def instance_pre_delete(sender, instance, ModelCls=linklist_cls.model,  **kwargs):
        from linkcheck.models import Url
        instance.linkcheck_deleting = True
        deleted_url = instance.get_absolute_url()
        if deleted_url:
            old_urls = Url.objects.filter(url__startswith=deleted_url).exclude(status=False)
            if old_urls:
                old_urls.update(status=False, message='Broken internal link')
    listeners.append(instance_pre_delete)
    model_signals.pre_delete.connect(listeners[-1], sender=linklist_cls.model)

################################################
# Integrate with django-filebrowser if present #
################################################

def handle_upload(sender, path=None, **kwargs):
    from linkcheck.models import Url
    url = os.path.join(settings.RELATIVE_MEDIA_URL, kwargs['file'].url_relative)
    url_qs = Url.objects.filter(url=url).filter(status=False)
    count = url_qs.count()
    if count:
        url_qs.update(status=True, message='Working document link')
        msg = "Please note. Uploading %s has corrected %s broken link%s. See the Link Manager for more details" % (url, count, count>1 and 's' or '')
        sender.user.message_set.create(message=msg)

def handle_rename(sender, path=None, **kwargs):
    from linkcheck.models import Url
    old_url = os.path.join(settings.RELATIVE_MEDIA_URL, DIRECTORY, path, kwargs['filename'])
    new_url = os.path.join(settings.RELATIVE_MEDIA_URL, DIRECTORY, path, kwargs['new_filename'])
    # rename a file will cause the urls to it invalid
    # rename a directory will cause the urls to its files invalid
    old_url_qs = Url.objects.filter(url=old_url).filter(status=True)
    if isdir(kwargs['filename']):
        old_url_qs = Url.objects.filter(url__startswith=old_url).filter(status=True)
    old_count = old_url_qs.count()
    if old_count:
        old_url_qs.update(status=False, message='Missing Document')
        msg = "Warning. Renaming %s has caused %s link%s to break. Please use the Link Manager to fix them" % (old_url, old_count, old_count>1 and 's' or '')
        sender.user.message_set.create(message=msg)

    # the new directory may fix some invalid links, so we make a check here.
    if isdir(kwargs['new_filename']):
        new_count = 0
        new_url_qs = Url.objects.filter(url__startswith=new_url).filter(status=False)
        for url in new_url_qs:
            if url.check():
                new_count += 1
    else:
        new_url_qs = Url.objects.filter(url=new_url).filter(status=False)
        new_count = new_url_qs.count()
        if new_count:
            new_url_qs.update(status=True, message='Working document link')
    if new_count:
        msg = "Please note. Renaming %s has corrected %s broken link%s. See the Link Manager for more details" % (new_url, new_count, new_count>1 and 's' or '')
        sender.user.message_set.create(message=msg)

def handle_delete(sender, path=None, **kwargs):
    from linkcheck.models import Url
    url = os.path.join(settings.RELATIVE_MEDIA_URL, DIRECTORY, path, kwargs['filename'])
    url_qs = Url.objects.filter(url=url).filter(status=True)
    count = url_qs.count()
    if count:
        url_qs.update(status=False, message='Missing Document')
        msg = "Warning. Deleting %s has caused %s link%s to break. Please use the Link Manager to fix them" % (url, count, count>1 and 's' or '')
        sender.user.message_set.create(message=msg)

if FILEBROWSER_PRESENT:
    filebrowser_post_upload.connect(handle_upload)
    filebrowser_post_rename.connect(handle_rename)
    filebrowser_post_delete.connect(handle_delete)

def isdir(filename):
    '''!!!only used for filebrowser'''
    if filename.count('.'):
        return False
    else:
        return True
<|endoftext|>


<commit_before>import os.path
import sys

title = "PointDensitySyn"
author = "Max Larsson"
version = "1.3.2"
date = ("May", "31", "2018")
email = "max.larsson@liu.se"
homepage = "www.liu.se/medfak/forskning/larsson-max/software"
if hasattr(sys, 'frozen'):
    if '_MEIPASS2' in os.environ:
        path = os.environ['_MEIPASS2']
    else:
        path = sys.argv[0]
else:
    path = __file__
app_path = os.path.dirname(path)
icon = os.path.join(app_path, "pds.ico")

<commit_msg>Bumped version to 1.3.3.<commit_after>import os.path
import sys

title = "PointDensitySyn"
author = "Max Larsson"
version = "1.3.3"
date = ("June", "13", "2018")
email = "max.larsson@liu.se"
homepage = "www.liu.se/medfak/forskning/larsson-max/software"
if hasattr(sys, 'frozen'):
    if '_MEIPASS2' in os.environ:
        path = os.environ['_MEIPASS2']
    else:
        path = sys.argv[0]
else:
    path = __file__
app_path = os.path.dirname(path)
icon = os.path.join(app_path, "pds.ico")

<|endoftext|>