def create(username, password, name=None, **kwargs): """Create and return a new instance of :class:`pooldlib.postgresql.models.User`. Any unspecified kwargs will be assumed to be metadata to be associated with the new user. All errors are bubbled up. :param username: The username of the new user. :type username: string :param password: The password for the new user. :type password: string :param name: The full name of the new user (optional). :type password: string :param kwargs: Metadata to associate with the new user. :type kwargs: kwarg dictionary :raises: :class:`pooldlib.exceptions.InvalidPasswordError` :class:`pooldlib.exceptions.UsernameUnavailableError` :class:`pooldlib.exceptions.EmailUnavailableError` :returns: :class:`pooldlib.postgresql.models.User` """ validate_password(password, exception_on_invalid=True) if 'email' in kwargs: if email_exists(kwargs['email']): msg = 'The email address %s is already assigned to another user.' msg %= kwargs['email'] raise EmailUnavailableError(msg) # Only store lower-case emails in the system kwargs['email'] = kwargs['email'].lower() u = UserModel() u.username = username u.password = password if name: u.name = name with transaction_session() as session: try: session.add(u) session.commit() except SQLAlchemyIntegrityError: msg = "Username %s already in use." % username raise UsernameUnavailableError(msg) meta = list() for (k, v) in kwargs.items(): um = UserMetaModel() um.key = k um.value = v um.user = u meta.append(um) with transaction_session(auto_commit=True) as session: for um in meta: session.add(um) return u
def add_goal(campaign, name, description, type, predecessor=None, start=None, end=None, **kwargs): """Add a goal to an existing campaign. ``name`` and ``description`` are required. Any key-value pair will be assumed to be metadata to be added to the goal instance. :param campaign: The campaign goal which the add a new goal. :type campaign: :class:`pooldlib.postgresql.models.Campaign` :param name: The name of the newly created campaign goal. :type name: string :param description: The description of the newly created campaign goal. :type description: string :param type: The type of goal to add (fund-raiser, project or group-purchase) :type type: string :param start: Active start datetime for the campaign in UTC, defaults to `datetime.utcnow` :type start: :class:`datetime.datetime` :param end: Active end datetime for the campaign in UTC, optional :type end: :class:`datetime.datetime` or `None` :param kwargs: Keyword arguments consisting of key-value pairs to be added to the newly created goal as metadata. :type kwargs: unspecified keyword arguments to the function. :returns: :class:`pooldlib.postgresql.models.CommunitiyGoal` """ goal = CampaignGoalModel() goal.name = name goal.description = description goal.start = start or pytz.UTC.localize(datetime.utcnow()) goal.end = end goal.type = type if predecessor is not None: goal.predecessor = predecessor campaign.goals.append(goal) with transaction_session() as session: session.add(goal) session.commit() meta = list() for (k, v) in kwargs.items(): goal_meta = CampaignGoalMetaModel() goal_meta.key = k goal_meta.value = v goal_meta.campaign_goal = goal meta.append(goal_meta) with transaction_session(auto_commit=True) as session: for goal_meta in meta: session.add(goal_meta) return goal
def update_goal(update_goal, name=None, predecessor=None, description=None, start=None, end=None, **kwargs): """Update an existing goal for a campaign. Only ``goal`` is required. All given goal properties will be updated. Any unspecified keyword arguments will be used to update the goal's metadata. To delete metadata for a campaign goal, pass ``None`` as the value for the to be deleted key in the kwarg key-value pair. :param update_goal: Identifier for the target campaign. :type update_goal: :class:`pooldlib.postgresql.models.Goal` :param name: The name of the newly created campaign goal. :type name: string :param description: The description of the newly created campaign goal. :type name: string :param kwargs: Keyword arguments consisting of key-value pairs to be added to the newly created goal as metadata. :type kwargs: unspecified keyword arguments to the function. """ if name is not None: update_goal.name = name if description is not None: update_goal.description = description if start is not None: update_goal.start = start if end is not None: update_goal.end = end if predecessor is not None: goal.predecessor_id = predecessor.id update_meta = [m for m in update_goal.metadata if m.key in kwargs] create_meta = [(k, v) for (k, v) in kwargs.items() if not hasattr(update_goal, k)] meta_delta = list() meta_remove = list() for goal_meta in update_meta: value = kwargs[m.key] if value is None: meta_remove.append(goal_meta) else: goal_meta.value = value meta_delta.append(goal_meta) for (k, v) in create_meta: goal_meta = CampaignGoalMetaModel() goal_meta.key = k goal_meta.value = v goal_meta.campaign_goal = update_goal meta_delta.append(goal_meta) with transaction_session() as session: session.add(update_goal) # Technically not needed session.flush() for goal_meta in meta_delta: session.add(goal_meta) for goal_meta in meta_remove: session.delete(goal_meta) session.commit() return update_goal
def associate_user(campaign, user, role, goal_participation, pledge=None): """Associate a user with a campaign filling a specified role. :param campaign: The campaign with which to associate the user. :type campaign: :class:`pooldlib.postgresql.models.Campaign` :param user: The user for which to create the association. :type user: :class:`pooldlib.postgresql.models.User` :param role: The role to assign the user (either `organizer` or `participant`) :type role: string :param goal_participation: The participation description for the campaign goals. One of `participating`, `nonparticipating`, `opted-in` or `opted-out` (See :func:`pooldlib.api.campaign.associate_user_with_goal`) :type goal_participation: string :raises: :class:`pooldlib.exceptions.InvalidUserRoleError` :class:`pooldlib.exceptions.DuplicateCampaignUserAssociationError` :return: :class:`pooldlib.postgresql.models.CampaignAssociation` """ # NOTE :: We intentionally associate the user with all existing goals, not just # NOTE :: active ones. Date stamps can distinguish users who joined prior to goal # NOTE :: becoming inactive. campaign_goals = goals(campaign, filter_inactive=False) ca = CampaignAssociationModel() ca.enabled = True ca.campaign = campaign ca.user = user ca.role = role goal_pledge = None if pledge is not None: ca.pledge = pledge if campaign_goals: goal_pledge = pledge / len(campaign_goals) for goal in campaign_goals: associate_user_with_goal(goal, user, goal_participation, pledge=goal_pledge) # Check to see if this user was invited and mark them as accepted update_invitee = None for invitee in campaign.invitees: if invitee.user == user or invitee.email == user.email: invitee.accepted = pytz.UTC.localize(datetime.utcnow()) invitee.user = user update_invitee = invitee with transaction_session() as session: session.add(ca) if update_invitee is not None: session.add(update_invitee) try: session.commit() except SQLAlchemyDataError: raise InvalidUserRoleError() except SQLAlchemyIntegrityError: raise DuplicateCampaignUserAssociationError() return ca
def create(organizer, name, description, start=None, end=None, **kwargs): """Create and return a new instance of :class:`pooldlib.postgresql.models.Campaign`. :param origanizer: The user to be classified as the campaign's organizing member. :type origanizer: :class:`pooldlib.postgresql.models.User` :param name: The name of the campaign :type name: string :param description: The description of the campaign :type description: string :param start: Active start datetime for the campaign in UTC, defaults to `datetime.utcnow` :type start: :class:`datetime.datetime` :param end: Active end datetime for the campaign in UTC, optional :type end: :class:`datetime.datetime` or `None` :param kwargs: Metadata to associate with the new campaign. :type kwargs: kwarg dictionary :returns: :class:`pooldlib.postgresql.models.Campaign` """ campaign = CampaignModel() campaign.name = name campaign.description = description campaign.start = start or pytz.UTC.localize(datetime.utcnow()) campaign.end = end with transaction_session() as session: session.add(campaign) session.commit() meta = list() for (k, v) in kwargs.items(): cm = CampaignMetaModel() cm.key = k cm.value = v cm.campaign_id = campaign.id meta.append(cm) with transaction_session(auto_commit=True) as session: for cm in meta: session.add(cm) associate_user(campaign, organizer, 'organizer', 'participating') return campaign
def update(campaign, name=None, description=None, **kwargs): """Update name and description of a specified campaign. :param campaign: Campaign to update. :type campaign: :class:`pooldlib.postgresql.models.Campaign` :param name: If specified, the new name for the campaign. :type name: string :param name: If specified, the new description for the campaign. :type name: string :param kwargs: key-value pairs to associate with the `User` data model instance as metadata. :type kwargs: kwarg dictionary :returns: :class:`pooldlib.postgresql.models.Campaign` """ if name is not None: campaign.name = name if description is not None: campaign.description = description # TODO :: This is really inefficient, fix it. <*****@*****.**> # TODO :: This methodology is mirrored in the user API as well. update_meta = [m for m in campaign.metadata if m.key in kwargs] create_meta = [(k, v) for (k, v) in kwargs.items() if not hasattr(campaign, k)] meta_delta = list() meta_remove = list() for campaign_meta in update_meta: value = kwargs[campaign_meta.key] if value is None: meta_remove.append(campaign_meta) else: campaign_meta.value = value meta_delta.append(campaign_meta) for (k, v) in create_meta: m = CampaignMetaModel() m.key = k m.value = v m.campaign_id = campaign.id meta_delta.append(m) with transaction_session() as session: session.add(campaign) session.commit() for m in meta_delta: session.add(m) session.flush() for m in meta_remove: session.delete(m) session.commit() return campaign
def disable(user): """Disable a specific instance of the User data model. This will prevent the user from being returned by calls to :func:`pooldlib.api.user.get` and any further updates to the user being allowed. :param user: User which to disable. :type user: :class:`pooldlib.postgresql.models.User` """ user.update_field('enabled', False) with transaction_session(auto_commit=True) as session: session.add(user)
def disable(campaign): """Disable a specified campaign. This will prevent it from being returned by calls to :func:`pooldlib.api.campaign.get` and :func:`pooldlib.api.campaign.campaigns` :param campaign: The campaign to disable. :type campaign: :class:`pooldlib.postgresql.models.Campaign` """ campaign.enabled = False with transaction_session() as session: session.add(campaign) session.commit()
def create_for_campaign(campaign, currency): b = BalanceModel() b.enabled = True b.amount = Decimal('0.0000') b.currency = currency b.campaign = campaign b.type = 'campaign' with transaction_session() as session: session.add(b) session.commit() return b
def create_for_user(user, currency): b = BalanceModel() b.enabled = True b.amount = Decimal('0.0000') b.currency = currency b.user = user b.type = 'user' with transaction_session() as session: session.add(b) session.commit() return b
def update_user_association(campaign, user, role=None, pledge=None, only_active_goals=True): """Update an existing User/Campaign association to change the specified user's role in the campaign. :param campaign: The campaign to which the user is associated. :type campaign: :class:`pooldlib.postgresql.models.Campaign` :param user: The user for which to update a campaign association. :type user: :class:`pooldlib.postgresql.models.User` :param role: The role to assign the user, either `organizer` or `participant` (optional). :type role: string :param pledge: The amount the user has pledged to the campaign (optional). :type pledge: Decimal :raises: :class:`pooldlib.exceptions.InvalidUserRoleError` :class:`pooldlib.exceptions.UnknownCampaignAssociationError` :return: :class:`pooldlib.postgresql.models.CampaignAssociation` """ ca = get_associations(campaign, user=user) if not ca: msg = "User %s is not associated with campaign %s. Please create "\ "one with campaign.associate_user()." raise UnknownCampaignAssociationError(msg) ca = ca[0] updated = False if role is not None: updated = ca.update_field('role', role) if pledge is not None: if ca.pledge is not None: msg = "User %s has previously contributed to campaign '%s'" msg %= (user.username, campaign.name) raise PreviousUserContributionError(msg) updated = ca.update_field('pledge', pledge) goal_pledge = None campaign_goals = goals(campaign, filter_inactive=only_active_goals) if campaign_goals: goal_pledge = pledge / len(campaign_goals) for goal in campaign_goals: update_user_goal_association(goal, user, pledge=goal_pledge) if updated: with transaction_session() as session: try: session.commit() except SQLAlchemyIntegrityError: raise InvalidUserRoleError()
def reset_password(user): """Generate a new, random, password for a specific ``User`` data model instance. :param user: User for which to reset password. :type user: :class:`pooldlib.postgresql.models.User` :returns: The new password for the User data model instance as a ``string`` """ newpass = alphanumeric_string(alpha_count=8, number_count=2) user.password = newpass with transaction_session(auto_commit=True) as session: session.add(user) return newpass
def execute(self): """Atomically execute the full transact list. :raises: InsufficentFundsTransferError """ if self._errors: exc, msg = self._errors.pop() self.reset() raise exc(msg) with transaction_session(auto_commit=True) as session: for xfer_class_values in self._transfers.values(): for xfer in xfer_class_values.values(): session.add(xfer) session.flush() for txn_class_values in self._transactions.values(): for txn in txn_class_values.values(): session.add(txn) session.flush()
def disassociate_user(campaign, user): """Remove the association between a User and a Campaign. :param campaign: The campaign to which the user is associated. :type campaign: :class:`pooldlib.postgresql.models.Campaign` :param user: The user for which to remove a campaign association. :type user: :class:`pooldlib.postgresql.models.User` :raises: :class:`pooldlib.exceptions.UnknownCampaignAssociationError` :return: :class:`pooldlib.postgresql.models.CampaignAssociation` """ ca = get_associations(campaign, user=user) if not ca: msg = "User %s is not associated with campaign %s. Please create "\ "one with campaign.associate_user()." raise UnknownCampaignAssociationError(msg) ca = ca[0] with transaction_session() as session: session.delete(ca) session.commit()
def update_user_goal_association(campaign_goal, user, participation=None, pledge=None): """Update a given user's association with ``campaign_goal``. The association will be described by ``participation``, which can be one of 'opted-in' and 'opted-out', 'participating', 'nonparticipating'. The values 'participating' and 'nonparticipating' should only be used for default participation descriptions. If a use opts to change their participation, use appropriate descriptor of 'opted-in' or 'opted-out'. These rules should be followed precisely for reporting purposes. :raises: :class:`pooldlib.exceptions.InvalidGoalParticipationNameError` """ cga = CampaignGoalAssociationModel.query.filter_by(campaign=campaign_goal.campaign)\ .filter_by(campaign_goal=campaign_goal)\ .filter_by(user=user)\ .first() if not cga: msg = "User %s is not associated with goals for campaign %s. Please create "\ "one with campaign.associate_user()." raise UnknownCampaignGoalAssociationError(msg) updated = False if participation is not None: updated = cga.update_field('participation', participation) if pledge is not None: if cga.pledge is not None: msg = "User %s has previously contributed to campaign '%s'" msg %= (user.username, campaign_goal.campaign.name) raise PreviousUserContributionError(msg) updated = cga.update_field('pledge', pledge) if updated: with transaction_session() as session: session.add(cga) try: session.commit() except SQLAlchemyDataError: raise InvalidGoalParticipationNameError() return cga
def set_password(user, password): """Reset a user's password. The new password must conform to the user password requirements as defined in the doc string for :func:`pooldlib.api.user.validate_password`. :param user: User for which to update password. :type user: :class:`pooldlib.postgresql.models.User` :param password: The new password to associate with the user. :type password: string :raises: :class:`pooldlib.exceptions.InvalidPasswordError` :returns: :class:`pooldlib.postgresql.models.User` """ validate_password(password, exception_on_invalid=True) user.update_field('password', password) with transaction_session(auto_commit=True) as session: session.add(user) return user
def add_invite(campaign, email): from pooldlib.api import user usr = user.get_by_email(email) q = InviteeModel.query.filter_by(campaign_id=campaign.id) if usr is not None: q = q.filter_by(user_id=usr.id) else: q = q.filter_by(email=email) existing_invite = q.first() if existing_invite is not None: return None invite = InviteeModel() invite.email = email invite.campaign_id = campaign.id if usr is not None: invite.user_id = usr.id with transaction_session() as session: session.add(invite) session.commit() return invite
def associate_user_with_goal(campaign_goal, user, participation, pledge=None): """Associate given user with ``campaign_goal``. The association will be described by ``participation``, which can be one of 'opted-in', 'opted-out', 'participating', 'nonparticipating'. The values 'participating' and 'nonparticipating' should only be used for default participation descriptions. If a use opts to change their participation, use appropriate descriptor of 'opted-in' or 'opted-out'. These rules should be followed precisely for reporting purposes. :param campaign: The campaign which to associate the user. :type campaign: :class:`pooldlib.postgresql.models.Campaign` :param user: The user to associate. :type user: :class:`pooldlib.postgresql.models.User` :param role: The role to assign the user (either `organizer` or `participant`) :type role: string :param pledge: The amount the user has pledged to the campaign (optional). :type pledge: Decimal :raises: :class:`pooldlib.exceptions.InvalidGoalParticipationNameError` :class:`pooldlib.exceptions.DuplicateCampaignGoalUserAssociationError` """ cga = CampaignGoalAssociationModel() cga.campaign_id = campaign_goal.campaign.id cga.campaign_goal = campaign_goal cga.user = user cga.participation = participation cga.pledge = pledge with transaction_session() as session: session.add(cga) try: session.commit() except SQLAlchemyDataError: raise InvalidGoalParticipationNameError() except SQLAlchemyIntegrityError: raise DuplicateCampaignGoalUserAssociationError() return cga
def update(user, username=None, name=None, password=None, **kwargs): """Update properties of a specific User data model instance. Any unspecified keyword arguments will be assumed to be metadata. Existing metadata will be updated to the newly supplied value, and any new metadata keys will be associated with the user. :func:`pooldlib.api.user.reset_password` can be used to update a users password as well. To delete a user's metadata, pass ``None`` as the value for the to be deleted key in the kwarg key-value pair. The ``name`` property of the User model can be cleared by passing an empty string ('') as the value for the name kwarg. :param user: User for which to update ``User`` model data. :type user: :class:`pooldlib.postgresql.models.User` :param username: New username to associate with `User` instance. :type username: string :param name: New name to associate with `User` data model instance. :type name: string :param kwargs: key-value pairs to associate with the `User` data model instance as metadata. :type kwargs: kwarg dictionary :raises: :class:`pooldlib.exceptions.InvalidPasswordError` :returns: :class:`pooldlib.postgresql.models.User` """ if username: user.update_field('username', username) if name is not None: name = None if name == '' else name user.update_field('name', name, nullable=True) if password: validate_password(password, exception_on_invalid=True) user.update_field('password', password) if 'email' in kwargs: if email_exists(kwargs['email'], user=user): msg = 'The email address %s is already assigned to another user.' msg %= kwargs['email'] raise EmailUnavailableError(msg) kwargs['email'] = kwargs['email'].lower() update_meta = [m for m in user.metadata if m.key in kwargs] create_meta = [(k, v) for (k, v) in kwargs.items() if not hasattr(user, k)] meta_delta = list() meta_remove = list() for user_meta in update_meta: value = kwargs[user_meta.key] if value is None: meta_remove.append(user_meta) else: user_meta.value = value meta_delta.append(user_meta) for (k, v) in create_meta: m = UserMetaModel() m.key = k m.value = v m.user = user meta_delta.append(m) with transaction_session() as session: # Technically not needed, but gives the context content session.add(user) try: session.flush() except SQLAlchemyIntegrityError, e: if username is not None: msg = "Username %s already in use." % username raise UsernameUnavailableError(msg) raise e for m in meta_delta: session.add(m) session.flush() for m in meta_remove: session.delete(m) session.commit()