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
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
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
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,)
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()
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')
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
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()
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)
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)
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'
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")
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]
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,)
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
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
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
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
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)
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()
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') ]) })
class TestModel(models.Model): f = MultiEmailField(null=True, blank=True) f_default = MultiEmailField(default=["*****@*****.**"], null=True, blank=True)
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())
class TestModel(models.Model): f = MultiEmailField(null=True, blank=True)
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
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
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']