class Signup(ModelBase): project = models.ForeignKey('projects.Project', related_name='sign_up') public = RichTextField(config_name='rich', blank=True) between_participants = RichTextField(config_name='rich', blank=True) author = models.ForeignKey('users.UserProfile') MODERATED = 'moderated' NON_MODERATED = 'non-moderated' CLOSED = 'closed' STATUS_CHOICES = ( (CLOSED, _('Closed signup')), (MODERATED, _('Moderated signup')), (NON_MODERATED, _('Non-moderated signup')), ) status = models.CharField(max_length=30, choices=STATUS_CHOICES, default=CLOSED, null=True, blank=False) @models.permalink def get_absolute_url(self): return ('sign_up', (), { 'slug': self.project.slug, }) @property def standard(self): return render_to_string('signups/sign_up_initial_content.html', {'project': self.project}) def pending_answers(self): return self.answers.filter(accepted=False, deleted=False) def get_visible_answers(self, user): answers = self.answers.all() if user.is_authenticated(): profile = user.get_profile() is_organizing = self.project.organizers().filter( user=profile).exists() if not is_organizing: answers = answers.filter(Q(accepted=True) | Q(author=profile)) else: answers = answers.filter(accepted=True) return answers.order_by('-created_on') def get_page_for_answer(self, answer, user): answer_index = 0 for visible_answer in self.get_visible_answers(user): if visible_answer.id == answer.id: break answer_index += 1 items_per_page = settings.PAGINATION_DEFAULT_ITEMS_PER_PAGE return (answer_index / items_per_page) + 1 def get_answer_url(self, answer, user): page = self.get_page_for_answer(answer, user) url = self.get_absolute_url() return url + '?pagination_page_number=%s#answer-%s' % ( page, answer.id)
class Submission(ModelBase): """Application for a badge""" # TODO Refactor this and PageComment and Assessment? # to extend off same base url = models.URLField(max_length=1023) content = RichTextField(config_name='rich', blank=False) author = models.ForeignKey('users.UserProfile', related_name='submissions') badge = models.ForeignKey('badges.Badge', related_name="submissions") created_on = models.DateTimeField(auto_now_add=True, default=datetime.datetime.now) pending = models.BooleanField(default=True) def __unicode__(self): return _('%(author)s\'s application for %(badge)s') % { 'author': self.author, 'badge': self.badge } @models.permalink def get_absolute_url(self): return ('submission_show', (), { 'slug': self.badge.slug, 'submission_id': self.id, }) def send_notification(self): """Send notification when a new submission is posted.""" subject_template = 'badges/emails/new_submission_subject.txt' body_template = 'badges/emails/new_submission.txt' context = { 'submission': self, 'domain': Site.objects.get_current().domain, } profiles = self.badge.get_adopters() send_notifications(profiles, subject_template, body_template, context)
class Status(ModelBase): object_type = object_types['status'] author = models.ForeignKey('users.UserProfile') project = models.ForeignKey('projects.Project', null=True, blank=True) status = RichTextField(blank=False) created_on = models.DateTimeField( auto_now_add=True, default=datetime.datetime.now) important = models.BooleanField(default=False) activity = generic.GenericRelation(Activity, content_type_field='target_content_type', object_id_field='target_id') class Meta: verbose_name_plural = _('statuses') def __unicode__(self): return _('message: %s') % self.status def get_absolute_url(self): ct = ContentType.objects.get_for_model(Status) activity = Activity.objects.get(target_id=self.id, target_content_type=ct) return activity.get_absolute_url() def send_wall_notification(self): if not self.project: return context = { 'status': self, 'project': self.project, 'domain': Site.objects.get_current().domain, } subjects, bodies = localize_email( 'statuses/emails/wall_updated_subject.txt', 'statuses/emails/wall_updated.txt', context) from_organizer = self.project.organizers().filter( user=self.author).exists() for participation in self.project.participants(): if self.important: unsubscribed = False elif from_organizer: unsubscribed = participation.no_organizers_wall_updates else: unsubscribed = participation.no_participants_wall_updates if self.author != participation.user and not unsubscribed: SendUserEmail.apply_async( (participation.user, subjects, bodies)) @staticmethod def filter_activities(activities): ct = ContentType.objects.get_for_model(Status) return activities.filter(target_content_type=ct)
class Assessment(ModelBase): """Assessment for a badge""" final_rating = models.FloatField(null=True, blank=True, default=0) assessor = models.ForeignKey('users.UserProfile', related_name='assessments') assessed = models.ForeignKey('users.UserProfile', related_name='badge_assessments') comment = RichTextField(config_name='rich', blank=False) badge = models.ForeignKey('badges.Badge', related_name="assessments") created_on = models.DateTimeField(auto_now_add=True, default=datetime.datetime.now) submission = models.ForeignKey('badges.Submission', related_name="assessments", null=True, blank=True, help_text=_('If submission is blank, this is a '\ 'peer awarded assessment or superuser granted')) def final_rating_as_percentage(self): """Return the final rating as a percentage for styling of assessment view. Max number of ratings is 4""" return (self.final_rating / 4.0) * 100 def get_final_rating_display(self): rating_position = int(round(self.final_rating)) - 1 # Guarantee rating_position does not go above or below # the boundaries. if rating_position < 0: rating_position = 0 max_index = len(Rating.RATING_CHOICES) - 1 if rating_position > max_index: rating_position = max_index return Rating.RATING_CHOICES[rating_position][1] def update_final_rating(self): """Used on Rating save signal to update the final rating for the assessment""" ratings = Rating.objects.filter(assessment=self) final_rating = ratings.aggregate( final_rating=Avg('score'))['final_rating'] self.final_rating = final_rating self.save() def __unicode__(self): return _('%(assessor)s for %(assessed)s for %(badge)s') % { 'assessor': self.assessor, 'assessed': self.assessed, 'badge': self.badge} @models.permalink def get_absolute_url(self): return ('assessment_show', (), { 'slug': self.badge.slug, 'assessment_id': self.id, })
class PageVersion(ModelBase): title = models.CharField(max_length=100) sub_header = models.CharField(max_length=150, blank=True, null=True) content = RichTextField(config_name='rich', blank=False) author = models.ForeignKey('users.UserProfile', related_name='page_versions') date = models.DateTimeField() page = models.ForeignKey('content.Page', related_name='page_versions') deleted = models.BooleanField(default=False) minor_update = models.BooleanField(default=True) @models.permalink def get_absolute_url(self): return ('page_version', (), { 'slug': self.page.project.slug, 'page_slug': self.page.slug, 'version_id': self.id, })
class Submission(ModelBase): """Application for a badge""" # TODO Refactor this and PageComment and Assessment? # to extend off same base url = models.URLField(max_length=1023) content = RichTextField(config_name='rich', blank=False) author = models.ForeignKey('users.UserProfile', related_name='submissions') badge = models.ForeignKey('badges.Badge', related_name="submissions") created_on = models.DateTimeField(auto_now_add=True, default=datetime.datetime.now) def __unicode__(self): return _('%s' 's application for %s') % (self.author, self.badge) @models.permalink def get_absolute_url(self): return ('submission_show', (), { 'slug': self.badge.slug, 'submission_id': self.id, })
class Review(ModelBase): project = models.ForeignKey('projects.Project', related_name='reviews') author = models.ForeignKey('users.UserProfile', related_name='reviews') accepted = models.BooleanField(default=False) mark_deleted = models.BooleanField(default=False) mark_featured = models.BooleanField(default=False) content = RichTextField(config_name='rich', blank=False) created_on = models.DateTimeField(auto_now_add=True, default=datetime.datetime.now) def __unicode__(self): return 'Review for %s' % self.project.name @models.permalink def get_absolute_url(self): return ('show_project_reviews', (), { 'slug': self.project.slug, }) def send_notifications_i18n(self): subject_template = 'reviews/emails/review_submitted_subject.txt' body_template = 'reviews/emails/review_submitted.txt' context = { 'course': self.project.name, 'reviewer': self.author.username, 'review_text': self.content, 'review_url': self.get_absolute_url(), 'domain': Site.objects.get_current().domain, } profiles = [recipient.user for recipient in self.project.organizers()] send_notifications_i18n( profiles, subject_template, body_template, context, notification_category=u'course-review.project-{0}'.format( self.project.slug))
class UserProfile(ModelBase): """Each user gets a profile.""" object_type = object_types['person'] username = models.CharField(max_length=255, default='', unique=True) full_name = models.CharField(max_length=255, default='', null=True, blank=True) password = models.CharField(max_length=255, default='') email = models.EmailField(unique=True, null=True) bio = RichTextField(blank=True) image = models.ImageField(upload_to=determine_upload_path, default='', blank=True, null=True, storage=storage.ImageStorage()) confirmation_code = models.CharField(max_length=255, default='', blank=True) location = models.CharField(max_length=255, blank=True, default='') featured = models.BooleanField() newsletter = models.BooleanField() discard_welcome = models.BooleanField(default=False) created_on = models.DateTimeField(auto_now_add=True, default=datetime.datetime.now) preflang = models.CharField(verbose_name='preferred language', max_length=16, choices=settings.SUPPORTED_LANGUAGES, default=settings.LANGUAGE_CODE) deleted = models.BooleanField(default=False) last_active = models.DateTimeField(null=True, blank=True) user = models.ForeignKey(User, null=True, editable=False, blank=True) tags = CategoryTaggableManager(through=TaggedProfile, blank=True) objects = UserProfileManager() def __unicode__(self): if self.deleted: return ugettext('Anonym') return self.full_name or self.username def following(self, model=None): """ Return a list of objects this user is following. All objects returned will be ```Project``` or ```UserProfile``` instances. Optionally filter by type by including a ```model``` parameter. """ if (model == 'Project' or isinstance(model, Project) or model == Project): relationships = Relationship.objects.select_related( 'target_project').filter( source=self, deleted=False).exclude(target_project__isnull=True) return [ rel.target_project for rel in relationships if not rel.target_project.archived ] relationships = Relationship.objects.select_related( 'target_user').filter( source=self, target_user__deleted=False, deleted=False).exclude(target_user__isnull=True) return [rel.target_user for rel in relationships] def followers(self): """Return a list of this users followers.""" relationships = Relationship.objects.select_related('source').filter( target_user=self, source__deleted=False, deleted=False) return [rel.source for rel in relationships] def is_following(self, model): """Determine whether this user is following ```model```.""" return model in self.following(model=model) def get_current_projects(self, only_public=False): projects = self.following(model=Project) projects_organizing = [] projects_participating = [] projects_following = [] count = len(projects) for project in projects: if only_public and project.not_listed: count -= 1 continue is_challenge = (project.category == Project.CHALLENGE) if is_challenge: organizers = project.adopters() participants = project.non_adopter_participants() else: organizers = project.organizers() participants = project.non_organizer_participants() if organizers.filter(user=self).exists(): if is_challenge: project.relation_text = _('(adopted)') else: project.relation_text = _('(organizing)') projects_organizing.append(project) elif participants.filter(user=self).exists(): project.relation_text = _('(participating)') projects_participating.append(project) elif not is_challenge: project.relation_text = _('(following)') projects_following.append(project) data = { 'organizing': projects_organizing, 'participating': projects_participating, 'following': projects_following, 'count': count, } return data def get_past_projects(self, only_public=False): participations = Participation.objects.filter(user=self) current = participations.filter(project__archived=False, left_on__isnull=True) participations = participations.exclude( project__id__in=current.values('project_id')) past_projects = {} for p in participations: if p.project.slug in past_projects: past_projects[p.project.slug]['organizer'] |= p.organizing elif not only_public or not p.project.not_listed: past_projects[p.project.slug] = { 'name': p.project.name, 'url': p.project.get_absolute_url(), 'organizer': p.organizing, 'image_url': p.project.get_image_url(), } return past_projects.values() @models.permalink def get_absolute_url(self): username = '******' if self.deleted else self.username return ('users_profile_view', (), { 'username': username, }) def email_confirmation_code(self, url, new_user=True): """Send a confirmation email to the user after registering.""" subject_template = 'users/emails/registration_confirm_subject.txt' body_template = 'users/emails/registration_confirm.txt' context = {'confirmation_url': url, 'new_user': new_user} send_notifications([self], subject_template, body_template, context) def image_or_default(self): """Return user profile image or a default.""" avatar = '%s%s' % (settings.STATIC_URL, '/images/member-missing.png') if not self.deleted: gravatarUrl = self.gravatar(240) if self.image: avatar = '%s%s' % (settings.MEDIA_URL, self.image) elif gravatarUrl: avatar = gravatarUrl return mark_safe(avatar) def gravatar(self, size=240): hash = hashlib.md5(self.email.lower()).hexdigest() default = urlquote_plus(settings.DEFAULT_PROFILE_IMAGE) return GRAVATAR_TEMPLATE % { 'size': size, 'gravatar_hash': hash, 'default': default, 'rating': "g", 'username': self.username, } def generate_confirmation_code(self): if not self.confirmation_code: self.confirmation_code = ''.join( random.sample(string.letters + string.digits, 60)) return self.confirmation_code def set_password(self, raw_password, algorithm='sha512'): self.password = create_password(algorithm, raw_password) def check_password(self, raw_password): if '$' not in self.password: valid = (get_hexdigest('md5', '', raw_password) == self.password) if valid: # Upgrade an old password. self.set_password(raw_password) self.save() return valid algo, salt, hsh = self.password.split('$') return hsh == get_hexdigest(algo, salt, raw_password) def can_post(self): return len(self.confirmation_code) == 0 and self.deleted == False
class SignupAnswer(ModelBase): object_type = object_types['comment'] sign_up = models.ForeignKey('signups.Signup', related_name='answers') standard = RichTextField(config_name='rich', blank=False) public = RichTextField(config_name='rich', blank=True) between_participants = RichTextField(config_name='rich', blank=True) author = models.ForeignKey('users.UserProfile', related_name='sigup_answers') created_on = models.DateTimeField( auto_now_add=True, default=datetime.datetime.now) accepted = models.BooleanField(default=False) deleted = models.BooleanField(default=False) comments = generic.GenericRelation(PageComment, content_type_field='page_content_type', object_id_field='page_id') def __unicode__(self): return _("the signup answer of %(author)s at %(project)s") % { 'author': self.author, 'project': self.project} @property def project(self): return self.sign_up.project @models.permalink def get_absolute_url(self): return ('show_signup_answer', (), { 'slug': self.project.slug, 'answer_id': self.id, }) def can_edit(self, user): if user.is_authenticated(): profile = user.get_profile() return (profile == self.author) else: return False def first_level_comments(self): return self.comments.filter(reply_to__isnull=True).order_by( 'created_on') def has_visible_childs(self): return self.comments.filter(deleted=False).exists() def can_comment(self, user, reply_to=None): if user.is_authenticated(): if self.accepted: return self.project.is_participating(user) else: is_organizing = self.project.is_organizing(user) profile = user.get_profile() return is_organizing or (profile == self.author) else: return False def get_comment_url(self, comment, user): page = self.sign_up.get_page_for_answer(self, user) url = self.sign_up.get_absolute_url() return url + '?pagination_page_number=%s#%s' % ( page, comment.id) def comments_fire_activity(self): return False def comment_notification_recipients(self, comment): project = self.project recipients = {self.author.username: self.author} for organizer in project.organizers(): recipients[organizer.user.username] = organizer.user while comment.reply_to: comment = comment.reply_to recipients[comment.author.username] = comment.author return recipients.values() def accept(self, as_organizer=False, reviewer=None): if not reviewer: reviewer = self.sign_up.author is_organizing = self.project.organizers().filter( user=self.author).exists() is_participating = self.project.participants().filter( user=self.author).exists() if not is_organizing and not is_participating: participation = Participation(project=self.project, user=self.author, organizing=as_organizer) participation.save() accept_content = render_to_string( "signups/accept_sign_up_comment.html", {'as_organizer': as_organizer}) accept_comment = PageComment(content=accept_content, author=reviewer, page_object=self, scope_object=self.project) accept_comment.save() self.accepted = True self.save()
class Badge(models.Model): """Representation of a Badge""" name = models.CharField(max_length=225, blank=False) slug = models.SlugField(unique=True, max_length=110) description = models.CharField(max_length=225, blank=False) requirements = RichTextField(blank=True, null=True) image = models.ImageField(upload_to=determine_upload_path, default='', blank=True, null=True, storage=storage.ImageStorage()) prerequisites = models.ManyToManyField('self', symmetrical=False, blank=True, null=True) unique = models.BooleanField( help_text=_('If can only be awarded to the user once.'), default=False) SELF = 'self' PEER = 'peer' STEALTH = 'stealth' ASSESSMENT_TYPE_CHOICES = ( (SELF, _('Self assessment -- able to get the badge without ' \ 'outside assessment')), (PEER, _('Peer assessment -- community or skill badges users ' \ 'grant each other')), (STEALTH, _('Stealth assessment -- badges granted by the system '\ 'based on supplied logic. Accumulative.')) ) assessment_type = models.CharField(max_length=30, choices=ASSESSMENT_TYPE_CHOICES, default=SELF, null=True, blank=False) COMPLETION = 'completion/aggregate' SKILL = 'skill' COMMUNITY = 'peer-to-peer/community' STEALTH = 'stealth' OTHER = 'other' BADGE_TYPE_CHOICES = ( (COMPLETION, _('Completion/aggregate badge -- awarded by self '\ 'assessments')), (SKILL, _('Skill badge -- badges that are skill based and assessed '\ 'by peers with related logic')), (COMMUNITY, _('Peer-to-peer/community badge -- badges granted by '\ 'peers')), (STEALTH, _('Stealth badge -- system awarded badges')), (OTHER, _('Other badges -- badges like course organizer or those '\ 'staff issued')) ) badge_type = models.CharField(max_length=30, choices=BADGE_TYPE_CHOICES, default=COMPLETION, null=True, blank=False) rubrics = models.ManyToManyField('badges.Rubric', related_name='badges', null=True, blank=True) logic = models.ForeignKey('badges.Logic', related_name='badges', null=True, blank=True, help_text=_('If no logic is chosen, no logic required. '\ ' Example: self-assessment badges.')) groups = models.ManyToManyField('projects.Project', related_name='badges', null=True, blank=True) creator = models.ForeignKey('users.UserProfile', related_name='badges', blank=True, null=True) created_on = models.DateTimeField(auto_now_add=True, blank=False) def __unicode__(self): return "%s %s" % (self.name, _('Badge')) @models.permalink def get_absolute_url(self): return ('badges_show', (), { 'slug': self.slug, }) def get_image_url(self): # TODO: using project's default image until a default badge # image is added. missing = settings.MEDIA_URL + 'images/missing-badge.png' image_path = self.image.url if self.image else missing return image_path def save(self): """Make sure each badge has a unique slug.""" count = 1 if not self.slug: slug = slugify(self.name) self.slug = slug while True: existing = Badge.objects.filter(slug=self.slug) if len(existing) == 0: break self.slug = "%s-%s" % (slug, count + 1) count += 1 super(Badge, self).save() def is_eligible(self, user): """Check if the user eligible for the badge. If some prerequisite badges have not been awarded returns False.""" if user.is_authenticated(): profile = user.get_profile() awarded_badges = Award.objects.filter( user=profile).values('badge_id') return not self.prerequisites.exclude( id__in=awarded_badges).exists() else: return False def is_awarded_to(self, user): """Does the user have the badge?""" return Award.objects.filter(user=user, badge=self).count() > 0 def award_to(self, user): """Award the badge to the user. Returns None if no badge is awarded.""" if not self.is_eligible(user.user): # If the user is not eligible the badge # is not awarded. return None # If logic restrictions are not meet the badge is not awarded. if self.logic and not self.logic.is_eligible(self, user): return None if self.unique: # Do not award the badge if the user can not have the badge # more than once and it was already awarded. award, created = Award.objects.get_or_create(user=user, badge=self) return award if created else None return Award.objects.create(user=user, badge=self) def get_pending_submissions(self): """Submissions of users who haven't received the award yet""" all_submissions = Submission.objects.filter(badge=self) pending_submissions = [] for submission in all_submissions: if not self.is_awarded_to(submission.author): pending_submissions.append(submission) return pending_submissions def progress_for(self, user): """Progress for a user for this badge""" progress = Progress.objects.filter(user=user, badge=self) if progress: progress = progress[0] else: progress = Progress(user=user, badge=self) return progress def get_peers(self, profile): from projects.models import Participation from users.models import UserProfile badge_projects = self.groups.values('id') user_projects = Participation.objects.filter( user=profile).values('project__id') peers = Participation.objects.filter( project__in=badge_projects).filter( project__in=user_projects).values('user__id') return UserProfile.objects.filter(deleted=False, id__in=peers).exclude(id=profile.id) def other_badges_can_apply_for(self): badges = Badge.objects.exclude(id=self.id).filter( badge_type=Badge.SKILL).filter(assessment_type=Badge.PEER) badge_groups = self.groups.values('id') related_badges = badges.filter(groups__in=badge_groups).distinct() non_related_badges = badges.exclude(groups__in=badge_groups).distinct() return MultiQuerySet(related_badges, non_related_badges)
class Project(ModelBase): """Placeholder model for projects.""" object_type = object_types['group'] name = models.CharField(max_length=100) # Select kind of project (study group, course, or other) STUDY_GROUP = 'study group' COURSE = 'course' CHALLENGE = 'challenge' CATEGORY_CHOICES = ( (STUDY_GROUP, _('Study Group -- group of people working ' \ 'collaboratively to acquire and share knowledge.')), (COURSE, _('Course -- led by one or more organizers with skills on ' \ 'a field who direct and help participants during their ' \ 'learning.')), (CHALLENGE, _('Challenge -- series of tasks peers can engage in ' \ 'to develop skills.')) ) category = models.CharField(max_length=30, choices=CATEGORY_CHOICES, default=STUDY_GROUP, null=True, blank=False) tags = TaggableManager(through=GeneralTaggedItem, blank=True) other = models.CharField(max_length=30, blank=True, null=True) other_description = models.CharField(max_length=150, blank=True, null=True) short_description = models.CharField(max_length=150) long_description = RichTextField(validators=[MaxLengthValidator(700)]) start_date = models.DateField(null=True, blank=True) end_date = models.DateField(null=True, blank=True) school = models.ForeignKey('schools.School', related_name='projects', null=True, blank=True) detailed_description = models.ForeignKey('content.Page', related_name='desc_project', null=True, blank=True) image = models.ImageField(upload_to=determine_image_upload_path, null=True, storage=storage.ImageStorage(), blank=True) slug = models.SlugField(unique=True, max_length=110) featured = models.BooleanField(default=False) created_on = models.DateTimeField( auto_now_add=True, default=datetime.datetime.now) under_development = models.BooleanField(default=True) not_listed = models.BooleanField(default=False) archived = models.BooleanField(default=False) clone_of = models.ForeignKey('projects.Project', blank=True, null=True, related_name='derivated_projects') imported_from = models.CharField(max_length=150, blank=True, null=True) next_projects = models.ManyToManyField('projects.Project', symmetrical=False, related_name='previous_projects', blank=True, null=True) objects = ProjectManager() class Meta: verbose_name = _('group') def __unicode__(self): return _('%(name)s %(kind)s') % dict(name=self.name, kind=self.kind.lower()) @models.permalink def get_absolute_url(self): return ('projects_show', (), { 'slug': self.slug, }) def friendly_verb(self, verb): if verbs['post'] == verb: return _('created') @property def kind(self): return self.other.lower() if self.other else self.category def followers(self, include_deleted=False): relationships = Relationship.objects.all() if not include_deleted: relationships = relationships.filter( source__deleted=False) return relationships.filter(target_project=self, deleted=False) def previous_followers(self, include_deleted=False): """Return a list of users who were followers if this project.""" relationships = Relationship.objects.all() if not include_deleted: relationships = relationships.filter( source__deleted=False) return relationships.filter(target_project=self, deleted=True) def non_participant_followers(self, include_deleted=False): return self.followers(include_deleted).exclude( source__id__in=self.participants(include_deleted).values('user_id')) def participants(self, include_deleted=False): """Return a list of users participating in this project.""" participations = Participation.objects.all() if not include_deleted: participations = participations.filter(user__deleted=False) return participations.filter(project=self, left_on__isnull=True) def non_organizer_participants(self, include_deleted=False): return self.participants(include_deleted).filter(organizing=False) def adopters(self, include_deleted=False): return self.participants(include_deleted).filter(Q(adopter=True) | Q(organizing=True)) def non_adopter_participants(self, include_deleted=False): return self.non_organizer_participants(include_deleted).filter( adopter=False) def organizers(self, include_deleted=False): return self.participants(include_deleted).filter(organizing=True) def is_organizing(self, user): if user.is_authenticated(): profile = user.get_profile() is_organizer = self.organizers().filter(user=profile).exists() is_superuser = user.is_superuser return is_organizer or is_superuser else: return False def is_following(self, user): if user.is_authenticated(): profile = user.get_profile() is_following = self.followers().filter(source=profile).exists() return is_following else: return False def is_participating(self, user): if user.is_authenticated(): profile = user.get_profile() is_organizer_or_participant = self.participants().filter( user=profile).exists() is_superuser = user.is_superuser return is_organizer_or_participant or is_superuser else: return False def get_metrics_permissions(self, user): """Provides metrics related permissions for metrics overview and csv download.""" if user.is_authenticated(): if user.is_superuser: return True, True allowed_schools = settings.STATISTICS_ENABLED_SCHOOLS if not self.school or self.school.slug not in allowed_schools: return False, False csv_downloaders = settings.STATISTICS_CSV_DOWNLOADERS profile = user.get_profile() csv_permission = profile.username in csv_downloaders is_school_organizer = self.school.organizers.filter( id=user.id).exists() if is_school_organizer or self.is_organizing(user): return True, csv_permission return False, False def activities(self): return Activity.objects.filter(deleted=False, scope_object=self).order_by('-created_on') def create(self): self.save() self.send_creation_notification() def save(self): """Make sure each project has a unique slug.""" count = 1 if not self.slug: slug = slugify(self.name) self.slug = slug while True: existing = Project.objects.filter(slug=self.slug) if len(existing) == 0: break self.slug = "%s-%s" % (slug, count + 1) count += 1 super(Project, self).save() def get_image_url(self): missing = settings.MEDIA_URL + 'images/project-missing.png' image_path = self.image.url if self.image else missing return image_path def send_creation_notification(self): """Send notification when a new project is created.""" context = { 'project': self, 'domain': Site.objects.get_current().domain, } subjects, bodies = localize_email( 'projects/emails/project_created_subject.txt', 'projects/emails/project_created.txt', context) for organizer in self.organizers(): SendUserEmail.apply_async((organizer.user, subjects, bodies)) admin_subject = render_to_string( "projects/emails/admin_project_created_subject.txt", context).strip() admin_body = render_to_string( "projects/emails/admin_project_created.txt", context).strip() for admin_email in settings.ADMIN_PROJECT_CREATE_EMAIL: send_mail(admin_subject, admin_body, admin_email, [admin_email], fail_silently=True) def accepted_school(self): # Used previously when schools had to decline groups. return self.school def check_tasks_completion(self, user): total_count = self.pages.filter(listed=True, deleted=False).count() completed_count = PerUserTaskCompletion.objects.filter( page__project=self, page__deleted=False, unchecked_on__isnull=True, user=user).count() if total_count == completed_count: badges = self.get_project_badges(only_self_completion=True) for badge in badges: badge.award_to(user) def completed_tasks_users(self): total_count = self.pages.filter(listed=True, deleted=False).count() completed_stats = PerUserTaskCompletion.objects.filter( page__project=self, page__deleted=False, unchecked_on__isnull=True).values( 'user__username').annotate(completed_count=Count('page')).filter( completed_count=total_count) usernames = completed_stats.values( 'user__username') return Relationship.objects.filter(source__username__in=usernames, target_project=self, source__deleted=False) def get_project_badges(self, only_self_completion=False, only_peer_skill=False, only_peer_community=False): from badges.models import Badge assessment_types = [] badge_types = [] if not only_self_completion and not only_peer_community: assessment_types.append(Badge.PEER) badge_types.append(Badge.SKILL) if not only_peer_skill and not only_peer_community: assessment_types.append(Badge.SELF) badge_types.append(Badge.COMPLETION) if not only_peer_skill and not only_self_completion: assessment_types.append(Badge.PEER) badge_types.append(Badge.COMMUNITY) if assessment_types and badge_types: return self.badges.filter(assessment_type__in=assessment_types, badge_type__in=badge_types) else: return Badge.objects.none() def get_upon_completion_badges(self, user): from badges.models import Badge, Award if user.is_authenticated(): profile = user.get_profile() awarded_badges = Award.objects.filter( user=profile).values('badge_id') self_completion_badges = self.get_project_badges( only_self_completion=True) upon_completion_badges = [] for badge in self_completion_badges: missing_prerequisites = badge.prerequisites.exclude( id__in=awarded_badges).exclude( id__in=self_completion_badges.values('id')) if not missing_prerequisites.exists(): upon_completion_badges.append(badge.id) return Badge.objects.filter(id__in=upon_completion_badges) else: return Badge.objects.none() def get_awarded_badges(self, user, only_peer_skill=False): from badges.models import Badge, Award if user.is_authenticated(): profile = user.get_profile() awarded_badges = Award.objects.filter( user=profile).values('badge_id') project_badges = self.get_project_badges(only_peer_skill) return project_badges.filter( id__in=awarded_badges) else: return Badge.objects.none() def get_badges_in_progress(self, user): from badges.models import Badge, Award, Submission if user.is_authenticated(): profile = user.get_profile() awarded_badges = Award.objects.filter( user=profile).values('badge_id') attempted_badges = Submission.objects.filter( author=profile).values('badge_id') project_badges = self.get_project_badges( only_peer_skill=True) return project_badges.filter( id__in=attempted_badges).exclude( id__in=awarded_badges) else: return Badge.objects.none() def get_non_attempted_badges(self, user): from badges.models import Badge, Award, Submission if user.is_authenticated(): profile = user.get_profile() awarded_badges = Award.objects.filter( user=profile).values('badge_id') attempted_badges = Submission.objects.filter( author=profile).values('badge_id') project_badges = self.get_project_badges( only_peer_skill=True) # Excluding both awarded and attempted badges # In case honorary award do not rely on submissions. return project_badges.exclude( id__in=attempted_badges).exclude( id__in=awarded_badges) else: return Badge.objects.none() def get_need_reviews_badges(self, user): from badges.models import Badge, Award, Submission if user.is_authenticated(): profile = user.get_profile() project_badges = self.get_project_badges( only_peer_skill=True) peers_submissions = Submission.objects.filter( badge__id__in=project_badges.values('id')).exclude( author=profile) peers_attempted_badges = project_badges.filter( id__in=peers_submissions.values('badge_id')) need_reviews_badges = [] for badge in peers_attempted_badges: peers_awards = Award.objects.filter( badge=badge).exclude(user=profile) pending_submissions = peers_submissions.filter( badge=badge).exclude( author__id__in=peers_awards.values('user_id')) if pending_submissions.exists(): need_reviews_badges.append(badge.id) return project_badges.filter( id__in=need_reviews_badges) else: return Badge.objects.none() def get_non_started_next_projects(self, user): """To be displayed in the Join Next Challenges section.""" if user.is_authenticated(): profile = user.get_profile() joined = Participation.objects.filter( user=profile).values('project_id') return self.next_projects.exclude( id__in=joined) else: return Project.objects.none() @staticmethod def filter_activities(activities): from statuses.models import Status content_types = [ ContentType.objects.get_for_model(Page), ContentType.objects.get_for_model(PageComment), ContentType.objects.get_for_model(Status), ContentType.objects.get_for_model(Project), ] return activities.filter(target_content_type__in=content_types) @staticmethod def filter_learning_activities(activities): pages_ct = ContentType.objects.get_for_model(Page) comments_ct = ContentType.objects.get_for_model(PageComment) return activities.filter( target_content_type__in=[pages_ct, comments_ct])
class PageComment(ModelBase): """Placeholder model for comments.""" object_type = object_types['comment'] content = RichTextField(config_name='rich', blank=False) author = models.ForeignKey('users.UserProfile', related_name='comments') # the comments can live inside a project, a school, or just be associated # with their author scope_content_type = models.ForeignKey(ContentType, null=True, related_name='scope_page_comments') scope_id = models.PositiveIntegerField(null=True) scope_object = generic.GenericForeignKey('scope_content_type', 'scope_id') # object to which the comments are associated (a task, a signup answer, # an activity on the wall, ...) page_content_type = models.ForeignKey(ContentType, null=True) page_id = models.PositiveIntegerField(null=True) page_object = generic.GenericForeignKey('page_content_type', 'page_id') created_on = models.DateTimeField(auto_now_add=True, default=datetime.datetime.now) reply_to = models.ForeignKey('replies.PageComment', blank=True, null=True, related_name='replies') abs_reply_to = models.ForeignKey('replies.PageComment', blank=True, null=True, related_name='all_replies') deleted = models.BooleanField(default=False) # indicate that a comment was sent via email sent_by_email = models.BooleanField(default=False, blank=True) def __unicode__(self): return _('comment at %s') % self.page_object @models.permalink def get_absolute_url(self): return ('comment_show', (), { 'comment_id': self.id, }) def has_visible_childs(self): return self.visible_replies().exists() def visible_replies(self): return self.all_replies.filter(deleted=False).order_by('created_on') def can_edit(self, user): if user.is_authenticated(): profile = user.get_profile() return (profile == self.author) else: return False def reply(self, user, reply_body, sent_by_email=False): """ Create a reply from user to this comment """ # TODO check if user can reply reply_comment = PageComment() reply_comment.author = user reply_comment.content = reply_body reply_comment.page_object = self.page_object reply_comment.scope_object = self.scope_object reply_comment.reply_to = self reply_comment.abs_reply_to = self.abs_reply_to or self reply_comment.sent_by_email = sent_by_email reply_comment.save() reply_comment.send_comment_notification() return True def send_comment_notification(self): recipients = self.page_object.comment_notification_recipients(self) subject_template = 'replies/emails/post_comment_subject.txt' body_template = 'replies/emails/post_comment.txt' context = { 'comment': self, 'domain': Site.objects.get_current().domain, } profiles = [] for profile in recipients: if self.author != profile: profiles.append(profile) reply_url = reverse('email_reply', args=[self.id]) send_notifications(profiles, subject_template, body_template, context, reply_url, self.author.username)
class Badge(ModelBase): """ Representation of a Badge """ name = models.CharField(max_length=225, blank=False) slug = models.SlugField(unique=True, max_length=110) description = models.CharField(max_length=225, blank=False) requirements = RichTextField(blank=True, null=True) image = models.ImageField(upload_to=determine_upload_path, default='', blank=True, null=True, storage=storage.ImageStorage()) prerequisites = models.ManyToManyField('self', symmetrical=False, blank=True, null=True) rubrics = models.ManyToManyField('badges.Rubric', related_name='badges', null=True, blank=True) logic = models.ForeignKey( 'badges.Logic', related_name='badges', help_text=_('Regulates how the badge is awarded to users.')) all_groups = models.BooleanField(default=False) groups = models.ManyToManyField('projects.Project', related_name='badges', null=True, blank=True) creator = models.ForeignKey('users.UserProfile', related_name='badges', blank=True, null=True) created_on = models.DateTimeField(auto_now_add=True, blank=False) def __unicode__(self): return "%s %s" % (self.name, _('Badge')) @models.permalink def get_absolute_url(self): return ('badges_show', (), { 'slug': self.slug, }) def get_image_url(self): # TODO: using project's default image until a default badge # image is added. missing = settings.STATIC_URL + 'images/missing-badge.png' image_path = self.image.url if self.image else missing return image_path def save(self): """Make sure each badge has a unique slug.""" count = 1 if not self.slug: slug = slugify(self.name) self.slug = slug while True: existing = Badge.objects.filter(slug=self.slug) if len(existing) == 0: break self.slug = "%s-%s" % (slug, count + 1) count += 1 super(Badge, self).save() def is_eligible(self, user): """Check if the user eligible for the badge. If some prerequisite badges have not been awarded returns False.""" awarded_badges = Award.objects.filter(user=user).values('badge_id') return not self.prerequisites.exclude(id__in=awarded_badges).exists() def is_awarded_to(self, user): """Does the user have the badge?""" return Award.objects.filter(user=user, badge=self).count() > 0 def award_to(self, user, submission=None): """Award the badge to the user. Returns None if no badge is awarded.""" if not self.is_eligible(user): # If the user is not eligible the badge # is not awarded. return None if self.pending_peer_reviews(user, submission): # The user has not received the necessary satisfactory reviews # for the badge to be awarded. return None if submission: submission.pending = False submission.save() if self.logic.unique: # Do not award the badge if the user can not have the badge # more than once and it was already awarded. award, created = Award.objects.get_or_create(user=user, badge=self) return award if created else None return Award.objects.create(user=user, badge=self) def pending_peer_reviews(self, user, submission): if not self.logic.min_votes: return False assessments = Assessment.objects.filter(badge=self, assessed=user, ready=True) if submission: assessments = assessments.filter(submission=submission) else: assessments = assessments.filter(submission__isnull=True) if assessments.count() < self.logic.min_votes: # More votes needed. return True if not self.logic.min_avg_rating: return False avg_rating = Assessment.compute_average_rating(assessments) if avg_rating < self.logic.min_avg_rating: # Rating too low. return True def get_pending_submissions(self): """Submissions of users who haven't received the award yet""" return Submission.objects.filter(badge=self, pending=True) def get_peers(self, profile): from projects.models import Participation from users.models import UserProfile user_projects = Participation.objects.filter( user=profile).values('project__id') peers = Participation.objects.filter(project__in=user_projects) if not self.all_groups: badge_projects = self.groups.values('id') peers = peers.filter(project__in=badge_projects) return UserProfile.objects.filter( deleted=False, id__in=peers.values('user__id')).exclude(id=profile.id) def other_badges_can_apply_for(self): badges = Badge.objects.exclude(id=self.id).exclude( logic__submission_style=Logic.NO_SUBMISSIONS) badge_groups = self.groups.values('id') related_badges = badges.filter(groups__in=badge_groups).distinct() non_related_badges = badges.exclude(groups__in=badge_groups).distinct() return MultiQuerySet(related_badges, non_related_badges) def can_post_submission(self, user): if self.logic.submission_style == Logic.NO_SUBMISSIONS: return False if user.is_authenticated(): profile = user.get_profile() if not profile.can_post(): return False if not self.is_eligible(profile): return False awards = Award.objects.filter(user=profile, badge=self) if self.logic.unique and awards.exists(): return False return True else: return False def can_give_to_peer(self, user): if not user.is_authenticated(): return False if self.logic.submission_style == Logic.SUBMISSION_REQUIRED: return False if self.logic.min_votes != 1 or self.logic.min_avg_rating > 0: return False return True def can_review_submission(self, submission, user): # only authenticated users can review a submission if not user.is_authenticated(): return False profile = user.get_profile() # user cannot review his/her own submission if profile == submission.author: return False # if this is a unique badge, only allow one review of submission if self.logic.unique and not submission.pending: return False # user can only submit one review assessments = submission.assessments.filter(assessor=profile) if assessments.exists(): return False return True def get_adopters(self): from projects.models import Participation from users.models import UserProfile adopters = Participation.objects.filter( project__in=self.groups.values('id'), left_on__isnull=True).filter(Q(adopter=True) | Q( organizing=True)).values('user_id').distinct() return UserProfile.objects.filter(id__in=adopters)
class Assessment(ModelBase): """Assessment for a badge""" final_rating = models.FloatField(default=0) weight = models.FloatField( default=1, help_text=_( "Allows to give more or less weight to the assessor's vote.")) assessor = models.ForeignKey('users.UserProfile', related_name='assessments') assessed = models.ForeignKey('users.UserProfile', related_name='badge_assessments') comment = RichTextField(config_name='rich', blank=False) badge = models.ForeignKey('badges.Badge', related_name="assessments") created_on = models.DateTimeField(auto_now_add=True, default=datetime.datetime.now) submission = models.ForeignKey('badges.Submission', related_name="assessments", null=True, blank=True, help_text=_('If submission is blank, this is a '\ 'peer awarded assessment or superuser granted')) ready = models.BooleanField( default=False, help_text=_("If all rubric ratings were provided.")) def __unicode__(self): return _('%(assessor)s for %(assessed)s for %(badge)s') % { 'assessor': self.assessor, 'assessed': self.assessed, 'badge': self.badge } @models.permalink def get_absolute_url(self): return ('assessment_show', (), { 'slug': self.badge.slug, 'assessment_id': self.id, }) def final_rating_as_percentage(self): """Return the final rating as a percentage for styling of assessment view. Max number of ratings is 4""" return (self.final_rating / 4.0) * 100 def get_final_rating_display(self): rating_position = int(round(self.final_rating)) - 1 # Guarantee rating_position does not go above or below # the boundaries. if rating_position < 0: rating_position = 0 max_index = len(Rating.RATING_CHOICES) - 1 if rating_position > max_index: rating_position = max_index return Rating.RATING_CHOICES[rating_position][1] def update_final_rating(self): """Used on Rating save signal to update the final rating for the assessment""" if self.ready: return ratings = Rating.objects.filter(assessment=self) self.final_rating = ratings.aggregate( final_rating=Avg('score'))['final_rating'] or 0 if ratings.count() == self.badge.rubrics.count(): self.ready = True self.save() if self.submission and not self.submission.pending: return if self.ready: self.badge.award_to(self.assessed, self.submission) @classmethod def compute_average_rating(cls, assessments): ratings_sum = 0 weights_sum = 0 for assessment in assessments: ratings_sum += assessment.final_rating weights_sum += assessment.weight return ratings_sum / weights_sum if weights_sum > 0 else 0
class Status(ModelBase): object_type = object_types['status'] author = models.ForeignKey('users.UserProfile') project = models.ForeignKey('projects.Project', null=True, blank=True) status = RichTextField(blank=False) created_on = models.DateTimeField(auto_now_add=True, default=datetime.datetime.now) important = models.BooleanField(default=False) activity = generic.GenericRelation( Activity, content_type_field='target_content_type', object_id_field='target_id') class Meta: verbose_name_plural = _('statuses') def __unicode__(self): return _('message: %s') % self.status def get_absolute_url(self): ct = ContentType.objects.get_for_model(Status) activity = Activity.objects.get(target_id=self.id, target_content_type=ct) return activity.get_absolute_url() def send_wall_notification(self): if not self.project: return recipients = self.project.participants() subject_template = 'statuses/emails/wall_updated_subject.txt' body_template = 'statuses/emails/wall_updated.txt' context = { 'status': self, 'project': self.project, 'domain': Site.objects.get_current().domain, } from_organizer = self.project.organizers().filter( user=self.author).exists() profiles = [ recipient.user for recipient in recipients if self.author != recipient.user ] kwargs = { 'page_app_label': 'activity', 'page_model': 'activity', 'page_pk': self.activity.get().id, } if self.project: kwargs.update({ 'scope_app_label': 'projects', 'scope_model': 'project', 'scope_pk': self.project.id, }) callback_url = reverse('page_comment_callback', kwargs=kwargs) send_notifications_i18n( profiles, subject_template, body_template, context, callback_url, self.author.username, notification_category=u'course-announcement.project-{0}'.format( self.project.slug)) @staticmethod def filter_activities(activities): ct = ContentType.objects.get_for_model(Status) return activities.filter(target_content_type=ct)
class PageComment(ModelBase): """Placeholder model for comments.""" object_type = object_types['comment'] content = RichTextField(config_name='rich', blank=False) author = models.ForeignKey('users.UserProfile', related_name='comments') # the comments can live inside a project, a school, or just be associated # with their author scope_content_type = models.ForeignKey(ContentType, null=True, related_name='scope_page_comments') scope_id = models.PositiveIntegerField(null=True) scope_object = generic.GenericForeignKey('scope_content_type', 'scope_id') # object to which the comments are associated (a task, a signup answer, # an activity on the wall, ...) page_content_type = models.ForeignKey(ContentType, null=True) page_id = models.PositiveIntegerField(null=True) page_object = generic.GenericForeignKey('page_content_type', 'page_id') created_on = models.DateTimeField(auto_now_add=True, default=datetime.datetime.now) reply_to = models.ForeignKey('replies.PageComment', blank=True, null=True, related_name='replies') abs_reply_to = models.ForeignKey('replies.PageComment', blank=True, null=True, related_name='all_replies') deleted = models.BooleanField(default=False) def __unicode__(self): return _('comment at %s') % self.page_object @models.permalink def get_absolute_url(self): return ('comment_show', (), { 'comment_id': self.id, }) def has_visible_childs(self): return self.visible_replies().exists() def visible_replies(self): return self.all_replies.filter(deleted=False).order_by('created_on') def can_edit(self, user): if user.is_authenticated(): profile = user.get_profile() return (profile == self.author) else: return False def send_comment_notification(self): context = { 'comment': self, 'domain': Site.objects.get_current().domain, } subjects, bodies = localize_email( 'replies/emails/post_comment_subject.txt', 'replies/emails/post_comment.txt', context) recipients = self.page_object.comment_notification_recipients(self) for recipient in recipients: if self.author != recipient: SendUserEmail.apply_async((recipient, subjects, bodies))
class Project(ModelBase): """Placeholder model for projects.""" object_type = object_types['group'] name = models.CharField(max_length=100) short_name = models.CharField(max_length=20, null=True, blank=True) # Select kind of project (study group, course, or other) STUDY_GROUP = 'study group' COURSE = 'course' CHALLENGE = 'challenge' CATEGORY_CHOICES = ( (STUDY_GROUP, _('Study Group -- group of people working ' \ 'collaboratively to acquire and share knowledge.')), (COURSE, _('Course -- led by one or more organizers with skills on ' \ 'a field who direct and help participants during their ' \ 'learning.')), (CHALLENGE, _('Challenge -- series of tasks peers can engage in ' \ 'to develop skills.')) ) category = models.CharField(max_length=30, choices=CATEGORY_CHOICES, default=STUDY_GROUP, null=True, blank=False) tags = TaggableManager(through=GeneralTaggedItem, blank=True) language = models.CharField(max_length=16, choices=settings.LANGUAGES, default=settings.LANGUAGE_CODE) other = models.CharField(max_length=30, blank=True, null=True) other_description = models.CharField(max_length=150, blank=True, null=True) short_description = models.CharField(max_length=150) long_description = RichTextField(validators=[MaxLengthValidator(700)]) start_date = models.DateField(null=True, blank=True) end_date = models.DateField(null=True, blank=True) duration_hours = models.PositiveIntegerField(default=0, blank=True) duration_minutes = models.PositiveIntegerField(default=0, blank=True) school = models.ForeignKey('schools.School', related_name='projects', null=True, blank=True) detailed_description = models.ForeignKey('content.Page', related_name='desc_project', null=True, blank=True) image = models.ImageField(upload_to=determine_image_upload_path, null=True, storage=storage.ImageStorage(), blank=True) slug = models.SlugField(unique=True, max_length=110) # this field is deprecated featured = models.BooleanField(default=False, verbose_name='staff favourite') # this field is deprecated community_featured = models.BooleanField(default=False, verbose_name='community pick') created_on = models.DateTimeField( auto_now_add=True, default=datetime.datetime.now) # Indicates a test course. Affects activities and notifications test = models.BooleanField(default=False) under_development = models.BooleanField(default=True) not_listed = models.BooleanField(default=False) archived = models.BooleanField(default=False) clone_of = models.ForeignKey('projects.Project', blank=True, null=True, related_name='derivated_projects') imported_from = models.CharField(max_length=150, blank=True, null=True) next_projects = models.ManyToManyField('projects.Project', symmetrical=False, related_name='previous_projects', blank=True, null=True) # Stealth Badges awarded upon completion of all tasks. completion_badges = models.ManyToManyField('badges.Badge', null=True, blank=True, related_name='projects_completion') deleted = models.BooleanField(default=False) class Meta: verbose_name = _('group') def __unicode__(self): return _('%(name)s %(kind)s') % dict(name=self.name, kind=self.kind.lower()) @models.permalink def get_absolute_url(self): return ('projects_show', (), { 'slug': self.slug, }) def friendly_verb(self, verb): if verbs['post'] == verb: return _('created') @property def kind(self): return self.other.lower() if self.other else self.category def followers(self, include_deleted=False): relationships = Relationship.objects.all() if not include_deleted: relationships = relationships.filter( source__deleted=False) return relationships.filter(target_project=self, deleted=False) def previous_followers(self, include_deleted=False): """Return a list of users who were followers if this project.""" relationships = Relationship.objects.all() if not include_deleted: relationships = relationships.filter( source__deleted=False) return relationships.filter(target_project=self, deleted=True) def non_participant_followers(self, include_deleted=False): return self.followers(include_deleted).exclude( source__id__in=self.participants(include_deleted).values('user_id')) def participants(self, include_deleted=False): """Return a list of users participating in this project.""" participations = Participation.objects.all() if not include_deleted: participations = participations.filter(user__deleted=False) return participations.filter(project=self, left_on__isnull=True) def non_organizer_participants(self, include_deleted=False): return self.participants(include_deleted).filter(organizing=False) def adopters(self, include_deleted=False): return self.participants(include_deleted).filter(Q(adopter=True) | Q(organizing=True)) def non_adopter_participants(self, include_deleted=False): return self.non_organizer_participants(include_deleted).filter( adopter=False) def organizers(self, include_deleted=False): return self.participants(include_deleted).filter(organizing=True) def publish(self): """ Remove all test, under_development and closed signups from the course """ self.test = False self.under_development = False self.save() if self.category == self.COURSE: signup = self.sign_up.get() if signup.is_closed(): signup.set_unmoderated_signup() def is_organizing(self, user): if user.is_authenticated(): profile = user.get_profile() is_organizer = self.organizers().filter(user=profile).exists() is_superuser = user.is_superuser return is_organizer or is_superuser else: return False def is_following(self, user): if user.is_authenticated(): profile = user.get_profile() is_following = self.followers().filter(source=profile).exists() return is_following else: return False def is_participating(self, user): if user.is_authenticated(): profile = user.get_profile() is_organizer_or_participant = self.participants().filter( user=profile).exists() is_superuser = user.is_superuser return is_organizer_or_participant or is_superuser else: return False def get_metrics_permissions(self, user): """Provides metrics related permissions for metrics overview and CSV download.""" if user.is_authenticated(): if user.is_superuser: return True if self.is_organizing(user): return True if not self.school: return False is_school_organizer = self.school.organizers.filter( id=user.id).exists() if is_school_organizer: return True return False def get_metric_csv_permission(self, user): """Provides metrics related permissions for metrics CSV download.""" if user.is_authenticated(): # check for explicit permission grant csv_downloaders = settings.STATISTICS_CSV_DOWNLOADERS profile = user.get_profile() if profile.username in csv_downloaders: return True return self.get_metrics_permissions(user) return False def activities(self): return Activity.objects.filter(deleted=False, scope_object=self).order_by('-created_on') def create(self): self.save() self.send_creation_notification() def update_learn_api(self): if not self.pk: return try: learn_model.update_course_listing(**self.get_learn_api_data()) except: learn_model.add_course_listing(**self.get_learn_api_data()) course_url = reverse('projects_show', kwargs={'slug': self.slug}) course_lists = learn_model.get_lists_for_course(course_url) list_names = [ l['name'] for l in course_lists ] if self.not_listed or self.test: for list_name in list_names: learn_model.remove_course_from_list(course_url, list_name) else: desired_list = 'drafts' if not (self.under_development or self.archived): desired_list = 'listed' elif self.archived: desired_list = 'archived' possible_lists = ['drafts', 'listed', 'archived'] possible_lists.remove(desired_list) for l in possible_lists: if l in list_names: learn_model.remove_course_from_list(course_url, l) if desired_list not in list_names: learn_model.add_course_to_list(course_url, desired_list) def save(self): """Make sure each project has a unique slug.""" count = 1 if not self.slug: slug = slugify(self.name) self.slug = slug while True: existing = Project.objects.filter(slug=self.slug) if len(existing) == 0: break self.slug = "%s-%s" % (slug, count + 1) count += 1 try: self.update_learn_api() except: log.error('Could not update course info in the learn API') super(Project, self).save() def set_duration(self, value): """Sets (without saving) duration in hours and minutes given a decimal value. e.g., a decimal value of 10.3 equals 10 hours and 18 minutes.""" value = value or 0 hours = int(value) minutes = int(60 * (value - hours)) self.duration_hours = hours self.duration_minutes = minutes def get_duration(self): """Gets closest decimal value that represents the current duration. e.g., a duration of 10 hours and 18 minutes corresponds to the decimal value 10.3 """ return round(self.duration_hours + (self.duration_minutes / 60.0), 1) def get_image_url(self): missing = settings.STATIC_URL + 'images/project-missing.png' image_path = self.image.url if self.image else missing return image_path def get_learn_api_data(self): """ return data used for learn API """ course_url = reverse('projects_show', kwargs={'slug': self.slug}) learn_api_data = { "course_url": course_url, "title": self.name, "description": self.short_description, "data_url": "", "language": self.language, "thumbnail_url": self.get_image_url(), "tags": self.tags.all().values_list('name', flat=True) } return learn_api_data def send_creation_notification(self): """Send notification when a new project is created.""" subject_template = 'projects/emails/project_created_subject.txt' body_template = 'projects/emails/project_created.txt' context = { 'project': self, 'domain': Site.objects.get_current().domain, } profiles = [recipient.user for recipient in self.organizers()] send_notifications_i18n(profiles, subject_template, body_template, context, notification_category='course-created' ) if not self.test: admin_subject = render_to_string( "projects/emails/admin_project_created_subject.txt", context).strip() admin_body = render_to_string( "projects/emails/admin_project_created.txt", context).strip() # TODO send using notifications and get email addresses from group, not settings for admin_email in settings.ADMIN_PROJECT_CREATE_EMAIL: send_mail(admin_subject, admin_body, admin_email, [admin_email], fail_silently=True) def accepted_school(self): # Used previously when schools had to decline groups. return self.school def check_tasks_completion(self, user): total_count = self.pages.filter(listed=True, deleted=False).count() completed_count = PerUserTaskCompletion.objects.filter( page__project=self, page__deleted=False, unchecked_on__isnull=True, user=user).count() if total_count == completed_count: for badge in self.completion_badges.all(): badge.award_to(user) def completed_tasks_users(self): custom_query = """ SELECT `relationships_relationship`.`id`, `relationships_relationship`.`source_id`, `relationships_relationship`.`target_user_id`, `relationships_relationship`.`target_project_id`, `relationships_relationship`.`created_on`, `relationships_relationship`.`deleted` FROM `relationships_relationship` INNER JOIN `users_userprofile` ON (`relationships_relationship`.`source_id` = `users_userprofile`.`id`) INNER JOIN `projects_perusertaskcompletion` ON (`relationships_relationship`.`source_id` = `projects_perusertaskcompletion`.`user_id`) INNER JOIN `content_page` U1 ON (`projects_perusertaskcompletion`.`page_id` = U1.`id`) WHERE `projects_perusertaskcompletion`.`unchecked_on` IS NULL AND U1.`project_id` = {project_id} AND U1.`deleted` = False AND `users_userprofile`.`deleted` = False AND `relationships_relationship`.`target_project_id` = {project_id} GROUP BY `projects_perusertaskcompletion`.`user_id`, `projects_perusertaskcompletion`.`user_id` HAVING COUNT(`projects_perusertaskcompletion`.`page_id`) = {task_count} LIMIT 56;""" total_count = self.pages.filter(listed=True, deleted=False).count() #completed_stats = PerUserTaskCompletion.objects.filter( # page__project=self, page__deleted=False, # unchecked_on__isnull=True).values( # 'user').annotate(completed_count=Count('page')).filter( # completed_count=total_count) #usernames = completed_stats.values_list( # 'user', flat=True) #return Relationship.objects.filter(source__in=usernames, # target_project=self, source__deleted=False) return Relationship.objects.raw(custom_query.format(project_id=self.id, task_count=total_count)) def get_badges(self): from badges.models import Badge return Badge.objects.filter( Q(groups=self.id) | Q(all_groups=True)).distinct() def get_submission_enabled_badges(self): from badges.models import Logic return self.get_badges().exclude( logic__submission_style=Logic.NO_SUBMISSIONS) def get_badges_peers_can_give(self): from badges.models import Logic, Badge return self.get_badges().filter( logic__min_votes=1, logic__min_avg_rating=0).exclude( logic__submission_style=Logic.SUBMISSION_REQUIRED) def get_upon_completion_badges(self, user): from badges.models import Badge, Award if user.is_authenticated(): profile = user.get_profile() awarded_badges = Award.objects.filter( user=profile).values('badge_id') self_completion_badges = self.completion_badges.all() upon_completion_badges = [] for badge in self_completion_badges: missing_prerequisites = badge.prerequisites.exclude( id__in=awarded_badges).exclude( id__in=self_completion_badges.values('id')) if not missing_prerequisites.exists(): upon_completion_badges.append(badge.id) return Badge.objects.filter(id__in=upon_completion_badges) else: return Badge.objects.none() def get_awarded_badges(self, user, exclude_completion_badges=False): from badges.models import Badge, Award if user.is_authenticated(): profile = user.get_profile() project_badges = self.get_badges() if exclude_completion_badges: completion_badges = self.completion_badges.all() project_badges = project_badges.exclude( id__in=completion_badges.values('id')) awarded_badges = Award.objects.filter( user=profile).values('badge_id') return project_badges.filter( id__in=awarded_badges) else: return Badge.objects.none() def get_badges_in_progress(self, user): from badges.models import Badge, Award, Submission if user.is_authenticated(): profile = user.get_profile() awarded_badges = Award.objects.filter( user=profile).values('badge_id') attempted_badges = Submission.objects.filter( author=profile, pending=True).values('badge_id') return self.badges.filter( id__in=attempted_badges).exclude( id__in=awarded_badges) else: return Badge.objects.none() def get_non_attempted_badges(self, user): from badges.models import Badge, Award, Submission if user.is_authenticated(): profile = user.get_profile() awarded_badges = Award.objects.filter( user=profile).values('badge_id') attempted_badges = Submission.objects.filter( author=profile).values('badge_id') project_badges = self.get_submission_enabled_badges() # Excluding both awarded and attempted badges # In case honorary award do not rely on submissions. return project_badges.exclude( id__in=attempted_badges).exclude( id__in=awarded_badges) else: return Badge.objects.none() def get_non_started_next_projects(self, user): """To be displayed in the Join Next Challenges section.""" if user.is_authenticated(): profile = user.get_profile() joined = Participation.objects.filter( user=profile).values('project_id') return self.next_projects.exclude( id__in=joined) else: return Project.objects.none() @staticmethod def filter_activities(activities): from statuses.models import Status content_types = [ ContentType.objects.get_for_model(Page), ContentType.objects.get_for_model(PageComment), ContentType.objects.get_for_model(Status), ContentType.objects.get_for_model(Project), ] return activities.filter(target_content_type__in=content_types) @staticmethod def filter_learning_activities(activities): pages_ct = ContentType.objects.get_for_model(Page) comments_ct = ContentType.objects.get_for_model(PageComment) return activities.filter( target_content_type__in=[pages_ct, comments_ct])
class ProjectSet(ModelBase): "Model for the project sets" name = models.CharField(max_length=100) slug = models.SlugField(unique=True, blank=True) description = RichTextField(config_name='rich') short_description = models.CharField(max_length=150) school = models.ForeignKey(School, blank=True, null=True, related_name="project_sets") projects = models.ManyToManyField('projects.Project', blank=True, null=True, related_name="projectsets", through='schools.ProjectSetIndex') featured = models.BooleanField(default=False) image = models.ImageField( upload_to=projectsets_determine_image_upload_path, null=True, storage=storage.ImageStorage(), blank=True) def __unicode__(self): return self.name @models.permalink def get_absolute_url(self): return ('school_projectset', (), { 'slug': self.school.slug, 'set_slug': self.slug, }) def get_image_url(self): # TODO: using project's default image until a default badge # image is added. missing = settings.STATIC_URL + 'images/missing-challenge-set.png' image_path = self.image.url if self.image else missing return image_path def get_projects(self): return self.projects.order_by('projectsetindex__index') @property def first_project(self): try: return self.projects.order_by('projectsetindex__index')[0] except IndexError: return None def _distinct_participants(self): return UserProfile.objects.filter( participations__project__projectsets=self, deleted=False, participations__left_on__isnull=True).distinct() def total_participants(self): return self._distinct_participants().filter( participations__adopter=False, participations__organizing=False).count() def total_adopters(self): return self._distinct_participants().filter( Q(participations__adopter=True) | Q(participations__organizing=True)).count() def total_badges(self): return Badge.objects.filter( groups__projectsets=self).distinct().count()
class Page(ModelBase): """Placeholder model for pages.""" object_type = object_types['article'] title = models.CharField(max_length=100) slug = models.SlugField(max_length=110) sub_header = models.CharField(max_length=150, blank=True, null=True) content = RichTextField(config_name='rich') author = models.ForeignKey('users.UserProfile', related_name='pages') last_update = models.DateTimeField(auto_now_add=True, default=datetime.datetime.now) project = models.ForeignKey('projects.Project', related_name='pages') listed = models.BooleanField(default=True) minor_update = models.BooleanField(default=True) collaborative = models.BooleanField(default=True) index = models.IntegerField() deleted = models.BooleanField(default=False) comments = generic.GenericRelation(PageComment, content_type_field='page_content_type', object_id_field='page_id') # Badges to which the user can submit their work to. # Used to facilitate both posting a comment to the task with # a link to the work they did on the task and apply for skills badges badges_to_apply = models.ManyToManyField( 'badges.Badge', null=True, blank=True, related_name='tasks_accepting_submissions') def __unicode__(self): return self.title @models.permalink def get_absolute_url(self): return ('page_show', (), { 'slug': self.project.slug, 'page_slug': self.slug, }) def friendly_verb(self, verb): if verbs['post'] == verb: return _('added') def save(self): """Make sure each page has a unique url.""" count = 1 if not self.slug: slug = slugify(self.title) self.slug = slug while True: existing = Page.objects.filter(project__slug=self.project.slug, slug=self.slug) if len(existing) == 0: break self.slug = "%s-%s" % (slug, count + 1) count += 1 if not self.index: if self.listed: max_index = Page.objects.filter(project=self.project, listed=True).aggregate( Max('index'))['index__max'] self.index = max_index + 1 if max_index else 1 else: self.index = 0 super(Page, self).save() def get_next_page(self): if self.listed and not self.deleted: try: return self.project.pages.filter( deleted=False, index__gt=self.index, listed=True).order_by('index')[0] except IndexError: pass return None def get_next_badge_can_apply(self, profile): next_badges = self.badges_to_apply.order_by('id') next_badges_can_apply = [] for badge in next_badges: awarded = Award.objects.filter(user=profile, badge=badge).exists() applied = Submission.objects.filter(author=profile, badge=badge).exists() elegible = badge.is_eligible(profile) if not awarded and not applied and elegible: next_badges_can_apply.append(badge) if len(next_badges_can_apply) > 1: break next_badge = next_badges_can_apply[0] if next_badges_can_apply else None is_last_badge = not next_badges_can_apply[1:] return next_badge, is_last_badge def can_edit(self, user): if self.project.is_organizing(user): return True if self.collaborative: return self.project.is_participating(user) return False def first_level_comments(self): return self.comments.filter( reply_to__isnull=True).order_by('-created_on') def can_comment(self, user, reply_to=None): conditions = [ self.project.is_participating(user), not self.project.archived ] return all(conditions) def get_comment_url(self, comment, user): comment_index = 0 abs_reply_to = comment.abs_reply_to or comment for first_level_comment in self.first_level_comments(): if abs_reply_to.id == first_level_comment.id: break comment_index += 1 items_per_page = settings.PAGINATION_DEFAULT_ITEMS_PER_PAGE page = (comment_index / items_per_page) + 1 url = self.get_absolute_url() return url + '?pagination_page_number=%s#%s' % (page, comment.id) def comments_fire_activity(self): return True def comment_notification_recipients(self, comment): from users.models import UserProfile participants = self.project.participants() from_organizer = self.project.organizers().filter( user=comment.author).exists() if from_organizer: participants = participants.filter( no_organizers_content_updates=False) else: participants = participants.filter( no_participants_content_updates=False) return UserProfile.objects.filter( id__in=participants.values('user__id')) def recent_activity(self, min_count=2): comments = self.comments.filter(deleted=False) today = datetime.date.today() day = today.day month = today.month year = today.year # get today's commments count today_comments_count = comments.filter(created_on__day=day, created_on__month=month, created_on__year=year).count() if today_comments_count >= min_count: return today_comments_count, _('today') # get this week comments count week = today.isocalendar()[1] first_day = datetime.date(year, 1, 1) delta_days = first_day.isoweekday() - 1 delta_weeks = week if year == first_day.isocalendar()[0]: delta_weeks -= 1 week_start_delta = datetime.timedelta(days=-delta_days, weeks=delta_weeks) week_start = first_day + week_start_delta week_end_delta = datetime.timedelta(days=7 - delta_days, weeks=delta_weeks) week_end = first_day + week_end_delta this_week_comments_count = comments.filter( created_on__gte=week_start, created_on__lt=week_end).count() if this_week_comments_count >= min_count: return this_week_comments_count, _('this week') # get this month comments count this_month_comments_count = comments.filter( created_on__month=month, created_on__year=year).count() return this_month_comments_count, _('this month')
class School(ModelBase): """Placeholder model for schools.""" name = models.CharField(max_length=100) slug = models.SlugField(unique=True, blank=True) short_name = models.CharField(max_length=20) description = RichTextField(config_name='rich') more_info = RichTextField(config_name='rich', null=True, blank=True) organizers = models.ManyToManyField('users.UserProfile', null=True, blank=True) featured = models.ManyToManyField('projects.Project', related_name='school_featured', null=True, blank=True) logo = models.ImageField(upload_to=schools_determine_image_upload_path, null=True, storage=storage.ImageStorage(), blank=True) groups_icon = models.ImageField( upload_to=schools_determine_image_upload_path, null=True, storage=storage.ImageStorage(), blank=True) background = models.ImageField( upload_to=schools_determine_image_upload_path, null=True, storage=storage.ImageStorage(), blank=True) site_logo = models.ImageField( upload_to=schools_determine_image_upload_path, null=True, storage=storage.ImageStorage(), blank=True) headers_color = models.CharField(max_length=7, default='#5a6579') headers_color_light = models.CharField(max_length=7, default='#f08c00') background_color = models.CharField(max_length=7, default='#ffffff') menu_color = models.CharField(max_length=7, default='#36cdc4') menu_color_light = models.CharField(max_length=7, default='#4bd2c9') sidebar_width = models.CharField(max_length=5, default='245px') show_school_organizers = models.BooleanField(default=True) extra_styles = models.TextField(blank=True) # The term names are used to import school courses from the old site. OLD_TERM_NAME_CHOICES = YEAR_IN_SCHOOL_CHOICES = ( ('Math Future', 'School of the Mathematical Future'), ('SoSI', 'School of Social Innovation'), ('Webcraft', 'School of Webcraft'), ) old_term_name = models.CharField(max_length=15, blank=True, null=True, choices=OLD_TERM_NAME_CHOICES) mentor_form_url = models.URLField(blank=True, null=True) mentee_form_url = models.URLField(blank=True, null=True) def __unicode__(self): return self.name @models.permalink def get_absolute_url(self): return ('school_home', (), { 'slug': self.slug, }) def save(self): """Make sure each school has a unique slug.""" count = 1 if not self.slug: slug = slugify(self.name) self.slug = slug while True: existing = School.objects.filter(slug=self.slug) if len(existing) == 0: break self.slug = "%s-%s" % (slug, count + 1) count += 1 super(School, self).save()