class Meta: model = Appliance fields = [ 'name', 'short_description', 'description', 'documentation', 'appliance_icon', 'chi_tacc_appliance_id', 'chi_uc_appliance_id', 'kvm_tacc_appliance_id', 'template', 'author_name', 'author_url', 'support_contact_name', 'support_contact_url', 'keywords', 'new_keywords', 'version', 'project_supported' ] labels = { 'short_description': 'Short description (140 characters)', 'chi_tacc_appliance_id': 'Appliance ID for ' '<a href="https://chi.tacc.chameleoncloud.org">CHI@TACC</a>', 'chi_uc_appliance_id': 'Appliance ID for ' '<a href="https://chi.uc.chameleoncloud.org">CHI@UC</a>', 'kvm_tacc_appliance_id': 'Appliance ID for ' '<a href="https://openstack.tacc.chameleoncloud.org">KVM@TACC</a>', 'template': 'Template (Complex Appliances Only)', 'author_url': 'Author: Contact URL or Email', 'support_contact_name': 'Support: Contact Name', 'support_contact_url': 'Support: Contact URL or Email', } widgets = { 'chi_tacc_appliance_id': forms.TextInput(attrs={'placeholder': ''}), 'chi_uc_appliance_id': forms.TextInput(attrs={'placeholder': ''}), 'kvm_tacc_appliance_id': forms.TextInput(attrs={'placeholder': ''}), } help_texts = { 'description': markdown_allowed(), 'documentation': markdown_allowed() }
class AdminRevokeForm(forms.Form): user_reason = forms.CharField(label='Reason for rejection (user)', required=False, widget=forms.Textarea(attrs={'rows': 5}), help_text=markdown_allowed()) internal_reason = forms.CharField(label='Reason for rejection (internal)', required=False, widget=forms.Textarea(attrs={'rows': 5}), help_text=markdown_allowed())
class Election(ExtraInfoMixin, models.Model): name = models.CharField(max_length=255) slug = AutoSlugField(populate_from='name', unique=True) description = models.TextField(blank=True) tags = TaggableManager(blank=True) searchable = models.BooleanField(default=True) highlighted = models.BooleanField(default=False) extra_info_title = models.CharField(max_length=50, blank=True, null=True) extra_info_content = models.TextField(max_length=3000, blank=True, null=True, help_text=_("Puedes usar Markdown. <br/> ") + markdown_allowed()) uses_preguntales = models.BooleanField(default=False, help_text=_(u"Esta elección debe usar preguntales?")) uses_ranking = models.BooleanField(default=False, help_text=_(u"Esta elección debe usar ranking")) uses_face_to_face = models.BooleanField(default=True, help_text=_(u"Esta elección debe usar frente a frente")) uses_soul_mate = models.BooleanField(default=True, help_text=_(u"Esta elección debe usar 1/2 naranja")) uses_questionary = models.BooleanField(default=True, help_text=_(u"Esta elección debe usar cuestionario")) default_extra_info = settings.DEFAULT_ELECTION_EXTRA_INFO area = models.ForeignKey(Area, null=True, related_name="elections") def __unicode__(self): return self.name def get_absolute_url(self): return reverse('election_view', kwargs={'slug': self.slug}) def get_extra_info_url(self): return reverse('election_extra_info', kwargs={'slug': self.slug}) class Meta: verbose_name = _(u'Mi Elección') verbose_name_plural = _(u'Mis Elecciones')
class Tournament(models.Model): id = models.IntegerField(primary_key=True) # Tournament display information title = models.CharField(max_length=100) description = models.TextField(blank=True, help_text=markdown_allowed()) start = models.DateTimeField(default=timezone.now) end = models.DateTimeField(default=timezone.now) # TODO: handle signups through warwick.gg itself rather than an external signup form. signup_form = models.URLField() signup_start = models.DateTimeField(default=timezone.now) signup_end = models.DateTimeField(default=timezone.now) # Associated event for_event = models.ForeignKey(Event, on_delete=models.CASCADE, blank=True, null=True) requires_attendance = models.BooleanField(default=False) # Routing slug = models.SlugField(max_length=40, unique=True) @property def signups_open(self): return self.signup_start < timezone.now() <= self.signup_end def get_absolute_url(self): return '/events/tournament/{slug}'.format(slug=self.slug)
class Election(ExtraInfoMixin, models.Model, OGPMixin): name = models.CharField(max_length=255) slug = AutoSlugField(populate_from='name', unique=True) description = models.TextField(blank=True) tags = TaggableManager(blank=True) searchable = models.BooleanField(default=True) highlighted = models.BooleanField(default=False) extra_info_title = models.CharField(max_length=50, blank=True, null=True) extra_info_content = models.TextField( max_length=3000, blank=True, null=True, help_text=_("Puedes usar Markdown. <br/> ") + markdown_allowed()) uses_preguntales = models.BooleanField( default=False, help_text=_(u"Esta elección debe usar preguntales?")) uses_ranking = models.BooleanField( default=False, help_text=_(u"Esta elección debe usar ranking")) uses_face_to_face = models.BooleanField( default=True, help_text=_(u"Esta elección debe usar frente a frente")) uses_soul_mate = models.BooleanField( default=True, help_text=_(u"Esta elección debe usar 1/2 naranja")) uses_questionary = models.BooleanField( default=True, help_text=_(u"Esta elección debe usar cuestionario")) position = models.CharField(default='', null=True, blank=True, max_length=255, help_text=_(u'A qué cargo está postulando?')) default_extra_info = settings.DEFAULT_ELECTION_EXTRA_INFO area = models.ForeignKey(Area, blank=True, null=True, related_name="elections") ogp_enabled = True def __unicode__(self): return self.name def get_absolute_url(self): return reverse('election_view', kwargs={'slug': self.slug}) def ranking(self): return Candidate.ranking.filter(elections=self) def position_in_ranking(self, candidate): qs = self.ranking() return get_position_in_(qs, candidate) def get_extra_info_url(self): return reverse('election_extra_info', kwargs={'slug': self.slug}) def has_anyone_answered(self): return TakenPosition.objects.filter( person__in=self.candidates.all()).exists() class Meta: verbose_name = _(u'Mi Elección') verbose_name_plural = _(u'Mis Elecciones')
class Election(models.Model): name = models.CharField(max_length=255) slug = AutoSlugField(populate_from='name', unique=True) description = models.TextField(blank=True) tags = TaggableManager(blank=True) can_election = models.OneToOneField(CanElection, null=True, blank=True) searchable = models.BooleanField(default=True) highlighted = models.BooleanField(default=False) popit_api_instance = models.ForeignKey(PopitApiInstance, null=True, blank=True) writeitinstance = models.ForeignKey(WriteItInstance, null=True, blank=True) extra_info_title = models.CharField(max_length=50, blank=True, null=True) extra_info_content = models.TextField(max_length=3000, blank=True, null=True, help_text=_("Puedes usar Markdown. <br/> ") + markdown_allowed()) uses_preguntales = models.BooleanField(default=True, help_text=_(u"Esta elección debe usar preguntales?")) uses_ranking = models.BooleanField(default=True, help_text=_(u"Esta elección debe usar ranking")) uses_face_to_face = models.BooleanField(default=True, help_text=_(u"Esta elección debe usar frente a frente")) uses_soul_mate = models.BooleanField(default=True, help_text=_(u"Esta elección debe usar 1/2 naranja")) uses_questionary = models.BooleanField(default=True, help_text=_(u"Esta elección debe usar cuestionario")) def __unicode__(self): return self.name def get_absolute_url(self): return reverse('election_view', kwargs={'slug': self.slug}) def get_extra_info_url(self): return reverse('election_extra_info', kwargs={'slug': self.slug}) class Meta: verbose_name = _(u'Mi Elección') verbose_name_plural = _(u'Mis Elecciones')
class Post(models.Model): """ Database model for individual posts. """ contentHelpText = markdown_allowed() + " <a id='ref'>Quick reference</a>" title = models.CharField(max_length=100) content = models.TextField(help_text=contentHelpText) date_posted = models.DateTimeField(auto_now_add=True) author = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) view_count = models.PositiveIntegerField(default=0) tags = TaggableManager() TAGGIT_CASE_INSENSITIVE = True class Meta: ordering = ['-date_posted'] def __str__(self): return self.title def get_absolute_url(self): return reverse('post-detail', kwargs={'pk': self.pk}) def getContent(self): return markdown(self.content)
class Bio(models.Model): artist = models.OneToOneField(Artist, on_delete=models.CASCADE) bio = models.TextField(help_text="Short biography of artist. " + markdown_allowed()) def __str__(self): return str(self.artist)
class AlbumBio(models.Model): album = models.OneToOneField(Album, on_delete=models.CASCADE) bio = models.TextField( help_text="Tracklisting and other info about the album. " + markdown_allowed()) def __str__(self): return str(self.album)
class PostForm(forms.ModelForm): title = forms.CharField() body = forms.CharField( widget=PagedownWidget(), help_text=markdown_allowed() ) class Meta: model = Post fields = ['title', 'body']
class Update(models.Model): artist = models.ForeignKey(Artist, on_delete=models.CASCADE) created_datetime = models.DateTimeField(db_index=True, auto_now_add=True) title = models.CharField(max_length=75) text = models.TextField(help_text="The content of the update. " + markdown_allowed()) def __str__(self): return self.title
class Tournament(models.Model): id = models.IntegerField(primary_key=True) # Tournament display information title = models.CharField(max_length=100) description = models.TextField(blank=True, help_text=markdown_allowed()) start = models.DateTimeField(default=timezone.now) end = models.DateTimeField(default=timezone.now) # TODO: handle signups through warwick.gg itself rather than an external signup form. signup_form = models.URLField() signup_start = models.DateTimeField(default=timezone.now) signup_end = models.DateTimeField(default=timezone.now) # Associated event for_event = models.ForeignKey(Event, on_delete=models.CASCADE, blank=True, null=True) requires_attendance = models.BooleanField(default=False) # Routing slug = models.SlugField(max_length=40, unique=True) objects = TournamentManager() def __str__(self): return self.title @property def signups_open(self): return self.signup_start < timezone.now() <= self.signup_end @property def is_onging(self): return timezone.now() < self.end def get_absolute_url(self): return '/events/tournament/{slug}'.format(slug=self.slug) # class TournamentSignupManager(models.Manager): # pass # # # class TournamentSignup(models.Model): # user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) # tournament = models.ForeignKey(Tournament, on_delete=models.CASCADE) # created_at = models.DateTimeField(default=timezone.now) # comment = models.TextField(blank=True, max_length=255) # # # Stripe transaction tokens in case of refund # ticket = models.OneToOneField(Ticket, related_name='signup', on_delete=models.CASCADE, blank=True, null=True) # # objects = TournamentSignupManager()
class Eleccion(models.Model): nombre = models.CharField(max_length=255) popit_api_instance = models.OneToOneField(ApiInstance) write_it_instance = models.OneToOneField(WriteItInstance, null=True) slug = models.CharField(max_length=255) main_embedded = models.CharField(max_length=512, blank=True, null=True) messaging_extra_app_url = models.CharField(max_length=512, blank=True, null=True) mapping_extra_app_url = models.CharField(max_length=512, blank=True, null=True) featured = models.BooleanField(default=False) searchable = models.BooleanField(default=True) featured_caption = models.CharField(max_length=100, blank=True, null=True) extra_info_title = models.CharField(max_length=50, blank=True, null=True) extra_info_content = models.TextField( max_length=3000, blank=True, null=True, help_text="Puedes usar Markdown. <br/> " + markdown_allowed()) def __unicode__(self): return self.nombre def preguntas(self): #solo preguntas aprobadas candidatos_eleccion = Candidato.objects.filter(eleccion=self) preguntas_candidatos_eleccion = Pregunta.objects.filter( aprobada=True).filter( candidato__in=candidatos_eleccion).distinct() return preguntas_candidatos_eleccion def numero_preguntas(self): preg = self.preguntas() return preg.count() def numero_respuestas(self): resp = Respuesta.objects.filter(pregunta__in=self.preguntas()).exclude( texto_respuesta=settings.NO_ANSWER_DEFAULT_MESSAGE).distinct() return resp.count()
class Request(HasMetadata): """ Represents a request to grant a user a role. There may be many requests for each access(role/user combination). A request is considered 'active' if: 1. It is the most recent request for the role/user combination 2. It was requested after the active grant for the role/user combination A can be in one of three states: PENDING, REJECTED or APPROVED. A rejected request will have a reason to present to the user, and an approved request will have an associated grant. A request can have arbitrary metadata associated with it. That metadata is defined by the service. """ class Meta: ordering = ( 'access__role__service__category__position', 'access__role__service__category__long_name', 'access__role__service__position', 'access__role__service__name', 'access__role__position', 'access__role__name', '-requested_at', ) get_latest_by = 'requested_at' permissions = (('decide_request', 'Can make decisions on requests'), ) objects = RequestQuerySet.as_manager() #: The role/user combination that the request is for access = models.ForeignKey(Access, models.CASCADE, related_name='requests', related_query_name='request') #: Username of the user who requested the role requested_by = models.CharField(max_length=200) #: The datetime at which the service request was created requested_at = models.DateTimeField(auto_now_add=True) #: The current state of the request state = RequestState.model_field(default=RequestState.PENDING) #: True if request requires more information incomplete = models.BooleanField(default=False) #: If approved, this is the resulting access grant resulting_grant = models.OneToOneField(Grant, models.SET_NULL, null=True, blank=True, related_name='request') #: If approved, this is the access grant being superceeded previous_grant = models.ForeignKey(Grant, models.SET_NULL, null=True, blank=True, related_name='next_requests') #: Request that this request superceeds previous_request = models.OneToOneField('self', models.SET_NULL, null=True, blank=True, related_name='next_request') #: If rejected, this is a reason for the user user_reason = models.TextField(blank=True, verbose_name='Reason for rejection (user)', help_text=markdown_allowed()) #: Optional internal reason, for sensitive details internal_reason = models.TextField( blank=True, verbose_name='Reason for rejection (internal)', help_text=markdown_allowed()) def __str__(self): return '{} : {}'.format(self.access, 'INCOMPETE' if self.incomplete else self.state) @property def active(self): """ Returns ``True`` if this request is the active request for the service/role/user combination. """ if not hasattr(self, '_active'): if self.resulting_grant or hasattr(self, 'next_request'): # Unsaved requests won't have a resulting grant self._active = False else: self._active = True return self._active @active.setter def active(self, value): self._active = value @property def pending(self): """ ``True`` if the request is in the PENDING state, ``False`` otherwise. """ return self.state == RequestState.PENDING @property def approved(self): """ ``True`` if the request is in the APPROVED state, ``False`` otherwise. """ return self.state == RequestState.APPROVED @property def rejected(self): """ ``True`` if the request is in the REJECTED state, ``False`` otherwise. """ return self.state == RequestState.REJECTED def clean(self): errors = {} # If we are approved, we need a grant if self.state == RequestState.APPROVED and not self.resulting_grant: errors['grant'] = 'Required for state {}'.format(self.state) # If we have a grant, we must be approved if self.resulting_grant and not self.state == RequestState.APPROVED: errors['grant'] = 'Not allowed for state {}'.format(self.state) # If the state is rejected, we need a user reason if self.state == RequestState.REJECTED and not self.user_reason: errors['user_reason'] = 'Please give a reason for rejection' # If the request is incomplete, it must be rejected if self.incomplete and not self.state == RequestState.REJECTED: errors[ 'state'] = 'Incomplete requests must be in the REJECTED state' try: user = self.access.user # Ensure that the user is active if not user.is_active: errors['user'] = '******' # if not settings.MULTIPLE_REQUESTS_ALLOWED: active_grant = Grant.objects.filter(access=self.access, next_grant__isnull=True) active_request = Request.objects.filter( access=self.access, resulting_grant__isnull=True, next_request__isnull=True) if self.active and active_grant and self.previous_grant != active_grant[ 0]: errors = 'There is already an existing active grant for this access' if self.active and active_request and self != active_request[0]: errors = 'There is already an existing active request for this access' except ObjectDoesNotExist: pass # Check that the grant is for the same service/role/user combination if self.resulting_grant: try: if self.resulting_grant.access != self.access: errors[ 'grant'] = 'Grant must be for same access as request' except ObjectDoesNotExist: pass if errors: raise ValidationError(errors)
class DecisionForm(forms.Form): """ Form for making a decision on a request. """ # Constants defining options for the quick expiry selection EXPIRES_SIX_MONTHS = 1 EXPIRES_ONE_YEAR = 2 EXPIRES_TWO_YEARS = 3 EXPIRES_THREE_YEARS = 4 EXPIRES_FIVE_YEARS = 5 EXPIRES_TEN_YEARS = 6 EXPIRES_CUSTOM = 7 state = forms.TypedChoiceField(label='Decision', choices=[(None, '---------'), ('APPROVED', 'APPROVED'), ('INCOMPLETE', 'INCOMPLETE'), ('REJECTED', 'REJECTED')], coerce=str, empty_value=None) expires = forms.TypedChoiceField( label='Expiry date', help_text= 'Pick a duration from the dropdown list, or pick a custom expiry date', required=False, choices=[ (0, '---------'), (EXPIRES_SIX_MONTHS, 'Six months from now'), (EXPIRES_ONE_YEAR, 'One year from now'), (EXPIRES_TWO_YEARS, 'Two years from now'), (EXPIRES_THREE_YEARS, 'Three years from now'), (EXPIRES_FIVE_YEARS, 'Five years from now'), (EXPIRES_TEN_YEARS, 'Ten years from now'), (EXPIRES_CUSTOM, 'Custom expiry date'), ], coerce=int, empty_value=0) expires_custom = forms.DateField(label='Custom expiry date', required=False, input_formats=['%Y-%m-%d', '%d/%m/%Y'], widget=forms.DateInput( format='%Y-%m-%d', attrs={'type': 'date'})) user_reason = forms.CharField(label='Reason for rejection (user)', required=False, widget=forms.Textarea(attrs={'rows': 5}), help_text=mark_safe(markdown_allowed())) internal_reason = forms.CharField(label='Reason for rejection (internal)', required=False, widget=forms.Textarea(attrs={'rows': 5}), help_text=mark_safe(markdown_allowed())) def __init__(self, request, approver, *args, **kwargs): self._request = request self._approver = approver super().__init__(*args, **kwargs) def clean_state(self): state = self.cleaned_data.get('state') if state is None: raise ValidationError('This field is required') return state def clean_expires(self): state = self.cleaned_data.get('state') expires = self.cleaned_data.get('expires') if state == 'APPROVED' and not expires: raise ValidationError('Please give an expiry date for access') return expires def clean_expires_custom(self): state = self.cleaned_data.get('state') expires = self.cleaned_data.get('expires') expires_custom = self.cleaned_data.get('expires_custom') if state == 'APPROVED' and expires == self.EXPIRES_CUSTOM and not expires_custom: raise ValidationError('Please give an expiry date for access') if expires_custom and expires_custom < date.today(): raise ValidationError('Expiry date must be in the future') return expires_custom def clean_user_reason(self): state = self.cleaned_data.get('state') user_reason = self.cleaned_data.get('user_reason') if state != 'APPROVED' and not user_reason: raise ValidationError( 'Please give a reason for rejection or incompletion') return user_reason def save(self): # Update the request from the form if self.cleaned_data['state'] == 'APPROVED': # Get the expiry date expires = self.cleaned_data['expires'] if expires == self.EXPIRES_SIX_MONTHS: expires_date = date.today() + relativedelta(months=6) elif expires == self.EXPIRES_ONE_YEAR: expires_date = date.today() + relativedelta(years=1) elif expires == self.EXPIRES_TWO_YEARS: expires_date = date.today() + relativedelta(years=2) elif expires == self.EXPIRES_THREE_YEARS: expires_date = date.today() + relativedelta(years=3) elif expires == self.EXPIRES_FIVE_YEARS: expires_date = date.today() + relativedelta(years=5) elif expires == self.EXPIRES_TEN_YEARS: expires_date = date.today() + relativedelta(years=10) else: expires_date = self.cleaned_data['expires_custom'] self._request.state = RequestState.APPROVED # If the request has a previous_grant create a new grant # and link with the old grant previous_grant = self._request.previous_grant if previous_grant: self._request.resulting_grant = Grant.objects.create( access=self._request.access, granted_by=self._approver.username, expires=expires_date, previous_grant=previous_grant) else: # Else create the access if it does not already exist and # then create the new grant access, _ = Access.objects.get_or_create( user=self._request.access.user, role=self._request.access.role) self._request.resulting_grant = Grant.objects.create( access=access, granted_by=self._approver.username, expires=expires_date) # Copy the metadata from the request to the grant self._request.copy_metadata_to(self._request.resulting_grant) else: self._request.state = RequestState.REJECTED self._request.incomplete = True if self.cleaned_data[ 'state'] == 'INCOMPLETE' else False self._request.user_reason = self.cleaned_data['user_reason'] self._request.internal_reason = self.cleaned_data[ 'internal_reason'] self._request.save() return self._request
class Tournament(models.Model): PLATFORM_STEAM = 'S' PLATFORM_BNET = 'B' PLATFORM_LEAGUE = 'L' PLATFORM_SWITCH = 'N' PLATFORM_XBOX = 'X' PLATFORM_PLAYSTATION = 'P' PLATFORM_OTHER = 'O' PLATFORM_CHOICES = ((PLATFORM_STEAM, 'Steam'), (PLATFORM_BNET, 'Battle.NET'), (PLATFORM_LEAGUE, 'League of Legends'), (PLATFORM_SWITCH, 'Nintendo Switch'), (PLATFORM_XBOX, 'Xbox Live'), (PLATFORM_PLAYSTATION, 'Playstation Network'), (PLATFORM_OTHER, 'Other')) id = models.AutoField(primary_key=True) # Tournament display information title = models.CharField(max_length=100) description = models.TextField(blank=True, help_text=markdown_allowed()) start = models.DateTimeField(default=timezone.now) end = models.DateTimeField(default=timezone.now) platform = models.CharField(max_length=1, choices=PLATFORM_CHOICES, help_text='If \"other\" please specify') platform_other = models.CharField(max_length=64, blank=True) games = models.CharField( max_length=1024, blank=True, help_text= 'A comma separated list of the game(s) played in this tournament') signup_start = models.DateTimeField(default=timezone.now) signup_end = models.DateTimeField(default=timezone.now) signup_limit = models.IntegerField(blank=True, null=True) # Associated event for_event = models.ForeignKey(Event, on_delete=models.CASCADE, blank=True, null=True) requires_attendance = models.BooleanField(default=False) # Routing slug = models.SlugField(max_length=40, unique=True) objects = TournamentManager() def __str__(self): return self.title @property def signups_open(self): return self.signup_start < timezone.now() <= self.signup_end @property def is_onging(self): return self.start <= timezone.now() < self.end @property def platform_verbose(self): return dict(Tournament.PLATFORM_CHOICES).get(self.platform).replace( 'Other', self.platform_other) @property def platform_icon(self): return { Tournament.PLATFORM_STEAM: 'steam', Tournament.PLATFORM_BNET: 'battle-net', Tournament.PLATFORM_XBOX: 'xbox', Tournament.PLATFORM_PLAYSTATION: 'playstation', Tournament.PLATFORM_SWITCH: 'nintendo-switch' }.get(self.platform, None) @property def signup_count(self): return len(TournamentSignup.objects.all_for_tournament(self).all()) @property def games_list(self): return list(map(lambda x: x.strip(), self.games.split(','))) @property def signups_remaining(self): return self.signup_limit - len(TournamentSignup.objects.all_for_tournament(self)) if self.signup_limit else \ sys.maxsize @property def signups(self): signups = TournamentSignup.objects.filter(tournament=self, is_unsigned_up=False).exclude(comment__exact='')\ .order_by('commented_at', '-created_at').all() return signups def user_signed_up(self, user): return TournamentSignup.objects.for_tournament(self, user).exists() def get_absolute_url(self): return '/events/tournament/{slug}'.format(slug=self.slug)
class Grant(HasMetadata): """ Represents the granting of a role to a user. There may be many grants for each role/user combination. However, when determining whether a user is approved for a role, only the most recent grant for the role/user combination is considered. This is referred to as the 'active' grant. A grant can have arbitrary metadata associated with it. That metadata is defined by the service. """ class Meta: ordering = ( 'role__service__category__position', 'role__service__category__long_name', 'role__service__position', 'role__service__name', 'role__position', 'role__name', '-granted_at', ) get_latest_by = 'granted_at' objects = GrantQuerySet.as_manager() #: The role that the grant is for role = models.ForeignKey(Role, models.CASCADE, related_name='grants', related_query_name='grant') #: The user for whom the role is granted user = models.ForeignKey(settings.AUTH_USER_MODEL, models.CASCADE) #: Username of the user who granted the role granted_by = models.CharField(max_length=200) #: The datetime at which the role was granted granted_at = models.DateTimeField(auto_now_add=True) #: The date that the the grant expires #: * Access is assumed to expire at the **end** of the given day #: * Default expiry is one year from now expires = models.DateField(default=_default_expiry, verbose_name='expiry date') #: Indicates whether the grant has been revoked #: This overrides any expiry date revoked = models.BooleanField(default=False) #: If revoked, this is a reason for the user user_reason = models.TextField(blank=True, verbose_name='Reason for revocation (user)', help_text=markdown_allowed()) #: Optional internal reason, for sensitive details internal_reason = models.TextField( blank=True, verbose_name='Reason for revocation (internal)', help_text=markdown_allowed()) def __str__(self): return '{} : {}'.format(self.role, self.user) @property def active(self): """ Returns ``True`` if this grant is the active grant for the service/role/user combination. """ if not hasattr(self, '_active'): if self.pk: self._active = self.__class__.objects \ .filter_active() \ .filter(pk = self.pk) \ .exists() else: # Unsaved grants are never active self._active = False return self._active @active.setter def active(self, value): self._active = value @property def expired(self): """ Shortcut to check if a grant has expired. """ return self.expires < date.today() @property def expiring(self): """ Shortcut to check if a grant is expiring in the next 2 months. """ today = date.today() return today <= self.expires < (today + relativedelta(months=2)) def clean(self): errors = {} try: user = self.user # Ensure that the user is active if not user.is_active: errors['user'] = '******' except ObjectDoesNotExist: pass # Ensure that at least a user reason is given if the grant is revoked if self.revoked and not self.user_reason: errors['user_reason'] = 'Please give a reason' # Ensure that expires is in the future if self.expires < date.today(): errors['expires'] = 'Expiry date must be in the future' if errors: raise ValidationError(errors)
class Request(HasMetadata): """ Represents a request to grant a user a role. There may be many requests for each role/user combination. A request is considered 'active' if: 1. It is the most recent request for the role/user combination 2. It was requested after the active grant for the role/user combination A can be in one of three states: PENDING, REJECTED or APPROVED. A rejected request will have a reason to present to the user, and an approved request will have an associated grant. A request can have arbitrary metadata associated with it. That metadata is defined by the service. """ class Meta: ordering = ( 'role__service__category__position', 'role__service__category__long_name', 'role__service__position', 'role__service__name', 'role__position', 'role__name', '-requested_at', ) get_latest_by = 'requested_at' permissions = (('decide_request', 'Can make decisions on requests'), ) objects = RequestQuerySet.as_manager() #: The role that the request is for role = models.ForeignKey(Role, models.CASCADE, related_name='requests', related_query_name='request') #: User for whom the role is being requested user = models.ForeignKey(settings.AUTH_USER_MODEL, models.CASCADE) #: Username of the user who requested the role requested_by = models.CharField(max_length=200) #: The datetime at which the service request was created requested_at = models.DateTimeField(auto_now_add=True) #: The current state of the request state = RequestState.model_field(default=RequestState.PENDING) #: If approved, this is the resulting access grant grant = models.OneToOneField(Grant, models.SET_NULL, null=True, blank=True, related_name='request') #: If rejected, this is a reason for the user user_reason = models.TextField(blank=True, verbose_name='Reason for rejection (user)', help_text=markdown_allowed()) #: Optional internal reason, for sensitive details internal_reason = models.TextField( blank=True, verbose_name='Reason for rejection (internal)', help_text=markdown_allowed()) def __str__(self): return '{} : {} : {}'.format(self.role, self.user, self.state) @property def active(self): """ Returns ``True`` if this request is the active request for the service/role/user combination. """ if not hasattr(self, '_active'): if self.pk: self._active = self.__class__.objects \ .filter_active() \ .filter(pk = self.pk) \ .exists() else: # Unsaved grants are never active self._active = False return self._active @active.setter def active(self, value): self._active = value @property def pending(self): """ ``True`` if the request is in the PENDING state, ``False`` otherwise. """ return self.state == RequestState.PENDING @property def approved(self): """ ``True`` if the request is in the APPROVED state, ``False`` otherwise. """ return self.state == RequestState.APPROVED @property def rejected(self): """ ``True`` if the request is in the REJECTED state, ``False`` otherwise. """ return self.state == RequestState.REJECTED def clean(self): errors = {} # If we are approved, we need a grant if self.state == RequestState.APPROVED and not self.grant: errors['grant'] = 'Required for state {}'.format(self.state) # If we have a grant, we must be approved if self.grant and not self.state == RequestState.APPROVED: errors['grant'] = 'Not allowed for state {}'.format(self.state) # If the state is rejected, we need a user reason if self.state == RequestState.REJECTED and not self.user_reason: errors['user_reason'] = 'Please give a reason for rejection' try: user = self.user # Ensure that the user is active if not user.is_active: errors['user'] = '******' except ObjectDoesNotExist: pass # Check that the grant is for the same service/role/user combination if self.grant: if self.grant.role != self.role: errors['grant'] = 'Grant must be for same role as request' try: if self.grant.service != self.service: errors[ 'grant'] = 'Grant must be for same service as request' if self.grant.user != self.user: errors['grant'] = 'Grant must be for same user as request' except ObjectDoesNotExist: pass if errors: raise ValidationError(errors)
class DecisionForm(forms.Form): """ Form for making a decision on a request. """ # Constants defining options for the quick expiry selection EXPIRES_SIX_MONTHS = 1 EXPIRES_ONE_YEAR = 2 EXPIRES_TWO_YEARS = 3 EXPIRES_THREE_YEARS = 4 EXPIRES_FIVE_YEARS = 5 EXPIRES_TEN_YEARS = 6 EXPIRES_CUSTOM = 7 approved = forms.NullBooleanField( label='Decision', widget=forms.Select( choices=[(None, '---------'), (True, 'APPROVED'), (False, 'REJECTED')])) expires = forms.TypedChoiceField( label='Expiry date', help_text= 'Pick a duration from the dropdown list, or pick a custom expiry date', required=False, choices=[ (0, '---------'), (EXPIRES_SIX_MONTHS, 'Six months from now'), (EXPIRES_ONE_YEAR, 'One year from now'), (EXPIRES_TWO_YEARS, 'Two years from now'), (EXPIRES_THREE_YEARS, 'Three years from now'), (EXPIRES_FIVE_YEARS, 'Five years from now'), (EXPIRES_TEN_YEARS, 'Ten years from now'), (EXPIRES_CUSTOM, 'Custom expiry date'), ], coerce=int, empty_value=0) expires_custom = forms.DateField(label='Custom expiry date', required=False, input_formats=['%Y-%m-%d', '%d/%m/%Y'], widget=forms.DateInput( format='%Y-%m-%d', attrs={'type': 'date'})) user_reason = forms.CharField(label='Reason for rejection (user)', required=False, widget=forms.Textarea(attrs={'rows': 5}), help_text=markdown_allowed()) internal_reason = forms.CharField(label='Reason for rejection (internal)', required=False, widget=forms.Textarea(attrs={'rows': 5}), help_text=markdown_allowed()) def __init__(self, request, approver, *args, **kwargs): self._request = request self._approver = approver super().__init__(*args, **kwargs) def clean_approved(self): approved = self.cleaned_data.get('approved') if approved is None: raise ValidationError('This field is required') return approved def clean_expires(self): approved = self.cleaned_data.get('approved') expires = self.cleaned_data.get('expires') if approved and not expires: raise ValidationError('Please give an expiry date for access') return expires def clean_expires_custom(self): approved = self.cleaned_data.get('approved') expires = self.cleaned_data.get('expires') expires_custom = self.cleaned_data.get('expires_custom') if approved and expires == self.EXPIRES_CUSTOM and not expires_custom: raise ValidationError('Please give an expiry date for access') if expires_custom and expires_custom < date.today(): raise ValidationError('Expiry date must be in the future') return expires_custom def clean_user_reason(self): approved = self.cleaned_data.get('approved') user_reason = self.cleaned_data.get('user_reason') if approved is False and not user_reason: raise ValidationError('Please give a reason for rejection') return user_reason def save(self): # Update the request from the form if self.cleaned_data['approved']: # Get the expiry date expires = self.cleaned_data['expires'] if expires == self.EXPIRES_SIX_MONTHS: expires_date = date.today() + relativedelta(months=6) elif expires == self.EXPIRES_ONE_YEAR: expires_date = date.today() + relativedelta(years=1) elif expires == self.EXPIRES_TWO_YEARS: expires_date = date.today() + relativedelta(years=2) elif expires == self.EXPIRES_THREE_YEARS: expires_date = date.today() + relativedelta(years=3) elif expires == self.EXPIRES_FIVE_YEARS: expires_date = date.today() + relativedelta(years=5) elif expires == self.EXPIRES_TEN_YEARS: expires_date = date.today() + relativedelta(years=10) else: expires_date = self.cleaned_data['expires_custom'] self._request.state = RequestState.APPROVED # If the request was approved, create the grant self._request.grant = Grant.objects.create( role=self._request.role, user=self._request.user, granted_by=self._approver.username, expires=expires_date) # Copy the metadata from the request to the grant self._request.copy_metadata_to(self._request.grant) else: self._request.state = RequestState.REJECTED self._request.user_reason = self.cleaned_data['user_reason'] self._request.internal_reason = self.cleaned_data[ 'internal_reason'] self._request.save() return self._request
class Event(models.Model): SOCIETY_CHOICES = ( ('UWCS', 'Uni of Warwick Computing Society'), ('WE', 'Warwick Esports'), ) id = models.IntegerField(primary_key=True) # Event display information title = models.CharField(max_length=100) description = models.TextField(blank=True, help_text=markdown_allowed()) start = models.DateTimeField(default=timezone.now) end = models.DateTimeField(default=timezone.now) location = models.CharField(max_length=100) # Signup information signup_start = models.DateTimeField(default=timezone.now) signup_end = models.DateTimeField(default=timezone.now) signup_start_fresher = models.DateTimeField(blank=True, null=True) signup_limit = models.IntegerField(default=70) # Society eligibility criteria hosted_by = MultiSelectField(blank=True, choices=SOCIETY_CHOICES, max_choices=2) cost_member = models.DecimalField(default=0, decimal_places=2, max_digits=3) cost_non_member = models.DecimalField(default=5, decimal_places=2, max_digits=3) # Routing slug = models.SlugField(max_length=40, unique=True) # Seating plan has_seating = models.BooleanField(default=True) seating_location = models.ForeignKey(SeatingRoom, on_delete=models.PROTECT, blank=True, null=True) # Signup options has_photography = models.BooleanField(default=False) has_livestream = models.BooleanField(default=False) def __str__(self): return self.title def __bytes__(self): return bytes(self.title.encode()) + bytes(self.id) + bytes( str(self.start).encode()) def get_absolute_url(self): return '/events/{slug}'.format(slug=self.slug) @property def signups(self): signups = EventSignup.objects.filter( event=self, is_unsigned_up=False).exclude(comment__exact='').order_by( 'commented_at', '-created_at').all() return signups @property def signups_all(self): signups = EventSignup.objects.filter( event=self, is_unsigned_up=False).order_by('-created_at').all() return signups @property def signup_count(self): return len(self.signups_all) @property def signups_left(self): return self.signup_limit - self.signup_count def signup_start_for_user(self, user): if self.signup_start_fresher: if not user.is_authenticated: return self.signup_start else: profile = WarwickGGUser.objects.get(user=user) return self.signup_start_fresher if profile.is_fresher else self.signup_start else: return self.signup_start def signups_open(self, user): return self.signup_start_for_user(user) < timezone.now( ) <= self.signup_end and self.signup_count < self.signup_limit @property def is_ongoing(self): if self.start < timezone.now() <= self.end: return True else: return False
class Grant(HasMetadata): """ Represents the granting of a role to a user. There may be many grants for each role/user combination. However, when determining whether a user is approved for a role, only grants the head of a grant chain (one without a next_grant) for the role/user combination is considered. These are referred to as the 'active' grants. A grant can have arbitrary metadata associated with it. That metadata is defined by the service. """ class Meta: ordering = ( 'access__role__service__category__position', 'access__role__service__category__long_name', 'access__role__service__position', 'access__role__service__name', 'access__role__position', 'access__role__name', '-granted_at', ) get_latest_by = 'granted_at' indexes = [models.Index(fields=['access', 'granted_at'])] objects = GrantQuerySet.as_manager() #: The role/user combination that the grant is for access = models.ForeignKey(Access, models.CASCADE, related_name='grants', related_query_name='grant') #: Username of the user who granted the role granted_by = models.CharField(max_length=200) #: The datetime at which the role was granted granted_at = models.DateTimeField(auto_now_add=True) #: The date that the the grant expires #: * Access is assumed to expire at the **end** of the given day #: * Default expiry is one year from now expires = models.DateField(default=_default_expiry, verbose_name='expiry date') #: Indicates whether the grant has been revoked #: This overrides any expiry date revoked = models.BooleanField(default=False) #: If revoked, this is a reason for the user user_reason = models.TextField(blank=True, verbose_name='Reason for revocation (user)', help_text=markdown_allowed()) #: Optional internal reason, for sensitive details internal_reason = models.TextField( blank=True, verbose_name='Reason for revocation (internal)', help_text=markdown_allowed()) #: Grant that this grant superceeds previous_grant = models.OneToOneField('self', models.SET_NULL, null=True, blank=True, related_name='next_grant') def __str__(self): if hasattr(self, 'next_grant'): return '{} : old'.format(self.access) else: return '{} : active'.format(self.access) @property def active(self): """ Returns ``True`` if this grant is the active grant for the service/role/user combination. """ if not hasattr(self, '_active'): if not hasattr(self, 'next_grant'): self._active = True else: # Unsaved grants are never active self._active = False return self._active @active.setter def active(self, value): self._active = value @property def expired(self): """ Shortcut to check if a grant has expired. """ return self.expires < date.today() @property def expiring(self): """ Shortcut to check if a grant is expiring in the next 2 months. """ today = date.today() return today <= self.expires < (today + relativedelta(months=2)) def clean(self): errors = {} try: user = self.access.user # Ensure that the user is active if not user.is_active: errors['user'] = '******' if not settings.MULTIPLE_REQUESTS_ALLOWED: active_grant = Grant.objects.filter(access=self.access, next_grant__isnull=True) active_request = Request.objects.filter( access=self.access, resulting_grant__isnull=True, next_request__isnull=True) if self.active and active_grant and self.previous_grant != active_grant[ 0] and self != active_grant[0]: errors = 'There is already an existing active grant for this access' if self.active and active_request: errors = 'There is already an existing active request for this access' except ObjectDoesNotExist: pass # Ensure that at least a user reason is given if the grant is revoked if self.revoked and not self.user_reason: errors['user_reason'] = 'Please give a reason' # Ensure that expires is in the future if self.expires < date.today(): errors['expires'] = 'Expiry date must be in the future' if errors: raise ValidationError(errors)