コード例 #1
0
ファイル: models.py プロジェクト: GenRockeR/securemail
class MassSender(models.Model):
    class Meta:
        verbose_name = u'рассылку'
        verbose_name_plural = u'рассылки'

    sender_name_desc = models.CharField(u'Имя отправителя',
                                        max_length=100,
                                        null=True)
    sender_name = models.EmailField(u'Отправить от', max_length=50)
    bcc_list = MultiEmailField(
        u'Кому:',
        null=True,
        help_text=u'Добавьте список для рассылки по одному адресу в строке')
    mail_subject = models.CharField(u'Тема письма', max_length=200, null=True)
    mailbody = RichTextField(u'Тело письма')
    mail_attachment = models.FileField(u'Вложение',
                                       validators=[validate_file_extension],
                                       storage=OverwriteStorage())
    pub_date = models.DateTimeField(u'Дата добавления рассылки',
                                    editable=False,
                                    default=datetime.now())
    status = models.CharField(u'Статус рассылки',
                              max_length=1,
                              choices=STATUS_CHOICES,
                              editable=False,
                              default='n')
    send_date = models.DateTimeField(u'Дата отправки рассылки',
                                     editable=False,
                                     blank=True,
                                     null=True)

    def __unicode__(self):
        return self.sender_name
コード例 #2
0
class Priority(models.Model):
    myclass = models.ForeignKey(Class,
                                related_name='priority',
                                on_delete=models.CASCADE)
    to = MultiEmailField()
    expiry_date = models.DateField()
    expiry_time = models.TimeField()
    mylist = models.TextField(blank=True, null=True)

    def set_list(self, element):
        if self.mylist:
            self.mylist = self.mylist + element + ","
        else:
            self.mylist = element + ","

    def remove_list(self, element):
        if self.mylist:
            mylist = self.mylist.split(",")
            if element in mylist:
                element_raplace = element + ','
                self.mylist = self.mylist.replace(element_raplace, '')
                print(self.mylist)

    def get_list(self):
        if self.mylist:
            return self.mylist.split(",")[:-1]
        else:
            return []

    def len_list(self):
        if self.mylist:
            return len(self.mylist.split(",")) - 1
        else:
            return 0
コード例 #3
0
class ScientificSociety(TranslatableModel):
    #region           -----Translation-----
    translations = TranslatedFields(
        description=HTMLField(verbose_name=_("Description"),
                              blank=False,
                              default=""),
        sub_title=CharField(verbose_name=_("Sub title"),
                            max_length=400,
                            blank=False,
                            default=""))
    #endregion

    #region           -----Information-----
    phone = CharField(max_length=20,
                      blank=False,
                      verbose_name=_("Phone number"))
    emails = MultiEmailField()
    #endregion

    #region            -----Relations-----
    staff = ManyToManyField("StaffCathedra", verbose_name=_("Staff"))

    #endregion

    #region            -----Metadata-----
    class Meta(object):
        verbose_name_plural = _("Scientific Societies")
        verbose_name = _("Scientific Society")

    #endregion

    def __str__(self) -> str:
        return self.sub_title
コード例 #4
0
ファイル: models.py プロジェクト: Gatekeeper2019/Crossroad
class newvisitor(models.Model):
    STATE_CHOICES =(("NA", "NA"),("Andhra Pradesh","Andhra Pradesh"),("Arunachal Pradesh ","Arunachal Pradesh "),("Assam","Assam"),("Bihar","Bihar"),("Chhattisgarh","Chhattisgarh"),("Goa","Goa"),("Gujarat","Gujarat"),("Haryana","Haryana"),("Himachal Pradesh","Himachal Pradesh"),("Jammu and Kashmir ","Jammu and Kashmir "),("Jharkhand","Jharkhand"),("Karnataka","Karnataka"),("Kerala","Kerala"),("Madhya Pradesh","Madhya Pradesh"),("Maharashtra","Maharashtra"),("Manipur","Manipur"),("Meghalaya","Meghalaya"),("Mizoram","Mizoram"),("Nagaland","Nagaland"),("Odisha","Odisha"),("Punjab","Punjab"),("Rajasthan","Rajasthan"),("Sikkim","Sikkim"),("Tamil Nadu","Tamil Nadu"),("Telangana","Telangana"),("Tripura","Tripura"),("Uttar Pradesh","Uttar Pradesh"),("Uttarakhand","Uttarakhand"),("West Bengal","West Bengal"),("Andaman and Nicobar Islands","Andaman and Nicobar Islands"),("Chandigarh","Chandigarh"),("Dadra and Nagar Haveli","Dadra and Nagar Haveli"),("Daman and Diu","Daman and Diu"),("Lakshadweep","Lakshadweep"),("National Capital Territory of Delhi","National Capital Territory of Delhi"),("Puducherry","Puducherry"))
    DISTRICT_CHOICES =(("NA", "NA"),("Bengaluru", "Bengaluru"),("Chennai", "Chennai"),("Delhi", "Delhi"),("Dharwad", "Dharwad"),("Dimapur", "Dimapur"),("Imphal","Imphal"),("Ernakulam","Ernakulam"),
        ("Kolkata", "Kolkata"),("Mumbai", "Mumbai"),("Mysore", "Mysore"), ("Nagpur", "Nagpur"),("Pune", "Pune"),
        ("Shillong", "Shillong"),("Visakhapatnam", "Visakhapatnam"),
        ("Vellore","Vellore"),("Chittoor", "Chittoor"))
    #timothy_CHOICES = (("True", "Yes"),("False","No") )
    salutation =(("Mr","Mr"), ("Mrs","Mrs"), ("Ms","Ms"), ("Master","Master"), ("NA","NA"))
    status = (("New Visitor","New Visitor"), ("0","0"))
    how = (("God","God"),("Friend","Friend"),("Google","Google"), ("NA","NA"))
    connect = (("NA","NA"),("Would like to talk","Would like to talk"),("Exploring faith","Exploring faith"),
        ("Would like a visit","Would like a visit"), ("Interested in membership","Interested in membership"), 
        ("New to this area","New to this area"), ("Put on mailing list","Put on mailing list") )
    id = models.AutoField(primary_key=True,)
    status = models.CharField(max_length=100, null=True, choices=status)
    date_visited = models.DateField(null=True)
    salutation = models.CharField(max_length=100, null=True,  choices=salutation)
    first_name = models.CharField(max_length=100, null=True, blank=True)
    last_name = models.CharField(max_length=100, null=True, blank=True)
    country_of_Citizenship = CountryField(null=True, blank_label='(select country)')
    state = models.CharField(max_length=200, null=True,  choices = STATE_CHOICES)
    district = models.CharField(max_length=200, null=True,  choices = DISTRICT_CHOICES)
    location= models.CharField(max_length=100, null=True, blank=True)
    pin_code = models.IntegerField(null=True,)
    email = MultiEmailField(blank=True, default='', null=True,help_text='email')
    phone =  PhoneField(blank=True, default='', null=True,help_text='Contact phone number')
    How_did_you_know_about_us = models.CharField(max_length=100, null=True, choices=how)
    choices = models.CharField(max_length=100, null=True, choices=connect)
    Note = models.TextField(blank=True, null=True,)
コード例 #5
0
class Transaction(models.Model):
	type_requiriment = models.CharField(max_length=150)
	priority = models.CharField(max_length=2)
	request = models.ForeignKey(Request, on_delete=models.CASCADE)
	description = models.TextField()
	copy = MultiEmailField()
	attached = models.FileField(upload_to='folder/')
	request_status = models.BooleanField(null = True)
	deadline = models.DateTimeField()
コード例 #6
0
ファイル: models.py プロジェクト: genesisOTV/Hackathon2019
class Event(models.Model):
    title = models.CharField(max_length=256)
    description = models.TextField()
    amount = models.IntegerField(default=0)
    members = MultiEmailField(blank=True)
    created_at = models.DateTimeField(default=datetime.now, blank=True)
    end_date = models.DateTimeField(default=datetime.now, blank=True)

    def get_absolute_url(self):
        return reverse('mainview')
コード例 #7
0
ファイル: customer.py プロジェクト: oucsaw/silver
class Customer(BaseBillingEntity):
    emails = MultiEmailField(blank=True, null=True)
    payment_due_days = models.PositiveIntegerField(
        default=PAYMENT_DUE_DAYS,
        help_text='Due days for generated proforma/invoice.')
    consolidated_billing = models.BooleanField(
        default=False, help_text='A flag indicating consolidated billing.')
    customer_reference = models.CharField(
        max_length=256,
        blank=True,
        null=True,
        validators=[validate_reference],
        help_text="It's a reference to be passed between silver and clients. "
        "It usually points to an account ID.")
    sales_tax_number = models.CharField(max_length=64, blank=True, null=True)
    sales_tax_percent = models.DecimalField(
        max_digits=4,
        decimal_places=2,
        null=True,
        blank=True,
        validators=[MinValueValidator(0.0)],
        help_text="Whenever to add sales tax. "
        "If null, it won't show up on the invoice.")
    sales_tax_name = models.CharField(
        max_length=64,
        null=True,
        blank=True,
        help_text="Sales tax name (eg. 'sales tax' or 'VAT').")

    def __init__(self, *args, **kwargs):
        super(Customer, self).__init__(*args, **kwargs)
        company_field = self._meta.get_field("company")
        company_field.help_text = "The company to which the bill is issued."

    def clean(self):
        if (self.sales_tax_number and is_vat_number_format_valid(
                self.sales_tax_number, self.country) is False):
            raise ValidationError(
                {'sales_tax_number': 'The sales tax number is not valid.'})

    def get_archivable_field_values(self):
        base_fields = super(Customer, self).get_archivable_field_values()
        customer_fields = [
            'customer_reference', 'consolidated_billing', 'payment_due_days',
            'sales_tax_number', 'sales_tax_percent', 'emails'
        ]
        fields_dict = {
            field: getattr(self, field, '')
            for field in customer_fields
        }
        base_fields.update(fields_dict)
        return base_fields
コード例 #8
0
class Report(models.Model):
    # TODO: Remove hard coded report types
    # Currently report types are hard coded. According to these types
    # which template should be used to generate report can be decide.
    REPORT_TYPE = (
        ("00", "Report type 1"),
        ("01", "Report type 2"),
    )

    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    name = models.CharField(max_length=250)
    created_date = models.DateTimeField(auto_now_add=True)

    # FEATURE: Allow user to create user groups, i.e. default user groups,
    # admin user group etc.
    # FEATURE: Allow user to directly add user groups(group of emails)
    emails_to_send = MultiEmailField()
コード例 #9
0
ファイル: models.py プロジェクト: medcv/flintlock
class Form(models.Model):
    """
    A data-driver way of creating a form. the schema describes the fields, their types, order, etc of the form
    """

    COLOR_CHOICES = (
        ('#001F3F', 'Navy'),
        ('#0074D9', 'Blue'),
        ('#7FDBFF', 'Aqua'),
        ('#39CCCC', 'Teal'),
        ('#3D9970', 'Olive'),
        ('#2ECC40', 'Green'),
        ('#01FF70', 'Lime'),
        ('#FFDC00', 'Yellow'),
        ('#FF851B', 'Orange'),
        ('#FF4136', 'Red'),
        ('#F012BE', 'Fuchsia'),
        ('#B10DC9', 'Purple'),
        ('#85144B', 'Maroon'),
        ('#FFFFFF', 'White'),
        ('#DDDDDD', 'Silver'),
        ('#AAAAAA', 'Gray'),
        ('#111111', 'Black')
    )

    user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True)
    timestamp = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)
    schema = models.TextField(null=False, blank=False)
    color = models.CharField(max_length=10, choices=COLOR_CHOICES, blank=True, null=True, verbose_name='Map icon color')
    emails = MultiEmailField(null=True, blank=True)

    @cached_property
    def title(self):
        try:
            schema = json.loads(self.schema)
            return schema.get('title', '<no title>')
        except TypeError:
            return

    def __unicode__(self):
        schema_dict = json.loads(self.schema)
        title = '<no title>'
        if 'title' in schema_dict:
            title = schema_dict['title']
        return u'id={}, {}'.format(self.id, title)
コード例 #10
0
ファイル: models.py プロジェクト: Gatekeeper2019/Crossroad
class NewChurches(models.Model):
    DISTRICT_CHOICES =(("NA", "NA"),("Bengaluru", "Bengaluru"),("Chandigarh", "Chandigarh"),("Chennai", "Chennai"),("Delhi", "Delhi"),("Dharwad", "Dharwad"),("Dimapur", "Dimapur"),("Imphal","Imphal"),("Ernakulam","Ernakulam"),
        ("Kolkata", "Kolkata"),("Mumbai", "Mumbai"),("Mysore", "Mysore"), ("Nagpur", "Nagpur"),("Pune", "Pune"),
        ("Shillong", "Shillong"),("Katmandu", "Katmandu"),("Visakhapatnam", "Visakhapatnam"),("Vellore","Vellore"),("Chittoor", "Chittoor"))
    status = (("Alive","Alive"), ("Defunct","Defunct"))

    id =models.AutoField(primary_key=True,)
    status = models.CharField(max_length=100, null=True, choices=status)
    country = CountryField(null=True, blank_label='(select country)')
    city = models.CharField(max_length=200, null=True,  choices = DISTRICT_CHOICES)
    location= models.CharField(max_length=100, null=True, blank=True)
    pin_code = models.IntegerField(null=True,)
    Planter = models.CharField(max_length=100, null=True, blank=True)
    email = MultiEmailField(blank=True, default='', null=True,help_text='email')
    phone =  PhoneField(blank=True, default='', null=True,help_text='Contact phone number')
    Year_Established = models.DateField(null=True)
    Summary = models.TextField(blank=True, null=True,)
    image = models.ImageField(upload_to ='media/profile_pic/', null=True, blank=True)
コード例 #11
0
class Message(models.Model):
    sender = models.ForeignKey(User,
                               on_delete=models.SET_NULL,
                               null=True,
                               related_name='sender')
    emails = MultiEmailField(blank=True, null=True)
    url = models.UUIDField(default=uuid4, editable=False)
    title = models.CharField(max_length=200, blank=True)
    text = models.TextField(blank=True, null=True)
    send_date = models.DateTimeField('date to send', blank=True, null=True)
    pub_date = models.DateTimeField('date message created', auto_now_add=True)
    show = models.BooleanField(default=True)
    show_template = models.BooleanField(default=False)

    def __str__(self):
        return str(self.title)

    class Meta:
        verbose_name = 'Message'
        verbose_name_plural = 'Messages'
コード例 #12
0
ファイル: models.py プロジェクト: juniverjair/ingreso
class Reunion(models.Model):
    id = models.AutoField(primary_key=True)
    autor = models.ForeignKey(User, on_delete=models.CASCADE, editable=False)
    cedula = models.CharField(max_length=10, null=True, blank=True)
    nombre = models.CharField(max_length=45)
    razon_social = models.CharField(max_length=45, null=True)
    correo = MultiEmailField(
        help_text=
        "A esta lista de email se enviará el QR de acceso, sepárelos mediante ENTER"
    )
    horario = models.DateTimeField()
    sala = models.ForeignKey(Sala, on_delete=models.CASCADE)
    activa = models.BooleanField(default=True)
    descripcion = models.TextField(max_length=100)
    create_at = models.DateTimeField(default=datetime.now(), editable=False)
    edited_at = models.DateTimeField(editable=False)

    def __str__(self):
        return self.nombre

    class Meta:
        verbose_name_plural = "Reuniones"

    def save(self, *args, **kwargs):
        self.edited_at = datetime.now()
        super().save(*args, **kwargs)

        if self.activa == True:
            mensaje = str(
                self.horario.strftime("%Y-%m-%d %H:%M:%S")
            ) + ''' en el piso #''' + str(self.sala.piso) + ''', sala ''' + str(
                self.sala
            ) + '''. Recuerde llegar a tiempo, caso contrario no podrá ingresar. El código QR adjunto le permitirá su ingreso 10 minutos antes o después de la hora indicada.'''  # + str(self.cantidad_personas) + ''' persona(s).'''

            try:
                enviar_correo(mensaje, self.correo, str(self.id))
            except:
                enviar_correo(
                    "ERROR SE INFORMA QUE SE PRESENTA UN ERROR EN LA REUNION CON CODIGO "
                    + str(self.id), ["*****@*****.**"], "test")
コード例 #13
0
class Contract(models.Model):
    contract_text = models.TextField(null=True, blank=True)
    tenant = models.ForeignKey(Profile,
                               on_delete=models.CASCADE,
                               related_name="tenant",
                               null=True)
    tenants = MultiEmailField(null=True, blank=True)
    pending = models.BooleanField()
    owner_approved = models.BooleanField(null=True, blank=True)
    start_date = models.DateField()
    end_date = models.DateField()
    review_made = models.BooleanField(default=False)

    def __str__(self):
        return '' + str(self.start_date.strftime("%d.%m.%Y")) + ' - ' + \
               str(self.end_date.strftime("%d.%m.%Y"))

    def calculate_tenants(self):
        return len(self.tenants)

    def get_main_tenant(self):
        return self.tenants[0]
コード例 #14
0
ファイル: models.py プロジェクト: Gatekeeper2019/Crossroad
class crossroadmember(models.Model):
    STATE_CHOICES =(("NA", "NA"),("Andhra Pradesh","Andhra Pradesh"),("Arunachal Pradesh ","Arunachal Pradesh "),("Assam","Assam"),("Bihar","Bihar"),("Chhattisgarh","Chhattisgarh"),("Goa","Goa"),("Gujarat","Gujarat"),("Haryana","Haryana"),("Himachal Pradesh","Himachal Pradesh"),("Jammu and Kashmir ","Jammu and Kashmir "),("Jharkhand","Jharkhand"),("Karnataka","Karnataka"),("Kerala","Kerala"),("Madhya Pradesh","Madhya Pradesh"),("Maharashtra","Maharashtra"),("Manipur","Manipur"),("Meghalaya","Meghalaya"),("Mizoram","Mizoram"),("Nagaland","Nagaland"),("Odisha","Odisha"),("Punjab","Punjab"),("Rajasthan","Rajasthan"),("Sikkim","Sikkim"),("Tamil Nadu","Tamil Nadu"),("Telangana","Telangana"),("Tripura","Tripura"),("Uttar Pradesh","Uttar Pradesh"),("Uttarakhand","Uttarakhand"),("West Bengal","West Bengal"),("Andaman and Nicobar Islands","Andaman and Nicobar Islands"),("Chandigarh","Chandigarh"),("Dadra and Nagar Haveli","Dadra and Nagar Haveli"),("Daman and Diu","Daman and Diu"),("Lakshadweep","Lakshadweep"),("National Capital Territory of Delhi","National Capital Territory of Delhi"),("Puducherry","Puducherry"))
    DISTRICT_CHOICES =(("NA", "NA"),("Bengaluru", "Bengaluru"),("Chennai", "Chennai"),("Delhi", "Delhi"),("Dharwad", "Dharwad"),("Dimapur", "Dimapur"),("Imphal","Imphal"),("Ernakulam","Ernakulam"),
        ("Kolkata", "Kolkata"),("Mumbai", "Mumbai"),("Mysore", "Mysore"), ("Nagpur", "Nagpur"),("Pune", "Pune"),
        ("Shillong", "Shillong"),("Visakhapatnam", "Visakhapatnam"),
        ("Vellore","Vellore"),("Chittoor", "Chittoor"))
    
    #timothy_CHOICES = (("True", "Yes"),("False","No") )
    salutation =(("Mr","Mr"), ("Mrs","Mrs"), ("Ms","Ms"), ("Master","Master"))
    status = (("Covenant Member","Covenant Member"), ("Regular Visitor","Regular Visitor"), ("left","left"))
    id = models.AutoField(primary_key=True,)
    status = models.CharField(max_length=100, null=True, choices=status)
    date_joined = models.DateField(null=True)
    salutation = models.CharField(max_length=100, null=True,  choices=salutation)
    first_name = models.CharField(max_length=100, null=True, blank=True)
    last_name = models.CharField(max_length=100, null=True, blank=True)
    profile_pic = models.ImageField(upload_to ='media/profile_pic/', null=True, blank=True)
    country_of_Citizenship = CountryField(null=True, blank_label='(select country)')
    state = models.CharField(max_length=200, null=True,  choices = STATE_CHOICES)
    district = models.CharField(max_length=200, null=True,  choices = DISTRICT_CHOICES)
    pin_code = models.IntegerField(null=True,)
    email = MultiEmailField(blank=True, default='', null=True,help_text='email')
    phone =  PhoneField(blank=True, default='', null=True,help_text='Contact phone number')
    Note = models.TextField(blank=True, null=True,)
コード例 #15
0
class Repository(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    organization = models.ForeignKey(Organization,
                                     related_name='repositories',
                                     null=True,
                                     on_delete=models.PROTECT)
    name = BleachField(max_length=DEFAULT_CHAR_LENGTH)
    # Not exposed to API
    # Attached to the RepoType enum. Doesn't need to be a BleachField since not exposed to user.
    type = EnumField(RepoType)
    description = BleachField()
    token = BleachField(blank=True, max_length=DEFAULT_CHAR_LENGTH)
    # Console-like logging
    log = BleachField(default="")
    # Space separated list of topics as seen on github
    topics = BleachField(blank=True)
    # TODO: Use choices rather than free text? -djc 2018-02-26
    language = BleachField(max_length=DEFAULT_CHAR_LENGTH)
    address = BleachField(blank=True, max_length=DEFAULT_CHAR_LENGTH)
    # Users with these email addresses can access the repo, or any user
    # if empty
    allowed_emails = MultiEmailField()

    class Meta:
        verbose_name_plural = 'Repositories'

    def __str__(self):
        return self.name

    def allow_access(self, email):
        """Should we allow access to a user with the given email?"""
        ret = False

        if self.allowed_emails:
            if email:
                ret = email in self.allowed_emails
        else:  # if no list of emails, allow
            ret = True

        return ret

    def get_benchmarks(self, uloc: int, core: bool) -> list:
        """ Return a list of benchmarks for this repo, based on predicted or actual lines of code """
        generator = benchmarks.BenchmarkGenerator('src/scoring/resources/')
        BenchmarkDescription.objects.filter(repository=self).delete()
        description = BenchmarkDescription.objects.create(repository=self,
                                                          date=timezone.now())
        try:
            a, b = generator.get_benchmarks(uloc, self.language, self.topics,
                                            core, description)
            return a, b, description
        except RuntimeError as error:
            # We expect these values to always work, and assume giving a default is the right thing to do :P
            # -djc 2018-04-05
            logger.error("Unable to create benchmarks: " + str(error))

    def reset_benchmarks(self, uloc: int, core: bool) -> list:
        """Clear out existing benchmarks attached to this repository and create
        new ones based on predicted or actual lines of code"""
        Benchmark.objects.filter(repository=self).delete()

        new_benchmarks = []

        benchmark_list, grade_percentiles, description = self.get_benchmarks(
            uloc, core)
        for benchmark_dict in benchmark_list:
            benchmark_dict['repository'] = self
            bm = Benchmark.objects.create(**benchmark_dict)
            new_benchmarks.append(bm)

        return new_benchmarks, grade_percentiles, description
コード例 #16
0
class Cathedra(TranslatableModel):
    YEAR_CHOICES = [(r, r) for r in reversed(range(1800,
                                                   date.today().year + 1))]

    #region           -----Translation-----
    translations = TranslatedFields(
        description=HTMLField(verbose_name=_("Description"),
                              blank=True,
                              default=""),
        title=CharField(default="",
                        unique=True,
                        verbose_name=_("Title"),
                        blank=False,
                        max_length=100),
        history=HTMLField(blank=True,
                          default="",
                          verbose_name=_("History of cathedra")),
        goal=TextField(blank=False, default="", verbose_name=_("Goal")))
    #endregion

    #region           -----Information-----
    catalog_of_disciplines = URLField(
        blank=True, null=True, verbose_name=_("Catalog of disciplines link"))
    educational_programs = URLField(
        blank=True, null=True, verbose_name=_("Educational programs link"))
    emblem = ImageField(upload_to="cathedras/emblems",
                        blank=False,
                        verbose_name=_("Emblem"),
                        default="")
    emails = MultiEmailField(blank=True, null=True)
    phone = CharField(max_length=20,
                      blank=True,
                      verbose_name=_("Phone number"))
    year = IntegerField(verbose_name=_("Year"),
                        choices=YEAR_CHOICES,
                        null=True)
    #endregion

    #region            -----Relation-----
    material_technical_base = ManyToManyField(
        MaterialBaseNode,
        verbose_name=_("Material-technical base"),
        blank=True)
    faculty = ForeignKey("Faculty",
                         blank=True,
                         null=True,
                         on_delete=CASCADE,
                         verbose_name=_("Faculty"),
                         related_name="cathedras")

    gallery = ForeignKey(Gallery,
                         blank=True,
                         null=True,
                         on_delete=SET_NULL,
                         verbose_name=_("Career guidance"),
                         related_name="cathedras")

    staff = ManyToManyField("StaffCathedra", verbose_name=_("Staff"))

    #endregion

    #region            -----Metadata-----
    class Meta(object):
        verbose_name_plural = _("Departments")
        verbose_name = _("Departments")

    #endregion

    #region         -----Internal Methods-----
    def get_absolute_url(self) -> str:
        """@return link to model"""
        return reverse('cathedra', kwargs={'cathedra_id': self.pk})

    def searching_fields(self) -> List[str]:
        """@return translated fields"""
        return [
            "translations__title", "translations__description",
            "translations__goal", "translations__history"
        ]

    def __str__(self) -> str:
        """@return title of cathedra"""
        return self.title
コード例 #17
0
ファイル: models.py プロジェクト: mtrx1337/kubeportal
class User(AbstractUser):
    '''
    A portal user.
    '''
    state = FSMField(
        default=UserState.NEW,
        verbose_name="Cluster access",
        help_text="The state of the cluster access approval workflow.",
        choices=user_state_list)
    approval_id = models.UUIDField(default=uuid.uuid4,
                                   editable=False,
                                   null=True)
    answered_by = models.ForeignKey(
        'User',
        help_text="Which user approved the cluster access for this user.",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        verbose_name="Approved by")
    comments = models.CharField(max_length=150,
                                default="",
                                null=True,
                                blank=True)
    alt_mails = MultiEmailField(default=None, null=True, blank=True)
    portal_groups = models.ManyToManyField(
        PortalGroup,
        blank=True,
        verbose_name='Groups',
        help_text="The user groups this account belongs to.",
        related_name='members')

    service_account = models.ForeignKey(
        KubernetesServiceAccount,
        related_name="portal_users",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        verbose_name="Kubernetes account",
        help_text="Kubernetes namespace + service account of this user.")

    def k8s_namespace(self):
        '''
        Property used by the API serializer.
        '''
        if self.service_account:
            return self.service_account.namespace
        else:
            return None

    def can_subauth(self, webapp):
        user_groups_with_this_app = self.portal_groups.filter(
            can_web_applications__in=[webapp.pk])
        allowed = user_groups_with_this_app.count() > 0
        if allowed:
            logger.debug(
                "Subauth allowed for app {} with user {} due to membership in groups"
                .format(webapp, self))
            return True
        else:
            logger.debug(
                "Subauth not allowed for user {}, none of the user groups allows the app {}"
                .format(self, webapp))
            return False

    def has_access_approved(self):
        result = (self.state == UserState.ACCESS_APPROVED
                  and self.service_account)
        logger.debug("Access approved for user {0}: {1}".format(self, result))
        return result

    def has_access_rejected(self):
        result = (self.state == UserState.ACCESS_REJECTED)
        logger.debug("Access rejected for user {0}: {1}".format(self, result))
        return result

    def has_access_requested(self):
        result = (self.state == UserState.ACCESS_REQUESTED
                  and self.approval_id)
        logger.debug("Access requested by user {0}: {1}".format(self, result))
        return result

    @transition(field=state,
                source=[
                    UserState.NEW, UserState.ACCESS_REQUESTED,
                    UserState.ACCESS_APPROVED, UserState.ACCESS_REJECTED
                ],
                target=UserState.ACCESS_REQUESTED)
    def send_access_request(self, request, administrator=None):
        '''
        Requests approval for cluster access.

        Note: The user object must be saved by the caller, to reflect the state change,
              when this method returns "True".

        Note: The parameter administrator is an optional argument which can be
              used to send an access request to a specific super user.
        '''
        self.approval_id = uuid.uuid4()

        html_mail = render_to_string(
            'mail_access_request.html', {
                'branding':
                settings.BRANDING,
                'user':
                str(self),
                'approve_url':
                request.build_absolute_uri(
                    reverse('admin:access_approve',
                            kwargs={'approval_id': self.approval_id})),
                'reject_url':
                request.build_absolute_uri(
                    reverse('admin:access_reject',
                            kwargs={'approval_id': self.approval_id}))
            })

        text_mail = strip_tags(html_mail)
        subject = 'Request for access to "{0}"'.format(settings.BRANDING)

        cluster_admins = []

        if administrator:
            cluster_admins.append(User.objects.get(username=administrator))
            logger.info(
                F"Sending access request from '{self.username}' to '{administrator}'"
            )
        else:
            for admin in User.objects.filter(is_superuser=True):
                cluster_admins.append(admin)
            logger.info(
                F"Sending access request from '{self.username}' to all administrators"
            )

        cluster_admin_emails = [admin.email for admin in cluster_admins]

        try:
            send_mail(subject,
                      text_mail,
                      settings.ADMIN_EMAIL,
                      cluster_admin_emails,
                      html_message=html_mail,
                      fail_silently=False)

            logger.debug('Sent email to admins about access request from ' +
                         str(self))
            return True
        except Exception:
            logger.exception(
                'Problem while sending email to admins about access request from '
                + str(self))
            return False

    @transition(field=state, source='*', target=UserState.ACCESS_REJECTED)
    def reject(self, request):
        '''
        Answers a approval request with "rejected".
        The state transition happens automatically, an additional information
        is send to the denied user by email.

        Note: The user object must be saved by the caller, to reflect the state change,
              when this method returns "True".
        '''
        messages.add_message(
            request, messages.INFO,
            "Access request for '{0}' was rejected.".format(self))
        logger.info("Access for user '{0}' was rejected by user '{1}'.".format(
            self, request.user))

        html_mail = render_to_string('mail_access_rejected.html', {
            'branding': settings.BRANDING,
            'user': str(self),
        })

        text_mail = strip_tags(html_mail)
        subject = 'Your request for access to "{0}"'.format(settings.BRANDING)

        try:
            if self.email:
                send_mail(subject,
                          text_mail,
                          settings.ADMIN_EMAIL, [
                              self.email,
                          ],
                          html_message=html_mail,
                          fail_silently=False)
                logger.debug(
                    "Sent email to user '{0}' about access request rejection".
                    format(self))
        except Exception:
            logger.exception(
                "Problem while sending email to user '{0}' about access request rejection"
                .format(self))

        # overwrite old approval, if URL is used again by the admins
        self.service_account = None
        self.answered_by = request.user
        return True

    @transition(field=state, source='*', target=UserState.ACCESS_APPROVED)
    def approve(self, request, new_svc):
        '''
        Answers a approval request with "approved".
        The state transition happens automatically.

        Note: The user object must be saved by the caller, to reflect the state change,
              when this method returns "True".
        '''
        self.service_account = new_svc
        self.answered_by = request.user
        messages.info(
            request,
            "User '{0}' is now assigned to existing Kubernetes namespace '{1}'."
            .format(self, new_svc.namespace))
        logger.info(
            "User '{0}' was assigned to existing Kubernetes namespace {1} by {2}."
            .format(self, new_svc.namespace, request.user))

        html_mail = render_to_string('mail_access_approved.html', {
            'branding': settings.BRANDING,
            'user': str(self),
        })

        text_mail = strip_tags(html_mail)
        subject = 'Your request for access to the {0} Kubernetes Cluster'.format(
            settings.BRANDING)

        try:
            if self.email:
                send_mail(subject,
                          text_mail,
                          settings.ADMIN_EMAIL, [
                              self.email,
                          ],
                          html_message=html_mail,
                          fail_silently=False)
                logger.debug(
                    "Sent email to user '{0}' about access request approval".
                    format(self))
                messages.info(request,
                              "User '{0}' informed by eMail.".format(self))
        except Exception:
            logger.exception(
                "Problem while sending email to user '{0}' about access request approval"
                .format(self))
            messages.error(
                request,
                "Problem while sending information to user '{0}' by eMail.".
                format(self))
        return True

    def approve_link(self):
        if self.has_access_requested():
            uri = reverse('admin:access_approve',
                          kwargs={'approval_id': self.approval_id})
            return mark_safe(
                '<a class="grp-button" href="{0}" target="blank">Approve request</a>'
                .format(uri))
        else:
            return None

    approve_link.short_description = ('Action')

    @property
    def token(self):
        from kubeportal.kubernetes import get_token
        try:
            return get_token(self.service_account)
        except Exception:
            return None
コード例 #18
0
class User(AbstractUser):
    """
    A portal user.
    """
    NEW = 'NEW'
    ACCESS_REQUESTED = 'ACCESS_REQUESTED'
    ACCESS_REJECTED = 'ACCESS_REJECTED'
    ACCESS_APPROVED = 'ACCESS_APPROVED'

    state = models.CharField(default=NEW, 
                             verbose_name="Cluster access",
                             max_length=100,
                             help_text="The state of the cluster access approval workflow.", 
                             choices=USER_STATES)
    approval_id = models.UUIDField(
        default=uuid.uuid4, editable=False, null=True)
    answered_by = models.ForeignKey(
        'User', help_text="Which user approved the cluster access for this user.", on_delete=models.SET_NULL, null=True,
        blank=True, verbose_name="Approved by")
    comments = models.CharField(
        max_length=150, default="", null=True, blank=True)
    alt_mails = MultiEmailField(default=None, null=True, blank=True)
    portal_groups = models.ManyToManyField(
        'PortalGroup', blank=True, verbose_name='Groups', help_text="The user groups this account belongs to.",
        related_name='members')

    service_account = models.ForeignKey(
        'KubernetesServiceAccount', related_name="portal_users", on_delete=models.SET_NULL, null=True, blank=True,
        verbose_name="Kubernetes account", help_text="Kubernetes namespace + service account of this user.")

    def user_id(self):
        """
        Used as property by the API serializer.
        """
        return self.pk

    def all_emails(self):
        """
        Used as property by the API serializer.
        """
        return [self.email, *self.alt_mails]

    def webapps(self):
        """
        Used as property by the API serializer.
        """
        return self.web_applications(False)        


    def k8s_accounts(self):
        """
        Used as property by the API serializer.
        Prepared for future support of multiple namespaces per user.
        """
        if self.service_account:
            return KubernetesServiceAccount.objects.filter(pk = self.service_account.pk)
        else:
            return KubernetesServiceAccount.objects.none()


    def k8s_namespaces(self):
        """
        Used as property by the API serializer.
        Prepared for future support of multiple namespaces per user.
        """
        if self.service_account:
            return KubernetesNamespace.objects.filter(pk = self.service_account.namespace.pk)
        else:
            return KubernetesNamespace.objects.none()

    def k8s_namespace_names(self):
        """
        Used as property by the API serializer.
        Prepared for future support of multiple namespaces per user.
        """
        if self.service_account:
            return KubernetesNamespace.objects.filter(pk = self.service_account.namespace.pk).values_list('name', flat=True)
        else:
            return KubernetesNamespace.objects.none()


    def has_namespace(self, namespace):
        """
        Check if this user has permissions for this Kubernetes namespace.
        This decision is based on the configuration in the portal,
        not on the RBAC situation in the cluster.
        Prepared for future support of multiple namespaces per user.
        """
        if self.service_account:
            return namespace == self.service_account.namespace.name
        else:
            return False


    def web_applications(self, include_invisible):
        """
        Returns a querset for the list of web applications allowed for this
        user.
        """
        from kubeportal.models.webapplication import WebApplication
        if include_invisible:
            return WebApplication.objects.filter(portal_groups__members__pk=self.pk).distinct()
        else:
            return WebApplication.objects.filter(portal_groups__members__pk=self.pk, link_show=True).distinct()

    def k8s_pods(self):
        """
        Returns a list of K8S pods for this user.
        """
        if self.service_account:
            return kubernetes_api.get_namespaced_pods(self.service_account.namespace.name, self)
        else:
            logger.error(f"Cannot determine list of pods for user {self}, since she has no service account attached.")
            return []

    def k8s_deployments(self):
        """
        Returns a list of K8 deployments for this user.
        """
        if self.service_account:
            return kubernetes_api.get_namespaced_deployments(self.service_account.namespace.name, self)
        else:
            logger.error(f"Cannot determine list of deployments for user {self}, since she has no service account attached.")
            return []

    def k8s_services(self):
        """
        Returns a list of K8S services for this user.
        """
        if self.service_account:
            return kubernetes_api.get_namespaced_services(self.service_account.namespace.name, self)
        else:
            logger.error(f"Cannot determine list of services for user {self}, since she has no service account attached.")
            return []

    def k8s_ingresses(self):
        """
        Returns a list of K8S ingresses for this user.
        """
        if self.service_account:
            return kubernetes_api.get_namespaced_ingresses(self.service_account.namespace.name, self)
        else:
            logger.error(f"Cannot determine list of ingresses for user {self}, since she has no service account attached.")
            return []


    def can_subauth(self, webapp):
        user_groups_with_this_app = self.portal_groups.filter(can_web_applications__in=[webapp.pk])
        allowed = user_groups_with_this_app.count() > 0
        if allowed:
            # Prevent event storm
            # logger.debug("Subauth allowed for app {} with user {} due to membership in groups".format(webapp, self))
            return True
        else:
            logger.debug(
                "Subauth not allowed for user {}, none of the user groups allows the app {}".format(self, webapp))
            return False

    def has_access_approved(self):
        result = (self.state == User.ACCESS_APPROVED and self.service_account)
        return result

    def has_access_rejected(self):
        result = (self.state == User.ACCESS_REJECTED)
        return result

    def has_access_requested(self):
        result = (self.state == User.ACCESS_REQUESTED and self.approval_id)
        return result

    @classmethod
    def inactive_users(cls):
        """
        returns a list of users that haven't logged in x months ago.
        """
        # 30 days (1 month times the amount of months we look behind)
        x_months_ago = timezone.now() - timezone.timedelta(days=30 * settings.LAST_LOGIN_MONTHS_AGO)
        result = list(cls.objects.filter(last_login__lte=x_months_ago))
        return result

    def send_access_request(self, request, administrator=None):
        """
        Requests approval for cluster access.

        Note: The parameter administrator is an optional argument which can be
              used to send an access request to a specific super user.
        """
        approve_url = request.build_absolute_uri(reverse('admin:access_approve', kwargs={'approval_id': self.approval_id}))
        reject_url  = request.build_absolute_uri(reverse('admin:access_reject',  kwargs={'approval_id': self.approval_id}))

        html_mail = render_to_string('mail_access_request.html', {'branding': settings.BRANDING,
                                                                  'user': str(self),
                                                                  'approve_url': approve_url,
                                                                  'reject_url': reject_url
                                                                  })

        text_mail = strip_tags(html_mail)
        subject = 'Request for access to "{0}"'.format(settings.BRANDING)

        cluster_admins = []

        if administrator:
            cluster_admins.append(User.objects.get(username=administrator))
            logger.info(F"Sending access request from '{self.username}' to '{administrator}'")
            logger.debug(F"Approval URL: {approve_url}")
        else:
            for admin in User.objects.filter(is_superuser=True):
                cluster_admins.append(admin)
            logger.info(F"Sending access request from '{self.username}' to all administrators")

        cluster_admin_emails = [admin.email for admin in cluster_admins]

        try:
            send_mail(subject, text_mail, settings.ADMIN_EMAIL,
                      cluster_admin_emails, html_message=html_mail, fail_silently=False)

            logger.debug(
                'Sent email to admins about access request from ' + str(self))

            self.state = self.ACCESS_REQUESTED
            self.answered_by = None
            self.save()
            return True
        except Exception:
            logger.exception(
                'Problem while sending email to admins about access request from ' + str(self))
            return False

    def reject(self, request):
        """
        Answers a approval request with "rejected".
        The state transition happens automatically, an additional information
        is send to the denied user by email.
        """
        self.service_account = None
        self.state = self.ACCESS_REJECTED
        self.answered_by = request.user
        self.save()

        messages.add_message(request, messages.INFO,
                             "Access request for '{0}' was rejected.".format(self))
        logger.info("Access for user '{0}' was rejected by user '{1}'.".format(
            self, request.user))

        html_mail = render_to_string('mail_access_rejected.html', {'branding': settings.BRANDING,
                                                                   'user': str(self),
                                                                   })

        text_mail = strip_tags(html_mail)
        subject = 'Your request for access to "{0}"'.format(settings.BRANDING)

        try:
            if self.email:
                send_mail(subject, text_mail, settings.ADMIN_EMAIL, [
                    self.email, ], html_message=html_mail, fail_silently=False)
                logger.debug(
                    "Sent email to user '{0}' about access request rejection".format(self))
        except Exception:
            logger.exception(
                "Problem while sending email to user '{0}' about access request rejection".format(self))

        return True


    def approve(self, request, new_svc):
        """
        Answers a approval request with "approved".
        """
        self.state = self.ACCESS_APPROVED
        self.service_account = new_svc
        self.answered_by = request.user
        self.save()

        messages.info(
            request,
            "User '{0}' is now assigned to existing Kubernetes namespace '{1}'.".format(self, new_svc.namespace))
        logger.info(
            "User '{0}' was assigned to existing Kubernetes namespace {1} by {2}.".format(self, new_svc.namespace,
                                                                                          request.user))

        html_mail = render_to_string('mail_access_approved.html', {'branding': settings.BRANDING,
                                                                   'user': str(self),
                                                                   })

        text_mail = strip_tags(html_mail)
        subject = 'Your request for access to the {0} Kubernetes Cluster'.format(
            settings.BRANDING)

        try:
            if self.email:
                send_mail(subject, text_mail, settings.ADMIN_EMAIL, [
                    self.email, ], html_message=html_mail, fail_silently=False)
                logger.debug(
                    "Sent email to user '{0}' about access request approval".format(self))
                messages.info(
                    request, "User '{0}' informed by eMail.".format(self))
        except Exception:
            logger.exception(
                "Problem while sending email to user '{0}' about access request approval".format(self))
            messages.error(
                request, "Problem while sending information to user '{0}' by eMail.".format(self))

        return True

    def approve_link(self):
        if self.has_access_requested():
            uri = reverse('admin:access_approve', kwargs={
                'approval_id': self.approval_id})
            return mark_safe('<a class="grp-button" href="{0}" target="blank">Approve request</a>'.format(uri))
        else:
            return None

    approve_link.short_description = 'Action'

    @property
    def token(self):
        from kubeportal.k8s.kubernetes_api import get_token
        try:
            return get_token(self.service_account)
        except Exception:
            return None
コード例 #19
0
class Staff(TranslatableModel):
    #region           -----Translation-----
    translations = TranslatedFields(
        second_name=CharField(max_length=40,
                              blank=False,
                              verbose_name=_("Second name")),
        first_name=CharField(max_length=40,
                             blank=False,
                             verbose_name=_("First name")),
        third_name=CharField(max_length=40,
                             blank=False,
                             verbose_name=_("Third name")),
        rank=CharField(max_length=300, blank=True, verbose_name=_("Rank")),
        methodical_works=HTMLField(blank=True,
                                   verbose_name=_("Methodical works")),
        description=HTMLField(blank=True, verbose_name=_("Description")),
        ndr_theme=TextField(blank=True,
                            verbose_name=_("Topics of research work")))
    #endregion

    #region           -----Information-----
    google_scholar = URLField(blank=True,
                              verbose_name=_("Google Scholar"),
                              null=True)
    web_of_science = URLField(blank=True,
                              verbose_name=_("Web Of Science"),
                              null=True)
    researchgate = URLField(blank=True,
                            verbose_name=_("Researchgate"),
                            null=True)
    scopus = URLField(verbose_name=_("Scopus"), blank=True, null=True)
    orcid = URLField(verbose_name=_("ORCID"), blank=True, null=True)
    photo = ImageField(verbose_name=_("Photo"),
                       upload_to="photos",
                       blank=False)
    phone = CharField(max_length=20,
                      blank=True,
                      null=True,
                      verbose_name=_("Phone number"))
    emails = MultiEmailField(blank=True)
    #endregion

    #region            -----Relation-----
    disciplines = ManyToManyField(Discipline, blank=True)
    books = ManyToManyField(Book, blank=True)
    rewards = ManyToManyField("Reward",
                              blank=True,
                              verbose_name=_("Rewards"),
                              related_name="staff_rewards")

    #endregion

    #region            -----Metadata-----
    class Meta(object):
        verbose_name_plural = _("Staff")
        verbose_name = _("Staff")

    #endregion

    #region         -----Internal Methods-----
    def get_absolute_url(self) -> str:
        """@return link to model"""
        return reverse('teacher', kwargs={'teacher_id': self.pk})

    def searching_fields(self) -> List[str]:
        """@return translated fields"""
        return [
            "translations__first_name", "translations__methodical_works",
            "translations__second_name", "translations__description",
            "translations__third_name"
        ]

    def __str__(self) -> str:
        """@return staff information"""
        return (self.second_name + " " + self.first_name + " " +
                self.third_name)
コード例 #20
0
class EMail(CustomModel):
    """Model representing an email"""

    subject = models.CharField(
        verbose_name="Betreff",
        max_length=50,
        help_text="Wird Standardmässig auch als Untertitel verwendet.",
    )

    to = MultiEmailField(
        verbose_name="Empfänger",
        help_text="Direkte Empfänger",
    )
    cc = MultiEmailField(
        verbose_name="CC",
        default="",
        blank=True,
        help_text="Kopie",
    )
    bcc = MultiEmailField(
        verbose_name="BCC",
        default="",
        blank=True,
        help_text="Blindkopie",
    )

    html_template = models.CharField(
        verbose_name="Designvorlage",
        default="default.html",
        max_length=50,
        help_text="Dateiname der Designvorlage unter 'kmuhelper/emails/'.",
    )
    text = models.TextField(
        verbose_name="Text",
        default="",
        blank=True,
        help_text=
        "Hauptinhalt - wird nicht von allen Designvorlagen verwendet. " +
        "Links und E-Mail Adressen werden automatisch verlinkt.",
    )
    html_context = models.JSONField(
        verbose_name="Daten",
        default=dict,
        blank=True,
        null=True,
        help_text=
        "Daten im JSON-Format, mit welchen die Designvorlage befüllt wird.",
    )

    token = models.UUIDField(
        verbose_name="Token",
        default=uuid.uuid4,
        editable=False,
    )

    time_created = models.DateTimeField(
        verbose_name="Erstellt am",
        auto_now_add=True,
        help_text="Datum und Zeit der Erstellung dieser E-Mail.",
    )
    time_sent = models.DateTimeField(
        verbose_name="Gesendet um",
        blank=True,
        null=True,
        default=None,
        help_text="Datum und Zeit des letzten erfolgreichen Sendeversuches.",
    )

    sent = models.BooleanField(
        verbose_name="Gesendet?",
        default=False,
    )

    notes = models.TextField(
        verbose_name="Notizen",
        blank=True,
        default="",
        help_text="Diese Notizen haben keine Einwirkung auf die E-Mail selbst.",
    )

    attachments = models.ManyToManyField(
        to='Attachment',
        through='EMailAttachment',
    )

    @admin.display(description="E-Mail")
    def __str__(self):
        return f"{self.subject} ({self.pk})"

    def is_valid(self, request=None):
        """Check if the email is valid.
        If not, add messages to request.

        Returns:
            0: Has errors
            1: Has warnings
            2: No warnings or errors
        """

        template = f"kmuhelper/emails/{self.html_template}"
        errors = []
        warnings = []

        # Check 1: Template

        try:
            get_template(template)
        except TemplateDoesNotExist:
            errors.append(f"Vorlage '{template}' wurde nicht gefunden.")
        except TemplateSyntaxError as error:
            errors.append(
                f"Vorlage '{template}' enthält ungültige Syntax: {error}")

        # Check 2: Receiver

        if not self.to:
            warnings.append("Nachricht hat keine(n) Empfänger!")

        # Add messages and return

        if request:
            for msg in errors:
                messages.error(request, msg)
            for msg in warnings:
                messages.warning(request, msg)

        haserrors = len(errors) > 0
        haswarnings = len(warnings) > 0
        return 0 if haserrors else 1 if haswarnings else 2

    def get_context(self):
        """Get the context for rendering"""

        ctx = dict(self.html_context) if self.html_context is not None else {}

        defaultcontext = settings.get_file_setting(
            "KMUHELPER_EMAILS_DEFAULT_CONTEXT", {})
        signature = settings.get_db_setting("email-signature", "") or \
            defaultcontext.get("postcontent", "")

        data = {
            **defaultcontext,
            "subtitle": self.subject,
            "postconent": signature,
            **ctx,
            "text": self.text,
        }

        return data

    def render(self, online=False):
        """Render the email and return the rendered string"""

        context = self.get_context()

        if online:
            context["isonline"] = True
            context["attachments"] = list(self.attachments.all())
        else:
            context["view_online_url"] = self.get_url_with_domain()
        return get_template("kmuhelper/emails/" +
                            str(self.html_template)).render(context)

    def add_attachments(self, *attachments):
        """Add one or more Attachment objects to this EMail"""

        self.attachments.add(*attachments)
        self.save()

    def send(self, **options):
        """Send the mail with given attachments.
        Options are passed to django.core.mail.EmailMessage"""

        msg = mail.EmailMessage(
            subject=self.subject,
            body=self.render(),
            to=self.to.splitlines() if isinstance(self.to, str) else self.to,
            cc=self.cc.splitlines() if isinstance(self.cc, str) else self.cc,
            bcc=self.bcc.splitlines()
            if isinstance(self.bcc, str) else self.bcc,
            **options)

        if settings.has_file_setting("KMUHELPER_LOG_EMAIL"):
            msg.bcc.append(settings.get_file_setting("KMUHELPER_LOG_EMAIL"))

        msg.content_subtype = "html"

        for attachment in self.attachments.all():
            msg.attach(filename=attachment.filename,
                       content=attachment.file.read())

        success = msg.send()

        log("ID:", self.pk, "- Subject:", self.subject, "- Success:", success)

        if success:
            self.time_sent = timezone.now()
            self.sent = True
            self.save()
        return success

    def get_url(self):
        """Get the public view online URL"""

        path = reverse("kmuhelper:email-view", kwargs={"object_id": self.pk})
        return path + f"?token={self.token}"

    def get_url_with_domain(self):
        """Get the public view online URL with domain prefix"""

        domain = settings.get_file_setting("KMUHELPER_DOMAIN", None)

        if domain:
            url = self.get_url()
            return domain + url

        log("[orange_red1]Einstellung KMUHELPER_DOMAIN ist nicht gesetzt! " +
            "E-Mails werden ohne 'online ansehen' Links versendet!")
        return None

    class Meta:
        verbose_name = "E-Mail"
        verbose_name_plural = "E-Mails"
        default_permissions = ('add', 'change', 'view', 'delete', 'send')

    objects = models.Manager()
コード例 #21
0
ファイル: resource.py プロジェクト: pnuz3n/respa
class Resource(ModifiableModel, AutoIdentifiedModel):
    AUTHENTICATION_TYPES = (('unauthenticated',
                             _('Unauthenticated')), ('none', _('None')),
                            ('weak', _('Weak')), ('strong', _('Strong')))
    ACCESS_CODE_TYPE_NONE = 'none'
    ACCESS_CODE_TYPE_PIN4 = 'pin4'
    ACCESS_CODE_TYPE_PIN6 = 'pin6'
    ACCESS_CODE_TYPES = (
        (ACCESS_CODE_TYPE_NONE, _('None')),
        (ACCESS_CODE_TYPE_PIN4, _('4-digit PIN code')),
        (ACCESS_CODE_TYPE_PIN6, _('6-digit PIN code')),
    )

    PRICE_TYPE_HOURLY = 'hourly'
    PRICE_TYPE_DAILY = 'daily'
    PRICE_TYPE_WEEKLY = 'weekly'
    PRICE_TYPE_FIXED = 'fixed'
    PRICE_TYPE_CHOICES = (
        (PRICE_TYPE_HOURLY, _('Hourly')),
        (PRICE_TYPE_DAILY, _('Daily')),
        (PRICE_TYPE_WEEKLY, _('Weekly')),
        (PRICE_TYPE_FIXED, _('Fixed')),
    )
    id = models.CharField(primary_key=True, max_length=100)
    public = models.BooleanField(default=True, verbose_name=_('Public'))
    unit = models.ForeignKey('Unit',
                             verbose_name=_('Unit'),
                             db_index=True,
                             null=True,
                             blank=True,
                             related_name="resources",
                             on_delete=models.PROTECT)
    type = models.ForeignKey(ResourceType,
                             verbose_name=_('Resource type'),
                             db_index=True,
                             on_delete=models.PROTECT)
    purposes = models.ManyToManyField(Purpose, verbose_name=_('Purposes'))
    name = models.CharField(verbose_name=_('Name'), max_length=200)
    description = models.TextField(verbose_name=_('Description'),
                                   null=True,
                                   blank=True)

    tags = TaggableManager(through=CleanResourceID, blank=True)

    min_age = models.PositiveIntegerField(
        verbose_name=_('Age restriction (min)'),
        null=True,
        blank=True,
        default=0)
    max_age = models.PositiveIntegerField(
        verbose_name=_('Age restriction (max)'),
        null=True,
        blank=True,
        default=0)
    need_manual_confirmation = models.BooleanField(
        verbose_name=_('Need manual confirmation'), default=False)

    resource_staff_emails = MultiEmailField(
        verbose_name=_('E-mail addresses for client correspondence'),
        null=True,
        blank=True)

    authentication = models.CharField(blank=False,
                                      verbose_name=_('Authentication'),
                                      max_length=20,
                                      choices=AUTHENTICATION_TYPES)
    people_capacity = models.PositiveIntegerField(
        verbose_name=_('People capacity'), null=True, blank=True)
    area = models.PositiveIntegerField(verbose_name=_('Area (m2)'),
                                       null=True,
                                       blank=True)

    # if not set, location is inherited from unit
    location = models.PointField(verbose_name=_('Location'),
                                 null=True,
                                 blank=True,
                                 srid=settings.DEFAULT_SRID)

    min_period = models.DurationField(
        verbose_name=_('Minimum reservation time'),
        default=datetime.timedelta(minutes=30))
    max_period = models.DurationField(
        verbose_name=_('Maximum reservation time'), null=True, blank=True)

    cooldown = models.DurationField(verbose_name=_('Reservation cooldown'),
                                    null=True,
                                    blank=True,
                                    default=datetime.timedelta(minutes=0))

    slot_size = models.DurationField(
        verbose_name=_('Slot size for reservation time'),
        null=True,
        blank=True,
        default=datetime.timedelta(minutes=30))

    equipment = EquipmentField(Equipment,
                               through='ResourceEquipment',
                               verbose_name=_('Equipment'))
    max_reservations_per_user = models.PositiveIntegerField(
        verbose_name=_('Maximum number of active reservations per user'),
        null=True,
        blank=True)
    reservable = models.BooleanField(verbose_name=_('Reservable'),
                                     default=False)
    reservation_info = models.TextField(verbose_name=_('Reservation info'),
                                        null=True,
                                        blank=True)
    responsible_contact_info = models.TextField(
        verbose_name=_('Responsible contact info'), blank=True)
    generic_terms = models.ForeignKey(
        TermsOfUse,
        verbose_name=_('Generic terms'),
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='resources_where_generic_terms')
    payment_terms = models.ForeignKey(
        TermsOfUse,
        verbose_name=_('Payment terms'),
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='resources_where_payment_terms')
    specific_terms = models.TextField(verbose_name=_('Specific terms'),
                                      blank=True)
    reservation_requested_notification_extra = models.TextField(verbose_name=_(
        'Extra content to "reservation requested" notification'),
                                                                blank=True)
    reservation_confirmed_notification_extra = models.TextField(verbose_name=_(
        'Extra content to "reservation confirmed" notification'),
                                                                blank=True)
    reservation_additional_information = models.TextField(
        verbose_name=_('Reservation additional information'), blank=True)

    min_price = models.DecimalField(
        verbose_name=_('Min price'),
        max_digits=8,
        decimal_places=2,
        blank=True,
        null=True,
        validators=[MinValueValidator(Decimal('0.00'))])
    max_price = models.DecimalField(
        verbose_name=_('Max price'),
        max_digits=8,
        decimal_places=2,
        blank=True,
        null=True,
        validators=[MinValueValidator(Decimal('0.00'))])

    price_type = models.CharField(max_length=32,
                                  verbose_name=_('price type'),
                                  choices=PRICE_TYPE_CHOICES,
                                  default=PRICE_TYPE_HOURLY)

    access_code_type = models.CharField(verbose_name=_('Access code type'),
                                        max_length=20,
                                        choices=ACCESS_CODE_TYPES,
                                        default=ACCESS_CODE_TYPE_NONE)
    # Access codes can be generated either by the general Respa code or
    # the Kulkunen app. Kulkunen will set the `generate_access_codes`
    # attribute by itself if special access code considerations are
    # needed.
    generate_access_codes = models.BooleanField(
        verbose_name=_('Generate access codes'),
        default=True,
        editable=False,
        help_text=_('Should access codes generated by the general system'))
    reservable_max_days_in_advance = models.PositiveSmallIntegerField(
        verbose_name=_('Reservable max. days in advance'),
        null=True,
        blank=True)
    reservable_min_days_in_advance = models.PositiveSmallIntegerField(
        verbose_name=_('Reservable min. days in advance'),
        null=True,
        blank=True)
    reservation_metadata_set = models.ForeignKey(
        'resources.ReservationMetadataSet',
        verbose_name=_('Reservation metadata set'),
        null=True,
        blank=True,
        on_delete=models.SET_NULL)
    reservation_home_municipality_set = models.ForeignKey(
        'resources.ReservationHomeMunicipalitySet',
        verbose_name=_('Reservation home municipality set'),
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='home_municipality_included_set')
    external_reservation_url = models.URLField(
        verbose_name=_('External reservation URL'),
        help_text=
        _('A link to an external reservation system if this resource is managed elsewhere'
          ),
        null=True,
        blank=True)

    resource_email = models.EmailField(verbose_name='Email for Outlook',
                                       null=True,
                                       blank=True)
    configuration = models.ForeignKey(
        'respa_outlook.RespaOutlookConfiguration',
        verbose_name=_('Outlook configuration'),
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='Configuration')

    objects = ResourceQuerySet.as_manager()

    class Meta:
        verbose_name = _("resource")
        verbose_name_plural = _("resources")
        ordering = (
            'unit',
            'name',
        )

    def __str__(self):
        return "%s (%s)/%s" % (get_translated(self,
                                              'name'), self.id, self.unit)

    @cached_property
    def main_image(self):
        resource_image = next(
            (image for image in self.images.all() if image.type == 'main'),
            None)

        return resource_image.image if resource_image else None

    def validate_reservation_period(self, reservation, user, data=None):
        """
        Check that given reservation if valid for given user.

        Reservation may be provided as Reservation or as a data dict.
        When providing the data dict from a serializer, reservation
        argument must be present to indicate the reservation being edited,
        or None if we are creating a new reservation.
        If the reservation is not valid raises a ValidationError.

        Staff members have no restrictions at least for now.

        Normal users cannot make multi day reservations or reservations
        outside opening hours.

        :type reservation: Reservation
        :type user: User
        :type data: dict[str, Object]
        """

        # no restrictions for staff
        if self.is_admin(user):
            return

        tz = self.unit.get_tz()
        # check if data from serializer is present:
        if data:
            begin = data['begin']
            end = data['end']
        else:
            # if data is not provided, the reservation object has the desired data:
            begin = reservation.begin
            end = reservation.end

        if begin.tzinfo:
            begin = begin.astimezone(tz)
        else:
            begin = tz.localize(begin)
        if end.tzinfo:
            end = end.astimezone(tz)
        else:
            end = tz.localize(end)

        if begin.date() != end.date():
            raise ValidationError(_("You cannot make a multi day reservation"))

        if not self.can_ignore_opening_hours(user):
            opening_hours = self.get_opening_hours(begin.date(), end.date())
            days = opening_hours.get(begin.date(), None)
            if days is None or not any(day['opens'] and begin >= day['opens']
                                       and end <= day['closes']
                                       for day in days):
                raise ValidationError(
                    _("You must start and end the reservation during opening hours"
                      ))

        if not self.can_ignore_max_period(user) and (
                self.max_period and (end - begin) > self.max_period):
            raise ValidationError(
                _("The maximum reservation length is %(max_period)s") %
                {'max_period': humanize_duration(self.max_period)})

    def validate_max_reservations_per_user(self, user):
        """
        Check maximum number of active reservations per user per resource.
        If the user has too many reservations raises ValidationError.

        Staff members have no reservation limits.

        :type user: User
        """
        if self.can_ignore_max_reservations_per_user(user):
            return

        max_count = self.max_reservations_per_user
        if max_count is not None:
            reservation_count = self.reservations.filter(
                user=user).active().count()
            if reservation_count >= max_count:
                raise ValidationError(
                    _("Maximum number of active reservations for this resource exceeded."
                      ))

    def check_reservation_collision(self, begin, end, reservation):
        overlapping = self.reservations.filter(end__gt=begin,
                                               begin__lt=end).active()
        if reservation:
            overlapping = overlapping.exclude(pk=reservation.pk)
        return overlapping.exists()

    def get_available_hours(self,
                            start=None,
                            end=None,
                            duration=None,
                            reservation=None,
                            during_closing=False):
        """
        Returns hours that the resource is not reserved for a given date range

        If include_closed=True, will also return hours when the resource is closed, if it is not reserved.
        This is so that admins can book resources during closing hours. Returns
        the available hours as a list of dicts. The optional reservation argument
        is for disregarding a given reservation during checking, if we wish to
        move an existing reservation. The optional duration argument specifies
        minimum length for periods to be returned.

        :rtype: list[dict[str, datetime.datetime]]
        :type start: datetime.datetime
        :type end: datetime.datetime
        :type duration: datetime.timedelta
        :type reservation: Reservation
        :type during_closing: bool
        """
        today = arrow.get(timezone.now())
        if start is None:
            start = today.floor('day').naive
        if end is None:
            end = today.replace(days=+1).floor('day').naive
        if not start.tzinfo and not end.tzinfo:
            """
            Only try to localize naive dates
            """
            tz = timezone.get_current_timezone()
            start = tz.localize(start)
            end = tz.localize(end)

        if not during_closing:
            """
            Check open hours only
            """
            open_hours = self.get_opening_hours(start, end)
            hours_list = []
            for date, open_during_date in open_hours.items():
                for period in open_during_date:
                    if period['opens']:
                        # if the start or end straddle opening hours
                        opens = period[
                            'opens'] if period['opens'] > start else start
                        closes = period[
                            'closes'] if period['closes'] < end else end
                        # include_closed to prevent recursion, opening hours need not be rechecked
                        hours_list.extend(
                            self.get_available_hours(start=opens,
                                                     end=closes,
                                                     duration=duration,
                                                     reservation=reservation,
                                                     during_closing=True))
            return hours_list

        reservations = self.reservations.filter(
            end__gte=start, begin__lte=end).order_by('begin')
        hours_list = [({'starts': start})]
        first_checked = False
        for res in reservations:
            # skip the reservation that is being edited
            if res == reservation:
                continue
            # check if the reservation spans the beginning
            if not first_checked:
                first_checked = True
                if res.begin < start:
                    if res.end > end:
                        return []
                    hours_list[0]['starts'] = res.end
                    # proceed to the next reservation
                    continue
            if duration:
                if res.begin - hours_list[-1]['starts'] < duration:
                    # the free period is too short, discard this period
                    hours_list[-1]['starts'] = res.end
                    continue
            hours_list[-1]['ends'] = timezone.localtime(res.begin)
            # check if the reservation spans the end
            if res.end > end:
                return hours_list
            hours_list.append({'starts': timezone.localtime(res.end)})
        # after the last reservation, we must check if the remaining free period is too short
        if duration:
            if end - hours_list[-1]['starts'] < duration:
                hours_list.pop()
                return hours_list
        # otherwise add the remaining free period
        hours_list[-1]['ends'] = end
        return hours_list

    def get_opening_hours(self,
                          begin=None,
                          end=None,
                          opening_hours_cache=None):
        """
        :rtype : dict[str, datetime.datetime]
        :type begin: datetime.date
        :type end: datetime.date
        """
        tz = pytz.timezone(self.unit.time_zone)
        begin, end = determine_hours_time_range(begin, end, tz)

        if opening_hours_cache is None:
            hours_objs = self.opening_hours.filter(
                open_between__overlap=(begin, end, '[)'))
        else:
            hours_objs = opening_hours_cache

        opening_hours = dict()
        for h in hours_objs:
            opens = h.open_between.lower.astimezone(tz)
            closes = h.open_between.upper.astimezone(tz)
            date = opens.date()
            hours_item = OrderedDict(opens=opens, closes=closes)
            date_item = opening_hours.setdefault(date, [])
            date_item.append(hours_item)

        # Set the dates when the resource is closed.
        date = begin.date()
        end = end.date()
        while date < end:
            if date not in opening_hours:
                opening_hours[date] = [OrderedDict(opens=None, closes=None)]
            date += datetime.timedelta(days=1)

        return opening_hours

    def update_opening_hours(self):
        hours = self.opening_hours.order_by('open_between')
        existing_hours = {}
        for h in hours:
            assert h.open_between.lower not in existing_hours
            existing_hours[h.open_between.lower] = h.open_between.upper

        unit_periods = list(self.unit.periods.all())
        resource_periods = list(self.periods.all())

        # Periods set for the resource always carry a higher priority. If
        # nothing is defined for the resource for a given day, use the
        # periods configured for the unit.
        for period in unit_periods:
            period.priority = 0
        for period in resource_periods:
            period.priority = 1

        earliest_date = None
        latest_date = None
        all_periods = unit_periods + resource_periods
        for period in all_periods:
            if earliest_date is None or period.start < earliest_date:
                earliest_date = period.start
            if latest_date is None or period.end > latest_date:
                latest_date = period.end

        # Assume we delete everything, but remove items from the delete
        # list if the hours are identical.
        to_delete = existing_hours
        to_add = {}
        if all_periods:
            hours = get_opening_hours(self.unit.time_zone, all_periods,
                                      earliest_date, latest_date)
            for hours_items in hours.values():
                for h in hours_items:
                    if not h['opens'] or not h['closes']:
                        continue
                    if h['opens'] in to_delete and h['closes'] == to_delete[
                            h['opens']]:
                        del to_delete[h['opens']]
                        continue
                    to_add[h['opens']] = h['closes']

        if to_delete:
            ret = ResourceDailyOpeningHours.objects.filter(
                open_between__in=[(opens, closes, '[)')
                                  for opens, closes in to_delete.items()],
                resource=self).delete()
            assert ret[0] == len(to_delete)

        add_objs = [
            ResourceDailyOpeningHours(resource=self,
                                      open_between=(opens, closes, '[)'))
            for opens, closes in to_add.items()
        ]
        if add_objs:
            ResourceDailyOpeningHours.objects.bulk_create(add_objs)

    def is_admin(self, user):
        """
        Check if the given user is an administrator of this resource.

        :type user: users.models.User
        :rtype: bool
        """
        # UserFilterBackend and ReservationFilterSet in resources.api.reservation assume the same behaviour,
        # so if this is changed those need to be changed as well.
        if not self.unit:
            return is_general_admin(user)
        return self.unit.is_admin(user)

    def is_manager(self, user):
        """
        Check if the given user is a manager of this resource.

        :type user: users.models.User
        :rtype: bool
        """
        if not self.unit:
            return False
        return self.unit.is_manager(user)

    def is_viewer(self, user):
        """
        Check if the given user is a viewer of this resource.

        :type user: users.models.User
        :rtype: bool
        """
        if not self.unit:
            return False
        return self.unit.is_viewer(user)

    def _has_perm(self, user, perm, allow_admin=True):
        if not is_authenticated_user(user):
            return False

        if (self.is_admin(user) and allow_admin) or user.is_superuser:
            return True

        if self.min_age and is_underage(user, self.min_age):
            return False

        if self.max_age and is_overage(user, self.max_age):
            return False

        if self.unit.is_manager(user) or self.unit.is_admin(user):
            return True

        return self._has_role_perm(user, perm) or self._has_explicit_perm(
            user, perm, allow_admin)

    def _has_explicit_perm(self, user, perm, allow_admin=True):
        if hasattr(self, '_permission_checker'):
            checker = self._permission_checker
        else:
            checker = ObjectPermissionChecker(user)

        # Permissions can be given per-unit
        if checker.has_perm('unit:%s' % perm, self.unit):
            return True
        # ... or through Resource Groups
        resource_group_perms = [
            checker.has_perm('group:%s' % perm, rg)
            for rg in self.groups.all()
        ]
        return any(resource_group_perms)

    def _has_role_perm(self, user, perm):
        allowed_roles = UNIT_ROLE_PERMISSIONS.get(perm)
        is_allowed = False

        if (UnitAuthorizationLevel.admin in allowed_roles
                or UnitGroupAuthorizationLevel.admin
                in allowed_roles) and not is_allowed:
            is_allowed = self.is_admin(user)

        if UnitAuthorizationLevel.manager in allowed_roles and not is_allowed:
            is_allowed = self.is_manager(user)

        if UnitAuthorizationLevel.viewer in allowed_roles and not is_allowed:
            is_allowed = self.is_viewer(user)

        return is_allowed

    def get_users_with_perm(self, perm):
        users = {
            u
            for u in get_users_with_perms(self.unit)
            if u.has_perm('unit:%s' % perm, self.unit)
        }
        for rg in self.groups.all():
            users |= {
                u
                for u in get_users_with_perms(rg)
                if u.has_perm('group:%s' % perm, rg)
            }
        return users

    def can_make_reservations(self, user):
        if self.min_age and is_underage(user, self.min_age):
            return False
        if self.max_age and is_overage(user, self.max_age):
            return False

        return self.reservable or self._has_perm(user, 'can_make_reservations')

    def can_modify_reservations(self, user):
        return self._has_perm(user, 'can_modify_reservations')

    def can_comment_reservations(self, user):
        return self._has_perm(user, 'can_comment_reservations')

    def can_ignore_opening_hours(self, user):
        return self._has_perm(user, 'can_ignore_opening_hours')

    def can_view_reservation_extra_fields(self, user):
        return self._has_perm(user, 'can_view_reservation_extra_fields')

    def can_view_reservation_user(self, user):
        return self._has_perm(user, 'can_view_reservation_user')

    def can_access_reservation_comments(self, user):
        return self._has_perm(user, 'can_access_reservation_comments')

    def can_view_reservation_catering_orders(self, user):
        return self._has_perm(user, 'can_view_reservation_catering_orders')

    def can_modify_reservation_catering_orders(self, user):
        return self._has_perm(user, 'can_modify_reservation_catering_orders')

    def can_view_reservation_product_orders(self, user):
        return self._has_perm(user,
                              'can_view_reservation_product_orders',
                              allow_admin=False)

    def can_modify_paid_reservations(self, user):
        return self._has_perm(user,
                              'can_modify_paid_reservations',
                              allow_admin=False)

    def can_approve_reservations(self, user):
        return self._has_perm(user,
                              'can_approve_reservation',
                              allow_admin=False)

    def can_view_reservation_access_code(self, user):
        return self._has_perm(user, 'can_view_reservation_access_code')

    def can_bypass_payment(self, user):
        return self._has_perm(user, 'can_bypass_payment')

    def can_create_staff_event(self, user):
        return self._has_perm(user, 'can_create_staff_event')

    def can_create_special_type_reservation(self, user):
        return self._has_perm(user, 'can_create_special_type_reservation')

    def can_bypass_manual_confirmation(self, user):
        return self._has_perm(user, 'can_bypass_manual_confirmation')

    def can_create_reservations_for_other_users(self, user):
        return self._has_perm(user, 'can_create_reservations_for_other_users')

    def can_create_overlapping_reservations(self, user):
        return self._has_perm(user, 'can_create_overlapping_reservations')

    def can_ignore_max_reservations_per_user(self, user):
        return self._has_perm(user, 'can_ignore_max_reservations_per_user')

    def can_ignore_max_period(self, user):
        return self._has_perm(user, 'can_ignore_max_period')

    def is_access_code_enabled(self):
        return self.access_code_type != Resource.ACCESS_CODE_TYPE_NONE

    def get_reservable_max_days_in_advance(self):
        return self.reservable_max_days_in_advance or self.unit.reservable_max_days_in_advance

    def get_reservable_before(self):
        return create_datetime_days_from_now(
            self.get_reservable_max_days_in_advance())

    def get_reservable_min_days_in_advance(self):
        return self.reservable_min_days_in_advance or self.unit.reservable_min_days_in_advance

    def get_reservable_after(self):
        return create_datetime_days_from_now(
            self.get_reservable_min_days_in_advance())

    def has_rent(self):
        return self.products.current().rents().exists()

    def get_supported_reservation_extra_field_names(self, cache=None):
        if not self.reservation_metadata_set_id:
            return []
        if cache:
            metadata_set = cache[self.reservation_metadata_set_id]
        else:
            metadata_set = self.reservation_metadata_set
        return [x.field_name for x in metadata_set.supported_fields.all()]

    def get_required_reservation_extra_field_names(self, cache=None):
        if not self.reservation_metadata_set:
            return []
        if cache:
            metadata_set = cache[self.reservation_metadata_set_id]
        else:
            metadata_set = self.reservation_metadata_set
        return [x.field_name for x in metadata_set.required_fields.all()]

    def get_included_home_municipality_names(self, cache=None):
        if not self.reservation_home_municipality_set_id:
            return []
        if cache:
            home_municipality_set = cache[
                self.reservation_home_municipality_set_id]
        else:
            home_municipality_set = self.reservation_home_municipality_set
        # get home municipalities with translations [{id: {fi, en, sv}}, ...]
        included_municipalities = home_municipality_set.included_municipalities.all(
        )
        result_municipalities = []

        for municipality in included_municipalities:
            result_municipalities.append({
                'id': municipality.id,
                "name": {
                    'fi': municipality.name_fi,
                    'en': municipality.name_en,
                    'sv': municipality.name_sv
                }
            })
        return result_municipalities

    def clean(self):
        if self.cooldown is None:
            self.cooldown = datetime.timedelta(0)
        if self.min_price is not None and self.max_price is not None and self.min_price > self.max_price:
            raise ValidationError({
                'min_price':
                _('This value cannot be greater than max price')
            })
        if self.min_period % self.slot_size != datetime.timedelta(0):
            raise ValidationError({
                'min_period':
                _('This value must be a multiple of slot_size')
            })

        if self.cooldown % self.slot_size != datetime.timedelta(0):
            raise ValidationError(
                {'cooldown': _('This value must be a multiple of slot_size')})

        if self.need_manual_confirmation and self.products.current().exists():
            raise ValidationError({
                'need_manual_confirmation':
                _('This cannot be enabled because the resource has product(s).'
                  )
            })
        if self.authentication == 'unauthenticated':
            if self.min_age and self.min_age > 0:
                raise ValidationError({
                    'min_age':
                    format_lazy(
                        '{}' * 2, *[
                            _('This value cannot be set to more than zero if resource authentication is: '
                              ),
                            _('Unauthenticated')
                        ])
                })
            if self.max_age and self.max_age > 0:
                raise ValidationError({
                    'max_age':
                    format_lazy(
                        '{}' * 2, *[
                            _('This value cannot be set to more than zero if resource authentication is: '
                              ),
                            _('Unauthenticated')
                        ])
                })
            if self.max_reservations_per_user and self.max_reservations_per_user > 0:
                raise ValidationError({
                    'max_reservations_per_user':
                    format_lazy(
                        '{}' * 2, *[
                            _('This value cannot be set to more than zero if resource authentication is: '
                              ),
                            _('Unauthenticated')
                        ])
                })
            if self.is_access_code_enabled():
                raise ValidationError({
                    'access_code_type':
                    format_lazy(
                        '{}' * 2, *[
                            _('This cannot be enabled if resource authentication is: '
                              ),
                            _('Unauthenticated')
                        ])
                })
コード例 #22
0
class TestModel(models.Model):
    f = MultiEmailField(null=True, blank=True)
    f_default = MultiEmailField(default=["*****@*****.**"], null=True, blank=True)
コード例 #23
0
class BulkEmail(models.Model):
    """
    Track a bulk email that needs to be sent in the background

    Important: By default, the MIME type of the body parameter in an
    EmailMessage is "text/plain". That makes it safe to use "|safe" in
    our email templates, and we do. If you change this to, say, send
    HTML format email, you must go through the email templates and do
    something better about escaping user data for safety.
    """
    status = models.IntegerField(
        choices=[
            (UNSENT, "Unsent"),
            (INPROGRESS, "In progress"),
            (SENT, "Sent"),
            (ERROR, "Error"),
        ],
        default=UNSENT
    )

    subject = models.CharField(max_length=250)
    body = models.TextField()
    from_address = models.EmailField()
    to_addresses = MultiEmailField(default=[])
    bcc_addresses = MultiEmailField(default=[])
    headers = models.TextField()  # stored as JSON
    error = models.TextField(default='', blank=True)

    start_time = models.DateTimeField(
        null=True, blank=True, default=None,
        help_text=_("Time when we started trying to send. Used to detect orphan records.")
    )
    end_time = models.DateTimeField(
        null=True, blank=True, default=None,
        help_text=_("When we finished trying to send.")
    )

    def get_context(self):
        return json.loads(self.context)

    def get_headers(self):
        return json.loads(self.headers)

    def send(self):
        """Send the message"""
        # Make sure this BulkEmail is neither sent nor in progress already,
        # and atomically set it to INPROGRESS so only one thread can be trying
        # to send it.
        num_updates = BulkEmail.objects.filter(pk=self.pk, status=UNSENT) \
            .update(status=INPROGRESS, start_time=now(), end_time=None)
        if 1 != num_updates:
            logger.info("BulkEmail %d state was %s, not UNSENT in send task, not sending.",
                        self.pk, self.get_status_display())
            return
        # Now status is INPROGRESS.  Get to work.
        try:
            email = EmailMessage(
                self.subject.rstrip(u"\n"),
                self.body,
                self.from_address,
                self.to_addresses,
                self.bcc_addresses,
                headers=self.get_headers(),
            )
            email.send()
            BulkEmail.objects.filter(pk=self.pk, status=INPROGRESS)\
                .update(status=SENT, end_time=now())
        except Exception as err:
            logger.exception("Exception sending BulkEmail")
            BulkEmail.objects.filter(pk=self.pk, status=INPROGRESS)\
                .update(status=ERROR, error=str(err), end_time=now())
        finally:
            # If we've not managed to send the email, set status to ERROR
            BulkEmail.objects.filter(pk=self.pk, status=INPROGRESS)\
                .update(status=ERROR, end_time=now())
コード例 #24
0
class TestModel(models.Model):
    f = MultiEmailField(null=True, blank=True)
コード例 #25
0
ファイル: models.py プロジェクト: gridl/pycon-1
class Sponsor(models.Model):

    applicant = models.ForeignKey(User,
                                  related_name="sponsorships",
                                  verbose_name=_("applicant"),
                                  null=True,
                                  on_delete=SET_NULL)

    name = models.CharField(_("Sponsor Name"), max_length=100)
    display_url = models.CharField(_(
        "Link text - text to display on link to sponsor webpage, if different from the actual link"
    ),
                                   max_length=200,
                                   default='',
                                   blank=True)
    external_url = models.URLField(
        _("Link to sponsor webpage"),
        help_text=_("(Must include https:// or http://.)"))
    twitter_username = models.CharField(
        _("Twitter username"),
        blank=True,
        max_length=15,
    )
    annotation = models.TextField(_("annotation"), blank=True)
    contact_name = models.CharField(_("Contact Name"), max_length=100)
    contact_emails = MultiEmailField(
        _(u"Contact Emails"),
        default='',
        help_text=_(u"Please enter one email address per line."))
    contact_phone = models.CharField(_(u"Contact Phone"), max_length=32)
    contact_address = models.TextField(_(u"Contact Address"))
    level = models.ForeignKey(SponsorLevel, verbose_name=_("level"))
    packages = models.ManyToManyField(SponsorPackage,
                                      verbose_name=_("packages"),
                                      blank=True)
    added = models.DateTimeField(_("added"), default=datetime.datetime.now)

    active = models.BooleanField(_("active"), default=False)
    approval_time = models.DateTimeField(null=True, blank=True, editable=False)

    wants_table = models.BooleanField(_(
        'Does your organization want a table at the job fair? '
        '(See <a href="/2019/sponsors/fees/">Estimated Sponsor Fees</a> '
        'for costs that might be involved.)'),
                                      default=False)
    wants_booth = models.BooleanField(_(
        'Does your organization want a booth on the expo floor? '
        '(See <a href="/2019/sponsors/fees/">Estimated Sponsor Fees</a> '
        'for costs that might be involved.)'),
                                      default=False)
    small_entity_discount = models.BooleanField(_(
        'Does your organization have fewer than 25 employees,'
        ' which qualifies you for our Small Entity Discount of 30%?'),
                                                default=False)

    # Whether things are complete
    # True = complete, False = incomplate, Null = n/a for this sponsor level
    print_logo_benefit = models.NullBooleanField(
        help_text=_(u"Print logo benefit is complete"))
    advertisement_benefit = models.NullBooleanField(
        help_text=_(u"Advertisement benefit is complete"))

    registration_promo_codes = models.CharField(max_length=200,
                                                blank=True,
                                                default='')
    expo_promo_codes = models.CharField(max_length=200, blank=True, default='')
    additional_discounted_registration_promo_codes = models.CharField(
        max_length=200, blank=True, default='')
    a_la_carte_registration_promo_codes = models.CharField(max_length=200,
                                                           blank=True,
                                                           default='')
    booth_number = models.CharField(max_length=5,
                                    blank=True,
                                    null=True,
                                    default=None)
    job_fair_participant = models.BooleanField(default=False)
    job_fair_table_number = models.CharField(max_length=5,
                                             blank=True,
                                             null=True,
                                             default=None)

    web_description = models.TextField(
        _(u"Company description (to show on the web site)"), )
    web_logo = models.ImageField(
        _(u"Web logo"),
        help_text=
        _("For display on our sponsor webpage. High resolution PNG or JPG, smallest dimension no less than 256px"
          ),
        upload_to="sponsor_files",
        null=True,  # This is nullable in case old data doesn't have a web logo
        # We enforce it on all new or edited sponsors though.
    )
    print_logo = models.FileField(
        _(u"Print logo"),
        help_text=_(
            "For printed materials, signage, and projection. SVG or EPS"),
        upload_to="sponsor_files",
        blank=True,
        null=
        True,  # This is nullable in case old data doesn't have a printed logo
        # We enforce it on all new or edited sponsors though.
    )

    objects = SponsorManager()

    def __unicode__(self):
        return self.name

    class Meta:
        verbose_name = _("sponsor")
        verbose_name_plural = _("sponsors")
        ordering = ['name']

    def save(self, *args, **kwargs):
        # Set fields related to benefits being complete
        for benefit in BENEFITS:
            field_name = benefit['field_name']
            benefit_name = benefit['name']
            setattr(self, field_name, self.benefit_is_complete(benefit_name))
        super(Sponsor, self).save(*args, **kwargs)

    def get_absolute_url(self):
        if self.active:
            return reverse("sponsor_detail", kwargs={"pk": self.pk})
        return reverse("sponsor_list")

    def get_display_url(self):
        """
        Return the text to display on the sponsor's link
        """
        if self.display_url:
            return self.display_url
        else:
            return self.external_url

    def render_email(self, text):
        """Replace special strings in text with values from the sponsor.

        %%NAME%% --> Sponsor name
        %%REGISTRATION_PROMO_CODES%% --> Registration promo codes, or empty string
        %%EXPO_PROMO_CODES%% --> Expo Hall only promo codes, or empty string
        %%ADDITIONAL_DISCOUNTED_REGISTRATION_PROMO_CODES%% --> Additional Discounted Registration promo codes, or empty string
        %%A_LA_CARTE_REGISTRATION_PROMO_CODES%% --> A la Carte Registration promo codes, or empty string
        %%BOOTH_NUMBER%% --> Booth number, or empty string if not set
        %%JOB_FAIR_TABLE_NUMBER%%" --> Job fair table number, or empty string if not set

        Flags:
          JOB_FAIR_PARTICIPANT: Use with {% if JOB_FAIR_PARTICIPANT %} block to
                                include some content for Job Fair Participants
        """
        text = text.replace("%%NAME%%", self.name)
        text = text.replace("%%REGISTRATION_PROMO_CODES%%",
                            self.registration_promo_codes or 'N/A')
        text = text.replace("%%EXPO_PROMO_CODES%%", self.expo_promo_codes
                            or 'N/A')
        text = text.replace(
            "%%ADDITIONAL_DISCOUNTED_REGISTRATION_PROMO_CODES%%",
            self.additional_discounted_registration_promo_codes or 'N/A')
        text = text.replace("%%A_LA_CARTE_REGISTRATION_PROMO_CODES%%",
                            self.a_la_carte_registration_promo_codes or 'N/A')

        # The next two are numbers, or if not set, None.  We don't want to
        # display "None" :-), but we might want to display "0".
        booth = str(self.booth_number) if self.booth_number is not None else ""
        text = text.replace("%%BOOTH_NUMBER%%", booth)
        table = str(self.job_fair_table_number
                    ) if self.job_fair_table_number is not None else ""
        text = text.replace("%%JOB_FAIR_TABLE_NUMBER%%", table)
        email_template = Template(text)
        email_context = Context({
            'JOB_FAIR_PARTICIPANT':
            self.job_fair_participant,
        })
        text = email_template.render(email_context)
        return text

    @cached_property
    def website_logo_url(self):
        if self.web_logo:
            return self.web_logo.url

    @property
    def joblisting_text(self):
        if not hasattr(self, "_joblisting_text"):
            self._joblisting_text = None
            benefits = self.sponsor_benefits.filter(
                benefit__name__startswith='Job Listing', )
            if benefits.count():
                self._joblisting_text = benefits[0].text
        return self._joblisting_text

    @property
    def website_logo(self):
        return self.web_logo

    def reset_benefits(self):
        """
        Reset all benefits for this sponsor to the defaults for their
        sponsorship level.
        """
        level = None

        try:
            level = self.level
        except SponsorLevel.DoesNotExist:
            pass

        try:
            packages = self.packages
        except SponsorPackage.DoesNotExist:
            pass

        allowed_benefits = []
        if level:
            for benefit_level in level.benefit_levels.all():
                # Create all needed benefits if they don't exist already
                sponsor_benefit, created = SponsorBenefit.objects.get_or_create(
                    sponsor=self, benefit=benefit_level.benefit)

                # and set to default limits for this level.
                sponsor_benefit.max_words = benefit_level.max_words
                sponsor_benefit.other_limits = benefit_level.other_limits

                # and set to active
                sponsor_benefit.active = True

                # @@@ We don't call sponsor_benefit.clean here. This means
                # that if the sponsorship level for a sponsor is adjusted
                # downwards, an existing too-long text entry can remain,
                # and won't raise a validation error until it's next
                # edited.
                sponsor_benefit.save()

                allowed_benefits.append(sponsor_benefit.pk)

        if packages:
            for package in packages.all():
                for benefit_package in package.benefit_packages.all():
                    # Create all needed benefits if they don't exist already
                    sponsor_benefit, created = SponsorBenefit.objects.get_or_create(
                        sponsor=self, benefit=benefit_package.benefit)

                    # and set to default limits for this level.
                    sponsor_benefit.max_words = benefit_package.max_words
                    sponsor_benefit.other_limits = benefit_package.other_limits

                    # and set to active
                    sponsor_benefit.active = True

                    # @@@ We don't call sponsor_benefit.clean here. This means
                    # that if the sponsorship level for a sponsor is adjusted
                    # downwards, an existing too-long text entry can remain,
                    # and won't raise a validation error until it's next
                    # edited.
                    sponsor_benefit.save()

                    allowed_benefits.append(sponsor_benefit.pk)

        # Any remaining sponsor benefits that don't normally belong to
        # this level are set to inactive
        self.sponsor_benefits.exclude(pk__in=allowed_benefits).update(
            active=False, max_words=None, other_limits="")

    # @@@ should this just be done centrally?
    def send_coordinator_emails(self):
        for user in User.objects.filter(groups__name=SPONSOR_COORDINATORS):
            send_email([user.email],
                       "sponsor_signup",
                       context={"sponsor": self})

    def benefit_is_complete(self, name):
        """Return True - benefit is complete, False - benefit is not complete,
         or None - benefit not applicable for this sponsor's level """
        if BenefitLevel.objects.filter(level=self.level,
                                       benefit__name=name).exists():
            try:
                benefit = self.sponsor_benefits.get(benefit__name=name)
            except SponsorBenefit.DoesNotExist:
                return False
            else:
                return benefit.is_complete
        else:
            return None  # Not an applicable benefit for this sponsor's level
コード例 #26
0
ファイル: models.py プロジェクト: Diwahars/pycon
class Sponsor(models.Model):

    applicant = models.ForeignKey(User,
                                  related_name="sponsorships",
                                  verbose_name=_("applicant"),
                                  null=True,
                                  on_delete=SET_NULL)

    name = models.CharField(_("Sponsor Name"), max_length=100)
    display_url = models.URLField(_(
        "Link text - text to display on link to sponsor page, if different from the actual link"
    ),
                                  blank=True)
    external_url = models.URLField(_("Link to sponsor web page"))
    annotation = models.TextField(_("annotation"), blank=True)
    contact_name = models.CharField(_("Contact Name"), max_length=100)
    contact_emails = MultiEmailField(
        _(u"Contact Emails"),
        default='',
        help_text=_(u"Please enter one email address per line."))
    contact_phone = models.CharField(_(u"Contact Phone"), max_length=32)
    contact_address = models.TextField(_(u"Contact Address"))
    level = models.ForeignKey(SponsorLevel, verbose_name=_("level"))
    added = models.DateTimeField(_("added"), default=datetime.datetime.now)

    active = models.BooleanField(_("active"), default=False)
    approval_time = models.DateTimeField(null=True, blank=True, editable=False)

    wants_table = models.BooleanField(
        _("Does your organization want a table at the job fair?"),
        default=False)
    wants_booth = models.BooleanField(
        _("Does your organization want a booth on the expo floor?"),
        default=False)

    # Whether things are complete
    # True = complete, False = incomplate, Null = n/a for this sponsor level
    print_logo_benefit = models.NullBooleanField(
        help_text=_(u"Print logo benefit is complete"))
    advertisement_benefit = models.NullBooleanField(
        help_text=_(u"Advertisement benefit is complete"))

    registration_promo_codes = models.CharField(max_length=200,
                                                blank=True,
                                                default='')
    booth_number = models.IntegerField(blank=True, null=True, default=None)
    job_fair_table_number = models.IntegerField(blank=True,
                                                null=True,
                                                default=None)

    web_description = models.TextField(
        _(u"Company description (to show on the web site)"), )
    web_logo = models.ImageField(
        _(u"Company logo (to show on the web site)"),
        upload_to="sponsor_files",
        null=True,  # This is nullable in case old data doesn't have a web logo
        # We enforce it on all new or edited sponsors though.
    )

    objects = SponsorManager()

    def __unicode__(self):
        return self.name

    class Meta:
        verbose_name = _("sponsor")
        verbose_name_plural = _("sponsors")
        ordering = ['name']

    def save(self, *args, **kwargs):
        # Set fields related to benefits being complete
        for benefit in BENEFITS:
            field_name = benefit['field_name']
            benefit_name = benefit['name']
            setattr(self, field_name, self.benefit_is_complete(benefit_name))
        super(Sponsor, self).save(*args, **kwargs)

    def get_absolute_url(self):
        if self.active:
            return reverse("sponsor_detail", kwargs={"pk": self.pk})
        return reverse("sponsor_list")

    def get_display_url(self):
        """
        Return the text to display on the sponsor's link
        """
        if self.display_url:
            return self.display_url
        else:
            return self.external_url

    def render_email(self, text):
        """Replace special strings in text with values from the sponsor.

        %%NAME%% --> Sponsor name
        %%REGISTRATION_PROMO_CODES%% --> Registration promo codes, or empty string
        %%BOOTH_NUMBER%% --> Booth number, or empty string if not set
        %%JOB_FAIR_TABLE_NUMBER%%" --> Job fair tabl number, or empty string if not set
        """
        text = text.replace("%%NAME%%", self.name)
        text = text.replace("%%REGISTRATION_PROMO_CODES%%",
                            self.registration_promo_codes)

        # The next two are numbers, or if not set, None.  We don't want to
        # display "None" :-), but we might want to display "0".
        booth = str(self.booth_number) if self.booth_number is not None else ""
        text = text.replace("%%BOOTH_NUMBER%%", booth)
        table = str(self.job_fair_table_number
                    ) if self.job_fair_table_number is not None else ""
        text = text.replace("%%JOB_FAIR_TABLE_NUMBER%%", table)
        return text

    @cached_property
    def website_logo_url(self):
        if self.web_logo:
            return self.web_logo.url

    @property
    def joblisting_text(self):
        if not hasattr(self, "_joblisting_text"):
            self._joblisting_text = None
            benefits = self.sponsor_benefits.filter(benefit__id=8)
            if benefits.count():
                self._joblisting_text = benefits[0].text
        return self._joblisting_text

    @property
    def website_logo(self):
        return self.web_logo

    def reset_benefits(self):
        """
        Reset all benefits for this sponsor to the defaults for their
        sponsorship level.
        """
        level = None

        try:
            level = self.level
        except SponsorLevel.DoesNotExist:
            pass

        allowed_benefits = []
        if level:
            for benefit_level in level.benefit_levels.all():
                # Create all needed benefits if they don't exist already
                sponsor_benefit, created = SponsorBenefit.objects.get_or_create(
                    sponsor=self, benefit=benefit_level.benefit)

                # and set to default limits for this level.
                sponsor_benefit.max_words = benefit_level.max_words
                sponsor_benefit.other_limits = benefit_level.other_limits

                # and set to active
                sponsor_benefit.active = True

                # @@@ We don't call sponsor_benefit.clean here. This means
                # that if the sponsorship level for a sponsor is adjusted
                # downwards, an existing too-long text entry can remain,
                # and won't raise a validation error until it's next
                # edited.
                sponsor_benefit.save()

                allowed_benefits.append(sponsor_benefit.pk)

        # Any remaining sponsor benefits that don't normally belong to
        # this level are set to inactive
        self.sponsor_benefits.exclude(pk__in=allowed_benefits).update(
            active=False, max_words=None, other_limits="")

    # @@@ should this just be done centrally?
    def send_coordinator_emails(self):
        for user in User.objects.filter(groups__name=SPONSOR_COORDINATORS):
            send_email([user.email],
                       "sponsor_signup",
                       context={"sponsor": self})

    def benefit_is_complete(self, name):
        """Return True - benefit is complete, False - benefit is not complete,
         or None - benefit not applicable for this sponsor's level """
        if BenefitLevel.objects.filter(level=self.level,
                                       benefit__name=name).exists():
            try:
                benefit = self.sponsor_benefits.get(benefit__name=name)
            except SponsorBenefit.DoesNotExist:
                return False
            else:
                return benefit.is_complete
        else:
            return None  # Not an applicable benefit for this sponsor's level
コード例 #27
0
class EmailMessage(models.Model):
    uuid = models.UUIDField(default=uuid.uuid4, editable=False)
    users = models.ManyToManyField(User, related_name='emailhub')
    subject = models.TextField(_('Subject'))
    body_text = models.TextField(_('Body (text)'))
    body_html = models.TextField(_('Body (HTML)'), blank=True, null=True)
    from_email = models.EmailField(_('From'))
    to = models.EmailField(_('To'))
    cc = MultiEmailField(_('C.C.'), blank=True, null=True)
    bcc = MultiEmailField(_('B.C.C.'), blank=True, null=True)
    date_created = models.DateTimeField(_('Date created'), auto_now_add=True)
    date_modified = models.DateTimeField(_('Date modified'), auto_now=True)
    date_sent = models.DateTimeField(_('Date sent'), blank=True, null=True)
    is_sent = models.BooleanField(_('Is sent'), default=False, db_index=True)
    is_error = models.BooleanField(_('Is error'), default=False, db_index=True)
    send_retries = models.SmallIntegerField(_('Send retries'), default=0)
    send_error_message = models.TextField(_('Send error message'),
                                          blank=True,
                                          null=True)
    send_error_code = models.SmallIntegerField(_('Send error code'),
                                               blank=True,
                                               null=True)
    from_template = models.CharField(_('From template'),
                                     max_length=100,
                                     blank=True,
                                     null=True)
    is_draft = models.BooleanField(
        _('Is draft'),
        default=False,
        help_text=_("Message marked as draft will not be sent"))
    is_locked = models.BooleanField(
        _('Is locked'),
        default=False,
        help_text=_("Message marked as locked is being processed"))

    def get_color(self):
        if self.is_error:
            return 'red'
        elif self.is_draft:
            return 'blue'
        elif self.is_locked:
            return 'orange'
        elif self.is_sent:
            return 'green'

    def get_state_label(self):
        if self.is_error:
            return _('Error')
        elif self.is_sent:
            return _('Sent')
        elif self.is_locked:
            return _('Sending')
        elif self.is_draft:
            return _('Draft')

    def get_icon(self):
        i = self.is_draft and 'drafts' or 'email'
        _kwargs = {
            'tooltip': self.get_state_label(),
            'css_class': '{}-text'.format(self.get_color())
        }
        return mark_safe(icon(i, **_kwargs))

    def get_badge(self):
        _tpl = '<span class="badge white-text {color}">{label}</span>'
        _kwargs = {'color': self.get_color(), 'label': self.get_state_label()}
        return mark_safe(_tpl.format(**_kwargs))

    def get_absolute_url(self):
        return reverse('admin:messaging_emailmessage_change', args=[self.pk])

    def save(self, *args, **kwargs):
        # force remove new lines & spaces from begining and end of the message
        self.body_text = re.sub('^(\n|\r|\s)+|(\n|\r|\s)+$', '',
                                self.body_text)
        return super(EmailMessage, self).save(*args, **kwargs)

    def __str__(self):
        return '<%s> %s' % (self.to, self.subject)

    class Meta:
        verbose_name = _('Email message')
        verbose_name_plural = _('Email messages')
        ordering = ['-date_created', '-date_sent']