class Sentiment(models.TimeStampedModel): """ Represents the sentiment of a user towards a feature in a given instant of time. """ STATUS_VERY_HAPPY, STATUS_HAPPY, STATUS_NEUTRAL, STATUS_SAD = range(4) STATUS_CHOICES = [ (STATUS_VERY_HAPPY, _('Very happy')), (STATUS_HAPPY, _('Happy')), (STATUS_NEUTRAL, _('Neutral')), (STATUS_SAD, _('Sad')), ] status = models.SmallIntegerField( _('status'), choices=STATUS_CHOICES, ) user = models.ForeignKey(models.User, related_name='sentiments') feature = models.ForeignKey( Feature, related_name='sentiments', ) board = models.ForeignKey( SentimentBoard, related_name='sentiments', )
class Task(models.TimeStampedModel): """ Represents a task in a Kanban activity. """ STATUS_TODO, STATUS_DOING, STATUS_DONE = range(3) STATUS_CHOICES = [ (STATUS_TODO, _('To do')), (STATUS_DOING, _('Doing')), (STATUS_DONE, _('Done')), ] name = models.CodeschoolNameField() description = models.CodeschoolDescriptionField() status = models.SmallIntegerField( _('status'), choices=STATUS_CHOICES, ) members = models.ManyToManyField( models.User, related_name='kanban_tasks', ) created_by = models.ForeignKey( models.User, related_name='created_tasks', null=True, blank=True, ) assigned_to = models.ManyToManyField( models.User, related_name='assigned_tasks', ) estimated_duration_hours = models.PositiveSmallIntegerField( _('Estimated duration (hours)'), default=0, ) objects = TaskQuerySet.as_manager()
class AttendanceCheck(models.Model): """ Confirms attendance by an user. """ user = models.ForeignKey(models.User) event = models.ForeignKey(Event) has_attended = models.BooleanField(default=bool) attempts = models.SmallIntegerField(default=int) def update(self, phrase): """ Update check with the given passphrase. """ sheet = self.event.sheet if self.attempts > sheet.max_attempts: return if string_distance(phrase, self.event.passphrase) <= sheet.max_string_distance: self.has_attended = True self.attempts += 1 self.save()
class Profile(UserenaBaseProfile): """ Userena profile. Social information about our users. """ user = models.OneToOneField( models.User, unique=True, verbose_name=_('user'), related_name='profile', ) nickname = models.CharField(max_length=50, blank=True, null=True) phone = models.CharField(max_length=20, blank=True, null=True) gender = models.SmallIntegerField(_('gender'), choices=[(0, _('male')), (1, _('female'))], blank=True, null=True) date_of_birth = models.DateField(_('date of birth'), blank=True, null=True) website = models.URLField(blank=True, null=True) about_me = models.TextField(blank=True, null=True) @property def age(self): if self.date_of_birth is None: return None today = timezone.now().date() return int(round((today - self.date_of_birth).days / 365.25)) @property def contact_classes(self): user = self.user try: friends = user.friends colleagues = user.staff_contacts staff = user.colleagues except AttributeError as ex: raise RuntimeError(ex) return [friends, colleagues, staff] def custom_fields(self, flat=False): """Return a dictionary with all custom fields""" D = {} for value in self.custom_field_values.all(): category = value.category_name field_name = value.field_name data = value.data if flat: D[(category, field_name)] = data else: category_dict = D.setdefault(category, {}) category_dict[field_name] = data return D class Meta: permissions = ( ('student', _('Can access/modify data visible to student\'s')), ('teacher', _('Can access/modify data visible only to Teacher\'s')), ) def __str__(self): if self.user is None: return __('unbound profile') return __('%(name)s\'s profile') % {'name': self.user.get_full_name()} def __repr__(self): return str(self.contact_classes)
class AttendanceSheet(models.Model): """ Controls student attendance by generating a new public passphrase under teacher request. Students confirm attendance by typing the secret phrase in a small interval. """ max_attempts = models.SmallIntegerField(default=3) expiration_minutes = models.SmallIntegerField(default=5) owner = models.ForeignKey(models.User) last_event = models.ForeignKey('Event', blank=True, null=True) max_string_distance = models.SmallIntegerField(default=0) max_number_of_absence = models.IntegerField(blank=True, null=True) @property def expiration_interval(self): return datetime.timedelta(minutes=self.expiration_minutes) @property def attendance_checks(self): return AttendanceCheck.objects.filter(event__sheet=self) def new_event(self): """ Create a new event in attendance sheet. """ current_time = now() new = self.events.create(passphrase=new_random_passphrase(), date=current_time.date(), created=current_time, expires=current_time + self.expiration_interval) self.last_event = new self.save(update_fields=['last_event']) return new def get_today_event(self): """ Return the last event created for today """ if self.last_event.date() == now().date(): return self.last_event else: return self.new_event() def number_of_absences(self, user): """ Return the total number of absence for user. """ return self.attendance_checks.filter(user=user, has_attended=False).count() def absence_table(self, users=None, method='fraction'): """ Return a mapping between users and their respective absence rate. Args: users: A queryset of users. method: One of 'fraction' (default), 'number', 'attendance' or 'attendance-fraction' """ try: get_value_from_absence = { 'fraction': lambda x: x / num_events, 'number': lambda x: x, 'attendance': lambda x: num_events - x, 'attendance-fraction': lambda x: (num_events - x) / num_events }[method] except KeyError: raise ValueError('invalid method: %r' % method) num_events = self.events.count() if users is None: users = models.User.objects.all() result = collections.OrderedDict() for user in users: absence = self.user_absence(user) result[user] = get_value_from_absence(absence) return result def render_dialog(self, request): """ Renders attendance dialog based on request. """ context = { 'passphrase': self.passphrase, 'is_expired': self.is_expired(), 'minutes_left': self.minutes_left(raises=False) } user = request.user if user == self.owner: template = 'attendance/edit.jinja2' else: template = 'attendance/view.jinja2' context['attempts'] = self.user_attempts(user) return render_to_string(template, request=request, context=context) def user_attempts(self, user): """ Return the number of user attempts in the last attendance event. """ if self.last_event is None: return 0 qs = self.attendance_checks.filter(user=user, event=self.last_event) return qs.count() def minutes_left(self, raises=True): """ Return how many minutes left for expiration. """ if self.last_event: time = now() if self.last_event.expires < time: return 0.0 else: dt = self.last_event.expires - time return dt.minutes if raises: raise ValueError('last event is not defined') else: return None def is_expired(self): """ Return True if last_event has already expired. """ if not self.last_event: return False return self.last_event.expires < now()
class Profile(UserenaBaseProfile, models.CodeschoolPage): """ Social information about users. """ class Meta: permissions = ( ('student', _('Can access/modify data visible to student\'s')), ('teacher', _('Can access/modify data visible only to Teacher\'s')), ) username = delegate_to('user', True) first_name = delegate_to('user') last_name = delegate_to('user') email = delegate_to('user') @property def short_description(self): return '%s (id: %s)' % (self.get_full_name_or_username(), self.school_id) @property def age(self): if self.date_of_birth is None: return None today = timezone.now().date() return int(round((today - self.date_of_birth).years)) user = models.OneToOneField( models.User, unique=True, blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_('user'), related_name='_profile', ) school_id = models.CharField( _('school id'), help_text=_('Identification number in your school issued id card.'), max_length=50, blank=True, null=True) nickname = models.CharField(max_length=50, blank=True, null=True) phone = models.CharField(max_length=20, blank=True, null=True) gender = models.SmallIntegerField(_('gender'), choices=[(0, _('male')), (1, _('female'))], blank=True, null=True) date_of_birth = models.DateField(_('date of birth'), blank=True, null=True) website = models.URLField(blank=True, null=True) about_me = models.RichTextField(blank=True, null=True) objects = ProfileManager.from_queryset(models.PageManager)() def __init__(self, *args, **kwargs): if 'user' in kwargs and 'id' not in kwargs: kwargs.setdefault('parent_page', profile_root()) super().__init__(*args, **kwargs) if self.pk is None and self.user is not None: user = self.user self.title = self.title or __("%(name)s's profile") % { 'name': user.get_full_name() or user.username } self.slug = self.slug or user.username.replace('.', '-') def __str__(self): if self.user is None: return __('Unbound profile') full_name = self.user.get_full_name() or self.user.username return __('%(name)s\'s profile') % {'name': full_name} def get_full_name_or_username(self): name = self.user.get_full_name() if name: return name else: return self.user.username # Wagtail admin parent_page_types = ['cs_core.ProfileRoot'] content_panels = models.CodeschoolPage.content_panels + [ panels.MultiFieldPanel([ panels.FieldPanel('school_id'), ], heading='Required information'), panels.MultiFieldPanel([ panels.FieldPanel('nickname'), panels.FieldPanel('phone'), panels.FieldPanel('gender'), panels.FieldPanel('date_of_birth'), ], heading=_('Personal Info')), panels.MultiFieldPanel([ panels.FieldPanel('website'), ], heading=_('Web presence')), panels.RichTextFieldPanel('about_me'), ]
class FormEntry(models.Model): """ An entry in a form. """ TYPE_STRING, TYPE_TEXT, TYPE_BOOLEAN, TYPE_INT, TYPE_FLOAT, TYPE_DATE = \ range(5) TYPE_CHOICES = [ (TYPE_STRING, _('String')), (TYPE_TEXT, _('Text')), (TYPE_BOOLEAN, _('Boolean')), (TYPE_INT, _('Integer')), (TYPE_FLOAT, _('Numeric')), (TYPE_DATE, _('Date')), ] TYPE_MAP = { TYPE_STRING: str, TYPE_TEXT: str, TYPE_BOOLEAN: bool, TYPE_INT: int, TYPE_FLOAT: float, TYPE_DATE: date, } form = models.ForeignKey( FormQuestion, related_name='entries', on_delete=models.CASCADE, ) name = models.CharField(max_length=30) label = models.CharField(max_length=40) help = models.CharField(max_length=140, help_text=True) type = models.SmallIntegerField(choices=TYPE_CHOICES) default = models.CharField(max_length=40, blank=True) placeholder = models.CharField(max_length=40, blank=True) grader_json = models.JSONField() class Meta: unique_together = [('name', 'form')] @property def py_type(self): return self.TYPE_MAP[self.type] @lazy def grader(self): return answer_from_json(self.grader_json) def clean(self): if self.default and not self._can_have_default(): raise ValidationError({ 'default': _('cannot define default values for this type of value.') }) self._validate_default() if self.placeholder and not self._can_have_placeholder(): raise ValidationError({ 'placeholder': _('cannot define a placeholder for this type of value.') }) def _can_placeholder(self): return self.py_type != bool def _can_have_default(self): return self.py_type in {str, int, float, date} def _validate_default(self): tt = self.py_type data = self.default try: if tt in {int, float}: tt(data) elif tt == date: yy, dd, aa = map(int, date.split('/')) elif tt == bool: if data not in {'true', 'false'}: raise ValueError except (ValueError, IndexError) as ex: raise ValidationError({'default': 'invalid default value'}) return self
class Profile(UserenaBaseProfile, models.Page): """ Social information about users. """ class Meta: permissions = ( ('student', _('Can access/modify data visible to student\'s')), ('teacher', _('Can access/modify data visible only to Teacher\'s')), ) user = models.OneToOneField( User, unique=True, blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_('user'), related_name='profile', ) school_id = models.CharField( _('school id'), help_text=_('Identification number in your school issued id card.'), max_length=50, blank=True, null=True) nickname = models.CharField(max_length=50, blank=True, null=True) phone = models.CharField(max_length=20, blank=True, null=True) gender = models.SmallIntegerField(_('gender'), choices=[(0, _('male')), (1, _('female'))], blank=True, null=True) date_of_birth = models.DateField(_('date of birth'), blank=True, null=True) website = models.URLField(blank=True, null=True) about_me = models.RichTextField(blank=True, null=True) objects = ProfileManager() # Delegates and properties username = delegate_to('user', True) first_name = delegate_to('user') last_name = delegate_to('user') email = delegate_to('user') @property def short_description(self): return '%s (id: %s)' % (self.get_full_name_or_username(), self.school_id) @property def age(self): if self.date_of_birth is None: return None today = timezone.now().date() birthday = self.date_of_birth years = today.year - birthday.year birthday = datetime.date(today.year, birthday.month, birthday.day) if birthday > today: return years - 1 else: return years def __str__(self): if self.user is None: return __('Unbound profile') full_name = self.user.get_full_name() or self.user.username return __('%(name)s\'s profile') % {'name': full_name} def save(self, *args, **kwargs): user = self.user if not self.title: self.title = self.title or __("%(name)s's profile") % { 'name': user.get_full_name() or user.username } if not self.slug: self.slug = user.username.replace('.', '-') # Set parent page, if necessary if not self.path: root = ProfileList.objects.instance() root.add_child(instance=self) else: super().save(*args, **kwargs) def get_full_name_or_username(self): name = self.user.get_full_name() if name: return name else: return self.user.username # Serving pages template = 'cs_auth/profile-detail.jinja2' def get_context(self, request, *args, **kwargs): context = super().get_context(request, *args, **kwargs) context['profile'] = self return context # Wagtail admin parent_page_types = ['ProfileList'] content_panels = models.Page.content_panels + [ panels.MultiFieldPanel([ panels.FieldPanel('school_id'), ], heading='Required information'), panels.MultiFieldPanel([ panels.FieldPanel('nickname'), panels.FieldPanel('phone'), panels.FieldPanel('gender'), panels.FieldPanel('date_of_birth'), ], heading=_('Personal Info')), panels.MultiFieldPanel([ panels.FieldPanel('website'), ], heading=_('Web presence')), panels.RichTextFieldPanel('about_me'), ]
class Profile(UserenaBaseProfile): """ Social information about users. """ class Meta: permissions = ( ('student', _('Can access/modify data visible to student\'s')), ('teacher', _('Can access/modify data visible only to Teacher\'s')), ) GENDER_MALE, GENDER_FEMALE = 0, 1 user = models.OneToOneField( User, verbose_name=_('user'), unique=True, blank=True, null=True, on_delete=models.SET_NULL, related_name='profile', ) school_id = models.CharField( _('school id'), max_length=50, blank=True, null=True, help_text=_('Identification number in your school issued id card.'), ) is_teacher = models.BooleanField(default=False) nickname = models.CharField(max_length=50, blank=True, null=True) phone = models.CharField(max_length=20, blank=True, null=True) gender = models.SmallIntegerField(_('gender'), choices=[(GENDER_MALE, _('Male')), (GENDER_FEMALE, _('Female'))], blank=True, null=True) date_of_birth = models.DateField(_('date of birth'), blank=True, null=True) website = models.URLField(blank=True, null=True) about_me = models.RichTextField(blank=True, null=True) # Delegates and properties username = delegate_to('user', True) first_name = delegate_to('user') last_name = delegate_to('user') email = delegate_to('user') @property def age(self): if self.date_of_birth is None: return None today = timezone.now().date() birthday = self.date_of_birth years = today.year - birthday.year birthday = datetime.date(today.year, birthday.month, birthday.day) if birthday > today: return years - 1 else: return years def __str__(self): if self.user is None: return __('Unbound profile') full_name = self.user.get_full_name() or self.user.username return __('%(name)s\'s profile') % {'name': full_name} def get_full_name_or_username(self): name = self.user.get_full_name() if name: return name else: return self.user.username def get_absolute_url(self): return reverse('auth:profile-detail', kwargs={'username': self.user.username}) # Serving pages template = 'cs_auth/profile-detail.jinja2' def get_context(self, request, *args, **kwargs): context = super().get_context(request, *args, **kwargs) context['profile'] = self return context # Wagtail admin panels = [ panels.MultiFieldPanel([ panels.FieldPanel('school_id'), ], heading='Required information'), panels.MultiFieldPanel([ panels.FieldPanel('nickname'), panels.FieldPanel('phone'), panels.FieldPanel('gender'), panels.FieldPanel('date_of_birth'), ], heading=_('Personal Info')), panels.MultiFieldPanel([ panels.FieldPanel('website'), ], heading=_('Web presence')), panels.RichTextFieldPanel('about_me'), ]
class Profile(models.TimeStampedModel): """ Social information about users. """ GENDER_MALE, GENDER_FEMALE, GENDER_OTHER = 0, 1, 2 GENDER_CHOICES = [ (GENDER_MALE, _('Male')), (GENDER_FEMALE, _('Female')), (GENDER_OTHER, _('Other')), ] VISIBILITY_PUBLIC, VISIBILITY_FRIENDS, VISIBILITY_HIDDEN = range(3) VISIBILITY_CHOICES = enumerate( [_('Any Codeschool user'), _('Only friends'), _('Private')]) visibility = models.IntegerField( _('Visibility'), choices=VISIBILITY_CHOICES, default=VISIBILITY_FRIENDS, help_text=_('Who do you want to share information in your profile?')) user = models.OneToOneField( User, verbose_name=_('user'), related_name='profile_ref', ) phone = models.CharField( _('Phone'), max_length=20, blank=True, null=True, ) gender = models.SmallIntegerField( _('gender'), choices=GENDER_CHOICES, blank=True, null=True, ) date_of_birth = models.DateField( _('date of birth'), blank=True, null=True, ) website = models.URLField( _('Website'), blank=True, null=True, help_text=_('A website that is shown publicly in your profile.')) about_me = models.RichTextField( _('About me'), blank=True, help_text=_('A small description about yourself.')) # Delegates and properties username = delegate_to('user', True) name = delegate_to('user') email = delegate_to('user') class Meta: permissions = ( ('student', _('Can access/modify data visible to student\'s')), ('teacher', _('Can access/modify data visible only to Teacher\'s')), ) @property def age(self): if self.date_of_birth is None: return None today = timezone.now().date() birthday = self.date_of_birth years = today.year - birthday.year birthday = datetime.date(today.year, birthday.month, birthday.day) if birthday > today: return years - 1 else: return years def __str__(self): if self.user is None: return __('Unbound profile') full_name = self.user.get_full_name() or self.user.username return __('%(name)s\'s profile') % {'name': full_name} def get_absolute_url(self): self.user.get_absolute_url()
class AttendanceSheet(models.Model): """ Controls student attendance by generating a new public pass-phrase under teacher request. Students confirm attendance by typing the secret phrase within a small interval after the teacher starts checking the attendance. """ max_attempts = models.SmallIntegerField( _('Maximum number of attempts'), default=3, help_text=_( 'How many times a student can attempt to prove attendance. A ' 'maximum is necessary to avoid a brute force attack.' ), ) expiration_minutes = models.SmallIntegerField( _('Expiration time'), default=5, help_text=_( 'Time (in minutes) before attendance session expires.' ) ) owner = models.ForeignKey(models.User) last_event = models.ForeignKey('Event', blank=True, null=True) max_string_distance = models.SmallIntegerField( _('Fuzzyness'), default=1, help_text=_( 'Maximum number of wrong characters that is considered acceptable ' 'when comparing the expected passphrase with the one given by the' 'student.' ), ) max_number_of_absence = models.IntegerField(blank=True, null=True) # Properties expiration_interval = property( lambda self: datetime.timedelta(minutes=self.expiration_minutes)) attendance_checks = property( lambda self: AttendanceCheck.objects.filter(event__sheet=self) ) def __str__(self): try: return self.attendancepage_set.first().title except models.ObjectDoesNotExist: user = self.owner.get_full_name() or self.owner.username return _('Attendance sheet (%s)' % user) def new_event(self, commit=True): """ Create a new event in attendance sheet. """ current_time = now() event = Event( passphrase=phrase(), date=current_time.date(), created=current_time, expires=current_time + self.expiration_interval, sheet=self, ) self.last_event = event if commit: event.save() self.save(update_fields=['last_event']) return event def current_passphrase(self): """ Return the current passphrase. """ return self.current_event().passphrase def current_event(self): """ Return the last event created for today. If no event is found, create a new one. """ if self.last_event and self.last_event.date == now().date(): return self.last_event else: return self.new_event() def number_of_absences(self, user): """ Return the total number of absence for user. """ return self.attendance_checks.filter(user=user, has_attended=False).count() def absence_table(self, users=None, method='fraction'): """ Return a mapping between users and their respective absence rate. Args: users: A queryset of users. method: One of 'fraction' (default), 'number', 'attendance' or 'attendance-fraction' """ try: get_value_from_absence = { 'fraction': lambda x: x / num_events, 'number': lambda x: x, 'attendance': lambda x: num_events - x, 'attendance-fraction': lambda x: (num_events - x) / num_events }[method] except KeyError: raise ValueError('invalid method: %r' % method) num_events = self.events.count() if users is None: users = models.User.objects.all() result = collections.OrderedDict() for user in users: absence = self.user_absence(user) result[user] = get_value_from_absence(absence) return result def user_attempts(self, user): """ Return the number of user attempts in the last attendance event. """ if self.last_event is None: return 0 qs = self.attendance_checks.filter(user=user, event=self.last_event) return qs.count() def minutes_left(self, raises=True): """ Return how many minutes left for expiration. """ if self.last_event: time = now() if self.last_event.expires < time: return 0.0 else: dt = self.last_event.expires - time return dt.total_seconds() / 60. if raises: raise ValueError('last event is not defined') else: return None def is_expired(self): """ Return True if last_event has already expired. """ if not self.last_event: return False return self.last_event.expires < now() def is_valid(self, passphrase): """ Check if passphrase is valid. """ if self.is_expired(): return False distance = string_distance(passphrase, self.current_passphrase()) return distance <= self.max_string_distance