Ejemplo n.º 1
0
class Discipline(models.TimeStampedModel):
    """
    Represents an academic discipline.
    """

    name = models.CharField(max_length=100)
    slug = models.SlugField(_('short name'))
    description = models.RichTextField(blank=True)
    syllabus = models.RichTextField(blank=True)
Ejemplo n.º 2
0
class FormPage(AbstractEmailForm):
    intro = models.RichTextField(blank=True)
    thank_you_text = models.RichTextField(blank=True)

    content_panels = AbstractEmailForm.content_panels + [
        panels.FieldPanel('intro', classname="full"),
        panels.InlinePanel('form_fields', label="Form fields"),
        panels.FieldPanel('thank_you_text', classname="full"),
        panels.MultiFieldPanel([
            panels.FieldPanel('to_address', classname="full"),
            panels.FieldPanel('from_address', classname="full"),
            panels.FieldPanel('subject', classname="full"),
        ], heading=_("Email"))
    ]
Ejemplo n.º 3
0
class Sprint(models.Model):
    """
    A sprint
    """

    project = models.ForeignKey(ScrumProject, related_name='sprints')
    description = models.RichTextField(blank=True)
    start_date = models.DateTimeField()
    due_date = models.DateTimeField()
    duration_weeks = models.PositiveIntegerField(default=1,
                                                 validators=[non_null])

    def next_start_date(self, date=None):
        """
        Return the next valid date that the sprint could start after the given.

        If no arguments are given, consider the current time.
        """

        date = date or now()
        return date

    def attach(self, project, commit=True):
        """
        Associate sprint to project, updating required values.
        """

        date = project.finish_date()
        self.project = project
        self.start_date = self.next_start_date(date)
        self.due_date = self.start_date + one_week * self.duration_weeks
        if commit:
            self.save()
Ejemplo n.º 4
0
class Badge(models.Model):
    """
    Represents an abstract badge.

    Instances of these class are not associated to specific users. GivenBadge
    makes the association between badges and users.
    """

    track = models.ForeignKey(BadgeTrack, related_name='badges')
    name = models.CharField(max_length=200)
    image = models.ImageField(
        upload_to='gamification/badges/',
        blank=True,
        null=True,
    )
    required_points = models.PositiveIntegerField(default=0)
    required_score = models.PositiveIntegerField(default=0)
    required_stars = models.PositiveIntegerField(default=0)
    description = models.TextField()
    details = models.RichTextField(blank=True)

    @property
    def value(self):
        """
        A sortable element that describes the overall badge difficulty.
        """
        return self.required_stars, self.required_points, self.required_score

    @classmethod
    def update_for_user(cls, user, **kwargs):
        """
Ejemplo n.º 5
0
class ScrumProject(models.RoutablePageMixin, models.Page):
    """
    A simple scrum project.
    """

    description = models.RichTextField()
    members = models.ManyToManyField(models.User)
    workday_duration = models.IntegerField(default=2)

    @property
    def backlog_tasks(self):
        return self.tasks.filter(status=Task.STATUS_BACKLOG)

    # Public functions
    def finish_date(self):
        """
        Return the finish date for the last sprint.
        """
        try:
            return self.sprints.order_by('due_date').last().due_date

        except Sprint.DoesNotExist:
            return now()

    # Serving pages
    @models.route(r'^sprints/new/$')
    def serve_new_sprint(self, request):
        return serve_new_sprint(request, self)

    @models.route(r'^sprints/(?P<id>[0-9]+)/$')
    def serve_view_sprint(self, request, id=None, *args, **kwargs):
        print(args)
        print(kwargs)
        sprint = get_object_or_404(Sprint, id=id)
        return serve_view_sprint(request, self, sprint)

    @models.route(r'^sprints/$')
    def serve_list_sprint(self, request, *args, **kwargs):
        return serve_list_sprints(request, self)

    # Wagtail specific
    template = 'scrum/project.jinja2'

    content_panels = models.Page.content_panels + [
        panels.FieldPanel('description'),
        panels.FieldPanel('workday_duration'),
    ]
Ejemplo n.º 6
0
class Post(models.TimeStampedModel, models.PolymorphicModel):
    """
    Represents a post in the user time-line.
    """

    VISIBILITY_PUBLIC = 1
    VISIBILITY_FRIENDS = 0
    VISIBILITY_OPTIONS = [
        (VISIBILITY_FRIENDS, _('Friends only')),
        (VISIBILITY_PUBLIC, _('Pubic')),
    ]

    user = models.ForeignKey(models.User)
    text = models.RichTextField()
    visibility = models.IntegerField(choices=VISIBILITY_OPTIONS,
                                     default=VISIBILITY_FRIENDS)

    def __str__(self):
        return 'Post by %s at %s' % (self.user, self.created)
Ejemplo n.º 7
0
class Task(models.Model):
    """
    A task that can be on the backlog or on a sprint.
    """

    STATUS_BACKLOG = 0
    STATUS_TODO = 1
    STATUS_DOING = 2
    STATUS_DONE = 3
    STATUS = models.Choices(
        (STATUS_BACKLOG, 'backlog'),
        (STATUS_TODO, 'todo'),
        (STATUS_DOING, 'doing'),
        (STATUS_DONE, 'done'),
    )
    sprint = models.ForeignKey(Sprint, related_name='tasks')
    project = models.ForeignKey(ScrumProject, related_name='tasks')
    status = models.StatusField()
    created_by = models.ForeignKey(models.User, related_name='+')
    assigned_to = models.ManyToManyField(models.User, related_name='+')
    description = models.RichTextField()
    duration_hours = models.IntegerField()
    objects = TaskQuerySet.as_manager()
Ejemplo n.º 8
0
class Choice(models.Orderable):
    question = models.ParentalKey(MultipleChoiceQuestion,
                                  related_name='choices')
    text = models.RichTextField(_('Choice description'))
    uuid = models.UUIDField(default=uuid.uuid4)
    value = models.DecimalField(
        _('Value'), decimal_places=1, max_digits=4,
        validators=[validators.MinValueValidator(0),
                    validators.MaxValueValidator(100)],
        help_text=_(
            'Grade given for users that choose these option (value=0, for an '
            'incorrect choice and value=100 for a correct one).'
        ),
        default=0,
    )

    def __repr__(self):
        return 'Choice(value=%s, ...)' % self.value

    panels = [
        panels.FieldPanel('text'),
        panels.FieldPanel('value'),
    ]
Ejemplo n.º 9
0
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'),
    ]
Ejemplo n.º 10
0
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'),
    ]
Ejemplo n.º 11
0
class Question(models.RoutablePageMixin,
               models.ShortDescriptionPageMixin,
               Activity,
               metaclass=QuestionMeta):
    """
    Base abstract class for all question types.
    """
    class Meta:
        abstract = True
        permissions = (("download_question", "Can download question files"), )

    EXT_TO_METHOD_CONVERSIONS = {'yml': 'yaml'}
    OPTIONAL_IMPORT_FIELDS = [
        'author_name', 'comments', 'score_value', 'star_value'
    ]
    base_form_class = QuestionAdminModelForm

    body = models.StreamField(
        QUESTION_BODY_BLOCKS,
        blank=True,
        null=True,
        verbose_name=_('Question description'),
        help_text=_(
            'Describe what the question is asking and how should the students '
            'answer it as clearly as possible. Good questions should not be '
            'ambiguous.'),
    )
    comments = models.RichTextField(
        _('Comments'),
        blank=True,
        help_text=_('(Optional) Any private information that you want to '
                    'associate to the question page.'))
    import_file = models.FileField(
        _('import question'),
        null=True,
        blank=True,
        upload_to='question-imports',
        help_text=_(
            'Fill missing fields from question file. You can safely leave this '
            'blank and manually insert all question fields.'))
    __imported_data = None

    def load_from_file_data(self, file_data):
        """
        Import content from raw file data.
        """

        fmt = self.loader_format_from_filename(file_data.name)
        self.load_from(file_data, format=fmt)
        self.__imported_data = dict(self.__dict__)

        logger.info('Imported question "%s" from file "%s"' %
                    (self.title, self.import_file.name))

        # We fake POST data after loading data from file in order to make the
        # required fields to validate. This part constructs a dictionary that
        # will be used to feed a fake POST data in the QuestionAdminModelForm
        # instance
        fake_post_data = {
            'title': self.title or _('Untitled'),
            'short_description': self.short_description or _('untitled'),
        }

        for field in self.OPTIONAL_IMPORT_FIELDS:
            if getattr(self, field, None):
                fake_post_data[field] = getattr(self, field)

        base_slug = slugify(fake_post_data['title'])
        auto_generated_slug = self._get_autogenerated_slug(base_slug)
        fake_post_data['slug'] = auto_generated_slug
        return fake_post_data

    def loader_format_from_filename(self, name):
        """
        Returns a string with the loader method from the file extension
        """

        _, ext = os.path.splitext(name)
        ext = ext.lstrip('.')
        return self.EXT_TO_METHOD_CONVERSIONS.get(ext, ext)

    def load_from(self, data, format='yaml'):
        """
        Load data from the given file or string object using the specified
        method.
        """

        try:
            loader = getattr(self, 'load_from_%s' % format)
        except AttributeError:
            raise ValueError('format %r is not implemented' % format)
        return loader(data)

    def full_clean(self, *args, **kwargs):
        if self.__imported_data is not None:
            blacklist = {
                # References
                'id',
                'owner_id',
                'page_ptr_id',
                'content_type_id',

                # Saved fields
                'title',
                'short_description',
                'seo_title',
                'author_name',
                'slug',
                'comments',
                'score_value',
                'stars_value',
                'difficulty',

                # Forbidden fields
                'import_file',

                # Wagtail fields
                'path',
                'depth',
                'url_path',
                'numchild',
                'go_live_at',
                'expire_at',
                'show_in_menus',
                'has_unpublished_changes',
                'latest_revision_created_at',
                'first_published_at',
                'live',
                'expired',
                'locked',
                'search_description',
            }

            data = {
                k: v
                for k, v in self.__imported_data.items()
                if (not k.startswith('_')) and k not in blacklist and v not in
                (None, '')
            }

            for k, v in data.items():
                setattr(self, k, v)

        super().full_clean(*args, **kwargs)

    # Serve pages
    def get_context(self, request, *args, **kwargs):
        return dict(
            super().get_context(request, *args, **kwargs),
            response=self.responses.response_for_request(request),
            question=self,
            form_name='response-form',
        )

    @srvice.route(r'^submit-response/$')
    def route_submit(self, client, **kwargs):
        """
        Handles student responses via AJAX and a srvice program.
        """

        response = self.submit(user=client.user, **kwargs)
        response.autograde()
        data = render_html(response)
        client.dialog(html=data)

    @models.route(r'^submissions/$')
    def route_submissions(self, request, *args, **kwargs):
        submissions = self.submissions.user(request.user).order_by('-created')
        context = self.get_context(request, *args, **kwargs)
        context['submissions'] = submissions

        # Fetch template name from explicit configuration or compute the default
        # value from the class name
        try:
            template = getattr(self, 'template_submissions')
            return render(request, template, context)
        except AttributeError:
            name = self.__class__.__name__.lower()
            if name.endswith('question'):
                name = name[:-8]
            template = 'questions/%s/submissions.jinja2' % name

            try:
                return render(request, template, context)
            except TemplateDoesNotExist:
                raise ImproperlyConfigured(
                    'Model %s must define a template_submissions attribute. '
                    'You  may want to extend this template from '
                    '"questions/submissions.jinja2"' % self.__class__.__name__)

    @models.route(r'^leaderboard/$')
    @models.route(r'^statistics/$')
    @models.route(r'^submissions/$')
    @models.route(r'^social/$')
    def route_page_does_not_exist(self, request):
        return render(
            request, 'base.jinja2', {
                'content_body':
                'The page you are trying to see is not implemented '
                'yet.',
                'content_title':
                'Not implemented',
                'title':
                'Not Implemented'
            })

    # Wagtail admin
    subpage_types = []
    content_panels = models.ShortDescriptionPageMixin.content_panels[:-1] + [
        panels.MultiFieldPanel([
            panels.FieldPanel('import_file'),
            panels.FieldPanel('short_description'),
        ],
                               heading=_('Options')),
        panels.StreamFieldPanel('body'),
        panels.MultiFieldPanel([
            panels.FieldPanel('author_name'),
            panels.FieldPanel('comments'),
        ],
                               heading=_('Optional information'),
                               classname='collapsible collapsed'),
    ]
Ejemplo n.º 12
0
class Question(models.DecoupledAdminPage, mixins.ShortDescriptionPage,
               Activity):
    """
    Base abstract class for all question types.
    """
    class Meta:
        abstract = True
        permissions = (("download_question", "Can download question files"), )

    body = models.StreamField(
        QUESTION_BODY_BLOCKS,
        blank=True,
        null=True,
        verbose_name=_('Question description'),
        help_text=_(
            'Describe what the question is asking and how should the students '
            'answer it as clearly as possible. Good questions should not be '
            'ambiguous.'),
    )
    comments = models.RichTextField(
        _('Comments'),
        blank=True,
        help_text=_('(Optional) Any private information that you want to '
                    'associate to the question page.'))
    import_file = models.FileField(
        _('import question'),
        null=True,
        blank=True,
        upload_to='question-imports',
        help_text=_(
            'Fill missing fields from question file. You can safely leave this '
            'blank and manually insert all question fields.'))

    def get_navbar(self, user):
        """
        Returns the navbar for the given question.
        """

        from .components import navbar_question

        return navbar_question(self, user)

    # Serve pages
    def get_submission_kwargs(self, request, kwargs):
        return {}

    def get_context(self, request, *args, **kwargs):
        context = dict(super().get_context(request, *args, **kwargs),
                       question=self,
                       form_name='response-form',
                       navbar=self.get_navbar(request.user))
        return context

    #
    # Routes
    #
    def serve_ajax_submission(self, client, **kwargs):
        """
        Serve AJAX request for a question submission.
        """
        kwargs = self.get_submission_kwargs(client.request, kwargs)
        submission = self.submit(client.request, **kwargs)
        if submission.recycled:
            client.dialog(html='You already submitted this response!')
        elif self._meta.instant_feedback:
            feedback = submission.auto_feedback()
            data = feedback.render_message()
            client.dialog(html=data)
        else:
            client.dialog(html='Your submission is on the correction queue!')

    @srvice.route(r'^submit-response.api/$', name='submit-ajax')
    def route_ajax_submission(self, client, **kwargs):
        return self.serve_ajax_submission(client, **kwargs)
Ejemplo n.º 13
0
class Question(models.RoutablePageMixin, Activity):
    """
    Base abstract class for all question types.
    """

    class Meta:
        abstract = True
        permissions = (("download_question", "Can download question files"),)

    stem = models.StreamField(
        QUESTION_STEM_BLOCKS,
        blank=True,
        null=True,
        verbose_name=_('Question description'),
        help_text=_(
            'Describe what the question is asking and how should the students '
            'answer it as clearly as possible. Good questions should not be '
            'ambiguous.'
        ),
    )
    author_name = models.CharField(
        _('Author\'s name'),
        max_length=100,
        blank=True,
        help_text=_(
            'The author\'s name, if not the same user as the question owner.'
        ),
    )
    comments = models.RichTextField(
        _('Comments'),
        blank=True,
        help_text=_('(Optional) Any private information that you want to '
                    'associate to the question page.')
    )

    @property
    def long_description(self):
        return str(self.stem)

    # Permission control
    def can_edit(self, user):
        """Only the owner of the question can edit it"""
        if user is None or self.owner is None:
            return False
        return self.owner.pk == user.pk

    def can_create(self, user):
        """You have to be the teacher of a course in order to create new
        questions."""

        return not user.courses_as_teacher.empty()

    # Serving pages and routing
    @srvice.route(r'^submit-response/$')
    def respond_route(self, client, **kwargs):
        """
        Handles student responses via AJAX and a srvice program.
        """

        raise NotImplementedError

    @models.route(r'^stats/$')
    def stats_route(self, request, **kwargs):
        """
        Shows the stats for each question.
        """

        data = """<dl>
            <dt>Name<dt><dd>{name}<dd>
            <dt>Best grade<dt><dd>{best}<dd>
            <dt>Responses<dt><dd>{n_responses}<dd>
            <dt>Response items<dt><dd>{n_response_items}<dd>
            <dt>Correct responses<dt><dd>{n_correct}<dd>
            <dt>Mean grade responses<dt><dd>{mean}<dd>
            <dt>Context id</dt><dd>{context_id}</dd>
        </dl>
        """.format(
            context_id=self.default_context.id,
            name=self.title,
            best=self.best_final_grade(),
            mean=self.mean_final_grade(),
            n_correct=self.correct_responses().count(),
            n_response_items=self.response_items().count(),
            n_responses=self.responses.count(),
        )

        # Renders content
        context = {'content_body': data,
                   'content_text': 'Stats'}
        return render(request, 'base.jinja2', context)

    @models.route(r'^responses/')
    def response_list_route(self, request):
        """
        Renders a list of responses
        """

        user = request.user
        context = self.get_context(request)
        items = self.response_items(user=user, context='any')
        items = (x.get_real_instance() for x in items)
        context.update(
            question=self,
            object_list=items,
        )
        return render(request, 'cs_questions/response-list.jinja2', context)

    # Wagtail admin
    parent_page_types = [
        'cs_questions.QuestionList',
        'cs_core.Discipline',
        'cs_core.Faculty'
    ]
    content_panels = Activity.content_panels + [
        panels.StreamFieldPanel('stem'),
        panels.MultiFieldPanel([
            panels.FieldPanel('author_name'),
            panels.FieldPanel('comments'),
        ], heading=_('Optional information'),
           classname='collapsible collapsed'),
    ]
Ejemplo n.º 14
0
class Classroom(models.TimeStampedModel, models.DecoupledAdminPage,
                models.RoutablePageExt):
    """
    One specific occurrence of a course for a given teacher in a given period.
    """

    discipline = models.ForeignKey('academic.Discipline',
                                   blank=True,
                                   null=True,
                                   on_delete=models.DO_NOTHING)
    teacher = models.ForeignKey(models.User,
                                related_name='classrooms_as_teacher',
                                on_delete=models.PROTECT)
    students = models.ManyToManyField(
        models.User,
        related_name='classrooms_as_student',
        blank=True,
    )
    staff = models.ManyToManyField(
        models.User,
        related_name='classrooms_as_staff',
        blank=True,
    )
    weekly_lessons = models.BooleanField(
        _('weekly lessons'),
        default=False,
        help_text=_(
            'If true, the lesson spans a whole week. Otherwise, each lesson '
            'would correspond to a single day/time slot.'),
    )
    accept_subscriptions = models.BooleanField(
        _('accept subscriptions'),
        default=True,
        help_text=_('Set it to false to prevent new student subscriptions.'),
    )
    is_public = models.BooleanField(
        _('is it public?'),
        default=True,
        help_text=_(
            'If true, all students will be able to see the contents of the '
            'course. Most activities will not be available to non-subscribed '
            'students.'),
    )
    subscription_passphrase = models.CharField(
        _('subscription passphrase'),
        default=random_subscription_passphase,
        max_length=140,
        help_text=_(
            'A passphrase/word that students must enter to subscribe in the '
            'course. Leave empty if no passphrase should be necessary.'),
        blank=True,
    )
    short_description = models.CharField(max_length=140)
    description = models.RichTextField()
    template = models.CharField(max_length=20,
                                choices=[
                                    ('programming-beginner',
                                     _('A beginner programming course')),
                                    ('programming-intermediate',
                                     _('An intermediate programming course')),
                                    ('programming-marathon',
                                     _('A marathon-level programming course')),
                                ],
                                blank=True)

    objects = ClassroomManager()

    def save(self, *args, **kwargs):
        self.teacher = self.teacher or self.owner
        super().save(*args, **kwargs)

    def enroll_student(self, student):
        """
        Register a new student in the course.
        """

        if student == self.teacher:
            raise ValidationError(_('Teacher cannot enroll as student.'))
        elif student in self.staff.all():
            raise ValidationError(_('Staff member cannot enroll as student.'))
        self.students.add(student)
Ejemplo n.º 15
0
class CodeExhibit(Activity):
    """
    Students can publish code + an associated image (think about turtle art or
    Processing) and then vote in the best submissions.
    """
    class Meta:
        verbose_name = _('Code exhibit')
        verbose_name_plural = _('Code exhibits')

    description = models.RichTextField()
    language = models.ForeignKey(
        'core.ProgrammingLanguage',
        on_delete=models.PROTECT,
    )

    def get_submit_form(self, *args, **kwargs):
        class ExhibitEntryForm(forms.ModelForm):
            class Meta:
                model = ExhibitEntry
                fields = ['name', 'image', 'source']

        return ExhibitEntryForm(*args, **kwargs)

    # Page rendering and views
    template = 'code_exhibit/code_exhibit.jinja2'

    def get_context(self, request, *args, **kwargs):
        context = super().get_context(request, *args, **kwargs)
        context['entries'] = self.entries.all()
        return context

    @bricks.route(r'^get-form/$')
    def route_submit_form(self, client):
        form = self.get_submit_form()
        context = {'form': form, 'language': self.language}
        html = render_to_string('code_exhibit/submit.jinja2',
                                context,
                                request=client.request)
        client.dialog(html=html)

    @models.route(r'^submit-entry/$')
    def route_submit(self, request):
        if request.method == 'POST':
            print(request.POST)
            form = self.get_submit_form(request.POST, request.FILES)
            if form.is_valid():
                instance = form.save(commit=False)
                print('saving model', instance)
                instance.user = request.user
                instance.exhibit = self
                instance.save()
            else:
                print(form.errors)
        return redirect(self.get_absolute_url())

    # Wagtail Admin
    content_panels = Activity.content_panels + [
        panels.FieldPanel('description'),
        panels.FieldPanel('language'),
        panels.InlinePanel('entries'),
    ]
Ejemplo n.º 16
0
class Course(models.RoutablePageMixin, models.TimeStampedModel, models.Page):
    """
    One specific occurrence of a course for a given teacher in a given period.
    """

    discipline = models.ForeignKey('Discipline',
                                   blank=True,
                                   null=True,
                                   on_delete=models.DO_NOTHING)
    teacher = models.ForeignKey(models.User,
                                related_name='courses_as_teacher',
                                on_delete=models.DO_NOTHING)
    students = models.ManyToManyField(
        models.User,
        related_name='courses_as_student',
        blank=True,
    )
    staff = models.ManyToManyField(
        models.User,
        related_name='courses_as_staff',
        blank=True,
    )
    weekly_lessons = models.BooleanField(
        _('weekly lessons'),
        default=False,
        help_text=_(
            'If true, the lesson spans a whole week. Othewise, each lesson '
            'would correspond to a single day/time slot.'),
    )
    accept_subscriptions = models.BooleanField(
        _('accept subscriptions'),
        default=True,
        help_text=_('Set it to false to prevent new student subscriptions.'),
    )
    is_public = models.BooleanField(
        _('is it public?'),
        default=True,
        help_text=_(
            'If true, all students will be able to see the contents of the '
            'course. Most activities will not be available to non-subscribed '
            'students.'),
    )
    subscription_passphrase = models.CharField(
        _('subscription passphrase'),
        default=random_subscription_passphase,
        max_length=140,
        help_text=_(
            'A passphrase/word that students must enter to subscribe in the '
            'course. Leave empty if no passphrase should be necessary.'),
        blank=True,
    )
    short_description = models.CharField(max_length=140, blank=True)
    description = models.RichTextField(blank=True)
    activities_template = models.CharField(
        max_length=20,
        choices=[
            ('programming-beginner', _('A beginner programming course')),
            ('programming-intermediate',
             _('An intermediate programming course')),
            ('programming-marathon', _('A marathon-level programming course')),
        ],
        blank=True)

    @lazy
    def academic_description(self):
        return getattr(self.discipline, 'description', '')

    @lazy
    def syllabus(self):
        return getattr(self.discipline, 'syllabus', '')

    objects = CourseManager()

    @property
    def calendar_page(self):
        content_type = models.ContentType.objects.get(app_label='cs_core',
                                                      model='calendarpage')
        return apps.get_model('cs_core', 'CalendarPage').objects.get(
            depth=self.depth + 1,
            path__startswith=self.path,
            content_type_id=content_type,
        )

    @property
    def activities_page(self):
        content_type = models.ContentType.objects.get(app_label='cs_questions',
                                                      model='questionlist')
        return apps.get_model('cs_questions', 'QuestionList').objects.get(
            depth=self.depth + 1,
            path__startswith=self.path,
            content_type_id=content_type,
        )

    def save(self, *args, **kwargs):
        with transaction.atomic():
            created = self.id is None

            if not self.path:
                created = False
                root = model_reference.load('course-list')
                root.add_child(instance=self)
            else:
                super().save(*args, **kwargs)

            if created:
                self.create_calendar_page()
                self.create_activities_page()

    def create_calendar_page(self):
        """
        Creates a new calendar page if it does not exist.
        """

        model = apps.get_model('courses', 'calendar')
        calendar = model()
        self.add_child(instance=calendar)

    def create_activities_page(self):
        """
        Creates a new activities page if it does not exist.
        """

        model = apps.get_model('activities', 'activitylist')
        activities = model(
            title=_('Activities'),
            slug='activities',
            short_description=ACTIVITY_DESCRIPTION % {'name': self.title},
        )
        self.add_child(instance=activities)

    def enroll_student(self, student):
        """
        Register a new student in the course.
        """

        self.students.add(student)
        self.update_friendship_status(student)

    def is_registered(self, user):
        """
        Check if user is associated with the course in any way.
        """

        if self.teacher == user:
            return True
        elif user in self.students.all():
            return True
        elif user in self.staff.all():
            return True
        else:
            return False

    def update_friendship_status(self, student=None):
        """
        Recompute the friendship status for a single student by marking it as
        a colleague of all participants in the course.

        If no student is given, update the status of all enrolled students.
        """

        update = self._update_friendship_status
        if student is None:
            for student in self.students.all():
                update(student)
        else:
            update(student)

    def _update_friendship_status(self, student):
        for colleague in self.students.all():
            if colleague != student:
                FriendshipStatus.objects.get_or_create(
                    owner=student,
                    other=colleague,
                    status=FriendshipStatus.STATUS_COLLEAGUE)

    def get_user_role(self, user):
        """Return a string describing the most privileged role the user has
        as in the course. The possible values are:

        teacher:
            Owns the course and can do any kind of administrative tasks in
            the course.
        staff:
            Teacher assistants. May have some privileges granted by the teacher.
        student:
            Enrolled students.
        visitor:
            Have no relation to the course. If course is marked as public,
            visitors can access the course contents.
        """

        if user == self.teacher:
            return 'teacher'
        if user in self.staff.all():
            return 'staff'
        if user in self.students.all():
            return 'student'
        return 'visitor'

    def info_dict(self):
        """
        Return an ordered dictionary with relevant internationalized
        information about the course.
        """
        def yn(x):
            return _('Yes' if x else 'No')

        data = [
            ('Teacher', hyperlink(self.teacher)),
            ('Created', self.created),
            ('Accepting new subscriptions?', yn(self.accept_subscriptions)),
            ('Private?', yn(not self.is_public)),
        ]
        if self.academic_description:
            data.append(('Description', self.academic_description))
        if self.syllabus:
            data.append(('Description', self.academic_description))

        return OrderedDict([(_(k), v) for k, v in data])

    # Serving pages
    template = 'courses/detail.jinja2'

    def get_context(self, request, *args, **kwargs):
        return dict(
            super().get_context(request, *args, **kwargs),
            course=self,
        )

    def serve(self, request, *args, **kwargs):
        if self.is_registered(request.user):
            return super().serve(request, *args, **kwargs)
        return self.serve_registration(request, *args, **kwargs)

    def serve_registration(self, request, *args, **kwargs):
        context = self.get_context(request)
        if request.method == 'POST':
            form = PassPhraseForm(request.POST)
            if form.is_valid():
                self.enroll_student(request.user)
                return super().serve(request, *args, **kwargs)
        else:
            form = PassPhraseForm()

        context['form'] = form
        return render(request, 'courses/course-enroll.jinja2', context)

    # Wagtail admin
    parent_page_types = ['courses.CourseList']
    subpage_types = []
    content_panels = models.Page.content_panels + [
        panels.MultiFieldPanel([
            panels.FieldPanel('short_description'),
            panels.FieldPanel('description'),
            panels.FieldPanel('teacher')
        ],
                               heading=_('Options')),
        panels.InlinePanel(
            'time_slots',
            label=_('Time slots'),
            help_text=_('Define when the weekly classes take place.'),
        ),
    ]
    settings_panels = models.Page.settings_panels + [
        panels.MultiFieldPanel([
            panels.FieldPanel('weekly_lessons'),
        ],
                               heading=_('Options')),
        panels.MultiFieldPanel([
            panels.FieldPanel('accept_subscriptions'),
            panels.FieldPanel('is_public'),
            panels.FieldPanel('subscription_passphrase'),
        ],
                               heading=_('Subscription')),
    ]
Ejemplo n.º 17
0
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()
Ejemplo n.º 18
0
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'),
    ]
Ejemplo n.º 19
0
class ResponseContext(models.PolymorphicModel):
    """
    Define a different context for a response object.

    The context group responses into explicit groups and may also be used to
    define additional constraints on the correct answers.
    """
    class Meta:
        unique_together = [('activity', 'name')]

    # Basic
    activity = models.ParentalKey(
        'wagtailcore.Page',
        related_name='contexts',
    )
    name = models.CharField(_('name'),
                            max_length=140,
                            blank=True,
                            help_text=_('A unique identifier.'))
    description = models.RichTextField(
        _('description'),
        blank=True,
    )

    # Grading and submissions
    grading_method = models.ForeignKey(
        'cs_core.GradingMethod',
        on_delete=models.SET_DEFAULT,
        default=grading_method_best,
        blank=True,
        help_text=_('Choose the strategy for grading this activity.'))
    single_submission = models.BooleanField(
        _('single submission'),
        default=False,
        help_text=_(
            'If set, students will be allowed to send only a single response.',
        ),
    )

    # Feedback
    delayed_feedback = models.BooleanField(
        _('delayed feedback'),
        default=False,
        help_text=_(
            'If set, students will be only be able to see the feedback after '
            'the activity expires its deadline.'))

    # Deadlines
    deadline = models.DateTimeField(
        _('deadline'),
        blank=True,
        null=True,
    )
    hard_deadline = models.DateTimeField(
        _('hard deadline'),
        blank=True,
        null=True,
        help_text=_(
            'If set, responses submitted after the deadline will be accepted '
            'with a penalty.'))
    delay_penalty = models.DecimalField(
        _('delay penalty'),
        default=25,
        decimal_places=2,
        max_digits=6,
        help_text=_(
            'Sets the percentage of the total grade that will be lost due to '
            'delayed responses.'),
    )

    # Programming languages/formats
    format = models.ForeignKey(
        'cs_core.FileFormat',
        blank=True,
        null=True,
        help_text=_(
            'Defines the required file format or programming language for '
            'student responses, when applicable.'))

    # Extra constraints and resources
    constraints = models.StreamField([], default=[])
    resources = models.StreamField([], default=[])

    def clean(self):
        if not isinstance(self.activity, Activity):
            return ValidationError({
                'parent':
                _('Parent is not an Activity subclass'),
            })
        super().clean()