class IssueComment(UIDMixin): issue = models.ForeignKey(Issue, related_name="comments") active = models.BooleanField(default=True) ordinal = models.PositiveIntegerField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at")) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_("Created by"), related_name="issue_comments_created") version = models.PositiveIntegerField(default=1) last_edited_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Last Edited at")) last_edited_by = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_("Created by"), related_name="issue_comments_last_edited", null=True, blank=True) content = HTMLField(verbose_name=_("Comment")) class Meta: ordering = ('created_at', ) def update_content(self, expected_version, author, content): """ creates a new revision and updates current comment """ if self.version != expected_version: return False content = enhance_html(content.strip()) if self.content == content: return True with transaction.commit_on_success(): IssueCommentRevision.objects.create(comment=self, version=expected_version, created_at=self.created_at, created_by=self.created_by, content=self.content) self.version += 1 self.last_edited_at = timezone.now() self.last_edited_by = author self.content = content self.save() return True @models.permalink def get_delete_url(self): return "delete_issue_comment", (self.issue.community.id, self.id) @models.permalink def get_edit_url(self): return "edit_issue_comment", (self.issue.community.id, self.id)
class AgendaItem(ConfidentialByRelationMixin): confidential_from = 'issue' objects = ConfidentialManager() meeting = models.ForeignKey('Meeting', verbose_name=_("Meeting"), related_name="agenda") issue = models.ForeignKey(Issue, verbose_name=_("Issue"), related_name="agenda_items") background = HTMLField(_("Background"), null=True, blank=True) order = models.PositiveIntegerField(default=100, verbose_name=_("Order")) closed = models.BooleanField(_('Closed'), default=True) class Meta: unique_together = (("meeting", "issue"),) verbose_name = _("Agenda Item") verbose_name_plural = _("Agenda Items") ordering = ('meeting__created_at', 'order') def __unicode__(self): return self.issue.title # def natural_key(self): # return (self.meeting.natural_key(), self.issue.natural_key()) # natural_key.dependencies = ['meetings.meeting', 'issues.issue'] def attachments(self): return self.issue.attachments.filter(agenda_item=self) def comments(self): return self.issue.comments.filter(active=True, meeting=self.meeting) def proposals(self, user=None, community=None): rv = self.issue.proposals.object_access_control( user=user, community=community).filter( active=True, decided_at_meeting=self.meeting) return rv def accepted_proposals(self, user=None, community=None): rv = self.proposals(user=user, community=community).filter( status=ProposalStatus.ACCEPTED) return rv def rejected_proposals(self, user=None, community=None): rv = self.proposals(user=user, community=community).filter( status=ProposalStatus.REJECTED) return rv
class Issue(UIDMixin, ConfidentialMixin): objects = IssueManager() active = models.BooleanField(_("Active"), default=True) community = models.ForeignKey('communities.Community', on_delete=models.PROTECT, related_name="issues") created_at = models.DateTimeField(_("Created at"), auto_now_add=True) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_("Created by"), on_delete=models.PROTECT, related_name="issues_created") title = models.CharField(_("Title"), max_length=300) abstract = HTMLField(_("Background"), null=True, blank=True) content = HTMLField(_("Content"), null=True, blank=True) # TODO: remove me safely calculated_score = models.IntegerField(_("Calculated Score"), default=0) # TODO: remove me status = models.IntegerField(choices=IssueStatus.choices, default=IssueStatus.OPEN) statuses = IssueStatus order_in_upcoming_meeting = models.IntegerField( _("Order in upcoming meeting"), default=0, null=True, blank=True) order_by_votes = models.FloatField( _("Order in upcoming meeting by votes"), default=0, null=True, blank=True) length_in_minutes = models.IntegerField(_("Length (in minutes)"), null=True, blank=True) completed = models.BooleanField(_("Discussion completed"), default=False) # TODO: remove me safely is_published = models.BooleanField(_("Is published to members"), default=False) voteable = models.BooleanField(_("Open for voting"), default=False) class Meta: verbose_name = _("Issue") verbose_name_plural = _("Issues") ordering = ['order_in_upcoming_meeting', 'title'] def __str__(self): return self.title @models.permalink def get_edit_url(self): return ("issue_edit", (str(self.community.pk), str(self.pk),)) @models.permalink def get_delete_url(self): return ("issue_delete", (str(self.community.pk), str(self.pk),)) @models.permalink def get_absolute_url(self): return ("issue", (str(self.community.pk), str(self.pk),)) @models.permalink def get_next_upcoming_issue_url(self): try: next = Issue.objects.filter(community=self.community, status=2, active=True).filter( order_in_upcoming_meeting__gt=self.order_in_upcoming_meeting).order_by( 'order_in_upcoming_meeting').first() return "issue", (str(self.community.pk), str(next.pk),) except: return "community", (str(self.community.pk),) def active_proposals(self): return self.proposals.filter(active=True) def open_proposals(self): return self.active_proposals().filter( status=Proposal.statuses.IN_DISCUSSION) def active_comments(self): return self.comments.filter(active=True) def new_comments(self): return self.comments.filter(meeting_id=None) def historical_comments(self): return self.comments.filter(active=True).exclude(meeting_id=None) def active_references(self): return self.references.filter(active=True) def new_references(self): return self.references.filter(meeting_id=None) def historical_references(self): return self.references.filter(active=True).exclude(meeting_id=None) def has_closed_parts(self): """ Should be able to be viewed """ @property def is_upcoming(self): return self.status in IssueStatus.IS_UPCOMING @property def is_current(self): return self.status in IssueStatus.IS_UPCOMING and self.community.upcoming_meeting_started def changed_in_current(self): decided_at_current = self.proposals.filter(active=True, decided_at_meeting=None, status__in=[ ProposalStatus.ACCEPTED, ProposalStatus.REJECTED ]) return decided_at_current or self.new_comments().filter(active=True) @property def is_archived(self): return self.status == IssueStatus.ARCHIVED @property def in_closed_meeting(self): return meetings.models.AgendaItem.objects.filter(issue=self).exists() @property def can_straw_vote(self): # test date/time limit # if self.community.voting_ends_at: # time_till_close = self.community.voting_ends_at - timezone.now() # if time_till_close.total_seconds() <= 0: # return False return self.community.straw_voting_enabled and \ self.is_upcoming and \ self.voteable and \ self.community.upcoming_meeting_is_published and \ self.proposals.open().count() > 0 def current_attachments(self): """Returns attachments not yet attached to an agenda item""" return self.attachments.filter(agenda_item__isnull=True)
class Proposal(UIDMixin, ConfidentialMixin): objects = ProposalManager() issue = models.ForeignKey(Issue, related_name="proposals", on_delete=models.PROTECT) active = models.BooleanField(_("Active"), default=True) created_at = models.DateTimeField(_("Create at"), auto_now_add=True) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="proposals_created", on_delete=models.PROTECT, verbose_name=_("Created by")) type = models.PositiveIntegerField(_("Type"), choices=ProposalType.CHOICES, default=ProposalType.GENERAL) types = ProposalType title = models.CharField(_("Title"), max_length=800) content = HTMLField(_("Details"), null=True, blank=True) status = models.IntegerField(choices=ProposalStatus.choices, default=ProposalStatus.IN_DISCUSSION) statuses = ProposalStatus decided_at_meeting = models.ForeignKey('meetings.Meeting', null=True, blank=True, on_delete=models.PROTECT) assigned_to = models.CharField(_("Assigned to"), max_length=200, null=True, blank=True) assigned_to_user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_("Assigned to user"), null=True, blank=True, on_delete=models.PROTECT, related_name="proposals_assigned") due_by = models.DateField(_("Due by"), null=True, blank=True) task_completed = models.BooleanField(_("Completed"), default=False) votes_pro = models.PositiveIntegerField(_("Votes pro"), null=True, blank=True) votes_con = models.PositiveIntegerField(_("Votes con"), null=True, blank=True) community_members = models.PositiveIntegerField(_("Community members"), null=True, blank=True) tags = TaggableManager(_("Tags"), blank=True) register_board_votes = models.BooleanField(default=False) class Meta: verbose_name = _("Proposal") verbose_name_plural = _("Proposals") def __str__(self): return self.title @property def is_open(self): return self.decided_at_meeting is None @property def decided(self): return self.status != ProposalStatus.IN_DISCUSSION @property def can_vote(self): """ Returns True if the proposal, issue and meeting are open """ return self.is_open and self.issue.is_current @property def has_votes(self): """ Returns True if the proposal has any vote """ return self.votes_con or self.votes_pro @property def has_arguments(self): return ProposalVoteArgument.objects.filter(proposal_vote__proposal=self).exists() @property def can_straw_vote(self): return self.status == ProposalStatus.IN_DISCUSSION and self.issue.can_straw_vote and self.issue.voteable @property def can_show_straw_votes(self): return self.has_votes and \ (not self.issue.is_upcoming or \ not self.issue.community.upcoming_meeting_is_published or \ self.issue.community.straw_vote_ended) def get_comments(self): return self.proposal_comments.all().order_by('-created_at') def get_straw_results(self, meeting_id=None): """ get straw voting results registered for the given meeting """ if meeting_id: try: res = VoteResult.objects.get(proposal=self, meeting_id=meeting_id) except VoteResult.DoesNotExist: return None return res else: if self.issue.is_upcoming and self.issue.community.straw_vote_ended: return self else: try: res = VoteResult.objects.filter(proposal=self) \ .latest('meeting__held_at') return res except VoteResult.DoesNotExist: return None def board_vote_by_member(self, user_id): try: vote = ProposalVoteBoard.objects.get(user_id=user_id, proposal=self) return vote.value except ProposalVoteBoard.DoesNotExist: return None @property def board_vote_result(self): total_votes = 0 votes_dict = {'sums': {}, 'total': total_votes, 'per_user': {}} pro_count = 0 con_count = 0 neut_count = 0 users = self.issue.community.upcoming_meeting_participants.all() for u in users: vote = ProposalVoteBoard.objects.filter(proposal=self, user=u) if vote.exists(): votes_dict['per_user'][u] = vote[0].value if vote[0].value == 1: pro_count += 1 total_votes += 1 elif vote[0].value == -1: con_count += 1 total_votes += 1 elif vote[0].value == 0: neut_count += 1 else: votes_dict['per_user'][u] = 0 neut_count += 1 votes_dict['sums']['pro_count'] = pro_count votes_dict['sums']['con_count'] = con_count votes_dict['sums']['neut_count'] = neut_count votes_dict['total'] = total_votes return votes_dict def do_votes_summation(self, members_count): pro_votes = ProposalVote.objects.filter(proposal=self, value=ProposalVoteValue.PRO).count() con_votes = ProposalVote.objects.filter(proposal=self, value=ProposalVoteValue.CON).count() self.votes_pro = pro_votes self.votes_con = con_votes self.community_members = members_count self.save() def is_task(self): return self.type == ProposalType.TASK @models.permalink def get_absolute_url(self): return ("proposal", (str(self.issue.community.pk), str(self.issue.pk), str(self.pk))) @models.permalink def get_email_vote_url(self): return ("vote_on_proposal", (str(self.issue.community.pk), str(self.pk))) @models.permalink def get_edit_url(self): return ( "proposal_edit", (str(self.issue.community.pk), str(self.issue.pk), str(self.pk))) @models.permalink def get_edit_task_url(self): return ("proposal_edit_task", (str(self.issue.community.pk), str(self.issue.pk), str(self.pk))) @models.permalink def get_delete_url(self): return ( "proposal_delete", (str(self.issue.community.pk), str(self.issue.pk), str(self.pk))) def get_status_class(self): if self.status == self.statuses.ACCEPTED: return "accepted" if self.status == self.statuses.REJECTED: return "rejected" return "" def enforce_confidential_rules(self): # override `enforce_confidential_rules` on ConfidentialMixin # for the special logic required for Proposal objects if self.confidential_reason is None: if self.issue.is_confidential is True: self.is_confidential = True else: self.is_confidential = False else: self.is_confidential = True @property def arguments_for(self): return sorted( ProposalVoteArgument.objects.filter(proposal_vote__in=self.votes.filter(value=ProposalVoteValue.PRO)), key=lambda a: a.argument_score, reverse=True) @property def arguments_against(self): return sorted( ProposalVoteArgument.objects.filter(proposal_vote__in=self.votes.filter(value=ProposalVoteValue.CON)), key=lambda a: a.argument_score, reverse=True) @property def elegantly_interleaved_for_and_against_arguments(self): if not self.arguments_for: return list(self.arguments_against) if not self.arguments_against: return list(self.arguments_for) a = list(self.arguments_against) b = list(self.arguments_for) b, a = sorted((a, b), key=len) len_ab = len(a) + len(b) groups = groupby(((a[len(a) * i // len_ab], b[len(b) * i // len_ab]) for i in range(len_ab)), key=lambda x: x[0]) return [j[i] for k, g in groups for i, j in enumerate(g)]
class Reference(UIDMixin): issue = models.ForeignKey(Issue, related_name="references", on_delete=models.CASCADE) active = models.BooleanField(default=True) created_at = models.DateTimeField(_("Created at"), auto_now_add=True) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_("Created by"), on_delete=models.CASCADE, related_name="reference_created") meeting = models.ForeignKey('meetings.Meeting', null=True, blank=True, on_delete=models.CASCADE) version = models.PositiveIntegerField(default=1) last_edited_at = models.DateTimeField(_("Last Edited at"), auto_now_add=True) last_edited_by = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_("Created by"), on_delete=models.CASCADE, related_name="references_last_edited", null=True, blank=True) reference = HTMLField(_("Reference")) @property def is_confidential(self): return self.issue.is_confidential class Meta: ordering = ('created_at',) verbose_name = _("Reference") verbose_name_plural = _("References") @property def is_open(self): return self.meeting_id is None def update_reference(self, expected_version, author, reference): """ creates a new revision and updates current comment """ if self.version != expected_version: return False reference = enhance_html(reference.strip()) if self.reference == reference: return True with transaction.atomic(): ReferenceRevision.objects.create(reference=self, version=expected_version, created_at=self.created_at, created_by=self.created_by, content=self.reference) self.version += 1 self.last_edited_at = timezone.now() self.last_edited_by = author self.reference = reference self.save() return True @models.permalink def get_delete_url(self): return "delete_reference", (self.issue.community.id, self.id) @models.permalink def get_edit_url(self): return "edit_reference", (self.issue.community.id, self.id) @models.permalink def get_absolute_url(self): return "issue", (str(self.issue.community.pk), str(self.issue.pk),)
class Community(UIDMixin): name = models.CharField(max_length=200, verbose_name=_("Name")) is_public = models.BooleanField(_("Public community"), default=False, db_index=True) logo = models.ImageField(_("Community logo"), upload_to='community_logo', blank=True, null=True) official_identifier = models.CharField(_("Community identifier"), max_length=300, blank=True, null=True) upcoming_meeting_started = models.BooleanField(_("Meeting started"), default=False) upcoming_meeting_title = models.CharField(_("Upcoming meeting title"), max_length=300, null=True, blank=True) upcoming_meeting_scheduled_at = models.DateTimeField( _("Upcoming meeting scheduled at"), blank=True, null=True) upcoming_meeting_location = models.CharField( _("Upcoming meeting location"), max_length=300, null=True, blank=True) upcoming_meeting_comments = HTMLField(_("Upcoming meeting background"), null=True, blank=True) upcoming_meeting_participants = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, related_name="+", verbose_name=_("Participants in upcoming meeting")) upcoming_meeting_guests = models.TextField( _("Guests in upcoming meeting"), null=True, blank=True, help_text=_("Enter each guest in a separate line")) upcoming_meeting_version = models.IntegerField( _("Upcoming meeting version"), default=0) upcoming_meeting_is_published = models.BooleanField( _("Upcoming meeting is published"), default=False) upcoming_meeting_published_at = models.DateTimeField( _("Upcoming meeting published at"), blank=True, null=True) upcoming_meeting_summary = HTMLField(_("Upcoming meeting summary"), null=True, blank=True) board_name = models.CharField(_("Board name"), default=_("Board"), max_length=200) straw_voting_enabled = models.BooleanField(_("Straw voting enabled"), default=False) issue_ranking_enabled = models.BooleanField( _("Issue ranking votes enabled"), default=False) voting_ends_at = models.DateTimeField(_("Straw Vote ends at"), null=True, blank=True) referendum_started = models.BooleanField(_("Referendum started"), default=False) referendum_started_at = models.DateTimeField(_("Referendum started at"), null=True, blank=True) referendum_ends_at = models.DateTimeField(_("Referendum ends at"), null=True, blank=True) default_quorum = models.PositiveSmallIntegerField(_("Default quorum"), null=True, blank=True) allow_links_in_emails = models.BooleanField(_("Allow links inside emails"), default=True) email_invitees = models.BooleanField(_("Send mails to invitees"), default=False) register_missing_board_members = models.BooleanField( _("Register missing board members"), default=False) inform_system_manager = models.BooleanField(_('Inform System Manager'), default=False) no_meetings_community = models.BooleanField( _('Community without meetings?'), default=False) class Meta: verbose_name = _("Community") verbose_name_plural = _("Communities") def __unicode__(self): return self.name @models.permalink def get_absolute_url(self): return "community", (str(self.pk), ) @models.permalink def get_upcoming_absolute_url(self): return "community", (str(self.pk), ) def upcoming_issues(self, user=None, community=None, upcoming=True): l = issues_models.IssueStatus.IS_UPCOMING if upcoming else \ issues_models.IssueStatus.NOT_IS_UPCOMING if self.issues.all(): rv = self.issues.object_access_control( user=user, community=community).filter( active=True, status__in=(l)).order_by('order_in_upcoming_meeting') else: rv = None return rv def available_issues(self, user=None, community=None): if self.issues.all(): rv = self.issues.object_access_control( user=user, community=community).filter( active=True, status=issues_models.IssueStatus.OPEN).order_by( '-created_at') else: rv = None return rv def available_issues_by_rank(self): return self.issues.filter( active=True, status=issues_models.IssueStatus.OPEN).order_by('order_by_votes') def issues_ready_to_close(self, user=None, community=None): if self.upcoming_issues(user=user, community=community): rv = self.upcoming_issues(user=user, community=community).filter( proposals__active=True, proposals__decided_at_meeting=None, proposals__status__in=[ ProposalStatus.ACCEPTED, ProposalStatus.REJECTED ]) else: rv = None return rv def get_board_name(self): return self.board_name or _('Board') def get_members(self): return OCUser.objects.filter(memberships__community=self) def meeting_participants(self): meeting_participants = { 'chairmen': [], 'board': [], 'members': [], } board_ids = [m.user.id for m in self.memberships.board()] for u in self.upcoming_meeting_participants.all(): if u.id in board_ids: if u.get_default_group(self) == DefaultGroups.CHAIRMAN: meeting_participants['chairmen'].append(u) else: meeting_participants['board'].append(u) else: meeting_participants['members'].append(u) # doing it simply like this, as I'd need to refactor models # just to order in the way that is now required. for index, item in enumerate(meeting_participants['board']): if item.get_default_group(self) == DefaultGroups.MEMBER: meeting_participants['board'].insert( 0, meeting_participants['board'].pop(index)) return meeting_participants def previous_members_participations(self): participations = MeetingParticipant.objects.filter( \ default_group_name=DefaultGroups.MEMBER, meeting__community=self) \ .order_by('-meeting__held_at') return list(set([p.user for p in participations]) - \ set(self.upcoming_meeting_participants.all())) def previous_guests_participations(self): guests_list = Meeting.objects.filter(community=self) \ .values_list('guests', flat=True) prev_guests = set() upcoming_guests = self.upcoming_meeting_guests or ' ' for guest in guests_list: if guest: prev_guests.update(guest.splitlines()) prev_guests.difference_update(upcoming_guests.splitlines()) return prev_guests def get_board_members(self): board_memberships = Membership.objects.filter(community=self) \ .exclude(default_group_name=DefaultGroups.MEMBER) # doing it simply like this, as I'd need to refactor models # just to order in the way that is now required. board = [m.user for m in board_memberships] for index, item in enumerate(board): if item.get_default_group(self) == DefaultGroups.MEMBER: board.insert(0, board.pop(index)) return board def get_board_count(self): return len(self.get_board_members()) def get_none_board_members(self): return Membership.objects.filter( community=self, default_group_name=DefaultGroups.MEMBER) def get_guest_list(self): if not self.upcoming_meeting_guests: return [] return filter( None, [s.strip() for s in self.upcoming_meeting_guests.splitlines()]) def full_participants(self): guests_count = len(self.upcoming_meeting_guests.splitlines()) \ if self.upcoming_meeting_guests else 0 return guests_count + self.upcoming_meeting_participants.count() @property def straw_vote_ended(self): if not self.upcoming_meeting_is_published: return True if not self.voting_ends_at: return False time_till_close = self.voting_ends_at - timezone.now() return time_till_close.total_seconds() < 0 def has_straw_votes(self, user=None, community=None): if not self.straw_voting_enabled or self.straw_vote_ended: return False return self.upcoming_proposals_any({'is_open': True}, user=user, community=community) def sum_vote_results(self, only_when_over=True): if not self.voting_ends_at: return time_till_close = self.voting_ends_at - timezone.now() if only_when_over and time_till_close.total_seconds() > 0: return proposals_to_sum = issues_models.Proposal.objects.filter( # votes_pro=None, status=ProposalStatus.IN_DISCUSSION, issue__status=IssueStatus.IN_UPCOMING_MEETING, issue__community_id=self.id) member_count = self.get_members().count() for prop in proposals_to_sum: prop.do_votes_summation(member_count) def _get_upcoming_proposals(self, user=None, community=None): proposals = [] upcoming = self.upcoming_issues(user=user, community=community) if upcoming: for issue in upcoming: if issue.proposals.all(): proposals.extend( [p for p in issue.proposals.all() if p.active]) return proposals def upcoming_proposals_any(self, prop_dict, user=None, community=None): """ test multiple properties against proposals belonging to the upcoming meeting return True if any of the proposals passes the tests """ proposals = self._get_upcoming_proposals(user=user, community=community) test_attrs = lambda p: [ getattr(p, k) == val for k, val in prop_dict.items() ] for p in proposals: if all(test_attrs(p)): return True return False def _register_absents(self, meeting, meeting_participants): board_members = [mm.user for mm in Membership.objects.board() \ .filter(community=self, user__is_active=True)] absents = set(board_members) - set(meeting_participants) ordinal_base = len(meeting_participants) for i, a in enumerate(absents): try: mm = a.memberships.get(community=self) except Membership.DoesNotExist: mm = None MeetingParticipant.objects.create( meeting=meeting, user=a, display_name=a.display_name, ordinal=ordinal_base + i, is_absent=True, default_group_name=mm.default_group_name if mm else None) def close_meeting(self, m, user, community): """ Creates a :model:`meetings.Meeting` instance, with corresponding :model:`meetings.AgenddItem`s. Optionally changes statuses for :model:`issues.Issue`s and :model:`issues.Proposal`s. """ with transaction.commit_on_success(): m.community = self m.created_by = user m.title = self.upcoming_meeting_title m.scheduled_at = (self.upcoming_meeting_scheduled_at or timezone.now()) m.location = self.upcoming_meeting_location m.comments = self.upcoming_meeting_comments m.guests = self.upcoming_meeting_guests m.summary = self.upcoming_meeting_summary m.save() self.upcoming_meeting_started = False self.upcoming_meeting_title = None self.upcoming_meeting_scheduled_at = None self.upcoming_meeting_location = None self.upcoming_meeting_comments = None self.upcoming_meeting_summary = None self.upcoming_meeting_version = 0 self.upcoming_meeting_is_published = False self.upcoming_meeting_published_at = None self.upcoming_meeting_guests = None self.voting_ends_at = None self.save() for i, issue in enumerate( self.upcoming_issues(user=user, community=community)): proposals = issue.proposals.filter( active=True, decided_at_meeting=None).exclude( status=ProposalStatus.IN_DISCUSSION) for p in proposals: p.decided_at_meeting = m p.save() for p in issue.proposals.all(): if p.votes_pro is not None: try: VoteResult.objects.create( proposal=p, meeting=m, votes_pro=p.votes_pro, votes_con=p.votes_con, community_members=p.community_members) except: pass for c in issue.comments.filter(meeting=None): c.meeting = m c.save() ai = meetings_models.AgendaItem.objects.create( meeting=m, issue=issue, order=i, background=issue.abstract, closed=issue.completed) issue.attachments.filter(active=True, agenda_item__isnull=True) \ .update(agenda_item=ai) issue.is_published = True issue.abstract = None if issue.completed: issue.order_in_upcoming_meeting = None issue.save() meeting_participants = self.upcoming_meeting_participants.all() for i, p in enumerate(meeting_participants): try: mm = p.memberships.get(community=self) except Membership.DoesNotExist: mm = None MeetingParticipant.objects.create( meeting=m, ordinal=i, user=p, display_name=p.display_name, default_group_name=mm.default_group_name if mm else None) self._register_absents(m, meeting_participants) self.upcoming_meeting_participants = [] return m def draft_meeting(self): if self.upcoming_meeting_scheduled_at: held_at = self.upcoming_meeting_scheduled_at.date() else: held_at = None return { 'id': '', 'held_at': held_at, } def draft_agenda(self, payload): """ prepares a fake agenda item list for 'protocol_draft' template. """ # payload should be a list of dicts. Each dict has these keys: # * issue # * proposals # # The values are querysets def as_agenda_item(obj): return { 'issue': obj['issue'], 'proposals': obj['proposals'].filter( decided_at_meeting=None, active=True).exclude(status=ProposalStatus.IN_DISCUSSION), 'accepted_proposals': obj['proposals'].filter(decided_at_meeting=None, active=True, status=ProposalStatus.ACCEPTED), 'rejected_proposals': obj['proposals'].filter(decided_at_meeting=None, active=True, status=ProposalStatus.REJECTED), 'comments': obj['issue'].comments.filter(meeting=None, active=True), 'attachments': obj['issue'].current_attachments() } return [as_agenda_item(x) for x in payload]
class Community(UIDMixin): name = models.CharField(max_length=200, verbose_name=_("Name")) is_public = models.BooleanField(_("Public Community"), default=False, db_index=True) logo = models.ImageField(upload_to='community_logo', verbose_name=_("Community Logo"), blank=True, null=True) official_identifier = models.CharField( max_length=300, verbose_name=_("Community Identifier"), blank=True, null=True) upcoming_meeting_started = models.BooleanField(_("Meeting started"), default=False) upcoming_meeting_title = models.CharField(_("Upcoming meeting title"), max_length=300, null=True, blank=True) upcoming_meeting_scheduled_at = models.DateTimeField( _("Upcoming meeting scheduled at"), blank=True, null=True) upcoming_meeting_location = models.CharField( _("Upcoming meeting location"), max_length=300, null=True, blank=True) upcoming_meeting_comments = HTMLField(_("Upcoming meeting background"), null=True, blank=True) upcoming_meeting_participants = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, related_name="+", verbose_name=_("Participants in upcoming meeting")) upcoming_meeting_guests = models.TextField( _("Guests in upcoming meeting"), null=True, blank=True, help_text=_("Enter each guest in a separate line")) upcoming_meeting_version = models.IntegerField( _("Upcoming meeting version"), default=0) upcoming_meeting_is_published = models.BooleanField( _("Upcoming meeting is published"), default=False) upcoming_meeting_published_at = models.DateTimeField( _("Upcoming meeting published at"), blank=True, null=True) upcoming_meeting_summary = HTMLField(_("Upcoming meeting summary"), null=True, blank=True) board_name = models.CharField(_("Board Name"), max_length=200, null=True, blank=True) class Meta: verbose_name = _("Community") verbose_name_plural = _("Communities") def __unicode__(self): return self.name @models.permalink def get_absolute_url(self): return "community", (str(self.pk), ) @models.permalink def get_upcoming_absolute_url(self): return "community", (str(self.pk), ) def upcoming_issues(self, upcoming=True): l = issues_models.IssueStatus.IS_UPCOMING if upcoming else \ issues_models.IssueStatus.NOT_IS_UPCOMING return self.issues.filter( active=True, status__in=(l)).order_by('order_in_upcoming_meeting') def available_issues(self): return self.issues.filter( active=True, status=issues_models.IssueStatus.OPEN).order_by('created_at') def issues_ready_to_close(self): return self.upcoming_issues().filter( proposals__active=True, proposals__decided_at_meeting=None, proposals__status__in=[ ProposalStatus.ACCEPTED, ProposalStatus.REJECTED ]) def get_board_name(self): return self.board_name or _('Board') def get_members(self): return OCUser.objects.filter(memberships__community=self) def get_guest_list(self): if not self.upcoming_meeting_guests: return [] return filter( None, [s.strip() for s in self.upcoming_meeting_guests.splitlines()]) def send_mail(self, template, sender, send_to, data=None, base_url=None): if not base_url: base_url = settings.HOST_URL d = data.copy() if data else {} d.update({ 'base_url': base_url, 'community': self, 'LANGUAGE_CODE': settings.LANGUAGE_CODE, 'MEDIA_URL': settings.MEDIA_URL, 'STATIC_URL': settings.STATIC_URL, }) subject = render_to_string("emails/%s_title.txt" % template, d).strip() message = render_to_string("emails/%s.txt" % template, d) html_message = render_to_string("emails/%s.html" % template, d) from_email = "%s <%s>" % (self.name, settings.FROM_EMAIL) recipient_list = set([sender.email]) if send_to == SendToOption.ALL_MEMBERS: recipient_list.update( list(self.memberships.values_list('user__email', flat=True))) elif send_to == SendToOption.BOARD_ONLY: recipient_list.update( list(self.memberships.board().values_list('user__email', flat=True))) elif send_to == SendToOption.ONLY_ATTENDEES: recipient_list.update( list( self.upcoming_meeting_participants.values_list('email', flat=True))) logger.info("Sending agenda to %d users" % len(recipient_list)) send_mails(from_email, recipient_list, subject, message, html_message) return len(recipient_list) def close_meeting(self, m, user): with transaction.commit_on_success(): m.community = self m.created_by = user m.title = self.upcoming_meeting_title m.scheduled_at = (self.upcoming_meeting_scheduled_at or timezone.now()) m.location = self.upcoming_meeting_location m.comments = self.upcoming_meeting_comments m.guests = self.upcoming_meeting_guests m.summary = self.upcoming_meeting_summary m.save() self.upcoming_meeting_started = False self.upcoming_meeting_title = None self.upcoming_meeting_scheduled_at = None self.upcoming_meeting_location = None self.upcoming_meeting_comments = None self.upcoming_meeting_summary = None self.upcoming_meeting_version = 0 self.upcoming_meeting_is_published = False self.upcoming_meeting_published_at = None self.upcoming_meeting_guests = None self.save() for i, issue in enumerate(self.upcoming_issues()): proposals = issue.proposals.filter( active=True, decided_at_meeting=None).exclude( status=ProposalStatus.IN_DISCUSSION) for p in proposals: p.decided_at_meeting = m p.save() for c in issue.comments.filter(active=True, meeting=None): c.meeting = m c.save() meetings_models.AgendaItem.objects.create( meeting=m, issue=issue, order=i, closed=issue.completed) issue.is_published = True if issue.completed: issue.status = issue.statuses.ARCHIVED issue.order_in_upcoming_meeting = None issue.save() for i, p in enumerate(self.upcoming_meeting_participants.all()): try: mm = p.memberships.get(community=self) except Membership.DoesNotExist: mm = None MeetingParticipant.objects.create( meeting=m, ordinal=i, user=p, display_name=p.display_name, default_group_name=mm.default_group_name if mm else None) self.upcoming_meeting_participants = [] return m def draft_agenda(self): """ prepares a fake agenda item list for 'protocol_draft' template. """ def as_agenda_item(issue): return { 'issue': issue, 'proposals': issue.proposals.filter( decided_at_meeting=None, active=True).exclude(status=ProposalStatus.IN_DISCUSSION), 'accepted_proposals': issue.proposals.filter(decided_at_meeting=None, active=True, status=ProposalStatus.ACCEPTED), 'rejected_proposals': issue.proposals.filter(decided_at_meeting=None, active=True, status=ProposalStatus.REJECTED), 'comments': issue.comments.filter(meeting=None, active=True), } return [ as_agenda_item(x) for x in self.issues.filter(status__in=IssueStatus.IS_UPCOMING) ]
class Community(UIDMixin): name = models.CharField(max_length=200, verbose_name=_("Name")) is_public = models.BooleanField(_("Public Community"), default=False, db_index=True) logo = models.ImageField(upload_to='community_logo', verbose_name=_("Community Logo"), blank=True, null=True) official_identifier = models.CharField(max_length=300, verbose_name=_("Community Identifier"), blank=True, null=True) upcoming_meeting_started = models.BooleanField( _("Meeting started"), default=False) upcoming_meeting_title = models.CharField( _("Upcoming meeting title"), max_length=300, null=True, blank=True) upcoming_meeting_scheduled_at = models.DateTimeField( _("Upcoming meeting scheduled at"), blank=True, null=True) upcoming_meeting_location = models.CharField( _("Upcoming meeting location"), max_length=300, null=True, blank=True) upcoming_meeting_comments = HTMLField(_("Upcoming meeting background"), null=True, blank=True) upcoming_meeting_participants = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, related_name="+", verbose_name=_( "Participants in upcoming meeting")) upcoming_meeting_guests = models.TextField(_("Guests in upcoming meeting"), null=True, blank=True, help_text=_("Enter each guest in a separate line")) upcoming_meeting_version = models.IntegerField( _("Upcoming meeting version"), default=0) upcoming_meeting_is_published = models.BooleanField( _("Upcoming meeting is published"), default=False) upcoming_meeting_published_at = models.DateTimeField( _("Upcoming meeting published at"), blank=True, null=True) upcoming_meeting_summary = HTMLField(_("Upcoming meeting summary"), null=True, blank=True) board_name = models.CharField(_("Board Name"), max_length=200, null=True, blank=True) class Meta: verbose_name = _("Community") verbose_name_plural = _("Communities") def __unicode__(self): return self.name @models.permalink def get_absolute_url(self): return "community", (str(self.pk),) @models.permalink def get_upcoming_absolute_url(self): return "community", (str(self.pk),) def upcoming_issues(self, upcoming=True): return self.issues.filter(active=True, is_closed=False, in_upcoming_meeting=upcoming).order_by('order_in_upcoming_meeting') def available_issues(self): return self.upcoming_issues(False) def issues_ready_to_close(self): return self.upcoming_issues().filter( proposals__is_accepted=True ).annotate( num_proposals=models.Count('proposals') ) def get_board_name(self): return self.board_name or _('Board') def get_members(self): return OCUser.objects.filter(memberships__community=self) def get_guest_list(self): if not self.upcoming_meeting_guests: return [] return filter(None, [s.strip() for s in self.upcoming_meeting_guests.splitlines()]) def send_mail(self, template, sender, send_to, data=None, base_url=None): if not base_url: base_url = settings.HOST_URL d = data.copy() if data else {} d.update({ 'base_url': base_url, 'community': self, 'LANGUAGE_CODE': settings.LANGUAGE_CODE, }) subject = render_to_string("emails/%s_title.txt" % template, d) message = render_to_string("emails/%s.txt" % template, d) html_message = render_to_string("emails/%s.html" % template, d) from_email = "%s <%s>" % (self.name, settings.FROM_EMAIL) recipient_list = set([sender.email]) if send_to == SendToOption.ALL_MEMBERS: recipient_list.update(list( self.memberships.values_list('user__email', flat=True))) elif send_to == SendToOption.BOARD_ONLY: recipient_list.update(list( self.memberships.board().values_list('user__email', flat=True))) elif send_to == SendToOption.ONLY_ATTENDEES: recipient_list.update(list( self.upcoming_meeting_participants.values_list( 'email', flat=True))) logger.info("Sending agenda to %d users" % len(recipient_list)) send_mails(from_email, recipient_list, subject, message, html_message) return len(recipient_list)
class Issue(UIDMixin): active = models.BooleanField(default=True, verbose_name=_("Active")) community = models.ForeignKey(Community, verbose_name=_("Community"), related_name="issues") created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at")) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_("Create by"), related_name="issues_created") title = models.CharField(max_length=300, verbose_name=_("Title")) abstract = HTMLField(null=True, blank=True, verbose_name=_("Background")) content = HTMLField(null=True, blank=True, verbose_name=_("Content")) calculated_score = models.IntegerField(default=0, verbose_name=_("Calculated Score")) in_upcoming_meeting = models.BooleanField(_("In upcoming meeting"), default=False) order_in_upcoming_meeting = models.IntegerField( _("Order in upcoming meeting"), default=9999, null=True, blank=True) length_in_minutes = models.IntegerField(_("Length (in minutes)"), null=True, blank=True) completed = models.BooleanField(default=False, verbose_name=_("Discussion completed")) is_closed = models.BooleanField(default=False, verbose_name=_("Is close")) closed_at_meeting = models.ForeignKey('meetings.Meeting', null=True, blank=True, verbose_name=_("Closed at meeting")) class Meta: verbose_name = _("Issue") verbose_name_plural = _("Issues") def __unicode__(self): return self.title @models.permalink def get_edit_url(self): return ("issue_edit", ( str(self.community.pk), str(self.pk), )) @models.permalink def get_delete_url(self): return ("issue_delete", ( str(self.community.pk), str(self.pk), )) @models.permalink def get_absolute_url(self): return ("issue", ( str(self.community.pk), str(self.pk), )) def accepted_proposals(self): return self.proposals.filter(is_accepted=True, active=True) def active_proposals(self): return self.proposals.filter(active=True) def active_comments(self): return self.comments.filter(active=True)
class Proposal(UIDMixin): issue = models.ForeignKey(Issue, related_name="proposals", verbose_name=_("Issue")) active = models.BooleanField(default=True, verbose_name=_("Active")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Create at")) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="proposals_created", verbose_name=_("Created by")) type = models.PositiveIntegerField(choices=ProposalType.CHOICES, verbose_name=_("Type")) title = models.CharField(max_length=300, verbose_name=_("Title")) content = HTMLField(null=True, blank=True, verbose_name=_("Content")) is_accepted = models.BooleanField(_("Is accepted"), default=False) accepted_at = models.DateTimeField(_("Accepted at"), null=True, blank=True) assigned_to = models.CharField(_("Assigned to"), max_length=200, null=True, blank=True) assigned_to_user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_("Assigned to user"), null=True, blank=True, related_name="proposals_assigned") due_by = models.DateField(null=True, blank=True, verbose_name=_("Due by")) votes = models.ManyToManyField(settings.AUTH_USER_MODEL, verbose_name=_("Votes"), blank=True, related_name="proposals", through="ProposalVote") objects = ProposalManager() class Meta: verbose_name = _("Proposal") verbose_name_plural = _("Proposals") def __unicode__(self): return self.title def is_task(self): return self.type == ProposalType.TASK @models.permalink def get_absolute_url(self): return ("proposal", (str(self.issue.community.pk), str(self.issue.pk), str(self.pk))) @models.permalink def get_edit_url(self): return ("proposal_edit", (str(self.issue.community.pk), str(self.issue.pk), str(self.pk))) @models.permalink def get_edit_task_url(self): return ("proposal_edit_task", (str(self.issue.community.pk), str(self.issue.pk), str(self.pk))) @models.permalink def get_delete_url(self): return ("proposal_delete", (str(self.issue.community.pk), str(self.issue.pk), str(self.pk)))
class Proposal(UIDMixin): issue = models.ForeignKey(Issue, related_name="proposals", verbose_name=_("Issue")) active = models.BooleanField(_("Active"), default=True) created_at = models.DateTimeField(_("Create at"), auto_now_add=True) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="proposals_created", verbose_name=_("Created by")) type = models.PositiveIntegerField(_("Type"), choices=ProposalType.CHOICES) types = ProposalType title = models.CharField(_("Title"), max_length=300) content = HTMLField(_("Content"), null=True, blank=True) status = models.IntegerField(choices=ProposalStatus.choices, default=ProposalStatus.IN_DISCUSSION) statuses = ProposalStatus decided_at_meeting = models.ForeignKey('meetings.Meeting', null=True, blank=True) assigned_to = models.CharField(_("Assigned to"), max_length=200, null=True, blank=True) assigned_to_user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_("Assigned to user"), null=True, blank=True, related_name="proposals_assigned") due_by = models.DateField(_("Due by"), null=True, blank=True) votes = models.ManyToManyField(settings.AUTH_USER_MODEL, verbose_name=_("Votes"), blank=True, related_name="proposals", through="ProposalVote") objects = ProposalManager() class Meta: verbose_name = _("Proposal") verbose_name_plural = _("Proposals") def __unicode__(self): return self.title @property def is_open(self): return self.decided_at_meeting is None @property def can_vote(self): """ Returns True if the proposal, issue and meeting are open """ print "xyz", self.id, self.is_open, self.issue.is_upcoming, \ self.issue.community.upcoming_meeting_started return self.is_open and self.issue.is_upcoming and \ self.issue.community.upcoming_meeting_started def is_task(self): return self.type == ProposalType.TASK @models.permalink def get_absolute_url(self): return ("proposal", (str(self.issue.community.pk), str(self.issue.pk), str(self.pk))) @models.permalink def get_edit_url(self): return ("proposal_edit", (str(self.issue.community.pk), str(self.issue.pk), str(self.pk))) @models.permalink def get_edit_task_url(self): return ("proposal_edit_task", (str(self.issue.community.pk), str(self.issue.pk), str(self.pk))) @models.permalink def get_delete_url(self): return ("proposal_delete", (str(self.issue.community.pk), str(self.issue.pk), str(self.pk))) def get_status_class(self): if self.status == self.statuses.ACCEPTED: return "accepted" if self.status == self.statuses.REJECTED: return "rejected" return "" return ""
class Issue(UIDMixin): active = models.BooleanField(default=True, verbose_name=_("Active")) community = models.ForeignKey('communities.Community', verbose_name=_("Community"), related_name="issues") created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at")) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_("Create by"), related_name="issues_created") title = models.CharField(max_length=300, verbose_name=_("Title")) abstract = HTMLField(null=True, blank=True, verbose_name=_("Background")) content = HTMLField(null=True, blank=True, verbose_name=_("Content")) calculated_score = models.IntegerField(default=0, verbose_name=_("Calculated Score")) status = models.IntegerField(choices=IssueStatus.choices, default=IssueStatus.OPEN) statuses = IssueStatus order_in_upcoming_meeting = models.IntegerField( _("Order in upcoming meeting"), default=9999, null=True, blank=True) length_in_minutes = models.IntegerField(_("Length (in minutes)"), null=True, blank=True) completed = models.BooleanField(default=False, verbose_name=_("Discussion completed")) is_published = models.BooleanField(_("Is published to members"), default=False) class Meta: verbose_name = _("Issue") verbose_name_plural = _("Issues") ordering = ['order_in_upcoming_meeting', 'title'] def __unicode__(self): return self.title @models.permalink def get_edit_url(self): return ("issue_edit", ( str(self.community.pk), str(self.pk), )) @models.permalink def get_delete_url(self): return ("issue_delete", ( str(self.community.pk), str(self.pk), )) @models.permalink def get_absolute_url(self): return ("issue", ( str(self.community.pk), str(self.pk), )) def active_proposals(self): return self.proposals.filter(active=True) def new_comments(self): return self.comments.filter(active=True, meeting_id=None) def has_closed_parts(self): """ Should be able to be viewed """ @property def is_upcoming(self): return self.status in IssueStatus.IS_UPCOMING @property def is_current(self): return self.status in IssueStatus.IS_UPCOMING and self.community.upcoming_meeting_started def changed_in_current(self): decided_at_current = self.proposals.filter( active=True, decided_at_meeting=None, status__in=[ProposalStatus.ACCEPTED, ProposalStatus.REJECTED]) return decided_at_current or self.new_comments() @property def is_archived(self): return self.status == IssueStatus.ARCHIVED