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)
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")) ]
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()
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): """
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'), ]
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)
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()
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'), ]
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 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 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'), ]
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)
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'), ]
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)
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'), ]
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')), ]
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 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 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()