From ce160e61fffbc3370497a1adc299dad230411b72 Mon Sep 17 00:00:00 2001 From: zsloan Date: Fri, 5 Aug 2016 20:53:38 +0000 Subject: Fixed issue that caused some interval mapping results to be wrong; the cause seems to be related to some strains being renamed in the genofiles Added a message to the Add to Collection page warning users that anonymous collections will only be stored for 5 days Updated genofiles --- wqflask/base/webqtlCaseData.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'wqflask/base') diff --git a/wqflask/base/webqtlCaseData.py b/wqflask/base/webqtlCaseData.py index c80fcb65..8df9939e 100644 --- a/wqflask/base/webqtlCaseData.py +++ b/wqflask/base/webqtlCaseData.py @@ -52,6 +52,10 @@ class webqtlCaseData(object): str += " variance=%2.3f" % self.variance if self.num_cases != None: str += " ndata=%d" % self.num_cases + if self.name != None: + str += " name=%s" % self.name + if self.name2 != None: + str += " name2=%s" % self.name2 return str @property -- cgit v1.2.3 From e695e8d162ca04f41694a525e93ced1b9d23a85b Mon Sep 17 00:00:00 2001 From: zsloan Date: Tue, 16 Aug 2016 20:30:38 +0000 Subject: Users can now select specific traits from collection when using collection tools (correlation matrix, wgcna, etc) To do the above, changed the way form submission worked for those features; previously each feature had its own form, but that was dumb so instead I wrote a javascript function that just changed a single form's target url Duplicate traits can no longer by added to collections Fixed the digits for a few table columns in collection (additive effect, etc). --- wqflask/base/trait.py | 1 - wqflask/utility/helper_functions.py | 16 +- wqflask/wqflask/collect.py | 41 +--- wqflask/wqflask/model.py | 2 - wqflask/wqflask/templates/collections/add.html | 11 - wqflask/wqflask/templates/collections/view.html | 235 ++++++++++++---------- wqflask/wqflask/templates/search_result_page.html | 2 +- 7 files changed, 149 insertions(+), 159 deletions(-) (limited to 'wqflask/base') diff --git a/wqflask/base/trait.py b/wqflask/base/trait.py index 9566c192..f18481a8 100644 --- a/wqflask/base/trait.py +++ b/wqflask/base/trait.py @@ -116,7 +116,6 @@ class GeneralTrait(object): return stringy - def display_name(self): stringy = "" if self.dataset and self.name: diff --git a/wqflask/utility/helper_functions.py b/wqflask/utility/helper_functions.py index 15f60765..149ee553 100644 --- a/wqflask/utility/helper_functions.py +++ b/wqflask/utility/helper_functions.py @@ -4,6 +4,8 @@ from base.trait import GeneralTrait from base import data_set from base.species import TheSpecies +from wqflask import user_manager + def get_species_dataset_trait(self, start_vars): #assert type(read_genotype) == type(bool()), "Expecting boolean value for read_genotype" @@ -23,13 +25,15 @@ def get_species_dataset_trait(self, start_vars): def get_trait_db_obs(self, trait_db_list): - + if isinstance(trait_db_list, basestring): + trait_db_list = trait_db_list.split(",") + self.trait_list = [] - for i, trait_db in enumerate(trait_db_list): - if i == (len(trait_db_list) - 1): - break - trait_name, dataset_name = trait_db.split(":") - #print("dataset_name:", dataset_name) + for trait in trait_db_list: + data, _separator, hmac = trait.rpartition(':') + data = data.strip() + assert hmac==user_manager.actual_hmac_creation(data), "Data tampering?" + trait_name, dataset_name = data.split(":") dataset_ob = data_set.create_dataset(dataset_name) trait_ob = GeneralTrait(dataset=dataset_ob, name=trait_name, diff --git a/wqflask/wqflask/collect.py b/wqflask/wqflask/collect.py index 5484633b..7e7aba89 100644 --- a/wqflask/wqflask/collect.py +++ b/wqflask/wqflask/collect.py @@ -165,12 +165,19 @@ class UserCollection(object): return create_new("Default") else: uc = model.UserCollection.query.get(params['existing_collection']) - members = uc.members_as_set() #set(json.loads(uc.members)) + members = list(uc.members_as_set()) #set(json.loads(uc.members)) len_before = len(members) traits = process_traits(params['traits']) - - members_now = list(members | traits) + + members_now = members + for trait in traits: + if trait in members: + continue + else: + members_now.append(trait) + + #members_now = list(members | traits) len_now = len(members_now) uc.members = json.dumps(members_now) @@ -184,28 +191,6 @@ class UserCollection(object): # Probably have to change that return redirect(url_for('view_collection', uc_id=uc.id)) - def remove_traits(self, params): - - #params = request.form - print("params are:", params) - uc_id = params['uc_id'] - uc = model.UserCollection.query.get(uc_id) - traits_to_remove = params.getlist('traits[]') - print("traits_to_remove are:", traits_to_remove) - traits_to_remove = process_traits(traits_to_remove) - print("\n\n after processing, traits_to_remove:", traits_to_remove) - all_traits = uc.members_as_set() - print(" all_traits:", all_traits) - members_now = all_traits - traits_to_remove - print(" members_now:", members_now) - print("Went from {} to {} members in set.".format(len(all_traits), len(members_now))) - uc.members = json.dumps(list(members_now)) - uc.changed_timestamp = datetime.datetime.utcnow() - db_session.commit() - - # We need to return something so we'll return this...maybe in the future - # we can use it to check the results - return str(len(members_now)) def report_change(len_before, len_now): new_length = len_now - len_before @@ -218,8 +203,6 @@ def report_change(len_before, len_now): print("No new traits were added.") - - @app.route("/collections/add") def collections_add(): traits=request.args['traits'] @@ -329,7 +312,6 @@ def list_collections(): @app.route("/collections/remove", methods=('POST',)) def remove_traits(): - params = request.form print("params are:", params) @@ -337,14 +319,11 @@ def remove_traits(): uc_id = params['uc_id'] uc = model.UserCollection.query.get(uc_id) traits_to_remove = params.getlist('traits[]') - print("traits_to_remove are:", traits_to_remove) traits_to_remove = process_traits(traits_to_remove) print("\n\n after processing, traits_to_remove:", traits_to_remove) all_traits = uc.members_as_set() - print(" all_traits:", all_traits) members_now = all_traits - traits_to_remove print(" members_now:", members_now) - print("Went from {} to {} members in set.".format(len(all_traits), len(members_now))) uc.members = json.dumps(list(members_now)) uc.changed_timestamp = datetime.datetime.utcnow() db_session.commit() diff --git a/wqflask/wqflask/model.py b/wqflask/wqflask/model.py index 17343186..5321e420 100644 --- a/wqflask/wqflask/model.py +++ b/wqflask/wqflask/model.py @@ -177,12 +177,10 @@ class UserCollection(Base): except: return 0 - #@property #def display_num_members(self): # return display_collapsible(self.num_members) - def members_as_set(self): return set(json.loads(self.members)) diff --git a/wqflask/wqflask/templates/collections/add.html b/wqflask/wqflask/templates/collections/add.html index c5598e84..47b87d73 100644 --- a/wqflask/wqflask/templates/collections/add.html +++ b/wqflask/wqflask/templates/collections/add.html @@ -6,16 +6,6 @@ -
- {% if selectedChr == -1 %} + {% if selectedChr == -1 %} +

Results

+
- + + {% if plotScale == "centimorgan" %} @@ -184,7 +186,12 @@ {% else %} {% endif %} - + {% if 'additive' in trimmed_markers[0] %} + + {% endif %} + {% if 'dominance' in trimmed_markers[0] %} + + {% endif %} @@ -196,6 +203,7 @@ value="{{ marker.name }}" checked="checked"> + {% if LRS_LOD == "LOD" %} {% if 'lod_score' in marker %} @@ -211,13 +219,22 @@ {% endif %} - + {% if 'additive' in marker %} + + {% endif %} + {% if 'dominance' in marker %} + + {% endif %} {% endfor %}
IndexRowLocus {{ LRS_LOD }} ChrMbLocusAdd EffDom Eff
{{ loop.index }}{{ marker.name }}{{ '%0.2f' | format(marker.lod_score|float) }}{{marker.chr}} {{ '%0.6f' | format(marker.Mb|float) }}{{ marker.name }}{{ '%0.3f' | format(marker.additive|float) }}{{ '%0.2f' | format(marker.dominance|float) }}
- {% else %} +
+
+ {% else %} +

Interval Analyst

+
@@ -236,8 +253,9 @@ {% endfor %}
- {% endif %} +
+ {% endif %}
@@ -251,7 +269,7 @@ - + @@ -274,26 +292,35 @@ - @@ -171,7 +170,6 @@ - + + + + + + + + + + + +{% endblock %} diff --git a/wqflask/wqflask/views.py b/wqflask/wqflask/views.py index 63dceb42..41e2c7be 100644 --- a/wqflask/wqflask/views.py +++ b/wqflask/wqflask/views.py @@ -38,6 +38,7 @@ from wqflask.show_trait import export_trait_data from wqflask.heatmap import heatmap from wqflask.marker_regression import marker_regression from wqflask.marker_regression import marker_regression_gn1 +from wqflask.network_graph import network_graph from wqflask.correlation import show_corr_results from wqflask.correlation_matrix import show_corr_matrix from wqflask.correlation import corr_scatter_plot @@ -530,6 +531,22 @@ def export_pdf(): response.headers["Content-Disposition"] = "attachment; filename=%s"%filename return response +@app.route("/network_graph", methods=('POST',)) +def network_graph_page(): + logger.info("In network_graph, request.form is:", pf(request.form)) + + start_vars = request.form + traits = [trait.strip() for trait in start_vars['trait_list'].split(',')] + if traits[0] != "": + template_vars = network_graph.NetworkGraph(start_vars) + template_vars.js_data = json.dumps(template_vars.js_data, + default=json_default_handler, + indent=" ") + + return render_template("network_graph.html", **template_vars.__dict__) + else: + return render_template("empty_collection.html", **{'tool':'Network Graph'}) + @app.route("/corr_compute", methods=('POST',)) def corr_compute_page(): logger.info("In corr_compute, request.form is:", pf(request.form)) -- cgit v1.2.3 From ec7d4e1198bc8d2f83ce99b41e2084fbce6a0be7 Mon Sep 17 00:00:00 2001 From: Pjotr Prins Date: Sat, 10 Sep 2016 11:43:21 +0200 Subject: Support for running maintenance scripts so they can pick up all webserver settings Run with ./bin/genenetwork2 ~/my_settings.py -c ./wqflask/maintenance/gen_select_dataset.py --- bin/genenetwork2 | 27 ++++++++++++++++++++++++--- wqflask/base/data_set.py | 2 +- wqflask/base/webqtlConfig.py | 1 - wqflask/maintenance/gen_select_dataset.py | 26 +++++++++++++++++--------- wqflask/utility/tools.py | 1 + 5 files changed, 43 insertions(+), 14 deletions(-) (limited to 'wqflask/base') diff --git a/bin/genenetwork2 b/bin/genenetwork2 index d3bf3299..3a8c3ff4 100755 --- a/bin/genenetwork2 +++ b/bin/genenetwork2 @@ -1,6 +1,14 @@ #! /bin/bash # -# This will run the GN2 server (with default settings if none supplied). +# This will run the GN2 server (with default settings if none supplied). Pass in +# your own settings file, e.g. +# +# ./bin/genenetwork2 ~/my_settings.py +# +# To run a maintenance script with settings (instead of the webserver) add that with +# a -c switch, e.g. +# +# ./bin/genenetwork2 ~/my_settings.py -c ./wqflask/maintenance/gen_select_dataset.py # # Environment settings can be used to preconfigure as well as a # settings.py file. @@ -18,7 +26,12 @@ echo $GN2_BASE_PATH # Handle settings parameter settings=$1 -if [ -z $settings ]; then settings=$GN2_BASE_PATH/etc/default_settings.py ; fi +if [ -z $settings ]; then + # get default + settings=$GN2_BASE_PATH/etc/default_settings.py +else + shift +fi if [ ! -e $settings ]; then echo "ERROR: can not locate settings file - pass it in the command line" exit 1 @@ -32,7 +45,15 @@ export PYTHONPATH=$GN2_BASE_PATH/wqflask:$PYTHONPATH if [ -z $TEMPDIR ]; then TEMPDIR="/tmp" fi - + +# Now handle command parameter -c +if [ $1 = '-c' ] ; then + echo PYTHONPATH=$PYTHONPATH + echo RUNNING COMMAND $2 + /usr/bin/env python $2 + exit 0 +fi + echo "Starting the redis server:" echo -n "dir $TEMPDIR dbfilename gn2.rdb diff --git a/wqflask/base/data_set.py b/wqflask/base/data_set.py index 1b4e1195..6cd3c8e6 100644 --- a/wqflask/base/data_set.py +++ b/wqflask/base/data_set.py @@ -91,7 +91,7 @@ Publish or ProbeSet. E.g. if USE_GN_SERVER: data = menu_main() else: - file_name = "wqflask/static/new/javascript/dataset_menu_structure.json" + file_name = "wqflask/wqflask/static/new/javascript/dataset_menu_structure.json" with open(file_name, 'r') as fh: data = json.load(fh) diff --git a/wqflask/base/webqtlConfig.py b/wqflask/base/webqtlConfig.py index f76d8140..8c67a6fd 100644 --- a/wqflask/base/webqtlConfig.py +++ b/wqflask/base/webqtlConfig.py @@ -80,4 +80,3 @@ PORTADDR = "http://50.16.251.170" INFOPAGEHREF = '/dbdoc/%s.html' CGIDIR = '/webqtl/' #XZ: The variable name 'CGIDIR' should be changed to 'PYTHONDIR' SCRIPTFILE = 'main.py' - diff --git a/wqflask/maintenance/gen_select_dataset.py b/wqflask/maintenance/gen_select_dataset.py index d39bf4a5..5c25c15b 100644 --- a/wqflask/maintenance/gen_select_dataset.py +++ b/wqflask/maintenance/gen_select_dataset.py @@ -1,7 +1,9 @@ """Script that generates the data for the main dropdown menus on the home page Writes out data as /static/new/javascript/dataset_menu_structure.json -It needs to be run manually when database has been changed. +It needs to be run manually when database has been changed. Run it as + + python gen_select_dataset.py """ @@ -37,9 +39,15 @@ from __future__ import print_function, division #print("cdict is:", cdict) import sys -# import zach_settings # no hard code paths! -# import MySQLdb +# NEW: Note we prepend the current path - otherwise a guix instance of GN2 is used instead +sys.path.insert(0,'./wqflask') +# NEW: import app to avoid a circular dependency on utility.tools +from wqflask import app + +from utility.tools import locate, locate_ignore_error, TEMPDIR, SQL_URI + +import MySQLdb # import simplejson as json import urlparse @@ -55,14 +63,13 @@ from pprint import pformat as pf #conn = Engine.connect() -print('ERROR: This conversion is now OBSOLETE as the menu gets built from the database in Javascript using GN_SERVER instead!') -sys.exit() +print('WARNING: This conversion is now OBSOLETE as the menu gets built from the database in Javascript using GN_SERVER instead!') def parse_db_uri(db_uri): """Converts a database URI to the db name, host name, user name, and password""" - parsed_uri = urlparse.urlparse(zach_settings.DB_URI) + parsed_uri = urlparse.urlparse(SQL_URI) db_conn_info = dict( db = parsed_uri.path[1:], @@ -70,6 +77,7 @@ def parse_db_uri(db_uri): user = parsed_uri.username, passwd = parsed_uri.password) + print(db_conn_info) return db_conn_info @@ -258,7 +266,7 @@ def build_datasets(species, group, type_name): def main(): """Generates and outputs (as json file) the data for the main dropdown menus on the home page""" - parse_db_uri(zach_settings.SQL_URI) + parse_db_uri(SQL_URI) species = get_species() groups = get_groups(species) @@ -281,7 +289,7 @@ def main(): #print("data:", data) - output_file = """../wqflask/static/new/javascript/dataset_menu_structure.json""" + output_file = """./wqflask/wqflask/static/new/javascript/dataset_menu_structure.json""" with open(output_file, 'w') as fh: json.dump(data, fh, indent=" ", sort_keys=True) @@ -297,6 +305,6 @@ def _test_it(): #print("build_datasets:", pf(datasets)) if __name__ == '__main__': - Conn = MySQLdb.Connect(**parse_db_uri(zach_settings.SQL_URI)) + Conn = MySQLdb.Connect(**parse_db_uri(SQL_URI)) Cursor = Conn.cursor() main() diff --git a/wqflask/utility/tools.py b/wqflask/utility/tools.py index bb8241f5..2c8cc5c5 100644 --- a/wqflask/utility/tools.py +++ b/wqflask/utility/tools.py @@ -3,6 +3,7 @@ import os import sys + from wqflask import app # Use the standard logger here to avoid a circular dependency -- cgit v1.2.3 From f399b3bf514b396de67644d4b1e2018886cecac2 Mon Sep 17 00:00:00 2001 From: Pjotr Prins Date: Tue, 13 Sep 2016 09:09:21 +0200 Subject: Run scripts from ./wqflask - just like the webserver --- bin/genenetwork2 | 9 +++++---- wqflask/base/data_set.py | 2 +- wqflask/maintenance/gen_select_dataset.py | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) (limited to 'wqflask/base') diff --git a/bin/genenetwork2 b/bin/genenetwork2 index 3a8c3ff4..d926d6a1 100755 --- a/bin/genenetwork2 +++ b/bin/genenetwork2 @@ -13,13 +13,12 @@ # Environment settings can be used to preconfigure as well as a # settings.py file. -# Absolute path to this script, e.g. /home/user/bin/foo.sh SCRIPT=$(readlink -f "$0") -# Absolute path this script is in, thus /home/user/bin GN2_BASE_PATH=$(dirname $(dirname "$SCRIPT")) GN2_GUIX_PATH=$GN2_BASE_PATH/lib/python2.7/site-packages/genenetwork2-2.0-py2.7.egg if [ -d $GN2_GUIX_PATH ]; then + echo GN2 is running from GUIX GN2_BASE_PATH=$GN2_GUIX_PATH fi echo $GN2_BASE_PATH @@ -48,9 +47,11 @@ fi # Now handle command parameter -c if [ $1 = '-c' ] ; then + cd $GN2_BASE_PATH/wqflask + cmd=${2#wqflask/} echo PYTHONPATH=$PYTHONPATH - echo RUNNING COMMAND $2 - /usr/bin/env python $2 + echo RUNNING COMMAND $cmd + /usr/bin/env python $cmd exit 0 fi diff --git a/wqflask/base/data_set.py b/wqflask/base/data_set.py index 6cd3c8e6..1b4e1195 100644 --- a/wqflask/base/data_set.py +++ b/wqflask/base/data_set.py @@ -91,7 +91,7 @@ Publish or ProbeSet. E.g. if USE_GN_SERVER: data = menu_main() else: - file_name = "wqflask/wqflask/static/new/javascript/dataset_menu_structure.json" + file_name = "wqflask/static/new/javascript/dataset_menu_structure.json" with open(file_name, 'r') as fh: data = json.load(fh) diff --git a/wqflask/maintenance/gen_select_dataset.py b/wqflask/maintenance/gen_select_dataset.py index f2f0830f..23adc4f9 100644 --- a/wqflask/maintenance/gen_select_dataset.py +++ b/wqflask/maintenance/gen_select_dataset.py @@ -40,8 +40,8 @@ from __future__ import print_function, division import sys -# NEW: Note we prepend the current path - otherwise a guix instance of GN2 is used instead -sys.path.insert(0,'./wqflask') +# NEW: Note we prepend the current path - otherwise a guix instance of GN2 may be used instead +sys.path.insert(0,'./') # NEW: import app to avoid a circular dependency on utility.tools from wqflask import app -- cgit v1.2.3 From e7693e53821747d294452c9bff7e8b0f38a0eb8e Mon Sep 17 00:00:00 2001 From: Pjotr Prins Date: Sat, 24 Sep 2016 07:41:39 +0000 Subject: tools: export GENENETWORK_FILES and move cache into TMPDIR/gn2 --- etc/default_settings.py | 13 ++++++++----- wqflask/base/webqtlConfig.py | 5 +++-- wqflask/utility/tools.py | 7 +++++-- 3 files changed, 16 insertions(+), 9 deletions(-) (limited to 'wqflask/base') diff --git a/etc/default_settings.py b/etc/default_settings.py index df734f7c..9dddd0ad 100644 --- a/etc/default_settings.py +++ b/etc/default_settings.py @@ -22,7 +22,7 @@ SQLALCHEMY_DATABASE_URI = 'mysql://gn2:mysql_password@localhost/db_webqtl_s' SQLALCHEMY_POOL_RECYCLE = 3600 GN_SERVER_URL = "http://localhost:8880/" -# Flask configuration (see website) +# ---- Flask configuration (see website) TRAP_BAD_REQUEST_ERRORS = True SECURITY_CONFIRMABLE = True SECURITY_TRACKABLE = True @@ -34,8 +34,8 @@ SECURITY_POST_LOGIN_VIEW = "/thank_you" SERVER_PORT = 5003 SECRET_HMAC_CODE = '\x08\xdf\xfa\x93N\x80\xd9\\H@\\\x9f`\x98d^\xb4a;\xc6OM\x946a\xbc\xfc\x80:*\xebc' -# Behavioural settings (defaults) note that logger and log levels can -# be overridden at the module level and with enviroment settings +# ---- Behavioural settings (defaults) note that logger and log levels can +# be overridden at the module level and with enviroment settings WEBSERVER_MODE = 'DEV' # Python webserver mode (DEBUG|DEV|PROD) WEBSERVER_BRANDING = None # Set the branding (nyi) WEBSERVER_DEPLOY = None # Deployment specifics (nyi) @@ -49,10 +49,13 @@ LOG_BENCH = True # Log bench marks USE_REDIS = True # REDIS caching (note that redis will be phased out) USE_GN_SERVER = 'False' # Use GN_SERVER SQL calls -# Path overrides for Genenetwork +# ---- Path overrides for Genenetwork +# TMPDIR is normally picked up from the environment HOME=os.environ['HOME'] LOGFILE = HOME+"/genenetwork2.log" -GENENETWORK_FILES = HOME+"/gn2_data" +GENENETWORK_FILES = HOME+"/gn2_data" # base dir for all static data files + +# ---- GN2 Executables PYLMM_COMMAND = str.strip(os.popen("which pylmm_redis").read()) PLINK_COMMAND = str.strip(os.popen("which plink2").read()) GEMMA_COMMAND = str.strip(os.popen("which gemma").read()) diff --git a/wqflask/base/webqtlConfig.py b/wqflask/base/webqtlConfig.py index 8c67a6fd..6bbabdec 100644 --- a/wqflask/base/webqtlConfig.py +++ b/wqflask/base/webqtlConfig.py @@ -60,9 +60,10 @@ ENSEMBLETRANSCRIPT_URL="http://useast.ensembl.org/Mus_musculus/Lucene/Details?sp # HTMLPATH is replaced by GENODIR # IMGDIR is replaced by GENERATED_IMAGE_DIR -# Temporary storage: +# Temporary storage (note that this TMPDIR is not the same directory +# as the UNIX TMPDIR) TMPDIR = mk_dir(TEMPDIR+'/gn2/') -CACHEDIR = mk_dir(TEMPDIR+'/cache/') +CACHEDIR = mk_dir(TMPDIR+'/cache/') # We can no longer write into the git tree: GENERATED_IMAGE_DIR = mk_dir(TMPDIR+'/generated/') GENERATED_TEXT_DIR = mk_dir(TMPDIR+'/generated_text/') diff --git a/wqflask/utility/tools.py b/wqflask/utility/tools.py index 2c8cc5c5..907b0d6a 100644 --- a/wqflask/utility/tools.py +++ b/wqflask/utility/tools.py @@ -147,7 +147,10 @@ def locate_ignore_error(name, subdir=None): return None def tempdir(): - return valid_path(get_setting("TEMPDIR","/tmp")) + """ + Get UNIX TMPDIR by default + """ + return valid_path(get_setting("TMPDIR","/tmp")) BLUE = '\033[94m' GREEN = '\033[92m' @@ -184,9 +187,9 @@ LOG_BENCH = get_setting_bool('LOG_BENCH') LOG_FORMAT = "%(message)s" # not yet in use USE_REDIS = get_setting_bool('USE_REDIS') USE_GN_SERVER = get_setting_bool('USE_GN_SERVER') +GENENETWORK_FILES = get_setting_bool('GENENETWORK_FILES') PYLMM_COMMAND = pylmm_command() GEMMA_COMMAND = gemma_command() PLINK_COMMAND = plink_command() -FLAT_FILES = flat_files() TEMPDIR = tempdir() -- cgit v1.2.3