pax_global_header00006660000000000000000000000064123773616060014525gustar00rootroot0000000000000052 comment=519370b697b39fe85efc9aff99c7859cebe0cb55 Turnin-NG-1.3/000077500000000000000000000000001237736160600131515ustar00rootroot00000000000000Turnin-NG-1.3/.gitignore000066400000000000000000000001221237736160600151340ustar00rootroot00000000000000build/ .*.swp .*.swo doc/turnin-ng.pdf doc/turnin-ng.info doc/turnin-ng.html tags Turnin-NG-1.3/AUTHORS000066400000000000000000000006761237736160600142320ustar00rootroot00000000000000Written by Ryan Kavanagh in 2009 as part of an internship at Queen's University, one of Canada's leading universities with an international reputation for scholarship, social purpose, spirit and diversity. Maintained outside of Queen's University after September 2009. It is Copyright 2009-2013 Ryan Kavanagh and is distributed under the terms of the GNU General Public License version 2, or (at your option) any later version. Turnin-NG-1.3/CHANGES000066400000000000000000000160351237736160600141510ustar00rootroot000000000000001.3 2014-08-27 turnin: * Create ~/.turnin-ng/submissions if it doesn't yet exist 1.2 2013-08-30 Global: * Use errno module instead of raw values Documentation: * Fix broken texinfo * Switch HTML/PDF documentation building to use makeinfo 1.1 2010-09-01 Global: * Ignore build files in Git (generated documentation, build/*, etc.) * Always initialize all of the course's options in the config files * Upgrade configuration files from old releases / installations. Preserves previous settings, just adds the options to the config files. * Accept that the legacy symlink may already exist from a previous install turnincfg: * Add support for group managed courses * Don't die if we receive an invalid username or group when creating courses * Don't strip a submission if it causes an overwrite * Added forgotten import (sys.exit() needs sys to be imported) * Don't crash when raising CalledProcessError when gpg --verify fails * Warn on dangling signatures * Warn on signatures submitted by other students * Set the group on the compressed tarball and extracted project directory * Also delete tarball on delete_project if project is compressed turnin: * Don't needlessly import chown * Fixed vulnerability. Set student's $HOME/.turnin-ng/list to 0600 so that, on off chance their home directory is world readable, other students can't find out what their submitted file suffixes are (and thus be able to play with them). * Print a pretty error message when we try to submit to a closed project * Don't crash when GPG fails to sign and we try to copy a non-existent .sig file * Remove stale signatures on unsigned resubmits Documentation: * Fix broken infodir entry * Properly install info manual with install-info * Expand the administrative section of the info manual 1.0.1 2010-02-12 Global: * Fix typos in turnin and turnincfg option descriptions Documentation: * Fix typos in the Texinfo documentation and in manpages 1.0 2009-09-01 Global: * The project command has been renamed to "turnincfg". We still install a link from $prefix/bin/project -> $prefix/bin/tcpsadmin , but we print a warning message when project is called and the link may disappear in any future release. This was done following this discussion: http://www.mail-archive.com/debian-bugs-dist@lists.debian.org/msg677272.html * Rename turninng.sys to turninng.fileperms to avoid name clashes with Python's sys. turnin: * Elaborate the -v option in --help to avoid confusion project/tcpsadmin: * Project without any options calls --help * --list for projects works. * Archiving a course sets it 0600 and is owned by the course's user and group. * Added a workaround for python2.4 since its tar.tarfile doesn't have an extractall() * When disabling a course, check if it's default. If it is, unset the default course. * Print the course directory when asking if we want to delete it. Documentation: * Commented out '@allowcodebreaks false' since it breaks on older systems and the output remains the same. * Now build PDF/HTML/INFO pages and install them * Updated for the name change 1.0~rc1 2009-08-06 Global: * Dropped missed references to check_group * Updated the docstrings project: * Substitute Error for Warning when stripping random suffixes. * [BUG FIX]: Check for a tarball, not a directory when we set the tarball key in the config * Check that ther's a course before creating a project in situations like 'project -c foo apple', which creates the project apple in the course foo. * Simplify how we get the config file and the course from the user Documentation: * Update the INSTALL file to reference to the documentation for more information on configuring. * Add examples to the manpages 1.0~beta2 2009-08-04 Global: * Get rid of 'No `START-INFO-DIR-ENTRY' and no `This file documents' when installing the info file. * If users don't pass any arguments, print --help message * Add --prefix note to the INSTALL file so sysadmins don't have to search online for how to install under /usr/local/ * Install man pages in the right directories. * Project submission directory is 733 now, it would be too much of a hassle sysadmins to create a group per course and add students to a group per course. Drop group checking. * Split the configurations into a global config file (used by students and administrators) and a course config file (used by professors and TAs). * [BUG FIX]: When listing projects, check for a 'default' project. This avoids KeyErrors. * Rename config files to turnin-ng.cf * Add option to print warranty information, as suggested by GPL * Set the default configuration file in /etc/turnin-ng.cf * Rename the turnin package to turninng turnin: * Use random 16 character suffixes (username-XXXXXXXXXXXXXXXX.tar.gz) when submitting to prevent users from copying each other or overwriting each other's assignments. * Store these suffixes in $HOME/.turnin-ng/submissions and use UUIDs to identify each project. This is to prevent a course having the project A, removing it, and then recreating it. * Have the user specify a project if the course doesn't have a default one. project: * Don't abort creating the course if the directory already exists. * [BUG FIX]: When run without args, we should not create a course and write to config file. * Initialize a project if we pass only a project name * Add turnin's list projects option. * When we compress projects, print the path to the tarball. * Add an option to compress a whole course directory. * Add an option to strip the random suffixes (-D / --perm-disable). * [BUG FIX]: Check that a project exists in a course before enabling/disabling/etc'ing it. * Professors can no longer switch between courses. Leave the 'default' option in the per course config so that TAs/professors don't have to specify the -c course option. * [BUG FIX]: Check that a course exists before archiving, creating or deleting it. * [BUG FIX]: Print project name instead of %s when disabling a project. * [BUG FIX]: Reset course directory to 755 when creating it instead of 733. Documentation: * Updated with all the above changes * [BUG FIX]: Info page now properly displays VERSION & UPDATED values. * Added this changelog 1.0~beta1 First release tested on a Queens server. Global: * Dropped course 'section' in the configuration files turnin: * Add option to sign submissions with GPG * Display project list in a table * No longer needs to be run setuid project: * Add option to verify GPG signatures * [BUG FIX]: Use path to course directory instead of project when creating courses * [BUG FIX]: Set the project directory as drwx------ after disabling so that students can't manually submit assignments after it's disabled. Documentation: * Added an INSTALL file * Added a Texinfo document * Fixed manpage header so that lexgrog / apropos work 0.1~beta1 First release, following the release early and release often idea Turnin-NG-1.3/COPYING000066400000000000000000000431031237736160600142050ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. Turnin-NG-1.3/INSTALL000066400000000000000000000020561237736160600142050ustar00rootroot00000000000000[DEPENDENCIES] * You need to have the configobj module installed. See http://www.voidspace.org.uk/python/configobj.html to get it or install the python-configobj package on Debian based systems. * You also need to have the UUID module, provided in python 2.5+. If you are running python <= 2.4, you can get it from http://pypi.python.org/pypi/uuid/ Your mileage may vary. [INSTALLING] Once you have all the dependencies, , in the source's root directory, run: % sudo python setup.py install This will install turnin-ng to your system's default location, /usr under UNIX. If you wanted to install it under /usr/local, pass the --prefix option: % sudo python setup.py install --prefix=/usr/local [CONFIGURING] See the manpages and the info page for details and examples on configuring Turnin-NG. To generate the Texinfo documentation, run: % texi2pdf doc/turnin-ng.texi # Generate the PDF documentation % sudo makeinfo -o /usr/share/info/turnin-ng.info doc/turnin-ng.texi && \ sudo install-info /usr/share/info/turnin-ng.info # Generate the info files. Turnin-NG-1.3/TODO000066400000000000000000000001341237736160600136370ustar00rootroot00000000000000Planned features: * Add a per-course keyring * Add support for remote submissions (SFTP?) Turnin-NG-1.3/doc/000077500000000000000000000000001237736160600137165ustar00rootroot00000000000000Turnin-NG-1.3/doc/gpl-2.0.texi000066400000000000000000000436641237736160600157050ustar00rootroot00000000000000@c The GNU General Public License. @center Version 2, June 1991 @c This file is intended to be included within another document, @c hence no sectioning command or @node. @display Copyright @copyright{} 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. @end display @heading Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software---to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. @heading TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION @enumerate 0 @item This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The ``Program'', below, refers to any such program or work, and a ``work based on the Program'' means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term ``modification''.) Each licensee is addressed as ``you''. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. @item You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. @item You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: @enumerate a @item You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. @item You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. @item If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) @end enumerate These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. @item You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: @enumerate a @item Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, @item Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, @item Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) @end enumerate The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. @item You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. @item You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. @item Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. @item If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. @item If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. @item The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and ``any later version'', you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. @item If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. @iftex @end enumerate @heading NO WARRANTY @enumerate 10 @end iftex @ifinfo @center NO WARRANTY @end ifinfo @item BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM ``AS IS'' WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. @item IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. @end enumerate @iftex @heading END OF TERMS AND CONDITIONS @end iftex @ifinfo @center END OF TERMS AND CONDITIONS @end ifinfo @page @heading Appendix: How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the ``copyright'' line and a pointer to where the full notice is found. @smallexample @var{one line to give the program's name and a brief idea of what it does.} Copyright (C) @var{yyyy} @var{name of author} This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. @end smallexample Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: @smallexample Gnomovision version 69, Copyright (C) @var{year} @var{name of author} Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. @end smallexample The hypothetical commands @samp{show w} and @samp{show c} should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than @samp{show w} and @samp{show c}; they could even be mouse-clicks or menu items---whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a ``copyright disclaimer'' for the program, if necessary. Here is a sample; alter the names: @example Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. @var{signature of Ty Coon}, 1 April 1989 Ty Coon, President of Vice @end example This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. Turnin-NG-1.3/doc/layout.txt000066400000000000000000000007751237736160600160050ustar00rootroot00000000000000Suggested layout for project and turnin. The path /srv/submit may be changed to whatever suits your fancy. /etc/turnin-ng.cf /srv/submit/course1/project1/ project2/ project3/ turnin-ng.cf course2/project1/ project2/ project3/ turnin-ng.cf ...... courseZ/project1/ .... projectX/ turnin-ng.cf Turnin-NG-1.3/doc/project.1000066400000000000000000000000251237736160600154430ustar00rootroot00000000000000.so man1/turnincfg.1 Turnin-NG-1.3/doc/turnin-ng.texi000066400000000000000000000364661237736160600165510ustar00rootroot00000000000000\input texinfo @c -*-texinfo-*- @setfilename turnin-ng.info @settitle Turnin-NG @syncodeindex pg cp @comment Commented out allowcodebreaks because it breaks on older versions of @comment texi2pdf and there aren't any differences in output with or without it. @comment @allowcodebreaks false @set VERSION 1.3 @set UPDATED August 27 2014 @set UPDATED-MONTH August 27 2014 @comment %**end of header @copying This manual is for Turnin-NG (version @value{VERSION}, @value{UPDATED}). Copyright @copyright{} 2009--2014 Ryan Kavanagh (@email{rak@@debian.org}). @quotation Copying and distribution of this file, with or without modification, are permitted in any medium without royalty provided the copyright notice and this notice are preserved. @end quotation @end copying @dircategory Turnin-NG Assignment Submission Suite @direntry * Turnin-NG: (turnin-ng). The Turnin-NG Assignment Submission Suite @end direntry @titlepage @title Turnin-NG @subtitle Assignment Submission Suite @subtitle Version @value{VERSION}, @value{UPDATED-MONTH} @author Ryan Kavanagh (@email{rak@@debian.org}) @page @vskip 0pt plus 1filll @insertcopying @end titlepage @contents @ifnottex @node Top, Introduction to Turnin-NG, (dir), (dir) @top Turnin-NG @ifinfo This manual is for @code{Turnin-NG} (version @value{VERSION}, @value{UPDATED}). @end ifinfo @end ifnottex @ifhtml @insertcopying @end ifhtml @ifnottex @menu * Introduction to Turnin-NG:: * turnin:: * turnincfg:: * Distributing Turnin-NG:: @end menu @end ifnottex @node Introduction to Turnin-NG, turnin, Top, Top @comment node-name, next, previous, up @chapter Introduction to Turnin-NG Turnin-NG is an assignment submission suite written in Python and composed of @command{turnin} and @command{turnincfg}. Students can use the @command{turnin} command to submit an assignment to a course. Professors and @acronym{TAs, teaching assistants} can use @command{turnincfg} to manage submitted assignments, making them easier to grade. @node turnin, turnincfg, Introduction to Turnin-NG, Top @chapter turnin @command{turnin} compresses your assignments using the @command{tar} and @command{gzip} formats before submitting them to be graded. If you feel so inclined, @command{turnin} can cryptographically sign your assignments using @command{GnuPG} to prevent someone from submitting an assignment in your name. @menu * Invoking turnin:: * Making turnin easier to use:: * Common situations for turnin:: @end menu @node Invoking turnin, Making turnin easier to use, turnin, turnin @section Invoking turnin @command{turnin} was designed with ease of use in mind, all while retaining the functionality of the original @command{turnin} command. The command line format for turnin is as follows. @code{ @command{turnin} [ -vhlw ] [ --version ] [ -p @var{project} ] [ -C @var{config} ] [ -k @var{keyid} ] -c @var{course} files } You must provide the course name and a list of files to submit. @table @code @item @option{-v}, @option{--verbose} Print a list of submitted files once they have been submitted. @item @option{-h}, @option{--help} Print a help message. @item @option{-l}, @option{--list} Print a list of projects, along with whether or not they are enabled and if the project is the default project. @item @option{--version} Print turnin's version. @item @option{-p}, @option{--project} Set the project to which we'll submit our assignments. The project must be enabled. If we do not pass this option, @command{turnin} will submit to the @key{Default} project listed by the @option{-l} or @option{--list} option. @item @option{-C}, @option{--config} Path to an alternate configuration file. @item @option{-k}, @option{--keyid} Cryptographically sign an assignment using @command{gpg}. @item @option{-c}, @option{--course} Set the course to which we'll submit our assignments. @item @option{-w}, @option{--legal} Print warranty and license information. @item @file{files} The files and directories we want to submit @end table @node Making turnin easier to use, Common situations for turnin, Invoking turnin, turnin @section Making turnin easier to use You can use your shell's built in @command{alias} command to reduce the typing required when using @command{turnin}. These aliases should be appended to your shell's configuration file (@file{$HOME/.profile} for Bourne compatible shells). If you are only in one course, you could use the following alias: @example @code{alias turnin='turnin -c YOUR_COURSE'} @end example If you are in multiple courses, the following aliases may be useful: @example @code{alias turnin-COURSE1='turnin -c COURSE1'} @code{alias turnin-COURSE2='turnin -c COURSE2'} @code{# ...} @code{alias turnin-COURSEX='turnin -c COURSEX'} @end example If you want to always sign your assignments before submitting them, you could use an alias like this one: @example @code{alias turnin='turnin -k E95EDDC9'} @end example Just make sure to replace the text in capitals with the appropriate information and 'E95EDDC9' with your GnuPG key id. @node Common situations for turnin, , Making turnin easier to use, turnin @section Examples You want to submit an assignment, the files @file{bubblesort.hs} and @file{radixsort.hs} to the course `cisc2323', but you aren't sure of the project name. You start by finding the project list. To do this, you need to pass the @option{-l} option to turnin, along with the required @option{-c @var{coursename}} argument. @smallexample @code{ ryanakca@@zeus:~$ turnin -c cisc2323 -l ------------------------------------------------------------------------------------ | Enabled | Project | Description | ------------------------------------------------------------------------------------ | False | searching | Implement a binary search | | True | haskell_sort | Implement the bubble sort and the radix sort in Haskell | | Default | bake | Bake cookies for the professor | | True | museum_guard | Write a paper about the museum guard problem | | False | packing | Implement a packing algorithm | ------------------------------------------------------------------------------------} @end smallexample You see that you want the @option{haskell_sort} project. Since it is not the default project, we will have to pass the @option{-p @var{project}} option, in this case, @option{-p @var{haskell_sort}}, to turnin when we invoke it. We now proceed to submit our assignments. @smallexample @code{ ryanakca@@zeus:~$ turnin -c cisc2323 -p haskell_sort bubblesort.hs radixsort.hs Successfully submitted your assignment for grading. } @end smallexample If we want to make sure @command{turnin} did not forget a file, we can pass the @option{-v} or @option{--verbose} option when we invoke it. @smallexample @code{ ryanakca@@zeus:~$ turnin -c cisc2323 -p haskell_sort -v bubblesort.hs radixsort.hs Submitted files: sort-ryanakca sort-ryanakca/bubblesort.hs sort-ryanakca/radixsort.hs } @end smallexample We decide that we had wanted to sign our submission, we can do so by passing the @option{-k @var{KEYID}}. @smallexample @code{ ryanakca@@zeus:~$ turnin -c cisc-2323 -p haskell_sort -kD295EAC1 \ > bubblesort.hs radixsort.hs You need a passphrase to unlock the secret key for user: "Ryan Kavanagh " 1024-bit RSA key, ID D295EAC1, created 2009-07-13 gpg: gpg-agent is not available in this session Successfully submitted your assignment for grading. } @end smallexample If the project is still enabled and we want to resubmit our assignment, we may do so by rerunning the @command{turnin} command. @node turnincfg, Distributing Turnin-NG, turnin, Top @chapter turnincfg Turnin-NG's @command{turnincfg} command retains most of the functionality of the original @command{project} command and seeks to make assignment management as easy as possible. To make file management and storage as easy and efficient as possible, each submission is compressed using the @command{tar} and @command{gzip} formats. @menu * Invoking turnincfg:: * Common situations for turnincfg:: @end menu @node Invoking turnincfg, Common situations for turnincfg, turnincfg, turnincfg @section Invoking turnincfg The author of Turnin-NG strived to make @command{turnincfg} as easy to use, all while retaining the used functionality of the original @command{project} command. The command line format for turnincfg is as follows. @code{ @command{turnincfg} [ -hdelripxvw ] [--version] [ -C @var{config} ] [ -c @var{course} ] [ --create-course @var{course} ] [ --delete-course @var{course} ] [ --archive-course @var{course} ] [ @var{project name} ] } @table @code @item @option{--version} Print @command{turnincfg}'s version. @item @option{-h}, @option{--help} Print a help message. @item @option{-D}, @option{--perm-disable} Permanently disable the current project and strip all unique suffixes. You will generally want to use this option instead of @option{--disable}. @item @option{-d}, @option{--disable} Disable the current project. @item @option{-e}, @option{--enable} Enable submissions for the current project and make it the default project. @item @option{-l} Enable submissions for the current project but don't make it default. @item @option{-r}, @option{--remove} Remove the current project and all associated files. @item @option{-i}, @option{--init} Initialize this project. @item @option{-p}, @option{--compress} Compress this project using the @command{tar} and @command{gzip} formats. @item @option{-x}, @option{--extract} Extract this project. @item @option{-v}, @option{--verify} Verify @command{GnuPG} signatures on submitted projects. @item @option{-C}, @option{--config} Path to an alternate configuration file. The default configuration file is @file{/etc/turnin-ng.cf}. @item @option{-w}, @option{--legal} Print warranty and license information. @item @option{--create-course} Create a course. @item @option{--delete-course} Delete a course and all associated files. @item @option{--archive-course} Archive a course using the @command{tar} and @command{gzip} formats to create a @file{.tar.gz} file. @end table @node Common situations for turnincfg, , Invoking turnincfg, turnincfg @section Administration A course (or many!) needs to be created by an administrator before users can use the @command{turnincfg} and @command{turnin} commands. There are two styles of course management: user managed and group managed. With user managed courses, all project management actions are done with a single account. With group management, project management actions are done by accounts in a UNIX group. The original @command{project} and @command{turnin} commands only supported user managed courses. Due to the limited granularity of UNIX permissions, group managed courses permit submissions from all accounts. The following example shows the creation of a user managed course. @smallexample @code{ root@@zeus:~$ turnincfg --create-course cisc2323 Managing username [usually your UNIX login]: ryanakca Full path to the course directory: /srv/submit/cisc2323 Managed by a User or Group [U/G]: U Student group: students Please make sure the account ryanakca is a member of the group students. Successfully created the course cisc-2323. } @end smallexample This creates a directory with the permissions and contents: @smallexample @code{ ryanakca@@zeus:~$ ls -lash /srv/submit/cisc2323 total 4,0K 0 drwxr-xr-x 2 ryanakca students 60 19 Aug 13:01 ./ 0 drwxrwxrwt 17 root root 400 19 Aug 13:12 ../ 4,0K -rw-r--r-- 1 ryanakca students 134 19 Aug 13:01 turnin-ng.cf } @end smallexample Submission subdirectories for enabled projects will have the permissions @code{0770} and an ownership of @code{MANAGING_USERNAME:STUDENT_GROUP}. Once disabled, they will have the permissions @code{0700}. The following example shows the creation of a group managed course. In this case, we'll set the managing username to the professor's account and the managing group to the group containing the course's professor and TAs. @smallexample @code{ root@@zeus:~$ turnincfg --create-course cisc2323 Managing username [usually your UNIX login]: ryanakca Full path to the course directory: /srv/submit/cisc2323 Managed by a User or Group [U/G]: G Managing group: cisc-2323 Successfully created the course cisc-2323. } @end smallexample This creates a directory with the permissions and contents: @smallexample @code{ total 4,0K 0 drwxrwxr-x 2 ryanakca cisc-2323 60 19 Aug 13:25 ./ 0 drwxrwxrwt 18 root root 420 19 Aug 13:25 ../ 4,0K -rw-rw-r-- 1 ryanakca cisc-2323 121 19 Aug 13:25 turnin-ng.cf } @end smallexample Submission subdirectories for enabled projects will have the permissions @code{0777} and an ownership of @code{MANAGING_USERNAME:MANAGING_GROUP}. Once disabled, they will have the permissions @code{0770}. Once a course has finished, you may remove it from the global configuration file and delete all files in its directory by running the command @command{turnincfg --delete-course @var{COURSENAME}}. If you would rather keep its files and still remove it from the global configuration file, you can run @command{turnincfg --archive-course @var{COURSENAME}}. This will create a @file{.tar.gz} file containing the course directory. @section Examples You're the professor for the course cisc2323 and want to create the project `haskell_sort' for your students, enable submissions for it and make it the default project (the default project is the project to which @command{turnin} submits assignments if the student doesn't pass the @option{--project @var{PROJECT}} option when invoking @command{turnin}). @smallexample @code{ ryanakca@@zeus:~$ turnincfg -i haskell_sort [Optional] Project description: Implement a bubble sort and the radix sort using Haskell Successfully created the project haskell_sort in the course cisc2323 ryanakca@@zeus:~$ turnincfg -e haskell_sort Successfully enabled and set default the project haskell_sort } @end smallexample In the previous example, we created the project @file{haskell_sort} by running the command @command{turnincfg -i haskell_sort}. Finally, we enabled it and set it default by running @command{turnincfg -e haskell_sort}. We want to close submissions for the project `haskell_sort' and compress it at 24:00 UTC, September 15 2009. We can do this using the @command{at} command. @smallexample @code{ ryanakca@@zeus:~$ at 2400 UTC Sep 15 2009 warning: commands will be executed using /bin/sh at> turnincfg -c cisc2323 at> turnincfg -D haskell_sort at> turnincfg -p haskell_sort job 92 at Tue Sep 15 19:00:00 2009 } @end smallexample For more details on @command{at}'s time format, please see it's @command{man} page. In this case we passed the @command{turnincfg -D haskell_sort} command instead of the @command{turnincfg -d haskell_sort} command since we do not want the long suffixed file names in the archive, we want them in the @file{username.tar.gz} format. @node Distributing Turnin-NG, , turnincfg, Top @chapter Distributing Turnin-NG Turnin-NG is Copyright @copyright{} 2009--2010 Ryan Kavanagh (@email{rak@@debian.org}). It is licensed under the terms of the GNU General Public License version 2, or (at your option) any later version. @menu * GNU General Public License version 2:: @end menu @node GNU General Public License version 2, , , Distributing Turnin-NG @section GNU General Public License version 2 @include gpl-2.0.texi @comment @node Index @comment @unnumbered Index @comment @printindex cp @bye Turnin-NG-1.3/doc/turnin-ng.texi.init000066400000000000000000000000531237736160600174720ustar00rootroot00000000000000$region_formats_kept{'insertcopying'} = 1; Turnin-NG-1.3/doc/turnin.1000066400000000000000000000043661237736160600153300ustar00rootroot00000000000000.TH TURNIN 1 "SEPTEMBER 2010" Turnin-NG .SH NAME turnin \- submit assignments to be graded .SH SYNOPSYS .B turnin [ .I OPTIONS .B ] -c [ .I course-name .B ] files .SH DESCRIPTION Submits assignments to be graded. We submit to the default project if it is not specified as an option. .SH REQUIRED ARGUMENTS .IP "\-c, \-\-course" Sets the course to which we'll submit our assignments. .IP files A list of files we wish to submit. They will be compressed using .B tar(1) and .B gzip(1) formats before being copied to the submission directory. .SH OPTIONS .IP \-\-version Print turnin's version .IP "\-h, \-\-help" Print a help message .IP "\-l, \-\-list" Prints a list of projects, along with whether or not they are enabled and shows which project is the default project. .IP "\-p, \-\-project" Sets the project to which we'll submit our assignments. .IP "\-C, \-\-config" Path to an alternate configuration file .IP "\-v, \-\-verbose" Prints a list of submitted files once they have been submitted. .IP "\-k, \-\-keyid" Cryptographically signs an assignment using .B gpg(1) . .IP "\-w, \-\-legal" Print warranty and license information. .SH EXAMPLES .TP \w'turnin\ 'u .BI turnin\ \-c \ world-cheeses \ \-\-list List the projects in the course world-cheeses. .TP .BI turnin\ \-c \ world-cheeses\ stilton-recipe Submit the file stilton-recipe to the default project in the course world-cheeses. .TP .BI turnin\ \-c \ world-cheeses \ \-p \ stilton\ stilton-recipe Submit the file stilton-recipe to the project stilton in the course world-cheeses. .SH FILES .I /etc/turnin-ng.cf .I ~/.turnin-ng/submissions .SH SEE ALSO .B turnincfg(1) The full documentation for .B turnin is maintained as a Texinfo manual. If the .B info and .B turnin programs are properly installed at your site, the command .IP .B info turnin-ng .PP should give you access to the complete manual. .SH AUTHOR Turnin-NG and this manpage were written by Ryan Kavanagh in the summer of 2009 and are still actively maintained. Turnin-NG is a replacement for the 'project' and 'turnin' commands written in 1990 for the SPARC architecture by an unknown author. Both Turnin-NG and this manpage are distributed under the terms of the GNU General Public License version 2, or (at your option) any later version. Turnin-NG-1.3/doc/turnin.cf000066400000000000000000000046071237736160600155560ustar00rootroot00000000000000# Turnin-NG, an assignment submitter and manager. --- Example config file # Copyright (C) 2009 Ryan Kavanagh # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # This is an example configuration file. It doesn't need to be copied, it will # be automatically created on project's first run. ## Original turnin.cf: # # $Id: turnin.cf,v 5.19 90/09/18 15:12:38 ksb Exp $ # # The file turnin.cf consists of a line for each course with the following # # information separated by colons: The course name, the id for the account # # the data should be put into, the subdirectory name in which the data should # # be kept, the group which should own the files after they have been # # submitted, and a comma-separated list of the sections for this course. # test:rountree:submit:sysstaff:ALL # cisc221:cisc221t:submit:cisc221t:ALL # cisc236:cisc236:submit:cisc236:ALL # cisc332:wendy:submit:cisc332:ALL # cisc432:cisc432t:submit:cisc432t:ALL # cisc434:cisc434:submit:cisc434:ALL # cisc435:cisc435:submit:cisc435:ALL # cisc454:cisc454t:submit:cisc454t:ALL # cisc458:cisc458:submit:cisc458t:ALL # We will use the following format: [Global] default = 'course1' [course1] user = 'professor1' directory = '/srv/submit/course1/' group = 'course1_students' sections = '' [[project1]] enabled = True description = "Course1's first project." [[project2]] enabled = False description = "Course1's second project." [course2] user = 'professor2' directory = '/srv/submit/course2/' group = 'course2_students' sections = '' [[project1]] enabled = False description = "Course2's first project." [[project2]] enabled = True description = "Course2's second project." Turnin-NG-1.3/doc/turnincfg.1000066400000000000000000000066131237736160600160050ustar00rootroot00000000000000.TH TURNINCFG 1 "SEPTEMBER 2010" Turnin-NG .SH NAME turnincfg \- manage assignments submitted with turnin .SH SYNOPSYS .B turnincfg [ .I OPTIONS .B ] [ .I project-name .B ] .SH DESCRIPTION Manages assignments submitted with the .B turnin(1) program. Allows the creation of courses and projects and the management of said courses and projects. This command used to be known as .B project in the early days of Turnin-NG and in the original turnin/project suite. Omitting .B [ .I project-name .B ] will cause .B turnincfg to use the default project. .SH OPTIONS .IP \-\-version Print project's version. .IP "\-h, \-\-help" Print these options. .IP "\-d, \-\-disable" Disable the current project. .IP "\-D, \-\-perm-disable" Permanently disable submissions for the current project. .IP "\-e, \-\-enable" Enable submissions for the current project and make it the default project. .IP "\-l" Enable submissions for the current project but does not make it the default project. .IP "\-r, \-\-remove" Remove the current project and all associated files. .IP "\-i, \-\-init" Initialize a project. Requires you to pass .I project-name .IP "\-p, \-\-compress" Compress this project using the .B tar(1) and .B gzip(1) formats. .IP "\-x, \-\-extract" Extract this project. This option will be called automatically if you enable a compressed project. .IP "\-v, \-\-verify" Verify the cryptographic signatures on submitted assignments using .B gpg(1) May require you to fetch the student's public key. .IP "\-\-list" Print a list of projects, along with whether or not they are enabled and shows which project is the default project. .IP "\-\-config=CONFIG" Path to an alternate configuration file. .IP "\-c COURSE, \-\-course=COURSE" Switch between the courses you are currently administering. .IP "\-w, \-\-legal" Print warranty and license information. .B "ADMINISTRATIVE OPTIONS" .IP "\-\-create-course=COURSE" Creates the course COURSE. .IP "\-\-delete-course=COURSE" Deletes the course COURSE. .IP "\-\-archive-course=COURSE" Archives the course using the .B tar(1) and .B gzip(1) formats. .SH EXAMPLES .TP \w'project\ 'u .BI turnincfg\ \-\-create-course \ world-cheeses Create the course world-cheeses. .TP .BI turnincfg\ \-c \ world-cheeses \ \-i \ stilton Create the project stilton in the course world-cheeses. .TP .BI turnincfg\ \-c \ world-cheeses \ \-e \ stilton Enable and make default the project stilton .TP .BI turnincfg\ \-c \ world-cheeses \ \-D \ stilton Permanently disable submissions for the project stilton. .TP .BI turnincfg\ \-c \ world-cheeses \ \-p \ stilton Compress the project stilton. .TP .BI turnincfg\ \-\-archive-course \ world-cheeses Compress the course world-cheeses and remove it from the global configuration file. .SH FILES .I /etc/turnin-ng.cf .SH SEE ALSO .B turnin(1) The full documentation for .B turnincfg is maintained as a Texinfo manual. If the .B info and .B turnincfg programs are properly installed at your site, the command .IP .B info turnin-ng .PP should give you access to the complete manual. .SH AUTHOR Turnin-NG and this manpage were written by Ryan Kavanagh in the summer of 2009 and are still actively maintained. Turnin-NG is a replacement for the 'project' and 'turnin' commands written in 1990 for the SPARC architecture by an unknown author. Both Turnin-NG and this manpage are distributed under the terms of the GNU General Public License version 2, or (at your option) any later version. Turnin-NG-1.3/setup.py000066400000000000000000000170661237736160600146750ustar00rootroot00000000000000#!/usr/bin/python from distutils.command.build import build from distutils.command.install import install from distutils.core import Command, setup import distutils.sysconfig import os import os.path import re import shutil import subprocess import sys import tempfile # Default prefix prefix = '/usr/local' # Get the install prefix if one is specified from the command line for i, arg in enumerate(sys.argv): prefix_regex = re.compile('(?P--prefix)?[\=\s]?(?P/[\w\s/]*)') if prefix_regex.match(arg): if prefix_regex.match(arg).group('prefix') and not prefix_regex.match(arg).group('path'): # We got --prefix with a space instead of an equal. The next arg will have our path. prefix = os.path.expandvars(prefix_regex.match(sys.argv[i+1]).group('path')) elif prefix_regex.match(arg).group('path'): prefix = prefix_regex.match(arg).group('path') elif (sys.argv[i-1] == '--prefix') and prefix_regex.match(arg).group('path'): prefix = os.path.expandvars(prefix_regex.match(arg).group('path')) data_files = [(os.path.join(prefix,'share/man/man1/'), ['doc/turnin.1', 'doc/turnincfg.1'])] def check_executable_in_path(executable, message): """ Checks that executable is installed in the system's PATH and returns it's location. """ if os.environ.has_key('PATH'): for directory in os.environ['PATH'].split(':'): if os.path.exists(os.path.join(directory, executable)): return os.path.join(directory, executable) else: for directory in ['/usr/bin', '/usr/local/bin']: if os.path.exists(os.path.join(directory, executable)): return os.path.join(directory, executable) print message return False class build_htmldocs(Command): description = 'Generate the HTML documentation.' user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): """ Call makeinfo on the Texinfo document. """ makeinfo=check_executable_in_path('makeinfo', "Please install the makeinfo executable to generate the HTML " + "documentation") if makeinfo: cargs = [makeinfo, '--html', '--no-split', '-odoc/turnin-ng.html',\ '--init-file=doc/turnin-ng.texi.init', 'doc/turnin-ng.texi'] retcode = subprocess.call(cargs) if retcode < 0: raise subprocess.CalledProcessError(retcode, ' '.join(cargs)) data_files.append((os.path.join(prefix, 'share/doc/turnin-ng/'), ['doc/turnin-ng.html'])) build.sub_commands.append(('build_htmldocs', None)) class build_infopage(Command): description = 'Generate the info document.' user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): """ Call makeinfo on the Texinfo document. """ makeinfo = check_executable_in_path('makeinfo', "Please install the makeinfo executable to generate the info document") if makeinfo: cargs = [makeinfo, '-o', 'doc/turnin-ng.info', 'doc/turnin-ng.texi'] retcode = subprocess.call(cargs) if retcode < 0: raise subprocess.CalledProcessError(retcode, ' '.join(cargs)) data_files.append((os.path.join(prefix, 'share/info/'), ['doc/turnin-ng.info'])) build.sub_commands.append(('build_infopage', None)) class build_pdf(Command): description = 'Generate the pdf document.' user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): """ Call makeinfo on the Texinfo document. """ makeinfo = check_executable_in_path('makeinfo', "Please install makeinfo to generate the PDF documentation") if makeinfo: tempdir = tempfile.mkdtemp() shutil.copy(os.path.join('doc', 'turnin-ng.texi'), tempdir) shutil.copy(os.path.join('doc', 'gpl-2.0.texi'), tempdir) doc = os.path.join(os.getcwd(), 'doc') os.chdir(tempdir) success = True cargs = [makeinfo, '--pdf', 'turnin-ng.texi'] # We need to call texi2pdf twice. for i in range(1): retcode = subprocess.call(cargs) if retcode < 0: success = False raise subprocess.CalledProcessError(retcode, ' '.join(cargs)) break if success: try: shutil.copy('turnin-ng.pdf', doc) shutil.rmtree(tempdir, ignore_errors=True) # This is required so that the install command can find the # build directory. Without it, it searches for it in the # non-existent tempdir. os.chdir(os.path.join(doc, os.pardir)) data_files.append((os.path.join(prefix, 'share/doc/turnin-ng/'), ['doc/turnin-ng.pdf'])) except: print 'An error has occured, skipping PDF documentation.' build.sub_commands.append(('build_pdf', None)) class build_legacy(Command): description = 'Include the legacy files from renaming project to turnincfg' user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): data_files.append((os.path.join(prefix, 'share/man/man1'), ['doc/project.1'])) build.sub_commands.append(('build_legacy', None)) class install_legacy(Command): description = 'Install the legacy symlink of project to turnincfg' user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): print os.path.join(prefix, 'bin/turnincfg') try: os.symlink(os.path.join(prefix, 'bin/turnincfg'), os.path.join(prefix, 'bin/project')) except: pass install.sub_commands.append(('install_legacy', None)) class install_infopage(Command): description = 'Install the info page' user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): install_info = check_executable_in_path('install-info', "Please install the install-info executable to install the info " + "documentation") if install_info: cargs = ['install-info', os.path.join(prefix, 'share/info/turnin-ng.info'), os.path.join(prefix, 'share/info/dir')] retcode = subprocess.call(cargs) if retcode < 0: raise subprocess.CalledProcessError(retcode, ' '.join(cargs)) install.sub_commands.append(('install_infopage', None)) setup(name='turnin-ng', version='1.3', description='Turn in your assignments with turnin', author='Ryan Kavanagh', author_email='rak@debian.org', license='GNU General Public License version 2, or (at your option) ' +\ 'any later version', scripts=['src/bin/turnincfg', 'src/bin/turnin'], packages=['turninng'], package_dir={'turninng':'src/turninng'}, data_files=data_files, cmdclass={'build_infopage': build_infopage, 'build_pdf':build_pdf, 'build_htmldocs': build_htmldocs, 'build_legacy': build_legacy, 'install_infopage': install_infopage, 'install_legacy': install_legacy} ) Turnin-NG-1.3/src/000077500000000000000000000000001237736160600137405ustar00rootroot00000000000000Turnin-NG-1.3/src/bin/000077500000000000000000000000001237736160600145105ustar00rootroot00000000000000Turnin-NG-1.3/src/bin/turnin000066400000000000000000000104031237736160600157500ustar00rootroot00000000000000#!/usr/bin/python2.5 # Turnin-NG, an assignment submitter and manager. --- Turnin script # Copyright (C) 2009-2010 Ryan Kavanagh # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from optparse import OptionParser, OptionGroup import os.path import sys import turninng from turninng.configparser import TurninGlobal, TurninCourse, TurninProject from turninng.submitter import submit_files, list_projects if __name__ == '__main__': usage = '%prog [options] [files]' parser = OptionParser(version=turninng.__version__, usage=usage) parser.add_option('-c', '--course', help='Set the course to submit the ' + 'assignment to.') parser.add_option('-l', '--list', help='Lists projects for the course. ' + 'also displays whether or not the project is open.', action='store_true') parser.add_option('-p', '--project', help='Set the project to submit the '+ 'assigmnent to.') parser.add_option('-C', '--config', help='Set a custom configuration ' + 'file.') parser.add_option('-k', '--keyid', help='Cryptographically sign this ' + 'assignment using this key.') parser.add_option('-v', '--verbose', action='store_true', help='Print ' + 'a list of submitted files after submitting.') parser.add_option('-w', '--legal', action='store_true', help='Print warranty and license information.') parser.set_defaults(config=os.path.join('/etc', 'turnin-ng.cf')) (options, args) = parser.parse_args() if options.legal: sys.exit(turninng.__license__) config = options.config # If we're listing projects, it's obvious we won't be submitting to one. if not options.list and not args: print ValueError("Error, please submit at least one document.") parser.print_help() sys.exit(ValueError('')) if not options.course: sys.exit(ValueError("Error, please specify a course.")) try: # We're listing the courses: course = TurninCourse(TurninCourse(config, options.course).course['projlist'], options.course) if options.list: projects = list_projects(course.config.filename, options.course) for i in projects: print i sys.exit() except ValueError, e: sys.exit(e) try: # We're creating after having run list because if the user is asking for # a list of projects, the user won't be passing a project. if options.project: project = TurninProject(course.config.filename, options.course, options.project) else: if course.course.has_key('default') and course.course['default']: project = TurninProject(course.config.filename, options.course, course.course['default']) else: raise ValueError("It appears that this course does not have a "+ "default project. You will have to specify one " + "yourself using the '-p' or '--project' option.") except ValueError, e: sys.exit(e) if not project.project['enabled']: sys.exit(ValueError("Error, project %s is not enabled." % project.name)) # We have checked that the user has provided a course, a project and # assignments. We can now proceed to submit them. files = submit_files(project, args, gpg_key=options.keyid) if options.verbose: print "Submitted files:" for file in files: print file sys.exit() else: sys.exit("Successfully submitted your assignment for grading.") Turnin-NG-1.3/src/bin/turnincfg000066400000000000000000000270641237736160600164430ustar00rootroot00000000000000#!/usr/bin/python2.5 # Turnin-NG, an assignment submitter and manager. --- Project script # Copyright (C) 2009-2011 Ryan Kavanagh # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import errno from optparse import OptionParser, OptionGroup import os import os.path import re import sys import turninng from turninng.configparser import ProjectGlobal, ProjectCourse, ProjectProject from turninng.coursemanage import create_course, delete_course, archive_course from turninng.fileperms import chmod from turninng.projectmanage import create_project, delete_project, verify_sig from turninng.projectmanage import compress_project, extract_project from turninng.projectmanage import strip_random_suffix from turninng.submitter import list_projects if __name__ == '__main__': usage = '%prog [options] [project name]' parser = OptionParser(version=turninng.__version__, usage=usage) parser.add_option('-d', '--disable', action='store_false', dest='enabled', help='Disable submissions for the current project.') parser.add_option('-D', '--perm-disable', action='store_true', dest='pdisable', help='Permanently disable submissions for the current project.') parser.add_option('-e', '--enable', action='store_true', dest='enabled', help='Enable submissions for the current project and make it ' + 'the default project.') parser.add_option('-l', dest='enabled_nodefault', action='store_true', help="Enable submissions for the current project but don't make " + "it default.") parser.add_option('-r', '--remove', action='store_true', dest='remove', help='Remove the current project and all associated files.') parser.add_option('-i', '--init', action='store_true', dest='init', help='Initialize this project.') parser.add_option('-p', '--compress', action='store_true', help='Compress this project.') parser.add_option('-x', '--extract', action='store_true', help='Extract this project.') parser.add_option('-v', '--verify', action='store_true', help='Verify signatures on submitted projects.') parser.add_option('--list', help='Lists projects for the course. ' + 'Also displays wether or not the project is enabled.', action='store_true') parser.add_option('-c', '--course', help='Course name') # parser.add_option('-v', '--verbose', action='store_true', dest='verbose', # help='Verbose. Print shell commands as they are executed.') parser.add_option('-C', '--config', help='Use an alternate config file.') parser.add_option('-w', '--legal', action='store_true', help='Print warranty and license information.') admin = OptionGroup(parser, "Administrative options", "These options can add or remove courses, etc.") admin.add_option('--create-course', help='Creates a course.') admin.add_option('--delete-course', help='Deletes a course.') admin.add_option('--archive-course', help='Archive a course.') parser.add_option_group(admin) parser.set_defaults(config=os.path.join('/etc', 'turnin-ng.cf')) (options, args) = parser.parse_args() config = options.config # We're moving away from the Uber-generic "project" command if re.match('.*project', sys.argv[0]): print ( """ ***************************************************************** * WARNING: The 'project' command is now deprecated, this legacy * * link to the new executable, turnincfg, may disappear in any * * future release. * *****************************************************************""") if options.legal: sys.exit(turninng.__license__) # Administrative functions : if options.create_course: try: create_course(config, options.create_course) sys.exit("Successfully created the course %s." % options.create_course) except ValueError, e: sys.exit(e) if options.delete_course: try: delete_course(config, options.delete_course) sys.exit("Successfully deleted the course %s." % options.delete_course) except ValueError, e: sys.exit(e) if options.archive_course: try: path = archive_course(config, options.archive_course, ret_path=True) sys.exit("Successfully archived the course %s to %s." % (options.archive_course, path)) except ValueError, e: sys.exit(e) except OSError, e: if e.errno == errno.ENOENT: print "Has someone already moved the submission directory?" sys.exit(e) # End user functions : if options.course: if ProjectGlobal(config).config.has_key(options.course): # From now on we want to use the projlist as the config file config = ProjectCourse(config, options.course).course['projlist'] default_course = options.course else: sys.exit("The course %s does not exist." % options.course) else: parser.print_help() sys.exit("Please specify your course using the '-c course' or " + "'--course=COURSE' options.") # List projects if options.list: try: projects = list_projects(config, default_course) for i in projects: print i sys.exit() except ValueError, e: sys.exit(e) # Let's set the project_name variable if len(args) > 1: raise ValueError("Error, please pass one project name at a time.") elif len(args) == 1: project_name = args[0] elif ProjectCourse(config, default_course).course['default']: project_name = ProjectCourse(config, default_course).course['default'] else: print "Please specify the project or set a default project." sys.exit(parser.print_help()) # Create, delete or (un)compress the project if needed before creating an # object if options.init: create_project(config, default_course, project_name) sys.exit("Successfully created the project %s in the course %s." % (project_name, default_course)) elif options.remove: try: delete_project(config, default_course, project_name) sys.exit("Successfully deleted the project %s." % project_name) except ValueError, e: sys.exit(e) # Compress or decompress try: if options.compress: compress_project(config, default_course, project_name) sys.exit("Successfully compressed the project %s." % project_name) elif options.extract: extract_project(config, default_course, project_name) sys.exit("Successfully extracted the project %s." % project_name) except ValueError, e: sys.exit(e) # Enable submissions for a project if options.enabled: if ProjectCourse(config, default_course).course.has_key(project_name): project = ProjectProject(config, default_course, project_name) else: sys.exit(ValueError('Course %s does not have the project %s.' % (default_course, project_name))) if project.project.has_key('tarball') and project.project['tarball']: extract_project(config, default_course, project_name) print "Extracted the project %s." % project_name if project.course['group_managed']: # Set the directory writable by everybody chmod(project.project['directory'], 0773) else: # Set the directory writable by the student group. chmod(project.project['directory'], 0730) project.write(True, default=True) sys.exit("Successfully enabled and set default the project %s." % project_name) elif options.enabled == False: # Disable it. I know, using '== False' is # bad according to PEP 8. However, using 'elif not options.enabled' will # return true even if the user didn't pass the option since # options.enabled will be None. We could check for != None, but I'd # rather be explicit than implicit ;) # This chmod line is to prevent students from manually uploading # assignments to a project once it's disabled. if ProjectCourse(config, default_course).course.has_key(project_name): project = ProjectProject(config, default_course, project_name) else: sys.exit(ValueError('Course %s does not have the project %s.' % (default_course, project_name))) if project.course['group_managed']: chmod(project.project['directory'], 0770) else: chmod(project.project['directory'], 0700) project.write(False) sys.exit("Successfully disabled the project %s." % project_name) elif options.enabled_nodefault: if ProjectCourse(config, default_course).course.has_key(project_name): project = ProjectProject(config, default_course, project_name) else: sys.exit(ValueError('Course %s does not have the project %s.' % (default_course, project_name))) if project.project.has_key('tarball') and project.project['tarball']: extract_project(config, default_course, project_name) print "Extracted the project %s." % project_name project.write(True) sys.exit("Successfully enabled the project %s." % project_name) elif options.verify: if ProjectCourse(config, default_course).course.has_key(project_name): project = ProjectProject(config, default_course, project_name) else: sys.exit(ValueError('Course %s does not have the project %s.' % (default_course, project_name))) try: unsigned = verify_sig(project) sys.exit("\n".join(unsigned)) except ValueError, e: sys.exit(e) elif options.pdisable: if ProjectCourse(config, default_course).course.has_key(project_name): project = ProjectProject(config, default_course, project_name) else: sys.exit(ValueError('Course %s does not have the project %s.' % (default_course, project_name))) if project.course['group_managed']: chmod(project.project['directory'], 0770) else: chmod(project.project['directory'], 0700) project.write(False) try: strip_random_suffix(project) except ValueError, e: sys.exit(e) sys.exit("Successfully disabled the project %s and " % project_name + "stripped random suffixes.") elif args and default_course: create_project(config, default_course, project_name) sys.exit("Successfully created the project %s in the course %s." % (project_name, default_course)) else: # We didn't get anything. parser.print_help() Turnin-NG-1.3/src/turninng/000077500000000000000000000000001237736160600156045ustar00rootroot00000000000000Turnin-NG-1.3/src/turninng/__init__.py000066400000000000000000000033611237736160600177200ustar00rootroot00000000000000# Turnin-NG, an assignment submitter and manager. # Copyright (C) 2009-2013 Ryan Kavanagh # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. __version__ = "1.3" __license__ = """Turnin-NG, an assignment submitter and manager. Copyright (C) 2009-2014 Ryan Kavanagh This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ __copyright__ = "Copyright (C) 2009-2014 Ryan Kavanagh " __author__ = "Ryan Kavanagh http://ryanak.ca/" Turnin-NG-1.3/src/turninng/configparser.py000066400000000000000000000336121237736160600206450ustar00rootroot00000000000000# Turnin-NG, an assignment submitter and manager. --- config parser # Copyright (C) 2009-2011 Ryan Kavanagh # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import errno import os.path import uuid from configobj import ConfigObj # The Project prefix represents classes that were used by 'project', now # 'turnincfg'. # The Turnin prefix represents classes that are used by 'turnin'. class ProjectGlobal(object): """ This class class represents the global configurations for the turnincfg command. """ def __init__(self, config_file): """ Initialize. We'll create the configuration file if it doesn't already exist. We'll also add the [Global] section if it isn't already present. @type config_file: string @param config_file: path to the project configuration file @rtype: None """ self.config = ConfigObj() self.config.filename = config_file self.config.indent_type = ' ' self.config.unrepr = True self.config.reload() self.config_defaults = {'default': ''} if not self.config.has_key('Global'): self.config['Global'] = self.config_defaults self.config.write() else: self.config_ref = self.config['Global'] self.upgrade_config() def upgrade_config(self): """ Update the configuration file from a previous release. """ for key, default in self.config_defaults.items(): if not self.config_ref.has_key(key): self.config_ref[key] = default try: self.config.write() except IOError, err: if err.errno == errno.EACCES: # We don't have write permissions, we're probably running as a # student, ignore. pass def set_default(self, course): """ Set the course 'course' as the default course for this professor. @type course: string @param course: Name of the course we want to set as default @rtype: None @raise ValueError: We try to set a non-existent course as default. """ if self.config.has_key(course): self.config['Global']['default'] = course self.config.write() else: raise ValueError("Please add the course %s first." % course) class ProjectAdminCourse(ProjectGlobal): """ This class represents a course object for turnincfg in the global configuration file. """ def __init__(self, config_file, course): """ Initialize the course. If it doesn't already exist, we'll create it. self.course will be a shortcut to access the course configurations. @type config_file: string @param config_file: path to the project configuration file @type course: string @param course: name of the course @rtype: None """ super(ProjectAdminCourse, self).__init__(config_file) self.config_defaults = {'projlist': '', 'user': '', 'directory': '', 'group': '', 'group_managed': False} if not self.config.has_key(course): self.config[course] = self.config_defaults self.config.write() else: self.config_ref = self.config[course] self.upgrade_config() self.course = self.config[course] """ @ivar: A shortcut to access the course configurations. """ def write(self, user='', directory='', group='', group_managed=False): """ Modifies the config file. @type user: string @param user: username that owns the course directory. @type directory: string @param directory: path to the course submission directory. @type group: string @param group: group that owns the course directory. @type group_managed: Bool @param group_managed: Course managed by a group instead of a user? @rtype: None """ if user: self.course['user'] = user if directory: self.course['directory'] = directory if group: self.course['group'] = group if group_managed != self.course['group_managed']: self.course['group_managed'] = group_managed self.course['projlist'] = os.path.join(self.course['directory'], 'turnin-ng.cf') self.config.write() projlist = ProjectCourse(self.course['projlist'], self.course.name) projlist.write(user = self.course['user'], directory = self.course['directory'], group = self.course['group'], group_managed = self.course['group_managed']) class ProjectCourse(ProjectGlobal): """ This class represents a course object for in the course configuration file (projlist) for turnincfg. """ def __init__(self, config_file, course): """ Initialize the course. If it doesn't already exist, we'll create it. self.course will be a shortcut to access the course configurations. @type config_file: string @param config_file: path to the project configuration file @type course: string @param course: name of the course @rtype: None """ super(ProjectCourse, self).__init__(config_file) self.config_defaults = {'default': '', 'user': '', 'directory': '', 'group': '', 'group_managed': False} if not self.config.has_key(course): self.config[course] = self.config_defaults self.config.write() else: self.config_ref = self.config[course] self.upgrade_config() self.course = self.config[course] """ @ivar: A shortcut to access the course configurations. """ def set_default(self, project): """ Set the project 'project' as the default project for this course. @type project: string @param project: Name of the project we want to set as default @rtype: None @raise ValueError: We try to set a non-existent project as default. """ if self.course.has_key(project): self.course['default'] = project self.config.write() else: raise ValueError("Please add the project %s first." % project) def write(self, user='', directory='', group='', group_managed=False): """ Modifies the config file. @type user: string @param user: username that owns the course directory. @type directory: string @param directory: path to the course submission directory. @type group: string @param group: group that owns the course directory. @rtype: None """ if user: self.course['user'] = user if directory: self.course['directory'] = directory if group: self.course['group'] = group if group_managed != self.course['group_managed']: self.course['group_managed'] = group_managed self.config.write() class ProjectProject(ProjectCourse): """ This class represents a project object for turnincfg. """ def __init__(self, config_file, course, project): """ Initialize the project. If it doesn't already exist, we'll create it. self.project will be a shortcut to access the project configurations. @type config_file: string @param config_file: path to the project configuration file @type course: string @param course: name of the course @type project: string @param project: name of the project. @rtype: None """ super(ProjectProject, self).__init__(config_file, course) self.config_defaults = {'enabled': False, 'description': '', 'uuid': str(uuid.uuid4()), 'managing_accounts': []} if not self.course.has_key(project): self.config[course][project] = self.config_defaults self.config.write() else: self.config_ref = self.config[course][project] self.upgrade_config() self.project = self.course[project] """ @ivar: shortcut to the project configurations """ self.project['directory'] = os.path.join(self.course['directory'], project) self.name = project def write(self, enabled, description='', directory='', tarball='', default=False): """ Modifies the config file. @type enabled: Bool @param enabled: Is this course enabled? True/False @type description: string @param description: Optional description for this project. @type directory: string @param directory: project directory @type tarball: string @param tarball: Path to the compressed project's tarball. @rtype: None """ self.project['enabled'] = enabled if description: self.project['description'] = description if default: super(ProjectProject, self).set_default(self.name) if directory: self.project['directory'] = directory if tarball: self.project['tarball'] = tarball if not enabled: if self.course['default'] == self.name: self.course['default'] = '' self.config.write() def add_manager(self, manager): """ Adds a UNIX username that has managed the projects. @type manager: string @param manager: username that has managed accounts @rtype: None """ if manager not in self.project['managing_accounts']: self.project['managing_accounts'].append(manager) self.config.write() self.config.reload() class TurninGlobal(object): """ This class class represents the global configurations for the turnin command. """ def __init__(self, config_file): """ Initialize the global configurations. @type config_file: string @param config_file: path to the project configuration file @rtype: None @raise ValueError: The user is trying to use a poorly formatted configuration file. """ self.config = ConfigObj() self.config.filename = config_file self.config.indent_type = ' ' self.config.unrepr = True self.config.reload() if not self.config.has_key('Global'): raise ValueError("Invalid config file") class TurninCourse(TurninGlobal): """ This class represents a course object for turnin. """ def __init__(self, config_file, course): """ Initialize the turnin course object. self.course is an alias to access the course configurations. @type config_file: string @param config_file: path to the project configuration file @type course: string @param course: name of the course @rtype: None @raise ValueError: The course isn't defined in the config file. """ super(TurninCourse, self).__init__(config_file) if not self.config.has_key(course): raise ValueError("Course %s does not exists!" % course) self.course = self.config[course] """ @ivar: shortcut to the course configurations. """ class TurninProject(TurninCourse): """ This class represents a turnin course's project object. """ def __init__(self, config_file, course, project): """ Initialize the project's configurations for turnin. self.project will be a shortcut to access the project configurations. @type config_file: string @param config_file: path to the project configuration file @type course: string @param course: name of the course @type project: string @param project: name of the project. @rtype: None @raise ValueError: The project isn't defined in the config file. """ super(TurninProject, self).__init__(config_file, course) if not self.course.has_key(project): raise ValueError("Project %s does not exist in course %s!" % (project, course)) self.project = self.course[project] """ @ivar: shortcut to the project configurations. """ self.project['directory'] = os.path.join(self.course['directory'], project) self.name = project class TurninList: """ This class represents a list of suffixes for submitted assignments. """ def __init__(self, config_file): self.config = ConfigObj() self.config.filename = config_file self.config.indent_type = ' ' self.config.unrepr = True self.config.reload() def write(self, project, suffix): """ Write the project submission's suffix to the list file. @type project: TurninProject @param project: Project to which we submitted @type suffix: string @param suffix: submitted archive's unique suffix @rtype: None """ course = project.course.name uuid = project.project['uuid'] if not self.config.has_key(course): self.config[course] = {} self.config[course][uuid] = suffix self.config.write() Turnin-NG-1.3/src/turninng/coursemanage.py000066400000000000000000000170131237736160600206310ustar00rootroot00000000000000# Turnin-NG, an assignment submitter and manager. --- Course manager # Copyright (C) 2009-2010 Ryan Kavanagh # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import datetime import errno import grp import os import os.path import pwd import shutil import sys import tarfile from turninng.configparser import ProjectGlobal, ProjectAdminCourse from turninng.fileperms import chown def create_course(config_file, course): """ Create the course 'course'. @type config_file: string @param config_file: path to the configuration file @type course: string @param course: course name @rtype: None @raise ValueError: The course already exists. """ config_obj = ProjectGlobal(config_file) if not config_obj.config.has_key(course): course = ProjectAdminCourse(config_file, course) # Don't die if we get an invalid user while True: user = raw_input("Managing username [usually your UNIX login]: ") try: pwd.getpwnam(user) except KeyError, e: print "User does not exist. Please try again." else: break directory = raw_input("Full path to the course directory: ") group_managed = '?' # Don't check if "in 'UuGg'" because we don't want 'uGg' to match while group_managed not in ('U', 'u', 'G', 'g'): group_managed = raw_input("Managed by a User or Group [U/G]: ") # Don't die if we get an invalid group while True: if group_managed in ('G', 'g'): group_managed = True group = raw_input("Managing group: ") else: group_managed = False group = raw_input("Student group: ") try: grp.getgrnam(group) except KeyError, e: print "Group does not exist. Please try again." else: break try: try: os.makedirs(directory) # We could supply the mode here, but it might get #ignored on some systems. We'll do it here instead except OSError, e: # We don't want to abort of the directory already exists if e.errno == errno.EEXIST: print e print 'Continuing' else: sys.exit(e) chown(directory, user, group) chown(config_file, user, group) if group_managed: os.chmod(directory, 0775) else: os.chmod(directory, 0755) print "Please make sure the account %s" % user +\ " is a member of the group %s." % group except OSError, e: print e course.write(user, directory, group, group_managed) # We want to set the default course for the per course config. global_course_conf = ProjectGlobal(course.course['projlist']) global_course_conf.set_default(course.course.name) chown(course.course['projlist'], user, group) if group_managed: os.chmod(course.course['projlist'], 0664) else: os.chmod(course.course['projlist'], 0644) else: raise ValueError ('The course %s already exists, aborting' % course) def delete_course(config_file, course): """ Delete the course 'course'. @type config_file: string @param config_file: path to the configuration file @type course: string @param course: course name @rtype: None @raise ValueError: The user enters anything but 'YES' at the prompt. @raise ValueError: The course does not exist. """ config_obj = ProjectGlobal(config_file) if config_obj.config.has_key(course): course_obj = ProjectAdminCourse(config_file, course) if raw_input("If you really want to delete this course and all " + "files in the course directory %s , " % ( course_obj.course['directory'] ) + "enter 'yes' in capital" + " letters: ") == 'YES': shutil.rmtree(course_obj.course['directory'], ignore_errors=True) del course_obj.config[course] # We need to check that Global has the key 'default', otherwise we # get a KeyError if it doesn't. if ((course_obj.config['Global'].has_key('default')) and (course_obj.config['Global']['default'] == course)): course_obj.config['Global']['default'] = '' course_obj.config.write() else: raise ValueError("Aborting and keeping course %s" % course) else: raise ValueError("%s is not an existing course" % course) def archive_course(config_file, course, ret_path=False): """ Archive the course in .tar.gz format. @type config_file: string @param config_file: path to the configuration file @type course: string @param course: course name @type ret_path: bool @param ret_path: Do we return the archive's path? @rtype: string @return: Path to the archive. @raise ValueError: The user enters anything but 'YES' at the prompt. @raise ValueError: The course does not exist. """ config_obj = ProjectGlobal(config_file) if config_obj.config.has_key(course): config_obj = ProjectAdminCourse(config_file, course) if raw_input("If you really want to archive this course and erase it "+ "from the configuration file, enter 'yes' in capital " + "letters: ") == 'YES': archive_path = os.path.normpath(os.path.join( config_obj.course['directory'], os.pardir, course + '-' + str(datetime.datetime.now().year) + '.tar.gz')) tar = tarfile.open(archive_path, 'w:gz') tar.add(config_obj.course['directory'], course + '-' + str(datetime.datetime.now().year)) tar.close() if config_obj.course['group_managed']: os.chmod(archive_path, 0660) else: os.chmod(archive_path, 0600) chown(archive_path, config_obj.course['user'], config_obj.course['group']) shutil.rmtree(config_obj.course['directory'], ignore_errors=True) del config_obj.config[course] # We need to check that Global has the key 'default', otherwise we # get a KeyError if it doesn't. if ((config_obj.config['Global'].has_key('default')) and (config_obj.config['Global']['default'] == course)): config_obj.config['Global']['default'] = '' config_obj.config.write() if ret_path: return archive_path else: raise ValueError("Aborting and keeping course %s unarchived" % course) Turnin-NG-1.3/src/turninng/fileperms.py000066400000000000000000000075321237736160600201530ustar00rootroot00000000000000# Turnin-NG, an assignment submitter and manager. --- Custom system utilities # Copyright (C) 2009, 2011 Ryan Kavanagh # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import pwd import grp import os import shutil import tempfile from shutil import Error from stat import ST_UID, ST_GID def chown(path, user='', group=''): """ Change the owner and the group of 'path' @type path: string @param path: file / directory for which to change the owner/group @type user: string @param user: username @type group: string @param group: groupname @rtype: function @return: Function to change the owner and group of 'path' """ if user: UID = pwd.getpwnam(user)[2] else: # Set to -1 to leave unchanged. UID = -1 if group: GID = grp.getgrnam(group)[2] else: GID = -1 return os.chown(path, UID, GID) def chgrp(path, group): """ Change the group of 'path' @type path: string @param path: file / directory for which to change the group @type group: string @param group: group name @rtype: function @return: Function to change the group of path """ return chown(path, group=group) def chmod_hack(path): """ We need to be the owner of 'path' to be able to change the permissions. This convoluted hack is necessary if we want to change the permissions of a directory we have g+rwx, but not u+rwx. Loosely based on the sourcecode for shutil.copytree @type path: string @param path: file / directory on which to change permissions @return: None """ # Get the name of the group that owns path path_group = grp.getgrgid(os.stat(path)[ST_GID])[0] temp_dir = tempfile.mkdtemp() files = os.listdir(path) errors = [] for file in files: src = os.path.join(path, file) dst = os.path.join(temp_dir, file) try: if os.path.islink(src): linkto = os.readlink(src) os.symlink(linkto, dst) elif os.path.isdir(src): shutil.copytree(src, dst, symlinks=True) else: shutil.copy2(src, dst) except (IOError, os.error), why: errors.append((src, dst, str(why))) except Error, err: errors.extend(err.args[0]) try: shutil.copystat(path, temp_dir) shutil.rmtree(path) os.rename(temp_dir, path) chgrp(path, path_group) except OSError, why: errors.extend((path, temp_dir, str(why))) if errors: raise Error(errors) def chmod(path, permissions): """ Change the permissions of 'path' @type path: string @param path: file / directory on which to change permissions @type permissions: int @param permissions: 4 digit octal permissions for path @return: None @raise ValueError: permissions aren't valid octal permissions """ if not 0 <= permissions <= 7777: raise ValueError("%s are invalid octal permissions", oct(permissions)) # Do we own the file or are we root? if os.stat(path)[ST_UID] != os.geteuid() and os.geteuid() != 0: # Ewwww! chmod_hack(path) os.chmod(path, permissions) Turnin-NG-1.3/src/turninng/projectmanage.py000066400000000000000000000345321237736160600210040ustar00rootroot00000000000000# Turnin-NG, an assignment submitter and manager. --- Project manager # Copyright (C) 2009, 2010 Ryan Kavanagh # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import os import os.path import pwd import re import shutil import subprocess import sys import tarfile from turninng.configparser import ProjectCourse, ProjectProject from turninng.fileperms import chmod, chown, chgrp # Username of the current user whoami = pwd.getpwuid(os.getuid())[0] def create_project(config_file, course, project): """ Create the project 'project'. @type config_file: string @param config_file: path to the configuration file @type course: string @param course: course name @type project: string @param project project name @rtype: None """ project_obj = ProjectProject(config_file, course, project) user = project_obj.course['user'] group = project_obj.course['group'] directory = project_obj.project['directory'] os.makedirs(directory) if project_obj.course['group_managed']: chmod(directory, 0773) chgrp(directory, group) else: chmod(directory, 0733) chown(directory, user, group) description = raw_input("[Optional] Project description: ") project_obj.write(True, description) project_obj.add_manager(whoami) def delete_project(config_file, course, project): """ Delete the project 'project' @type config_file: string @param config_file: path to the configuration file @type course: string @param course: course name @type project: string @param project: project name @rtype: None @raise ValueError: The user enters anything but 'YES' at the prompt. @raise valueError: The project doesn't exist. """ if ProjectCourse(config_file, course).course.has_key(project): project_obj = ProjectProject(config_file, course, project) if raw_input("If you really want to delete this project and all " + "associated files, enter 'yes' in capital letters: ") == 'YES': shutil.rmtree(project_obj.project['directory'], ignore_errors=True) if project_obj.project['tarball']: os.remove(project_obj.project['tarball']) if project_obj.course['default'] == project: project_obj.course['default'] = '' del project_obj.course[project] project_obj.config.write() else: raise ValueError("Aborting and keeping project.") else: raise ValueError("%s is not an existing project in the course %s" % (project, course)) def compress_project(config_file, course, project): """ Compress the project 'project'. @type config_file: string @param config_file: path to the configuration file @type course: string @param course: course name @type project: string @param project: project name @rtype: None @raise ValueError: The project is enabled / accepting submissions @raise ValueError: The project is already compressed @raise ValueError: The project doesn't exist. """ if ProjectCourse(config_file, course).course.has_key(project): project_obj = ProjectProject(config_file, course, project) if project_obj.project['enabled']: raise ValueError("Project %s is enabled, please disable it first." % project) # We need to check that it has a key before checking if it's Null or # not. If we skipped straight to checking if Null, and the key didn't # exist, we would get a KeyError. elif (project_obj.project.has_key('tarball') and project_obj.project['tarball']): raise ValueError("Project %s is already compressed." % project) archive_name = os.path.join(project_obj.course['directory'], project_obj.name + '.tar.gz') tar = tarfile.open(archive_name, 'w:gz') tar.add(project_obj.project['directory'], project_obj.name) tar.close() # This writes the tarball project_obj.project['tarball'] = archive_name if project_obj.course['group_managed']: chmod(archive_name, 0660) chgrp(archive_name, project_obj.course['group']) else: chmod(archive_name, 0600) project_obj.config.write() project_obj.add_manager(whoami) shutil.rmtree(project_obj.project['directory'], ignore_errors=True) else: raise ValueError("%s is not an existing project in the course %s" % (project, course)) def extract_project(config_file, course, project): """ Uncompress the project 'project'. @type config_file: string @param config_file: path to the configuration file @type course: string @param course: course name @type project: string @param project: project name @rtype: None @raise ValueError: The project is not compressed @raise ValueError: The project does not exist. """ if ProjectCourse(config_file, course).course.has_key(project): project_obj = ProjectProject(config_file, course, project) # We need to check that it has a key before checking if it's Null or # not. If we skipped straight to checking if Null, and the key didn't # exist, we would get a KeyError. if (project_obj.project.has_key('tarball') and not project_obj.project['tarball']): raise ValueError("This project is not compressed.") print project_obj.project['tarball'] tar = tarfile.open(project_obj.project['tarball'], 'r:gz') # Extract it to the course directory instead of to '.' if sys.version_info[1] >= 5: # extractall was added in Python 2.5 tar.extractall(path=project_obj.course['directory']) else: for member in tar.getmembers(): tar.extract(member, path=project_obj.course['directory']) chgrp(project_obj.project['directory'], project_obj.course['group']) tar.close() os.remove(project_obj.project['tarball']) project_obj.project['tarball'] = '' project_obj.config.write() project_obj.add_manager(whoami) else: raise ValueError("%s is not an existing project in the course %s" % (project, course)) def verify_sig(project_obj): """ Verify the signatures of the projects with a signature. @type project_obj: ProjectProject @param project_obj: Project for which we'll verify the signatures @rtype: list @return: unsigned submissions @raise subprocess.CalledProcessError: gpg encounters an issue when verifying """ submissions = os.listdir(project_obj.project['directory']) if not submissions: raise ValueError("No assignments have been submitted yet.") signatures = [] # We need to work with a copy of submissions since we should never mutate a # list in place. Don't go submissions_copy = submissions , submissions_copy # would point to the same list submissions is pointing to. submissions_copy = list(submissions) for file in submissions_copy: if file.endswith('.tar.gz'): if file + '.sig' in submissions: signatures.append(file + '.sig') submissions.remove(file) submissions.remove(file + '.sig') for sig in signatures: print "Verifying %s" % sig[:-4] cargs = ['gpg', '--verify', os.path.join(project_obj.project['directory'], sig)] retcode = subprocess.call(cargs) if retcode < 0: raise subprocess.CalledProcessError(retcode, ' '.join(cargs)) ret = ['Unsigned submissions: '] if len(submissions) == 0: ret.append('None') else: ret += submissions return ret def strip_random_suffix(project_obj): """ Remove the 16 byte suffixes of the style username-XXXXXXXXXXXXXXXX.tar.gz @type project_obj: ProjectProject @param project_obj: Project for which we will strip the suffixes @rtype: None @raise ValueError: No assignments have been submitted. """ project_obj.add_manager(whoami) files = os.listdir(project_obj.project['directory']) if not files: raise ValueError("No assignments have been submitted yet. Not " + "stripping suffixes") # They need to be sorted since there is no guarantee that os.listdir will # read files in alphabetical order. submissions = {} rejects = set() filename_format = re.compile( '^(?P\w+)-\w{16}(?P\.tar\.gz(|.sig))$') for submission in files: if filename_format.match(submission): username = filename_format.match(submission).group('username') format = filename_format.match(submission).group('format') if not submissions.has_key(username): submissions[username] = {'tarfiles': set(), 'sigfiles': set()} if format.endswith('.tar.gz'): submissions[username]['tarfiles'].add(submission) else: submissions[username]['sigfiles'].add(submission) else: rejects.add(submission) if rejects: print ValueError("Warning: The following file(s) do not have the " + "the format username-XXXXXXXXXXXXXXXX.tar.gz or " + "username-XXXXXXXXXXXXXXX.tar.gz.sig, skipping: %s" % '\n'.join(rejects)) def get_owner(project, submission): """ Which user owns the submission @type project: ProjectProject @param project: current project @type submission: str @param submission: file in the submission directory @rtype: string @return: the owner of the submission """ owner = pwd.getpwuid(os.stat(os.path.join( project.project['directory'], submission)).st_uid).pw_name return owner for user in submissions.keys(): # Make copies of the sets to iterate through since we'll be modifying # them and can't mutate sets while iterating through them submissions_user_tarfiles = set(submissions[user]['tarfiles']) for tar in submissions_user_tarfiles: owner = get_owner(project_obj, tar) if not owner == user and \ owner not in project_obj.project['managing_accounts']: # Let's get rid of assignments other people submitted for the # user print Warning('Warning: Student %s submitted an ' % owner + 'assignment for the student %s. Skipping file %s.' % (user, tar)) submissions[user]['tarfiles'].discard(tar) if tar + '.sig' in submissions[user]['sigfiles']: print Warning('Also skipping associated signature file' + ' %s.' % (tar + '.sig')) submissions[user]['sigfiles'].discard(tar + '.sig') # Make the sigfiles copy here since we may have modified sigfiles in the # tar checking loop submissions_user_sigfiles = set(submissions[user]['sigfiles']) for sig in submissions_user_sigfiles: owner = get_owner(project_obj, sig) if not owner == user and \ owner not in project_obj.project['managing_accounts']: # Let's get rid of signatures other people submitted for the # user. This should probably never get called unless we have # really really stupidsilly people who are # desperately trying to get caught. print Warning('Warning: Student %s submitted a ' % owner + 'signature for the student %s. Skipping file %s.' % (user, sig)) submissions[user]['sigfiles'].discard(sig) elif sig[:-4] not in submissions[user]['tarfiles']: # Remove hanging signature files so that # len(submissions[user]['sigfiles']) <= # len(submissions[user]['tarfiles) print Warning('Warning: Signature file %s' % sig + ' has no associated archive. Skipping file %s.' % sig) submissions[user]['sigfiles'].discard(sig) if len(submissions[user]['tarfiles']) > 1: print Warning('Warning: Student %s submitted ' % user + 'more than one assignment, skipping the files: %s' % ' '.join(submissions[user].pop('tarfiles') | submissions[user].pop('sigfiles'))) submissions.pop(user) elif len(submissions[user]['tarfiles']) == 0: # If a user doesn't have any valid assignments associated with him, # take him out of the to-stip queue submissions.pop(user) for user in submissions.keys(): submitted_files = submissions[user]['tarfiles'] | \ submissions[user]['sigfiles'] # Any overwrites? overwrites = set([user + '.tar.gz', user + '.tar.gz.sig']) & rejects if overwrites: print ValueError("Warning: Stripping the user %s's" % user + " file(s) (%s) would either cause the files %s" % ( ', '.join(submitted_files), ', '.join(overwrites)) + " to be overwritten or would break signatures. Skipping" + " the files: %s" % ' '.join(submitted_files)) else: for submission in submitted_files: format = filename_format.match(submission).group('format') os.rename( os.path.join(project_obj.project['directory'], submission), os.path.join(project_obj.project['directory'], user + format)) Turnin-NG-1.3/src/turninng/submitter.py000066400000000000000000000172731237736160600202060ustar00rootroot00000000000000# Turnin-NG, an assignment submitter and manager. --- Submission utilities # Copyright (C) 2009-2014 Ryan Kavanagh # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import errno import os import os.path import pwd import random import shutil import string import subprocess import tarfile import tempfile from turninng.configparser import TurninCourse, TurninList def submit_files(project, files, tlist='', gpg_key=''): """ Submits the files to the project. @type project: TurninProject @param project: Project to which we should submit the files @type files: list @param files: Python list of files. @type tlist: TurninList @param tlist: list of submitted files @type gpg_key: string @param gpg_key: GnuPG public-key ID @rtype: list @return: Python list of submitted files @raise subprocess.CalledProcessError: GnuPG fails to sign @raise EnvironmentError: tlist exists and is not a file """ if not tlist: tlist = os.path.join(os.path.expanduser('~'), '.turnin-ng', 'submissions') # We don't want something like /home/ryan/.turning-ng/submissions../ if not os.path.isdir(os.path.normpath(os.path.join(tlist, os.pardir))): os.makedirs(os.path.normpath(os.path.join(tlist, os.pardir))) # Create the TurninList if it doesn't exist: if not os.path.isfile(tlist): if os.path.exists(tlist): raise EnvironmentError( "Path for list of submissions %s exists " % tlist + "but is not a file.") else: open(tlist, 'w').close() # We don't want other people to be able to read out submitted file # suffixes and messing with our files! os.chmod(tlist, 0600) list_file = TurninList(tlist) random_suffix = '' if list_file.config.has_key(project.course.name): if list_file.config[project.course.name].has_key( project.project['uuid']): random_suffix = list_file.config[project.course.name][ project.project['uuid']] if not random_suffix: chars = string.letters + string.digits for i in range(16): random_suffix += random.choice(chars) list_file.write(project, random_suffix) temparchive = tempfile.NamedTemporaryFile(suffix='.tar.gz') filename = '%(username)s-%(suffix)s.tar.gz' % \ {'username': pwd.getpwuid(os.getuid())[0], # This is the username that # that owns the process 'suffix': random_suffix} tempdir = tempfile.mkdtemp() for file in files: if os.path.isdir(file): shutil.copytree(file, os.path.join(tempdir, file)) else: shutil.copy(file, tempdir) tar = tarfile.open(temparchive.name, 'w:gz') # We want the uncompressed directory to have the username in it so the # professor can easily find a student's assignment. tar.add(tempdir, '%(projectname)s-%(username)s' % {'projectname': project.name, 'username': pwd.getpwuid(os.getuid())[0]}) submitted_files = tar.members tar.close() if gpg_key: cargs = ['gpg', '--sign', '-u ' + gpg_key, '-b', temparchive.name] retcode = subprocess.call(cargs) if retcode < 0: raise subprocess.CalledProcessError(retcode, ' '.join(cargs)) print "An error occured when calling GPG, not submitting signature" # It's important that we set gpg_key to False. Otherwise, # shutil.copy below will fail because it can't find the .sig file # and Turnin-NG will crash. gpg_key = False shutil.copyfile(temparchive.name, os.path.join(project.project['directory'], filename)) try: os.chmod(os.path.join(project.project['directory'], filename), 0666) except OSError, e: if e.errno == e.EPERM: # We've got an error on our hands: # OSError: [Errno 1] Operation not permitted # # Probable cause: # # We've previously submitted. A managing user has since # disabled/enabled the project and is now the owner of our archive. # Since we aren't the owner, we don't have permisisons to run chmod. # In any case, unless we have a malicious TA, since we've already # submitted, the permissions on our file should already be correct. pass # We want the signature's timestamp to be more recent than the archive's. if gpg_key: shutil.copyfile(temparchive.name + '.sig', os.path.join(project.project['directory'], filename + '.sig')) os.remove(temparchive.name + '.sig') # GPG signatures are 644 by default os.chmod(os.path.join(project.project['directory'], filename + '.sig'), 0666) else: submitted_sig = os.path.join(project.project['directory'], filename + '.sig') if os.path.exists(submitted_sig): # We've already submitted our project and we had signed it. Since # we're resubmitting, our old signature won't match. Delete it. os.remove(submitted_sig) temparchive.close() shutil.rmtree(tempdir, ignore_errors=True) for j, file in enumerate(submitted_files): submitted_files[j] = file.name return submitted_files def list_projects(config, course): """ Lists available projects for the course. @type config: string @param config: configuration file @type course: string @param course: course name @rtype: list @return: List of '| Enabled/Disabled/Default | Project name | Description |' strings """ projects = [['Enabled', 'Project', 'Description']] course_obj = TurninCourse(config, course) default = '' if course_obj.course.has_key('default'): default = course_obj.course['default'] for i in course_obj.course.__dict__['sections']: if default == i: projects.append(['Default', i, course_obj.course[i]['description']]) else: projects.append([course_obj.course[i]['enabled'], i, course_obj.course[i]['description']]) maxlen = [0,0,0] for p, project in enumerate(projects): for i, item in enumerate(project): # We need to convert item to string since item might be a bool. projects[p][i] = str(item) if len(str(item)) > maxlen[i]: maxlen[i] = len(str(item)) for p, project in enumerate(projects): new_line = '' for i, item in enumerate(project): # We need the space difference + 1 so that we don't get "item|" new_line += '| ' + item + (maxlen[i] - len(item) + 1) * ' ' new_line += '|' projects[p] = new_line # Let's insert the top, the header seperater and the bottom projects.insert(0, len(projects[0]) * '-') projects.insert(2, projects[0]) projects.append(projects[0]) return projects