Beispiel #1
0
class ExtendedAgencyMembership(AgencyMembership, HiddenFromPublicExtension):
    """ An extended version of the standard membership from onegov.people. """

    __mapper_args__ = {'polymorphic_identity': 'extended'}

    es_type_name = 'extended_membership'

    @property
    def es_public(self):
        if self.agency:
            if getattr(self.agency, 'is_hidden_from_public', False):
                return False
        if self.person:
            if getattr(self.person, 'is_hidden_from_public', False):
                return False
        return not self.is_hidden_from_public

    #: The prefix character.
    prefix = meta_property()

    #: A note to the membership.
    note = meta_property()

    #: An addition to the membership.
    addition = meta_property()
Beispiel #2
0
class GazetteNoticeChange(Message, CachedUserNameMixin):
    """ A changelog entry for an official notice. """

    __mapper_args__ = {'polymorphic_identity': 'gazette_notice'}

    #: the user which made this change
    user = relationship(
        User,
        primaryjoin=(
            'foreign(GazetteNoticeChange.owner) == cast(User.id, TEXT)'),
        backref=backref('changes', lazy='select'))

    @observes('user', 'user.realname', 'user.username')
    def user_observer(self, user, realname, username):
        if hasattr(self, '_user_observer'):
            self._user_observer(user, realname, username)

    #: the notice which this change belongs to
    notice = relationship(
        GazetteNotice,
        primaryjoin=('foreign(GazetteNoticeChange.channel_id)'
                     '== cast(GazetteNotice.id, TEXT)'),
        backref=backref('changes',
                        lazy='dynamic',
                        cascade='all,delete-orphan',
                        order_by='desc(GazetteNoticeChange.id)'))

    #: the event
    event = meta_property('event')
Beispiel #3
0
class Municipality(UserGroup, TimestampMixin):
    """ A municipality / user group. """

    __mapper_args__ = {'polymorphic_identity': 'wtfs'}

    #: The name of the municipality.
    bfs_number = meta_property()

    #: The address supplement, used for invoices.
    address_supplement = meta_property()

    #: The GPN number, used for invoices.
    gpn_number = meta_property()

    #: The payment type. Typically normal (7.00) or special (8.50).
    payment_type = meta_property('payment_type')

    @property
    def price_per_quantity(self):
        if self.payment_type:
            query = object_session(self).query(PaymentType)
            query = query.filter_by(name=self.payment_type)
            payment_type = query.first()
            if payment_type:
                return payment_type.price_per_quantity or 0

        return 0

    @property
    def has_data(self):
        if self.pickup_dates.first() or self.scan_jobs.first():
            return True
        return False

    @property
    def contacts(self):
        return [
            user.username for user in self.users
            if (user.data or {}).get('contact', False)
        ]
Beispiel #4
0
class Organization(AdjacencyList, ContentMixin, TimestampMixin):

    """ Defines an organization for official notices.

    Although the categories are defined as a flexible adjacency list, we
    currently use it only as a two-stage adjacency list key-value list
    (name-title).

    """

    __tablename__ = 'gazette_organizations'

    #: True, if this organization is still in use.
    active = Column(Boolean, nullable=True)

    external_name = meta_property('external_name')

    def notices(self):
        """ Returns a query to get all notices related to this category. """

        from onegov.gazette.models.notice import GazetteNotice  # circular

        notices = object_session(self).query(GazetteNotice)
        notices = notices.filter(
            GazetteNotice._organizations.has_key(self.name)  # noqa
        )

        return notices

    @property
    def in_use(self):
        """ True, if the organization is used by any notice. """

        if self.notices().first():
            return True

        return False

    @observes('title')
    def title_observer(self, title):
        from onegov.gazette.models.notice import GazetteNotice  # circular

        notices = self.notices()
        notices = notices.filter(
            or_(
                GazetteNotice.organization.is_(None),
                GazetteNotice.organization != title
            )
        )
        for notice in notices:
            notice.organization = title
Beispiel #5
0
class Notification(Message):
    """ A changelog entry for an official notice. """

    __mapper_args__ = {'polymorphic_identity': 'wtfs_notification'}

    title = meta_property('title')

    @classmethod
    def create(cls, request, title='', text=''):

        return cls.bound_messages(request.session).add(
            channel_id=request.identity.application_id,
            owner=request.identity.userid,
            text=text,
            meta={'title': title})
Beispiel #6
0
class CachedGroupNameMixin(object):
    """ Mixin providing a cached version of the group name. There needs to be:
    - a ``group`` relationship (which has no dynamic backref)
    - a meta column

    The observer needs to be registered in the children:

    @observes('group', 'group.name')
    def group_observer(self, group, name):
        if hasattr(self, '_group_observerr'):
            self._group_observerr(user, realname, username)

    """

    #: The name of the group in case the owner and its group get deleted.
    _group_name = meta_property('group_name')

    @property
    def group_name(self):
        """ Returns the name of the group this notice belongs to.

        If the group has been deleted, the last known name in brackets is
        returned.
        """

        if self.group:
            return self.group.name
        return '({})'.format(self._group_name) if self._group_name else None

    def _group_observer(self, group, name):
        """ Upates the last known name of the group.

        This never deletes the stored name, set ``self._group_name`` yourself
        if you want to clear it.

        """

        group_name = group.name if group else None
        group_name = group_name or name or self._group_name
        self._group_name = group_name
Beispiel #7
0
class CachedUserNameMixin(object):
    """ Mixin providing a cached version of the user name. There needs to be:
    - a ``user`` relationship (which has no dynamic backref)
    - a meta column

    The observer needs to be registered in the children:

    @observes('user', 'user.realname', 'user.username')
    def user_observer(self, user, realname, username):
        if hasattr(self, '_user_observer'):
            self._user_observer(user, realname, username)

    """

    #: The name of the user in case he gets deleted.
    _user_name = meta_property('user_name')

    @property
    def user_name(self):
        """ Returns the name of the owner.

        If the user has been deleted, the last known name in brackets is
        returned.
        """
        if self.user:
            return self.user.realname or self.user.username
        return '({})'.format(self._user_name) if self._user_name else None

    def _user_observer(self, user, realname, username):
        """ Upates the last known name of the owner.

        This never deletes the stored name, set ``self._user_name`` yourself
        if you want to clear it.

        """
        user_name = user.realname or user.username if user else None
        user_name = user_name or realname or username or self._user_name
        self._user_name = user_name
Beispiel #8
0
class StripeConnect(PaymentProvider):

    __mapper_args__ = {'polymorphic_identity': 'stripe_connect'}

    fee_policy = StripeFeePolicy

    #: The Stripe Connect client id
    client_id = meta_property()

    #: The API key of the connect user
    client_secret = meta_property()

    #: The oauth_redirect gateway in use (see seantis/oauth_redirect on github)
    oauth_gateway = meta_property()

    #: The auth code required by oauth_redirect
    oauth_gateway_auth = meta_property()

    #: The oauth_redirect secret that should be used
    oauth_gateway_secret = meta_property()

    #: The authorization code provided by OAuth
    authorization_code = meta_property()

    #: The public stripe key
    publishable_key = meta_property()

    #: The stripe user id as confirmed by OAuth
    user_id = meta_property()

    #: The refresh token provided by OAuth
    refresh_token = meta_property()

    #: The access token provieded by OAuth
    access_token = meta_property()

    #: The id of the latest processed balance transaction
    latest_payout = meta_property()

    #: Should the fee be charged to the customer or not?
    charge_fee_to_customer = meta_property()

    def adjust_price(self, price):
        if price and self.charge_fee_to_customer:
            new_price = self.fee_policy.compensate(price.amount)
            new_fee = self.fee_policy.from_amount(new_price)

            return Price(new_price, price.currency, new_fee)

        return price

    @property
    def livemode(self):
        return not self.access_token.startswith('sk_test')

    @property
    def payment_class(self):
        return StripePayment

    @property
    def title(self):
        return 'Stripe Connect'

    @property
    def url(self):
        return 'https://dashboard.stripe.com/'

    @property
    def public_identity(self):
        account = self.account
        return ' / '.join((account.business_name, account.email))

    @property
    def identity(self):
        return self.user_id

    @cached_property
    def account(self):
        with stripe_api_key(self.access_token):
            return stripe.Account.retrieve(id=self.user_id)

    @property
    def connected(self):
        return self.account and True or False

    def charge(self, amount, currency, token):
        session = object_session(self)

        payment = self.payment(
            id=uuid5(STRIPE_NAMESPACE, token),
            amount=amount,
            currency=currency,
            state='open'
        )

        with stripe_api_key(self.access_token):
            charge = stripe.Charge.create(
                amount=round(amount * 100, 0),
                currency=currency,
                source=token,
                capture=False,
                idempotency_key=token,
                metadata={
                    'payment_id': payment.id.hex
                }
            )

        StripeCaptureManager.capture_charge(self.access_token, charge.id)
        payment.remote_id = charge.id

        # we do *not* want to lose this information, so even though the
        # caller should make sure the payment is stored, we make sure
        session.add(payment)

        return payment

    def checkout_button(self, label, amount, currency, action='submit',
                        **extra):
        """ Generates the html for the checkout button. """

        extra['amount'] = round(amount * 100, 0)
        extra['currency'] = currency
        extra['key'] = self.publishable_key

        attrs = {
            'data-stripe-{}'.format(key): str(value)
            for key, value in extra.items()
        }
        attrs['data-action'] = action

        return """
            <input type="hidden" name="payment_token" id="{target}">
            <button class="checkout-button stripe-connect"
                    data-target-id="{target}"
                    {attrs}>{label}</button>
        """.format(
            label=escape(label),
            attrs=' '.join(
                '{}="{}"'.format(escape(k), escape(v))
                for k, v in attrs.items()
            ),
            target=uuid4().hex
        )

    def oauth_url(self, redirect_uri, state=None, user_fields=None):
        """ Generates an oauth url to be shown in the browser. """

        return stripe.OAuth.authorize_url(
            client_id=self.client_id,
            client_secret=self.client_secret,
            scope='read_write',
            redirect_uri=redirect_uri,
            stripe_user=user_fields,
            state=state
        )

    def prepare_oauth_request(self, redirect_uri, success_url, error_url,
                              user_fields=None):
        """ Registers the oauth request with the oauth_gateway and returns
        an url that is ready to be used for the complete oauth request.

        """
        register = '{}/register/{}'.format(
            self.oauth_gateway,
            self.oauth_gateway_auth)

        assert self.oauth_gateway \
            and self.oauth_gateway_auth \
            and self.oauth_gateway_secret

        payload = {
            'url': redirect_uri,
            'secret': self.oauth_gateway_secret,
            'method': 'GET',
            'success_url': success_url,
            'error_url': error_url
        }

        response = requests.post(register, json=payload)
        assert response.status_code == 200

        return self.oauth_url(
            redirect_uri='{}/redirect'.format(self.oauth_gateway),
            state=response.json()['token'],
            user_fields=user_fields
        )

    def process_oauth_response(self, request_params):
        """ Takes the parameters of an incoming oauth request and stores
        them on the payment provider if successful.

        """

        if 'error' in request_params:
            raise RuntimeError("Stripe OAuth request failed ({}: {})".format(
                request_params['error'], request_params['error_description']
            ))

        assert request_params['oauth_redirect_secret'] \
            == self.oauth_gateway_secret

        self.authorization_code = request_params['code']

        with stripe_api_key(self.client_secret):
            data = stripe.OAuth.token(
                grant_type='authorization_code',
                code=self.authorization_code,
            )

        assert data['scope'] == 'read_write'

        self.publishable_key = data['stripe_publishable_key']
        self.user_id = data['stripe_user_id']
        self.refresh_token = data['refresh_token']
        self.access_token = data['access_token']

    def sync(self):
        session = object_session(self)
        self.sync_payment_states(session)
        self.sync_payouts(session)

    def sync_payment_states(self, session):

        def payments(ids):
            q = session.query(self.payment_class)
            q = q.filter(self.payment_class.id.in_(ids))

            return q

        charges = self.paged(
            stripe.Charge.list,
            limit=50,
            include=lambda r: 'payment_id' in r.metadata
        )

        by_payment = {}

        for charge in charges:
            by_payment[charge.metadata['payment_id']] = charge

        for payment in payments(by_payment.keys()):
            payment.sync(remote_obj=by_payment[payment.id.hex])

    def sync_payouts(self, session):

        payouts = self.paged(stripe.Payout.list, limit=50, status='paid')
        latest_payout = None

        paid_charges = {}

        for payout in payouts:
            if latest_payout is None:
                latest_payout = payout

            if payout.id == self.latest_payout:
                break

            transactions = self.paged(
                stripe.BalanceTransaction.list,
                limit=50,
                payout=payout.id,
                type='charge'
            )

            for charge in transactions:
                paid_charges[charge.source] = (
                    datetime.fromtimestamp(payout.arrival_date),
                    payout.id,
                    charge.fee / 100
                )

        if paid_charges:
            q = session.query(self.payment_class)
            q = q.filter(self.payment_class.remote_id.in_(paid_charges.keys()))

            for p in q:
                p.payout_date, p.payout_id, p.effective_fee\
                    = paid_charges[p.remote_id]

        self.latest_payout = latest_payout and latest_payout.id

    def paged(self, method, include=lambda record: True, **kwargs):
        with stripe_api_key(self.access_token):
            records = method(**kwargs)
            records = (r for r in records.auto_paging_iter())
            records = (r for r in records if include(r))

            yield from records
Beispiel #9
0
class StripePayment(Payment):
    __mapper_args__ = {'polymorphic_identity': 'stripe_connect'}

    fee_policy = StripeFeePolicy

    #: the date of the payout
    payout_date = meta_property()

    #: the id of the payout
    payout_id = meta_property()

    #: the fee deducted by stripe
    effective_fee = meta_property()

    @property
    def fee(self):
        """ The calculated fee or the effective fee if available.

        The effective fee is taken from the payout records. In practice
        these values should always be the same.

        """

        if self.effective_fee:
            return Decimal(self.effective_fee)

        return Decimal(self.fee_policy.from_amount(self.amount))

    @property
    def remote_url(self):
        if self.provider.livemode:
            base = 'https://dashboard.stripe.com/payments/{}'
        else:
            base = 'https://dashboard.stripe.com/test/payments/{}'

        return base.format(self.remote_id)

    @property
    def charge(self):
        with stripe_api_key(self.provider.access_token):
            return stripe.Charge.retrieve(self.remote_id)

    def refund(self):
        with stripe_api_key(self.provider.access_token):
            refund = stripe.Refund.create(charge=self.remote_id)
            self.state = 'cancelled'
            return refund

    def sync(self, remote_obj=None):
        charge = remote_obj or self.charge

        if not charge.captured:
            self.state = 'open'

        elif charge.refunded:
            self.state = 'cancelled'

        elif charge.status == 'failed':
            self.state = 'failed'

        elif charge.captured and charge.paid:
            self.state = 'paid'
Beispiel #10
0
class GazetteNotice(OfficialNotice, CachedUserNameMixin, CachedGroupNameMixin,
                    AssociatedFiles):
    """ An official notice with extras.

    We use a combination of the categories/organizations HSTORE and the
    individual category/organization columns. The ID of the category/
    organization is stored in the HSTORE column and the actual name ist copied
    when calling ``apply_meta``.

    We store only the issue names (year-number) in the HSTORE.

    It's possible to add a changelog entry by calling ``add_change``. Changelog
    entries are created for state changes by default.

    The user name accessible by ``user_name`` gets cached in case the user is
    deleted.

    The group name accessible by ``group_name`` gets cached in case the group
    is deleted.

    """

    __mapper_args__ = {'polymorphic_identity': 'gazette'}

    #: True, if the official notice only appears in the print version
    print_only = meta_property('print_only')

    #: True, if the official notice needs to be paid for
    at_cost = meta_property('at_cost')

    #: The billing address in case the official notice need to be paid for
    billing_address = content_property('billing_address')

    @observes('user', 'user.realname', 'user.username')
    def user_observer(self, user, realname, username):
        if hasattr(self, '_user_observer'):
            self._user_observer(user, realname, username)

    @observes('group', 'group.name')
    def group_observer(self, group, name):
        if hasattr(self, '_group_observer'):
            self._group_observer(group, name)

    def add_change(self, request, event, text=None):
        """ Adds en entry to the changelog. """

        session = object_session(self)
        try:
            username = request.identity.userid
            owner = str(UserCollection(session).by_username(username).id)
        except Exception:
            owner = None

        self.changes.append(
            GazetteNoticeChange(channel_id=str(self.id),
                                owner=owner,
                                text=text or '',
                                meta={'event': event}))

    def submit(self, request):
        """ Submit a drafted notice.

        This automatically adds en entry to the changelog.

        """

        super(GazetteNotice, self).submit()
        self.add_change(request, _("submitted"))

    def reject(self, request, comment):
        """ Reject a submitted notice.

        This automatically adds en entry to the changelog.

        """

        super(GazetteNotice, self).reject()
        self.add_change(request, _("rejected"), comment)

    def accept(self, request):
        """ Accept a submitted notice.

        This automatically adds en entry to the changelog.

        """

        super(GazetteNotice, self).accept()
        self.add_change(request, _("accepted"))

    def publish(self, request):
        """ Publish an accepted notice.

        This automatically adds en entry to the changelog.

        """

        super(GazetteNotice, self).publish()
        self.add_change(request, _("published"))

    @property
    def rejected_comment(self):
        """ Returns the comment of the last rejected change log entry. """

        for change in self.changes:
            if change.event == 'rejected':
                return change.text

        return ''

    @property
    def issues(self):
        """ Returns the issues sorted (by year/number). """

        issues = self._issues or {}
        keys = [IssueName.from_string(issue) for issue in (self._issues or {})]
        keys = sorted(keys, key=lambda x: (x.year, x.number))
        return OrderedDict((str(key), issues[str(key)]) for key in keys)

    @issues.setter
    def issues(self, value):
        if isinstance(value, dict):
            self._issues = value
        else:
            self._issues = {item: None for item in value}

    @property
    def issue_objects(self):
        if self._issues:
            query = object_session(self).query(Issue)
            query = query.filter(Issue.name.in_(self._issues.keys()))
            query = query.order_by(Issue.date)
            return query.all()
        return []

    def set_publication_number(self, issue, number):
        assert issue in self.issues
        issues = dict(self.issues)
        issues[issue] = str(number)
        self._issues = issues

    @property
    def category_id(self):
        """ The ID of the category. We store this the ID in the HSTORE (we use
        only one!) and additionaly store the title of the category in the
        category column.

        """
        keys = list(self.categories.keys())
        return keys[0] if keys else None

    @category_id.setter
    def category_id(self, value):
        self.categories = [value]

    @property
    def category_object(self):
        if self.category_id:
            query = object_session(self).query(Category)
            query = query.filter(Category.name == self.category_id)
            return query.first()

    @property
    def organization_id(self):
        """ The ID of the organization. We store this the ID in the HSTORE (we
        use only one!) and additionaly store the title of the organization in
        the organization column.

        """
        keys = list(self.organizations.keys())
        return keys[0] if keys else None

    @organization_id.setter
    def organization_id(self, value):
        self.organizations = [value]

    @property
    def organization_object(self):
        if self.organization_id:
            query = object_session(self).query(Organization)
            query = query.filter(Organization.name == self.organization_id)
            return query.first()

    @property
    def overdue_issues(self):
        """ Returns True, if any of the issue's deadline is reached. """

        if self._issues:
            query = object_session(self).query(Issue)
            query = query.filter(Issue.name.in_(self._issues.keys()))
            query = query.filter(Issue.deadline < utcnow())
            if query.first():
                return True

        return False

    @property
    def expired_issues(self):
        """ Returns True, if any of the issue's issue date is reached. """
        if self._issues:
            query = object_session(self).query(Issue)
            query = query.filter(Issue.name.in_(self._issues.keys()))
            query = query.filter(Issue.date <= date.today())
            if query.first():
                return True

        return False

    @property
    def invalid_category(self):
        """ Returns True, if the category of the is invalid or inactive. """
        query = object_session(self).query(Category.active)
        query = query.filter(Category.name == self.category_id).first()
        return (not query[0]) if query else True

    @property
    def invalid_organization(self):
        """ Returns True, if the category of the is invalid or inactive. """
        query = object_session(self).query(Organization.active)
        query = query.filter(Organization.name == self.organization_id).first()
        return (not query[0]) if query else True

    def apply_meta(self, session):
        """ Updates the category, organization and issue date from the meta
        values.

        """
        self.organization = None
        query = session.query(Organization.title)
        query = query.filter(Organization.name == self.organization_id).first()
        if query:
            self.organization = query[0]

        self.category = None
        query = session.query(Category.title)
        query = query.filter(Category.name == self.category_id).first()
        if query:
            self.category = query[0]

        self.first_issue = None
        if self._issues:
            query = session.query(Issue.date)
            query = query.filter(Issue.name.in_(self._issues.keys()))
            query = query.order_by(Issue.date).first()
            if query:
                self.first_issue = standardize_date(as_datetime(query[0]),
                                                    'UTC')
Beispiel #11
0
class ExtendedAgency(Agency, HiddenFromPublicExtension):
    """ An extended version of the standard agency from onegov.people. """

    __mapper_args__ = {'polymorphic_identity': 'extended'}

    es_type_name = 'extended_agency'

    @property
    def es_public(self):
        return not self.is_hidden_from_public

    #: Defines which fields of a membership and person should be exported to
    #: the PDF. The fields are expected to contain two parts seperated by a
    #: point. The first part is either `membership` or `person`, the second
    #: the name of the attribute (e.g. `membership.title`).
    export_fields = meta_property(default=list)

    #: The PDF for the agency and all its suborganizations.
    pdf = associated(AgencyPdf, 'pdf', 'one-to-one')

    trait = 'agency'

    @property
    def pdf_file(self):
        """ Returns the PDF content for the agency (and all its
        suborganizations).

        """

        if self.pdf:
            return self.pdf.reference.file

    @pdf_file.setter
    def pdf_file(self, value):
        """ Sets the PDF content for the agency (and all its
        suborganizations). Automatically sets a nice filename. Replaces only
        the reference, if possible.

        """

        filename = '{}.pdf'.format(normalize_for_url(self.title))
        if self.pdf:
            self.pdf.reference = as_fileintent(value, filename)
            self.pdf.name = filename
        else:
            pdf = AgencyPdf(id=random_token())
            pdf.reference = as_fileintent(value, filename)
            pdf.name = filename
            self.pdf = pdf

    @property
    def portrait_html(self):
        """ Returns the portrait as HTML. """

        return '<p>{}</p>'.format(linkify(self.portrait).replace('\n', '<br>'))

    def proxy(self):
        """ Returns a proxy object to this agency allowing alternative linking
        paths. """

        return AgencyProxy(self)

    def add_person(self, person_id, title, **kwargs):
        """ Appends a person to the agency with the given title. """

        order_within_agency = kwargs.pop('order_within_agency', 2**16)
        session = object_session(self)

        orders_for_person = session.query(
            ExtendedAgencyMembership.order_within_person).filter_by(
                person_id=person_id).all()

        orders_for_person = list(
            (o.order_within_person for o in orders_for_person))

        if orders_for_person:
            try:
                order_within_person = max(orders_for_person) + 1
            except ValueError:
                order_within_person = 0
            assert len(orders_for_person) == max(orders_for_person) + 1
        else:
            order_within_person = 0

        self.memberships.append(
            ExtendedAgencyMembership(person_id=person_id,
                                     title=title,
                                     order_within_agency=order_within_agency,
                                     order_within_person=order_within_person,
                                     **kwargs))

        for order, membership in enumerate(self.memberships):
            membership.order_within_agency = order