4.5. Running scripts directly

In many occasions it is handy for administrators to run scripts directly. So, instead of having to create a custom operation script, then a custom operation, then granting permissions, refreshing the browser and running, there is a menu called Run script, which presents a text box where the script may be typed in or pasted, which can be executed directly. Of course, only the basic bindings are available.

The result of the script can be one of the following:

So, for example, to return an HTML text with a title, the script can return [title:"The result title", richText:"<b>Formatted</b> text"]. To show a notification, the the script can return [notification:"Notification text"]. The same prefixes available on notifications for custom operations are available on notifications: [INFO], [WARN] and [ERROR].

4.5.1. Examples

4.5.1.1. Remove all users, transactions and related data

Here is an example of a script to remove all regular users (not administrators) and related data, as well as all system to system transactions. This script must be executed in a network. Be advised that there will be no confirmation, and all users and all related data will be removed.

The script works by first recreating all database constraints with the option ON DELETE CASCADE. Then, all users are removed, which will cascade the removal to accounts, transfers, advertisements, records, messages, notifications, references and so on. For this reason all tables are locked, and the script will likely fail if there is any activity in the system, such as active users or background tasks.

Be careful when running in systems where specific users are used in the configuration, such as fees that are paid by a specific user, or payment types which are restricted to specific users. In such configurations, all such related data will be removed as well. Also, note that it may take a while to run, so, please, wait before the script completes.

AGAIN: be very careful when using this script! Only run it on test instances and always have a database backup before running it.

import org.cyclos.db.DeleteNetworkData
import org.cyclos.impl.utils.cache.CacheType
import org.cyclos.model.ValidationException

if (sessionData.network == null) {
    throw new ValidationException("This script can only be executed in a network")
}

def deleteNetworkData = beanHandler.autowire(DeleteNetworkData)
def users = deleteNetworkData.deleteUsersAndBanking(sessionData.network)
CacheType.all().each { cacheHandler.scheduleClear(it) }
searchHandler.reindex()
return "Removed ${users} users"

4.5.1.2. Export advertisements with images

Here is an example of a script to export the published advertisements with its images in the same format used in the Cyclos import, so the result can be used to add the returned advertisements in other Cyclos network.

import java.nio.charset.StandardCharsets
import java.sql.ResultSet
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream

import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.cyclos.entities.marketplace.AdImage
import org.cyclos.entities.marketplace.QAdImage
import org.cyclos.impl.access.SessionData
import org.cyclos.impl.marketplace.AdImageServiceLocal
import org.cyclos.impl.storage.StoredFileHandler
import org.cyclos.impl.utils.formatting.FormatterImpl
import org.cyclos.impl.utils.persistence.EntityManagerHandler
import org.cyclos.model.marketplace.advertisements.BasicAdVO
import org.cyclos.model.utils.FileInfo
import org.cyclos.server.utils.SerializableInputStream
import org.cyclos.utils.ContentType
import org.springframework.jdbc.core.ColumnMapRowMapper
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.RowCallbackHandler

import com.opencsv.CSVParserBuilder
import com.opencsv.CSVWriterBuilder

JdbcTemplate jdbc = binding.jdbc
SessionData sessionData = binding.sessionData
EntityManagerHandler entityManagerHandler = binding.entityManagerHandler
StoredFileHandler storedFileHandler = binding.storedFileHandler
FormatterImpl formatter = binding.formatter
AdImageServiceLocal adImageService = binding.adImageService

def sql = """
select
    ad.id,
    u.username as user,
    ad.creation_date as creationdate,
    ad.name as title,
    ad.description,
    ad.status,
    array_to_string(array_agg(
        name_hierarchy(ac.category_id, 'ad_categories', 'internal_name')
    ), ',') as categories,
    ad.begin_publication_period as publicationbegin,
    ad.end_publication_period as publicationend,
    ad.price_amount as price,
    ad.promotional_price as promotionalprice,
    ad.begin_promotional_price_period as promotionalperiodbegin,
    ad.end_promotional_price_period as promotionalperiodend
from ads ad
    inner join users u on ad.owner_id = u.id
    inner join ads_categories ac on ad.id = ac.ad_id
    inner join ad_categories c on ac.category_id = c.id
where u.network_id = ${sessionData.network.id}
and ad.end_publication_period > now()
group by 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12
"""

def file = File.createTempFile("export", ".zip")
def zip = new ZipOutputStream(new FileOutputStream(file), StandardCharsets.UTF_8)

// Write the index
zip.putNextEntry(new ZipEntry("index.csv"))
def parser = new CSVParserBuilder().withSeparator(sessionData.configuration.listSeparator.value as char).build()
def csv = new CSVWriterBuilder(new OutputStreamWriter(zip, StandardCharsets.UTF_8)).withParser(parser).build()
csv.writeNext([
    'user',
    'creationdate',
    'title',
    'description',
    'status',
    'categories',
    'publicationbegin',
    'publicationend',
    'price',
    'promotionalprice',
    'promotionalperiodbegin',
    'promotionalperiodend',
    'images'
] as String[])
def mapper = new ColumnMapRowMapper()
def rch = { ResultSet rs ->
    def map = mapper.mapRow(rs, 0)
    def images = adImageService.list(new BasicAdVO(map.id as long))
    def imageNames = images.collect {
        def contentType = ContentType.getByMimeType(it.contentType)
        return "${it.id}.${contentType.extension}"
    }
    csv.writeNext([
        map.user,
        formatter.format(map.creationdate),
        map.title,
        map.description,
        map.status,
        (map.categories as String).tokenize(',').collect {StringUtils.strip(it, '_').replace('_a_', '>')}.unique().join(', '),
        formatter.format(map.publicationbegin),
        formatter.format(map.publicationend),
        formatter.format(map.price, 2) ?: '',
        formatter.format(map.promotionalprice, 2) ?: '',
        formatter.format(map.promotionalperiodbegin) ?: '',
        formatter.format(map.promotionalperiodend) ?: '',
        imageNames.join(',')
    ] as String[])
    entityManagerHandler.clear()
} as RowCallbackHandler
jdbc.query(sql, rch)
csv.flush()
zip.closeEntry()

// Write each image
def ai = QAdImage.adImage
entityManagerHandler
        .from(ai)
        .where(ai.ad().publicationPeriod().end.future())
        .iterate(ai)
        .forEachRemaining { AdImage image ->
            def contentType = ContentType.getByMimeType(image.contentType)
            zip.putNextEntry(new ZipEntry("${image.id}.${contentType.extension}"))
            storedFileHandler.getContent(image).withCloseable { content -> IOUtils.copy(content, zip) }
            zip.closeEntry()
            entityManagerHandler.clear()
        }

zip.finish()
zip.close()

return new FileInfo(
        content: new SerializableInputStream(file),
        contentType: ContentType.ZIP.mimeType,
        name: 'ads.zip',
        length: file.length())

4.5.1.3. Generating an account number for all accounts which doesn't have a number yet

If the account number is enabled after existing users / transactions, existing accounts will not have numbers automatically assigned. To assign a number to all accounts (even system accounts) which don't have a number yet, run the following script:

import org.cyclos.entities.banking.QSystemAccount
import org.cyclos.entities.users.QBulkActionUser
import org.cyclos.impl.banking.AccountServiceLocal
import org.cyclos.model.users.bulkactions.AdjustAccountsBulkActionDTO
import org.cyclos.model.users.users.UserQuery

int system = 0
def sa = QSystemAccount.systemAccount
entityManagerHandler
        .from(sa)
        .where(sa.number.isNull())
        .stream(sa)
        .forEach { account ->
            account.number = accountService.generateNumber(account.type, account.owner)
            system++
        }

def query = new UserQuery();
query.setUserStatus(AccountServiceLocal.POSSIBLE_STATUSES_TO_OWN_ACCOUNTS);
def id = bulkActionService.save(new AdjustAccountsBulkActionDTO(query: query));

def bau = QBulkActionUser.bulkActionUser
def users = entityManagerHandler.from(bau).where(bau.bulkAction().id.eq(id)).fetchCount()

return "Generated account numbers for ${system} system accounts" +
        " and scheduled generation for ${users} users"

4.5.1.4. Generating a custom PDF file

In this example, a custom PDF file is downloaded directly. A similar example could be used as a custom operation:

import org.cyclos.CyclosVersion

import groovy.xml.MarkupBuilder

def out = new StringWriter()

// We'll be using Groovy's MarkupBuilder. Could also be a hand-crafted string
def html = new MarkupBuilder(out)
html.div {
    p "Currently logged-in as ${sessionData.loggedUser?.name}."
    p "This is an example PDF."
    div class:'note', {
        mkp.yield "Built with "
        a href: "https://www.cyclos.org", "Cyclos"
        mkp.yield " version ${CyclosVersion.get()}"
    }
}

def css = """
    .note {
        font-size: 90%;
        color: #333;
        margin-top: 2cm;
        text-align: center;
    }
"""

return pdfHandler
        .newTemplate(out.toString(), css)
        .title("Example PDF")
        // Or, instead of title, hide the header with .noHeader()
        // Similarly, could hide the footer with .noFooter()
        .renderToFile("custom.pdf")
// Alternatively, could use .render() instead of .renderToFile() to get the InputStream
// If the script needs a Base64 version of the content, do:
// Base64.encoder.encodeToString(inputStream.bytes)