Exemple #1
0
def view_notices_statistics_xlsx(self, request):
    """ View the statistics as XLSX. """

    output = BytesIO()
    workbook = Workbook(output)
    for title, row, content in (
        (_("Organizations"), _("Organization"), self.count_by_organization),
        (_("Categories"), _("Category"), self.count_by_category),
        (_("Groups"), _("Group"), self.count_by_group),
        (_("Rejected"), _("Name"), self.count_rejected),
    ):
        worksheet = workbook.add_worksheet()
        worksheet.name = request.translate(title)
        worksheet.write_row(0, 0, (
            request.translate(row),
            request.translate(_("Count"))
        ))
        for index, row in enumerate(content()):
            worksheet.write_row(index + 1, 0, row)
    workbook.close()
    output.seek(0)

    response = Response()
    response.content_type = (
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    )
    response.content_disposition = 'inline; filename={}-{}-{}.xlsx'.format(
        request.translate(_("Statistics")).lower(),
        normalize_for_url(request.translate(TRANSLATIONS.get(self.state, ''))),
        datetime.utcnow().strftime('%Y%m%d%H%M')
    )
    response.body = output.read()

    return response
Exemple #2
0
class CategoryForm(Form):

    title = StringField(label=_("Title"), validators=[InputRequired()])

    active = BooleanField(label=_("Active"), default=True)

    name = StringField(
        label=_("ID"),
        description=_("Leave blank to set the value automatically."),
        validators=[
            UniqueColumnValue(Category),
            UnusedColumnKeyValue(GazetteNotice._categories)
        ])

    def update_model(self, model):
        model.title = self.title.data
        model.active = self.active.data
        if self.name.data:
            model.name = self.name.data

    def apply_model(self, model):
        self.title.data = model.title
        self.active.data = model.active
        self.name.data = model.name
        self.name.default = model.name
        if model.in_use:
            self.name.render_kw = {'readonly': True}
Exemple #3
0
    def format_issue(self, issue, date_format='date', notice=None):
        """ Returns the issues number and date and optionally the publication
        number of the given notice. """

        assert isinstance(issue, Issue)

        issue_number = issue.number or ''
        issue_date = self.format_date(issue.date, date_format)
        notice_number = notice.issues.get(issue.name, None) if notice else None

        if notice_number:
            return self.request.translate(
                _("No. ${issue_number}, ${issue_date} / ${notice_number}",
                  mapping={
                      'issue_number': issue_number,
                      'issue_date': issue_date,
                      'notice_number': notice_number
                  }))
        else:
            return self.request.translate(
                _("No. ${issue_number}, ${issue_date}",
                  mapping={
                      'issue_number': issue_number,
                      'issue_date': issue_date
                  }))
Exemple #4
0
def upload_attachment(self, request):
    """ Upload an attachment and add it to the notice.

    Raises a HTTP 405 (Metho not Allowed) for non-admins if the notice has
    already been accepted.

    Raises a HTTP 415 (Unsupported Media Type) if the file format is not
    supported.

    """

    if self.state == 'accepted' or self.state == 'published':
        if not request.is_secret(self):
            raise exc.HTTPMethodNotAllowed()

    request.assert_valid_csrf_token()

    attachment = GazetteNoticeFile(id=random_token())
    attachment.name = request.params['file'].filename
    attachment.reference = as_fileintent(request.params['file'].file,
                                         request.params['file'].filename)

    if attachment.reference.content_type != 'application/pdf':
        raise exc.HTTPUnsupportedMediaType()

    self.files.append(attachment)
    self.add_change(request, _("Attachment added."))

    request.message(_("Attachment added."), 'success')
    return redirect(request.link(self, 'attachments'))
Exemple #5
0
def edit_user(self, request, form):
    """ Edit the role, name and email of a user.

    Publishers may only edit members. Admins can not be edited.

    """

    layout = Layout(self, request)

    if self.role != 'member' and not request.is_secret(self):
        raise HTTPForbidden()

    if form.submitted(request):
        form.update_model(self)
        self.logout_all_sessions(request)
        request.message(_("User modified."), 'success')
        return redirect(layout.manage_users_link)

    if not form.errors:
        form.apply_model(self)

    return {
        'layout': layout,
        'form': form,
        'title': self.title,
        'subtitle': _("Edit User"),
        'cancel': layout.manage_users_link
    }
Exemple #6
0
def handle_password_reset(self, request, form):

    layout = Layout(self, request)
    callout = None
    show_form = True
    if form.submitted(request):
        if form.update_password(request):
            show_form = False
            request.message(_("Password changed."), 'success')
            return redirect(layout.homepage_link)
        else:
            form.error_message = _(
                "Wrong username or password reset link not valid any more."
            )
            log.info(
                "Failed password reset attempt by {}".format(
                    request.client_addr
                )
            )

    if 'token' in request.params:
        form.token.data = request.params['token']

    return {
        'layout': layout,
        'title': _('Reset password'),
        'form': form,
        'show_form': show_form,
        'callout': callout
    }
Exemple #7
0
def view_notices_update(self, request, form):
    """ Updates all notices (of this state): Applies the categories, issues and
    organization from the meta informations. This view is not used normally
    and only intended when changing category names in the principal definition,
    for example.

    """

    layout = Layout(self, request)
    session = request.session

    if form.submitted(request):
        for notice in self.query():
            notice.apply_meta(session)
        request.message(_("Notices updated."), 'success')

        return redirect(layout.dashboard_or_notices_link)

    return {
        'layout': layout,
        'form': form,
        'title': _("Update notices"),
        'button_text': _("Update"),
        'cancel': layout.dashboard_or_notices_link
    }
Exemple #8
0
def create_notice(self, request, form):
    """ Create a new notice.

    If a valid UID of a notice is given (via 'source' query parameter), its
    values are pre-filled in the form.

    This view is mainly used by the editors.

    """

    layout = Layout(self, request)
    user = get_user(request)

    source = None
    if self.source:
        source = self.query().filter(GazetteNotice.id == self.source)
        source = source.first()

    if form.submitted(request):
        notice = self.add(
            title=form.title.data,
            text=form.text.data,
            author_place=form.author_place.data,
            author_date=form.author_date_utc,
            author_name=form.author_name.data,
            organization_id=form.organization.data,
            category_id=form.category.data,
            print_only=form.print_only.data if form.print_only else False,
            at_cost=form.at_cost.data == 'yes',
            billing_address=form.billing_address.data,
            user=get_user(request),
            issues=form.issues.data
        )
        if form.phone_number.data:
            user.phone_number = form.phone_number.data
        if source:
            notice.note = source.note
        return redirect(request.link(notice))

    if not form.errors:
        if source:
            form.apply_model(source)
            if form.print_only:
                form.print_only.data = False

        form.phone_number.data = user.phone_number

    return {
        'layout': layout,
        'form': form,
        'title': _("New Official Notice"),
        'helptext': _(
            "The fields marked with an asterisk * are mandatory fields."
        ),
        'button_text': _("Save"),
        'cancel': layout.dashboard_or_notices_link,
        'current_issue': layout.current_issue
    }
Exemple #9
0
    def import_editors_and_groups(request, app):
        request.locale = locale
        headers = {
            'group': request.translate(_("Group")),
            'name': request.translate(_("Name")),
            'email': request.translate(_("E-Mail"))
        }

        session = app.session()
        users = UserCollection(session)
        groups = UserGroupCollection(session)

        if clear:
            click.secho("Deleting all editors", fg='yellow')
            for user in users.query().filter(User.role == 'member'):
                session.delete(user)

            click.secho("Deleting all groups", fg='yellow')
            for group in groups.query():
                session.delete(group)

        csvfile = convert_xls_to_csv(
            file, sheet_name=request.translate(_("Editors"))
        )
        csv = CSVFile(csvfile, expected_headers=headers.values())
        lines = list(csv.lines)
        columns = {
            key: csv.as_valid_identifier(value)
            for key, value in headers.items()
        }

        added_groups = {}
        for group in set([line.gruppe for line in lines]):
            added_groups[group] = groups.add(name=group)
        count = len(added_groups)
        click.secho(f"{count} group(s) imported", fg='green')

        count = 0
        for line in lines:
            count += 1
            email = getattr(line, columns['email'])
            realname = getattr(line, columns['name'])
            group = getattr(line, columns['group'])
            group = added_groups[group] if group else None
            users.add(
                username=email,
                realname=realname,
                group=group,
                password=random_password(),
                role='member',
            )

        click.secho(f"{count} editor(s) imported", fg='green')

        if dry_run:
            transaction.abort()
            click.secho("Aborting transaction", fg='yellow')
Exemple #10
0
def handle_password_reset_request(self, request, form):
    """ Handles the password reset requests. """

    show_form = True
    callout = None

    if form.submitted(request):
        users = UserCollection(request.session)
        user = users.by_username(form.email.data)
        if user:
            url = password_reset_url(
                user,
                request,
                request.link(self, name='reset-password')
            )

            request.app.send_transactional_email(
                subject=request.translate(_("Password reset")),
                receivers=(user.username, ),
                reply_to=request.app.mail['transactional']['sender'],
                content=render_template(
                    'mail_password_reset.pt',
                    request,
                    {
                        'title': request.translate(_("Password reset")),
                        'model': None,
                        'url': url,
                        'layout': MailLayout(self, request)
                    }
                )
            )
        else:
            log.info(
                "Failed password reset attempt by {}".format(
                    request.client_addr
                )
            )

        show_form = False
        callout = _(
            (
                'A password reset link has been sent to ${email}, provided an '
                'account exists for this email address.'
            ),
            mapping={'email': form.email.data}
        )

    return {
        'layout': Layout(self, request),
        'title': _('Reset password'),
        'form': form,
        'show_form': show_form,
        'callout': callout
    }
Exemple #11
0
def handle_notfound(self, request):

    @request.after
    def set_status_code(response):
        response.status_code = self.code  # pass along 404

    return {
        'layout': Layout(self, request),
        'title': _("Page not Found"),
        'message': _("The page you are looking for could not be found."),
    }
Exemple #12
0
def view_dashboard(self, request):
    """ The dashboard view (for editors).

    Shows the drafted, submitted and rejected notices, shows warnings and
    allows to create a new notice.

    """
    layout = Layout(self, request)

    user_ids, group_ids = get_user_and_group(request)
    collection = GazetteNoticeCollection(request.session,
                                         user_ids=user_ids,
                                         group_ids=group_ids)

    # rejected
    rejected = collection.for_state('rejected').query().all()
    if rejected:
        request.message(_("You have rejected messages."), 'warning')

    # drafted
    drafted = collection.for_state('drafted').query().all()
    now = utcnow()
    limit = now + timedelta(days=2)
    past_issues_selected = False
    deadline_reached_soon = False
    for notice in drafted:
        for issue in notice.issue_objects:
            if issue.deadline < now:
                past_issues_selected = True
            elif issue.deadline < limit:
                deadline_reached_soon = True
    if past_issues_selected:
        request.message(_("You have drafted messages with past issues."),
                        'warning')
    if deadline_reached_soon:
        request.message(
            _("You have drafted messages with issues close to the deadline."),
            'warning')

    # submitted
    submitted = collection.for_state('submitted').query().all()

    new_notice = request.link(collection.for_state('drafted'),
                              name='new-notice')

    return {
        'layout': layout,
        'title': _("Dashboard"),
        'rejected': rejected,
        'drafted': drafted,
        'submitted': submitted,
        'new_notice': new_notice,
        'current_issue': layout.current_issue
    }
Exemple #13
0
    def _import_issues(request, app):
        if not app.principal:
            return

        request.locale = locale
        headers = {
            'number': request.translate(_("Number")),
            'date': request.translate(_("Date")),
            'deadline': request.translate(_("Deadline"))
        }

        session = app.session()
        issues = IssueCollection(session)

        if clear:
            click.secho("Deleting issues", fg='yellow')
            for category in issues.query():
                session.delete(category)

        csvfile = convert_xls_to_csv(
            file, sheet_name=request.translate(_("Issues"))
        )
        csv = CSVFile(csvfile, expected_headers=headers.values())
        lines = list(csv.lines)
        columns = {
            key: csv.as_valid_identifier(value)
            for key, value in headers.items()
        }

        count = 0
        for line in lines:
            count += 1
            number = int(getattr(line, columns['number']))
            date_ = parser.parse(getattr(line, columns['date'])).date()
            deadline = standardize_date(
                parser.parse(getattr(line, columns['deadline'])),
                timezone or request.app.principal.time_zone
            )
            name = str(IssueName(date_.year, number))

            issues.add(
                name=name,
                number=number,
                date=date_,
                deadline=deadline
            )

        click.secho(f"{count} categorie(s) imported", fg='green')

        if dry_run:
            transaction.abort()
            click.secho("Aborting transaction", fg='yellow')
Exemple #14
0
def handle_forbidden(self, request):

    @request.after
    def set_status_code(response):
        response.status_code = self.code  # pass along 403

    return {
        'layout': Layout(self, request),
        'title': _("Access Denied"),
        'message': _(
            "You are trying to open a page for which you are not authorized."
        )
    }
Exemple #15
0
    def on_request(self):
        self.role.choices = []
        model = getattr(self, 'model', None)
        if self.request.is_private(model):
            self.role.choices = [('member', _("Editor"))]
        if self.request.is_secret(model):
            self.role.choices.append(('editor', _("Publisher")))

        self.group.choices = self.request.session.query(
            cast(UserGroup.id, String), UserGroup.name
        ).all()
        self.group.choices.insert(
            0, ('', self.request.translate(_("- none -")))
        )
Exemple #16
0
    def excluded_notices_note(self, number, request):
        """ Adds a paragraph with the number of excluded (print only) notices.

        """

        if number:
            note = _(
                "${number} publication(s) with particularly sensitive data "
                "according to BGS 152.3 §7 Abs. 2.",
                mapping={'number': number})
            self.p_markup(request.translate(note), style=self.style.paragraph)

        note = _("The electronic official gazette is available at "
                 "www.amtsblattzug.ch.")
        self.p_markup(request.translate(note), style=self.style.paragraph)
Exemple #17
0
class IssueForm(Form):

    number = IntegerField(label=_("Number"),
                          validators=[InputRequired(),
                                      NumberRange(min=1)])

    date_ = DateField(label=_("Date"), validators=[InputRequired()])

    deadline = DateTimeLocalField(label=_("Deadline"),
                                  validators=[InputRequired()])

    timezone = HiddenField()

    name = HiddenField(validators=[
        UniqueColumnValue(Issue),
        UnusedColumnKeyValue(GazetteNotice._issues)
    ])

    def validate(self):
        if self.date_.data and self.number.data:
            self.name.data = str(
                IssueName(self.date_.data.year, self.number.data))
        return super().validate()

    def on_request(self):
        self.timezone.data = self.request.app.principal.time_zone

    def update_model(self, model):
        model.number = self.number.data
        model.date = self.date_.data
        model.deadline = self.deadline.data
        model.name = str(IssueName(model.date.year, model.number))
        # Convert the deadline from the local timezone to UTC
        if model.deadline:
            model.deadline = standardize_date(model.deadline,
                                              self.timezone.data)

    def apply_model(self, model):
        self.number.data = model.number
        self.name.data = model.name
        self.date_.data = model.date
        self.deadline.data = model.deadline
        # Convert the deadline from UTC to the local timezone
        if self.deadline.data:
            self.deadline.data = to_timezone(
                self.deadline.data, self.timezone.data).replace(tzinfo=None)
        if model.in_use:
            self.number.render_kw = {'readonly': True}
Exemple #18
0
    def menu(self):
        result = []

        if self.request.is_private(self.model):
            # Publisher and Admin
            result.append((_("Official Notices"), self.manage_notices_link,
                           (isinstance(self.model, GazetteNoticeCollection)
                            and 'statistics' not in self.request.url), []))

            active = (isinstance(self.model, IssueCollection)
                      or isinstance(self.model, OrganizationCollection)
                      or isinstance(self.model, CategoryCollection)
                      or isinstance(self.model, UserCollection)
                      or isinstance(self.model, UserGroupCollection))
            manage = [(_("Issues"), self.manage_issues_link,
                       isinstance(self.model, IssueCollection), []),
                      (_("Organizations"), self.manage_organizations_link,
                       isinstance(self.model, OrganizationCollection), []),
                      (_("Categories"), self.manage_categories_link,
                       isinstance(self.model, CategoryCollection), []),
                      (_("Groups"), self.manage_groups_link,
                       isinstance(self.model, UserGroupCollection), []),
                      (_("Users"), self.manage_users_link,
                       isinstance(self.model, UserCollection), [])]
            result.append((_("Manage"), None, active, manage))

            result.append(
                (_("Statistics"),
                 self.request.link(GazetteNoticeCollection(self.session,
                                                           state='accepted'),
                                   name='statistics'),
                 (isinstance(self.model, GazetteNoticeCollection)
                  and 'statistics' in self.request.url), []))

        elif self.request.is_personal(self.model):
            # Editor
            result.append((_("Dashboard"), self.dashboard_link,
                           isinstance(self.model, Principal), []))

            result.append(
                (_("Published Official Notices"),
                 self.request.link(
                     GazetteNoticeCollection(self.session,
                                             state='published' if
                                             self.publishing else 'accepted')),
                 isinstance(self.model, GazetteNoticeCollection), []))

        return result
Exemple #19
0
    def _import_categories(request, app):

        request.locale = locale
        headers = {
            'id': request.translate(_("ID")),
            'name': request.translate(_("Name")),
            'title': request.translate(_("Title")),
            'active': request.translate(_("Active"))
        }

        session = app.session()
        categories = CategoryCollection(session)

        if clear:
            click.secho("Deleting categories", fg='yellow')
            for category in categories.query():
                session.delete(category)

        csvfile = convert_xls_to_csv(
            file, sheet_name=request.translate(_("Categories"))
        )
        csv = CSVFile(csvfile, expected_headers=headers.values())
        lines = list(csv.lines)
        columns = {
            key: csv.as_valid_identifier(value)
            for key, value in headers.items()
        }

        count = 0
        for line in lines:
            count += 1
            id_ = int(getattr(line, columns['id']))
            name = getattr(line, columns['name'])
            title = getattr(line, columns['title'])
            active = bool(int(getattr(line, columns['active'])))
            categories.add_root(
                id=id_,
                name=name,
                title=title,
                active=active,
                order=count
            )

        click.secho(f"{count} categorie(s) imported", fg='green')

        if dry_run:
            transaction.abort()
            click.secho("Aborting transaction", fg='yellow')
Exemple #20
0
    def construct_subject(notice, request):
        issues = notice.issue_objects
        number = issues[0].number if issues else ''

        organization = notice.organization_object
        parent = organization.parent if organization else None
        parent_id = (parent.external_name or '') if parent else ''
        prefixes = []
        if notice.at_cost:
            prefixes.append(request.translate(_("With costs")))
        if notice.print_only:
            prefixes.append(request.translate(_("Print only")))
        prefix = '' if not prefixes else "{} - ".format(" / ".join(prefixes))

        return "{}{} {} {} {}".format(prefix, number, parent_id, notice.title,
                                      notice.id)
Exemple #21
0
    def add(self, title, text, organization_id, category_id, user, issues,
            **kwargs):
        """ Add a new notice.

        A unique, URL-friendly name is created automatically for this notice
        using the title and optionally numbers for duplicate names.

        A entry is added automatically to the audit trail.

        Returns the created notice.
        """

        notice = GazetteNotice(id=uuid4(),
                               state='drafted',
                               title=title,
                               text=text,
                               name=self._get_unique_name(title),
                               issues=issues,
                               **kwargs)
        notice.user = user
        notice.group = user.group if user else None
        notice.organization_id = organization_id
        notice.category_id = category_id
        notice.apply_meta(self.session)
        self.session.add(notice)
        self.session.flush()

        audit_trail = MessageCollection(self.session, type='gazette_notice')
        audit_trail.add(channel_id=str(notice.id),
                        owner=str(user.id) if user else '',
                        meta={'event': _("created")})

        return notice
Exemple #22
0
    def excluded_notices_note(self, number, request):
        """ Adds a paragraph with the number of excluded (print only) notices.

        """

        note = _("The electronic official gazette is available at "
                 "www.amtsblattzug.ch.")
        self.p_markup(request.translate(note), style=self.style.paragraph)

        if number:
            note = _(
                "${number} publication(s) with particularly sensitive data "
                "are not available online. They are available in paper form "
                "from the State Chancellery, Seestrasse 2, 6300 Zug, or can "
                "be subscribed to at [email protected].",
                mapping={'number': number})
            self.p_markup(request.translate(note), style=self.style.paragraph)
Exemple #23
0
 def on_request(self):
     session = self.request.session
     query = session.query(cast(Organization.id, String),
                           Organization.title)
     query = query.filter(Organization.parent_id.is_(None))
     query = query.order_by(Organization.order)
     self.parent.choices = query.all()
     self.parent.choices.insert(0,
                                ('', self.request.translate(_("- none -"))))
Exemple #24
0
    def publish(self, request):
        """ Publish an accepted notice.

        This automatically adds en entry to the changelog.

        """

        super(GazetteNotice, self).publish()
        self.add_change(request, _("published"))
Exemple #25
0
 def __call__(self, form, field):
     if hasattr(form, 'model'):
         if hasattr(form.model, field.name):
             data = getattr(form.model, field.name)
             if data != field.data:
                 query = form.request.session.query(self.column)
                 query = query.filter(self.column.has_key(data))  # noqa
                 if query.first():
                     raise ValidationError(_("This value is in use."))
Exemple #26
0
    def accept(self, request):
        """ Accept a submitted notice.

        This automatically adds en entry to the changelog.

        """

        super(GazetteNotice, self).accept()
        self.add_change(request, _("accepted"))
Exemple #27
0
    def reject(self, request, comment):
        """ Reject a submitted notice.

        This automatically adds en entry to the changelog.

        """

        super(GazetteNotice, self).reject()
        self.add_change(request, _("rejected"), comment)
Exemple #28
0
    def submit(self, request):
        """ Submit a drafted notice.

        This automatically adds en entry to the changelog.

        """

        super(GazetteNotice, self).submit()
        self.add_change(request, _("submitted"))
Exemple #29
0
def delete_user(self, request, form):
    """ Delete a user.

    Publishers may only edit members. Admins can not be deleted.

    """

    layout = Layout(self, request)

    if self.role != 'member' and not request.is_secret(self):
        raise HTTPForbidden()

    if self.official_notices or self.changes:
        request.message(_("There are official notices linked to this user!"),
                        'warning')

    if form.submitted(request):
        collection = UserCollection(request.session)
        user = collection.by_username(self.username)
        if user.role != 'admin':
            self.logout_all_sessions(request)
            collection.delete(self.username)
            request.message(_("User deleted."), 'success')
        return redirect(layout.manage_users_link)

    return {
        'message':
        _('Do you really want to delete "${item}"?',
          mapping={'item': self.title}),
        'layout':
        layout,
        'form':
        form,
        'title':
        self.title,
        'subtitle':
        _("Delete User"),
        'button_text':
        _("Delete User"),
        'button_class':
        'alert',
        'cancel':
        layout.manage_users_link
    }
Exemple #30
0
    def from_notices(cls, notices, request):
        """ Create an index PDF from a collection of notices. """

        title = request.translate(_("Gazette"))
        result = BytesIO()
        pdf = cls(result, title=title, author=request.app.principal.name)
        pdf.init_a4_portrait(page_fn=page_fn_footer,
                             page_fn_later=page_fn_header_and_footer)
        pdf.h1(title)
        pdf.h1(request.translate(_("Index")))
        pdf.h2(request.translate(_("Organizations")))
        pdf.organization_index(notices)
        pdf.pagebreak()
        pdf.h2(request.translate(_("Categories")))
        pdf.category_index(notices)
        pdf.generate()

        result.seek(0)
        return result