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
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}
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 }))
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'))
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 }
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 }
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 }
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 }
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')
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 }
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."), }
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 }
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')
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." ) }
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 -"))) )
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)
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}
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
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')
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)
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
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)
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 -"))))
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"))
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."))
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"))
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)
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"))
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 }
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