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()
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')
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) ]
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
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})
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
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
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
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'
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')
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