<commit_before>"""
byceps.blueprints.admin.user.service
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:Copyright: 2006-2020 Jochen Kupperschmidt
:License: Revised BSD (see `LICENSE` file for details)
"""

from collections import defaultdict
from datetime import datetime, timedelta
from operator import attrgetter
from typing import Any, Dict, Iterator, List, Optional, Sequence, Set, Tuple

from ....database import db, paginate, Pagination
from ....services.consent import consent_service
from ....services.newsletter import service as newsletter_service
from ....services.newsletter.transfer.models import List as NewsletterList
from ....services.party import service as party_service
from ....services.party.transfer.models import Party
from ....services.shop.order import service as order_service
from ....services.ticketing.models.ticket import Ticket as DbTicket
from ....services.ticketing import attendance_service, ticket_service
from ....services.user import event_service
from ....services.user.models.detail import UserDetail as DbUserDetail
from ....services.user.models.event import (
    UserEvent as DbUserEvent,
    UserEventData,
)
from ....services.user.models.user import User as DbUser
from ....services.user import service as user_service
from ....services.user.transfer.models import User
from ....services.user_avatar import service as avatar_service
from ....services.user_badge import badge_service as user_badge_service
from ....typing import PartyID, UserID

from .models import Detail, UserStateFilter, UserWithCreationDetails

def get_users_paginated(
    page, per_page, *, search_term=None, state_filter=None
) -> Pagination:
    """Return the users to show on the specified page, optionally
    filtered by search term or flags.
    """
    query = DbUser.query \
        .options(
            db.joinedload('avatar_selection').joinedload('avatar'),
            db.joinedload('detail').load_only('first_names', 'last_name'),
        ) \
        .order_by(DbUser.created_at.desc())

    query = _filter_by_state(query, state_filter)

    if search_term:
        query = _filter_by_search_term(query, search_term)

    return paginate(
        query,
        page,
        per_page,
        item_mapper=_db_entity_to_user_with_creation_details,
    )

def _filter_by_state(query, state_filter):
    if state_filter == UserStateFilter.active:
        return query \
            .filter_by(initialized=True) \
            .filter_by(suspended=False) \
            .filter_by(deleted=False)
    elif state_filter == UserStateFilter.uninitialized:
        return query \
            .filter_by(initialized=False) \
            .filter_by(suspended=False) \
            .filter_by(deleted=False)
    elif state_filter == UserStateFilter.suspended:
        return query \
            .filter_by(suspended=True) \
            .filter_by(deleted=False)
    elif state_filter == UserStateFilter.deleted:
        return query \
            .filter_by(deleted=True)
    else:
        return query

def _filter_by_search_term(query, search_term):
    ilike_pattern = f'%{search_term}%'

    return query \
        .join(DbUserDetail) \
        .filter(
            db.or_(
                DbUser.email_address.ilike(ilike_pattern),
                DbUser.screen_name.ilike(ilike_pattern),
                DbUserDetail.first_names.ilike(ilike_pattern),
                DbUserDetail.last_name.ilike(ilike_pattern)
            )
        )

def get_users_created_since(
    delta: timedelta, limit: Optional[int] = None
) -> List[User]:
    """Return the user accounts created since `delta` ago."""
    filter_starts_at = datetime.utcnow() - delta

    query = DbUser.query \
        .options(
            db.joinedload('avatar_selection').joinedload('avatar'),
            db.joinedload('detail').load_only('first_names', 'last_name'),
        ) \
        .filter(DbUser.created_at >= filter_starts_at) \
        .order_by(DbUser.created_at.desc())

    if limit is not None:
        query = query.limit(limit)

    users = query.all()

    return [_db_entity_to_user_with_creation_details(u) for u in users]

def _db_entity_to_user_with_creation_details(
    user: DbUser,
) -> UserWithCreationDetails:
    is_orga = False  # Not interesting here.
    full_name = user.detail.full_name if user.detail is not None else None
    detail = Detail(full_name)

    return UserWithCreationDetails(
        user.id,
        user.screen_name,
        user.suspended,
        user.deleted,
        user.avatar.url if user.avatar else None,
        is_orga,
        user.created_at,
        user.initialized,
        detail,
    )

def get_parties_and_tickets(
    user_id: UserID,
) -> List[Tuple[Party, List[DbTicket]]]:
    """Return tickets the user uses or manages, and the related parties."""
    tickets = ticket_service.find_tickets_related_to_user(user_id)

    tickets_by_party_id = _group_tickets_by_party_id(tickets)

    party_ids = set(tickets_by_party_id.keys())
    parties_by_id = _get_parties_by_id(party_ids)

    parties_and_tickets = [
        (parties_by_id[party_id], tickets)
        for party_id, tickets in tickets_by_party_id.items()
    ]

    parties_and_tickets.sort(key=lambda x: x[0].starts_at, reverse=True)

    return parties_and_tickets

def _group_tickets_by_party_id(
    tickets: Sequence[DbTicket],
) -> Dict[PartyID, List[DbTicket]]:
    tickets_by_party_id: Dict[PartyID, List[DbTicket]] = defaultdict(list)

    for ticket in tickets:
        tickets_by_party_id[ticket.category.party_id].append(ticket)

    return tickets_by_party_id

def _get_parties_by_id(party_ids: Set[PartyID]) -> Dict[PartyID, Party]:
    parties = party_service.get_parties(party_ids)
    return {p.id: p for p in parties}

def get_attended_parties(user_id: UserID) -> List[Party]:
    """Return the parties attended by the user, in order."""
    attended_parties = attendance_service.get_attended_parties(user_id)
    attended_parties.sort(key=attrgetter('starts_at'), reverse=True)
    return attended_parties

def get_newsletter_subscriptions(
    user_id: UserID,
) -> Iterator[Tuple[NewsletterList, bool]]:
    lists = newsletter_service.get_all_lists()
    for list_ in lists:
        is_subscribed = newsletter_service.is_subscribed(user_id, list_.id)
        yield list_, is_subscribed

def get_events(user_id: UserID) -> Iterator[UserEventData]:
    events = event_service.get_events_for_user(user_id)
    events.extend(_fake_avatar_update_events(user_id))
    events.extend(_fake_consent_events(user_id))
    events.extend(_fake_newsletter_subscription_update_events(user_id))
    events.extend(_fake_order_events(user_id))

    user_ids = {
        event.data['initiator_id']
        for event in events
        if 'initiator_id' in event.data
    }
    users = user_service.find_users(user_ids, include_avatars=True)
    users_by_id = {str(user.id): user for user in users}

    for event in events:
        data = {
            'event': event.event_type,
            'occurred_at': event.occurred_at,
            'data': event.data,
        }

        additional_data = _get_additional_data(event, users_by_id)
        data.update(additional_data)

        yield data

def _fake_avatar_update_events(user_id: UserID) -> Iterator[DbUserEvent]:
    """Yield the user's avatar updates as volatile events."""
    avatar_updates = avatar_service.get_avatars_uploaded_by_user(user_id)

    for avatar_update in avatar_updates:
        data = {
            'initiator_id': str(user_id),
            'url_path': avatar_update.url_path,
        }

        yield DbUserEvent(
            avatar_update.occurred_at, 'user-avatar-updated', user_id, data
        )

def _fake_consent_events(user_id: UserID) -> Iterator[DbUserEvent]:
    """Yield the user's consents as volatile events."""
    consents = consent_service.get_consents_by_user(user_id)

    for consent in consents:
        data = {
            'initiator_id': str(user_id),
            'subject_title': consent.subject.title,
        }

        yield DbUserEvent(
            consent.expressed_at, 'consent-expressed', user_id, data
        )

def _fake_newsletter_subscription_update_events(
    user_id: UserID,
) -> Iterator[DbUserEvent]:
    """Yield the user's newsletter subscription updates as volatile events."""
    lists = newsletter_service.get_all_lists()
    lists_by_id = {list_.id: list_ for list_ in lists}

    updates = newsletter_service.get_subscription_updates_for_user(user_id)

    for update in updates:
        event_type = f'newsletter-{update.state.name}'

        list_ = lists_by_id[update.list_id]

        data = {
            'list_': list_,
            'initiator_id': str(user_id),
        }

        yield DbUserEvent(update.expressed_at, event_type, user_id, data)

def _fake_order_events(user_id: UserID) -> Iterator[DbUserEvent]:
    """Yield the orders placed by the user as volatile events."""
    orders = order_service.get_orders_placed_by_user(user_id)

    for order in orders:
        data = {
            'initiator_id': str(user_id),
            'order': order,
        }

        yield DbUserEvent(order.created_at, 'order-placed', user_id, data)

def _get_additional_data(
    event: DbUserEvent, users_by_id: Dict[str, User]
) -> Iterator[Tuple[str, Any]]:
    if event.event_type in {
        'user-created',
        'user-deleted',
        'user-details-updated',
        'user-email-address-changed',
        'user-email-address-invalidated',
        'user-initialized',
        'user-screen-name-changed',
        'user-suspended',
        'user-unsuspended',
        'password-updated',
        'user-avatar-updated',
        'consent-expressed',
        'newsletter-requested',
        'newsletter-declined',
        'order-placed',
        'orgaflag-added',
        'orgaflag-removed',
        'privacy-policy-accepted',
        'role-assigned',
        'role-deassigned',
        'user-badge-awarded',
    }:
        yield from _get_additional_data_for_user_initiated_event(
            event, users_by_id
        )

    if event.event_type == 'user-badge-awarded':
        badge = user_badge_service.find_badge(event.data['badge_id'])
        yield 'badge', badge

    if event.event_type == 'user-details-updated':
        details = {
            k: v
            for k, v in event.data.items()
            if k.startswith('old_') or k.startswith('new_')
        }
        yield 'details', details

def _get_additional_data_for_user_initiated_event(
    event: DbUserEvent, users_by_id: Dict[str, User]
) -> Iterator[Tuple[str, Any]]:
    initiator_id = event.data.get('initiator_id')
    if initiator_id is not None:
        yield 'initiator', users_by_id[initiator_id]
<commit_msg>Split user search term into separate words and clauses<commit_after>"""
byceps.blueprints.admin.user.service
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:Copyright: 2006-2020 Jochen Kupperschmidt
:License: Revised BSD (see `LICENSE` file for details)
"""

from collections import defaultdict
from datetime import datetime, timedelta
from itertools import chain
from operator import attrgetter
from typing import Any, Dict, Iterator, List, Optional, Sequence, Set, Tuple

from ....database import db, paginate, Pagination
from ....services.consent import consent_service
from ....services.newsletter import service as newsletter_service
from ....services.newsletter.transfer.models import List as NewsletterList
from ....services.party import service as party_service
from ....services.party.transfer.models import Party
from ....services.shop.order import service as order_service
from ....services.ticketing.models.ticket import Ticket as DbTicket
from ....services.ticketing import attendance_service, ticket_service
from ....services.user import event_service
from ....services.user.models.detail import UserDetail as DbUserDetail
from ....services.user.models.event import (
    UserEvent as DbUserEvent,
    UserEventData,
)
from ....services.user.models.user import User as DbUser
from ....services.user import service as user_service
from ....services.user.transfer.models import User
from ....services.user_avatar import service as avatar_service
from ....services.user_badge import badge_service as user_badge_service
from ....typing import PartyID, UserID

from .models import Detail, UserStateFilter, UserWithCreationDetails

def get_users_paginated(
    page, per_page, *, search_term=None, state_filter=None
) -> Pagination:
    """Return the users to show on the specified page, optionally
    filtered by search term or flags.
    """
    query = DbUser.query \
        .options(
            db.joinedload('avatar_selection').joinedload('avatar'),
            db.joinedload('detail').load_only('first_names', 'last_name'),
        ) \
        .order_by(DbUser.created_at.desc())

    query = _filter_by_state(query, state_filter)

    if search_term:
        query = _filter_by_search_term(query, search_term)

    return paginate(
        query,
        page,
        per_page,
        item_mapper=_db_entity_to_user_with_creation_details,
    )

def _filter_by_state(query, state_filter):
    if state_filter == UserStateFilter.active:
        return query \
            .filter_by(initialized=True) \
            .filter_by(suspended=False) \
            .filter_by(deleted=False)
    elif state_filter == UserStateFilter.uninitialized:
        return query \
            .filter_by(initialized=False) \
            .filter_by(suspended=False) \
            .filter_by(deleted=False)
    elif state_filter == UserStateFilter.suspended:
        return query \
            .filter_by(suspended=True) \
            .filter_by(deleted=False)
    elif state_filter == UserStateFilter.deleted:
        return query \
            .filter_by(deleted=True)
    else:
        return query

def _filter_by_search_term(query, search_term):
    terms = search_term.split(' ')
    clauses = chain.from_iterable(map(_generate_search_clauses_for_term, terms))

    return query \
        .join(DbUserDetail) \
        .filter(db.or_(*clauses))

def _generate_search_clauses_for_term(search_term: str) -> List:
    ilike_pattern = f'%{search_term}%'

    return [
        DbUser.email_address.ilike(ilike_pattern),
        DbUser.screen_name.ilike(ilike_pattern),
        DbUserDetail.first_names.ilike(ilike_pattern),
        DbUserDetail.last_name.ilike(ilike_pattern),
    ]

def get_users_created_since(
    delta: timedelta, limit: Optional[int] = None
) -> List[User]:
    """Return the user accounts created since `delta` ago."""
    filter_starts_at = datetime.utcnow() - delta

    query = DbUser.query \
        .options(
            db.joinedload('avatar_selection').joinedload('avatar'),
            db.joinedload('detail').load_only('first_names', 'last_name'),
        ) \
        .filter(DbUser.created_at >= filter_starts_at) \
        .order_by(DbUser.created_at.desc())

    if limit is not None:
        query = query.limit(limit)

    users = query.all()

    return [_db_entity_to_user_with_creation_details(u) for u in users]

def _db_entity_to_user_with_creation_details(
    user: DbUser,
) -> UserWithCreationDetails:
    is_orga = False  # Not interesting here.
    full_name = user.detail.full_name if user.detail is not None else None
    detail = Detail(full_name)

    return UserWithCreationDetails(
        user.id,
        user.screen_name,
        user.suspended,
        user.deleted,
        user.avatar.url if user.avatar else None,
        is_orga,
        user.created_at,
        user.initialized,
        detail,
    )

def get_parties_and_tickets(
    user_id: UserID,
) -> List[Tuple[Party, List[DbTicket]]]:
    """Return tickets the user uses or manages, and the related parties."""
    tickets = ticket_service.find_tickets_related_to_user(user_id)

    tickets_by_party_id = _group_tickets_by_party_id(tickets)

    party_ids = set(tickets_by_party_id.keys())
    parties_by_id = _get_parties_by_id(party_ids)

    parties_and_tickets = [
        (parties_by_id[party_id], tickets)
        for party_id, tickets in tickets_by_party_id.items()
    ]

    parties_and_tickets.sort(key=lambda x: x[0].starts_at, reverse=True)

    return parties_and_tickets

def _group_tickets_by_party_id(
    tickets: Sequence[DbTicket],
) -> Dict[PartyID, List[DbTicket]]:
    tickets_by_party_id: Dict[PartyID, List[DbTicket]] = defaultdict(list)

    for ticket in tickets:
        tickets_by_party_id[ticket.category.party_id].append(ticket)

    return tickets_by_party_id

def _get_parties_by_id(party_ids: Set[PartyID]) -> Dict[PartyID, Party]:
    parties = party_service.get_parties(party_ids)
    return {p.id: p for p in parties}

def get_attended_parties(user_id: UserID) -> List[Party]:
    """Return the parties attended by the user, in order."""
    attended_parties = attendance_service.get_attended_parties(user_id)
    attended_parties.sort(key=attrgetter('starts_at'), reverse=True)
    return attended_parties

def get_newsletter_subscriptions(
    user_id: UserID,
) -> Iterator[Tuple[NewsletterList, bool]]:
    lists = newsletter_service.get_all_lists()
    for list_ in lists:
        is_subscribed = newsletter_service.is_subscribed(user_id, list_.id)
        yield list_, is_subscribed

def get_events(user_id: UserID) -> Iterator[UserEventData]:
    events = event_service.get_events_for_user(user_id)
    events.extend(_fake_avatar_update_events(user_id))
    events.extend(_fake_consent_events(user_id))
    events.extend(_fake_newsletter_subscription_update_events(user_id))
    events.extend(_fake_order_events(user_id))

    user_ids = {
        event.data['initiator_id']
        for event in events
        if 'initiator_id' in event.data
    }
    users = user_service.find_users(user_ids, include_avatars=True)
    users_by_id = {str(user.id): user for user in users}

    for event in events:
        data = {
            'event': event.event_type,
            'occurred_at': event.occurred_at,
            'data': event.data,
        }

        additional_data = _get_additional_data(event, users_by_id)
        data.update(additional_data)

        yield data

def _fake_avatar_update_events(user_id: UserID) -> Iterator[DbUserEvent]:
    """Yield the user's avatar updates as volatile events."""
    avatar_updates = avatar_service.get_avatars_uploaded_by_user(user_id)

    for avatar_update in avatar_updates:
        data = {
            'initiator_id': str(user_id),
            'url_path': avatar_update.url_path,
        }

        yield DbUserEvent(
            avatar_update.occurred_at, 'user-avatar-updated', user_id, data
        )

def _fake_consent_events(user_id: UserID) -> Iterator[DbUserEvent]:
    """Yield the user's consents as volatile events."""
    consents = consent_service.get_consents_by_user(user_id)

    for consent in consents:
        data = {
            'initiator_id': str(user_id),
            'subject_title': consent.subject.title,
        }

        yield DbUserEvent(
            consent.expressed_at, 'consent-expressed', user_id, data
        )

def _fake_newsletter_subscription_update_events(
    user_id: UserID,
) -> Iterator[DbUserEvent]:
    """Yield the user's newsletter subscription updates as volatile events."""
    lists = newsletter_service.get_all_lists()
    lists_by_id = {list_.id: list_ for list_ in lists}

    updates = newsletter_service.get_subscription_updates_for_user(user_id)

    for update in updates:
        event_type = f'newsletter-{update.state.name}'

        list_ = lists_by_id[update.list_id]

        data = {
            'list_': list_,
            'initiator_id': str(user_id),
        }

        yield DbUserEvent(update.expressed_at, event_type, user_id, data)

def _fake_order_events(user_id: UserID) -> Iterator[DbUserEvent]:
    """Yield the orders placed by the user as volatile events."""
    orders = order_service.get_orders_placed_by_user(user_id)

    for order in orders:
        data = {
            'initiator_id': str(user_id),
            'order': order,
        }

        yield DbUserEvent(order.created_at, 'order-placed', user_id, data)

def _get_additional_data(
    event: DbUserEvent, users_by_id: Dict[str, User]
) -> Iterator[Tuple[str, Any]]:
    if event.event_type in {
        'user-created',
        'user-deleted',
        'user-details-updated',
        'user-email-address-changed',
        'user-email-address-invalidated',
        'user-initialized',
        'user-screen-name-changed',
        'user-suspended',
        'user-unsuspended',
        'password-updated',
        'user-avatar-updated',
        'consent-expressed',
        'newsletter-requested',
        'newsletter-declined',
        'order-placed',
        'orgaflag-added',
        'orgaflag-removed',
        'privacy-policy-accepted',
        'role-assigned',
        'role-deassigned',
        'user-badge-awarded',
    }:
        yield from _get_additional_data_for_user_initiated_event(
            event, users_by_id
        )

    if event.event_type == 'user-badge-awarded':
        badge = user_badge_service.find_badge(event.data['badge_id'])
        yield 'badge', badge

    if event.event_type == 'user-details-updated':
        details = {
            k: v
            for k, v in event.data.items()
            if k.startswith('old_') or k.startswith('new_')
        }
        yield 'details', details

def _get_additional_data_for_user_initiated_event(
    event: DbUserEvent, users_by_id: Dict[str, User]
) -> Iterator[Tuple[str, Any]]:
    initiator_id = event.data.get('initiator_id')
    if initiator_id is not None:
        yield 'initiator', users_by_id[initiator_id]
<|endoftext|>


<commit_before>"""
byceps.blueprints.snippet.templating
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:Copyright: 2006-2019 Jochen Kupperschmidt
:License: Modified BSD, see LICENSE for details.
"""

import sys
import traceback

from flask import abort, g, render_template, url_for
from jinja2 import TemplateNotFound

from ...services.snippet import service as snippet_service
from ...services.snippet.service import SnippetNotFound
from ...services.snippet.transfer.models import Scope
from ...util.templating import get_variable_value, load_template

def render_snippet_as_page(version):
    """Render the given version of the snippet, or an error page if
    that fails.
    """
    try:
        context = get_snippet_context(version)
        return render_template('snippet/view.html', **context)
    except TemplateNotFound:
        abort(404)
    except Exception as e:
        print('Error in snippet markup:', e, file=sys.stderr)
        traceback.print_exc()
        context = {
            'message': str(e),
        }
        return render_template('snippet/error.html', **context), 500

def get_snippet_context(version):
    """Return the snippet context to insert into the outer template."""
    template = _load_template_with_globals(version.body)

    current_page = get_variable_value(template, 'current_page')
    title = version.title
    head = _render_template(version.head) if version.head else None
    body = template.render()

    return {
        'title': title,
        'current_page': current_page,
        'head': head,
        'body': body,
    }

def render_snippet_as_partial(
    name, *, scope=None, ignore_if_unknown=False, context=None
):
    """Render the latest version of the snippet with the given name and
    return the result.
    """
    if scope is None:
        scope = Scope.for_site(g.site_id)

    current_version = snippet_service.find_current_version_of_snippet_with_name(
        scope, name
    )

    if current_version is None:
        if ignore_if_unknown:
            return ''
        else:
            raise SnippetNotFound(scope, name)

    if context is None:
        context = {}

    return _render_template(current_version.body, context=context)

def _render_template(source, *, context=None):
    template = _load_template_with_globals(source)

    if context is None:
        context = {}

    return template.render(**context)

def _load_template_with_globals(source):
    template_globals = {
        'render_snippet': render_snippet_as_partial,
        'url_for': url_for,
    }

    return load_template(source, template_globals=template_globals)

def url_for_snippet(name):
    """Render an URL pointing to the snippet document's mountpoint."""
    return url_for(f'snippet.{name}')
<commit_msg>Make `url_for_snippet` available inside of snippets, too<commit_after>"""
byceps.blueprints.snippet.templating
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:Copyright: 2006-2019 Jochen Kupperschmidt
:License: Modified BSD, see LICENSE for details.
"""

import sys
import traceback

from flask import abort, g, render_template, url_for
from jinja2 import TemplateNotFound

from ...services.snippet import service as snippet_service
from ...services.snippet.service import SnippetNotFound
from ...services.snippet.transfer.models import Scope
from ...util.templating import get_variable_value, load_template

def render_snippet_as_page(version):
    """Render the given version of the snippet, or an error page if
    that fails.
    """
    try:
        context = get_snippet_context(version)
        return render_template('snippet/view.html', **context)
    except TemplateNotFound:
        abort(404)
    except Exception as e:
        print('Error in snippet markup:', e, file=sys.stderr)
        traceback.print_exc()
        context = {
            'message': str(e),
        }
        return render_template('snippet/error.html', **context), 500

def get_snippet_context(version):
    """Return the snippet context to insert into the outer template."""
    template = _load_template_with_globals(version.body)

    current_page = get_variable_value(template, 'current_page')
    title = version.title
    head = _render_template(version.head) if version.head else None
    body = template.render()

    return {
        'title': title,
        'current_page': current_page,
        'head': head,
        'body': body,
    }

def render_snippet_as_partial(
    name, *, scope=None, ignore_if_unknown=False, context=None
):
    """Render the latest version of the snippet with the given name and
    return the result.
    """
    if scope is None:
        scope = Scope.for_site(g.site_id)

    current_version = snippet_service.find_current_version_of_snippet_with_name(
        scope, name
    )

    if current_version is None:
        if ignore_if_unknown:
            return ''
        else:
            raise SnippetNotFound(scope, name)

    if context is None:
        context = {}

    return _render_template(current_version.body, context=context)

def _render_template(source, *, context=None):
    template = _load_template_with_globals(source)

    if context is None:
        context = {}

    return template.render(**context)

def _load_template_with_globals(source):
    template_globals = {
        'render_snippet': render_snippet_as_partial,
        'url_for': url_for,
        'url_for_snippet': url_for_snippet,
    }

    return load_template(source, template_globals=template_globals)

def url_for_snippet(name):
    """Render an URL pointing to the snippet document's mountpoint."""
    return url_for(f'snippet.{name}')
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
#
# Copyright (c) 2011 -- Lars Heuer <heuer[at]semagia.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#
#     * Redistributions in binary form must reproduce the above
#       copyright notice, this list of conditions and the following
#       disclaimer in the documentation and/or other materials provided
#       with the distribution.
#
#     * Neither the project name nor the names of the contributors may be
#       used to endorse or promote products derived from this software
#       without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
"""\
Provides access to common functions of the reader module.

:author:       Lars Heuer (heuer[at]semagia.com)
:organization: Semagia - <http://www.semagia.com/>
:license:      BSD license
"""
from cablemap.core.reader import cable_from_file, cable_from_html
from cablemap.core.utils import cables_from_directory, cable_by_id

__all__ = ['cable_from_file', 'cable_from_html',
           'cables_from_directory', 'cable_from_id',
           ]
<commit_msg>Fixed typo<commit_after># -*- coding: utf-8 -*-
#
# Copyright (c) 2011 -- Lars Heuer <heuer[at]semagia.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#
#     * Redistributions in binary form must reproduce the above
#       copyright notice, this list of conditions and the following
#       disclaimer in the documentation and/or other materials provided
#       with the distribution.
#
#     * Neither the project name nor the names of the contributors may be
#       used to endorse or promote products derived from this software
#       without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
"""\
Provides access to common functions of the reader module.

:author:       Lars Heuer (heuer[at]semagia.com)
:organization: Semagia - <http://www.semagia.com/>
:license:      BSD license
"""
from cablemap.core.reader import cable_from_file, cable_from_html
from cablemap.core.utils import cables_from_directory, cable_by_id

__all__ = ['cable_from_file', 'cable_from_html',
           'cables_from_directory', 'cable_by_id',
           ]
<|endoftext|>


<commit_before>#!/usr/bin/python

from __future__ import absolute_import, division, print_function, unicode_literals
import configparser
import requests
import argparse
import logging
import time
from datetime import datetime, timedelta
from HTMLParser import HTMLParser

__version__ = "0.3"
__date__ = "2013/16/07"
__updated__ = "2013/02/08"
__author__ = "Andrew McIntosh (github.com/amcintosh)"
__copyright__ = "Copyright 2013, Andrew McIntosh"
__license__ = "GPL"

STACK_EXCHANGE_BASE_URL = "http://api.stackexchange.com/2.1"
PUSHOVER_BASE_URL = "https://api.pushover.net/1/messages.json"
LOGGER = None

def send_to_pushover(pushover_config, title, message, url=None, url_title=None):
    '''Send notification to Pushover'''
    payload = { "title": title, "message": message }
    payload["token"] = pushover_config.get("appkey")
    payload["user"] = pushover_config.get("userkey")
    payload["priority"] = pushover_config.get("priority")
    if "device" in pushover_config:
        payload["device"] = pushover_config.get("device")
    if url:
        payload["url"] = url
        payload["url_title"] = url_title

    res = requests.post(PUSHOVER_BASE_URL, data=payload)
    if LOGGER and res.status_code == requests.codes.ok:
        LOGGER.debug("Sent to Pushover: %s", payload)
    elif LOGGER:
        LOGGER.warn("Failed to send to Pushover: %s", res.text)

def send_questions_to_pushover(pushover_config, exchange, questions):
    '''Send notification of new StackExchange questions to Pushover.
  	   Title will contain the exchange name and message will contain
       either the number of new questions or the title of the question
       if only one exists.
    '''
    title = "PushOverflow: " + exchange.name
    if len(questions)==1:
        message = ("New question posted: "
                + HTMLParser().unescape(questions[0].get("title")))
        url = questions[0].get("link")
        url_title = "Open question"
    else:
        message = str(len(questions)) + " new questions posted."
        url = ("http://" + exchange.name
            + ".stackexchange.com/questions?sort=newest")
        url_title = "Open " + exchange.name + ".stackexchange.com"
    send_to_pushover(pushover_config, title, message, url, url_title)

def get_stack_exchange_questions(stack_exchange_site, from_date):
    '''Get new questions posted to stackexchange site since the
       provided time.
    '''
    stack_url = STACK_EXCHANGE_BASE_URL + "/questions"
    payload = { "fromdate": int(time.mktime(from_date.timetuple())),
                "site": stack_exchange_site}
    res = requests.get(stack_url, params=payload)
    if res.status_code != requests.codes.ok:
        if LOGGER:
            LOGGER.warn("Failed to retrieve from StackExchange: %s", res.text)
        return {}
    return res.json()

def filter_questions(questions, tags, excluded):
    '''Takes a list of questions and filters them.
       If tags is not empty, will filter any questions that do not include one of these tags.
       If excluded is not empty, will filter any questions that include one of these tags.
    '''
    filtered_questions = []
    for question in questions.get("items"):
        question_tags = question.get("tags")
        in_tags = False
        in_excluded = False
        for tag in question_tags:
            if len(tags)==0 or tag in tags:
                in_tags = True
            if tag in excluded:
                in_excluded = True
        if in_tags and not in_excluded:
            filtered_questions.append(question)
            if LOGGER:
                LOGGER.debug("Found question: '%s'", question.get("title"))
        elif LOGGER:
            LOGGER.debug("Filtered question: '%s', with tags: %s",
                         question.get("title"), question_tags)

    return filtered_questions

def check_exchange(exchange, from_date):
    '''Get new questions for this stackexchange site based off of
       filters provided in configuration file.
	'''
    tags = excluded = []
    if exchange.get("tags"):
        tags = [x.strip() for x in exchange.get("tags").split(",")]
    if exchange.get("exclude"):
        excluded = [x.strip() for x in exchange.get("exclude").split(",")]
    if LOGGER:
        LOGGER.info("check_exchange: [%s], from_date: %s",
                    exchange.name, from_date.isoformat())

    questions = get_stack_exchange_questions(exchange.name, from_date)
    if questions:
        questions = filter_questions(questions, tags, excluded)
    return questions

def main():
    '''Parse arguments, configuration file, loop through exchanges.'''
    parser = argparse.ArgumentParser(
               description="Check for new StackExchange questions and notify "
                           "via Pushover")
    parser.add_argument("config",
                        metavar="config_file",
                        nargs="?",
                        default="pushoverflow.ini",
                        help="Configuration file (defaults to "
                             "'./pushoverflow.ini')")
    parser.add_argument("-v", "--verbose",
                        dest="log_file",
                        action="store",
                        const=".pushoverflow.log",
                        nargs="?",
                        help="enable logging for debug (logs to "
                             "'./.pushoverflow.log')")
    parser.add_argument('--version', action='version', version=__version__)
    args = parser.parse_args()

    if (args.log_file):
        logging.basicConfig(filename=args.log_file,
                            format='%(asctime)s - %(levelname)s: %(message)s')
        global LOGGER
        LOGGER = logging.getLogger(__name__)
        LOGGER.setLevel(logging.DEBUG)

    config = configparser.ConfigParser()
    config.read(args.config)

    from_date = None
    try:
        time_delta = int(config.get("Global","time_delta_minutes"))
        from_date = datetime.now() - timedelta(minutes=time_delta)
    except configparser.NoOptionError as err:
        print ("Missing properties in configuration file:", err)
        return

    except configparser.NoOptionError as err:
        print ("Missing properties in configuration file:", err)
        return
    for section in config.sections():
        if section == "Global" or section == "Pushover":
            continue
        exchange = config[section]
        questions = check_exchange(exchange, from_date)
        if len(questions) > 0:
            send_questions_to_pushover(config["Pushover"], exchange, questions)

if __name__ == "__main__":
    main()
<commit_msg>Maintain python2/3 compatibility with HTMLParser/html.parser<commit_after>#!/usr/bin/python

from __future__ import absolute_import, division, print_function, unicode_literals
import configparser
import requests
import argparse
import logging
import time
from datetime import datetime, timedelta
try:
    from html.parser import HTMLParser
except ImportError:
    from HTMLParser import HTMLParser

__version__ = "0.3"
__date__ = "2013/16/07"
__updated__ = "2013/02/08"
__author__ = "Andrew McIntosh (github.com/amcintosh)"
__copyright__ = "Copyright 2013, Andrew McIntosh"
__license__ = "GPL"

STACK_EXCHANGE_BASE_URL = "http://api.stackexchange.com/2.1"
PUSHOVER_BASE_URL = "https://api.pushover.net/1/messages.json"
LOGGER = None

def send_to_pushover(pushover_config, title, message, url=None, url_title=None):
    '''Send notification to Pushover'''
    payload = { "title": title, "message": message }
    payload["token"] = pushover_config.get("appkey")
    payload["user"] = pushover_config.get("userkey")
    payload["priority"] = pushover_config.get("priority")
    if "device" in pushover_config:
        payload["device"] = pushover_config.get("device")
    if url:
        payload["url"] = url
        payload["url_title"] = url_title

    res = requests.post(PUSHOVER_BASE_URL, data=payload)
    if LOGGER and res.status_code == requests.codes.ok:
        LOGGER.debug("Sent to Pushover: %s", payload)
    elif LOGGER:
        LOGGER.warn("Failed to send to Pushover: %s", res.text)

def send_questions_to_pushover(pushover_config, exchange, questions):
    '''Send notification of new StackExchange questions to Pushover.
  	   Title will contain the exchange name and message will contain
       either the number of new questions or the title of the question
       if only one exists.
    '''
    title = "PushOverflow: " + exchange.name
    if len(questions)==1:
        message = ("New question posted: "
                + HTMLParser().unescape(questions[0].get("title")))
        url = questions[0].get("link")
        url_title = "Open question"
    else:
        message = str(len(questions)) + " new questions posted."
        url = ("http://" + exchange.name
            + ".stackexchange.com/questions?sort=newest")
        url_title = "Open " + exchange.name + ".stackexchange.com"
    send_to_pushover(pushover_config, title, message, url, url_title)

def get_stack_exchange_questions(stack_exchange_site, from_date):
    '''Get new questions posted to stackexchange site since the
       provided time.
    '''
    stack_url = STACK_EXCHANGE_BASE_URL + "/questions"
    payload = { "fromdate": int(time.mktime(from_date.timetuple())),
                "site": stack_exchange_site}
    res = requests.get(stack_url, params=payload)
    if res.status_code != requests.codes.ok:
        if LOGGER:
            LOGGER.warn("Failed to retrieve from StackExchange: %s", res.text)
        return {}
    return res.json()

def filter_questions(questions, tags, excluded):
    '''Takes a list of questions and filters them.
       If tags is not empty, will filter any questions that do not include one of these tags.
       If excluded is not empty, will filter any questions that include one of these tags.
    '''
    filtered_questions = []
    for question in questions.get("items"):
        question_tags = question.get("tags")
        in_tags = False
        in_excluded = False
        for tag in question_tags:
            if len(tags)==0 or tag in tags:
                in_tags = True
            if tag in excluded:
                in_excluded = True
        if in_tags and not in_excluded:
            filtered_questions.append(question)
            if LOGGER:
                LOGGER.debug("Found question: '%s'", question.get("title"))
        elif LOGGER:
            LOGGER.debug("Filtered question: '%s', with tags: %s",
                         question.get("title"), question_tags)

    return filtered_questions

def check_exchange(exchange, from_date):
    '''Get new questions for this stackexchange site based off of
       filters provided in configuration file.
	'''
    tags = excluded = []
    if exchange.get("tags"):
        tags = [x.strip() for x in exchange.get("tags").split(",")]
    if exchange.get("exclude"):
        excluded = [x.strip() for x in exchange.get("exclude").split(",")]
    if LOGGER:
        LOGGER.info("check_exchange: [%s], from_date: %s",
                    exchange.name, from_date.isoformat())

    questions = get_stack_exchange_questions(exchange.name, from_date)
    if questions:
        questions = filter_questions(questions, tags, excluded)
    return questions

def main():
    '''Parse arguments, configuration file, loop through exchanges.'''
    parser = argparse.ArgumentParser(
               description="Check for new StackExchange questions and notify "
                           "via Pushover")
    parser.add_argument("config",
                        metavar="config_file",
                        nargs="?",
                        default="pushoverflow.ini",
                        help="Configuration file (defaults to "
                             "'./pushoverflow.ini')")
    parser.add_argument("-v", "--verbose",
                        dest="log_file",
                        action="store",
                        const=".pushoverflow.log",
                        nargs="?",
                        help="enable logging for debug (logs to "
                             "'./.pushoverflow.log')")
    parser.add_argument('--version', action='version', version=__version__)
    args = parser.parse_args()

    if (args.log_file):
        logging.basicConfig(filename=args.log_file,
                            format='%(asctime)s - %(levelname)s: %(message)s')
        global LOGGER
        LOGGER = logging.getLogger(__name__)
        LOGGER.setLevel(logging.DEBUG)

    config = configparser.ConfigParser()
    config.read(args.config)

    from_date = None
    try:
        time_delta = int(config.get("Global","time_delta_minutes"))
        from_date = datetime.now() - timedelta(minutes=time_delta)
    except configparser.NoOptionError as err:
        print ("Missing properties in configuration file:", err)
        return

    except configparser.NoOptionError as err:
        print ("Missing properties in configuration file:", err)
        return
    for section in config.sections():
        if section == "Global" or section == "Pushover":
            continue
        exchange = config[section]
        questions = check_exchange(exchange, from_date)
        if len(questions) > 0:
            send_questions_to_pushover(config["Pushover"], exchange, questions)

if __name__ == "__main__":
    main()
<|endoftext|>


<commit_before>from datetime import timedelta, datetime
from django.test import TestCase
from django.utils import timezone
from puzzle.models import Author, Puzzle, Entry
from puzzle.feeds import PuzzleFeed
from puzzle.views import create_grid, get_clues, get_date_string

class PuzzleModelTests(TestCase):
    def test_default_author(self):
        Author.objects.create(name='Test Author')
        p = Puzzle.objects.create(number=0, pub_date=timezone.now())
        self.assertEqual(p.author.name, 'Test Author')

    def test_default_numbering(self):
        a = Author.objects.create(name='Test Author')
        p = Puzzle.objects.create(author=a, pub_date=timezone.now())
        self.assertEqual(p.number, 0)
        p = Puzzle.objects.create(author=a, pub_date=timezone.now())
        self.assertEqual(p.number, 1)

    def test_default_pub_date(self):
        a = Author.objects.create(name='Test Author')
        p = Puzzle.objects.create(number=0, author=a)
        self.assertTrue(p.pub_date > timezone.now())
        self.assertEqual(p.pub_date.year, 2100)
        self.assertEqual(p.pub_date.hour, 0)
        self.assertEqual(p.pub_date.second, 0)

    def test_published_puzzle_url(self):
        a = Author.objects.create(name='Test Author')
        p = Puzzle.objects.create(pub_date=timezone.now())
        self.assertFalse('preview' in p.get_absolute_url())
        self.assertTrue(str(p.number) in p.get_absolute_url())

    def test_preview_puzzle_url(self):
        a = Author.objects.create(name='Test Author')
        p = Puzzle.objects.create(pub_date=timezone.now() + timedelta(days=1))
        self.assertTrue('preview' in p.get_absolute_url())
        self.assertTrue(str(p.number) in p.get_absolute_url())

class FeedTests(TestCase):
    def get_author(self):
        if (Author.objects.count()):
            return Author.objects.first()
        else:
            return Author.objects.create(name='Test Author')

    def create_puzzle(self, number, pub_date):
        return Puzzle.objects.create(number=number, author=self.get_author(), pub_date=pub_date)

    def test_most_recent_first(self):
        self.create_puzzle(0, timezone.now() - timedelta(days=2))
        self.create_puzzle(1, timezone.now() - timedelta(days=1))
        self.create_puzzle(2, timezone.now())
        feed = PuzzleFeed()
        self.assertEqual(feed.items()[0].number, 2)
        self.assertEqual(feed.items()[1].number, 1)
        self.assertEqual(feed.items()[2].number, 0)

    def test_published_puzzles_only(self):
        self.create_puzzle(0, timezone.now() - timedelta(days=1))
        self.create_puzzle(1, timezone.now())
        self.create_puzzle(2, timezone.now() + timedelta(days=1))
        feed = PuzzleFeed()
        self.assertEqual(feed.items().count(), 2)
        self.assertEqual(feed.items()[0].number, 1)
        self.assertEqual(feed.items()[1].number, 0)

    def test_limited_number(self):
        num_puzzles = 10
        limit = 5
        for i in range(num_puzzles):
            self.create_puzzle(i, timezone.now() - timedelta(days=num_puzzles - i))
        feed = PuzzleFeed()
        self.assertEqual(feed.items().count(), limit)

def create_small_puzzle():
    size = 3
    a = Author.objects.create(name='Test Author')
    p = Puzzle.objects.create(size=size)
    e0 = Entry.objects.create(puzzle=p, clue='1a', answer='ab c', x=0, y=0, down=False)
    e1 = Entry.objects.create(puzzle=p, clue='3a', answer='x-yz', x=0, y=2, down=False)
    e2 = Entry.objects.create(puzzle=p, clue='1d', answer='amx', x=0, y=0, down=True)
    e3 = Entry.objects.create(puzzle=p, clue='2d', answer='cnz', x=2, y=0, down=True)
    return p

class GridCreationTests(TestCase):
    def test_create_grid_pattern(self):
        grid = create_grid(create_small_puzzle(), 3)
        for row in range(3):
            for col in range(3):
                self.assertEqual(grid[row][col]['row'], row)
                self.assertEqual(grid[row][col]['col'], col)
                if row == 1 and col == 1:
                    self.assertFalse('light' in grid[row][col]['type'])
                    self.assertTrue('block' in grid[row][col]['type'])
                else:
                    self.assertTrue('light' in grid[row][col]['type'])
                    self.assertFalse('block' in grid[row][col]['type'])

    def test_create_grid_clue_numbers(self):
        grid = create_grid(create_small_puzzle(), 3)
        expected_numbers = [[1, None, 2], [None, None, None], [3, None, None]]
        for row in range(3):
            for col in range(3):
                self.assertEqual(grid[row][col]['number'], expected_numbers[row][col])

    def test_create_grid_letters(self):
        grid = create_grid(create_small_puzzle(), 3)
        expected_letters = [['A', 'B', 'C'], ['M', None, 'N'], ['X', 'Y', 'Z']]
        for row in range(3):
            for col in range(3):
                self.assertEqual(grid[row][col]['letter'], expected_letters[row][col])

    def test_create_grid_borders(self):
        grid = create_grid(create_small_puzzle(), 3)
        for row in range(3):
            for col in range(3):
                if row == 0:
                    self.assertTrue('topmost' in grid[row][col]['type'])
                else:
                    self.assertFalse('topmost' in grid[row][col]['type'])
                if col == 0:
                    self.assertTrue('leftmost' in grid[row][col]['type'])
                else:
                    self.assertFalse('leftmost' in grid[row][col]['type'])

class ClueCreationTests(TestCase):
    def test_get_clues(self):
        p = create_small_puzzle()
        grid = create_grid(p, 3)

        across_clues = get_clues(p, grid, False)
        self.assertEquals(len(across_clues), 2)
        self.assertEquals(across_clues[0]['number'], 1)
        self.assertEquals(across_clues[0]['clue'], '1a')
        self.assertEquals(across_clues[0]['numeration'], '2,1')
        self.assertEquals(across_clues[1]['number'], 3)
        self.assertEquals(across_clues[1]['clue'], '3a')
        self.assertEquals(across_clues[1]['numeration'], '1-2')

        down_clues = get_clues(p, grid, True)
        self.assertEquals(len(down_clues), 2)
        self.assertEquals(down_clues[0]['number'], 1)
        self.assertEquals(down_clues[0]['clue'], '1d')
        self.assertEquals(down_clues[0]['numeration'], '3')
        self.assertEquals(down_clues[1]['number'], 2)
        self.assertEquals(down_clues[1]['clue'], '2d')
        self.assertEquals(down_clues[1]['numeration'], '3')

class DateFormattingTests(TestCase):
    def test_get_date_string(self):
        dt = datetime(1980, 3, 4, 1, 2, 3, tzinfo=timezone.get_default_timezone())
        a = Author.objects.create(name='Test Author')
        p = Puzzle.objects.create(pub_date=dt)
        self.assertEquals(get_date_string(p), '04 Mar 1980')
<commit_msg>Added tests for puzzle views.<commit_after>from datetime import timedelta, datetime
from django.test import TestCase
from django.utils import timezone
from django.core.urlresolvers import reverse
from puzzle.models import Author, Puzzle, Entry
from puzzle.feeds import PuzzleFeed
from puzzle.views import create_grid, get_clues, get_date_string

def create_small_puzzle():
    size = 3
    a = Author.objects.create(name='Test Author')
    p = Puzzle.objects.create(size=size)
    e0 = Entry.objects.create(puzzle=p, clue='1a', answer='ab c', x=0, y=0, down=False)
    e1 = Entry.objects.create(puzzle=p, clue='3a', answer='x-yz', x=0, y=2, down=False)
    e2 = Entry.objects.create(puzzle=p, clue='1d', answer='amx', x=0, y=0, down=True)
    e3 = Entry.objects.create(puzzle=p, clue='2d', answer='cnz', x=2, y=0, down=True)
    return p

def create_puzzle_range():
    for i in range(-2, 3):
        p = create_small_puzzle()
        p.pub_date = pub_date=timezone.now() + timedelta(days=i)
        p.save()

class PuzzleModelTests(TestCase):
    def test_default_author(self):
        Author.objects.create(name='Test Author')
        p = Puzzle.objects.create(number=0, pub_date=timezone.now())
        self.assertEqual(p.author.name, 'Test Author')

    def test_default_numbering(self):
        a = Author.objects.create(name='Test Author')
        p = Puzzle.objects.create(author=a, pub_date=timezone.now())
        self.assertEqual(p.number, 0)
        p = Puzzle.objects.create(author=a, pub_date=timezone.now())
        self.assertEqual(p.number, 1)

    def test_default_pub_date(self):
        a = Author.objects.create(name='Test Author')
        p = Puzzle.objects.create(number=0, author=a)
        self.assertTrue(p.pub_date > timezone.now())
        self.assertEqual(p.pub_date.year, 2100)
        self.assertEqual(p.pub_date.hour, 0)
        self.assertEqual(p.pub_date.second, 0)

    def test_published_puzzle_url(self):
        a = Author.objects.create(name='Test Author')
        p = Puzzle.objects.create(pub_date=timezone.now())
        self.assertNotIn('preview', p.get_absolute_url())
        self.assertIn(str(p.number), p.get_absolute_url())

    def test_preview_puzzle_url(self):
        a = Author.objects.create(name='Test Author')
        p = Puzzle.objects.create(pub_date=timezone.now() + timedelta(days=1))
        self.assertIn('preview', p.get_absolute_url())
        self.assertIn(str(p.number), p.get_absolute_url())

class FeedTests(TestCase):
    def get_author(self):
        if (Author.objects.count()):
            return Author.objects.first()
        else:
            return Author.objects.create(name='Test Author')

    def create_puzzle(self, number, pub_date):
        return Puzzle.objects.create(number=number, author=self.get_author(), pub_date=pub_date)

    def test_most_recent_first(self):
        self.create_puzzle(0, timezone.now() - timedelta(days=2))
        self.create_puzzle(1, timezone.now() - timedelta(days=1))
        self.create_puzzle(2, timezone.now())
        feed = PuzzleFeed()
        self.assertEqual(feed.items()[0].number, 2)
        self.assertEqual(feed.items()[1].number, 1)
        self.assertEqual(feed.items()[2].number, 0)

    def test_published_puzzles_only(self):
        self.create_puzzle(0, timezone.now() - timedelta(days=1))
        self.create_puzzle(1, timezone.now())
        self.create_puzzle(2, timezone.now() + timedelta(days=1))
        feed = PuzzleFeed()
        self.assertEqual(feed.items().count(), 2)
        self.assertEqual(feed.items()[0].number, 1)
        self.assertEqual(feed.items()[1].number, 0)

    def test_limited_number(self):
        num_puzzles = 10
        limit = 5
        for i in range(num_puzzles):
            self.create_puzzle(i, timezone.now() - timedelta(days=num_puzzles - i))
        feed = PuzzleFeed()
        self.assertEqual(feed.items().count(), limit)

class GridCreationTests(TestCase):
    def test_create_grid_pattern(self):
        grid = create_grid(create_small_puzzle(), 3)
        for row in range(3):
            for col in range(3):
                self.assertEqual(grid[row][col]['row'], row)
                self.assertEqual(grid[row][col]['col'], col)
                if row == 1 and col == 1:
                    self.assertNotIn('light', grid[row][col]['type'])
                    self.assertIn('block', grid[row][col]['type'])
                else:
                    self.assertIn('light', grid[row][col]['type'])
                    self.assertNotIn('block', grid[row][col]['type'])

    def test_create_grid_clue_numbers(self):
        grid = create_grid(create_small_puzzle(), 3)
        expected_numbers = [[1, None, 2], [None, None, None], [3, None, None]]
        for row in range(3):
            for col in range(3):
                self.assertEqual(grid[row][col]['number'], expected_numbers[row][col])

    def test_create_grid_letters(self):
        grid = create_grid(create_small_puzzle(), 3)
        expected_letters = [['A', 'B', 'C'], ['M', None, 'N'], ['X', 'Y', 'Z']]
        for row in range(3):
            for col in range(3):
                self.assertEqual(grid[row][col]['letter'], expected_letters[row][col])

    def test_create_grid_borders(self):
        grid = create_grid(create_small_puzzle(), 3)
        for row in range(3):
            for col in range(3):
                if row == 0:
                    self.assertIn('topmost', grid[row][col]['type'])
                else:
                    self.assertNotIn('topmost', grid[row][col]['type'])
                if col == 0:
                    self.assertIn('leftmost', grid[row][col]['type'])
                else:
                    self.assertNotIn('leftmost', grid[row][col]['type'])

class ClueCreationTests(TestCase):
    def test_get_clues(self):
        p = create_small_puzzle()
        grid = create_grid(p, 3)

        across_clues = get_clues(p, grid, False)
        self.assertEquals(len(across_clues), 2)
        self.assertEquals(across_clues[0]['number'], 1)
        self.assertEquals(across_clues[0]['clue'], '1a')
        self.assertEquals(across_clues[0]['numeration'], '2,1')
        self.assertEquals(across_clues[1]['number'], 3)
        self.assertEquals(across_clues[1]['clue'], '3a')
        self.assertEquals(across_clues[1]['numeration'], '1-2')

        down_clues = get_clues(p, grid, True)
        self.assertEquals(len(down_clues), 2)
        self.assertEquals(down_clues[0]['number'], 1)
        self.assertEquals(down_clues[0]['clue'], '1d')
        self.assertEquals(down_clues[0]['numeration'], '3')
        self.assertEquals(down_clues[1]['number'], 2)
        self.assertEquals(down_clues[1]['clue'], '2d')
        self.assertEquals(down_clues[1]['numeration'], '3')

class DateFormattingTests(TestCase):
    def test_get_date_string(self):
        dt = datetime(1980, 3, 4, 1, 2, 3, tzinfo=timezone.get_default_timezone())
        a = Author.objects.create(name='Test Author')
        p = Puzzle.objects.create(pub_date=dt)
        self.assertEquals(get_date_string(p), '04 Mar 1980')

class PuzzleViewTests(TestCase):
    def test_home_page_grid_squares(self):
        create_puzzle_range()
        response = self.client.get('/')
        self.assertContains(response, 'class="puzzle"')
        self.assertContains(response, 'id="grid"')
        self.assertEqual(response.content.count('class="light'), 8)
        self.assertEqual(response.content.count('class="block'), (15 * 15) - 8)
        self.assertEqual(response.content.count('topmost'), 15)
        self.assertEqual(response.content.count('leftmost'), 15)
        self.assertEqual(response.content.count('class="grid-number"'), 3)
        self.assertEqual(response.content.count('data-a='), 8)
        for i in range(3):
            self.assertEqual(response.content.count('data-x="%i"' % i), 15)
            self.assertEqual(response.content.count('data-y="%i"' % i), 15)

    def test_home_page_clues(self):
        create_puzzle_range()
        response = self.client.get('/')
        self.assertContains(response, 'clue-box')
        self.assertEqual(response.content.count('clue-number'), 4)

    def test_home_page_wrapping(self):
        create_puzzle_range()
        response = self.client.get('/')
        self.assertContains(response, '<title>Three Pins - A cryptic crossword outlet</title>')
        self.assertContains(response, '<meta name="description" content="A free interactive site')
        self.assertContains(response, '< Previous')
        self.assertNotContains(response, 'Next >')
        self.assertContains(response, reverse('solution', args=[2]))

    def test_home_page_contains_latest(self):
        create_puzzle_range()
        response = self.client.get('/')
        self.assertNotContains(response, 'data-number="1"')
        self.assertContains(response, 'data-number="2"')
        self.assertNotContains(response, 'data-number="3"')

    def test_home_page_answers_hidden(self):
        create_puzzle_range()
        response = self.client.get('/')
        self.assertNotContains(response, 'class="letter"')

    def test_puzzle_without_previous(self):
        create_puzzle_range()
        response = self.client.get(reverse('puzzle', args=[0]))
        self.assertContains(response, 'class="puzzle"')
        self.assertContains(response, 'id="grid"')
        self.assertContains(response, 'data-number="0"')
        self.assertNotContains(response, '< Previous')
        self.assertContains(response, 'Next >')
        self.assertContains(response, '<title>Three Pins - Crossword #0</title>')

    def test_puzzle_with_next_and_previous(self):
        create_puzzle_range()
        response = self.client.get(reverse('puzzle', args=[1]))
        self.assertContains(response, 'class="puzzle"')
        self.assertContains(response, 'id="grid"')
        self.assertContains(response, 'data-number="1"')
        self.assertContains(response, '< Previous')
        self.assertContains(response, 'Next >')
        self.assertContains(response, '<title>Three Pins - Crossword #1</title>')

    def test_future_puzzle_not_accessible(self):
        create_puzzle_range()
        response = self.client.get(reverse('puzzle', args=[3]))
        self.assertEqual(response.status_code, 404)

    def test_preview_future_puzzle(self):
        create_puzzle_range()
        response = self.client.get(reverse('preview', args=[3]))
        self.assertContains(response, 'class="puzzle"')
        self.assertContains(response, 'id="grid"')
        self.assertContains(response, 'data-number="3"')
        self.assertNotContains(response, 'class="letter"')
        self.assertContains(response, '<title>Three Pins - Preview #3</title>')

    def test_solution_available(self):
        create_puzzle_range()
        response = self.client.get(reverse('solution', args=[2]))
        self.assertContains(response, 'class="puzzle"')
        self.assertContains(response, 'id="grid"')
        self.assertContains(response, 'data-number="2"')
        self.assertEqual(response.content.count('class="letter"'), 8)
        self.assertContains(response, '<title>Three Pins - Solution #2</title>')

    def test_preview_solution_available(self):
        create_puzzle_range()
        response = self.client.get(reverse('preview_solution', args=[3]))
        self.assertContains(response, 'class="puzzle"')
        self.assertContains(response, 'id="grid"')
        self.assertContains(response, 'data-number="3"')
        self.assertEqual(response.content.count('class="letter"'), 8)
        self.assertContains(response, '<title>Three Pins - Solution #3</title>')

    def test_invalid_puzzle(self):
        create_puzzle_range()
        response = self.client.get(reverse('puzzle', args=[100]))
        self.assertEqual(response.status_code, 404)

    def test_invalid_solution(self):
        create_puzzle_range()
        response = self.client.get(reverse('solution', args=[100]))
        self.assertEqual(response.status_code, 404)

    def test_invalid_preview(self):
        create_puzzle_range()
        response = self.client.get(reverse('preview', args=[100]))
        self.assertEqual(response.status_code, 404)

    def test_invalid_preview_solution(self):
        create_puzzle_range()
        response = self.client.get(reverse('preview_solution', args=[100]))
        self.assertEqual(response.status_code, 404)

    def test_index_list(self):
        create_puzzle_range()
        response = self.client.get(reverse('index'))
        self.assertNotContains(response, reverse('puzzle', args=[4]))
        self.assertNotContains(response, reverse('puzzle', args=[3]))
        self.assertContains(response, reverse('puzzle', args=[2]))
        self.assertContains(response, reverse('puzzle', args=[1]))
        self.assertContains(response, reverse('puzzle', args=[0]))

    def test_empty_index_list(self):
        response = self.client.get(reverse('index'))
        self.assertEqual(response.status_code, 200)
<|endoftext|>


<commit_before>#!/usr/bin/env python

__author__ = 'mm'

import B593
import datetime
import time
from pymongo import MongoClient
import logging
from logging.handlers import RotatingFileHandler
import sys
import getopt
import os
from ConfigParser import SafeConfigParser
import twitter
from requests.packages import urllib3

class TrafficTweet():
    def __init__(self):
        # Kludge to silent warnings in python-twitter, not needed for Python versions > 2.7.9
        # See https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.InsecurePlatformWarning
        urllib3.disable_warnings()

        self.up = 0
        self.down = 0

        self.parser = SafeConfigParser()
        self.parser.read('traffic_collector.conf')

        self.consumer_key = self.parser.get('twitter', 'consumer_key')
        self.consumer_secret = self.parser.get('twitter', 'consumer_secret')
        self.access_key = self.parser.get('twitter', 'access_key')
        self.access_secret = self.parser.get('twitter', 'access_secret')
        self.api = twitter.Api(consumer_key=self.consumer_key,
                               consumer_secret=self.consumer_secret,
                               access_token_key=self.access_key,
                               access_token_secret=self.access_secret)

    @staticmethod
    def _scale(val):
        if val >= 1024**2:
            return val/1024**2, "(%.1f GB)"
        elif val >= 1024:
            return val/1024, "(%.1f MB)"
        else:
            val = 0 if val < 0 else val  # Avoid negative values
            return val, "(%.0f kB)"

    def update(self, up, down):
        self.parser.read('traffic_collector.conf')
        tweet_limit = self.parser.getint('twitter', 'limit')  # Limit in MB

        # up and down in kB
        up = float(up)
        down = float(down)

        do_tweet = True if ((up + down) - (self.up + self.down)) > (tweet_limit * 1024) else False

        diff_up = up - self.up
        diff_down = down - self.down
        diff_sum = diff_up + diff_down

        diff_up, diff_up_str = self._scale(diff_up)
        diff_down, diff_down_str = self._scale(diff_down)
        diff_sum, diff_sum_str = self._scale(diff_sum)

        # Avoid tweet if first time (self.up/down == 0)
        # Avoid tweet to be duplicate of previous => error by including date string
        if (self.up != 0 and self.down != 0) and do_tweet:
            status = self.api.PostUpdate(("%s @ecsmame #B593 Traffic: uplink " + diff_up_str +
                                          " downlink " + diff_down_str + " total " + diff_sum_str) %
                                         (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                                          diff_up, diff_down, diff_sum))
        else:
            status = None

        self.up = up
        self.down = down
        return status

    def daily_update(self, t, up, down):
        # Provide a daily summary of volume
        # Avoid tweet to be duplicate of previous => error by including date string

        status = None
        dt = datetime.datetime.fromtimestamp(int(t/1000)).timetuple()  # To time_struct
        if dt.tm_hour == 1:  # any time during the first hour of the day
            up = float(up)
            down = float(down)
            start_time = t - (24*60*60*1000)  # 24 hours before t
            client = MongoClient()
            db = client.Router
            coll = db.B593
            post = coll.find_one({"Time": {"$gte": start_time}})

            diff_up = up - post['UplinkVolume']
            diff_down = down - post['DownlinkVolume']
            diff_sum = diff_up + diff_down

            diff_up, diff_up_str = self._scale(diff_up)
            diff_down, diff_down_str = self._scale(diff_down)
            diff_sum, diff_sum_str = self._scale(diff_sum)

            # Calculate start_time as first time of month
            start_time = (datetime.datetime(dt.tm_year, dt.tm_mon, day=1) -
                          datetime.datetime(1970, 1, 1)).total_seconds()*1000
            post = coll.find_one({"Time": {"$gte": start_time}})

            if post is None:  # Might be none first day of month
                if not quiet:
                    app_log.warning('Null post at monthly tweet')
                return  # Might be none first day of month

            diff_month_up = up - post['UplinkVolume']
            diff_month_down = down - post['DownlinkVolume']
            diff_month_sum = diff_month_up + diff_month_down
            diff_month_left = (40*1024**2) - diff_month_sum

            diff_month_up, diff_month_up_str = self._scale(diff_month_up)
            diff_month_down, diff_month_down_str = self._scale(diff_month_down)
            diff_month_sum, diff_month_sum_str = self._scale(diff_month_sum)
            diff_month_left, diff_month_left_str = self._scale(diff_month_left)

            status = self.api.PostUpdate(("Daily %s @ecsmame #B593 up " + diff_up_str +
                                          " down " + diff_down_str + " total " + diff_sum_str) %
                                         (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                                          diff_up, diff_down, diff_sum))

            status = self.api.PostUpdate(("Monthly %s @ecsmame #B593 up " +
                                          diff_month_up_str + " down " + diff_month_down_str + " total " +
                                          diff_month_sum_str + " left " + diff_month_left_str) %
                                         (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                                          diff_month_up, diff_month_down, diff_month_sum, diff_month_left))

        else:
            status = None

        return status

def router_handler(data, sid):
    # Milliseconds since 1970-01-01 in local time (not UTC)
    data['time'] = time.mktime(datetime.datetime.now().timetuple()) * 1000
    data['session id'] = sid

    try:
        client = MongoClient()
        db = client.Router       # The name of the Mongo DB is 'Router'
        coll = db.B593           # The collection is named 'B593'
        result = coll.insert_one(
            {
                "Time":           data['time'],
                "UplinkVolume":   data['uplink'],
                "DownlinkVolume": data['downlink'],
                "UplinkRate":     data['uplink rate'],
                "DownlinkRate":   data['downlink rate'],
                "IPAddress":      data['IP address']
            }
        )
        if not quiet:
            app_log.info('Inserted %s', str(data))

    except:
        if not quiet:
            app_log.warning('Mongo exception: %s', sys.exc_info()[0])
        pass

    if tweet:
        status = tw.update(data['uplink'], data['downlink'])
        if not quiet and status is not None:
            app_log.info(status.text)

        status = tw.daily_update(data['time'], data['uplink'], data['downlink'])
        if not quiet and status is not None:
            app_log.info(status.text)

if __name__ == '__main__':
    interval = 60*60  # 1 hour
    logFile = '/var/log/traffic.log'
    quiet = False
    helpstr = 'collector.py -l <logfile> -r <seconds> -t -q'
    tweet = True

    # Change working directory to where the script is, needed for B593 to read rt.pem when running as a daemon
    # See http://stackoverflow.com/questions/595305/python-path-of-script
    pathname = os.path.dirname(sys.argv[0])
    os.chdir(os.path.abspath(pathname))

    try:
        opts, args = getopt.getopt(sys.argv[1:], "hl:r:tq", ["logfile=", "repeat=", "notweet", "quiet"])
    except getopt.GetoptError:
        print helpstr
        sys.exit(2)
    for opt, arg in opts:
        if opt == '-h':
            print helpstr
            sys.exit()
        elif opt in ("-l", "--logfile"):
            logFile = arg
        elif opt in ("-r", "--repeat"):
            interval = int(arg)
        elif opt in ("-t", "--notweet"):
            tweet = False
        elif opt in ("-q", "--quiet"):
            quiet = True

    if not quiet:
        # Setup logging with max 5kB and 2 backups
        log_formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')

        my_handler = RotatingFileHandler(logFile, mode='a', maxBytes=5 * 1024 * 1024, backupCount=2)
        my_handler.setFormatter(log_formatter)
        my_handler.setLevel(logging.INFO)

        app_log = logging.getLogger('Collector')
        app_log.setLevel(logging.DEBUG)

        app_log.addHandler(my_handler)
        app_log.info("Start")

    if tweet:
        tw = TrafficTweet()

    B593.Router(interval, router_handler)
<commit_msg>Correct daily tweet when over 40 GB limit<commit_after>#!/usr/bin/env python

__author__ = 'mm'

import B593
import datetime
import time
from pymongo import MongoClient
import logging
from logging.handlers import RotatingFileHandler
import sys
import getopt
import os
from ConfigParser import SafeConfigParser
import twitter
from requests.packages import urllib3

class TrafficTweet():
    def __init__(self):
        # Kludge to silent warnings in python-twitter, not needed for Python versions > 2.7.9
        # See https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.InsecurePlatformWarning
        urllib3.disable_warnings()

        self.up = 0
        self.down = 0

        self.parser = SafeConfigParser()
        self.parser.read('traffic_collector.conf')

        self.consumer_key = self.parser.get('twitter', 'consumer_key')
        self.consumer_secret = self.parser.get('twitter', 'consumer_secret')
        self.access_key = self.parser.get('twitter', 'access_key')
        self.access_secret = self.parser.get('twitter', 'access_secret')
        self.api = twitter.Api(consumer_key=self.consumer_key,
                               consumer_secret=self.consumer_secret,
                               access_token_key=self.access_key,
                               access_token_secret=self.access_secret)

    @staticmethod
    def _scale(val):
        if val >= 1024**2:
            return val/1024**2, "(%.1f GB)"
        elif val >= 1024:
            return val/1024, "(%.1f MB)"
        else:
            val = 0 if val < 0 else val  # Avoid negative values
            return val, "(%.0f kB)"

    def update(self, up, down):
        self.parser.read('traffic_collector.conf')
        tweet_limit = self.parser.getint('twitter', 'limit')  # Limit in MB

        # up and down in kB
        up = float(up)
        down = float(down)

        do_tweet = True if ((up + down) - (self.up + self.down)) > (tweet_limit * 1024) else False

        diff_up = up - self.up
        diff_down = down - self.down
        diff_sum = diff_up + diff_down

        diff_up, diff_up_str = self._scale(diff_up)
        diff_down, diff_down_str = self._scale(diff_down)
        diff_sum, diff_sum_str = self._scale(diff_sum)

        # Avoid tweet if first time (self.up/down == 0)
        # Avoid tweet to be duplicate of previous => error by including date string
        if (self.up != 0 and self.down != 0) and do_tweet:
            status = self.api.PostUpdate(("%s @ecsmame #B593 Traffic: uplink " + diff_up_str +
                                          " downlink " + diff_down_str + " total " + diff_sum_str) %
                                         (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                                          diff_up, diff_down, diff_sum))
        else:
            status = None

        self.up = up
        self.down = down
        return status

    def daily_update(self, t, up, down):
        # Provide a daily summary of volume
        # Avoid tweet to be duplicate of previous => error by including date string

        status = None
        dt = datetime.datetime.fromtimestamp(int(t/1000)).timetuple()  # To time_struct
        if dt.tm_hour == 1:  # any time during the first hour of the day
            up = float(up)
            down = float(down)
            start_time = t - (24*60*60*1000)  # 24 hours before t
            client = MongoClient()
            db = client.Router
            coll = db.B593
            post = coll.find_one({"Time": {"$gte": start_time}})

            diff_up = up - post['UplinkVolume']
            diff_down = down - post['DownlinkVolume']
            diff_sum = diff_up + diff_down

            diff_up, diff_up_str = self._scale(diff_up)
            diff_down, diff_down_str = self._scale(diff_down)
            diff_sum, diff_sum_str = self._scale(diff_sum)

            # Calculate start_time as first time of month
            start_time = (datetime.datetime(dt.tm_year, dt.tm_mon, day=1) -
                          datetime.datetime(1970, 1, 1)).total_seconds()*1000
            post = coll.find_one({"Time": {"$gte": start_time}})

            if post is None:  # First day of month, current value not synched to DB
                if not quiet:
                    app_log.warning('Null post at monthly tweet')
                diff_month_up = 0
                diff_month_down = 0
            else:
                diff_month_up = up - post['UplinkVolume']
                diff_month_down = down - post['DownlinkVolume']

            diff_month_sum = diff_month_up + diff_month_down
            diff_month_left = (40*1024**2) - diff_month_sum

            diff_month_up, diff_month_up_str = self._scale(diff_month_up)
            diff_month_down, diff_month_down_str = self._scale(diff_month_down)
            diff_month_sum, diff_month_sum_str = self._scale(diff_month_sum)
            diff_month_left, diff_month_left_str = self._scale(diff_month_left)

            status = self.api.PostUpdate(("Daily %s @ecsmame #B593 up " + diff_up_str +
                                          " down " + diff_down_str + " total " + diff_sum_str) %
                                         (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                                          diff_up, diff_down, diff_sum))

            status = self.api.PostUpdate(("Monthly %s @ecsmame #B593 up " +
                                          diff_month_up_str + " down " + diff_month_down_str + " total " +
                                          diff_month_sum_str + " left " + diff_month_left_str) %
                                         (datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                                          diff_month_up, diff_month_down, diff_month_sum, diff_month_left))

        else:
            status = None

        return status

def router_handler(data, sid):
    # Milliseconds since 1970-01-01 in local time (not UTC)
    data['time'] = time.mktime(datetime.datetime.now().timetuple()) * 1000
    data['session id'] = sid

    try:
        client = MongoClient()
        db = client.Router       # The name of the Mongo DB is 'Router'
        coll = db.B593           # The collection is named 'B593'
        result = coll.insert_one(
            {
                "Time":           data['time'],
                "UplinkVolume":   data['uplink'],
                "DownlinkVolume": data['downlink'],
                "UplinkRate":     data['uplink rate'],
                "DownlinkRate":   data['downlink rate'],
                "IPAddress":      data['IP address']
            }
        )
        if not quiet:
            app_log.info('Inserted %s', str(data))

    except:
        if not quiet:
            app_log.warning('Mongo exception: %s', sys.exc_info()[0])
        pass

    if tweet:
        status = tw.update(data['uplink'], data['downlink'])
        if not quiet and status is not None:
            app_log.info(status.text)

        status = tw.daily_update(data['time'], data['uplink'], data['downlink'])
        if not quiet and status is not None:
            app_log.info(status.text)

if __name__ == '__main__':
    interval = 60*60  # 1 hour
    logFile = '/var/log/traffic.log'
    quiet = False
    helpstr = 'collector.py -l <logfile> -r <seconds> -t -q'
    tweet = True

    # Change working directory to where the script is, needed for B593 to read rt.pem when running as a daemon
    # See http://stackoverflow.com/questions/595305/python-path-of-script
    pathname = os.path.dirname(sys.argv[0])
    os.chdir(os.path.abspath(pathname))

    try:
        opts, args = getopt.getopt(sys.argv[1:], "hl:r:tq", ["logfile=", "repeat=", "notweet", "quiet"])
    except getopt.GetoptError:
        print helpstr
        sys.exit(2)
    for opt, arg in opts:
        if opt == '-h':
            print helpstr
            sys.exit()
        elif opt in ("-l", "--logfile"):
            logFile = arg
        elif opt in ("-r", "--repeat"):
            interval = int(arg)
        elif opt in ("-t", "--notweet"):
            tweet = False
        elif opt in ("-q", "--quiet"):
            quiet = True

    if not quiet:
        # Setup logging with max 5kB and 2 backups
        log_formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')

        my_handler = RotatingFileHandler(logFile, mode='a', maxBytes=5 * 1024 * 1024, backupCount=2)
        my_handler.setFormatter(log_formatter)
        my_handler.setLevel(logging.INFO)

        app_log = logging.getLogger('Collector')
        app_log.setLevel(logging.DEBUG)

        app_log.addHandler(my_handler)
        app_log.info("Start")

    if tweet:
        tw = TrafficTweet()

    B593.Router(interval, router_handler)
<|endoftext|>