class VolunteeringReportSchema(ma.SQLAlchemyAutoSchema): class Meta: model = VolunteeringReport load_instance = True ordered = True include_fk = True include_relationships = True reporter_email = ma.Str(dump_only=True) application_id = ma.Int(dump_only=True) application = ma.Nested('ApplicationActivitySchema', data_key='application_on') rating = ma.Int(validate=validate.Range(min=1, max=5))
class AccountSchema(ma.SQLAlchemyAutoSchema): class Meta: model = Account load_instance = True ordered = True include_relationships = True def get_csrf_token(self, _account): return self.context.get('csrf_token') balance = ma.Int() csrf_token = ma.Method(serialize='get_csrf_token', dump_only=True)
class ProductSchema(ma.SQLAlchemyAutoSchema): class Meta: model = Product load_instance = True ordered = True include_relationships = True varieties = ma.Nested('VarietySchema', many=True, validate=validate.Length(min=1)) name = ma.Str(validate=validate.Length(min=1, max=128)) type = ma.Str(validate=validate.Length(min=1, max=128), allow_none=True) description = ma.Str(validate=validate.Length(max=1024)) price = ma.Int(validate=validate.Range(min=1))
class ProjectSchema(ma.SQLAlchemyAutoSchema): class Meta: model = Project load_instance = True ordered = True include_relationships = True name = ma.Str(allow_none=True, validate=validate.Length(max=128), error_messages={ 'validator_failed': 'The name must be below 128 characters.' }) creator = ma.Nested('AccountSchema', only=('full_name', 'email')) image_id = ma.Int(allow_none=True) review_status = EnumField(ReviewStatus) lifetime_stage = EnumField(LifetimeStage) activities = ma.Nested('ActivitySchema', many=True) moderators = ma.Nested('AccountSchema', only=('full_name', 'email'), many=True) start_date = ma.DateTime() end_date = ma.DateTime()
class VarietySchema(ma.SQLAlchemyAutoSchema): class Meta: model = Variety load_instance = True ordered = True include_fk = True include_relationships = True @pre_load def create_stock_change(self, data, **_kwargs): """Convert the integer `amount` property into a stock change.""" if 'stock_changes' in data: raise ValidationError( 'The stock changes are not to be specified explicitly.') if self.context.get('update', False): return data if 'amount' not in data: raise ValidationError('The amount for a variety is not specified.') amount = data.pop('amount') data['stock_changes'] = [{ 'amount': amount, 'account_email': self.context['user'].email, 'status': 'carried_out', }] return data @pre_load def normalize_color(self, data, **_kwargs): """Normalize the color value.""" if 'color' not in data: if self.context.get('update', False): return data raise ValidationError('The color must be specified.') if data['color'] is None: return data if data['color'].startswith('#'): data['color'] = data['color'][1:].upper() if len(data['color']) != 6: raise ValidationError( f'The color value is {len(data["color"])} characters long, 6 expected.' ) return data @pre_load def enumerate_images(self, data, **_kwargs): """Convert the array of URL strings to an array of image objects with order.""" if self.context.get('update', False): if 'images' in data: data['images'] = [{ 'order': idx, 'image_id': id } for (idx, id) in enumerate(data['images'], start=1)] else: try: data['images'] = [{ 'order': idx, 'image_id': id } for (idx, id) in enumerate(data['images'], start=1)] except KeyError: raise ValidationError('Images must be specified.') return data @post_dump def format_color(self, data, **_kwargs): """Add a '#' to the color value.""" if data['color'] is not None: data['color'] = '#' + data['color'] return data @post_dump def flatten_images(self, data, **_kwargs): """Convert an array of image objects with order into a flat array of URL strings.""" if 'images' not in data: return data data['images'] = [ image["image_id"] for image in sorted(data['images'], key=lambda x: x['order']) ] return data images = ma.Nested('ProductImageSchema', many=True) stock_changes = ma.Nested('StockChangeSchema', many=True) amount = ma.Int(dump_only=True) purchases = ma.Int(dump_only=True)
class ActivitySchema(ma.SQLAlchemyAutoSchema): class Meta: model = Activity load_instance = True ordered = True include_relationships = True @pre_load def unwrap_dates(self, data, **_kwargs): """Expand the {"start": , "end": } dates object into two separate properties.""" if 'timeframe' not in data: return data try: dates = data.pop('timeframe') data['start_date'] = dates['start'] data['end_date'] = dates['end'] except KeyError: raise ValidationError("The date range has a wrong format.") return data @post_dump def wrap_dates(self, data, **_kwargs): """Collapse the two date properties into the {"start": , "end": } dates object.""" if 'start_date' not in data: return data data['timeframe'] = { 'start': data.pop('start_date'), 'end': data.pop('end_date') } return data @validates_schema def work_hours_mutex(self, data, **_kwargs): """Ensure that working hours aren't specified along with the reward_rate.""" if 'work_hours' in data and 'reward_rate' in data: raise ValidationError( 'Working hours and reward rate are mutually exclusive.') @validates_schema def valid_date_range(self, data, **_kwargs): """Ensure that the start date is not beyond the end date.""" if data.get('start_date') is not None and data.get( 'end_date') is not None: if data['start_date'] > data['end_date']: raise ValidationError('The start date is beyond the end date.') def get_applications(self, activity): """Retrieve the applications for a particular activity. For non-moderators will only return the approved applications.""" fields = ['id', 'applicant', 'status', 'application_time'] filtering = { 'activity_id': activity.id, 'status': ApplicationStatus.approved } if 'user' not in self.context or not self.context[ 'user'].is_authenticated: return None if self.context['user'] in activity.project.moderators or self.context[ 'user'].is_admin: filtering.pop('status') fields.append('telegram_username') fields.append('comment') fields.append('actual_hours') fields.append('feedback') fields.append('reports') appl_schema = ApplicationSchema(only=fields, many=True) applications = Application.query.filter_by(**filtering) return appl_schema.dump(applications.all()) def get_existing_application(self, activity): """Using the user information from the context, provide a shorthand for the existing application of a volunteer.""" appl_schema = ApplicationSchema(only=('id', 'telegram_username', 'comment', 'actual_hours', 'status', 'feedback')) if 'user' in self.context and self.context['user'].is_authenticated: application = Application.query.filter_by( applicant_email=self.context['user'].email, activity_id=activity.id).one_or_none() if application is None: return None return appl_schema.dump(application) return None working_hours = ma.Int(allow_none=True, validate=validate.Range(min=1)) reward_rate = ma.Int(allow_none=True, validate=validate.Range(min=1)) people_required = ma.Int(allow_none=True, validate=validate.Range(min=0)) start_date = ma.AwareDateTime(allow_none=True, format='iso') end_date = ma.AwareDateTime(allow_none=True, format='iso') application_deadline = ma.AwareDateTime(allow_none=True, format='iso') competences = ma.Pluck(CompetenceSchema, 'id', many=True, validate=validate.Length(0, 3)) vacant_spots = ma.Int(dump_only=True) applications = ma.Method(serialize='get_applications', dump_only=True) existing_application = ma.Method(serialize='get_existing_application', dump_only=True)