This commit is contained in:
Soren I. Bjornstad 2014-06-17 08:14:49 -05:00
commit e0777602db
76 changed files with 819 additions and 521 deletions

View File

@ -1,7 +1,11 @@
Please see the README file for basic requirements.
You also need to have the python-pyqt development packages installed
(specifically, you need the binary pyuic4).
In addition to the basic requirements, you also need the PyQt development
tools (specifically pyrcc4 and pyuic4). These are often contained in a
separate package on Linux, such as 'pyqt4-dev-tools' on Debian/Ubuntu. On a Mac
they are part of the PyQt source install.
Windows users, please see the note at the bottom of this file before proceeding.
To use the development version:
@ -9,7 +13,11 @@ $ git clone https://github.com/dae/anki.git
$ cd anki
$ ./tools/build_ui.sh
Make sure you rebuild the UI every time you update the sources.
If you get any errors, you will not be able to proceed, so please return to
the top and check the requirements again.
ALL USERS: Make sure you rebuild the UI every time you git pull, otherwise you
will get errors down the road.
The translations are stored in a bazaar repo for integration with Launchpad's
translation services. If you want to use a language other than English:
@ -31,3 +39,20 @@ Before contributing code, please read the LICENSE file.
If you'd like to contribute translations, please see the translations section
of http://ankisrs.net/docs/manual.html#_contributing
WINDOWS USERS:
I have not tested the build scripts on Windows, so you'll need to solve any
problems you encounter on your own. The easiest way is to use a source
tarball instead of git, as that way you don't need to build the UI yourself.
If you do want to use git, a user contributed the following, which should get
you most of the way there:
1) Install "git bash".
2) In the tools directory, modify build_ui.sh. Locate the line that reads
"pyuic4 $i -o $py" and alter it to be of the following form:
"<python-path-string>" "<pyuic-path-string>" $i -o $py
These two paths must point to your python executable, and to pyuic.py, on your
system. Typical paths would be:
<python-path> = C:\\Python27\\python.exe
<pyuic-path-string> = C:\\Python27\\Lib\\site-packages\\PyQt4\\uic\\pyuic.py

View File

@ -30,6 +30,6 @@ if arch[1] == "ELF":
sys.path.insert(0, os.path.join(ext, "py2.%d-%s" % (
sys.version_info[1], arch[0][0:2])))
version="2.0.19" # build scripts grep this line, so preserve formatting
version="2.0.26" # build scripts grep this line, so preserve formatting
from anki.storage import Collection
__all__ = ["Collection"]

View File

@ -1,4 +1,142 @@
-----BEGIN CERTIFICATE-----
MIIFFjCCA/6gAwIBAgIQRi3682cfg0pV4BcKiOriGTANBgkqhkiG9w0BAQUFADBy
MQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD
VQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRlZDEYMBYGA1UE
AxMPRXNzZW50aWFsU1NMIENBMB4XDTE0MDQxMDAwMDAwMFoXDTE3MDQwOTIzNTk1
OVowWzEhMB8GA1UECxMYRG9tYWluIENvbnRyb2wgVmFsaWRhdGVkMR4wHAYDVQQL
ExVFc3NlbnRpYWxTU0wgV2lsZGNhcmQxFjAUBgNVBAMUDSouYW5raXdlYi5uZXQw
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+A1hdxZ8NNorbzRF9qutY
7DNuKQdXRjMuRszAeAHI8Ln6+zfvMvLPvCHrZ9aDNTpKhAQBuE7NrFkR9oR0xHT8
JtgVQKJ5zvVFfZmNoHyWYW0+dj8ay1Y/74V/I4Xhb43Fk4Q7UBgAl0kwm5vDWC6U
HsA9/KPfEbWCOLSVoA1sU60viggnfbIl0XmNkkt355KvUdJgT5LisOfi8KvH58RN
X8RHDsLI+Kv3rc11hLxwJ171NC0bPvjOIvQ5NKlNeDFZASx00kaGE3H26r1it2Gr
hrR8IlTSV967JDpYi1FO+Aom54/OZ6ozE/JNsbKlcwmdH2CO2aMizFZfoQmYEsyr
AgMBAAGjggG9MIIBuTAfBgNVHSMEGDAWgBTay+qtWwhdzP/8JlTOSeVVxjj0+DAd
BgNVHQ4EFgQU8P8DDEM/4k6bCQr8Z9BeoGw5ANgwDgYDVR0PAQH/BAQDAgWgMAwG
A1UdEwEB/wQCMAAwNAYDVR0lBC0wKwYIKwYBBQUHAwEGCCsGAQUFBwMCBgorBgEE
AYI3CgMDBglghkgBhvhCBAEwTwYDVR0gBEgwRjA6BgsrBgEEAbIxAQICBzArMCkG
CCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21vZG8uY29tL0NQUzAIBgZngQwB
AgEwOwYDVR0fBDQwMjAwoC6gLIYqaHR0cDovL2NybC5jb21vZG9jYS5jb20vRXNz
ZW50aWFsU1NMQ0EuY3JsMG4GCCsGAQUFBwEBBGIwYDA4BggrBgEFBQcwAoYsaHR0
cDovL2NydC5jb21vZG9jYS5jb20vRXNzZW50aWFsU1NMQ0FfMi5jcnQwJAYIKwYB
BQUHMAGGGGh0dHA6Ly9vY3NwLmNvbW9kb2NhLmNvbTAlBgNVHREEHjAcgg0qLmFu
a2l3ZWIubmV0ggthbmtpd2ViLm5ldDANBgkqhkiG9w0BAQUFAAOCAQEAR2JPdym8
MsSqmi2EyB4zR/UZH7XQUdIwx/1NhKf1XTN9akTNsS2y6Vp+TvaRVknEm7Z1i8CU
xiSZicsUOUr8MCzDVtTl3KUuYNUdsv0yXwTvGc01xJ26ix+KTmmQVKBq86gYGzXI
pLG0mfG1UAdZc6MxhcPXDROppvGXWk2vb6xYryVQiqV45SCiX2a4PtIf8zqO62eD
Xqazh3fpJ685rBnvWvpi8teYlYbJpJoaFEHa5ime2VixEYPVnfY2DBbiLQVImCgF
4NLwbCh79uD90CiGXHZbVUCq8fNf2gYzhK08erbDWhK39DzkBcE1XlbsPYzN9fUL
E1Re6AD7oBwALQ==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFAzCCA+ugAwIBAgIQGLLLuqME8aAPwfLzJkYqSjANBgkqhkiG9w0BAQUFADCB
gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV
BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw
MDBaFw0xOTEyMzEyMzU5NTlaMHIxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVh
dGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9E
TyBDQSBMaW1pdGVkMRgwFgYDVQQDEw9Fc3NlbnRpYWxTU0wgQ0EwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCt8AiwcsargxIxF3CJhakgEtSYau2A1NHf
5I5ZLdOWIY120j8YC0YZYwvHIPPlC92AGvFaoL0dds23Izp0XmEbdaqb1IX04XiR
0y3hr/yYLgbSeT1awB8hLRyuIVPGOqchfr7tZ291HRqfalsGs2rjsQuqag7nbWzD
ypWMN84hHzWQfdvaGlyoiBSyD8gSIF/F03/o4Tjg27z5H6Gq1huQByH6RSRQXScq
oChBRVt9vKCiL6qbfltTxfEFFld+Edc7tNkBdtzffRDPUanlOPJ7FAB1WfnwWdsX
Pvev5gItpHnBXaIcw5rIp6gLSApqLn8tl2X2xQScRMiZln5+pN0vAgMBAAGjggGD
MIIBfzAfBgNVHSMEGDAWgBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAdBgNVHQ4EFgQU
2svqrVsIXcz//CZUzknlVcY49PgwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQI
MAYBAf8CAQAwIAYDVR0lBBkwFwYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMD4GA1Ud
IAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21v
ZG8uY29tL0NQUzBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9kb2Nh
LmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBsBggrBgEFBQcB
AQRgMF4wNgYIKwYBBQUHMAKGKmh0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NvbW9k
b1VUTlNHQ0NBLmNydDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2Eu
Y29tMA0GCSqGSIb3DQEBBQUAA4IBAQAtlzR6QDLqcJcvgTtLeRJ3rvuq1xqo2l/z
odueTZbLN3qo6u6bldudu+Ennv1F7Q5Slqz0J790qpL0pcRDAB8OtXj5isWMcL2a
ejGjKdBZa0wztSz4iw+SY1dWrCRnilsvKcKxudokxeRiDn55w/65g+onO7wdQ7Vu
F6r7yJiIatnyfKH2cboZT7g440LX8NqxwCPf3dfxp+0Jj1agq8MLy6SSgIGSH6lv
+Wwz3D5XxqfyH8wqfOQsTEZf6/Nh9yvENZ+NWPU6g0QO2JOsTGvMd/QDzczc4BxL
XSXaPV7Od4rhPsbXlM1wSTz/Dr0ISKvlUhQVnQ6cGodWaK2cCQBk
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEqzCCA5OgAwIBAgIQLnmDLpCIh+qLjvMabuZ6RDANBgkqhkiG9w0BAQUFADCB
kzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug
Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZBgNVBAMTElVUTiAtIERBVEFDb3Jw
IFNHQzAeFw0wNjEyMDEwMDAwMDBaFw0yMDA1MzAxMDQ4MzhaMIGBMQswCQYDVQQG
EwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxm
b3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RP
IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZ
rts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAh
TaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23Iw
ambV4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVD
iOEjPqXSJDlqR6sA1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ
0o7KBWFxB3NH5YoZEr0ETc5OnKVIrLsm9wIDAQABo4IBCTCCAQUwHwYDVR0jBBgw
FoAUUzLRs89/+uDxoF2FTpLSnkUdtE8wHQYDVR0OBBYEFAtY5YvGTBU3pECpMKkh
vkc2Wlb/MA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MCAGA1UdJQQZ
MBcGCisGAQQBgjcKAwMGCWCGSAGG+EIEATARBgNVHSAECjAIMAYGBFUdIAAwbQYD
VR0fBGYwZDAxoC+gLYYraHR0cDovL2NybC5jb21vZG9jYS5jb20vVVROLURBVEFD
b3JwU0dDLmNybDAvoC2gK4YpaHR0cDovL2NybC5jb21vZG8ubmV0L1VUTi1EQVRB
Q29ycFNHQy5jcmwwDQYJKoZIhvcNAQEFBQADggEBANheksSuFNxDrcKkw2dFBx35
N6IZxxw3NZETHAfEfUKmDvCGXENrDkTPviRhOkKpzp1Mr3k5cN0OBCBOlZw83rdg
umNDQO1qD4FJRrsek8BL8/jhNkkbb7YMDfKQV4r8bZPyKMf6hgoosxcOWYoutr/N
4axMZmzyVZFWtzK/seR9teg6ti/bspzaUJOOTsWsmn5cnhI8O03GUHCzZSuO92uh
uyXAALv17BZlgQ771KMhlneaqHS8U6rCOVD/CwIJYcyVt9eIavZcxWjTFJUaR1/Z
+y3kL48ThqsxE0ATrG7ttRAwixtQqc7ujMrrfLW5Fj3U+m+SbR6ivfsCSsVwvvE=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEpjCCA46gAwIBAgIQRurwlgVMxeP6Zepun0LGZDANBgkqhkiG9w0BAQUFADBv
MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk
ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF
eHRlcm5hbCBDQSBSb290MB4XDTA1MDYwNzA4MDkxMFoXDTIwMDUzMDEwNDgzOFow
gZMxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJVVDEXMBUGA1UEBxMOU2FsdCBMYWtl
IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEhMB8GA1UECxMY
aHR0cDovL3d3dy51c2VydHJ1c3QuY29tMRswGQYDVQQDExJVVE4gLSBEQVRBQ29y
cCBTR0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDf7lgQoituVcSO
vy5GCefgCA8uK3oTlBu99raAjmUFkwAevK/iD44ZDRJH7Kyto/oucPjebvtWQhWe
LlzvI94huQV2JxkPT9bDnLS+lBlj8qYRCutTSJy+8ik7FugaoEymyfQYWWjAcPJT
AMBeUIKlVm82+UrgRIagTU7WR25JSstn16bEBbmOHvT8/83nNuCcBWyyMyIV0LTg
zBfAssD0/jI/KSqVe9jyp04PVHyhDYCzCQPB/1zdXpo+vK68R4pqrnHKH7EquF9C
BQvsRjDRcgvK6VZt9e/feL5hurKlrgRMvKisaRWXve/rtIy/NfjUw9EoDlw6n3AY
MyB3xKKvAgMBAAGjggEXMIIBEzAfBgNVHSMEGDAWgBStvZh6NLQm9/rEJlTvA73g
JMtUGjAdBgNVHQ4EFgQUUzLRs89/+uDxoF2FTpLSnkUdtE8wDgYDVR0PAQH/BAQD
AgEGMA8GA1UdEwEB/wQFMAMBAf8wIAYDVR0lBBkwFwYKKwYBBAGCNwoDAwYJYIZI
AYb4QgQBMBEGA1UdIAQKMAgwBgYEVR0gADB7BgNVHR8EdDByMDigNqA0hjJodHRw
Oi8vY3JsLmNvbW9kb2NhLmNvbS9BZGRUcnVzdEV4dGVybmFsQ0FSb290LmNybDA2
oDSgMoYwaHR0cDovL2NybC5jb21vZG8ubmV0L0FkZFRydXN0RXh0ZXJuYWxDQVJv
b3QuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQBjhpIQsRP6N76OKrYbikP1XK4OFN/3
aUB/vxpxAAnYv9QkSr/gk/8B2AvGD+x+R5ywXfd8FJ38wDOShFvSg/RS4iJYdPxD
Gz+no1jaA/288Drk7cwSu8m5rnsEoARyv+neLdKnUWYAc9K9fqqeU5Z9abIYPo6t
VlB+99Ww/zliZYKMllfDj/dg9sKNNIf8T0Pl278cqvaGzebfET+NB/dtgxPAOIg5
YKF+MOHjiD6ku2NvLOmKaCzulmmsBGHhT04OnXJM9nk4yMdIaW+UD3S0vMjPV025
dXGWDYoGC+vd0PA8fcYumEZqOMcCtci4smV13tqQCLZ3uFMAJctHynNf
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU
MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs
IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290
MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux
FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h
bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v
dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt
H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9
uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX
mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX
a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN
E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0
WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD
VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0
Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU
cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx
IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN
AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH
YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5
6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC
Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX
c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a
mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFFzCCA/+gAwIBAgIRAP+ceCiXnKf8x8mBIBmTckswDQYJKoZIhvcNAQEFBQAw
cTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ29tb2RvIENBIExpbWl0ZWQxFzAVBgNV
@ -53,3 +191,59 @@ Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX
c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a
mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEhjCCA26gAwIBAgIQUkIGSk83/kNpSHqWZ/9dJzANBgkqhkiG9w0BAQUFADBv
MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk
ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF
eHRlcm5hbCBDQSBSb290MB4XDTA1MDYwNzA4MDkxMFoXDTIwMDUzMDEwNDgzOFow
gZcxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJVVDEXMBUGA1UEBxMOU2FsdCBMYWtl
IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEhMB8GA1UECxMY
aHR0cDovL3d3dy51c2VydHJ1c3QuY29tMR8wHQYDVQQDExZVVE4tVVNFUkZpcnN0
LUhhcmR3YXJlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsffDOD+0
qH/POYJRZ9Btn9L/WPPnnyvsDYlUmbk4mRb34CF5SMK7YXQSlh08anLVPBBnOjnt
KxPNZuuVCTOkbJex6MbswXV5nEZejavQav25KlUXEFSzGfCa9vGxXbanbfvgcRdr
ooj7AN/+GjF3DJoBerEy4ysBBzhuw6VeI7xFm3tQwckwj9vlK3rTW/szQB6g1ZgX
vIuHw4nTXaCOsqqq9o5piAbF+okh8widaS4JM5spDUYPjMxJNLBpUb35Bs1orWZM
vD6sYb0KiA7I3z3ufARMnQpea5HW7sftKI2rTYeJc9BupNAeFosU4XZEA39jrOTN
SZzFkvSrMqFIWwIDAQABo4H0MIHxMB8GA1UdIwQYMBaAFK29mHo0tCb3+sQmVO8D
veAky1QaMB0GA1UdDgQWBBShcl8mGyiYQ5VdBzfVhZadS9LDRTAOBgNVHQ8BAf8E
BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zARBgNVHSAECjAIMAYGBFUdIAAwewYDVR0f
BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQWRkVHJ1c3RFeHRl
cm5hbENBUm9vdC5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BZGRU
cnVzdEV4dGVybmFsQ0FSb290LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAYGQ5WaJD
ZS79+R/WrjO76FMTxIjuIxpszthkWVNTkOg239T88055L9XmjwzvKkFtcb2beDgj
03BLhgz9EqciYhLYzOBR7y3lzQxFoura7X7s9zKa5wU1Xm7CLGhonf+M8cpVh8Qv
sUAG3IQiXG2zzdGbGgozKGYWDL0zwvYH8eOheZTg+NDQ099Shj+p4ckdPoaEsdtf
7uRJQ8E5fc8vlqd1XX5nZ4TlWSBAvzcivwdDtDDhQ4rNA11tuSnZhKf1YmOEhtY3
vm9nu/9iVzmdDE2yKmE9HZzvmncgoC/uGnKdsJ2/eBMnBwpgEZP1Dy7J72skg/6b
kLRLaIHQwvrgPw==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFAzCCA+ugAwIBAgIQTM1KmltFEyGMz5AviytRcTANBgkqhkiG9w0BAQUFADCB
lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug
Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt
SGFyZHdhcmUwHhcNMDYwOTE4MDAwMDAwWhcNMjAwNTMwMTA0ODM4WjBxMQswCQYD
VQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdT
YWxmb3JkMRowGAYDVQQKExFDb21vZG8gQ0EgTGltaXRlZDEXMBUGA1UEAxMOUG9z
aXRpdmVTU0wgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9T3lY
IpPJKD5SEQAvwKkgitctVR4Q57h/4oYqpOxe6eSSWJZUDfMXukGeFZFV78LuACAY
RYMm3yDMPbOhEzEKIVx5g3mrJBVcVvC0lZih2tIb6ha1y7ewwVP5pEba8C4kuGKe
joteK1qWoOpQ6Yj7KCpNmpxIT4O2h65Pxci12f2+P9GnncYsEw3AAcezcPOPabuw
PBDf6wkAhD9u7/zjLbTHXRHM9/Lx9uLjAH4SDt6NfQDKOj32cuh5JaYIFveriP9W
XgkXwFqCBWI0KyhIMpfQhAysExjbnmbHqhSLEWlN8QnTul2piDdi2L8Dm53X5gV+
wmpSqo0HgOqODvMdAgMBAAGjggFuMIIBajAfBgNVHSMEGDAWgBShcl8mGyiYQ5Vd
BzfVhZadS9LDRTAdBgNVHQ4EFgQUuMoR6QYxedvDlMboGSq8uzUWMaQwDgYDVR0P
AQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwewYDVR0fBHQwcjA4oDagNIYy
aHR0cDovL2NybC5jb21vZG9jYS5jb20vVVROLVVTRVJGaXJzdC1IYXJkd2FyZS5j
cmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9VVE4tVVNFUkZpcnN0LUhh
cmR3YXJlLmNybDCBhgYIKwYBBQUHAQEEejB4MDsGCCsGAQUFBzAChi9odHRwOi8v
Y3J0LmNvbW9kb2NhLmNvbS9VVE5BZGRUcnVzdFNlcnZlckNBLmNydDA5BggrBgEF
BQcwAoYtaHR0cDovL2NydC5jb21vZG8ubmV0L1VUTkFkZFRydXN0U2VydmVyQ0Eu
Y3J0MA0GCSqGSIb3DQEBBQUAA4IBAQAdtOf5GEhd7fpawx3jt++GFclsE0kWDTGM
MVzn2odkjq8SFqRaLZIaOz4hZaoXw5V+QBz9FGkGGM2sMexq8RaeiSY9WyGN6Oj5
qz2qPMuZ8oZfiFMVBRflqNKFp05Jfdbdx4/OiL9lBeAUtTF37r0qhujop2ot2mUZ
jGfibfZKhWaDtjJNn0IjF9dFQWp2BNStuY9u3MI+6VHyntjzf/tQKvCL/W8NIjYu
zg5G8t6P2jt9HpOs/PQyKw+rAR+lQI/jJJkfXbKqDLnioeeSDJBLU30fKO5WPa8Y
Z0nf1R7CqJgrTEeDgUwuRMLvyGPui3tbMfYmYb95HLCpTqnJUHvi
-----END CERTIFICATE-----

View File

@ -73,9 +73,8 @@ class _Collection(object):
self.crt = int(time.mktime(d.timetuple()))
self.sched = Scheduler(self)
if not self.conf.get("newBury", False):
mod = self.db.mod
self.sched.unburyCards()
self.db.mod = mod
self.conf['newBury'] = True
self.setMod()
def name(self):
n = os.path.splitext(os.path.basename(self.path))[0]
@ -148,10 +147,6 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
def close(self, save=True):
"Disconnect from DB."
if self.db:
if not self.conf.get("newBury", False):
mod = self.db.mod
self.sched.unburyCards()
self.db.mod = mod
if save:
self.save()
else:
@ -515,13 +510,11 @@ where c.nid = n.id and c.id in %s group by nid""" % ids2str(cids)):
afmt = afmt or template['afmt']
for (type, format) in (("q", qfmt), ("a", afmt)):
if type == "q":
format = format.replace("{{cloze:", "{{cq:%d:" % (
data[4]+1))
format = re.sub("{{(?!type:)(.*?)cloze:", r"{{\1cq-%d:" % (data[4]+1), format)
format = format.replace("<%cloze:", "<%%cq:%d:" % (
data[4]+1))
else:
format = format.replace("{{cloze:", "{{ca:%d:" % (
data[4]+1))
format = re.sub("{{(.*?)cloze:", r"{{\1ca-%d:" % (data[4]+1), format)
format = format.replace("<%cloze:", "<%%ca:%d:" % (
data[4]+1))
fields['FrontSide'] = stripSounds(d['q'])
@ -613,13 +606,19 @@ where c.nid == f.id
if self._undo[0] == 1:
old = self._undo[2]
self.clearUndo()
self._undo = [1, _("Review"), old + [copy.copy(card)]]
wasLeech = card.note().hasTag("leech") or False
self._undo = [1, _("Review"), old + [copy.copy(card)], wasLeech]
def _undoReview(self):
data = self._undo[2]
wasLeech = self._undo[3]
c = data.pop()
if not data:
self.clearUndo()
# remove leech tag if it didn't have it before
if not wasLeech and c.note().hasTag("leech"):
c.note().delTag("leech")
c.note().flush()
# write old data
c.flush()
# and delete revlog entry
@ -696,6 +695,10 @@ select id from notes where mid not in """ + ids2str(self.models.ids()))
self.remNotes(ids)
# for each model
for m in self.models.all():
for t in m['tmpls']:
if t['did'] == "None":
t['did'] = None
problems.append(_("Fixed AnkiDroid deck override bug."))
if m['type'] == MODEL_STD:
# model with missing req specification
if 'req' not in m:
@ -753,6 +756,17 @@ select id from cards where odue > 0 and (type=1 or queue=2) and not odid""")
"Fixed %d cards with invalid properties.", cnt) % cnt)
self.db.execute("update cards set odue=0 where id in "+
ids2str(ids))
# cards with odid set when not in a dyn deck
dids = [id for id in self.decks.allIds() if not self.decks.isDyn(id)]
ids = self.db.list("""
select id from cards where odid > 0 and did in %s""" % ids2str(dids))
if ids:
cnt = len(ids)
problems.append(
ngettext("Fixed %d card with invalid properties.",
"Fixed %d cards with invalid properties.", cnt) % cnt)
self.db.execute("update cards set odid=0, odue=0 where id in "+
ids2str(ids))
# tags
self.tags.registerNotes()
# field cache

View File

@ -38,6 +38,8 @@ DYN_DUE = 6
DYN_REVADDED = 7
DYN_DUEPRIORITY = 8
DYN_MAX_SIZE = 99999
# model types
MODEL_STD = 0
MODEL_CLOZE = 1

View File

@ -197,6 +197,12 @@ class DeckManager(object):
deck['collapsed'] = not deck['collapsed']
self.save(deck)
def collapseBrowser(self, did):
deck = self.get(did)
collapsed = deck.get('browserCollapsed', False)
deck['browserCollapsed'] = not collapsed
self.save(deck)
def count(self):
return len(self.decks)

View File

@ -20,10 +20,12 @@ class Exporter(object):
file.close()
def escapeText(self, text):
"Escape newlines, tabs and CSS."
"Escape newlines, tabs, CSS and quotechar."
text = text.replace("\n", "<br>")
text = text.replace("\t", " " * 8)
text = re.sub("(?i)<style>.*?</style>", "", text)
if "\"" in text:
text = "\"" + text.replace("\"", "\"\"") + "\""
return text
def cardIds(self):
@ -134,8 +136,14 @@ class AnkiExporter(Exporter):
data)
# notes
strnids = ids2str(nids.keys())
notedata = self.src.db.all("select * from notes where id in "+
strnids)
notedata = []
for row in self.src.db.all(
"select * from notes where id in "+strnids):
# remove system tags if not exporting scheduling info
if not self.includeSched:
row = list(row)
row[5] = self.removeSystemTags(row[5])
notedata.append(row)
self.dst.db.executemany(
"insert into notes values (?,?,?,?,?,?,?,?,?,?,?)",
notedata)
@ -206,6 +214,9 @@ class AnkiExporter(Exporter):
# overwrite to apply customizations to the deck before it's closed,
# such as update the deck description
pass
def removeSystemTags(self, tags):
return self.src.tags.remFromStr("marked leech", tags)
# Packaged Anki decks
######################################################################

View File

@ -18,7 +18,7 @@ class MnemosyneImporter(NoteImporter):
db = DB(self.file)
ver = db.scalar(
"select value from global_variables where key='version'")
assert ver.startswith('Mnemosyne SQL 1')
assert ver.startswith('Mnemosyne SQL 1') or ver == "2"
# gather facts into temp objects
curid = None
notes = {}

View File

@ -140,65 +140,29 @@ class SupermemoXmlImporter(NoteImporter):
#s = re.sub(u'>',u'&gt;',s)
#s = re.sub(u'<',u'&lt;',s)
return unicode(btflsoup(s,convertEntities=btflsoup.HTML_ENTITIES ))
return unicode(btflsoup(s, selfClosingTags=['br','hr','img','wbr'], convertEntities=btflsoup.HTML_ENTITIES))
def _afactor2efactor(self, af):
# Adapted from <http://www.supermemo.com/beta/xml/xml-core.htm>
def _unescape(self,s,initilize):
"""Note: This method is not used, BeautifulSoup does better job.
"""
# Ranges for A-factors and E-factors
af_min = 1.2
af_max = 6.9
ef_min = 1.3
ef_max = 3.3
if self._unescape_trtable == None:
self._unescape_trtable = (
('&euro;',u''), ('&#32;',u' '), ('&#33;',u'!'), ('&#34;',u'"'), ('&#35;',u'#'), ('&#36;',u'$'), ('&#37;',u'%'), ('&#38;',u'&'), ('&#39;',u"'"),
('&#40;',u'('), ('&#41;',u')'), ('&#42;',u'*'), ('&#43;',u'+'), ('&#44;',u','), ('&#45;',u'-'), ('&#46;',u'.'), ('&#47;',u'/'), ('&#48;',u'0'),
('&#49;',u'1'), ('&#50;',u'2'), ('&#51;',u'3'), ('&#52;',u'4'), ('&#53;',u'5'), ('&#54;',u'6'), ('&#55;',u'7'), ('&#56;',u'8'), ('&#57;',u'9'),
('&#58;',u':'), ('&#59;',u';'), ('&#60;',u'<'), ('&#61;',u'='), ('&#62;',u'>'), ('&#63;',u'?'), ('&#64;',u'@'), ('&#65;',u'A'), ('&#66;',u'B'),
('&#67;',u'C'), ('&#68;',u'D'), ('&#69;',u'E'), ('&#70;',u'F'), ('&#71;',u'G'), ('&#72;',u'H'), ('&#73;',u'I'), ('&#74;',u'J'), ('&#75;',u'K'),
('&#76;',u'L'), ('&#77;',u'M'), ('&#78;',u'N'), ('&#79;',u'O'), ('&#80;',u'P'), ('&#81;',u'Q'), ('&#82;',u'R'), ('&#83;',u'S'), ('&#84;',u'T'),
('&#85;',u'U'), ('&#86;',u'V'), ('&#87;',u'W'), ('&#88;',u'X'), ('&#89;',u'Y'), ('&#90;',u'Z'), ('&#91;',u'['), ('&#92;',u'\\'), ('&#93;',u']'),
('&#94;',u'^'), ('&#95;',u'_'), ('&#96;',u'`'), ('&#97;',u'a'), ('&#98;',u'b'), ('&#99;',u'c'), ('&#100;',u'd'), ('&#101;',u'e'), ('&#102;',u'f'),
('&#103;',u'g'), ('&#104;',u'h'), ('&#105;',u'i'), ('&#106;',u'j'), ('&#107;',u'k'), ('&#108;',u'l'), ('&#109;',u'm'), ('&#110;',u'n'),
('&#111;',u'o'), ('&#112;',u'p'), ('&#113;',u'q'), ('&#114;',u'r'), ('&#115;',u's'), ('&#116;',u't'), ('&#117;',u'u'), ('&#118;',u'v'),
('&#119;',u'w'), ('&#120;',u'x'), ('&#121;',u'y'), ('&#122;',u'z'), ('&#123;',u'{'), ('&#124;',u'|'), ('&#125;',u'}'), ('&#126;',u'~'),
('&#160;',u' '), ('&#161;',u'¡'), ('&#162;',u'¢'), ('&#163;',u'£'), ('&#164;',u'¤'), ('&#165;',u'¥'), ('&#166;',u'¦'), ('&#167;',u'§'),
('&#168;',u'¨'), ('&#169;',u'©'), ('&#170;',u'ª'), ('&#171;',u'«'), ('&#172;',u'¬'), ('&#173;',u'­'), ('&#174;',u'®'), ('&#175;',u'¯'),
('&#176;',u'°'), ('&#177;',u'±'), ('&#178;',u'²'), ('&#179;',u'³'), ('&#180;',u'´'), ('&#181;',u'µ'), ('&#182;',u''), ('&#183;',u'·'),
('&#184;',u'¸'), ('&#185;',u'¹'), ('&#186;',u'º'), ('&#187;',u'»'), ('&#188;',u'¼'), ('&#189;',u'½'), ('&#190;',u'¾'), ('&#191;',u'¿'),
('&#192;',u'À'), ('&#193;',u'Á'), ('&#194;',u'Â'), ('&#195;',u'Ã'), ('&#196;',u'Ä'), ('&Aring;',u'Å'), ('&#197;',u'Å'), ('&#198;',u'Æ'),
('&#199;',u'Ç'), ('&#200;',u'È'), ('&#201;',u'É'), ('&#202;',u'Ê'), ('&#203;',u'Ë'), ('&#204;',u'Ì'), ('&#205;',u'Í'), ('&#206;',u'Î'),
('&#207;',u'Ï'), ('&#208;',u'Ð'), ('&#209;',u'Ñ'), ('&#210;',u'Ò'), ('&#211;',u'Ó'), ('&#212;',u'Ô'), ('&#213;',u'Õ'), ('&#214;',u'Ö'),
('&#215;',u'×'), ('&#216;',u'Ø'), ('&#217;',u'Ù'), ('&#218;',u'Ú'), ('&#219;',u'Û'), ('&#220;',u'Ü'), ('&#221;',u'Ý'), ('&#222;',u'Þ'),
('&#223;',u'ß'), ('&#224;',u'à'), ('&#225;',u'á'), ('&#226;',u'â'), ('&#227;',u'ã'), ('&#228;',u'ä'), ('&#229;',u'å'), ('&#230;',u'æ'),
('&#231;',u'ç'), ('&#232;',u'è'), ('&#233;',u'é'), ('&#234;',u'ê'), ('&#235;',u'ë'), ('&#236;',u'ì'), ('&iacute;',u'í'), ('&#237;',u'í'),
('&#238;',u'î'), ('&#239;',u'ï'), ('&#240;',u'ð'), ('&#241;',u'ñ'), ('&#242;',u'ò'), ('&#243;',u'ó'), ('&#244;',u'ô'), ('&#245;',u'õ'),
('&#246;',u'ö'), ('&#247;',u'÷'), ('&#248;',u'ø'), ('&#249;',u'ù'), ('&#250;',u'ú'), ('&#251;',u'û'), ('&#252;',u'ü'), ('&#253;',u'ý'),
('&#254;',u'þ'), ('&#255;',u'ÿ'), ('&#256;',u'Ā'), ('&#257;',u'ā'), ('&#258;',u'Ă'), ('&#259;',u'ă'), ('&#260;',u'Ą'), ('&#261;',u'ą'),
('&#262;',u'Ć'), ('&#263;',u'ć'), ('&#264;',u'Ĉ'), ('&#265;',u'ĉ'), ('&#266;',u'Ċ'), ('&#267;',u'ċ'), ('&#268;',u'Č'), ('&#269;',u'č'),
('&#270;',u'Ď'), ('&#271;',u'ď'), ('&#272;',u'Đ'), ('&#273;',u'đ'), ('&#274;',u'Ē'), ('&#275;',u'ē'), ('&#276;',u'Ĕ'), ('&#277;',u'ĕ'),
('&#278;',u'Ė'), ('&#279;',u'ė'), ('&#280;',u'Ę'), ('&#281;',u'ę'), ('&#282;',u'Ě'), ('&#283;',u'ě'), ('&#284;',u'Ĝ'), ('&#285;',u'ĝ'),
('&#286;',u'Ğ'), ('&#287;',u'ğ'), ('&#288;',u'Ġ'), ('&#289;',u'ġ'), ('&#290;',u'Ģ'), ('&#291;',u'ģ'), ('&#292;',u'Ĥ'), ('&#293;',u'ĥ'),
('&#294;',u'Ħ'), ('&#295;',u'ħ'), ('&#296;',u'Ĩ'), ('&#297;',u'ĩ'), ('&#298;',u'Ī'), ('&#299;',u'ī'), ('&#300;',u'Ĭ'), ('&#301;',u'ĭ'),
('&#302;',u'Į'), ('&#303;',u'į'), ('&#304;',u'İ'), ('&#305;',u'ı'), ('&#306;',u'IJ'), ('&#307;',u'ij'), ('&#308;',u'Ĵ'), ('&#309;',u'ĵ'),
('&#310;',u'Ķ'), ('&#311;',u'ķ'), ('&#312;',u'ĸ'), ('&#313;',u'Ĺ'), ('&#314;',u'ĺ'), ('&#315;',u'Ļ'), ('&#316;',u'ļ'), ('&#317;',u'Ľ'),
('&#318;',u'ľ'), ('&#319;',u'Ŀ'), ('&#320;',u'ŀ'), ('&#321;',u'Ł'), ('&#322;',u'ł'), ('&#323;',u'Ń'), ('&#324;',u'ń'), ('&#325;',u'Ņ'),
('&#326;',u'ņ'), ('&#327;',u'Ň'), ('&#328;',u'ň'), ('&#329;',u'ʼn'), ('&#330;',u'Ŋ'), ('&#331;',u'ŋ'), ('&#332;',u'Ō'), ('&#333;',u'ō'),
('&#334;',u'Ŏ'), ('&#335;',u'ŏ'), ('&#336;',u'Ő'), ('&#337;',u'ő'), ('&#338;',u'Œ'), ('&#339;',u'œ'), ('&#340;',u'Ŕ'), ('&#341;',u'ŕ'),
('&#342;',u'Ŗ'), ('&#343;',u'ŗ'), ('&#344;',u'Ř'), ('&#345;',u'ř'), ('&#346;',u'Ś'), ('&#347;',u'ś'), ('&#348;',u'Ŝ'), ('&#349;',u'ŝ'),
('&#350;',u'Ş'), ('&#351;',u'ş'), ('&#352;',u'Š'), ('&#353;',u'š'), ('&#354;',u'Ţ'), ('&#355;',u'ţ'), ('&#356;',u'Ť'), ('&#357;',u'ť'),
('&#358;',u'Ŧ'), ('&#359;',u'ŧ'), ('&#360;',u'Ũ'), ('&#361;',u'ũ'), ('&#362;',u'Ū'), ('&#363;',u'ū'), ('&#364;',u'Ŭ'), ('&#365;',u'ŭ'),
('&#366;',u'Ů'), ('&#367;',u'ů'), ('&#368;',u'Ű'), ('&#369;',u'ű'), ('&#370;',u'Ų'), ('&#371;',u'ų'), ('&#372;',u'Ŵ'), ('&#373;',u'ŵ'),
('&#374;',u'Ŷ'), ('&#375;',u'ŷ'), ('&#376;',u'Ÿ'), ('&#377;',u'Ź'), ('&#378;',u'ź'), ('&#379;',u'Ż'), ('&#380;',u'ż'), ('&#381;',u'Ž'),
('&#382;',u'ž'), ('&#383;',u'ſ'), ('&#340;',u'Ŕ'), ('&#341;',u'ŕ'), ('&#342;',u'Ŗ'), ('&#343;',u'ŗ'), ('&#344;',u'Ř'), ('&#345;',u'ř'),
('&#346;',u'Ś'), ('&#347;',u'ś'), ('&#348;',u'Ŝ'), ('&#349;',u'ŝ'), ('&#350;',u'Ş'), ('&#351;',u'ş'), ('&#352;',u'Š'), ('&#353;',u'š'),
('&#354;',u'Ţ'), ('&#355;',u'ţ'), ('&#356;',u'Ť'), ('&#577;',u'ť'), ('&#358;',u'Ŧ'), ('&#359;',u'ŧ'), ('&#360;',u'Ũ'), ('&#361;',u'ũ'),
('&#362;',u'Ū'), ('&#363;',u'ū'), ('&#364;',u'Ŭ'), ('&#365;',u'ŭ'), ('&#366;',u'Ů'), ('&#367;',u'ů'), ('&#368;',u'Ű'), ('&#369;',u'ű'),
('&#370;',u'Ų'), ('&#371;',u'ų'), ('&#372;',u'Ŵ'), ('&#373;',u'ŵ'), ('&#374;',u'Ŷ'), ('&#375;',u'ŷ'), ('&#376;',u'Ÿ'), ('&#377;',u'Ź'),
('&#378;',u'ź'), ('&#379;',u'Ż'), ('&#380;',u'ż'), ('&#381;',u'Ž'), ('&#382;',u'ž'), ('&#383;',u'ſ'),
)
# Sanity checks for the A-factor
if af < af_min:
af = af_min
elif af > af_max:
af = af_max
# Scale af to the range 0..1
af_scaled = (af - af_min) / (af_max - af_min)
# Rescale to the interval ef_min..ef_max
ef = ef_min + af_scaled * (ef_max - ef_min)
#m = re.match()
#s = s.replace(code[0], code[1])
return ef
## DEFAULT IMPORTER METHODS
@ -249,7 +213,7 @@ class SupermemoXmlImporter(NoteImporter):
nextDue = tLastrep + (float(item.Interval) * 86400.0)
remDays = int((nextDue - time.time())/86400)
card.due = self.col.sched.today+remDays
card.factor = int(float(item.AFactor.replace(',','.'))*1000)
card.factor = int(self._afactor2efactor(float(item.AFactor.replace(',','.')))*1000)
note.cards[0] = card
# categories & tags

View File

@ -7,8 +7,13 @@ from anki.utils import checksum, call, namedtmp, tmpdir, isMac, stripHTML
from anki.hooks import addHook
from anki.lang import _
latexCmd = ["latex", "-interaction=nonstopmode"]
latexDviPngCmd = ["dvipng", "-D", "200", "-T", "tight"]
# if you modify these in an add-on, you must make sure to take tmp.tex as the
# input, and output tmp.png as the output file
latexCmds = [
["latex", "-interaction=nonstopmode", "tmp.tex"],
["dvipng", "-D", "200", "-T", "tight", "tmp.dvi", "-o", "tmp.png"]
]
build = True # if off, use existing media but don't create new
regexps = {
"standard": re.compile(r"\[latex\](.+?)\[/latex\]", re.DOTALL | re.IGNORECASE),
@ -89,14 +94,11 @@ package in the LaTeX header instead.""") % bad
oldcwd = os.getcwd()
png = namedtmp("tmp.png")
try:
# generate dvi
# generate png
os.chdir(tmpdir())
if call(latexCmd + ["tmp.tex"], stdout=log, stderr=log):
return _errMsg("latex", texpath)
# and png
if call(latexDviPngCmd + ["tmp.dvi", "-o", "tmp.png"],
stdout=log, stderr=log):
return _errMsg("dvipng", texpath)
for latexCmd in latexCmds:
if call(latexCmd, stdout=log, stderr=log):
return _errMsg(latexCmd[0], texpath)
# add to media
shutil.copyfile(png, os.path.join(mdir, fname))
return

View File

@ -217,6 +217,7 @@ class MediaManager(object):
files = os.listdir(mdir)
else:
files = local
renamedFiles = False
for file in files:
if not local:
if not os.path.isfile(file):
@ -236,14 +237,20 @@ class MediaManager(object):
# delete if we already have the NFC form, otherwise rename
if os.path.exists(nfcFile):
os.unlink(file)
renamedFiles = True
else:
os.rename(file, nfcFile)
renamedFiles = True
file = nfcFile
# compare
if nfcFile not in allRefs:
unused.append(file)
else:
allRefs.discard(nfcFile)
# if we renamed any files to nfc format, we must rerun the check
# to make sure the renamed files are not marked as unused
if renamedFiles:
return self.check(local=local)
nohave = [x for x in allRefs if not x.startswith("_")]
return (nohave, unused, invalid)
@ -333,7 +340,7 @@ class MediaManager(object):
# Illegal characters
##########################################################################
_illegalCharReg = re.compile(r'[][><:"/?*^\\|\0]')
_illegalCharReg = re.compile(r'[][><:"/?*^\\|\0\r\n]')
def stripIllegal(self, str):
return re.sub(self._illegalCharReg, "", str)

View File

@ -569,7 +569,7 @@ select id from notes where mid = ?)""" % " ".join(map),
sflds = splitFields(flds)
map = self.fieldMap(m)
ords = set()
matches = re.findall("{{cloze:(.+?)}}", m['tmpls'][0]['qfmt'])
matches = re.findall("{{[^}]*?cloze:(?:[^}]?:)*(.+?)}}", m['tmpls'][0]['qfmt'])
matches += re.findall("<%cloze:(.+?)%>", m['tmpls'][0]['qfmt'])
for fname in matches:
if fname not in map:

View File

@ -954,7 +954,9 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
def _fillDyn(self, deck):
search, limit, order = deck['terms'][0]
orderlimit = self._dynOrder(order, limit)
search += " -is:suspended -is:buried -deck:filtered"
if search.strip():
search = "(%s)" % search
search = "%s -is:suspended -is:buried -deck:filtered" % search
try:
ids = self.col.findCards(search, order=orderlimit)
except:
@ -997,7 +999,7 @@ due = odue, odue = 0, odid = 0, usn = ?, mod = ? where %s""" % lim,
elif o == DYN_DUE:
t = "c.due"
elif o == DYN_DUEPRIORITY:
t = "(case when queue=2 and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 10000+due end)" % (
t = "(case when queue=2 and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)" % (
self.today, self.today)
else:
# if we don't understand the term, default to due order

View File

@ -204,12 +204,12 @@ addHook("unloadProfile", stopMplayer)
##########################################################################
try:
import pyaudio
import wave
PYAU_FORMAT = pyaudio.paInt16
PYAU_CHANNELS = 1
PYAU_RATE = 44100
PYAU_INPUT_INDEX = None
except:
pass
@ -244,12 +244,16 @@ class PyAudioThreadedRecorder(threading.Thread):
except NameError:
raise Exception(
"Pyaudio not installed (recording not supported on OSX10.3)")
rate = int(p.get_default_input_device_info()['defaultSampleRate'])
stream = p.open(format=PYAU_FORMAT,
channels=PYAU_CHANNELS,
rate=PYAU_RATE,
rate=rate,
input=True,
input_device_index=PYAU_INPUT_INDEX,
frames_per_buffer=chunk)
all = []
while not self.finish:
try:
@ -267,7 +271,7 @@ class PyAudioThreadedRecorder(threading.Thread):
wf = wave.open(processingSrc, 'wb')
wf.setnchannels(PYAU_CHANNELS)
wf.setsampwidth(p.get_sample_size(PYAU_FORMAT))
wf.setframerate(PYAU_RATE)
wf.setframerate(rate)
wf.writeframes(data)
wf.close()

View File

@ -353,7 +353,7 @@ group by day order by day""" % (self._limit(), lim),
tot, period, unit))
if total and tot:
perMin = total / float(tot)
perMin = ngettext("%d card/minute", "%d cards/minute", perMin) % perMin
perMin = ngettext("%d card/minute", "%d cards/minute", perMin) % round(perMin)
self._line(
i, _("Average answer time"),
_("%(a)0.1fs (%(b)s)") % dict(a=(tot*60)/total, b=perMin))
@ -705,7 +705,7 @@ select
sum(case when queue=2 and ivl >= 21 then 1 else 0 end), -- mtr
sum(case when queue in (1,3) or (queue=2 and ivl < 21) then 1 else 0 end), -- yng/lrn
sum(case when queue=0 then 1 else 0 end), -- new
sum(case when queue=-1 then 1 else 0 end) -- susp
sum(case when queue<0 then 1 else 0 end) -- susp
from cards where did in %s""" % self._limit())
# Footer

View File

@ -48,10 +48,11 @@ def addForwardOptionalReverse(col):
mm = col.models
m = addBasicModel(col)
m['name'] = _("Basic (optional reversed card)")
fm = mm.newField(_("Add Reverse"))
av = _("Add Reverse")
fm = mm.newField(av)
mm.addField(m, fm)
t = mm.newTemplate(_("Card 2"))
t['qfmt'] = "{{#Add Reverse}}{{"+_("Back")+"}}{{/Add Reverse}}"
t['qfmt'] = "{{#%s}}{{%s}}{{/%s}}" % (av, _("Back"), av)
t['afmt'] = "{{FrontSide}}\n\n<hr id=answer>\n\n"+"{{"+_("Front")+"}}"
mm.addTemplate(m, t)
return m

View File

@ -757,6 +757,7 @@ class MediaSyncer(object):
# if the sanity check failed, force a resync
self.col.media.forceResync()
return "sanityCheckFailed"
return "success"
def removed(self):
return self.col.media.removed()

View File

@ -4,7 +4,7 @@ from anki.hooks import runFilter
from anki.template import furigana; furigana.install()
from anki.template import hint; hint.install()
clozeReg = r"\{\{c%s::(.*?)(::(.*?))?\}\}"
clozeReg = r"(?s)\{\{c%s::(.*?)(::(.*?))?\}\}"
modifiers = {}
def modifier(symbol):
@ -158,40 +158,45 @@ class Template(object):
return txt
# field modifiers
parts = tag_name.split(':',2)
parts = tag_name.split(':')
extra = None
if len(parts) == 1 or parts[0] == '':
return '{unknown field %s}' % tag_name
elif len(parts) == 2:
(mod, tag) = parts
elif len(parts) == 3:
(mod, extra, tag) = parts
else:
mods, tag = parts[:-1], parts[-1] #py3k has *mods, tag = parts
txt = get_or_attr(context, tag)
#Since 'text:' and other mods can affect html on which Anki relies to
#process clozes, we need to make sure clozes are always
#treated after all the other mods, regardless of how they're specified
#in the template, so that {{cloze:text: == {{text:cloze:
#For type:, we return directly since no other mod than cloze (or other
#pre-defined mods) can be present and those are treated separately
mods.reverse()
mods.sort(key=lambda s: not s=="type")
# built-in modifiers
if mod == 'text':
# strip html
if txt:
return stripHTML(txt)
return ""
elif mod == 'type':
# type answer field; convert it to [[type:...]] for the gui code
# to process
return "[[%s]]" % tag_name
elif mod == 'cq' or mod == 'ca':
# cloze deletion
if txt and extra:
return self.clozeText(txt, extra, mod[1])
for mod in mods:
# built-in modifiers
if mod == 'text':
# strip html
txt = stripHTML(txt) if txt else ""
elif mod == 'type':
# type answer field; convert it to [[type:...]] for the gui code
# to process
return "[[%s]]" % tag_name
elif mod.startswith('cq-') or mod.startswith('ca-'):
# cloze deletion
mod, extra = mod.split("-")
txt = self.clozeText(txt, extra, mod[1]) if txt and extra else ""
else:
return ""
else:
# hook-based field modifier
txt = runFilter('fmod_' + mod, txt or '', extra, context,
tag, tag_name);
if txt is None:
return '{unknown field %s}' % tag_name
return txt
# hook-based field modifier
mod, extra = re.search("^(.*?)(?:\((.*)\))?$", mod).groups()
txt = runFilter('fmod_' + mod, txt or '', extra or '', context,
tag, tag_name);
if txt is None:
return '{unknown field %s}' % tag_name
return txt
def clozeText(self, txt, ord, type):
reg = clozeReg

View File

@ -17,6 +17,7 @@ import sys
import locale
from hashlib import sha1
import platform
import traceback
from anki.lang import _, ngettext
@ -360,6 +361,8 @@ def invalidFilename(str, dirsep=True):
return "/"
elif (dirsep or not isWin) and "\\" in str:
return "\\"
elif str.strip().startswith("."):
return "."
def platDesc():
# we may get an interrupted system call, so try this in a loop
@ -382,3 +385,15 @@ def platDesc():
except:
continue
return theos
# Debugging
##############################################################################
class TimedLog(object):
def __init__(self):
self._last = time.time()
def log(self, s):
path, num, fn, y = traceback.extract_stack(limit=2)[0]
sys.stderr.write("%5dms: %s(): %s\n" % ((time.time() - self._last)*1000, fn, s))
self._last = time.time()

View File

@ -31,8 +31,8 @@ system. It's free and open source.")
Alex Fraser, Andreas Klauer, Andrew Wright, Bernhard Ibertsberger, Charlene Barina,
Christian Krause, Christian Rusche, David Smith, Dave Druelinger, Dotan Cohen,
Emilio Wuerges, Emmanuel Jarri, Frank Harper, Gregor Skumavc, H. Mijail,
Ian Lewis, Immanuel Asmus, Iroiro, Jarvik7,
Jin Eun-Deok, Jo Nakashima, Johanna Lindh, Kieran Clancy, LaC, Laurent Steffan,
Houssam Salem, Ian Lewis, Immanuel Asmus, Iroiro, Jarvik7,
Jin Eun-Deok, Jo Nakashima, Johanna Lindh, Julien Baley, Kieran Clancy, LaC, Laurent Steffan,
Luca Ban, Luciano Esposito, Marco Giancotti, Marcus Rubeus, Mari Egami, Michael Jürges, Mark Wilbur,
Matthew Duggan, Matthew Holtz, Meelis Vasser, Michael Keppler, Michael
Montague, Michael Penkov, Michal Čadil, Morteza Salehi, Nathanael Law, Nick Cook, Niklas

View File

@ -96,10 +96,14 @@ class DataModel(QAbstractTableModel):
return
elif role == Qt.DisplayRole and section < len(self.activeCols):
type = self.columnType(section)
txt = None
for stype, name in self.browser.columns:
if type == stype:
txt = name
break
# handle case where extension has set an invalid column type
if not txt:
txt = self.browser.columns[0][1]
return txt
else:
return
@ -500,15 +504,18 @@ class Browser(QMainWindow):
def setupSearch(self):
self.filterTimer = None
self.form.searchEdit.setLineEdit(FavouritesLineEdit(self.mw, self))
self.connect(self.form.searchButton,
SIGNAL("clicked()"),
self.onSearch)
self.connect(self.form.searchEdit.lineEdit(),
SIGNAL("returnPressed()"),
self.onSearch)
self.setTabOrder(self.form.searchEdit, self.form.tableView)
self.form.searchEdit.setCompleter(None)
self.form.searchEdit.addItems(self.mw.pm.profile['searchHistory'])
self.connect(self.form.searchEdit.lineEdit(),
SIGNAL("returnPressed()"),
self.onSearch)
def onSearch(self, reset=True):
"Careful: if reset is true, the current note is saved."
@ -712,11 +719,9 @@ by clicking on one on the left."""))
def setColumnSizes(self):
hh = self.form.tableView.horizontalHeader()
for i in range(len(self.model.activeCols)):
if hh.visualIndex(i) == len(self.model.activeCols) - 1:
hh.setResizeMode(i, QHeaderView.Stretch)
else:
hh.setResizeMode(i, QHeaderView.Interactive)
hh.setResizeMode(QHeaderView.Interactive)
hh.setResizeMode(hh.logicalIndex(len(self.model.activeCols)-1),
QHeaderView.Stretch)
# this must be set post-resize or it doesn't work
hh.setCascadingSectionResizes(False)
@ -727,9 +732,10 @@ by clicking on one on the left."""))
######################################################################
class CallbackItem(QTreeWidgetItem):
def __init__(self, root, name, onclick):
def __init__(self, root, name, onclick, oncollapse=None):
QTreeWidgetItem.__init__(self, root, [name])
self.onclick = onclick
self.oncollapse = oncollapse
def setupTree(self):
self.connect(
@ -739,22 +745,31 @@ by clicking on one on the left."""))
p.setColor(QPalette.Base, QColor("#d6dde0"))
self.form.tree.setPalette(p)
self.buildTree()
self.connect(
self.form.tree, SIGNAL("itemExpanded(QTreeWidgetItem*)"),
lambda item: self.onTreeCollapse(item))
self.connect(
self.form.tree, SIGNAL("itemCollapsed(QTreeWidgetItem*)"),
lambda item: self.onTreeCollapse(item))
def buildTree(self):
self.form.tree.clear()
root = self.form.tree
self._systemTagTree(root)
self._favTree(root)
self._decksTree(root)
self._modelTree(root)
self._userTagTree(root)
self.form.tree.expandAll()
self.form.tree.setItemsExpandable(False)
self.form.tree.setIndentation(15)
def onTreeClick(self, item, col):
if getattr(item, 'onclick', None):
item.onclick()
def onTreeCollapse(self, item):
if getattr(item, 'oncollapse', None):
item.oncollapse()
def setFilter(self, *args):
if len(args) == 1:
txt = args[0]
@ -804,6 +819,18 @@ by clicking on one on the left."""))
item.setIcon(0, QIcon(":/icons/" + icon))
return root
def _favTree(self, root):
saved = self.col.conf.get('savedFilters', [])
if not saved:
# Don't add favourites to tree if none saved
return
root = self.CallbackItem(root, _("My Searches"), None)
root.setExpanded(True)
root.setIcon(0, QIcon(":/icons/emblem-favorite-dark.png"))
for name, filt in saved.items():
item = self.CallbackItem(root, name, lambda s=filt: self.setFilter(s))
item.setIcon(0, QIcon(":/icons/emblem-favorite-dark.png"))
def _userTagTree(self, root):
for t in sorted(self.col.tags.all()):
if t.lower() == "marked" or t.lower() == "leech":
@ -817,10 +844,13 @@ by clicking on one on the left."""))
def fillGroups(root, grps, head=""):
for g in grps:
item = self.CallbackItem(
root, g[0], lambda g=g: self.setFilter(
"deck", head+g[0]))
root, g[0],
lambda g=g: self.setFilter("deck", head+g[0]),
lambda g=g: self.mw.col.decks.collapseBrowser(g[1]))
item.setIcon(0, QIcon(":/icons/deck16.png"))
newhead = head + g[0]+"::"
collapsed = self.mw.col.decks.get(g[1]).get('browserCollapsed', False)
item.setExpanded(not collapsed)
fillGroups(item, g[5], newhead)
fillGroups(root, grps)
@ -847,7 +877,7 @@ by clicking on one on the left."""))
d = QDialog(self)
l = QVBoxLayout()
l.setMargin(0)
w = AnkiWebView()
w = AnkiWebView(canCopy=True)
l.addWidget(w)
w.stdHtml(info + "<p>" + reps)
bb = QDialogButtonBox(QDialogButtonBox.Close)
@ -982,8 +1012,7 @@ where id in %s""" % ids2str(sf))
c(self._previewWindow, SIGNAL("finished(int)"), self._onPreviewFinished)
vbox = QVBoxLayout()
vbox.setMargin(0)
self._previewWeb = AnkiWebView()
self._previewWeb.setFocusPolicy(Qt.NoFocus)
self._previewWeb = AnkiWebView(True)
vbox.addWidget(self._previewWeb)
bbox = QDialogButtonBox()
self._previewPrev = bbox.addButton("<", QDialogButtonBox.ActionRole)
@ -1193,7 +1222,9 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
frm = aqt.forms.reposition.Ui_Dialog()
frm.setupUi(d)
(pmin, pmax) = self.col.db.first(
"select min(due), max(due) from cards where type=0")
"select min(due), max(due) from cards where type=0 and odid=0")
pmin = pmin or 0
pmax = pmax or 0
txt = _("Queue top: %d") % pmin
txt += "\n" + _("Queue bottom: %d") % pmax
frm.label.setText(txt)
@ -1708,3 +1739,87 @@ a { margin-right: 1em; }
self.browser.addTags()
elif l == "deletetag":
self.browser.deleteTags()
# Favourites button
######################################################################
class FavouritesLineEdit(QLineEdit):
buttonClicked = pyqtSignal(bool)
def __init__(self, mw, browser, parent=None):
super(FavouritesLineEdit, self).__init__(parent)
self.mw = mw
self.browser = browser
# add conf if missing
if not self.mw.col.conf.has_key('savedFilters'):
self.mw.col.conf['savedFilters'] = {}
self.button = QToolButton(self)
self.button.setStyleSheet('border: 0px;')
self.button.setCursor(Qt.ArrowCursor)
self.button.clicked.connect(self.buttonClicked.emit)
self.setIcon(':/icons/emblem-favorite-off.png')
# flag to raise save or delete dialog on button click
self.doSave = True
# name of current saved filter (if query matches)
self.name = None
self.buttonClicked.connect(self.onClicked)
self.connect(self, SIGNAL("textChanged(QString)"), self.updateButton)
def resizeEvent(self, event):
buttonSize = self.button.sizeHint()
frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)
self.button.move(self.rect().right() - frameWidth - buttonSize.width(),
(self.rect().bottom() - buttonSize.height() + 1) / 2)
super(FavouritesLineEdit, self).resizeEvent(event)
def setIcon(self, path):
self.button.setIcon(QIcon(path))
def setText(self, txt):
super(FavouritesLineEdit, self).setText(txt)
self.updateButton()
def updateButton(self, reset=True):
# If search text is a saved query, switch to the delete button.
# Otherwise show save button.
txt = unicode(self.text()).strip()
for key, value in self.mw.col.conf['savedFilters'].items():
if txt == value:
self.doSave = False
self.name = key
self.setIcon(QIcon(":/icons/emblem-favorite.png"))
return
self.doSave = True
self.setIcon(QIcon(":/icons/emblem-favorite-off.png"))
def onClicked(self):
if self.doSave:
self.saveClicked()
else:
self.deleteClicked()
def saveClicked(self):
txt = unicode(self.text()).strip()
dlg = QInputDialog(self)
dlg.setInputMode(QInputDialog.TextInput)
dlg.setLabelText(_("The current search terms will be added as a new "
"item in the sidebar.\n"
"Search name:"))
dlg.setWindowTitle(_("Save search"))
ok = dlg.exec_()
name = dlg.textValue()
if ok:
self.mw.col.conf['savedFilters'][name] = txt
self.updateButton()
self.browser.setupTree()
def deleteClicked(self):
msg = _('Remove "%s" from your saved searches?') % self.name
ok = QMessageBox.question(self, _('Remove search'),
msg, QMessageBox.Yes, QMessageBox.No)
if ok == QMessageBox.Yes:
self.mw.col.conf['savedFilters'].pop(self.name, None)
self.updateButton()
self.browser.setupTree()

View File

@ -120,9 +120,9 @@ class CardLayout(QDialog):
self.model, joinFields(self.note.fields)))
for g in pform.groupBox, pform.groupBox_2:
g.setTitle(g.title() + _(" (1 of %d)") % max(cnt, 1))
pform.frontWeb = AnkiWebView()
pform.frontWeb = AnkiWebView(True)
pform.frontPrevBox.addWidget(pform.frontWeb)
pform.backWeb = AnkiWebView()
pform.backWeb = AnkiWebView(True)
pform.backPrevBox.addWidget(pform.backWeb)
for wig in pform.frontWeb, pform.backWeb:
wig.page().setLinkDelegationPolicy(

View File

@ -41,7 +41,7 @@ class CustomStudy(QDialog):
def onRadioChange(self, idx):
f = self.form; sp = f.spin
smin = 1; smax = 9999; sval = 1
smin = 1; smax = DYN_MAX_SIZE; sval = 1
post = _("cards")
tit = ""
spShow = True
@ -127,15 +127,15 @@ class CustomStudy(QDialog):
# and then set various options
if i == RADIO_FORGOT:
dyn['delays'] = [1]
dyn['terms'][0] = ['rated:%d:1' % spin, 9999, DYN_RANDOM]
dyn['terms'][0] = ['rated:%d:1' % spin, DYN_MAX_SIZE, DYN_RANDOM]
dyn['resched'] = False
elif i == RADIO_AHEAD:
dyn['delays'] = None
dyn['terms'][0] = ['prop:due<=%d' % spin, 9999, DYN_DUE]
dyn['terms'][0] = ['prop:due<=%d' % spin, DYN_MAX_SIZE, DYN_DUE]
dyn['resched'] = True
elif i == RADIO_PREVIEW:
dyn['delays'] = None
dyn['terms'][0] = ['is:new added:%s'%spin, 9999, DYN_OLDEST]
dyn['terms'][0] = ['is:new added:%s'%spin, DYN_MAX_SIZE, DYN_OLDEST]
dyn['resched'] = False
elif i == RADIO_CRAM:
dyn['delays'] = None

View File

@ -17,6 +17,7 @@ class DeckBrowser(object):
self.mw = mw
self.web = mw.web
self.bottom = aqt.toolbar.BottomBar(mw, mw.bottomWeb)
self.scrollPos = QPoint(0, 0)
def show(self):
clearAudioQueue()
@ -65,6 +66,7 @@ class DeckBrowser(object):
key = unicode(evt.text())
def _selDeck(self, did):
self.scrollPos = self.web.page().mainFrame().scrollPosition()
self.mw.col.decks.select(did)
self.mw.onOverview()
@ -152,7 +154,7 @@ body { margin: 1em; -webkit-user-select: none; }
if self.web.key == "deckBrowser":
return self.web.page().mainFrame().scrollPosition()
else:
return QPoint(0,0)
return self.scrollPos
def _renderStats(self):
cards, thetime = self.mw.col.db.first("""

View File

@ -21,7 +21,7 @@ import anki.js
from BeautifulSoup import BeautifulSoup
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg")
audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv")
audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv", "m4a")
_html = """
<html><head>%s<style>
@ -690,7 +690,7 @@ class Editor(object):
def onCloze(self):
# check that the model is set up for cloze deletion
if '{{cloze:' not in self.note.model()['tmpls'][0]['qfmt']:
if not re.search('{{(.*:)*cloze:',self.note.model()['tmpls'][0]['qfmt']):
if self.addMode:
tooltip(_("Warning, cloze deletions will not work until "
"you switch the type at the top to Cloze."))
@ -988,7 +988,7 @@ to a cloze type first, via Edit>Change Note Type."""))
class EditorWebView(AnkiWebView):
def __init__(self, parent, editor):
AnkiWebView.__init__(self)
AnkiWebView.__init__(self, canFocus=True)
self.editor = editor
self.strip = self.editor.mw.pm.profile['stripHTML']
@ -1117,13 +1117,18 @@ class EditorWebView(AnkiWebView):
url = mime.urls()[0].toString()
# chrome likes to give us the URL twice with a \n
url = url.splitlines()[0]
mime = QMimeData()
newmime = QMimeData()
link = self.editor.urlToLink(url)
if link:
mime.setHtml(link)
newmime.setHtml(link)
elif mime.hasImage():
# if we couldn't convert the url to a link and there's an
# image on the clipboard (such as copy&paste from
# google images in safari), use that instead
return self._processImage(mime)
else:
mime.setText(url)
return mime
newmime.setText(url)
return newmime
# if the user has used 'copy link location' in the browser, the clipboard
# will contain the URL as text, and no URLs or HTML. the URL will already

View File

@ -75,7 +75,7 @@ into a bug report:""")
pluginText = _("""\
An error occurred in an add-on.<br>
Please post on the add-on forum:<br>%s<br>""")
pluginText %= "https://groups.google.com/forum/#!forum/anki-addons"
pluginText %= "https://anki.tenderapp.com/discussions/add-ons"
if "addon" in error:
txt = pluginText
else:

View File

@ -17,7 +17,6 @@ import aqt.forms
import aqt.modelchooser
import aqt.deckchooser
class ChangeMap(QDialog):
def __init__(self, mw, model, current):
QDialog.__init__(self, mw, Qt.Window)
@ -80,6 +79,9 @@ class ImportDialog(QDialog):
self.updateDelimiterButtonText()
self.frm.allowHTML.setChecked(self.mw.pm.profile.get('allowHTML', True))
self.frm.importMode.setCurrentIndex(self.mw.pm.profile.get('importMode', 1))
# import button
b = QPushButton(_("Import"))
self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole)
self.exec_()
def setupOptions(self):
@ -88,8 +90,6 @@ class ImportDialog(QDialog):
self.mw, self.frm.modelArea, label=False)
self.deck = aqt.deckchooser.DeckChooser(
self.mw, self.frm.deckArea, label=False)
self.connect(self.frm.importButton, SIGNAL("clicked()"),
self.doImport)
def modelChanged(self):
self.importer.model = self.mw.col.models.current()
@ -139,8 +139,8 @@ you can enter it here. Use \\t to represent tab."""),
d = `d`
txt = _("Fields separated by: %s") % d
self.frm.autoDetect.setText(txt)
def doImport(self, update=False):
def accept(self):
self.importer.mapping = self.mapping
if not self.importer.mappingOk():
showWarning(
@ -250,7 +250,7 @@ you can enter it here. Use \\t to represent tab."""),
QDialog.reject(self)
def helpRequested(self):
openHelp("FileImport")
openHelp("importing")
def showUnicodeWarning():
@ -338,6 +338,8 @@ with a different browser.""")
msg = _("""\
Invalid file. Please restore from backup.""")
showWarning(msg)
elif "invalidTempFolder" in err:
showWarning(mw.errorHandler.tempFolderMsg())
elif "readonly" in err:
showWarning(_("""\
Unable to import from a read-only file."""))

View File

@ -4,7 +4,7 @@
import re
import signal
import zipfile
import zipfile
from send2trash import send2trash
from aqt.qt import *
@ -63,6 +63,8 @@ class AnkiQt(QMainWindow):
self.onAppMsg(unicode(args[0], sys.getfilesystemencoding(), "ignore"))
# Load profile in a timer so we can let the window finish init and not
# close on profile load error.
if isMac and qtmajor >= 5:
self.show()
self.progress.timer(10, self.setupProfile, False)
def setupUI(self):
@ -264,15 +266,19 @@ To import into a password protected profile, please open the profile before atte
def loadCollection(self):
self.hideSchemaMsg = True
cpath = self.pm.collectionPath()
try:
self.col = Collection(self.pm.collectionPath(), log=True)
self.col = Collection(cpath, log=True)
except anki.db.Error:
# move back to profile manager
# warn user
showWarning("""\
Your collection is corrupt. Please see the manual for \
how to restore from a backup.""")
self.unloadProfile()
raise
# move it out of the way so the profile can be used again
newpath = cpath+str(intTime())
os.rename(cpath, newpath)
# then close
sys.exit(1)
except Exception, e:
# the custom exception handler won't catch this if we immediately
# unload, so we have to manually handle it
@ -377,7 +383,9 @@ the manual for information on how to restore from an automatic backup."))
if cleanup:
cleanup(state)
self.state = state
runHook('beforeStateChange', state, oldState, *args)
getattr(self, "_"+state+"State")(oldState, *args)
runHook('afterStateChange', state, oldState, *args)
def _deckBrowserState(self, oldState):
self.deckBrowser.show()
@ -405,10 +413,12 @@ the manual for information on how to restore from an automatic backup."))
def _reviewState(self, oldState):
self.reviewer.show()
self.web.setCanFocus(True)
def _reviewCleanup(self, newState):
if newState != "resetRequired" and newState != "review":
self.reviewer.cleanup()
self.web.setCanFocus(False)
def noteChanged(self, nid):
"Called when a card or note is edited (but not deleted)."
@ -504,7 +514,7 @@ title="%s">%s</button>''' % (
self.toolbar = aqt.toolbar.Toolbar(self, tweb)
self.toolbar.draw()
# main area
self.web = aqt.webview.AnkiWebView()
self.web = aqt.webview.AnkiWebView(canFocus=True)
self.web.setObjectName("mainText")
self.web.setFocusPolicy(Qt.WheelFocus)
self.web.setMinimumWidth(400)

View File

@ -123,7 +123,7 @@ to their original deck.""")
counts = list(self.mw.col.sched.counts())
finished = not sum(counts)
for n in range(len(counts)):
if counts[n] == 1000:
if counts[n] >= 1000:
counts[n] = "1000+"
but = self.mw.button
if finished:

View File

@ -18,6 +18,8 @@ class Preferences(QDialog):
self.prof = self.mw.pm.profile
self.form = aqt.forms.preferences.Ui_Preferences()
self.form.setupUi(self)
self.form.buttonBox.button(QDialogButtonBox.Help).setAutoDefault(False)
self.form.buttonBox.button(QDialogButtonBox.Close).setAutoDefault(False)
self.connect(self.form.buttonBox, SIGNAL("helpRequested()"),
lambda: openHelp("profileprefs"))
self.setupCollection()

View File

@ -179,7 +179,7 @@ documentation for information on using a flash drive.""")
def _defaultBase(self):
if isWin:
if qtmajor >= 5:
if False: #qtmajor >= 5:
loc = QStandardPaths.writeableLocation(QStandardPaths.DocumentsLocation)
else:
loc = QDesktopServices.storageLocation(QDesktopServices.DocumentsLocation)
@ -284,7 +284,15 @@ please see:
def _onLangSelected(self):
f = self.langForm
code = langs[f.lang.currentRow()][1]
obj = langs[f.lang.currentRow()]
code = obj[1]
name = obj[0]
en = "Are you sure you wish to display Anki's interface in %s?"
r = QMessageBox.question(
None, "Anki", en%name, QMessageBox.Yes | QMessageBox.No,
QMessageBox.No)
if r != QMessageBox.Yes:
return self._setDefaultLang()
self.meta['defaultLang'] = code
sql = "update profiles set data = ? where name = ?"
self.db.execute(sql, cPickle.dumps(self.meta), "_global")

View File

@ -2,9 +2,13 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
# imports are all in this file to make moving to pyside easier in the future
# fixme: make sure not to optimize imports on this file
import sip
import os
import sip, os
from anki.utils import isWin, isMac
sip.setapi('QString', 2)
sip.setapi('QVariant', 2)
sip.setapi('QUrl', 2)
@ -18,14 +22,16 @@ from PyQt4.QtGui import *
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
from PyQt4.QtNetwork import QLocalServer, QLocalSocket
def debug():
from PyQt4.QtCore import pyqtRemoveInputHook
from pdb import set_trace
pyqtRemoveInputHook()
set_trace()
import sys, traceback
if os.environ.get("DEBUG"):
import sys, traceback
def info(type, value, tb):
from PyQt4.QtCore import pyqtRemoveInputHook
for line in traceback.format_exception(type, value, tb):
@ -43,8 +49,3 @@ if qtmajor <= 4 and qtminor <= 6:
import anki.template.furigana
anki.template.furigana.ruby = r'<span style="display: inline-block; text-align: center; line-height: 1; white-space: nowrap; vertical-align: baseline; margin: 0; padding: 0"><span style="display: block; text-decoration: none; line-height: 1.2; font-weight: normal; font-size: 0.64em">\2</span>\1</span>'
if isWin or isMac:
# we no longer use this, but want it included in the mac+win builds
# so we don't break add-ons that use it. any new add-ons should use
# the above variables instead
from PyQt4 import pyqtconfig

View File

@ -134,7 +134,8 @@ function _updateQA (q, answerMode, klass) {
typeans.focus();
}
if (answerMode) {
window.location = "#answer";
var e = $("#answer");
if (e[0]) { e[0].scrollIntoView(); }
} else {
window.scrollTo(0, 0);
}
@ -433,10 +434,11 @@ Please run Tools>Empty Cards""")
return txt.split("::")[0]
return txt
matches = [noHint(txt) for txt in matches]
if len(matches) > 1:
txt = ", ".join(matches)
else:
uniqMatches = set(matches)
if len(uniqMatches) == 1:
txt = matches[0]
else:
txt = ", ".join(matches)
return txt
def tokenizeComparison(self, given, correct):

View File

@ -170,6 +170,8 @@ AnkiWeb is too busy at the moment. Please try again in a few minutes.""")
elif "10061" in err or "10013" in err or "10053" in err:
return _(
"Antivirus or firewall software is preventing Anki from connecting to the internet.")
elif "10054" in err or "Broken pipe" in err:
return _("Connection timed out. Either your internet connection is experiencing problems, or you have a very large file in your media folder.")
elif "Unable to find the server" in err:
return _(
"Server not found. Either your connection is down, or antivirus/firewall "
@ -178,6 +180,8 @@ AnkiWeb is too busy at the moment. Please try again in a few minutes.""")
return _("Proxy authentication required.")
elif "code: 413" in err:
return _("Your collection or a media file is too large to sync.")
elif "EOF occurred in violation of protocol" in err:
return _("Error establishing a secure connection. This is usually caused by filtering software, or problems with your ISP.")
return err
def _getUserPass(self):

View File

@ -240,7 +240,7 @@ def getFile(parent, title, cb, filter="*.*", dir=None, key=None):
ret = []
def accept():
# work around an osx crash
aqt.mw.app.processEvents()
#aqt.mw.app.processEvents()
file = unicode(list(d.selectedFiles())[0])
if dirkey:
dir = os.path.dirname(file)
@ -394,6 +394,7 @@ def tooltip(msg, period=3000, parent=None):
lab.setWindowFlags(Qt.ToolTip)
p = QPalette()
p.setColor(QPalette.Window, QColor("#feffc4"))
p.setColor(QPalette.WindowText, QColor("#000000"))
lab.setPalette(p)
lab.move(
aw.mapToGlobal(QPoint(0, -100 + aw.height())))

View File

@ -40,7 +40,8 @@ class AnkiWebPage(QWebPage):
class AnkiWebView(QWebView):
def __init__(self):
# canFocus implies canCopy
def __init__(self, canFocus=False, canCopy=False):
QWebView.__init__(self)
self.setRenderHints(
QPainter.TextAntialiasing |
@ -59,6 +60,8 @@ class AnkiWebView(QWebView):
self.allowDrops = False
# reset each time new html is set; used to detect if still in same state
self.key = None
self.setCanFocus(canFocus)
self._canCopy = canCopy or canFocus
def keyPressEvent(self, evt):
if evt.matches(QKeySequence.Copy):
@ -78,9 +81,7 @@ class AnkiWebView(QWebView):
QWebView.keyReleaseEvent(self, evt)
def contextMenuEvent(self, evt):
# lazy: only run in reviewer
import aqt
if aqt.mw.state != "review":
if not self._canCopy:
return
m = QMenu(self)
a = m.addAction(_("Copy"))
@ -131,6 +132,13 @@ button {
def setBridge(self, bridge):
self._bridge.setBridge(bridge)
def setCanFocus(self, canFocus=False):
self._canFocus = canFocus
if self._canFocus:
self.setFocusPolicy(Qt.WheelFocus)
else:
self.setFocusPolicy(Qt.NoFocus)
def eval(self, js):
self.page().mainFrame().evaluateJavaScript(js)

View File

@ -46,7 +46,7 @@
<number>1</number>
</property>
<property name="maximum">
<number>9999</number>
<number>99999</number>
</property>
</widget>
</item>

View File

@ -35,6 +35,8 @@
<file>icons/editclear.png</file>
<file>icons/view-statistics.png</file>
<file>icons/emblem-favorite.png</file>
<file>icons/emblem-favorite-dark.png</file>
<file>icons/emblem-favorite-off.png</file>
<file>icons/view-pim-calendar.png</file>
<file>icons/anki-tag.png</file>
<file>icons/edit-redo.png</file>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 514 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 537 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 482 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 457 B

View File

@ -94,20 +94,6 @@
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="importButton">
<property name="text">
<string>&amp;Import</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QScrollArea" name="mappingArea">
<property name="sizePolicy">
@ -133,8 +119,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>402</width>
<height>206</height>
<width>529</width>
<height>251</height>
</rect>
</property>
</widget>
@ -158,7 +144,6 @@
</layout>
</widget>
<tabstops>
<tabstop>importButton</tabstop>
<tabstop>buttonBox</tabstop>
</tabstops>
<resources/>

View File

@ -9,8 +9,27 @@ def assertException(exception, func):
found = True
assert found
def getEmptyDeck(**kwargs):
# Creating new decks is expensive. Just do it once, and then spin off
# copies from the master.
def getEmptyCol():
if len(getEmptyCol.master) == 0:
(fd, nam) = tempfile.mkstemp(suffix=".anki2")
os.close(fd)
os.unlink(nam)
col = aopen(nam)
col.db.close()
getEmptyCol.master = nam
(fd, nam) = tempfile.mkstemp(suffix=".anki2")
shutil.copy(getEmptyCol.master, nam)
return aopen(nam)
getEmptyCol.master = ""
# Fallback for when the DB needs options passed in.
def getEmptyDeckWith(**kwargs):
(fd, nam) = tempfile.mkstemp(suffix=".anki2")
os.close(fd)
os.unlink(nam)
return aopen(nam, **kwargs)

View File

@ -1,9 +1,9 @@
# coding: utf-8
from tests.shared import getEmptyDeck
from tests.shared import getEmptyCol
def test_previewCards():
deck = getEmptyDeck()
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = u'1'
f['Back'] = u'2'
@ -23,7 +23,7 @@ def test_previewCards():
assert deck.cardCount() == 1
def test_delete():
deck = getEmptyDeck()
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = u'1'
f['Back'] = u'2'
@ -39,7 +39,7 @@ def test_delete():
assert deck.db.scalar("select count() from graves") == 2
def test_misc():
d = getEmptyDeck()
d = getEmptyCol()
f = d.newNote()
f['Front'] = u'1'
f['Back'] = u'2'
@ -49,7 +49,7 @@ def test_misc():
assert c.template()['ord'] == 0
def test_genrem():
d = getEmptyDeck()
d = getEmptyCol()
f = d.newNote()
f['Front'] = u'1'
f['Back'] = u''
@ -76,7 +76,7 @@ def test_genrem():
assert len(f.cards()) == 2
def test_gendeck():
d = getEmptyDeck()
d = getEmptyCol()
cloze = d.models.byName("Cloze")
d.models.setCurrent(cloze)
f = d.newNote()

View File

@ -1,7 +1,7 @@
# coding: utf-8
import os
from tests.shared import assertException, getEmptyDeck
import os, tempfile
from tests.shared import assertException, getEmptyCol
from anki.stdmodels import addBasicModel
from anki import Collection as aopen
@ -11,8 +11,9 @@ newMod = None
def test_create():
global newPath, newMod
path = "/tmp/test_attachNew.anki2"
(fd, path) = tempfile.mkstemp(suffix=".anki2", prefix="test_attachNew")
try:
os.close(fd)
os.unlink(path)
except OSError:
pass
@ -40,7 +41,7 @@ def test_openReadOnly():
os.unlink(newPath)
def test_noteAddDelete():
deck = getEmptyDeck()
deck = getEmptyCol()
# add a note
f = deck.newNote()
f['Front'] = u"one"; f['Back'] = u"two"
@ -79,7 +80,7 @@ def test_noteAddDelete():
assert f2.dupeOrEmpty()
def test_fieldChecksum():
deck = getEmptyDeck()
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = u"new"; f['Back'] = u"new2"
deck.addNote(f)
@ -92,7 +93,7 @@ def test_fieldChecksum():
"select csum from notes") == int("302811ae", 16)
def test_addDelTags():
deck = getEmptyDeck()
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = u"1"
deck.addNote(f)
@ -111,14 +112,14 @@ def test_addDelTags():
assert len(f.tags) == 2
def test_timestamps():
deck = getEmptyDeck()
deck = getEmptyCol()
assert len(deck.models.models) == 4
for i in range(100):
addBasicModel(deck)
assert len(deck.models.models) == 104
def test_furigana():
deck = getEmptyDeck()
deck = getEmptyCol()
mm = deck.models
m = mm.current()
# filter should work

View File

@ -1,9 +1,9 @@
# coding: utf-8
from tests.shared import assertException, getEmptyDeck
from tests.shared import assertException, getEmptyCol
def test_basic():
deck = getEmptyDeck()
deck = getEmptyCol()
# we start with a standard deck
assert len(deck.decks.decks) == 1
# it should have an id of 1
@ -42,7 +42,7 @@ def test_basic():
deck.sched.deckDueList()
def test_remove():
deck = getEmptyDeck()
deck = getEmptyCol()
# create a new deck, and add a note/card to it
g1 = deck.decks.id("g1")
f = deck.newNote()
@ -68,7 +68,7 @@ def test_remove():
assert deck.noteCount() == 0
def test_rename():
d = getEmptyDeck()
d = getEmptyCol()
id = d.decks.id("hello::world")
# should be able to rename into a completely different branch, creating
# parents as necessary
@ -89,7 +89,7 @@ def test_rename():
assert n in d.decks.allNames()
def test_renameForDragAndDrop():
d = getEmptyDeck()
d = getEmptyCol()
def deckNames():
return [ name for name in sorted(d.decks.allNames()) if name <> u'Default' ]

View File

@ -4,7 +4,7 @@ import nose, os, tempfile
from anki import Collection as aopen
from anki.exporting import *
from anki.importing import Anki2Importer
from shared import getEmptyDeck
from shared import getEmptyCol
deck = None
ds = None
@ -12,7 +12,7 @@ testDir = os.path.dirname(__file__)
def setup1():
global deck
deck = getEmptyDeck()
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = ["tag", "tag2"]
deck.addNote(f)
@ -36,7 +36,9 @@ def test_export_anki():
deck.decks.setConf(dobj, confId)
# export
e = AnkiExporter(deck)
newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".anki2")[1])
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
newname = unicode(newname)
os.close(fd)
os.unlink(newname)
e.exportInto(newname)
# exporting should not have changed conf for original deck
@ -54,7 +56,9 @@ def test_export_anki():
# conf should be 1
assert dobj['conf'] == 1
# try again, limited to a deck
newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".anki2")[1])
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
newname = unicode(newname)
os.close(fd)
os.unlink(newname)
e.did = 1
e.exportInto(newname)
@ -69,13 +73,15 @@ def test_export_ankipkg():
n['Front'] = u'[sound:今日.mp3]'
deck.addNote(n)
e = AnkiPackageExporter(deck)
newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".apkg")[1])
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".apkg")
newname = unicode(newname)
os.close(fd)
os.unlink(newname)
e.exportInto(newname)
@nose.with_setup(setup1)
def test_export_anki_due():
deck = getEmptyDeck()
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = u"foo"
deck.addNote(f)
@ -92,11 +98,13 @@ def test_export_anki_due():
# export
e = AnkiExporter(deck)
e.includeSched = True
newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".anki2")[1])
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
newname = unicode(newname)
os.close(fd)
os.unlink(newname)
e.exportInto(newname)
# importing into a new deck, the due date should be equivalent
deck2 = getEmptyDeck()
deck2 = getEmptyCol()
imp = Anki2Importer(deck2, newname)
imp.run()
c = deck2.getCard(c.id)
@ -115,7 +123,9 @@ def test_export_anki_due():
@nose.with_setup(setup1)
def test_export_textnote():
e = TextNoteExporter(deck)
f = unicode(tempfile.mkstemp(prefix="ankitest")[1])
fd, f = tempfile.mkstemp(prefix="ankitest")
f = unicode(f)
os.close(fd)
os.unlink(f)
e.exportInto(f)
e.includeTags = True

View File

@ -1,7 +1,7 @@
# coding: utf-8
from anki.find import Finder
from tests.shared import getEmptyDeck
from tests.shared import getEmptyCol
def test_parse():
f = Finder(None)
@ -20,7 +20,7 @@ def test_parse():
assert f._tokenize("deck:'two words'") == ["deck:two words"]
def test_findCards():
deck = getEmptyDeck()
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = u'dog'
f['Back'] = u'cat'
@ -216,7 +216,7 @@ def test_findCards():
assert len(deck.findCards("added:2")) == deck.cardCount()
def test_findReplace():
deck = getEmptyDeck()
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = u'foo'
f['Back'] = u'bar'
@ -243,7 +243,7 @@ def test_findReplace():
f.load(); assert f['Back'] == "reg"
def test_findDupes():
deck = getEmptyDeck()
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = u'foo'
f['Back'] = u'bar'

View File

@ -1,7 +1,7 @@
# coding: utf-8
import os
from tests.shared import getUpgradeDeckPath, getEmptyDeck
from tests.shared import getUpgradeDeckPath, getEmptyCol
from anki.upgrade import Upgrader
from anki.utils import ids2str
from anki.importing import Anki1Importer, Anki2Importer, TextImporter, \
@ -27,7 +27,7 @@ def test_anki2():
open(os.path.join(src.media.dir(), "_foo.jpg"), "w").write("foo")
src.close()
# create a new empty deck
dst = getEmptyDeck()
dst = getEmptyCol()
# import src into dst
imp = Anki2Importer(dst, srcpath)
imp.run()
@ -50,7 +50,7 @@ def test_anki2():
assert len(os.listdir(dst.media.dir())) == 1
def test_anki2_mediadupes():
tmp = getEmptyDeck()
tmp = getEmptyCol()
# add a note that references a sound
n = tmp.newNote()
n['Front'] = "[sound:foo.mp3]"
@ -60,7 +60,7 @@ def test_anki2_mediadupes():
open(os.path.join(tmp.media.dir(), "foo.mp3"), "w").write("foo")
tmp.close()
# it should be imported correctly into an empty deck
empty = getEmptyDeck()
empty = getEmptyCol()
imp = Anki2Importer(empty, tmp.path)
imp.run()
assert os.listdir(empty.media.dir()) == ["foo.mp3"]
@ -95,7 +95,7 @@ def test_anki2_mediadupes():
assert "_" in n.fields[0]
def test_apkg():
tmp = getEmptyDeck()
tmp = getEmptyCol()
apkg = unicode(os.path.join(testDir, "support/media.apkg"))
imp = AnkiPackageImporter(tmp, apkg)
assert os.listdir(tmp.media.dir()) == []
@ -122,7 +122,7 @@ def test_anki1():
os.mkdir(mdir)
open(os.path.join(mdir, "_foo.jpg"), "w").write("foo")
# create a new empty deck
dst = getEmptyDeck()
dst = getEmptyCol()
# import src into dst
imp = Anki1Importer(dst, tmp)
imp.run()
@ -138,7 +138,7 @@ def test_anki1():
def test_anki1_diffmodels():
# create a new empty deck
dst = getEmptyDeck()
dst = getEmptyCol()
# import the 1 card version of the model
tmp = getUpgradeDeckPath("diffmodels1.anki")
imp = Anki1Importer(dst, tmp)
@ -165,7 +165,7 @@ def test_anki1_diffmodels():
def test_suspended():
# create a new empty deck
dst = getEmptyDeck()
dst = getEmptyCol()
# import the 1 card version of the model
tmp = getUpgradeDeckPath("suspended12.anki")
imp = Anki1Importer(dst, tmp)
@ -174,7 +174,7 @@ def test_suspended():
def test_anki2_diffmodels():
# create a new empty deck
dst = getEmptyDeck()
dst = getEmptyCol()
# import the 1 card version of the model
tmp = getUpgradeDeckPath("diffmodels2-1.apkg")
imp = AnkiPackageImporter(dst, tmp)
@ -206,7 +206,7 @@ def test_anki2_diffmodels():
def test_anki2_updates():
# create a new empty deck
dst = getEmptyDeck()
dst = getEmptyCol()
tmp = getUpgradeDeckPath("update1.apkg")
imp = AnkiPackageImporter(dst, tmp)
imp.run()
@ -232,7 +232,7 @@ def test_anki2_updates():
assert dst.db.scalar("select flds from notes").startswith("goodbye")
def test_csv():
deck = getEmptyDeck()
deck = getEmptyCol()
file = unicode(os.path.join(testDir, "support/text-2fields.txt"))
i = TextImporter(deck, file)
i.initMapping()
@ -266,7 +266,7 @@ def test_csv():
deck.close()
def test_csv2():
deck = getEmptyDeck()
deck = getEmptyCol()
mm = deck.models
m = mm.current()
f = mm.newField("Three")
@ -289,7 +289,7 @@ def test_csv2():
deck.close()
def test_supermemo_xml_01_unicode():
deck = getEmptyDeck()
deck = getEmptyCol()
file = unicode(os.path.join(testDir, "support/supermemo1.xml"))
i = SupermemoXmlImporter(deck, file)
#i.META.logToStdOutput = True
@ -297,12 +297,13 @@ def test_supermemo_xml_01_unicode():
assert i.total == 1
cid = deck.db.scalar("select id from cards")
c = deck.getCard(cid)
assert c.factor == 5701
# Applies A Factor-to-E Factor conversion
assert c.factor == 2879
assert c.reps == 7
deck.close()
def test_mnemo():
deck = getEmptyDeck()
deck = getEmptyCol()
file = unicode(os.path.join(testDir, "support/mnemo.db"))
i = MnemosyneImporter(deck, file)
i.run()

View File

@ -1,14 +1,14 @@
# coding: utf-8
import os
from tests.shared import getEmptyDeck
from tests.shared import getEmptyCol
from anki.utils import stripHTML
def test_latex():
d = getEmptyDeck()
d = getEmptyCol()
# change latex cmd to simulate broken build
import anki.latex
anki.latex.latexCmd[0] = "nolatex"
anki.latex.latexCmds[0][0] = "nolatex"
# add a note with latex
f = d.newNote()
f['Front'] = u"[latex]hello[/latex]"
@ -17,7 +17,7 @@ def test_latex():
assert len(os.listdir(d.media.dir())) == 0
# check the error message
msg = f.cards()[0].q()
assert "executing latex" in msg
assert "executing nolatex" in msg
assert "installed" in msg
# check if we have latex installed, and abort test if we don't
for cmd in ("latex", "dvipng"):
@ -26,7 +26,7 @@ def test_latex():
print "aborting test; %s is not installed" % cmd
return
# fix path
anki.latex.latexCmd[0] = "latex"
anki.latex.latexCmds[0][0] = "latex"
# check media db should cause latex to be generated
d.media.check()
assert len(os.listdir(d.media.dir())) == 1

View File

@ -4,12 +4,12 @@ import tempfile
import os
import time
from shared import getEmptyDeck, testDir
from shared import getEmptyCol, testDir
# copying files to media folder
def test_add():
d = getEmptyDeck()
d = getEmptyCol()
dir = tempfile.mkdtemp(prefix="anki")
path = os.path.join(dir, u"foo.jpg")
open(path, "w").write("hello")
@ -22,7 +22,7 @@ def test_add():
assert d.media.addFile(path) == "foo (1).jpg"
def test_strings():
d = getEmptyDeck()
d = getEmptyCol()
mf = d.media.filesInStr
mid = d.models.models.keys()[0]
assert mf(mid, "aoeu") == []
@ -46,7 +46,7 @@ def test_strings():
assert es('<img src="foo bar.jpg">') == '<img src="foo%20bar.jpg">'
def test_deckIntegration():
d = getEmptyDeck()
d = getEmptyCol()
# create a media dir
d.media.dir()
# put a file into it
@ -68,7 +68,7 @@ def test_deckIntegration():
assert ret[1] == ["foo.jpg"]
def test_changes():
d = getEmptyDeck()
d = getEmptyCol()
assert d.media._changed()
def added():
return d.media.db.execute("select fname from log where type = 0")
@ -103,7 +103,7 @@ def test_changes():
assert len(list(d.media.removed())) == 1
def test_illegal():
d = getEmptyDeck()
d = getEmptyCol()
aString = u"a:b|cd\\e/f\0g*h"
good = u"abcdefgh"
assert d.media.stripIllegal(aString) == good

View File

@ -1,10 +1,10 @@
# coding: utf-8
from tests.shared import getEmptyDeck
from tests.shared import getEmptyCol
from anki.utils import stripHTML, joinFields
def test_modelDelete():
deck = getEmptyDeck()
deck = getEmptyCol()
f = deck.newNote()
f['Front'] = u'1'
f['Back'] = u'2'
@ -14,7 +14,7 @@ def test_modelDelete():
assert deck.cardCount() == 0
def test_modelCopy():
deck = getEmptyDeck()
deck = getEmptyCol()
m = deck.models.current()
m2 = deck.models.copy(m)
assert m2['name'] == "Basic copy"
@ -27,7 +27,7 @@ def test_modelCopy():
assert deck.models.scmhash(m) == deck.models.scmhash(m2)
def test_fields():
d = getEmptyDeck()
d = getEmptyCol()
f = d.newNote()
f['Front'] = u'1'
f['Back'] = u'2'
@ -74,7 +74,7 @@ def test_fields():
assert d.getNote(d.models.nids(m)[0]).fields == ["", "2", "1"]
def test_templates():
d = getEmptyDeck()
d = getEmptyCol()
m = d.models.current(); mm = d.models
t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}"
@ -107,8 +107,31 @@ def test_templates():
mm.addTemplate(m, t)
assert not d.models.remTemplate(m, m['tmpls'][0])
def test_cloze_ordinals():
d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze"))
m = d.models.current(); mm = d.models
#We replace the default Cloze template
t = mm.newTemplate("ChainedCloze")
t['qfmt'] = "{{text:cloze:Text}}"
t['afmt'] = "{{text:cloze:Text}}"
mm.addTemplate(m, t)
mm.save(m)
d.models.remTemplate(m, m['tmpls'][0])
f = d.newNote()
f['Text'] = u'{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}'
d.addNote(f)
assert d.cardCount() == 2
(c, c2) = f.cards()
# first card should have first ord
assert c.ord == 0
assert c2.ord == 1
def test_text():
d = getEmptyDeck()
d = getEmptyCol()
m = d.models.current()
m['tmpls'][0]['qfmt'] = "{{text:Front}}"
d.models.save(m)
@ -118,7 +141,7 @@ def test_text():
assert "helloworld" in f.cards()[0].q()
def test_cloze():
d = getEmptyDeck()
d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze"))
f = d.newNote()
assert f.model()['name'] == "Cloze"
@ -163,8 +186,31 @@ def test_cloze():
f.flush()
assert len(f.cards()) == 2
def test_chained_mods():
d = getEmptyCol()
d.models.setCurrent(d.models.byName("Cloze"))
m = d.models.current(); mm = d.models
#We replace the default Cloze template
t = mm.newTemplate("ChainedCloze")
t['qfmt'] = "{{cloze:text:Text}}"
t['afmt'] = "{{cloze:text:Text}}"
mm.addTemplate(m, t)
mm.save(m)
d.models.remTemplate(m, m['tmpls'][0])
f = d.newNote()
q1 = '<span style=\"color:red\">phrase</span>'
a1 = '<b>sentence</b>'
q2 = '<span style=\"color:red\">en chaine</span>'
a2 = '<i>chained</i>'
f['Text'] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (q1,a1,q2,a2)
assert d.addNote(f) == 1
assert "This <span class=cloze>[sentence]</span> demonstrates <span class=cloze>[chained]</span> clozes." in f.cards()[0].q()
assert "This <span class=cloze>phrase</span> demonstrates <span class=cloze>en chaine</span> clozes." in f.cards()[0].a()
def test_modelChange():
deck = getEmptyDeck()
deck = getEmptyCol()
basic = deck.models.byName("Basic")
cloze = deck.models.byName("Cloze")
# enable second template and add a note
@ -238,7 +284,7 @@ def test_modelChange():
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1
def test_availOrds():
d = getEmptyDeck()
d = getEmptyCol()
m = d.models.current(); mm = d.models
t = m['tmpls'][0]
f = d.newNote()

View File

@ -1,171 +0,0 @@
# coding: utf-8
import nose, os, time
from tests.shared import assertException
from anki.sync import Syncer, FullSyncer, RemoteServer, \
MediaSyncer, RemoteMediaServer, httpCon
from anki import Collection as aopen
deck1=None
deck2=None
client=None
server=None
server2=None
# Remote tests
##########################################################################
import tests.test_sync as ts
from tests.test_sync import setup_basic
import anki.sync
anki.sync.SYNC_URL = "http://localhost:5000/sync/"
TEST_USER = "synctest@ichi2.net"
TEST_PASS = "abc123"
TEST_HKEY = "WqYF0m7fOHCNPI4a"
TEST_REMOTE = True
def setup_remote():
setup_basic()
# mark deck1 as changed
ts.deck1.save()
ts.server = RemoteServer(TEST_HKEY)
ts.client.server = ts.server
@nose.with_setup(setup_remote)
def test_meta():
global TEST_REMOTE
try:
# if the key is wrong, meta returns nothing
ts.server.hkey = "abc"
assert not ts.server.meta()
except Exception, e:
if e.errno == 61:
TEST_REMOTE = False
print "aborting; server offline"
return
ts.server.hkey = TEST_HKEY
meta = ts.server.meta()
assert meta['mod']
assert meta['scm']
assert meta['mod'] != ts.client.col.mod
assert abs(meta['ts'] - time.time()) < 3
@nose.with_setup(setup_remote)
def test_hkey():
if not TEST_REMOTE:
return
assert not ts.server.hostKey(TEST_USER, "wrongpass")
ts.server.hkey = "willchange"
k = ts.server.hostKey(TEST_USER, TEST_PASS)
assert k == ts.server.hkey == TEST_HKEY
@nose.with_setup(setup_remote)
def test_download():
if not TEST_REMOTE:
return
f = FullSyncer(ts.client.col, "abc", ts.server.con)
assertException(Exception, f.download)
f.hkey = TEST_HKEY
f.download()
@nose.with_setup(setup_remote)
def test_remoteSync():
if not TEST_REMOTE:
return
# not yet associated, so will require a full sync
assert ts.client.sync() == "fullSync"
# upload
f = FullSyncer(ts.client.col, TEST_HKEY, ts.server.con)
assert f.upload()
ts.client.col.reopen()
# should report no changes
assert ts.client.sync() == "noChanges"
# bump local col
ts.client.col.setMod()
ts.client.col.save()
assert ts.client.sync() == "success"
# again, no changes
assert ts.client.sync() == "noChanges"
# Remote media tests
##########################################################################
# We can't run useful tests for local media, because the desktop code assumes
# the current directory is the media folder.
def setup_remoteMedia():
setup_basic()
con = httpCon()
ts.server = RemoteMediaServer(TEST_HKEY, con)
ts.server2 = RemoteServer(TEST_HKEY)
ts.client = MediaSyncer(ts.deck1, ts.server)
@nose.with_setup(setup_remoteMedia)
def test_media():
if not TEST_REMOTE:
return
print "media test disabled"
return
ts.server.mediatest("reset")
assert len(os.listdir(ts.deck1.media.dir())) == 0
assert ts.server.mediatest("count") == 0
# initially, nothing to do
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
# add a file
time.sleep(1)
os.chdir(ts.deck1.media.dir())
p = os.path.join(ts.deck1.media.dir(), "foo.jpg")
open(p, "wb").write("foo")
assert len(os.listdir(ts.deck1.media.dir())) == 1
assert ts.server.mediatest("count") == 0
assert ts.client.sync(ts.server2.meta()[4]) == "success"
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
time.sleep(1)
# should have been synced
assert len(os.listdir(ts.deck1.media.dir())) == 1
assert ts.server.mediatest("count") == 1
# if we remove the file, should be removed
os.unlink(p)
assert ts.client.sync(ts.server2.meta()[4]) == "success"
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
assert len(os.listdir(ts.deck1.media.dir())) == 0
assert ts.server.mediatest("count") == 0
# we should be able to add it again
time.sleep(1)
open(p, "wb").write("foo")
assert ts.client.sync(ts.server2.meta()[4]) == "success"
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
assert len(os.listdir(ts.deck1.media.dir())) == 1
assert ts.server.mediatest("count") == 1
# if we modify it, it should get sent too. also we set the zip size very
# low here, so that we can test splitting into multiple zips
import anki.media; anki.media.SYNC_ZIP_SIZE = 1
time.sleep(1)
open(p, "wb").write("bar")
open(p+"2", "wb").write("baz")
assert len(os.listdir(ts.deck1.media.dir())) == 2
assert ts.client.sync(ts.server2.meta()[4]) == "success"
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
assert len(os.listdir(ts.deck1.media.dir())) == 2
assert ts.server.mediatest("count") == 2
# if we lose our media db, we should be able to bring it back in sync
time.sleep(1)
ts.deck1.media.close()
os.unlink(ts.deck1.media.dir()+".db")
ts.deck1.media.connect()
assert ts.client.sync(ts.server2.meta()[4]) == "success"
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
assert len(os.listdir(ts.deck1.media.dir())) == 2
assert ts.server.mediatest("count") == 2
# if we send an unchanged file, the server should cope
time.sleep(1)
ts.deck1.media.db.execute("insert into log values ('foo.jpg', 0)")
assert ts.client.sync(ts.server2.meta()[4]) == "success"
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
assert len(os.listdir(ts.deck1.media.dir())) == 2
assert ts.server.mediatest("count") == 2
# if we remove foo.jpg on the ts.server, the removal should be synced
assert ts.server.mediatest("removefoo") == "OK"
assert ts.client.sync(ts.server2.meta()[4]) == "success"
assert len(os.listdir(ts.deck1.media.dir())) == 1
assert ts.server.mediatest("count") == 1

View File

@ -3,13 +3,13 @@
import time
import copy
from tests.shared import getEmptyDeck
from tests.shared import getEmptyCol
from anki.utils import intTime
from anki.hooks import addHook
def test_clock():
d = getEmptyDeck()
d = getEmptyCol()
if (d.sched.dayCutoff - intTime()) < 10*60:
raise Exception("Unit tests will fail around the day rollover.")
@ -18,12 +18,12 @@ def checkRevIvl(d, c, targetIvl):
return min <= c.ivl <= max
def test_basics():
d = getEmptyDeck()
d = getEmptyCol()
d.reset()
assert not d.sched.getCard()
def test_new():
d = getEmptyDeck()
d = getEmptyCol()
d.reset()
assert d.sched.newCount == 0
# add a note
@ -43,29 +43,31 @@ def test_new():
assert c.queue == 1
assert c.type == 1
assert c.due >= t
# the default order should ensure siblings are not seen together, and
# should show all cards
m = d.models.current(); mm = d.models
t = mm.newTemplate("Reverse")
t['qfmt'] = "{{Back}}"
t['afmt'] = "{{Front}}"
mm.addTemplate(m, t)
mm.save(m)
f = d.newNote()
f['Front'] = u"2"; f['Back'] = u"2"
d.addNote(f)
f = d.newNote()
f['Front'] = u"3"; f['Back'] = u"3"
d.addNote(f)
d.reset()
qs = ("2", "3", "2", "3")
for n in range(4):
c = d.sched.getCard()
assert qs[n] in c.q()
d.sched.answerCard(c, 2)
# disabled for now, as the learn fudging makes this randomly fail
# # the default order should ensure siblings are not seen together, and
# # should show all cards
# m = d.models.current(); mm = d.models
# t = mm.newTemplate("Reverse")
# t['qfmt'] = "{{Back}}"
# t['afmt'] = "{{Front}}"
# mm.addTemplate(m, t)
# mm.save(m)
# f = d.newNote()
# f['Front'] = u"2"; f['Back'] = u"2"
# d.addNote(f)
# f = d.newNote()
# f['Front'] = u"3"; f['Back'] = u"3"
# d.addNote(f)
# d.reset()
# qs = ("2", "3", "2", "3")
# for n in range(4):
# c = d.sched.getCard()
# assert qs[n] in c.q()
# d.sched.answerCard(c, 2)
def test_newLimits():
d = getEmptyDeck()
d = getEmptyCol()
# add some notes
g2 = d.decks.id("Default::foo")
for i in range(30):
@ -95,7 +97,7 @@ def test_newLimits():
assert d.sched.newCount == 9
def test_newBoxes():
d = getEmptyDeck()
d = getEmptyCol()
f = d.newNote()
f['Front'] = u"one"
d.addNote(f)
@ -108,7 +110,7 @@ def test_newBoxes():
d.sched.answerCard(c, 2)
def test_learn():
d = getEmptyDeck()
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"two"
@ -176,15 +178,13 @@ def test_learn():
c.queue = 1
c.odue = 321
c.flush()
print "----begin"
d.sched.removeLrn()
c.load()
print c.__dict__
assert c.queue == 2
assert c.due == 321
def test_learn_collapsed():
d = getEmptyDeck()
d = getEmptyCol()
# add 2 notes
f = d.newNote()
f['Front'] = u"1"
@ -210,7 +210,7 @@ def test_learn_collapsed():
assert not c.q().endswith("2")
def test_learn_day():
d = getEmptyDeck()
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = u"one"
@ -268,7 +268,7 @@ def test_learn_day():
assert d.sched.counts() == (0, 0, 0)
def test_reviews():
d = getEmptyDeck()
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"two"
@ -360,7 +360,7 @@ def test_reviews():
assert c.queue == -1
def test_button_spacing():
d = getEmptyDeck()
d = getEmptyCol()
f = d.newNote()
f['Front'] = u"one"
d.addNote(f)
@ -382,7 +382,7 @@ def test_button_spacing():
def test_overdue_lapse():
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2
return
d = getEmptyDeck()
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = u"one"
@ -415,7 +415,7 @@ def test_overdue_lapse():
assert d.sched.counts() == (0, 0, 1)
def test_finished():
d = getEmptyDeck()
d = getEmptyCol()
# nothing due
assert "Congratulations" in d.sched.finishedMsg()
assert "limit" not in d.sched.finishedMsg()
@ -434,7 +434,7 @@ def test_finished():
assert "limit" not in d.sched.finishedMsg()
def test_nextIvl():
d = getEmptyDeck()
d = getEmptyCol()
f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"two"
d.addNote(f)
@ -490,7 +490,7 @@ def test_nextIvl():
assert d.sched.nextIvlStr(c, 4) == "10.8 months"
def test_misc():
d = getEmptyDeck()
d = getEmptyCol()
f = d.newNote()
f['Front'] = u"one"
d.addNote(f)
@ -504,7 +504,7 @@ def test_misc():
assert d.sched.getCard()
def test_suspend():
d = getEmptyDeck()
d = getEmptyCol()
f = d.newNote()
f['Front'] = u"one"
d.addNote(f)
@ -547,7 +547,7 @@ def test_suspend():
assert c.did == 1
def test_cram():
d = getEmptyDeck()
d = getEmptyCol()
f = d.newNote()
f['Front'] = u"one"
d.addNote(f)
@ -655,7 +655,7 @@ def test_cram():
assert c.did == 1
def test_cram_rem():
d = getEmptyDeck()
d = getEmptyCol()
f = d.newNote()
f['Front'] = u"one"
d.addNote(f)
@ -676,7 +676,7 @@ def test_cram_rem():
def test_cram_resched():
# add card
d = getEmptyDeck()
d = getEmptyCol()
f = d.newNote()
f['Front'] = u"one"
d.addNote(f)
@ -780,7 +780,7 @@ def test_cram_resched():
# print c.__dict__
def test_ordcycle():
d = getEmptyDeck()
d = getEmptyCol()
# add two more templates and set second active
m = d.models.current(); mm = d.models
t = mm.newTemplate("Reverse")
@ -804,7 +804,7 @@ def test_ordcycle():
assert d.sched.getCard().ord == 2
def test_counts_idx():
d = getEmptyDeck()
d = getEmptyCol()
f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"two"
d.addNote(f)
@ -826,7 +826,7 @@ def test_counts_idx():
assert d.sched.counts() == (0, 2, 0)
def test_repCounts():
d = getEmptyDeck()
d = getEmptyCol()
f = d.newNote()
f['Front'] = u"one"
d.addNote(f)
@ -878,7 +878,7 @@ def test_repCounts():
assert d.sched.counts() == (0, 1, 0)
def test_timing():
d = getEmptyDeck()
d = getEmptyCol()
# add a few review cards, due today
for i in range(5):
f = d.newNote()
@ -904,7 +904,7 @@ def test_timing():
assert c.queue == 1
def test_collapse():
d = getEmptyDeck()
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = u"one"
@ -918,7 +918,7 @@ def test_collapse():
assert not d.sched.getCard()
def test_deckDue():
d = getEmptyDeck()
d = getEmptyCol()
# add a note with default deck
f = d.newNote()
f['Front'] = u"one"
@ -968,7 +968,7 @@ def test_deckDue():
d.sched.deckDueTree()
def test_deckTree():
d = getEmptyDeck()
d = getEmptyCol()
d.decks.id("new::b::c")
d.decks.id("new2")
# new should not appear twice in tree
@ -977,7 +977,7 @@ def test_deckTree():
assert "new" not in names
def test_deckFlow():
d = getEmptyDeck()
d = getEmptyCol()
# add a note with default deck
f = d.newNote()
f['Front'] = u"one"
@ -1001,7 +1001,7 @@ def test_deckFlow():
d.sched.answerCard(c, 2)
def test_reorder():
d = getEmptyDeck()
d = getEmptyCol()
# add a note with default deck
f = d.newNote()
f['Front'] = u"one"
@ -1039,7 +1039,7 @@ def test_reorder():
assert f4.cards()[0].due == 2
def test_forget():
d = getEmptyDeck()
d = getEmptyCol()
f = d.newNote()
f['Front'] = u"one"
d.addNote(f)
@ -1053,7 +1053,7 @@ def test_forget():
assert d.sched.counts() == (1, 0, 0)
def test_resched():
d = getEmptyDeck()
d = getEmptyCol()
f = d.newNote()
f['Front'] = u"one"
d.addNote(f)
@ -1069,7 +1069,7 @@ def test_resched():
assert c.ivl == +1
def test_norelearn():
d = getEmptyDeck()
d = getEmptyCol()
# add a note
f = d.newNote()
f['Front'] = u"one"
@ -1090,7 +1090,7 @@ def test_norelearn():
d.sched.answerCard(c, 1)
def test_failmult():
d = getEmptyDeck()
d = getEmptyCol()
f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"two"
d.addNote(f)

View File

@ -1,10 +1,10 @@
# coding: utf-8
import os
from tests.shared import getEmptyDeck
from tests.shared import getEmptyCol
def test_stats():
d = getEmptyDeck()
d = getEmptyCol()
f = d.newNote()
f['Front'] = "foo"
d.addNote(f)
@ -18,7 +18,7 @@ def test_stats():
assert d.cardStats(c)
def test_graphs_empty():
d = getEmptyDeck()
d = getEmptyCol()
assert d.stats().report()
def test_graphs():

View File

@ -5,7 +5,7 @@ import nose, os, shutil, time
from anki import Collection as aopen, Collection
from anki.utils import intTime
from anki.sync import Syncer, LocalServer
from tests.shared import getEmptyDeck
from tests.shared import getEmptyCol, getEmptyDeckWith
# Local tests
##########################################################################
@ -18,7 +18,7 @@ server2=None
def setup_basic():
global deck1, deck2, client, server
deck1 = getEmptyDeck()
deck1 = getEmptyCol()
# add a note to deck 1
f = deck1.newNote()
f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = [u"foo"]
@ -26,7 +26,7 @@ def setup_basic():
# answer it
deck1.reset(); deck1.sched.answerCard(deck1.sched.getCard(), 4)
# repeat for deck2
deck2 = getEmptyDeck(server=True)
deck2 = getEmptyDeckWith(server=True)
f = deck2.newNote()
f['Front'] = u"bar"; f['Back'] = u"bar"; f.tags = [u"bar"]
deck2.addNote(f)
@ -247,7 +247,7 @@ def test_threeway2():
anki.notes.intTime = lambda x=1: intTime(1000)
def setup():
# create collection 1 with a single note
c1 = getEmptyDeck()
c1 = getEmptyCol()
f = c1.newNote()
f['Front'] = u"startingpoint"
nid = f.id
@ -325,7 +325,7 @@ def _test_speed():
deck1.tags.tags[tx] = -1
deck1._usn = -1
deck1.save()
deck2 = getEmptyDeck(server=True)
deck2 = getEmptyDeckWith(server=True)
deck1.scm = deck2.scm = 0
server = LocalServer(deck2)
client = Syncer(deck1, server)

View File

@ -1,11 +1,11 @@
# coding: utf-8
import time
from tests.shared import getEmptyDeck
from tests.shared import getEmptyCol
from anki.consts import *
def test_op():
d = getEmptyDeck()
d = getEmptyCol()
# should have no undo by default
assert not d.undoName()
# let's adjust a study option
@ -36,7 +36,7 @@ def test_op():
assert d.undoName() == "Review"
def test_review():
d = getEmptyDeck()
d = getEmptyCol()
d.conf['counts'] = COUNT_REMAINING
f = d.newNote()
f['Front'] = u"one"

View File

@ -1,6 +1,6 @@
# coding: utf-8
import datetime, shutil
import datetime, shutil, tempfile
from anki import Collection
from anki.consts import *
from shared import getUpgradeDeckPath, testDir
@ -63,8 +63,9 @@ def test_invalid_ords():
assert deck.db.scalar("select count() from cards where ord = 1") == 1
def test_upgrade2():
p = "/tmp/alpha-upgrade.anki2"
fd, p = tempfile.mkstemp(suffix=".anki2", prefix="alpha-upgrade")
if os.path.exists(p):
os.close(fd)
os.unlink(p)
shutil.copy2(os.path.join(testDir, "support/anki2-alpha.anki2"), p)
col = Collection(p)

11
thirdparty/pyaudio.py vendored
View File

@ -92,16 +92,7 @@ __author__ = "Hubert Pham"
__version__ = "0.2.4"
__docformat__ = "restructuredtext en"
import sys
# attempt to import PortAudio
try:
import _portaudio as pa
except ImportError:
print "Please build and install the PortAudio Python " +\
"bindings first."
sys.exit(-1)
import _portaudio as pa
# Try to use Python 2.4's built in `set'
try:

View File

@ -29,7 +29,7 @@ do
echo " * "$py
pyuic4 $i -o $py
# munge the output to use gettext
perl -pi.bak -e 's/QtGui.QApplication.translate\(".*?", /_(/; s/, None, QtGui.*/))/' $py
perl -pi.bak -e 's/(QtGui\.QApplication\.)?_?translate\(".*?", /_(/; s/, None.*/))/' $py
rm $py.bak
fi
done