class Membership(ExportModelOperationsMixin("membership"), TimeStampedModel): """ Model that represents the membership of a person in a support group This model also indicates if the person is referent for this support group """ MEMBERSHIP_TYPE_MEMBER = 10 MEMBERSHIP_TYPE_MANAGER = 50 MEMBERSHIP_TYPE_REFERENT = 100 MEMBERSHIP_TYPE_CHOICES = ( (MEMBERSHIP_TYPE_MEMBER, "Membre du groupe"), (MEMBERSHIP_TYPE_MANAGER, "Membre gestionnaire"), (MEMBERSHIP_TYPE_REFERENT, "Animateur⋅rice"), ) objects = MembershipQuerySet.as_manager() person = models.ForeignKey( "people.Person", related_name="memberships", on_delete=models.CASCADE, editable=False, ) supportgroup = models.ForeignKey( "SupportGroup", related_name="memberships", on_delete=models.CASCADE, editable=False, ) membership_type = models.IntegerField( _("Statut dans le groupe"), choices=MEMBERSHIP_TYPE_CHOICES, default=MEMBERSHIP_TYPE_MEMBER, ) notifications_enabled = models.BooleanField( _("Recevoir les notifications de ce groupe"), default=True, help_text=_( "Je recevrai des messages en cas de modification du groupe."), ) class Meta: verbose_name = _("adhésion") verbose_name_plural = _("adhésions") unique_together = ("supportgroup", "person") def __str__(self): return _("{person} --> {supportgroup}, ({type})").format( person=self.person, supportgroup=self.supportgroup, type=self.get_membership_type_display(), ) @property def is_referent(self): return self.membership_type >= Membership.MEMBERSHIP_TYPE_REFERENT @property def is_manager(self): return self.membership_type >= Membership.MEMBERSHIP_TYPE_MANAGER
class PersonEmail(ExportModelOperationsMixin("person_email"), models.Model): """ Model that represent a person email address """ objects = PersonEmailManager() address = models.EmailField( _("adresse email"), blank=False, help_text=_( "L'adresse email de la personne, utilisée comme identifiant"), ) _bounced = models.BooleanField( _("email rejeté"), default=False, db_column="bounced", help_text= _("Indique que des mails envoyés ont été rejetés par le serveur distant" ), ) bounced_date = models.DateTimeField( _("date de rejet de l'email"), null=True, blank=True, help_text=_( "Si des mails ont été rejetés, indique la date du dernier rejet"), ) @property def bounced(self): return self._bounced @bounced.setter def bounced(self, value): if value and self._bounced is False: self.bounced_date = timezone.now() self._bounced = value person = models.ForeignKey(Person, on_delete=models.CASCADE, null=False, related_name="emails") class Meta: order_with_respect_to = "person" verbose_name = _("Email") def __str__(self): return self.address def validate_unique(self, exclude=None): errors = {} try: super().validate_unique(exclude=exclude) except ValidationError as e: errors = e.message_dict if exclude is None or "address" not in exclude: qs = PersonEmail.objects.filter(address__iexact=self.address) if not self._state.adding and self.pk: qs = qs.exclude(pk=self.pk) if qs.exists(): errors.setdefault("address", []).append( ValidationError( message=_("Cette adresse email est déjà utilisée."), code="unique", )) if errors: raise ValidationError(errors) def clean(self): self.address = PersonEmail.objects.normalize_email(self.address)
class ProloginUser(ExportModelOperationsMixin('user'), AbstractUser, AddressableModel): @staticmethod def upload_seed(instance): return 'prologinuser/{}'.format(instance.pk).encode() def upload_avatar_to(self, *args, **kwargs): return upload_path('avatar', using=ProloginUser.upload_seed)(self, *args, **kwargs) def upload_picture_to(self, *args, **kwargs): return upload_path('picture', using=ProloginUser.upload_seed)(self, *args, **kwargs) USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email'] gender = GenderField(blank=True, null=True, db_index=True) school_stage = EnumField(EducationStage, null=True, db_index=True, blank=True, verbose_name=_("Educational stage")) phone = models.CharField(max_length=16, blank=True, verbose_name=_("Phone")) birthday = models.DateField(blank=True, null=True, verbose_name=_("Birth day")) allow_mailing = models.BooleanField( default=True, blank=True, db_index=True, verbose_name=_("Allow Prologin to send me emails"), help_text=_("We only mail you to provide useful information " "during the various stages of the contest. " "We hate spam as much as you do!")) signature = models.TextField(blank=True, verbose_name=_("Signature")) preferred_language = CodingLanguageField( blank=True, db_index=True, verbose_name=_("Preferred coding language")) timezone = TimeZoneField(default=settings.TIME_ZONE, verbose_name=_("Time zone")) preferred_locale = models.CharField(max_length=8, blank=True, verbose_name=_("Locale"), choices=settings.LANGUAGES) avatar = ResizeOnSaveImageField(upload_to=upload_avatar_to, storage=overwrite_storage, fit_into=settings.PROLOGIN_MAX_AVATAR_SIZE, blank=True, verbose_name=_("Profile picture")) picture = ResizeOnSaveImageField( upload_to=upload_picture_to, storage=overwrite_storage, fit_into=settings.PROLOGIN_MAX_AVATAR_SIZE, blank=True, verbose_name=_("Official member picture")) # MD5 password from <2015 Drupal website legacy_md5_password = models.CharField(max_length=32, blank=True) objects = ProloginUserManager() def get_homes(self): return [ c for c in self.contestants.order_by('-edition__year') if c.has_home ] def get_contestants(self): return self.contestants.select_related('edition').order_by( '-edition__year') def get_involved_contestants(self): return self.get_contestants().exclude( assignation_semifinal=Assignation.not_assigned.value) def can_edit_profile(self, edition): if edition is None: # no edition, fallback to allow return True if self.has_perm('users.edit-during-contest'): # privileged return True event, type = edition.phase if event is None: # future or finished, allow return True assigned_semifinal = self.contestants.filter( edition=edition, assignation_semifinal=Assignation.assigned.value).exists() if event == Event.Type.qualification and type == 'corrected' and assigned_semifinal: return False if not assigned_semifinal: return True # below: assigned to semifinal assigned_final = self.contestants.filter( edition=edition, assignation_final=Assignation.assigned.value).exists() if event == Event.Type.semifinal: if type in ('active', 'done'): return False if type == 'corrected' and assigned_final: return False if not assigned_final: return True # below: assigned to final if event == Event.Type.final and type in ('active', 'done'): return False return True @property def preferred_language_enum(self): return Language[self.preferred_language] def plaintext_password(self, event): event_salt = str(event) if event else '' return (base64.urlsafe_b64encode( hashlib.sha1("{}{}{}{}".format( self.first_name, self.last_name, event_salt, settings.PLAINTEXT_PASSWORD_SALT).encode( 'utf-8')).digest()).decode('ascii').translate( settings.PLAINTEXT_PASSWORD_DISAMBIGUATION) [:settings.PLAINTEXT_PASSWORD_LENGTH]) @property def normalized_username(self): return slugify("{}{}".format(self.first_name[:1], self.last_name)) @property def avatar_or_picture(self): if self.avatar: return self.avatar return self.picture @property def picture_or_avatar(self): if self.picture: return self.picture return self.avatar @property def unsubscribe_token(self): user_id = str(self.id).encode() secret = settings.SECRET_KEY.encode() return hashlib.sha256(user_id + secret).hexdigest() def has_partial_address(self): return any((self.address, self.city, self.country, self.postal_code)) def has_complete_address(self): return all((self.address, self.city, self.country, self.postal_code)) def has_complete_profile(self): return self.has_complete_address() and all((self.phone, self.birthday)) def get_absolute_url(self): return reverse('users:profile', args=[self.pk]) def get_unsubscribe_url(self): return '{}{}?uid={}&token={}'.format(settings.SITE_BASE_URL, reverse('users:unsubscribe'), self.id, self.unsubscribe_token) def young_enough_to_compete(self, edition): if not self.birthday: return False last_ok_year = edition - settings.PROLOGIN_MAX_AGE return last_ok_year <= self.birthday.year
class Event( ExportModelOperationsMixin("event"), BaseAPIResource, NationBuilderResource, LocationMixin, ImageMixin, DescriptionMixin, ContactMixin, ): """ Model that represents an event """ objects = EventQuerySet.as_manager() name = models.CharField( _("nom"), max_length=255, blank=False, help_text=_("Le nom de l'événement") ) VISIBILITY_ADMIN = "A" VISIBILITY_ORGANIZER = "O" VISIBILITY_PUBLIC = "P" VISIBILITY_CHOICES = ( (VISIBILITY_ADMIN, "Caché"), (VISIBILITY_ORGANIZER, "Visible par les organisateurs"), (VISIBILITY_PUBLIC, "Public"), ) visibility = models.CharField( "Visibilité", max_length=1, choices=VISIBILITY_CHOICES, default=VISIBILITY_PUBLIC, ) subtype = models.ForeignKey( "EventSubtype", verbose_name="Sous-type", related_name="events", on_delete=models.PROTECT, default=get_default_subtype, ) nb_path = models.CharField(_("NationBuilder path"), max_length=255, blank=True) tags = models.ManyToManyField("EventTag", related_name="events", blank=True) start_time = CustomDateTimeField(_("date et heure de début"), blank=False) end_time = CustomDateTimeField(_("date et heure de fin"), blank=False) max_participants = models.IntegerField( "Nombre maximum de participants", blank=True, null=True ) allow_guests = models.BooleanField( "Autoriser les participant⋅e⋅s à inscrire des invité⋅e⋅s", default=False ) facebook = FacebookEventField("Événement correspondant sur Facebook", blank=True) attendees = models.ManyToManyField( "people.Person", related_name="events", through="RSVP" ) organizers = models.ManyToManyField( "people.Person", related_name="organized_events", through="OrganizerConfig" ) organizers_groups = models.ManyToManyField( "groups.SupportGroup", related_name="organized_events", through="OrganizerConfig", ) report_image = StdImageField( verbose_name=_("image de couverture"), blank=True, variations={"thumbnail": (400, 250), "banner": (1200, 400)}, upload_to=report_image_path, help_text=_( "Cette image apparaîtra en tête de votre compte-rendu, et dans les partages que vous ferez du" " compte-rendu sur les réseaux sociaux." ), ) report_content = DescriptionField( verbose_name=_("compte-rendu de l'événement"), blank=True, allowed_tags="allowed_tags", help_text=_( "Ajoutez un compte-rendu de votre événement. N'hésitez pas à inclure des photos." ), ) report_summary_sent = models.BooleanField( "Le mail de compte-rendu a été envoyé", default=False ) subscription_form = models.OneToOneField( "people.PersonForm", null=True, blank=True, on_delete=models.PROTECT ) payment_parameters = JSONField( verbose_name=_("Paramètres de paiement"), null=True, blank=True ) scanner_event = models.IntegerField( "L'ID de l'événement sur le logiciel de tickets", blank=True, null=True ) scanner_category = models.IntegerField( "La catégorie que doivent avoir les tickets sur scanner", blank=True, null=True ) enable_jitsi = models.BooleanField("Activer la visio-conférence", default=False) participation_template = models.TextField( _("Template pour la page de participation"), blank=True, null=True ) do_not_list = models.BooleanField( "Ne pas lister l'événement", default=False, help_text="L'événement n'apparaîtra pas sur la carte, ni sur le calendrier " "et ne sera pas cherchable via la recherche interne ou les moteurs de recherche.", ) legal = JSONField( _("Informations juridiques"), default=dict, blank=True, encoder=CustomJSONEncoder, ) class Meta: verbose_name = _("événement") verbose_name_plural = _("événements") ordering = ("-start_time", "-end_time") permissions = ( # DEPRECIATED: every_event was set up as a potential solution to Rest Framework django permissions # Permission class default behaviour of requiring both global permissions and object permissions before # allowing users. Was not used in the end.s ("every_event", _("Peut éditer tous les événements")), ("view_hidden_event", _("Peut voir les événements non publiés")), ) indexes = ( models.Index( fields=["start_time", "end_time"], name="events_datetime_index" ), models.Index(fields=["end_time"], name="events_end_time_index"), models.Index(fields=["nb_path"], name="events_nb_path_index"), ) def __str__(self): return f"{self.name} ({self.get_display_date()})" def __repr__(self): return f"{self.__class__.__name__}(id={str(self.pk)!r}, name={self.name!r})" def to_ics(self): event_url = front_url("view_event", args=[self.pk], auto_login=False) return ics.Event( name=self.name, begin=self.start_time, end=self.end_time, uid=str(self.pk), description=self.description + f"<p>{event_url}</p>", location=self.short_address, url=event_url, ) @property def participants(self): try: return self.all_attendee_count except AttributeError: if self.subscription_form: return ( self.rsvps.annotate( identified_guests_count=Count("identified_guests") ).aggregate(participants=Sum(F("identified_guests_count") + 1))[ "participants" ] or 0 ) return ( self.rsvps.aggregate(participants=Sum(models.F("guests") + 1))[ "participants" ] or 0 ) @property def type(self): return self.subtype.type def get_display_date(self): tz = timezone.get_current_timezone() start_time = self.start_time.astimezone(tz) end_time = self.end_time.astimezone(tz) if start_time.date() == end_time.date(): date = formats.date_format(start_time, "DATE_FORMAT") return _("le {date}, de {start_hour} à {end_hour}").format( date=date, start_hour=formats.time_format(start_time, "TIME_FORMAT"), end_hour=formats.time_format(end_time, "TIME_FORMAT"), ) return _("du {start_date}, {start_time} au {end_date}, {end_time}").format( start_date=formats.date_format(start_time, "DATE_FORMAT"), start_time=formats.date_format(start_time, "TIME_FORMAT"), end_date=formats.date_format(end_time, "DATE_FORMAT"), end_time=formats.date_format(end_time, "TIME_FORMAT"), ) def get_simple_display_date(self): tz = timezone.get_current_timezone() start_time = self.start_time.astimezone(tz) return _("le {date} à {time}").format( date=formats.date_format(start_time, "DATE_FORMAT"), time=formats.time_format(start_time, "TIME_FORMAT"), ) def is_past(self): return timezone.now() > self.end_time def is_current(self): return self.start_time < timezone.now() < self.end_time def clean(self): if self.start_time and self.end_time and self.end_time < self.start_time: raise ValidationError( { "end_time": _( "La date de fin de l'événement doit être postérieure à sa date de début." ) } ) def get_price_display(self): if self.payment_parameters is None: return None base_price = self.payment_parameters.get("price", 0) min_price = base_price max_price = base_price for mapping in self.payment_parameters.get("mappings", []): prices = [m["price"] for m in mapping["mapping"]] min_price += min(prices) max_price += max(prices) if min_price == max_price == 0: if "free_pricing" in self.payment_parameters: return "Prix libre" else: return None if min_price == max_price: display = "{} €".format(floatformat(min_price / 100, 2)) else: display = "de {} à {} €".format( floatformat(min_price / 100, 2), floatformat(max_price / 100, 2) ) if "free_pricing" in self.payment_parameters: display += " + montant libre" return display @property def is_free(self): return self.payment_parameters is None def get_price(self, submission_data: dict = None): price = self.payment_parameters.get("price", 0) if submission_data is None: return price for mapping in self.payment_parameters.get("mappings", []): values = [submission_data.get(field) for field in mapping["fields"]] d = {tuple(v for v in m["values"]): m["price"] for m in mapping["mapping"]} price += d[tuple(values)] if "free_pricing" in self.payment_parameters: field = self.payment_parameters["free_pricing"] price += max(0, int(submission_data.get(field, 0) * 100)) return price def get_absolute_url(self): return front_url("view_event", args=[self.pk])
class Group(ExportModelOperationsMixin('group'), EffigiaModel): cover_image = models.ImageField(upload_to='covers/group/')
class RRset(ExportModelOperationsMixin('RRset'), models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) created = models.DateTimeField(auto_now_add=True) touched = models.DateTimeField(auto_now=True) domain = models.ForeignKey(Domain, on_delete=models.CASCADE) subname = models.CharField( max_length=178, blank=True, validators=[ validate_lower, RegexValidator( regex=r'^([*]|(([*][.])?([a-z0-9_-]+[.])*[a-z0-9_-]+))$', message= 'Subname can only use (lowercase) a-z, 0-9, ., -, and _, ' 'may start with a \'*.\', or just be \'*\'.', code='invalid_subname') ]) type = models.CharField( max_length=10, validators=[ validate_upper, RegexValidator( regex=r'^[A-Z][A-Z0-9]*$', message= 'Type must be uppercase alphanumeric and start with a letter.', code='invalid_type') ]) ttl = models.PositiveIntegerField() objects = RRsetManager() class Meta: constraints = [ ExclusionConstraint( name='cname_exclusivity', expressions=[ ('domain', RangeOperators.EQUAL), ('subname', RangeOperators.EQUAL), (RawSQL("int4(type = 'CNAME')", ()), RangeOperators.NOT_EQUAL), ], ), ] unique_together = (("domain", "subname", "type"), ) @staticmethod def construct_name(subname, domain_name): return '.'.join(filter(None, [subname, domain_name])) + '.' @property def name(self): return self.construct_name(self.subname, self.domain.name) def save(self, *args, **kwargs): self.full_clean(validate_unique=False) super().save(*args, **kwargs) def clean_records(self, records_presentation_format): """ Validates the records belonging to this set. Validation rules follow the DNS specification; some types may incur additional validation rules. Raises ValidationError if violation of DNS specification is found. Returns a set of records in canonical presentation format. :param records_presentation_format: iterable of records in presentation format """ rdtype = rdatatype.from_text(self.type) errors = [] if self.type == 'CNAME': if self.subname == '': errors.append('CNAME RRset cannot have empty subname.') if len(records_presentation_format) > 1: errors.append( 'RRset of type CNAME cannot have multiple records.') def _error_msg(record, detail): return f'Record content of {self.type} {self.name} invalid: \'{record}\': {detail}' records_canonical_format = set() for r in records_presentation_format: try: r_canonical_format = RR.canonical_presentation_format( r, rdtype) except binascii.Error: # e.g., odd-length string errors.append( _error_msg( r, 'Cannot parse hexadecimal or base64 record contents')) except dns.exception.SyntaxError as e: # e.g., A/127.0.0.999 if 'quote' in e.args[0]: errors.append( _error_msg( r, f'Data for {self.type} records must be given using quotation marks.' )) else: errors.append( _error_msg( r, f'Record content malformed: {",".join(e.args)}')) except dns.name.NeedAbsoluteNameOrOrigin: errors.append( _error_msg( r, 'Hostname must be fully qualified (i.e., end in a dot: "example.com.")' )) except ValueError: # e.g., string ("asdf") cannot be parsed into int on base 10 errors.append(_error_msg(r, 'Cannot parse record contents')) except Exception as e: # TODO see what exceptions raise here for faulty input raise e else: if r_canonical_format in records_canonical_format: errors.append( _error_msg( r, f'Duplicate record content: this is identical to ' f'\'{r_canonical_format}\'')) else: records_canonical_format.add(r_canonical_format) if any(errors): raise ValidationError(errors) return records_canonical_format def save_records(self, records): """ Updates this RR set's resource records, discarding any old values. Records are expected in presentation format and are converted to canonical presentation format (e.g., 127.00.0.1 will be converted to 127.0.0.1). Raises if a invalid set of records is provided. This method triggers the following database queries: - one DELETE query - one SELECT query for comparison of old with new records - one INSERT query, if one or more records were added Changes are saved to the database immediately. :param records: list of records in presentation format """ new_records = self.clean_records(records) # Delete RRs that are not in the new record list from the DB self.records.exclude(content__in=new_records).delete() # one DELETE # Retrieve all remaining RRs from the DB unchanged_records = set(r.content for r in self.records.all()) # one SELECT # Save missing RRs from the new record list to the DB added_records = new_records - unchanged_records rrs = [RR(rrset=self, content=content) for content in added_records] RR.objects.bulk_create(rrs) # One INSERT def __str__(self): return '<RRSet %s domain=%s type=%s subname=%s>' % ( self.pk, self.domain.name, self.type, self.subname)
class Lawn(ExportModelOperationsMixin("lawn"), Model): location = CharField(max_length=100)
class ResourceData(ExportModelOperationsMixin("resource_data"), models.Model): # type: ignore # noqa E501 class Tier(models.IntegerChoices): TIER_1 = 0, _("T1 - Placid") TIER_2 = 1, _("T2 - Temperate") TIER_3 = 2, _("T3 - Rugged") TIER_4 = 3, _("T4 - Inhospitable") TIER_5 = 4, _("T5 - Turbulent") TIER_6 = 5, _("T6 - Fierce") TIER_7 = 6, _("T7 - Savage") TIER_8 = 7, _("T8 - Brutal") item = models.OneToOneField(Item, on_delete=models.CASCADE, related_name="resource_data") is_embedded = models.BooleanField() exo_only = models.BooleanField(default=False) max_tier = models.PositiveSmallIntegerField( _("Max Tier"), choices=Tier.choices, help_text=_("Max tier of world to be found on. Starts at 0."), ) min_tier = models.PositiveSmallIntegerField( _("Min Tier"), choices=Tier.choices, help_text=_("Min tier of world to be found on. Starts at 0."), ) best_max_tier = models.PositiveSmallIntegerField( _("Max Tier"), choices=Tier.choices, help_text=_("Max tier of world to be found on. Starts at 0."), ) best_min_tier = models.PositiveSmallIntegerField( _("Min Tier"), choices=Tier.choices, help_text=_("Min tier of world to be found on. Starts at 0."), ) shape = models.PositiveSmallIntegerField() size_max = models.PositiveSmallIntegerField() size_min = models.PositiveSmallIntegerField() altitude_max = models.PositiveSmallIntegerField() altitude_min = models.PositiveSmallIntegerField() distance_max = models.PositiveSmallIntegerField(blank=True, null=True) distance_min = models.PositiveSmallIntegerField(blank=True, null=True) cave_weighting = models.FloatField() size_skew_to_min = models.FloatField() blocks_above_max = models.PositiveSmallIntegerField() blocks_above_min = models.PositiveSmallIntegerField() liquid_above_max = models.PositiveSmallIntegerField() liquid_above_min = models.PositiveSmallIntegerField() noise_frequency = models.FloatField(blank=True, null=True) noise_threshold = models.FloatField(blank=True, null=True) liquid_favorite = models.ForeignKey(Item, on_delete=models.CASCADE, blank=True, null=True, related_name="+") three_d_weighting = models.FloatField() surface_favorite = models.ForeignKey(Item, on_delete=models.CASCADE, blank=True, null=True, related_name="+") surface_weighting = models.FloatField() altitude_best_lower = models.PositiveSmallIntegerField() altitude_best_upper = models.PositiveSmallIntegerField() distance_best_lower = models.PositiveSmallIntegerField(blank=True, null=True) distance_best_upper = models.PositiveSmallIntegerField(blank=True, null=True) blocks_above_best_lower = models.PositiveSmallIntegerField() blocks_above_best_upper = models.PositiveSmallIntegerField() liquid_above_best_upper = models.PositiveSmallIntegerField() liquid_above_best_lower = models.PositiveSmallIntegerField() liquid_second_favorite = models.ForeignKey(Item, on_delete=models.CASCADE, blank=True, null=True, related_name="+") surface_second_favorite = models.ForeignKey(Item, on_delete=models.CASCADE, blank=True, null=True, related_name="+") @property def best_world_types(self): types = [] for world_type in self.best_worlds.all(): types.append(world_type.world_type) return types
class RecipeRequirement(ExportModelOperationsMixin("recipe_requirement"), models.Model): # type: ignore # noqa E501 skill = models.ForeignKey(Skill, on_delete=models.CASCADE) level = models.PositiveSmallIntegerField()
class Metal(ExportModelOperationsMixin("metal"), GameObj): # type: ignore # noqa E501 pass
class Item(ExportModelOperationsMixin("item"), GameObj): # type: ignore # noqa E501 item_subtitle = models.ForeignKey(Subtitle, on_delete=models.SET_NULL, blank=True, null=True) string_id = models.CharField(_("String ID"), max_length=64, db_index=True) name = models.CharField(_("Name"), max_length=64) mint_value = models.FloatField(_("Chrysominter Value"), null=True, blank=True) max_stack = models.PositiveSmallIntegerField(default=100) can_be_sold = models.BooleanField(db_index=True, default=True) list_type = models.ForeignKey( LocalizedString, on_delete=models.CASCADE, related_name="+", blank=True, null=True, ) description = models.ForeignKey( LocalizedString, on_delete=models.CASCADE, related_name="+", blank=True, null=True, ) is_resource = models.BooleanField(default=False, db_index=True) prestige = models.PositiveSmallIntegerField(default=0) mine_xp = models.PositiveSmallIntegerField(default=0) build_xp = models.PositiveSmallIntegerField(default=0) is_block = models.BooleanField(default=False, db_index=True) is_liquid = models.BooleanField(default=False, db_index=True) default_color = models.ForeignKey(Color, on_delete=models.CASCADE, blank=True, null=True) image = models.ImageField(storage=select_storage("items"), blank=True, null=True) image_small = models.ImageField(storage=select_storage("items"), blank=True, null=True) class Meta: indexes = [ GinIndex(fields=["string_id"]), ] @property def default_name(self): # pylint: disable=invalid-overridden-method return self.string_id @property def english(self): return super().default_name @property def buy_locations(self): return self.itemshopstandprice_set.filter(active=True) @property def sell_locations(self): return self.itemrequestbasketprice_set.filter(active=True) @property def has_colors(self): return self.game_id in get_block_color_item_ids() @property def has_world_colors(self): return self.game_id in get_world_block_color_item_ids() @property def has_metal_variants(self): return self.game_id in get_block_metal_item_ids() @property def next_shop_stand_update(self): return get_next_rank_update(self.itemsellrank_set.all()) @property def next_request_basket_update(self): return get_next_rank_update(self.itembuyrank_set.all())
class Subtitle(ExportModelOperationsMixin("subtitle"), GameObj): # type: ignore # noqa E501 pass
class Picture(ExportModelOperationsMixin("picture"), models.Model): title = models.CharField(max_length=100) description = models.TextField(blank=True) photo = models.ImageField(upload_to=get_photo_upload_path) tags = models.TextField( blank=True, validators=[ RegexValidator( r"^[a-z0-9- ]*$", "Tags must only contain lowercase characters a-z, numbers, and dashes (-)", code="invalid", ), validate_tags_under_max_length, validate_not_too_many_tags, ], ) uploaded_at = models.DateTimeField(default=now) updated_at = models.DateTimeField(default=now) public_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) # Thumbnails thumbnail_w_272 = models.ImageField(upload_to="thumbnails/w272/%Y/%m/%d/", blank=True) # Note: be sure to update how this is done (trigger in postgres) # once django releases a good way to do Stored Generated Columns # https://stackoverflow.com/questions/59675402/django-full-text-searchvectorfield-obsolete-in-postgresql indexed_tags_search = SearchVectorField(null=True) uploaded_by = models.ForeignKey(CustomUser, models.SET_NULL, blank=True, null=True) class Meta: indexes = [GinIndex(fields=["indexed_tags_search"])] def __str__(self): return self.title def save(self, *args, **kwargs): if not self.thumbnail_w_272: self.thumbnail_w_272 = self.make_thumbnail( self.photo, (272, self.photo.height)) self.tags = " ".join(set(self.tags.split())) super().save(*args, **kwargs) def make_thumbnail(self, image, size): """Makes thumbnails of given size from given image Args: image (``django.ImageField``): The image to generate a thumbnail for size (``tuple`` of ``int``): The width and height to generate Returns: ``django.File``: The resized image """ image.open() im = Image.open(image) im.thumbnail(size) # resize image thumb_io = io.BytesIO() # create a BytesIO object im = im.convert("RGB") im.save(thumb_io, "JPEG", quality=85) # save image to BytesIO object thumbnail = File(thumb_io, name=pathlib.Path( image.name).name) # create a django friendly File object return thumbnail # TODO: Will get called multiple times in template. Maybe persist when it is created? @property def split_tags(self): tags = str(self.tags).split() data = {"above_tags": [], "below_tags": []} max_width = 85 # Max width of the theoretical tag bar expander_length = 5 # Width of the "click to expand" symbol static_width_addition = 4 # How much to add in addition to each letter current_width = 0 above = True # TODO: Minor optimization, stop adding once above is hit for tag in tags: current_width += static_width_addition + len(tag) if current_width + expander_length > max_width: above = False if above: data["above_tags"].append(tag) else: data["below_tags"].append(tag) return data
class SupportGroup( ExportModelOperationsMixin("support_group"), BaseAPIResource, LocationMixin, ImageMixin, DescriptionMixin, ContactMixin, ): """ Model that represents a support group """ TYPE_LOCAL_GROUP = "L" TYPE_THEMATIC = "B" TYPE_FUNCTIONAL = "F" TYPE_PROFESSIONAL = "P" TYPE_2022 = "2" TYPE_LFI_CHOICES = ( (TYPE_LOCAL_GROUP, "Groupe local"), (TYPE_THEMATIC, "Groupe thématique"), (TYPE_FUNCTIONAL, "Groupe fonctionnel"), (TYPE_PROFESSIONAL, "Groupe professionel"), ) TYPE_NSP_CHOICES = ((TYPE_2022, "Équipe de soutien « Nous Sommes Pour ! »"), ) TYPE_CHOICES = TYPE_LFI_CHOICES + TYPE_NSP_CHOICES TYPE_PARAMETERS = { TYPE_LOCAL_GROUP: { "color": "#4a64ac", "icon_name": "users" }, TYPE_THEMATIC: { "color": "#49b37d", "icon_name": "book" }, TYPE_FUNCTIONAL: { "color": "#e14b35", "icon_name": "cog" }, TYPE_PROFESSIONAL: { "color": "#f4981e", "icon_name": "industry" }, TYPE_2022: { "color": "#571aff", "icon_name": "users" }, } TYPE_DESCRIPTION = { TYPE_LOCAL_GROUP: "Les groupes d’action géographiques de la France insoumise sont constitués sur la base d’un" " territoire réduit (quartier, villages ou petites villes, cantons). Chaque insoumis⋅e peut assurer" " l’animation d’un seul groupe d’action géographique.", TYPE_THEMATIC: "Les groupes d’action thématiques réunissent des insoumis⋅es qui" " souhaitent agir de concert sur un thème donné en lien avec les livrets" " thématiques correspondant.", TYPE_FUNCTIONAL: "Les groupes d’action fonctionnels remplissent" " des fonctions précises (formations, organisation" " des apparitions publiques, rédaction de tracts, chorale insoumise," " journaux locaux, auto-organisation, etc…).", TYPE_PROFESSIONAL: "Les groupes d’action professionnels rassemblent des insoumis⋅es qui" " souhaitent agir au sein de leur entreprise ou de leur lieu d’étude.", TYPE_2022: "Les équipes de soutien « Nous Sommes Pour ! » peuvent être rejointes par toutes les personnes " "ayant parainné la candidature de Jean-Luc Mélenchon.", } TYPE_DISABLED_DESCRIPTION = { TYPE_LOCAL_GROUP: "", TYPE_THEMATIC: "", TYPE_FUNCTIONAL: "", TYPE_PROFESSIONAL: "", TYPE_2022: "✅ Vous animez déjà une équipe de soutien", } MEMBERSHIP_LIMIT = 30 objects = SupportGroupQuerySet.as_manager() name = models.CharField(_("nom"), max_length=255, blank=False, help_text=_("Le nom du groupe")) type = models.CharField( _("type de groupe"), max_length=1, blank=False, default=TYPE_LOCAL_GROUP, choices=TYPE_CHOICES, ) subtypes = models.ManyToManyField("SupportGroupSubtype", related_name="supportgroups", blank=True) published = models.BooleanField( _("publié"), default=True, blank=False, help_text=_("Le groupe doit-il être visible publiquement."), ) tags = models.ManyToManyField("SupportGroupTag", related_name="groups", blank=True) members = models.ManyToManyField("people.Person", related_name="supportgroups", through="Membership", blank=True) @property def managers(self): return [ m.person for m in self.memberships.filter( membership_type__gte=Membership.MEMBERSHIP_TYPE_MANAGER) ] @property def referents(self): return [ m.person for m in self.memberships.filter( membership_type__gte=Membership.MEMBERSHIP_TYPE_REFERENT) ] @property def events_count(self): from agir.events.models import Event return self.organized_events.filter( visibility=Event.VISIBILITY_PUBLIC).count() @property def members_count(self): return Membership.objects.filter(supportgroup=self).count() @property def is_full(self): return self.is_2022 and self.members_count >= self.MEMBERSHIP_LIMIT @property def is_certified(self): return self.subtypes.filter( label__in=settings.CERTIFIED_GROUP_SUBTYPES).exists() @property def allow_external(self): return self.subtypes.filter(allow_external=True).exists() @property def external_help_text(self): subtype = self.subtypes.filter(allow_external=True).first() return subtype.external_help_text or "" @property def is_2022(self): return self.type == self.TYPE_2022 class Meta: verbose_name = _("groupe d'action") verbose_name_plural = _("groupes d'action") ordering = ("-created", ) permissions = (("view_hidden_supportgroup", _("Peut afficher les groupes non publiés")), ) def __str__(self): return self.name def __repr__(self): return f"{self.__class__.__name__}(id={str(self.pk)!r}, name={self.name!r})"
class Usuario(ExportModelOperationsMixin('usuario'), SimpleEmailConfirmationUserMixin, CustomAbstractUser, TemChaveExterna): """Classe de autenticacao do django, ela tem muitos perfis.""" SME = 0 PREFEITURA = 1 TIPOS_EMAIL = ((SME, '@sme.prefeitura.sp.gov.br'), (PREFEITURA, '@prefeitura.sp.gov.br')) nome = models.CharField(_('name'), max_length=150) email = models.EmailField(_('email address'), unique=True) tipo_email = models.PositiveSmallIntegerField(choices=TIPOS_EMAIL, null=True, blank=True) registro_funcional = models.CharField( _('RF'), max_length=7, blank=True, null=True, unique=True, # noqa DJ01 validators=[MinLengthValidator(7)]) # TODO: essew atributow deve pertencer somente a um model Pessoa cpf = models.CharField( _('CPF'), max_length=11, blank=True, null=True, unique=True, # noqa DJ01 validators=[MinLengthValidator(11)]) contatos = models.ManyToManyField('dados_comuns.Contato', blank=True) # TODO: esses atributos devem pertencer somente a um model Nutricionista super_admin_terceirizadas = models.BooleanField( 'É Administrador por parte das Terceirizadas?', default=False) crn_numero = models.CharField('Nutricionista crn', max_length=160, blank=True, null=True) # noqa DJ01 USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] # type: ignore @property def vinculos(self): return self.vinculos @property def vinculo_atual(self): if self.vinculos.filter( Q(data_inicial=None, data_final=None, ativo=False) | # noqa W504 esperando ativacao Q(data_inicial__isnull=False, data_final=None, ativo=True) ).exists(): return self.vinculos.get( Q(data_inicial=None, data_final=None, ativo=False) | # noqa W504 esperando ativacao Q(data_inicial__isnull=False, data_final=None, ativo=True)) return None @property def tipo_usuario(self): tipo_usuario = 'indefinido' if self.vinculo_atual: tipo_usuario = self.vinculo_atual.content_type.model if tipo_usuario == 'codae': if self.vinculo_atual.perfil.nome in [ COORDENADOR_GESTAO_ALIMENTACAO_TERCEIRIZADA, ADMINISTRADOR_GESTAO_ALIMENTACAO_TERCEIRIZADA ]: tipo_usuario = 'gestao_alimentacao_terceirizada' else: tipo_usuario = 'dieta_especial' return tipo_usuario @property def pode_efetuar_cadastro(self): # TODO: passar isso para o app EOL serviço headers = {'Authorization': f'Token {DJANGO_EOL_API_TOKEN}'} r = requests.get( f'{DJANGO_EOL_API_URL}/cargos/{self.registro_funcional}', headers=headers) response = r.json() if not isinstance(response, dict): raise EolException(f'{response}') diretor_de_escola = False for result in response['results']: if result['cargo'] == 'DIRETOR DE ESCOLA': diretor_de_escola = True break vinculo_aguardando_ativacao = self.vinculo_atual.status == Vinculo.STATUS_AGUARDANDO_ATIVACAO return diretor_de_escola or vinculo_aguardando_ativacao def enviar_email_confirmacao(self): self.add_email_if_not_exists(self.email) content = { 'uuid': self.uuid, 'confirmation_key': self.confirmation_key } self.email_user( subject='Confirme seu e-mail - SIGPAE', message=f'Clique neste link para confirmar seu e-mail no SIGPAE \n' f': {url_configs("CONFIRMAR_EMAIL", content)}', ) def enviar_email_recuperacao_senha(self): token_generator = PasswordResetTokenGenerator() token = token_generator.make_token(self) content = {'uuid': self.uuid, 'confirmation_key': token} self.email_user( subject='Email de recuperação de senha', message=f'Clique neste link para criar uma nova senha no SIGPAE \n' f': {url_configs("RECUPERAR_SENHA", content)}', ) def enviar_email_administrador(self): self.add_email_if_not_exists(self.email) self.email_user( subject='[SIGPAE] Novo cadastro de empresa', message= f'Seja bem vindo(a), {self.nome}\n\nSua empresa foi cadastrada no sistema SIGPAE e a partir ' + f'desse momento você terá acesso as suas funcionalidades.\n\nEfetue seu cadastro através do link ' + f'abaixo e acompanhe as suas solicitações.\n\n{url_configs("LOGIN_TERCEIRIZADAS", {})}' ) def atualiza_senha(self, senha, token): token_generator = PasswordResetTokenGenerator() if token_generator.check_token(self, token): self.set_password(senha) self.save() return True return False def criar_vinculo_administrador(self, instituicao, nome_perfil): perfil = Perfil.objects.get(nome=nome_perfil) Vinculo.objects.create(instituicao=instituicao, perfil=perfil, usuario=self, ativo=False) class Meta: ordering = ('-super_admin_terceirizadas', )
class LocalizedString(ExportModelOperationsMixin("localized_string"), models.Model): # type: ignore # noqa E501 string_id = models.CharField(max_length=128, unique=True) def __str__(self): return self.string_id
class Domain(ExportModelOperationsMixin('Domain'), models.Model): @staticmethod def _minimum_ttl_default(): return settings.MINIMUM_TTL_DEFAULT class RenewalState(models.IntegerChoices): IMMORTAL = 0 FRESH = 1 NOTIFIED = 2 WARNED = 3 created = models.DateTimeField(auto_now_add=True) name = models.CharField(max_length=191, unique=True, validators=validate_domain_name) owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='domains') published = models.DateTimeField(null=True, blank=True) minimum_ttl = models.PositiveIntegerField( default=_minimum_ttl_default.__func__) renewal_state = models.IntegerField(choices=RenewalState.choices, default=RenewalState.IMMORTAL) renewal_changed = models.DateTimeField(auto_now_add=True) _keys = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.pk is None and kwargs.get( 'renewal_state') is None and self.is_locally_registrable: self.renewal_state = Domain.RenewalState.FRESH @cached_property def public_suffix(self): try: public_suffix = psl.get_public_suffix(self.name) is_public_suffix = psl.is_public_suffix(self.name) except (Timeout, NoNameservers): public_suffix = self.name.rpartition('.')[2] is_public_suffix = ('.' not in self.name) # TLDs are public suffixes except psl_dns.exceptions.UnsupportedRule as e: # It would probably be fine to treat this as a non-public suffix (with the TLD acting as the # public suffix and setting both public_suffix and is_public_suffix accordingly). # However, in order to allow to investigate the situation, it's better not catch # this exception. For web requests, our error handler turns it into a 503 error # and makes sure admins are notified. raise e if is_public_suffix: return public_suffix # Take into account that any of the parent domains could be a local public suffix. To that # end, identify the longest local public suffix that is actually a suffix of domain_name. for local_public_suffix in settings.LOCAL_PUBLIC_SUFFIXES: has_local_public_suffix_parent = ( '.' + self.name).endswith('.' + local_public_suffix) if has_local_public_suffix_parent and len( local_public_suffix) > len(public_suffix): public_suffix = local_public_suffix return public_suffix def is_covered_by_foreign_zone(self): # Generate a list of all domains connecting this one and its public suffix. # If another user owns a zone with one of these names, then the requested # domain is unavailable because it is part of the other user's zone. private_components = self.name.rsplit(self.public_suffix, 1)[0].rstrip('.') private_components = private_components.split( '.') if private_components else [] private_domains = [ '.'.join(private_components[i:]) for i in range(0, len(private_components)) ] private_domains = [ f'{private_domain}.{self.public_suffix}' for private_domain in private_domains ] assert self.name == next(iter(private_domains), self.public_suffix) # Determine whether domain is covered by other users' zones return Domain.objects.filter( Q(name__in=private_domains) & ~Q(owner=self._owner_or_none)).exists() def covers_foreign_zone(self): # Note: This is not completely accurate: Ideally, we should only consider zones with identical public suffix. # (If a public suffix lies in between, it's ok.) However, as there could be many descendant zones, the accurate # check is expensive, so currently not implemented (PSL lookups for each of them). return Domain.objects.filter( Q(name__endswith=f'.{self.name}') & ~Q(owner=self._owner_or_none)).exists() def is_registrable(self): """ Returns False if the domain name is reserved, a public suffix, or covered by / covers another user's domain. Otherwise, True is returned. """ self.clean() # ensure .name is a domain name private_generation = self.name.count('.') - self.public_suffix.count( '.') assert private_generation >= 0 # .internal is reserved if f'.{self.name}'.endswith('.internal'): return False # Public suffixes can only be registered if they are local if private_generation == 0 and self.name not in settings.LOCAL_PUBLIC_SUFFIXES: return False # Disallow _acme-challenge.dedyn.io and the like. Rejects reserved direct children of public suffixes. reserved_prefixes = ( '_', 'autoconfig.', 'autodiscover.', ) if private_generation == 1 and any( self.name.startswith(prefix) for prefix in reserved_prefixes): return False # Domains covered by another user's zone can't be registered if self.is_covered_by_foreign_zone(): return False # Domains that would cover another user's zone can't be registered if self.covers_foreign_zone(): return False return True @property def keys(self): if not self._keys: self._keys = pdns.get_keys(self) return self._keys @property def touched(self): try: rrset_touched = max(updated for updated in self.rrset_set.values_list( 'touched', flat=True)) # If the domain has not been published yet, self.published is None and max() would fail return rrset_touched if not self.published else max( rrset_touched, self.published) except ValueError: # This can be none if the domain was never published and has no records (but there should be at least NS) return self.published @property def is_locally_registrable(self): return self.parent_domain_name in settings.LOCAL_PUBLIC_SUFFIXES @property def _owner_or_none(self): try: return self.owner except Domain.owner.RelatedObjectDoesNotExist: return None @property def parent_domain_name(self): return self._partitioned_name[1] @property def _partitioned_name(self): subname, _, parent_name = self.name.partition('.') return subname, parent_name or None def save(self, *args, **kwargs): self.full_clean(validate_unique=False) super().save(*args, **kwargs) def update_delegation(self, child_domain: Domain): child_subname, child_domain_name = child_domain._partitioned_name if self.name != child_domain_name: raise ValueError( 'Cannot update delegation of %s as it is not an immediate child domain of %s.' % (child_domain.name, self.name)) if child_domain.pk: # Domain real: set delegation child_keys = child_domain.keys if not child_keys: raise APIException( 'Cannot delegate %s, as it currently has no keys.' % child_domain.name) RRset.objects.create(domain=self, subname=child_subname, type='NS', ttl=3600, contents=settings.DEFAULT_NS) RRset.objects.create( domain=self, subname=child_subname, type='DS', ttl=300, contents=[ds for k in child_keys for ds in k['ds']]) metrics.get('desecapi_autodelegation_created').inc() else: # Domain not real: remove delegation for rrset in self.rrset_set.filter(subname=child_subname, type__in=['NS', 'DS']): rrset.delete() metrics.get('desecapi_autodelegation_deleted').inc() def delete(self): ret = super().delete() logger.warning(f'Domain {self.name} deleted (owner: {self.owner.pk})') return ret def __str__(self): return self.name class Meta: ordering = ('created', )
class Payment(ExportModelOperationsMixin("payment"), TimeStampedModel, LocationMixin): objects = PaymentManager() STATUS_WAITING = 0 STATUS_COMPLETED = 1 STATUS_ABANDONED = 2 STATUS_CANCELED = 3 STATUS_REFUSED = 4 STATUS_CHOICES = ( (STATUS_WAITING, "En attente"), (STATUS_COMPLETED, "Terminé"), (STATUS_ABANDONED, "Abandonné"), (STATUS_CANCELED, "Annulé"), (STATUS_REFUSED, "Refusé"), ) person = models.ForeignKey("people.Person", on_delete=models.SET_NULL, null=True, related_name="payments") email = models.EmailField("email", max_length=255) first_name = models.CharField("prénom", max_length=255) last_name = models.CharField("nom de famille", max_length=255) phone_number = PhoneNumberField("numéro de téléphone", null=True) type = models.CharField("type", choices=get_payment_choices(), max_length=255) mode = models.CharField(_("Mode de paiement"), max_length=70, null=False, blank=False) price = models.IntegerField(_("prix en centimes d'euros")) status = models.IntegerField("status", choices=STATUS_CHOICES, default=STATUS_WAITING) meta = JSONField(blank=True, default=dict) events = JSONField(_("Événements de paiement"), blank=True, default=list) def get_price_display(self): return "{} €".format(floatformat(self.price / 100, 2)) def get_mode_display(self): return (PAYMENT_MODES[self.mode].label if self.mode in PAYMENT_MODES else self.mode) def get_payment_url(self): return reverse("payment_page", args=[self.pk]) def can_retry(self): return (self.mode in PAYMENT_MODES and PAYMENT_MODES[self.mode].can_retry and self.status != self.STATUS_COMPLETED) def can_cancel(self): return (self.mode in PAYMENT_MODES and PAYMENT_MODES[self.mode].can_cancel and self.status != self.STATUS_COMPLETED) def html_full_address(self): return display_address(self) def description(self): from .actions import description_for_payment return description_for_payment(self) def __str__(self): return _("Paiement n°") + str(self.id) def __repr__(self): return "{klass}(email={email!r}, status={status!r}, type={type!r}, mode={mode!r}, price={price!r}".format( klass=self.__class__.__name__, email=self.email, status=self.status, type=self.type, mode=self.mode, price=self.price, ) class Meta: get_latest_by = "created"
class User(ExportModelOperationsMixin('User'), AbstractBaseUser): @staticmethod def _limit_domains_default(): return settings.LIMIT_USER_DOMAIN_COUNT_DEFAULT id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) email = CIEmailField( verbose_name='email address', unique=True, ) is_active = models.BooleanField(default=True) is_admin = models.BooleanField(default=False) created = models.DateTimeField(auto_now_add=True) limit_domains = models.IntegerField( default=_limit_domains_default.__func__, null=True, blank=True) objects = MyUserManager() USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] def get_full_name(self): return self.email def get_short_name(self): return self.email def __str__(self): return self.email # noinspection PyMethodMayBeStatic def has_perm(self, *_): """Does the user have a specific permission?""" # Simplest possible answer: Yes, always return True # noinspection PyMethodMayBeStatic def has_module_perms(self, *_): """Does the user have permissions to view the app `app_label`?""" # Simplest possible answer: Yes, always return True @property def is_staff(self): """Is the user a member of staff?""" # Simplest possible answer: All admins are staff return self.is_admin def activate(self): self.is_active = True self.save() def change_email(self, email): old_email = self.email self.email = email self.validate_unique() self.save() self.send_email('change-email-confirmation-old-email', recipient=old_email) def change_password(self, raw_password): self.set_password(raw_password) self.save() self.send_email('password-change-confirmation') def delete(self): pk = self.pk ret = super().delete() logger.warning(f'User {pk} deleted') return ret def send_email(self, reason, context=None, recipient=None): fast_lane = 'email_fast_lane' slow_lane = 'email_slow_lane' immediate_lane = 'email_immediate_lane' lanes = { 'activate': slow_lane, 'activate-with-domain': slow_lane, 'change-email': slow_lane, 'change-email-confirmation-old-email': fast_lane, 'password-change-confirmation': fast_lane, 'reset-password': fast_lane, 'delete-user': fast_lane, 'domain-dyndns': fast_lane, 'renew-domain': immediate_lane, } if reason not in lanes: raise ValueError( f'Cannot send email to user {self.pk} without a good reason: {reason}' ) context = context or {} content = get_template(f'emails/{reason}/content.txt').render(context) content += f'\nSupport Reference: user_id = {self.pk}\n' footer = get_template('emails/footer.txt').render() logger.warning( f'Queuing email for user account {self.pk} (reason: {reason}, lane: {lanes[reason]})' ) num_queued = EmailMessage( subject=get_template(f'emails/{reason}/subject.txt').render( context).strip(), body=content + footer, from_email=get_template('emails/from.txt').render(), to=[recipient or self.email], connection=get_connection(lane=lanes[reason], debug={ 'user': self.pk, 'reason': reason })).send() metrics.get('desecapi_messages_queued').labels( reason, self.pk, lanes[reason]).observe(num_queued) return num_queued
class Terceirizada(ExportModelOperationsMixin('terceirizada'), TemChaveExterna, Ativavel, TemIdentificadorExternoAmigavel, TemVinculos): # Tipo Empresa Choice ARMAZEM_DISTRIBUIDOR = 'ARMAZEM/DISTRIBUIDOR' FORNECEDOR_DISTRIBUIDOR = 'FORNECEDOR/DISTRIBUIDOR' # opção se faz necessária para atender o fluxo do alimentação terceirizada TERCEIRIZADA = 'TERCEIRIZADA' TIPO_EMPRESA_CHOICES = ( (ARMAZEM_DISTRIBUIDOR, 'Armazém/Distribuidor'), (FORNECEDOR_DISTRIBUIDOR, 'Fornecedor/Distribuidor'), (TERCEIRIZADA, 'Terceirizada'), ) # Tipo Alimento Choice TIPO_ALIMENTO_CONGELADOS = 'CONGELADOS_E_RESFRIADOS' TIPO_ALIMENTO_FLVO = 'FLVO' TIPO_ALIMENTO_PAES_E_BOLO = 'PAES_E_BOLO' TIPO_ALIMENTO_SECOS = 'SECOS' # opção se faz necessária para atender o fluxo do alimentação terceirizada TIPO_ALIMENTO_TERCEIRIZADA = 'TERCEIRIZADA' TIPO_ALIMENTO_NOMES = { TIPO_ALIMENTO_CONGELADOS: 'Congelados e resfriados', TIPO_ALIMENTO_FLVO: 'FLVO', TIPO_ALIMENTO_PAES_E_BOLO: 'Pães & bolos', TIPO_ALIMENTO_SECOS: 'Secos', TIPO_ALIMENTO_TERCEIRIZADA: 'Terceirizada', } TIPO_ALIMENTO_CHOICES = ( (TIPO_ALIMENTO_CONGELADOS, TIPO_ALIMENTO_NOMES[TIPO_ALIMENTO_CONGELADOS]), (TIPO_ALIMENTO_FLVO, TIPO_ALIMENTO_NOMES[TIPO_ALIMENTO_FLVO]), (TIPO_ALIMENTO_PAES_E_BOLO, TIPO_ALIMENTO_NOMES[TIPO_ALIMENTO_PAES_E_BOLO]), (TIPO_ALIMENTO_SECOS, TIPO_ALIMENTO_NOMES[TIPO_ALIMENTO_SECOS]), ) nome_fantasia = models.CharField('Nome fantasia', max_length=160, blank=True) razao_social = models.CharField('Razao social', max_length=160, blank=True) cnpj = models.CharField('CNPJ', validators=[MinLengthValidator(14)], max_length=14) representante_legal = models.CharField('Representante legal', max_length=160, blank=True) representante_telefone = models.CharField( 'Representante contato (telefone)', max_length=160, blank=True) representante_email = models.CharField('Representante contato (email)', max_length=160, blank=True) endereco = models.CharField('Endereco', max_length=160, blank=True) cep = models.CharField('CEP', max_length=8, blank=True) bairro = models.CharField('Bairro', max_length=150, blank=True) cidade = models.CharField('Cidade', max_length=150, blank=True) estado = models.CharField('Estado', max_length=150, blank=True) numero = models.CharField('Número', max_length=10, blank=True) complemento = models.CharField('Complemento', max_length=50, blank=True) eh_distribuidor = models.BooleanField('É distribuidor?', default=False) responsavel_nome = models.CharField('Responsável', max_length=160, blank=True) responsavel_email = models.CharField('Responsável contato (email)', max_length=160, blank=True) responsavel_cpf = models.CharField( max_length=11, blank=True, null=True, unique=True, # noqa DJ01 validators=[MinLengthValidator(11)]) responsavel_telefone = models.CharField('Responsável contato (telefone)', max_length=160, blank=True) responsavel_cargo = models.CharField('Responsável cargo', max_length=50, blank=True) # OBS.: Uso exclusivo do modulo de abastecimento(logistica). # Não tem relação com o processo do edital com associação de contrato a empresa. numero_contrato = models.CharField('Número de contrato', max_length=50, blank=True) tipo_empresa = models.CharField(choices=TIPO_EMPRESA_CHOICES, max_length=25, default=TERCEIRIZADA) tipo_alimento = models.CharField(choices=TIPO_ALIMENTO_CHOICES, max_length=25, default=TIPO_ALIMENTO_TERCEIRIZADA) criado_em = models.DateTimeField('Criado em', editable=False, auto_now_add=True) # TODO: criar uma tabela central (Instituição) para agregar Escola, DRE, Terc e CODAE??? # e a partir dai a instituição que tem contatos e endereço? # o mesmo para pessoa fisica talvez? contatos = models.ManyToManyField('dados_comuns.Contato', blank=True) @property def vinculos_que_podem_ser_finalizados(self): return self.vinculos.filter( Q(data_inicial=None, data_final=None, ativo=False) | # noqa W504 esperando ativacao Q(data_inicial__isnull=False, data_final=None, ativo=True) # noqa W504 ativo ).exclude(perfil__nome=NUTRI_ADMIN_RESPONSAVEL) def desvincular_lotes(self): for lote in self.lotes.all(): self.lotes.remove(lote) self.save() @property def quantidade_alunos(self): quantidade_total = 0 for lote in self.lotes.all(): quantidade_total += lote.quantidade_alunos return quantidade_total @property def nome(self): return self.nome_fantasia @property def nutricionistas(self): return self.nutricionistas @property def super_admin(self): vinculo = self.vinculos.filter( usuario__super_admin_terceirizadas=True).last() if vinculo: return vinculo.usuario return None @property def inclusoes_continuas_autorizadas(self): return InclusaoAlimentacaoContinua.objects.filter( escola__lote__in=self.lotes.all(), status__in=[ InclusaoAlimentacaoContinua.workflow_class.CODAE_AUTORIZADO, InclusaoAlimentacaoContinua.workflow_class. TERCEIRIZADA_TOMOU_CIENCIA ]) @property def inclusoes_normais_autorizadas(self): return GrupoInclusaoAlimentacaoNormal.objects.filter( escola__lote__in=self.lotes.all(), status__in=[ GrupoInclusaoAlimentacaoNormal.workflow_class.CODAE_AUTORIZADO, GrupoInclusaoAlimentacaoNormal.workflow_class. TERCEIRIZADA_TOMOU_CIENCIA ]) @property def inclusoes_continuas_reprovadas(self): return InclusaoAlimentacaoContinua.objects.filter( escola__lote__in=self.lotes.all(), status=InclusaoAlimentacaoContinua.workflow_class. CODAE_NEGOU_PEDIDO) @property def solicitacao_kit_lanche_avulsa_autorizadas(self): return SolicitacaoKitLancheAvulsa.objects.filter( escola__lote__in=self.lotes.all(), status__in=[ SolicitacaoKitLancheAvulsa.workflow_class.CODAE_AUTORIZADO, SolicitacaoKitLancheAvulsa.workflow_class. TERCEIRIZADA_TOMOU_CIENCIA ]) @property def inclusoes_normais_reprovadas(self): return GrupoInclusaoAlimentacaoNormal.objects.filter( escola__lote__in=self.lotes.all(), status=GrupoInclusaoAlimentacaoNormal.workflow_class. CODAE_NEGOU_PEDIDO) # TODO: talvez fazer um manager genérico pra fazer esse filtro def inclusoes_continuas_das_minhas_escolas_no_prazo_vencendo( self, filtro_aplicado): if filtro_aplicado == 'hoje': # TODO: rever filtro hoje que nao é mais usado inclusoes_continuas = InclusaoAlimentacaoContinua.objects else: # se o filtro nao for hoje, filtra o padrao inclusoes_continuas = InclusaoAlimentacaoContinua.vencidos return inclusoes_continuas.filter( status=InclusaoAlimentacaoContinua.workflow_class.CODAE_AUTORIZADO, escola__lote__in=self.lotes.all()) def inclusoes_continuas_das_minhas_escolas_no_prazo_limite( self, filtro_aplicado): if filtro_aplicado == 'daqui_a_7_dias': inclusoes_continuas = InclusaoAlimentacaoContinua.desta_semana else: inclusoes_continuas = InclusaoAlimentacaoContinua.objects # type: ignore return inclusoes_continuas.filter( status=InclusaoAlimentacaoContinua.workflow_class.CODAE_AUTORIZADO, escola__lote__in=self.lotes.all()) def inclusoes_continuas_das_minhas_escolas_no_prazo_regular( self, filtro_aplicado): if filtro_aplicado == 'daqui_a_30_dias': inclusoes_continuas = InclusaoAlimentacaoContinua.deste_mes elif filtro_aplicado == 'daqui_a_7_dias': inclusoes_continuas = InclusaoAlimentacaoContinua.desta_semana # type: ignore else: inclusoes_continuas = InclusaoAlimentacaoContinua.objects # type: ignore return inclusoes_continuas.filter( status=InclusaoAlimentacaoContinua.workflow_class.CODAE_AUTORIZADO, escola__lote__in=self.lotes.all()) def inclusoes_normais_das_minhas_escolas_no_prazo_vencendo( self, filtro_aplicado): if filtro_aplicado == 'hoje': # TODO: rever filtro hoje que nao é mais usado inclusoes_normais = GrupoInclusaoAlimentacaoNormal.objects else: inclusoes_normais = GrupoInclusaoAlimentacaoNormal.vencidos return inclusoes_normais.filter( status=InclusaoAlimentacaoContinua.workflow_class.CODAE_AUTORIZADO, escola__lote__in=self.lotes.all()) def inclusoes_normais_das_minhas_escolas_no_prazo_limite( self, filtro_aplicado): if filtro_aplicado == 'daqui_a_7_dias': inclusoes_normais = GrupoInclusaoAlimentacaoNormal.desta_semana else: inclusoes_normais = GrupoInclusaoAlimentacaoNormal.objects # type: ignore return inclusoes_normais.filter( status=InclusaoAlimentacaoContinua.workflow_class.CODAE_AUTORIZADO, escola__lote__in=self.lotes.all()) def inclusoes_normais_das_minhas_escolas_no_prazo_regular( self, filtro_aplicado): if filtro_aplicado == 'daqui_a_30_dias': inclusoes_normais = GrupoInclusaoAlimentacaoNormal.deste_mes elif filtro_aplicado == 'daqui_a_7_dias': inclusoes_normais = GrupoInclusaoAlimentacaoNormal.desta_semana # type: ignore else: inclusoes_normais = GrupoInclusaoAlimentacaoNormal.objects # type: ignore return inclusoes_normais.filter( status=InclusaoAlimentacaoContinua.workflow_class.CODAE_AUTORIZADO, escola__lote__in=self.lotes.all()) def alteracoes_cardapio_das_minhas_escolas_no_prazo_vencendo( self, filtro_aplicado): if filtro_aplicado == 'hoje': # TODO: rever filtro hoje que nao é mais usado alteracoes_cardapio = AlteracaoCardapio.objects else: alteracoes_cardapio = AlteracaoCardapio.vencidos return alteracoes_cardapio.filter( status=AlteracaoCardapio.workflow_class.CODAE_AUTORIZADO, escola__lote__in=self.lotes.all()) def alteracoes_cardapio_das_minhas_escolas_no_prazo_limite( self, filtro_aplicado): if filtro_aplicado == 'daqui_a_7_dias': alteracoes_cardapio = AlteracaoCardapio.desta_semana else: alteracoes_cardapio = AlteracaoCardapio.objects # type: ignore return alteracoes_cardapio.filter( status=AlteracaoCardapio.workflow_class.CODAE_AUTORIZADO, escola__lote__in=self.lotes.all()) def alteracoes_cardapio_das_minhas_escolas_no_prazo_regular( self, filtro_aplicado): if filtro_aplicado == 'daqui_a_30_dias': alteracoes_cardapio = AlteracaoCardapio.deste_mes elif filtro_aplicado == 'daqui_a_7_dias': alteracoes_cardapio = AlteracaoCardapio.desta_semana # type: ignore else: alteracoes_cardapio = AlteracaoCardapio.objects # type: ignore return alteracoes_cardapio.filter( status=AlteracaoCardapio.workflow_class.CODAE_AUTORIZADO, escola__lote__in=self.lotes.all()) def alteracoes_cardapio_das_minhas(self, filtro_aplicado): queryset = queryset_por_data(filtro_aplicado, AlteracaoCardapio) return queryset.filter(status__in=[ AlteracaoCardapio.workflow_class.CODAE_AUTORIZADO, AlteracaoCardapio.workflow_class.CODAE_QUESTIONADO ], escola__lote__in=self.lotes.all()) def alteracoes_cardapio_cei_das_minhas(self, filtro_aplicado): queryset = queryset_por_data(filtro_aplicado, AlteracaoCardapioCEI) return queryset.filter(status__in=[ AlteracaoCardapioCEI.workflow_class.CODAE_AUTORIZADO, AlteracaoCardapioCEI.workflow_class.CODAE_QUESTIONADO ], escola__lote__in=self.lotes.all()) def grupos_inclusoes_alimentacao_normal_das_minhas_escolas( self, filtro_aplicado): queryset = queryset_por_data(filtro_aplicado, GrupoInclusaoAlimentacaoNormal) return queryset.filter( status=AlteracaoCardapio.workflow_class.CODAE_AUTORIZADO, escola__lote__in=self.lotes.all()) def inclusoes_alimentacao_de_cei_das_minhas_escolas(self, filtro_aplicado): return self.filtra_solicitacoes_minhas_escolas_a_validar_por_data( filtro_aplicado, InclusaoAlimentacaoDaCEI) def inclusoes_alimentacao_continua_das_minhas_escolas( self, filtro_aplicado): queryset = queryset_por_data(filtro_aplicado, InclusaoAlimentacaoContinua) return queryset.filter( status=AlteracaoCardapio.workflow_class.CODAE_AUTORIZADO, escola__lote__in=self.lotes.all()) def suspensoes_alimentacao_das_minhas_escolas(self, filtro_aplicado): queryset = queryset_por_data(filtro_aplicado, GrupoSuspensaoAlimentacao) return queryset.filter( status=GrupoSuspensaoAlimentacao.workflow_class.INFORMADO, escola__lote__in=self.lotes.all()) @property def alteracoes_cardapio_autorizadas(self): return AlteracaoCardapio.objects.filter( escola__lote__in=self.lotes.all(), status__in=[ AlteracaoCardapio.workflow_class.CODAE_AUTORIZADO, AlteracaoCardapio.workflow_class.TERCEIRIZADA_TOMOU_CIENCIA ]) @property def alteracoes_cardapio_reprovadas(self): return AlteracaoCardapio.objects.filter( escola__lote__in=self.lotes.all(), status=AlteracaoCardapio.workflow_class.CODAE_NEGOU_PEDIDO) # # Inversão de dia de cardápio # def inversoes_cardapio_das_minhas_escolas(self, filtro_aplicado): if filtro_aplicado == 'daqui_a_7_dias': inversoes_cardapio = InversaoCardapio.desta_semana elif filtro_aplicado == 'daqui_a_30_dias': inversoes_cardapio = InversaoCardapio.deste_mes # type: ignore else: inversoes_cardapio = InversaoCardapio.objects # type: ignore return inversoes_cardapio.filter( escola__lote__in=self.lotes.all(), status=InversaoCardapio.workflow_class.CODAE_AUTORIZADO) @property def inversoes_cardapio_autorizadas(self): return InversaoCardapio.objects.filter( escola__lote__in=self.lotes.all(), status__in=[ InversaoCardapio.workflow_class.CODAE_AUTORIZADO, InversaoCardapio.workflow_class.TERCEIRIZADA_TOMOU_CIENCIA ]) # # Solicitação Unificada # def solicitacoes_unificadas_das_minhas_escolas(self, filtro_aplicado): if filtro_aplicado == 'daqui_a_7_dias': solicitacoes_unificadas = SolicitacaoKitLancheUnificada.desta_semana elif filtro_aplicado == 'daqui_a_30_dias': solicitacoes_unificadas = SolicitacaoKitLancheUnificada.deste_mes # type: ignore else: solicitacoes_unificadas = SolicitacaoKitLancheUnificada.objects # type: ignore return solicitacoes_unificadas.filter( escolas_quantidades__escola__lote__in=self.lotes.all(), status=SolicitacaoKitLancheUnificada.workflow_class. CODAE_AUTORIZADO).distinct() @property def solicitacoes_unificadas_autorizadas(self): return SolicitacaoKitLancheUnificada.objects.filter( escolas_quantidades__escola__lote__in=self.lotes.all(), status__in=[ SolicitacaoKitLancheUnificada.workflow_class.CODAE_AUTORIZADO, SolicitacaoKitLancheUnificada.workflow_class. TERCEIRIZADA_TOMOU_CIENCIA ]).distinct() def solicitacoes_kit_lanche_das_minhas_escolas_a_validar( self, filtro_aplicado): if filtro_aplicado == 'daqui_a_7_dias': solicitacoes_kit_lanche = SolicitacaoKitLancheAvulsa.desta_semana elif filtro_aplicado == 'daqui_a_30_dias': solicitacoes_kit_lanche = SolicitacaoKitLancheAvulsa.deste_mes # type: ignore else: solicitacoes_kit_lanche = SolicitacaoKitLancheAvulsa.objects # type: ignore return solicitacoes_kit_lanche.filter( escola__lote__in=self.lotes.all(), status__in=[ SolicitacaoKitLancheAvulsa.workflow_class.CODAE_AUTORIZADO, SolicitacaoKitLancheAvulsa.workflow_class.CODAE_QUESTIONADO ]) def solicitacoes_kit_lanche_cei_das_minhas_escolas_a_validar( self, filtro_aplicado): if filtro_aplicado == 'daqui_a_7_dias': solicitacoes_kit_lanche = SolicitacaoKitLancheCEIAvulsa.desta_semana elif filtro_aplicado == 'daqui_a_30_dias': solicitacoes_kit_lanche = SolicitacaoKitLancheCEIAvulsa.deste_mes # type: ignore else: solicitacoes_kit_lanche = SolicitacaoKitLancheCEIAvulsa.objects # type: ignore return solicitacoes_kit_lanche.filter( escola__lote__in=self.lotes.all(), status__in=[ SolicitacaoKitLancheCEIAvulsa.workflow_class.CODAE_AUTORIZADO, SolicitacaoKitLancheCEIAvulsa.workflow_class.CODAE_QUESTIONADO ]) def __str__(self): return f'{self.nome_fantasia}' class Meta: verbose_name = 'Terceirizada' verbose_name_plural = 'Terceirizadas'
class Dog(ExportModelOperationsMixin("dog"), Model): name = CharField(max_length=100, unique=True) breed = CharField(max_length=100, blank=True, null=True) age = PositiveIntegerField(blank=True, null=True)
class SubmissionCode(ExportModelOperationsMixin('submission_code'), models.Model): submission = models.ForeignKey(Submission, related_name='codes', on_delete=models.CASCADE) language = CodingLanguageField() code = models.TextField() summary = models.TextField(blank=True) score = models.IntegerField(null=True, blank=True) date_submitted = models.DateTimeField(default=timezone.now) date_corrected = models.DateTimeField(null=True, blank=True) celery_task_id = models.CharField(max_length=128, blank=True) result = MsgpackField(null=True, blank=True) def done(self): return not self.correctable() or self.score is not None def succeeded(self): return self.done() and self.score is not None and self.score > 0 def language_enum(self) -> Language: return Language[self.language] def correctable(self): return self.language_enum().correctable() def has_result(self): if not self.correctable(): return False if not self.done(): return False if not self.celery_task_id or not self.date_corrected: return False return self.result is not None def status(self): return _("Pending") if not self.done() else _("Corrected") def correction_results(self): if not self.celery_task_id or not self.date_corrected: return None if self.result is None: return None return Result.parse(self.submission.problem_model(), self.result) def request_printable(self): req = self.generate_request() req = rec_truncate(req, maxlen=100) return pprint.pformat(req, width=72) def result_printable(self): rep = rec_truncate(self.result, maxlen=100) return pprint.pformat(rep, width=72) def get_absolute_url(self): problem = self.submission.problem_model() return reverse('problems:submission', kwargs={ 'year': problem.challenge.year, 'type': problem.challenge.event_type.name, 'problem': problem.name, 'submission': self.id, }) def generate_request(self) -> dict: """Generate a camisole request for the SubmissionCode.""" problem = self.submission.problem_model() def build_tests(): for ref in problem.tests: yield {'name': ref.name, 'stdin': ref.stdin} language = self.language_enum() request = { 'lang': language.value.camisole_name, 'source': self.code, 'all_fatal': True, 'execute': problem.execution_limits(language), # FIXME: this is arbitrary 'compile': { 'mem': int(1e7), 'time': 20, 'wall-time': 60, 'fsize': 20000 }, 'tests': list(build_tests()), } return request def __str__(self): return "{} in {} (score: {})".format( self.submission, self.language_enum().name_display(), self.score if self.succeeded() else self.status()) class Meta: verbose_name = _("Submission code") verbose_name_plural = _("Submission codes") get_latest_by = 'date_submitted' ordering = ('-' + get_latest_by, '-pk')
class RSVP(ExportModelOperationsMixin("rsvp"), TimeStampedModel): """ Model that represents a RSVP for one person for an event. An additional field indicates if the person is bringing any guests with her """ STATUS_AWAITING_PAYMENT = "AP" STATUS_CONFIRMED = "CO" STATUS_CANCELED = "CA" STATUS_CHOICES = ( (STATUS_AWAITING_PAYMENT, _("En attente du paiement")), (STATUS_CONFIRMED, _("Inscription confirmée")), (STATUS_CANCELED, _("Inscription annulée")), ) objects = RSVPQuerySet.as_manager() person = models.ForeignKey( "people.Person", related_name="rsvps", on_delete=models.CASCADE, editable=False ) event = models.ForeignKey( "Event", related_name="rsvps", on_delete=models.CASCADE, editable=False ) guests = models.PositiveIntegerField( _("nombre d'invités supplémentaires"), default=0, null=False ) payment = models.OneToOneField( "payments.Payment", on_delete=models.SET_NULL, null=True, editable=False, related_name="rsvp", ) form_submission = models.OneToOneField( "people.PersonFormSubmission", on_delete=models.SET_NULL, null=True, editable=False, related_name="rsvp", ) guests_form_submissions = models.ManyToManyField( "people.PersonFormSubmission", related_name="guest_rsvp", through="IdentifiedGuest", ) status = models.CharField( _("Statut"), max_length=2, default=STATUS_CONFIRMED, choices=STATUS_CHOICES, blank=False, ) notifications_enabled = models.BooleanField( _("Recevoir les notifications"), default=True ) jitsi_meeting = models.ForeignKey( "JitsiMeeting", related_name="rsvps", null=True, blank=True, on_delete=models.SET_NULL, ) class Meta: verbose_name = "RSVP" verbose_name_plural = "RSVP" unique_together = ("event", "person") def __str__(self): info = "{person} --> {event} ({guests} invités)".format( person=self.person, event=self.event, guests=self.guests ) if self.status == RSVP.STATUS_AWAITING_PAYMENT or any( guest.status == RSVP.STATUS_AWAITING_PAYMENT for guest in self.identified_guests.all() ): info = info + " paiement(s) en attente" return info
class Category(ExportModelOperationsMixin('category'), models.Model): name = models.CharField(max_length=20) def category(self): return self.name
class Person( AbstractSubscriber, ExportModelOperationsMixin("person"), BaseAPIResource, NationBuilderResource, LocationMixin, ): """ Model that represents a physical person that signed as a JLM2017 supporter A person is identified by the email address he's signed up with. He is associated with permissions that determine what he can and cannot do with the API. He has an optional password, which will be only used to authenticate him with the API admin. """ objects = PersonManager() role = models.OneToOneField( "authentication.Role", on_delete=models.PROTECT, related_name="person", null=True, ) auto_login_salt = models.CharField(max_length=255, blank=True, default="") is_insoumise = models.BooleanField(_("Insoumis⋅e"), default=False) is_2022 = models.BooleanField(_("Soutien 2022"), default=False) MEMBRE_RESEAU_INCONNU = "I" MEMBRE_RESEAU_SOUHAITE = "S" MEMBRE_RESEAU_OUI = "O" MEMBRE_RESEAU_NON = "N" MEMBRE_RESEAU_EXCLUS = "E" MEMBRE_RESEAU_CHOICES = ( (MEMBRE_RESEAU_INCONNU, "Inconnu / Non pertinent"), (MEMBRE_RESEAU_SOUHAITE, "Souhaite faire partie du réseau des élus"), (MEMBRE_RESEAU_OUI, "Fait partie du réseau des élus"), (MEMBRE_RESEAU_NON, "Ne souhaite pas faire partie du réseau des élus"), (MEMBRE_RESEAU_EXCLUS, "Exclus du réseau"), ) membre_reseau_elus = models.CharField( _("Membre du réseau des élus"), max_length=1, blank=False, null=False, choices=MEMBRE_RESEAU_CHOICES, default=MEMBRE_RESEAU_INCONNU, help_text= "Pertinent uniquement si la personne a un ou plusieurs mandats électoraux.", ) NEWSLETTER_LFI = "LFI" NEWSLETTER_2022 = "2022" NEWSLETTER_2022_EXCEPTIONNEL = "2022_exceptionnel" NEWSLETTER_2022_EN_LIGNE = "2022_en_ligne" NEWSLETTER_2022_CHEZ_MOI = "2022_chez_moi" NEWSLETTER_2022_PROGRAMME = "2022_programme" NEWSLETTERS_CHOICES = ( (NEWSLETTER_LFI, "Lettre d'information de la France insoumise"), (NEWSLETTER_2022, "Lettre d'information NSP"), (NEWSLETTER_2022_EXCEPTIONNEL, "NSP : informations exceptionnelles"), (NEWSLETTER_2022_EN_LIGNE, "NSP actions en ligne"), (NEWSLETTER_2022_CHEZ_MOI, "NSP agir près de chez moi"), (NEWSLETTER_2022_PROGRAMME, "NSP processus programme"), ) newsletters = ChoiceArrayField( models.CharField(choices=NEWSLETTERS_CHOICES, max_length=255), default=list, blank=True, ) subscribed_sms = models.BooleanField( _("Recevoir les SMS d'information"), default=True, blank=True, help_text= _("Vous recevrez des SMS de la France insoumise comme des meeting près de chez vous ou des appels à volontaire..." ), ) event_notifications = models.BooleanField( _("Recevoir les notifications des événements"), default=True, blank=True, help_text=_( "Vous recevrez des messages quand les informations des évènements auxquels vous souhaitez participer" " sont mis à jour ou annulés."), ) group_notifications = models.BooleanField( _("Recevoir les notifications de mes groupes"), default=True, blank=True, help_text=_( "Vous recevrez des messages quand les informations du groupe change, ou quand le groupe organise des" " événements."), ) draw_participation = models.BooleanField( _("Participer aux tirages au sort"), default=False, blank=True, help_text= _("Vous pourrez être tiré⋅e au sort parmis les Insoumis⋅es pour participer à des événements comme la Convention." "Vous aurez la possibilité d'accepter ou de refuser cette participation." ), ) first_name = models.CharField(_("prénom"), max_length=255, blank=True) last_name = models.CharField(_("nom de famille"), max_length=255, blank=True) tags = models.ManyToManyField("PersonTag", related_name="people", blank=True) CONTACT_PHONE_UNVERIFIED = "U" CONTACT_PHONE_VERIFIED = "V" CONTACT_PHONE_PENDING = "P" CONTACT_PHONE_STATUS_CHOICES = ( (CONTACT_PHONE_UNVERIFIED, _("Non vérifié")), (CONTACT_PHONE_VERIFIED, _("Vérifié")), (CONTACT_PHONE_PENDING, _("En attente de validation manuelle")), ) contact_phone = ValidatedPhoneNumberField( _("Numéro de téléphone de contact"), blank=True, validated_field_name="contact_phone_status", unverified_value=CONTACT_PHONE_UNVERIFIED, ) contact_phone_status = models.CharField( _("Statut du numéro de téléphone"), choices=CONTACT_PHONE_STATUS_CHOICES, max_length=1, default=CONTACT_PHONE_UNVERIFIED, help_text=_( "Pour les numéros hors France métropolitaine, merci de les indiquer sous la forme internationale," " en les préfixant par '+' et le code du pays."), ) GENDER_FEMALE = "F" GENDER_MALE = "M" GENDER_OTHER = "O" GENDER_CHOICES = ( (GENDER_FEMALE, _("Femme")), (GENDER_MALE, _("Homme")), (GENDER_OTHER, _("Autre/Non défini")), ) gender = models.CharField(_("Genre"), max_length=1, blank=True, choices=GENDER_CHOICES) date_of_birth = models.DateField(_("Date de naissance"), null=True, blank=True) mandates = MandatesField(_("Mandats électoraux"), default=list, blank=True) meta = JSONField(_("Autres données"), default=dict, blank=True) commentaires = models.TextField( "Commentaires", blank=True, help_text= "ATTENTION : en cas de demande d'accès à ses données par la personne concernée par cette fiche, le" " contenu de ce champ lui sera communiqué. N'indiquez ici que des éléments factuels.", ) search = SearchVectorField("Données de recherche", editable=False, null=True) class Meta: verbose_name = _("personne") verbose_name_plural = _("personnes") ordering = ("-created", ) # add permission 'view' default_permissions = ("add", "change", "delete", "view") permissions = [( "select_person", "Peut lister pour sélectionner (dans un Select 2 par exemple)", )] indexes = ( GinIndex(fields=["search"], name="search_index"), models.Index(fields=["contact_phone"], name="contact_phone_index"), ) def save(self, *args, **kwargs): if self._state.adding: metrics.subscriptions.inc() return super().save(*args, **kwargs) def __str__(self): if self.first_name and self.last_name: return "{} {} <{}>".format(self.first_name, self.last_name, self.email) else: return self.email or "<pas d'email>" def __repr__(self): return f"{self.__class__.__name__}(pk={self.pk!r}, email={self.email})" @property def email(self): return self.primary_email.address if self.primary_email else "" @cached_property def primary_email(self): return self.emails.filter( _bounced=False).first() or self.emails.first() @property def bounced(self): return self.primary_email.bounced @bounced.setter def bounced(self, value): self.primary_email.bounced = value self.primary_email.save() @property def bounced_date(self): return self.primary_email.bounced_date @bounced_date.setter def bounced_date(self, value): self.primary_email.bounced_date = value self.primary_email.save() @property def subscribed(self): return self.NEWSLETTER_LFI in self.newsletters @subscribed.setter def subscribed(self, value): if value and not self.subscribed: self.newsletters.append(self.NEWSLETTER_LFI) if not value and self.subscribed: self.newsletters.remove(self.NEWSLETTER_LFI) def get_full_name(self): """ Returns the first_name plus the last_name, with a space in between. """ full_name = "%s %s" % (self.first_name, self.last_name) return full_name.strip() def get_short_name(self): "Returns the short name for the user." return self.first_name or self.email def get_greeting(self): if self.gender == self.GENDER_FEMALE: cher = "Chère" elif self.gender == self.GENDER_MALE: cher = "Cher" else: cher = "Chèr⋅e" if self.first_name and self.last_name: machin = self.get_full_name() elif self.gender == self.GENDER_FEMALE: machin = "insoumise" elif self.gender == self.GENDER_MALE: machin = "insoumis" else: machin = "insoumis⋅e" return f"{cher} {machin}" def add_email(self, email_address, primary=False, **kwargs): try: email = self.emails.get_by_natural_key(email_address) except PersonEmail.DoesNotExist: email = PersonEmail.objects.create_email(address=email_address, person=self, **kwargs) else: if email.person != self: raise IntegrityError( f"L'email '{email_address}' est déjà associé à une autre personne" ) email.bounced = kwargs.get("bounced", email.bounced) or False email.bounced_date = kwargs.get("bounced_date", email.bounced_date) email.save() if primary and email.person == self: self.set_primary_email(email) return email def set_primary_email(self, email_address): if isinstance(email_address, PersonEmail): email_instance = email_address else: email_instance = self.emails.get_by_natural_key(email_address) order = list(self.get_personemail_order()) order.remove(email_instance.id) order.insert(0, email_instance.id) self.set_personemail_order(order) self.__dict__["primary_email"] = email_instance def get_subscriber_status(self): if self.bounced: return AbstractSubscriber.STATUS_BOUNCED return AbstractSubscriber.STATUS_SUBSCRIBED def get_subscriber_email(self): return self.email def get_subscriber_data(self): data = super().get_subscriber_data() return { **data, "login_query": urlencode(generate_token_params(self)), "greeting": self.get_greeting(), "full_name": self.get_full_name(), "short_name": self.get_short_name(), "ancienne_region": self.ancienne_region, "region": self.region, "departement": self.departement, "city": self.location_city, "short_address": self.short_address, "short_location": self.short_location(), "full_address": self.html_full_address(), } def ensure_role_exists(self): """Crée un compte pour cette personne si aucun n'existe. Cette méthode n'a aucun effet si un compte existe déjà. """ if self.role_id is not None: return with transaction.atomic(): self.role = Role.objects.create( is_active=True, is_staff=False, is_superuser=False, type=Role.PERSON_ROLE, ) self.save(update_fields=["role"]) def has_perm(self, perm, obj=None): """Simple raccourci pour vérifier les permissions""" return self.role.has_perm(perm, obj)
class Usuario(ExportModelOperationsMixin('usuario'), SimpleEmailConfirmationUserMixin, CustomAbstractUser, TemChaveExterna): """Classe de autenticacao do django, ela tem muitos perfis.""" SME = 0 PREFEITURA = 1 TIPOS_EMAIL = ((SME, '@sme.prefeitura.sp.gov.br'), (PREFEITURA, '@prefeitura.sp.gov.br')) nome = models.CharField(_('name'), max_length=150) email = models.EmailField(_('email address'), unique=True) tipo_email = models.PositiveSmallIntegerField(choices=TIPOS_EMAIL, null=True, blank=True) registro_funcional = models.CharField( _('RF'), max_length=7, blank=True, null=True, unique=True, # noqa DJ01 validators=[MinLengthValidator(7)]) cargo = models.CharField(max_length=50, blank=True) # TODO: essew atributow deve pertencer somente a um model Pessoa cpf = models.CharField( _('CPF'), max_length=11, blank=True, null=True, unique=True, # noqa DJ01 validators=[MinLengthValidator(11)]) contatos = models.ManyToManyField('dados_comuns.Contato', blank=True) # TODO: esses atributos devem pertencer somente a um model Nutricionista super_admin_terceirizadas = models.BooleanField( 'É Administrador por parte das Terceirizadas?', default=False) # noqa crn_numero = models.CharField('Nutricionista crn', max_length=160, blank=True, null=True) # noqa DJ01 USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] # type: ignore def atualizar_cargo(self): cargo = self.cargos.filter(ativo=True).last() self.cargo = cargo.nome self.save() def desativa_cargo(self): cargo = self.cargos.last() if cargo is not None: cargo.finalizar_cargo() @property def vinculos(self): return self.vinculos @property def vinculo_atual(self): if self.vinculos.filter( Q(data_inicial=None, data_final=None, ativo=False) | # noqa W504 esperando ativacao Q(data_inicial__isnull=False, data_final=None, ativo=True) ).exists(): return self.vinculos.get( Q(data_inicial=None, data_final=None, ativo=False) | # noqa W504 esperando ativacao Q(data_inicial__isnull=False, data_final=None, ativo=True)) return None @property # noqa C901 def tipo_usuario(self): tipo_usuario = 'indefinido' if self.vinculo_atual: tipo_usuario = self.vinculo_atual.content_type.model if tipo_usuario == 'codae': if self.vinculo_atual.perfil.nome in [COORDENADOR_LOGISTICA]: tipo_usuario = 'coordenador_logistica' elif self.vinculo_atual.perfil.nome in [ COORDENADOR_GESTAO_ALIMENTACAO_TERCEIRIZADA, ADMINISTRADOR_GESTAO_ALIMENTACAO_TERCEIRIZADA ]: tipo_usuario = 'gestao_alimentacao_terceirizada' elif self.vinculo_atual.perfil.nome in [ COORDENADOR_GESTAO_PRODUTO, ADMINISTRADOR_GESTAO_PRODUTO ]: tipo_usuario = 'gestao_produto' elif self.vinculo_atual.perfil.nome in [ COORDENADOR_SUPERVISAO_NUTRICAO, ADMINISTRADOR_SUPERVISAO_NUTRICAO ]: tipo_usuario = 'supervisao_nutricao' else: tipo_usuario = 'dieta_especial' return tipo_usuario @property def pode_efetuar_cadastro(self): dados_usuario = EOLService.get_informacoes_usuario( self.registro_funcional) # noqa diretor_de_escola = False for dado in dados_usuario: if dado['cargo'] == 'DIRETOR DE ESCOLA': diretor_de_escola = True break vinculo_aguardando_ativacao = self.vinculo_atual.status == Vinculo.STATUS_AGUARDANDO_ATIVACAO return diretor_de_escola or vinculo_aguardando_ativacao def enviar_email_confirmacao(self): self.add_email_if_not_exists(self.email) content = { 'uuid': self.uuid, 'confirmation_key': self.confirmation_key } titulo = 'Confirmação de E-mail' template = 'email_cadastro_funcionario.html' dados_template = { 'titulo': titulo, 'link_cadastro': url_configs('CONFIRMAR_EMAIL', content), 'nome': self.nome } html = render_to_string(template, dados_template) self.email_user( subject='Confirme seu e-mail - SIGPAE', message='', template=template, dados_template=dados_template, html=html, ) def enviar_email_recuperacao_senha(self): token_generator = PasswordResetTokenGenerator() token = token_generator.make_token(self) content = {'uuid': self.uuid, 'confirmation_key': token} titulo = 'Recuperação de senha' conteudo = f'Clique neste link para criar uma nova senha no SIGPAE: {url_configs("RECUPERAR_SENHA", content)}' template = 'email_conteudo_simples.html' dados_template = {'titulo': titulo, 'conteudo': conteudo} html = render_to_string(template, dados_template) self.email_user( subject='Email de recuperação de senha - SIGPAE', message='', template=template, dados_template=dados_template, html=html, ) def enviar_email_administrador(self): self.add_email_if_not_exists(self.email) titulo = '[SIGPAE] Novo cadastro de empresa' template = 'email_cadastro_terceirizada.html' dados_template = { 'titulo': titulo, 'link_cadastro': url_configs('LOGIN_TERCEIRIZADAS', {}), 'nome': self.nome } html = render_to_string(template, dados_template) self.email_user(subject='[SIGPAE] Novo cadastro de empresa', message='', template=template, dados_template=dados_template, html=html) def atualiza_senha(self, senha, token): token_generator = PasswordResetTokenGenerator() if token_generator.check_token(self, token): self.set_password(senha) self.save() return True return False def criar_vinculo_administrador(self, instituicao, nome_perfil): perfil = Perfil.objects.get(nome=nome_perfil) Vinculo.objects.create(instituicao=instituicao, perfil=perfil, usuario=self, ativo=False) class Meta: ordering = ('-super_admin_terceirizadas', )
class Charge(ExportModelOperationsMixin('charge'), models.Model): name = models.CharField(max_length=255) amount = models.DecimalField(max_digits=10, decimal_places=2) raw_amount = models.CharField(max_length=1023) date = models.DateField() from_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="revenues") to_users = models.ManyToManyField(User, related_name="expenses") category = models.ForeignKey(Category, null=True, on_delete=models.SET_NULL) def __str__(self): return '{}: {}'.format(self.from_user.username, self.name) def clean(self): flat = self.from_user.profile.flat if any(user.profile.flat != flat for user in self.to_users.all()): raise ValidationError( 'You cannot charge user from different flat.') def save(self, *args, **kwargs): self.amount = float( ne.evaluate(self.raw_amount, local_dict={}, global_dict={}, truediv=True)) super(Charge, self).save(*args, **kwargs) @staticmethod def get_revenues(year, month, user): return Charge.objects.filter(date__year=year, date__month=month, from_user=user) @staticmethod def get_revenue(id, user): return Charge.objects.filter(id=id, from_user=user).first() @staticmethod def get_expenses(year, month, user): return Charge.objects.filter(date__year=year, date__month=month, to_users=user) @staticmethod def get_expense(id, user): return Charge.objects.filter(id=id, to_users=user).first() @staticmethod def get_summary(year, month, user): revenues = Charge.get_revenues(year, month, user) expenses = Charge.get_expenses(year, month, user) summary = {} def get_or_add(user_to_get): if user_to_get.id not in summary.keys(): summary[user_to_get.id] = {'user': user_to_get, 'amount': 0} return summary[user_to_get.id] for revenue in revenues: rev_users = revenue.to_users.all() for rev_user in rev_users: entry = get_or_add(rev_user) entry['amount'] -= revenue.amount / len(rev_users) for expense in expenses: entry = get_or_add(expense.from_user) entry['amount'] += expense.amount / expense.to_users.count() for user in User.objects.filter(profile__flat=user.profile.flat): get_or_add(user) return summary
class ItemImage(ExportModelOperationsMixin('item_image'), models.Model): item = models.ForeignKey(Item, related_name='images', on_delete=models.CASCADE) image = models.ImageField(upload_to='%Y/%m/%d') thumbnail = models.ImageField(upload_to='%Y/%m/%d') def save(self, *args, **kwargs): self.correct_image() super(ItemImage, self).save(*args, **kwargs) def correct_image(self): try: # Open image and set some variables img = Image.open(self.image) img_format = img.format max_size = (800, 800) thumb_size = (150, 150) file_ext = os_path.splitext(self.image.name)[1] random_str = token_urlsafe(12) img_name = f'{self.item.id}-{random_str}.%s{file_ext}' img_bytes = BytesIO() thumb_bytes = BytesIO() # If image contains exif check for orientation and rotate for orientation in ExifTags.TAGS.keys(): if ExifTags.TAGS[orientation] == 'Orientation': img_exif = img._getexif() if img_exif: if orientation in img_exif: image_orientation = img_exif[orientation] if image_orientation == 3: img = img.rotate(180, expand=True) if image_orientation == 6: img = img.rotate(-90, expand=True) if image_orientation == 8: img = img.rotate(90, expand=True) # Store a copy of the image for thumbnail thumb = img.copy() # TODO - task this and just present page without until it's done # Correct the image size to safe maximums img.thumbnail(max_size, Image.ANTIALIAS) img.save(img_bytes, format=img_format, quality=80) self.image = InMemoryUploadedFile(img_bytes, 'ImageField', img_name % 'full', self.image.file.content_type, img.size, self.image.file.charset) # Create thumbnail from image thumb.thumbnail(thumb_size, Image.ANTIALIAS) thumb.save(thumb_bytes, format=img_format, quality=80) self.thumbnail = InMemoryUploadedFile(thumb_bytes, 'ImageField', img_name % 'thumbnail', self.image.file.content_type, img.size, self.image.file.charset) thumb.close() img.close() except: raise Exception('Unable to correct image size') def __str__(self): return f'{self.id} - {self.item.name} - {self.id}'
class Domain(ExportModelOperationsMixin('Domain'), models.Model): created = models.DateTimeField(auto_now_add=True) name = models.CharField(max_length=191, unique=True, validators=validate_domain_name) owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='domains') published = models.DateTimeField(null=True, blank=True) minimum_ttl = models.PositiveIntegerField(default=get_minimum_ttl_default) _keys = None @classmethod def is_registrable(cls, domain_name: str, user: User): """ Returns False in any of the following cases: (a) the domain name is under .internal, (b) the domain_name appears on the public suffix list, (c) the domain is descendant to a zone that belongs to any user different from the given one, unless it's parent is a public suffix, either through the Internet PSL or local settings. Otherwise, True is returned. """ if domain_name != domain_name.lower(): raise ValueError if f'.{domain_name}'.endswith('.internal'): return False try: public_suffix = psl.get_public_suffix(domain_name) is_public_suffix = psl.is_public_suffix(domain_name) except (Timeout, NoNameservers): public_suffix = domain_name.rpartition('.')[2] is_public_suffix = ('.' not in domain_name) # TLDs are public suffixes except psl_dns.exceptions.UnsupportedRule as e: # It would probably be fine to treat this as a non-public suffix (with the TLD acting as the # public suffix and setting both public_suffix and is_public_suffix accordingly). # However, in order to allow to investigate the situation, it's better not catch # this exception. For web requests, our error handler turns it into a 503 error # and makes sure admins are notified. raise e if not is_public_suffix: # Take into account that any of the parent domains could be a local public suffix. To that # end, identify the longest local public suffix that is actually a suffix of domain_name. # Then, override the global PSL result. for local_public_suffix in settings.LOCAL_PUBLIC_SUFFIXES: has_local_public_suffix_parent = ( '.' + domain_name).endswith('.' + local_public_suffix) if has_local_public_suffix_parent and len( local_public_suffix) > len(public_suffix): public_suffix = local_public_suffix is_public_suffix = (public_suffix == domain_name) if is_public_suffix and domain_name not in settings.LOCAL_PUBLIC_SUFFIXES: return False # Generate a list of all domains connecting this one and its public suffix. # If another user owns a zone with one of these names, then the requested # domain is unavailable because it is part of the other user's zone. private_components = domain_name.rsplit(public_suffix, 1)[0].rstrip('.') private_components = private_components.split( '.') if private_components else [] private_components += [public_suffix] private_domains = [ '.'.join(private_components[i:]) for i in range(0, len(private_components) - 1) ] assert is_public_suffix or domain_name == private_domains[0] # Deny registration for non-local public suffixes and for domains covered by other users' zones user = user if not isinstance(user, AnonymousUser) else None return not cls.objects.filter( Q(name__in=private_domains) & ~Q(owner=user)).exists() @property def keys(self): if not self._keys: self._keys = pdns.get_keys(self) return self._keys @property def is_locally_registrable(self): return self.parent_domain_name in settings.LOCAL_PUBLIC_SUFFIXES @property def parent_domain_name(self): return self._partitioned_name[1] @property def _partitioned_name(self): subname, _, parent_name = self.name.partition('.') return subname, parent_name or None def save(self, *args, **kwargs): self.full_clean(validate_unique=False) super().save(*args, **kwargs) def update_delegation(self, child_domain: Domain): child_subname, child_domain_name = child_domain._partitioned_name if self.name != child_domain_name: raise ValueError( 'Cannot update delegation of %s as it is not an immediate child domain of %s.' % (child_domain.name, self.name)) if child_domain.pk: # Domain real: set delegation child_keys = child_domain.keys if not child_keys: raise APIException( 'Cannot delegate %s, as it currently has no keys.' % child_domain.name) RRset.objects.create(domain=self, subname=child_subname, type='NS', ttl=3600, contents=settings.DEFAULT_NS) RRset.objects.create( domain=self, subname=child_subname, type='DS', ttl=300, contents=[ds for k in child_keys for ds in k['ds']]) metrics.get('desecapi_autodelegation_created').inc() else: # Domain not real: remove delegation for rrset in self.rrset_set.filter(subname=child_subname, type__in=['NS', 'DS']): rrset.delete() metrics.get('desecapi_autodelegation_deleted').inc() def delete(self): ret = super().delete() logger.warning(f'Domain {self.name} deleted (owner: {self.owner.pk})') return ret def __str__(self): return self.name class Meta: ordering = ('created', )
class InclusaoAlimentacaoContinua( ExportModelOperationsMixin('inclusao_continua'), IntervaloDeDia, Descritivel, TemChaveExterna, DiasSemana, FluxoAprovacaoPartindoDaEscola, CriadoPor, TemIdentificadorExternoAmigavel, CriadoEm, Logs, TemPrioridade, SolicitacaoForaDoPrazo, TemTerceirizadaConferiuGestaoAlimentacao): # TODO: noralizar campo de Descritivel: descricao -> observacao DESCRICAO = 'Inclusão de Alimentação Contínua' outro_motivo = models.CharField('Outro motivo', blank=True, max_length=500) motivo = models.ForeignKey(MotivoInclusaoContinua, on_delete=models.DO_NOTHING) escola = models.ForeignKey('escola.Escola', on_delete=models.DO_NOTHING, related_name='inclusoes_alimentacao_continua') observacao = models.CharField('Observação', blank=True, max_length=1000) objects = models.Manager() # Manager Padrão desta_semana = InclusoesDeAlimentacaoContinuaDestaSemanaManager() deste_mes = InclusoesDeAlimentacaoContinuaDesteMesManager() vencidos = InclusoesDeAlimentacaoContinuaVencidaDiasManager() @property def data(self): data = self.data_inicial if self.data_final < data: data = self.data_final return data @classmethod def get_solicitacoes_rascunho(cls, usuario): inclusoes_continuas = cls.objects.filter( criado_por=usuario, status=InclusaoAlimentacaoContinua.workflow_class.RASCUNHO) return inclusoes_continuas @property def quantidades_periodo(self): return self.quantidades_por_periodo @property def template_mensagem(self): template = TemplateMensagem.objects.get( tipo=TemplateMensagem.INCLUSAO_ALIMENTACAO_CONTINUA) template_troca = { '@id': self.id_externo, '@criado_em': str(self.criado_em), '@criado_por': str(self.criado_por), '@status': str(self.status), # TODO: verificar a url padrão do pedido '@link': 'http://teste.com', } corpo = template.template_html for chave, valor in template_troca.items(): corpo = corpo.replace(chave, valor) return template.assunto, corpo def salvar_log_transicao(self, status_evento, usuario, **kwargs): justificativa = kwargs.get('justificativa', '') resposta_sim_nao = kwargs.get('resposta_sim_nao', False) LogSolicitacoesUsuario.objects.create( descricao=str(self), status_evento=status_evento, solicitacao_tipo=LogSolicitacoesUsuario. INCLUSAO_ALIMENTACAO_CONTINUA, usuario=usuario, uuid_original=self.uuid, justificativa=justificativa, resposta_sim_nao=resposta_sim_nao) def __str__(self): return f'de {self.data_inicial} até {self.data_final} para {self.escola} para {self.dias_semana_display()}' class Meta: verbose_name = 'Inclusão de alimentação contínua' verbose_name_plural = 'Inclusões de alimentação contínua' ordering = ['data_inicial']